Skip to content

feat(spec)!: RuntimeConfig resourceLimits.timeout carries its unit in the key name (#17781) - #17983

Merged
zhuangjianguo merged 4 commits into
mainfrom
claude/issue-17781-plugin-security-timeout-unit
Sep 13, 2026
Merged

zhuangjianguo merged 4 commits into
mainfrom
claude/issue-17781-plugin-security-timeout-unit

Conversation

@claude

@claude claude Bot commented Sep 13, 2026

Copy link
Copy Markdown
Contributor

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.

⚠️ The declaration reads yes, not the no ruling A wrote, and the changeset is minor, not patch: 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, which references/contract-review.md's mechanical floor reads as a mandatory affirmative, and the #15678 / #15679 shape ruling A names shipped as feat(spec)! at minor in every one of its four landed siblings. needs:contract-review is hung on the card and on this PR; check-changeset-no-major reads 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.timeout is 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() that content/docs/references/kernel/plugin-security-advanced.mdx renders 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-keys reads .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.

before after
authored key resourceLimits.timeout: 60000 resourceLimits.timeoutMs: 60000
published describe Maximum execution time Maximum execution time in milliseconds
value + bound milliseconds, int().min(0) unchanged

The spelling is Ms, derived from how the suffixed family already spells itself on this tree rather than from the dispatch order: 29 key-position timeoutMs declarations across packages/spec/src/**/*.zod.ts, 40 distinct *Ms keys, and zero timeoutMillis / timeout_ms / timeoutMS variants anywhere in packages/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:

  • a retiredKey() tombstone on the old spelling — tsc types it never and a value reaching the parse raises the rename prescription instead of being silently stripped (the nested resourceLimits object is not .strict())
  • ADR-0087 D3 semantic entry kernel-runtime-config-timeout-unit-in-key and the RETIRED_KEYS_BY_MAJOR[18] row kernel/RuntimeConfig:resourceLimits.timeout, both as migrations/entries/ files with registry.ts regenerated by gen:migration-registry (never hand-merged)
  • no D2 conversion: a RuntimeConfig is the engine block of the SandboxConfig a host or a plugin security manifest constructs, stack.zod.ts declares no sandbox, security-policy or runtime-config collection, and it is not a stored sys_metadata row — 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.mdx regenerated by gen:docs — three rows move and the tombstone prescription renders in place of the old describe
  • a minor changeset carrying the FROM to TO mapping and the adr-0087: registered disposition

One 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:schema and check:authorable-surface both run with OS_EAGER_SCHEMAS=1, which makes lazySchema evaluate 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.ts carried 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.

before after
what it pinned RuntimeConfigSchema.parse({ resourceLimits: { timeout: 60000 } }) succeeds and returns 60000 the bare spelling is REFUSED with the rename prescription; timeoutMs parses at the same magnitude beside its siblings; the describe publishes the unit; the two same-named timeout retirements on this file name their own shapes apart
its comment "leaves it bare — its describe names no unit … the JSDoc-channel gap is #15939" records what the slot used to pin, why the control was written, and that #15939 IS the sweep it was written for
where it lives inside describe('Plugin security durations carry their unit (#15678)') its own top-level describe, so #15678's block header — "FOUR durations … all four old spellings are 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 not unrecognized_keys, and the message carries the FROM to TO mapping — never a bare toThrow().

#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 (slot issue-17781), verdicts read from its own VERDICT command-exit line; every gate's exit code captured before any pipe (cmd > log 2>&1; ex=$?).

  • pnpm --filter '@objectstack/spec^...' buildempty closure (No projects matched): packages/spec has no workspace dependencies, so step ① is a documented no-op
  • pnpm --filter @objectstack/spec buildVERDICT command-exit 0, re-run after the last source edit and after the main merge
  • pnpm --filter @objectstack/spec test and pnpm --filter @objectstack/spec typecheck — see the round report for the counts
  • pnpm --filter @objectstack/spec check:generatedall 15 generated artifacts up to date after gen:migration-registry + gen:docs, re-run clean after the main merge. check:authorable-surface is green without regeneration and that is correct: the ratchet records top-level keys per def, and kernel/RuntimeConfig: carries exactly engine, engineConfig and resourceLimits — this key is nested one level below, so no row moves.
  • check:duration-unit-keys (the gate as it stands on main, not feat(spec): refuse a duration key whose JSDoc names a unit its describe does not #17635's) — exit 0
  • main merged with bash 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 0

Reverse 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 -l 1 → 0, blob hash 160ab18650abe71c), 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 with git checkout HEAD -- PATH under an EXIT INT TERM trap and proven byte-identical (git hash-object back to 160ab186, git diff HEAD empty, git status --porcelain empty); the suite re-run green. No ablation artefact is left in the tree. There is no dist leg: the test imports the schema by relative path from src, so the built package is not on the resolution path for this ablation.

The cross-repo reading, with a lit control

git grep of the pinned objectui checkout at .objectui-sha = 53ded82bf7a494f54e344e19099dbf00854b8694 (re-read from this tree; the commit is reachable in the local clone, 6409 tracked files):

term occurrences
resourceLimits.timeout 0
resourceLimits 2
RuntimeConfig 236 (lit control)
timeout 832 (lit control)
sandbox 106 (lit control)
a fabricated token 0 (dark control)

Both resourceLimits hits are prose in packages/app-shell recording that objectui's own AppShellRuntimeConfig "share not one key" with the spec's RuntimeConfig, 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

⛔ Draft on purpose: the in-seat clause-② contract review at CONTRACT_REVIEW_TIER that 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-report comment on #17781.

Authored in Claude Code session session_015c5G6TmpMKgnusmTpD7Ntt (mode:cloud), dispatched by the epic PM for #15939.


Generated by Claude Code

… 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
@github-actions github-actions Bot added size/m documentation Improvements or additions to documentation tests tooling labels Sep 13, 2026
@github-actions

github-actions Bot commented Sep 13, 2026

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

4 anchor(s) derived from 1 changed package(s); no hand-written page names any of them. ⚠️ 1 changed file(s) yielded no anchor (packages/spec/src/migrations/entries/retired-keys/18.kernel__RuntimeConfig__resourceLimits.timeout.ts), so the pages documenting them are NOT COVERED by this run — this is not a clean bill of health for those files.

What this run could not see
  • 1 changed file(s) yielded no anchor (packages/spec/src/migrations/entries/retired-keys/18.kernel__RuntimeConfig__resourceLimits.timeout.ts) — pages documenting those are invisible to this run
  • 4 name(s) were too generic to anchor anything (single lowercase words)
  • the SDK route bridge reached 60 of 215 client-bound route-ledger rows — the other 155 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 155: 0 are remediable by widening that discovery convention (an in-repo file declares the path; the convention did not scan it); 55 are structural — on a ledger where NOT ONE row is declared in-repo, so no discovery change reaches them at any price; 100 are undecided (no in-repo declaration, on a ledger that has other in-repo registrars — absence and an unreadable spelling are not distinguishable here). The rows themselves: node scripts/docs-audit/affected-docs.mjs --bridge-coverage
  • a page that states a rule by its inputs shares no identifier with the emitter that implements the rule, so an emitter-only diff cannot list it — not on this run and not on any run. Measured on fix(driver-sql): emit varchar(maxLength) for a text field a declared index keys on #11430: content/docs/protocol/objectql/types.mdx documents the text-family column mapping by the ObjectQL type names it maps FROM (text / textarea / html) while the diff changed createColumn; it went unlisted, and it was the page that diff falsified, in four places. No shared token exists to detect this on, so a rule your change carries has to be re-read by hand in the pages that restate it.

Coarse fallback — 136 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): node scripts/docs-audit/affected-docs.mjs --json 00c332b4d341f49c9810136f65a92d317f7586b0packageMentionDocs.

