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 —
|
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