feat(spec)!: RuntimeConfig resourceLimits.timeout carries its unit in the key name (#17781) - #17983
Conversation
Co-authored-by: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015c5G6TmpMKgnusmTpD7Ntt
… the key name Rename `RuntimeConfig.resourceLimits.timeout` to `timeoutMs` on `kernel/plugin-security-advanced.zod.ts`, the fifth duration on this file and the one #15678 deliberately left alone because its unit lived in a source JSDoc that `check:duration-unit-keys` does not read. - `retiredKey()` tombstone on the old spelling (the nested `resourceLimits` object is not strict, so a bare deletion would silently strip the key) - ADR-0087 D3 semantic entry `kernel-runtime-config-timeout-unit-in-key` and the `RETIRED_KEYS_BY_MAJOR[18]` row, registry regenerated by `gen:migration-registry` - the pin test that asserted the key stays bare is replaced, not removed - reference page regenerated by `gen:docs` - `minor` changeset with the FROM to TO mapping and the ADR-0087 disposition Co-authored-by: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015c5G6TmpMKgnusmTpD7Ntt
…ugin-security-timeout-unit
📓 Docs Drift Check4 anchor(s) derived from 1 changed package(s); no hand-written page names any of them. What this run could not see
Coarse fallback — 136 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): Which tree this was computed onThis run read A worktree cut from an older # while this PR is open — GitHub drops the merge commit once it closes
git fetch origin 711f9dff6f42da6de8f034dd02a6cdd1f16ae19a && git checkout 711f9dff6f42da6de8f034dd02a6cdd1f16ae19a
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 00c332b4d341f49c9810136f65a92d317f7586b0 65dae07b9053953d606db247d1b6f70dffd2309c && git checkout -B drift-repro 00c332b4d341f49c9810136f65a92d317f7586b0 && git merge --no-ff 65dae07b9053953d606db247d1b6f70dffd2309c
node scripts/docs-audit/affected-docs.mjs --json 00c332b4d341f49c9810136f65a92d317f7586b0 |
…ecognises `check:objectui-pin-citations` accepts exactly two spellings and a third is a hard red. The semantic entry's citation broke across a string concatenation, so the sha landed on a source line the mention could not reach — unrecognised, and therefore outside every check. The reading was taken against the sha read out of this tree's `.objectui-sha`, i.e. the pin we build against, so the asserting `=` form is the true one. Co-authored-by: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015c5G6TmpMKgnusmTpD7Ntt
Contract reviewHead sha reviewed: Every tree reading was taken in a fresh detached worktree at that sha ( ① derived judgments — accept set and public surface
⭐ The pin-test replacement (the item this review was pointed at): run against the pre-change schema, not by deleting the tombstone. ② semver grading —
|
…t in the key name (objectstack-ai#17783) (objectstack-ai#18007) Fixes objectstack-ai#17783 `Clause-②: yes` Executes director-seat **ruling A on objectstack-ai#15939** (2026-09-11, maintainer 「同意」, decision batch objectstack-ai#115) for `packages/spec/src/system/metrics.zod.ts` — the sixth of the seven per-file remediation cards. PR objectstack-ai#17635 (the gate) lands last, into a tree these cards have cleaned. ## The five rows, re-located by symbol path Line numbers from objectstack-ai#17635's enumeration rot; each row was re-located by symbol and its JSDoc read. | symbol path | JSDoc said | describe said | after | ratchet | |:--|:--|:--|:--|:--| | `MetricDefinition.summary.maxAge` | "Max age of observations in seconds" | *(none)* | `maxAgeSeconds` | nested — moves nothing | | `ServiceLevelObjective.errorBudget.burnRateWindows[].window` | "Window size in seconds" | "Window size" | `durationSeconds` | nested, array element — moves nothing | | `MetricExportConfig.interval` | "Export interval in seconds" | *(none)* | `intervalSeconds` | **top level — moves** | | `MetricsConfig.collectionInterval` | "Collection interval in seconds" | *(none)* | `collectionIntervalSeconds` | **top level — moves** | | `MetricsConfig.retention.period` | "Retention period in seconds" | *(none)* | `durationSeconds` | nested — moves nothing | Every value is seconds as before; every default (600, 60, 15, 604800) is unchanged. Four of the five carried **no `.describe()` at all**, so the reference page published a bare integer. ## Decoys — measured, and none of them moved Measured at `fc28c1d38`, occurrences via `grep -o | wc -l` (`grep -c` counts lines): | key | key-position declarations | of which decoys | |:--|--:|:--| | `window` | 3 | `:346` `MetricAggregationConfig.window` and `:442` `ServiceLevelIndicator.window`, both `z.object({` | | `period` | 2 | `:509` `ServiceLevelObjective.period`, a `z.object({` | | `maxAge` · `interval` · `collectionInterval` | 1 each | — | Lit control on that file `z.number` = 38; dark control (fabricated token) = 0. All three decoys are objects that already hold a `durationSeconds` of their own from objectstack-ai#15679, and a pin in this PR asserts none of them moved. ## Suffix: `Seconds`, and why three of the five are not the mechanical name Counted in key position across `packages/spec/src/**/*.zod.ts` at `fc28c1d38`: **`Seconds` 40 · `Sec` 1 · `S` 0**. The single `Sec` is `maxExecutionTimeSec`; the two bare `*S:` hits on the corpus are `maxCommitTimeMS` (a millisecond spelling) and `enableRLS` (a boolean), so as a *seconds* suffix `S` reads 0. At this PR's head `Seconds` is 45 — the five added here. This file already has a **stated, reasoned naming convention** from objectstack-ai#15679, recorded in its two tombstone entries, and counting alone cannot see it: - **`burnRateWindows[].window` → `durationSeconds`**, not `windowSeconds`. It is the *fourth* window length on this file; objectstack-ai#15679 renamed the other three to `durationSeconds` precisely "so the three measurements now read alike", and rejected `windowSeconds` because the parent key was already `window`. The same stutter applies here against the enclosing `burnRateWindows` array. Independently: on this tree `windowSeconds` is **not an authorable key at all** — its only key-position occurrence is `system/stack-server.zod.ts:88`, an entry in `ServerRateLimitConfigSchema`'s `aliases` map that maps the spelling **away** to `windowMs`. - **`retention.period` → `durationSeconds`**, not `periodSeconds`. `period` is calendar vocabulary elsewhere in this spec (`ServiceLevelObjective.period.type` selects rolling or calendar; `PluginRegistryEntry.pricing.billingPeriod` is monthly or yearly), so `periodSeconds` keeps the ambiguous half of the name — the objection objectstack-ai#15679 raised against `sizeSeconds`. `periodSeconds` is attested 0 times on this tree; `durationSeconds` 4. - **`collectionInterval` → `collectionIntervalSeconds`**, keeping the qualifier, because `MetricExportConfig.intervalSeconds` is a different cadence one def over that this same PR creates. The qualifier-plus-`IntervalSeconds` compound is attested: `syncIntervalSeconds`, `refreshIntervalSeconds`, `healthCheckIntervalSeconds`. The two mechanical spellings are attested rather than invented: - **`maxAgeSeconds`** — `AccessControlConfig.maxAgeSeconds` on `system/object-storage.zod.ts` is the landed rename of a `maxAge` under this same rule, tombstone and all. It also keeps the `age` stem that the sibling key `ageBuckets` counts buckets of; `durationSeconds` would have orphaned that pair. - **`intervalSeconds`** — 4 key-position occurrences, every one a seconds-valued cadence. ## The pin this card was written to trip `metrics.test.ts` carried `it('leaves the two non-duration keys on this file alone')`, holding the burn-rate `window` bare because it is "outside the gate population entirely". **This card is the sweep that guard was written to catch, so it succeeds by failing.** It bundled two subjects; the exporter batch `size` half is a COUNT of records, still true, and survives untouched under a header narrowed to name only it. The `window` half is replaced by a refusal pin, an acceptance pin at the same 3600, and a describe pin, in a block whose own header stays true. ⛔ Nothing deleted, skipped or loosened.⚠️ Note for whoever rebases **PR objectstack-ai#17635**: its diff patches exactly those comment lines (`- outside the gate population entirely` → `+ inside the gate's census and outside its verdict`), and its own text says the pin "must be re-read, not trusted, when the rename lands". That hunk has no subject after this PR; expect a conflict there and drop that half of its repair. ## Kit - `retiredKey()` tombstone per key (⛔ none of the five enclosing shapes is `.strict()` — measured: 0 `.strict()` on this file — so a bare deletion would silently strip). Every tombstone const is declared **above** the schema that reads it: `gen:schema` and `check:authorable-surface` run with `OS_EAGER_SCHEMAS=1`, which makes `lazySchema` evaluate at module load, and a const below its reader is a TDZ read (the trap PR objectstack-ai#17983 hit). - One ADR-0087 D3 semantic entry `system-metrics-jsdoc-durations-unit-in-key` + five `RETIRED_KEYS_BY_MAJOR[18]` rows. It opens by stating how it relates to objectstack-ai#15679's `system-metrics-window-durations-unit-in-key` rather than rewriting it. - **objectstack-ai#15679's entry gains a pointer clause** (contract review, `bcfdd35609`). Its `acceptanceCriteria` ended "Both keep their names." — and that field's contract is *how the consumer proves the hand-migration correct*, rendering as "Done when:" in the guide and "verify:" in `migrate meta`, so a normative sentence saying the burn-rate window keeps its name instructs a future reviewer that this correct sweep was an error. Every existing word is left in place and a clause is appended naming the successor (Prime Directive objectstack-ai#13 pointer, not a rewrite). This is not amending published history: the protocol-18 step is unreleased and still assembling — `PROTOCOL_VERSION` is `17.0.0`, `docs/protocol-upgrade-guide.md` ends at "Protocol 16 → 17", and **both** entry ids render 0 times in the guide, re-measured on this head. - No D2 conversion: `stack.zod.ts` declares no metrics collection and none of these defs is a stored `sys_metadata` row — the reading objectstack-ai#15679 already recorded for this file. - `minor` changeset with the `**BREAKING**` banner, FROM → TO for all five, and `adr-0087: registered`. ## Verification | reading | exit | note | |:--|--:|:--| | `pnpm --filter @objectstack/spec build` | 0 | via `os-verify-lock.sh`, `VERDICT command-exit 0` (latest on head `84e62ed712`) | | `pnpm --filter @objectstack/spec test` | 0 | 476 files, 13536 tests | | `pnpm --filter @objectstack/spec typecheck` | 0 | | | `check:generated` | 0 | 15/15 artifacts current, re-run on head `84e62ed712` after the pointer commit | | `dispatch-gates` derived families | 109/109 exit 0 | derived and run on `bcfdd35609`; the only change since is the objectstack-ai#15679 pointer clause and its `registry.ts` mirror | | consumers: `core` · `verify` · `cli` · `rest` typecheck | 0 each | on a **built** closure | | consumers: `observability` · `downstream-contract` · `http-conformance` test | 0 each | | | pinned objectui at `.objectui-sha` | 0 hits | all six def names + both distinctive keys, 6409 tracked files; lit `window` 2710 · `timeout` 832 · `period` 160 · `interval` 156 · `metrics` 301; dark 0 ⇒ **no pin bump owed** | **Consumers.** Outside `packages/spec`, every occurrence of every distinctive key on these shapes (`burnRateWindows`, `errorBudget`, `downsampling`, `collectionInterval`, `cardinalityLimits`, `maxLabelCombinations`, `ageBuckets`) is in the generated `content/docs/references/system/metrics.mdx`, which this PR regenerates — lit control `defineStack` 1195 on the same corpus, dark 0. Zero in-repo code consumers, confirming the dispatch's own measurement. **Ablation** (both hardest pins, `EXIT INT TERM` trap, byte-identity proven): | leg | anchor before → after | mutated blob ≠ HEAD | tests | restore | |:--|:--|:--|:--|:--| | delete `window: retiredKey(SLO_BURN_RATE_WINDOW_RETIRED),` | 1 → 0 | `8768749…` ≠ `a747965…` | **exit 1**, 1 failed / 45 passed | blob back to `a747965…`, `git diff HEAD` empty, `git status --porcelain` empty | | delete `period: retiredKey(RETENTION_PERIOD_RETIRED),` | 1 → 0 | `8a84628…` ≠ `a747965…` | **exit 1**, 1 failed / 45 passed | same | | control (unmutated) | — | — | **exit 0**, 46 passed | — | **The two legs fail differently, and prove different things** — corrected here after the contract review caught the body claiming they proved the same one: - **Leg 1** reads `AssertionError: expected undefined to be defined` at `metrics.test.ts:622`. `result.success` was still `false`, but no issue landed at `errorBudget.burnRateWindows.0.window`: the sibling `durationSeconds` on that array element is **required**, so with the tombstone gone the parse is refused anyway for a missing required key. What leg 1 proves is the **lost prescription** — the author gets a bare "required" refusal instead of the FROM → TO rename message the tombstone carries. ⛔ It is not a silent-strip demonstration. - **Leg 2** reads `AssertionError: expected true to be false` at `metrics.test.ts:659` — `result.success` was `true`. `retention.durationSeconds` is `.optional().default(604800)`, so with the tombstone gone the unknown `period` key is accepted and stripped and the parse succeeds. **This is the ADR-0049 silent strip**, live. Both legs are valid evidence that their pin can fail, which is what an ablation is for. Leg 1 also answers Zone 2.4's first question by test rather than assumption: `retiredKey()` on an **array-element** object refuses exactly as it does on a plain nested object, at path `errorBudget.burnRateWindows.0.window`. **Second-lander merge.** `bash scripts/pm/os-regen-merge.sh` (⛔ never a hand-merge) after objectstack-ai#17999 landed. Step 2 took main's side of both shards; the regeneration commit rebuilt them from the merged source. Set-difference over the whole registry across the merge: retired-key rows **175 → 182, 0 lost, 7 gained**; semantic ids **210 → 213, 0 lost, 3 gained**. Those totals are `gen:migration-registry`'s own printed line, not a hand-rolled census — an earlier revision of this body carried 177 → 184 and 209 → 212 from a regex over `registry.ts`, which miscounts; the review could not reproduce them and was right. The **deltas** were identical under both methods, and are corroborated a third way by file count: `entries/retired-keys` holds 177 files on `main` @ `8261ff7171` + this card's 5 = 182, and `entries/semantic` 212 + 1 = 213 — so nothing was dropped. objectstack-ai#17782's four logging rows and its `logging-durations-unit-in-key` id re-assert at 4 / 2 / 2 / 4 / 10 — matching the pre-merge baseline exactly — with lit control `kernel-runtime-config-timeout-unit-in-key` 4 and dark control 0. **Array path notation** — `system/ServiceLevelObjective:errorBudget.burnRateWindows.window` uses plain dots with no bracket token. Settled by the contract review, which closed the evidence gap I had declared: the two `change-management.zod.ts` precedents do cross an array element with plain dots (reachable at `bf1054a4c0` despite the shallow clone), and there is a **live** precedent I had missed — `kernel/Manifest:contributes.kinds.globs`, where `kinds: z.array(strictObject({` contains `globs: retiredKey(`. ## Acceptance notes - **`check-widening-tells` T1 on `retiredKey()` lines** — the known inverted false positive objectstack-ai#17955. Not reshaped, not weakened. In this run the whole derived family exited 0. - **`resolution` is a sixth JSDoc-only duration on this file, and no gate will ever say so.** `MetricsConfig.retention.downsampling[].resolution` — JSDoc "Resolution in seconds", describe "Downsampled resolution" — is outside `check:duration-unit-keys` on the **name** axis (`resolution` is not in `DURATION_SHAPED_TOKENS`), so unlike objectstack-ai#15939's prose axis, PR objectstack-ai#17635's widening does not reach it. Not folded in here: triage certified this batch as exactly 21 rows with "⛔ 无第八张". Filed as objectstack-ai#18030. ⛔ Draft. Not ready, not enqueued, no auto-merge. The in-seat clause-② contract review is owed first and landing is the PM's step. --- _Generated by [Claude Code](https://claude.ai/code)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
…ir unit in the key name (objectstack-ai#17785) (objectstack-ai#18016) Fixes objectstack-ai#17785 `Clause-②: yes` — each rename puts a spelling on `OpenTelemetryCompatibility` and `TracingConfig` that no author could write before. `minor`, `feat(spec)!`, with an `adr-0087: registered` disposition (epic settlement on objectstack-ai#15939, correcting Ruling A's own `patch`). Executes director-seat **ruling A on objectstack-ai#15939** (2026-09-11, maintainer 「同意」, decision batch objectstack-ai#115), the per-file remediation of the objectstack-ai#14478 rule — the **seventh and last** of that ruling's seven cards. ## The four rows Re-located by symbol path on `origin/main` @ `98bd7986fe`; line numbers came from PR objectstack-ai#17635's enumeration and are re-derived here, not trusted. | symbol path | before | after | default | |:--|:--|:--|--:| | `OpenTelemetryCompatibility.exporter` | `timeout` | `timeoutMs` | 10000 | | `OpenTelemetryCompatibility.exporter.batch` | `exportTimeout` | `exportTimeoutMs` | 30000 | | `OpenTelemetryCompatibility.exporter.batch` | `scheduledDelay` | `scheduledDelayMs` | 5000 | | `TracingConfig.performance` | `exportInterval` | `exportIntervalMs` | 5000 | Each declared exactly once in key position, lit control `z.number` 20 on the file, dark control 0. **All four carried no `.describe()` at all** — not "a describe that names no unit", but none — so the published reference row was a bare integer with no unit anywhere on the page. Values, defaults and `int().positive()` bounds are unchanged. ## The suffix is `Ms`, settled from tree counts Key position across `packages/spec/src`: **281** `*Ms` declarations over 42 distinct names — `timeoutMs` 65, `intervalMs` 14, `durationMs` 73 — against **0** key-position `timeoutSeconds` and 77 `*Seconds` of any name. `exportTimeoutMs` / `scheduledDelayMs` / `exportIntervalMs` have 0 competing spellings of any form. The Delay-plus-`Ms` pairing is already attested (`maxDelayMs`, `initialDelayMs`, `retryDelayMs`, `delayMs`, `debounceDelayMs`). This file's own landed precedent is `Span.duration` to `durationMs` (objectstack-ai#15679) — **the opposite of the sibling metrics card, whose rows were seconds**; every JSDoc here was read individually and every one says milliseconds. **The two `*TimeoutMs` keys one nesting level apart — accepted, and why.** The near-duplicate pre-exists the rename: the `batch` sub-object is the OpenTelemetry batch span processor's own four knobs (max batch size, max queue size, scheduled delay, export timeout) sitting beside the exporter's own request deadline. Renaming either to something more distinctive would depart from the vocabulary this shape mirrors, and the nesting already disambiguates every read point — `exporter.timeoutMs` versus `exporter.batch.exportTimeoutMs`, never interchangeable because they live in different objects. Rejected alternative: `requestTimeoutMs` for the outer one, which would have invented a spelling OTel does not use to solve a problem the nesting already solves. ## The ratchet moves nothing — and that is the correct outcome, not an omission Verified from the generator source, not assumed. `packages/spec/scripts/build-schemas.ts:852` reads `schema.properties` **one level deep** and records `${defKey}:${name}`, so only top-level keys per def ever enter `authorable-surface/` or `authorable-defaults/`. Corroborated on the committed artifacts: `system/OpenTelemetryCompatibility:exporter` is a single row (the whole nested object), there are **0** dotted keys anywhere in `authorable-surface/system.json` and **0** in `authorable-defaults/system.json` against 247 total keys there, and the lit control `system/Span:duration [RETIRED]` is present. All four of my rows are nested. `check:authorable-surface` is **green without any regeneration**, and `git diff --name-only` against the base matches 0 files under `authorable-surface` or `authorable-defaults`. This is the objectstack-ai#17954 reading, not objectstack-ai#17986's. ## The pin guard this card was written to trip `packages/spec/src/system/tracing.test.ts:545` — `it('leaves the OTel exporter timeout alone — its describe names no unit, so it is outside the population')`. **It succeeds by failing**: its key, its "names no unit" clause and its "outside the population" clause all go false here. It is neither deleted, skipped, weakened nor fixed green — it is **replaced and relocated** out of a `describe` headed `Span.duration carries its unit (objectstack-ai#15679)`, which is no longer its subject, into a new `objectstack-ai#17785` block carrying the three-part shape objectstack-ai#15679 itself set on this file: a refusal pin per row asserting the issue **code** (`not.toBe('unrecognized_keys')`) and the FROM to TO prescription, an acceptance pin at each retired key's magnitude with the same default, a bounds pin, and a describe pin proving the unit now reaches the published channel. ## Stale-prose sweep (`file:line`) Swept `packages/spec/src`, `packages/spec/scripts` and `content/docs` for sentences this card falsifies (`outside the population` · `names no unit` · `no unit at all` · `left alone` · `leaves ... alone` · `untouched here` · `stays bare`), then narrowed to hits whose SUBJECT is one of my four keys. Lit control `unit` 2308 on that corpus, dark control 0. | `file:line` | disposition | |:--|:--| | `packages/spec/src/system/tracing.test.ts:545` | this card's — replaced and relocated, above | | `packages/spec/CHANGELOG.md:2866` | **left alone, reported.** "`ServiceLevelObjective.errorBudget`'s burn-rate `window` and the OpenTelemetry exporter `timeout` name no unit anywhere in their prose, so both are outside the gate's population entirely." Two limbs go false — mine here, the `window` one at objectstack-ai#17783. `packages/*/CHANGELOG.md` is RELEASE-OWNED (AGENTS.md): never edited in a code PR; a factual error in a released entry is amended in a dedicated docs-only PR. It is also jointly owned by a sibling card, so it is not this PR's sentence to rewrite. Raised in the report. | | `packages/spec/src/migrations/entries/semantic/18.system-tracing-span-duration-unit-in-key.ts` | **left alone, deliberately.** Its "it is the only offender on its file" is scoped to ruling B's describe-channel population, which never contained these four. Per the epic settlement, a predecessor's semantic entry is never amended; this round's entry instead opens by saying how it relates to it. | ## Consumers **Zero in-repo readers outside `packages/spec`.** `OpenTelemetryCompatibility`, `TracingConfig`, `exportTimeout`, `scheduledDelay` and `exportInterval` each occur **0** times across the whole tree at `98bd7986fe` excluding `packages/spec` **and** `content/docs/references`, against a lit control of **18920** `Schema` occurrences on exactly that corpus — both counts from one `git grep -o` over that ref carrying both pathspec exclusions — and a dark control of 0. The named consumer packages were each run anyway rather than reasoned about — including the package being edited, the objectstack-ai#17986 lesson. **Pinned objectui**, `.objectui-sha` = `53ded82bf7a494f54e344e19099dbf00854b8694`, 6409 tracked files: all **37** exports of `tracing.zod.ts` and each of the four key names occur **0** times. The 404 `Span` and 40 `SpanSchema` raw hits are objectui's own HTML text-span component (`TextSpanSchema`) — a different name, read rather than counted. Lit controls `objectstack` 10171 and `@objectstack/spec` 3479 on the same corpus; dark control 0. **Rollback** is therefore consumer-free: reverting this PR restores the four bare spellings and drops four tombstones no caller in this repo, and none in the pinned sibling, reads. ## The kit - `retiredKey()` tombstone per row — none of these shapes is `.strict()`, so `unrecognized_keys` was never the alternative: a bare deletion is an ADR-0049 silent strip landing a default on an exporter deadline and a background export period - ADR-0087 D3 semantic entry `system-tracing-otel-exporter-durations-unit-in-key` **plus** four `RETIRED_KEYS_BY_MAJOR[18]` rows (the epic settlement: both, every time) - `registry.ts` regenerated with `gen:migration-registry`, never hand-merged - `content/docs/references/system/tracing.mdx` regenerated with `gen:docs` - `minor` changeset with the **BREAKING** banner, the FROM to TO mapping for all four and the `adr-0087: registered` disposition ## Acceptance notes - The prescriptions carry **no** `os migrate meta` sentence, matching objectstack-ai#17784 and objectstack-ai#17780: the sentence belongs to a surface an ADR-0087 **conversion** covers, and these are D3 semantic entries with no D2 conversion (`stack.zod.ts` declares no tracing collection; a tracing config is never a stored `sys_metadata` row). The class pin `retired-key-migrate-sentence.test.ts` deliberately does not judge a prescription that names no command. - Tombstone prescriptions are written **inline** rather than extracted to a `const`, which is the objectstack-ai#17954 shape on a nested key and sidesteps the `OS_EAGER_SCHEMAS=1` temporal-dead-zone trap objectstack-ai#17983 hit by construction — there is no const to order. - `acceptRetiredDefaultResidue()` was considered and **not** used: all four keys are defaulted, but every landed sibling of this epic tombstoned a defaulted key with plain `retiredKey()`. Noted, not filed — raised as an open question rather than decided here. - `content/docs/references/system/tracing.mdx` gains a `TracingConfig.performance` nested-shape section it did not have; the `exporter.batch` describes do not render a row because the generator's nested-shape rendering stops one level shallower. Generator behaviour, unchanged by this PR. Noted, not filed. Authored by the `os-dev` seat for the objectstack-ai#15939 epic PM, session `session_015c5G6TmpMKgnusmTpD7Ntt` (https://claude.ai/code/session_015c5G6TmpMKgnusmTpD7Ntt). --- _Generated by [Claude Code](https://claude.ai/code)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
Fixes #17781
Clause-②: yes
Executes director-seat ruling A on #15939 (2026-09-11, maintainer 「同意」, decision batch #115) — the per-file remediation of the #14478 duration-unit rule. This card owns exactly one row, in
packages/spec/src/kernel/plugin-security-advanced.zod.ts. The gate PR (#17635) is sequenced to land LAST and is untouched here.yes, not thenoruling A wrote, and the changeset isminor, notpatch: both were corrected by the epic PM before dispatch and recorded publicly on #17784 (5652094166) and #15939 (5652120294) — a rename puts a spelling on a published payload no author could write before, whichreferences/contract-review.md's mechanical floor reads as a mandatory affirmative, and the #15678 / #15679 shape ruling A names shipped asfeat(spec)!at minor in every one of its four landed siblings.needs:contract-reviewis hung on the card and on this PR;check-changeset-no-majorreads that label as a clause-② affirmative carrier that overrides the body line, so the body line above reads the affirmative too.What changed
RuntimeConfig.resourceLimits.timeoutis the key the whole of #15939 was filed about, and the one #15678 deliberately left alone. It named its unit in a source JSDoc — "Execution timeout in milliseconds" — and nowhere else. The.describe()thatcontent/docs/references/kernel/plugin-security-advanced.mdxrenders read "Maximum execution time" and named no unit at all, so the reader of the published reference page could not tell 60000 milliseconds from 60000 seconds.check:duration-unit-keysreads.describe()and.meta({ description }), not JSDoc, so it listed the key in its census without judging it — neither an offender nor an exemption. That gap is #15939, and this is its remediation.resourceLimits.timeout: 60000resourceLimits.timeoutMs: 60000Maximum execution timeMaximum execution time in millisecondsint().min(0)The spelling is
Ms, derived from how the suffixed family already spells itself on this tree rather than from the dispatch order: 29 key-positiontimeoutMsdeclarations acrosspackages/spec/src/**/*.zod.ts, 40 distinct*Mskeys, and zerotimeoutMillis/timeout_ms/timeoutMSvariants anywhere inpackages/spec/src.SandboxConfig.process.timeoutMs, renamed by #15678 on this same file, already carries the token.The kit, following the #15678 / #15679 shape the ruling names:
retiredKey()tombstone on the old spelling —tsctypes itneverand a value reaching the parse raises the rename prescription instead of being silently stripped (the nestedresourceLimitsobject is not.strict())kernel-runtime-config-timeout-unit-in-keyand theRETIRED_KEYS_BY_MAJOR[18]rowkernel/RuntimeConfig:resourceLimits.timeout, both asmigrations/entries/files withregistry.tsregenerated bygen:migration-registry(never hand-merged)RuntimeConfigis the engine block of theSandboxConfiga host or a plugin security manifest constructs,stack.zod.tsdeclares no sandbox, security-policy or runtime-config collection, and it is not a storedsys_metadatarow — so the chain has no seam that runs on it. That is the reading [#14478 stack 3/6]kernel/: the 14 remaining duration keys carry their unit in the key name — ADR-0087 conversions with readers (runtime-emitted measurements included) #15678 recorded for the four keys it renamed.content/docs/references/kernel/plugin-security-advanced.mdxregenerated bygen:docs— three rows move and the tombstone prescription renders in place of the old describeminorchangeset carrying the FROM to TO mapping and theadr-0087: registereddispositionOne implementation detail worth a reviewer's eye: the tombstone string const is declared above
RuntimeConfigSchema, not with the other four RETIRED consts below it.gen:schemaandcheck:authorable-surfaceboth run withOS_EAGER_SCHEMAS=1, which makeslazySchemaevaluate its factory at module load — a const declared after the schema would be read from its temporal dead zone. The four existing consts are each already declared before the block that reads them; this one had to move to keep that true.The pin test this card was warned about — replaced, not silenced
plugin-security-advanced.test.tscarried a NEGATIVE control asserting this exact key stays bare, with a comment stating why it exists: "Without this test, a later sweep reads the four renames above as 'every timeout on this file'." This card is that later sweep, so the guard succeeded by failing.RuntimeConfigSchema.parse({ resourceLimits: { timeout: 60000 } })succeeds and returns60000timeoutMsparses at the same magnitude beside its siblings; the describe publishes the unit; the two same-namedtimeoutretirements on this file name their own shapes apartdescribe('Plugin security durations carry their unit (#15678)')retiredKey()tombstones" — stays exactly true about #15678⛔ It was not deleted, not weakened, not
.skipped, and not "fixed green" by loosening an assertion. The refusal assertions pin the ADR-0112-shaped facts a tombstone owes — the issue is notunrecognized_keys, and the message carries the FROM to TO mapping — never a baretoThrow().⛔ #15678's semantic entry was NOT amended. Its "One key deliberately left alone: RuntimeConfig.resourceLimits.timeout … it is outside this rename" is a scoped, past-tense statement about what #15678 did, it stays true, and it is published through the upgrade guide. Instead, this card's own semantic entry opens by stating that it completes what #15678 deliberately left alone, citing #15678 and #15939, so the two read as a sequence rather than a contradiction.
Verification
Heavy runs through
scripts/pm/os-verify-lock.sh(slotissue-17781), verdicts read from its ownVERDICT command-exitline; every gate's exit code captured before any pipe (cmd > log 2>&1; ex=$?).pnpm --filter '@objectstack/spec^...' build— empty closure (No projects matched):packages/spechas no workspace dependencies, so step ① is a documented no-oppnpm --filter @objectstack/spec build—VERDICT command-exit 0, re-run after the last source edit and after themainmergepnpm --filter @objectstack/spec testandpnpm --filter @objectstack/spec typecheck— see the round report for the countspnpm --filter @objectstack/spec check:generated— all 15 generated artifacts up to date aftergen:migration-registry+gen:docs, re-run clean after themainmerge.check:authorable-surfaceis green without regeneration and that is correct: the ratchet records top-level keys per def, andkernel/RuntimeConfig:carries exactlyengine,engineConfigandresourceLimits— this key is nested one level below, so no row moves.check:duration-unit-keys(the gate as it stands onmain, not feat(spec): refuse a duration key whose JSDoc names a unit its describe does not #17635's) — exit 0mainmerged withbash scripts/pm/os-regen-merge.sh, never by hand; the registry was regenerated, and the sibling entries were asserted present by exact-name grep afterwards with a dark control at 0Reverse verification of the new pin
The refusal assertions were proven able to fail, from the committed state: the tombstone line was deleted, the deletion proven on disk (anchor
grep -o | wc -l1 → 0, blob hash160ab186→50abe71c), and the suite re-run — 2 tests red, exactly the refusal pair, with the acceptance and describe tests still green because the ablation removed only the tombstone. Restored withgit checkout HEAD -- PATHunder anEXIT INT TERMtrap and proven byte-identical (git hash-objectback to160ab186,git diff HEADempty,git status --porcelainempty); the suite re-run green. No ablation artefact is left in the tree. There is nodistleg: the test imports the schema by relative path fromsrc, so the built package is not on the resolution path for this ablation.The cross-repo reading, with a lit control
git grepof the pinned objectui checkout at.objectui-sha=53ded82bf7a494f54e344e19099dbf00854b8694(re-read from this tree; the commit is reachable in the local clone, 6409 tracked files):resourceLimits.timeoutresourceLimitsRuntimeConfigtimeoutsandboxBoth
resourceLimitshits are prose inpackages/app-shellrecording that objectui's ownAppShellRuntimeConfig"share not one key" with the spec'sRuntimeConfig, and both name only the top-level keys — they stay accurate after a rename one level below. ⇒ nothing in the pinned sibling authors this key, no objectui-side fix and no pin bump are owed (AGENTS.md Post-Task Checklist step 4).Acceptance notes
RuntimeConfig.resourceLimits.maxMemoryon this same nested object names its unit only in the JSDoc above it ("Maximum memory in bytes") while its describe reads "Maximum memory allocation" — the same channel gap this card closes fortimeout. It is a byte count, not a duration, so it is outside spec: duration-shaped number keys carry their unit in describe prose only — twottlkeys with different units in one block, baretimeoutkeys, unit-less tenant timeouts #14478's population and outside every card in the finding: check:duration-unit-keys reads .describe() but not JSDoc — a duration key documenting its unit only in JSDoc never enters the population, and one card already recorded a wrong reason because of it #15939 epic;check:duration-unit-keysneither judges nor lists it. Recorded rather than filed because no ruling covers a unit-in-name rule for non-duration quantities and inventing one is scope. Successor: whoever proposes widening spec: duration-shaped number keys carry their unit in describe prose only — twottlkeys with different units in one block, baretimeoutkeys, unit-less tenant timeouts #14478 beyond durations.packages/spec/scripts/check-duration-unit-keys.tsand its self-test (PR feat(spec): refuse a duration key whose JSDoc names a unit its describe does not #17635, sequenced last, red by construction — designed, not a defect), [#14478 stack 3/6]kernel/: the 14 remaining duration keys carry their unit in the key name — ADR-0087 conversions with readers (runtime-emitted measurements included) #15678's semantic entry, the five sibling files owned by spec: 3 duration key(s) inkernel/plugin-lifecycle-advanced.zod.tsname their unit only in JSDoc — #15939 Ruling A remediation (3 of the 21-row delta) #17780 spec: 4 duration key(s) insystem/logging.zod.tsname their unit only in JSDoc — #15939 Ruling A remediation (4 of the 21-row delta) #17782 spec: 5 duration key(s) insystem/metrics.zod.tsname their unit only in JSDoc — #15939 Ruling A remediation (5 of the 21-row delta) #17783 spec: 1 duration key(s) insystem/tenant.zod.tsname their unit only in JSDoc — #15939 Ruling A remediation (1 of the 21-row delta) #17784 spec: 4 duration key(s) insystem/tracing.zod.tsname their unit only in JSDoc — #15939 Ruling A remediation (4 of the 21-row delta) #17785, andcontent/docs/releases/.⛔ Draft on purpose: the in-seat clause-② contract review at
CONTRACT_REVIEW_TIERthat this PR declares is owed before it may turn ready or enqueue. That is the PM's step, not this round's.Round report, with every reading above and its exit code: the
os-dev-reportcomment on #17781.Authored in Claude Code session
session_015c5G6TmpMKgnusmTpD7Ntt(mode:cloud), dispatched by the epic PM for #15939.Generated by Claude Code