Which tree this was computed on

This run read content/docs from 711f9dff6f42da6de8f034dd02a6cdd1f16ae19a — the merge of head 65dae07b9053953d606db247d1b6f70dffd2309c into base 00c332b4d341f49c9810136f65a92d317f7586b0, which is what actions/checkout gives a pull_request run. Not the PR head.

A worktree cut from an older main holds a different content/docs, so re-deriving there can legitimately return a different list — that is a different tree, not a wrong row. To answer on the same tree:

# 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

⚠️ That checkout carried uncommitted changes, so the commit above does not fully identify what was read.

…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

Copy link
Copy Markdown
Collaborator

Contract review

Head sha reviewed: 65dae07b9053953d606db247d1b6f70dffd2309c — re-read from GitHub after every measurement below (last read after the full test run): unchanged. Merge base with origin/main (4dab2bc5c): e5272b774. PR still draft; needs:contract-review on PR and card #17781; 7 files, none on a governed surface; content/docs/releases/ untouched; base main at 45b90b6a9 per GitHub. The os-dev-report landed on the card at 10:10Z (5652629214) with open_questions: [].

Every tree reading was taken in a fresh detached worktree at that sha (pnpm install --frozen-lockfile --offline, exit 0), @objectstack/spec built through bash scripts/pm/os-verify-lock.sh (VERDICT command-exit 0, held 227 s). Judged from the diff and by parsing, never from the PR body.

① derived judgments — accept set and public surface

  1. resourceLimits.timeout: <any defined value> moves accept → refuse. Parsed at head: 60000, 0 and null are each REFUSED with one issue at path resourceLimits.timeout, code invalid_type, message = the rename prescription; undefined still parses. The refusal reaches every path to the def: RuntimeConfigSchema itself, the runtime: RuntimeConfigSchema.optional() arm at packages/spec/src/kernel/plugin-security-advanced.zod.ts:373 (issue at runtime.resourceLimits.timeout), and PluginSecurityManifestSchema.sandbox at :710 (issue at sandbox.runtime.resourceLimits.timeout). There is no union arm to miss — RuntimeConfigSchema has exactly three references in packages/spec/src outside tests: its declaration :193, the .optional() at :373, and the type exports :784-786. The tsc half is also measured: a probe assigning timeout: 60000 to RuntimeConfig fails TS2322: Type 'number' is not assignable to type 'undefined' at exactly that key, while timeoutMs: 60000 and timeout: undefined compile. PR characterisation ("raises the rename prescription instead of being silently stripped") — right.
  2. resourceLimits.timeoutMs: n moves from accepted-and-stripped to accepted-and-honoured. Under the merge-base schema the new acceptance pin fails with expected undefined to be 60000 (plugin-security-advanced.test.ts:435) — the non-strict object dropped the key silently; at head it carries 60000 with the int().min(0) bound (-1too_small, 1.5expected int). So on the pure payload accept/reject axis this PR is a strict narrowing; what widens is the public surface: a new authorable spelling with effect, a new property on the exported RuntimeConfig/RuntimeConfigParsed types (:784-786) and transitively on SandboxConfig*/PluginSecurityManifest*, and a new reference-page row. The PR's "a spelling on a published payload no author could write before" is right on the surface limb, imprecise on the accept-set limb (that payload parsed before, silently). Both limbs read clause-② yes (③ F1).
  3. Published describe Maximum execution timeMaximum execution time in milliseconds (:318, read back from the schema), tombstone describe [REMOVED] … — rendered at content/docs/references/kernel/plugin-security-advanced.mdx:362,370-371,398. gen:docs re-run at head: 222 files, git status --porcelain empty ⇒ generator output byte-exact. Right.
  4. Non-strict control: a typo key timeoutms: 1 is still accepted-and-stripped at head (pre-existing, untouched) — exactly why a bare deletion would have been the ADR-0049 silent strip; the tombstone closes that door for the old spelling only. Right (PR: "the nested resourceLimits object is not .strict()").
  5. Exports and ratchets: nothing moves, and that is correct for the nesting depth. check:generated at head on the fresh dist: all 15 artifacts up to date (exit 0). authorable-surface/kernel.json:661-663 and authorable-surface.base.json:4907-4909 record kernel/RuntimeConfig: as exactly engine, engineConfig, resourceLimits; packages/spec/scripts/build-schemas.ts:850-856 reads schema.properties one level deep, so no level-2 key can ever move that ratchet (lit control on the same file: kernel/SandboxConfig:process is recorded at kernel.json:684, process.timeoutMs is not). json-schema.manifest/kernel.json:133 records the def name only; api-surface records export existence only. Should a ratchet have moved? No — none can see this key by construction; consequence recorded under ③ F5. Right.
  6. ADR-0087 ledger: +1 semantic entry in step18 (registry.ts:8686-8744, id kernel-runtime-config-timeout-unit-in-key) and +1 row under RETIRED_KEYS_BY_MAJOR[18] (registry.ts:12790), beside the four [#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 rows on the same file (:12159,:12168,:12762,:12801). gen:migration-registry re-run → git status --porcelain empty (208 semantic / 169 retired-key / 178 retired-def); check:migration-registry exit 0. Generated, not hand-merged. spec-changes.json / upgrade guide do not move because protocol 18 is the open major — same as every sibling in step18.
  7. "No D2 conversion" — right in-repo, by measurement: stack.zod.ts declares no sandbox/security-policy/runtime-config collection (its security hits at :39-43 are permission/capability/sharing); MetadataTypeSchema (kernel/metadata-plugin.zod.ts, 27 types) has none that carries a SandboxConfig; applyConversionsToStoredItem (packages/spec/src/conversions/stored.ts:64-70) is keyed by metadata type; conversions/registry.ts has 0 kernel/ surfaces (its 4 sandbox hits at :3499,:4143,:4172,:4553 are prose about script sandboxes; lit control flow 172); the one runtime reader, packages/core/src/security/sandbox-runtime.ts:344-346, reads resourceLimits.maxCpu only and imports SandboxConfig as a type. Outside this repo (cloud, customer projects): NOT MEASURED — bounded: a stored old-spelling shape fails loudly with the prescription at parse and at tsc, never strips silently. Observation: no in-repo runtime reads timeout or timeoutMs at all, so the entry's acceptance-criteria sentence "aborts execution after sixty seconds exactly as timeout: 60000 did" is vacuously true here — pre-existing (the key was unenforced before), inside check:liveness's remit (green), not introduced by this PR.
  8. Consumers: resourceLimits.timeout outside packages/spec / generated docs / changelogs at head = 0 against lit control resourceLimits = 4 (sandbox-runtime.ts:344-346 ×3, a worker-option comment at scripts/check-dts-emitted.mjs:52); examples/ + skills/ = 0. Pinned objectui commit 53ded82bf7… (git cat-file -t = commit, 6409 tracked files, grepped as the commit object): resourceLimits\.timeout 0 · resourceLimits 2 (packages/app-shell/src/runtime-config.ts:261, __tests__/spec-symbol-parity.test.ts:162, both prose naming top-level keys only) · lit timeoutMs 92 / RuntimeConfig 236 / \btimeout\b 688 / sandbox 106 · dark qqzzxxnotakey 0. No consumer fixture can flip. Consumer-package suites NOT MEASURED locally; bounded by the zero census and by CI on this head (Test Core 1–6, Type Check · workspace, Build Core all success). ⚠️ Console Pin Gate is skipped on this head, so the objectui answer rests on the grep, not on that gate.
  9. Prose the entry bakes into the published upgrade guide (18.kernel-runtime-config-timeout-unit-in-key.ts:28-31): "29 key-position timeoutMs" is the merge-base count (head reads 30 — it now includes its own); "no timeoutMillis/timeout_ms/timeoutMS variant" is true on the base tree and true at head outside the entry's own self-mention (6 hits, all in the entry ×3 and its registry mirror ×3); "spells timeout 0 times" is true case-sensitively (NodeJS.Timeout at sandbox-runtime.ts:57 is a type name). Not contract facts; verdict-neutral; recorded so the next reader is not misled.
  10. Nothing else on the def moves — the source diff is confined to :179-192 (the const, declared above its consumer for the OS_EAGER_SCHEMAS=1 TDZ reason; check:authorable-surface, which is the eager run, is green) and :305-320. check:duration-unit-keys (main's gate) exit 0, 211 keys, zero offenders.

⭐ The pin-test replacement (the item this review was pointed at): run against the pre-change schema, not by deleting the tombstone. git restore --source=<merge-base> of the zod file (blob 160ab1868daf629b3427, anchor RUNTIME_RESOURCE_LIMITS_TIMEOUT_RETIRED 2 → 0 on disk), then the test file: 4 failed / 20 passed — exactly the four new pins: :418 expected true to be false (bare spelling was accepted), :435 expected undefined to be 60000 (timeoutMs was stripped), :445 (no timeoutMs describe), :458 (no refusal to read a message from). The 20 passing are the lit control that the harness ran. Restored with git checkout HEAD -- (blob back to 160ab1868d, anchor 2, porcelain empty, git diff HEAD empty); at head 24/24. ⇒ every new assertion fails against the pre-change schema and passes after — the slot pins something real. The round's own ablation (tombstone line only, 2 red) was a weaker cut; both agree. The rewritten comment (:397-411) is true: it says the slot held a negative control, why, that #15939 is the sweep it was written for, and that it now pins the opposite fact — which is what :413-466 pins. Moving the block out of describe('… (#15678)') keeps that header's "FOUR durations" exactly true.

② semver grading — minor, feat(spec)!

  • What the diff does: removes a published authorable spelling and adds one. Strict semver: major.
  • What the repo's written convention says: scripts/check-changeset-no-major.mjs:36-88 — during the launch window breaking changes ship as minor; major is refused while the guard is armed (it is: no .changeset/pre.json, no allow-major label on this PR); "the bump level tells a consumer nothing about whether the release breaks them", and the mandatory carriers are the BREAKING banner and the ADR-0087 disposition. Both present: .changeset/17781-runtime-config-resource-limits-timeout-ms.md:9 **BREAKING**, :7 <!-- adr-0087: registered kernel-runtime-config-timeout-unit-in-key -->, :20-25 FROM→TO diff + one-line fix. check-adr-0087-registration --base origin/main exit 0 ("registered … new here"); check-changeset-no-major --base origin/main exit 0; CI Check Changeset success on this head (the level axis with the PR payload: Clause-②: yes ⇒ ≥ minor on the moved package — satisfied by :2).
  • Is major the honest level? Under strict semver yes, and the repo says so itself ("that is the whole cost of the window"); under the convention in force it is refused, and the honesty is carried by the banner + disposition, both of which are here. minor is therefore the correct grade on this tree; major would be a gate red, patch would carry no breaking signal and fail the level axis.
  • Were the siblings right? Yes — for that written reason, not because they are siblings: CHANGELOG.md feat(spec)! entries under ## 17.4.0 corroborate, they are not the argument. Ruling A's patch was contradicted by its own "[#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/[#14478 stack 4/6] system/: the 15 remaining duration keys carry their unit in the key name — ADR-0087 conversions with readers; metrics.zod.ts size needs an honest name, not the mechanical one #15679 shape" and by the gate; correction 5652120294 is right.
  • Observation, pre-existing and tree-wide, not this PR's: the tombstone says "in @objectstack/spec 17" (package major; the four sibling tombstones on this file at :325,:331,:344 say the same) while the ledger row sits under protocol 18 (140 rows under 18: vs 29 under 17:; build-schemas.ts:848 CURRENT_MAJOR = 17). Consequence: the aging clock (:1101, 2 majors) starts at 18, so this tombstone cannot age out before package major 20. Consistent with every sibling; not a defect of this diff.

③ boundary flags

Implemented-by: claude/issue-17781-plugin-security-timeout-unit
Reviewed-by: session_015c5G6TmpMKgnusmTpD7Ntt

(Disclosed, not hidden: dispatcher, PM and this reviewer share one session — 席内至档 as recorded on #15939 5652016540; the pair reader will flag the same-session correction. The review worktree and owned ref were removed; the shared checkout is at 84e6b05 with an empty git status.)

PASS — the replacement pin is proven to fail against the pre-change schema and its comment is true, the tombstone refuses on the def and on every path that reaches it, the registry and reference page are byte-exact generator output, "no D2" holds in-repo by measurement, and both PM corrections are correct on the repo's own written text.


Provenance and tier verification — the seat's own record

Adopted verbatim by the epic PM seat for #15939, session_015c5G6TmpMKgnusmTpD7Ntt, 2026-09-13T10:22Z. The fuse permits adoption or voiding, ⛔ never rewriting or abridging.

  • Served tier, from the harness-stamped transcript, ⛔ not a self-report: 100 messages stamped "model":"claude-fable-5-1", zero other values — exact match to CONTRACT_REVIEW_TIER. Discriminating control: the authoring round's transcript over the same grep reads 239 × "model":"claude-opus-5".
  • Context-isolated seat, fed only the card, the standing rulings and the PR — ⛔ never the dispatch order or this seat's conclusions. It inherits this session id (席内至档) and discloses that itself.

Where this review beat the round it reviewed: the round proved its new pin by ablating the tombstone line only (2 assertions red). The reviewer instead restored the whole zod file to its merge-base state and got 4 failed / 20 passed — every one of the four new pins red against the pre-change schema, with the 20 passing as the lit control that the harness actually ran. Its own words: "the round's own ablation … was a weaker cut; both agree." That is the difference between "the tombstone is load-bearing" and "the slot pins something real", and only the second is what a replacement pin owes.

⚠️ Declared gaps I am accepting with the verdict, ⛔ not glossing: consumer-package suites were not run locally (bounded by a zero census plus CI green on this head), Console Pin Gate is skipped on this head so the objectui answer rests on the grep rather than that gate, and the cloud/customer population for the "no D2 conversion" reading is NOT MEASURED (bounded: a stored old spelling fails loudly at parse and at tsc, never strips silently).

F4's escalation is carried, not closed: retired-keys/18.kernel__SandboxConfig__process.timeout.ts:6-9 is now doubly stale about this key. It is #17635's owned correction, and #17635's own rewritten sentence must be re-read against the tree after all six renames land. Logged on the epic's landing checklist — ⛔ a green check:duration-unit-keys will not detect it.

epic PM for #15939 · session_015c5G6TmpMKgnusmTpD7Ntt · 2026-09-13T10:22Z


Generated by Claude Code

@zhuangjianguo
zhuangjianguo marked this pull request as ready for review September 13, 2026 10:22
@zhuangjianguo
zhuangjianguo added this pull request to the merge queue Sep 13, 2026
Merged via the queue into main with commit cbcae14 Sep 13, 2026
40 checks passed
@zhuangjianguo
zhuangjianguo deleted the claude/issue-17781-plugin-security-timeout-unit branch September 13, 2026 10:59
akarma-synetal pushed a commit to akarma-synetal/framework that referenced this pull request Sep 17, 2026
…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>
akarma-synetal pushed a commit to akarma-synetal/framework that referenced this pull request Sep 17, 2026
…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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation size/m tests tooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

spec: 1 duration key(s) in kernel/plugin-security-advanced.zod.ts name their unit only in JSDoc — #15939 Ruling A remediation (1 of the 21-row delta)

2 participants