feat(spec): refuse a duration key whose JSDoc names a unit its describe does not - #17635
Conversation
…be does not
`check:duration-unit-keys` read a key's unit from `.describe()` and
`.meta({ description })` only. A duration-shaped `z.number()` whose unit was
stated solely in the JSDoc block above it was a census row with `[prose: -]`
and no verdict — and the blindness did not merely miss such keys, it produced
confident wrong prose about why they were missed.
Ruled 2026-09-07 (decision batch #65): JSDoc is developer commentary, not
governed prose, so the gate does NOT start reading it as a unit channel. What
it refuses is the DIVERGENCE — JSDoc names a unit, describe names none (or
there is no describe at all) — as `unit-in-jsdoc-not-in-describe`. The JSDoc is
read in one direction only: to refuse, never to satisfy. A key with no unit in
either channel stays listed and not judged, unchanged.
The self-test pins the three measured sites as positive controls, a
unit-in-both-channels key as the negative control, and the two ways the reader
could over-fire: a `//` line comment is not a JSDoc block, and an enclosing
declaration's JSDoc is not inherited by the first property inside it.
Also repairs the two remaining imprecise recorded reasons this blindness
produced, both comment-only: the retired-key entry for
`SandboxConfig:process.timeout` and the burn-rate `window` pin comment in
`metrics.test.ts` both said "outside the gate's population" where the truth is
"inside the census, outside the verdict", and both now name the unpublished
JSDoc unit. `registry.ts` regenerated to mirror the entry.
Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MkQhmuuJAVDjmeWNixwDDH
Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MkQhmuuJAVDjmeWNixwDDH
📓 Docs Drift Check1 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 c9fb9105ca4dad8363bc6739d3b8f67b076f7ea8 && git checkout c9fb9105ca4dad8363bc6739d3b8f67b076f7ea8
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin e248c4d688ba46fc86af199cce7660b55b8cfb30 605b7f60a4d0ce228a6e61aff748c9de91b8dfea && git checkout -B drift-repro e248c4d688ba46fc86af199cce7660b55b8cfb30 && git merge --no-ff 605b7f60a4d0ce228a6e61aff748c9de91b8dfea
node scripts/docs-audit/affected-docs.mjs --json e248c4d688ba46fc86af199cce7660b55b8cfb30 |
Status update — the blocker CHANGED on 2026-09-11T14:08Z and this PR's body never said so
What the body still says, and why it is staleThe body's closing line reads "Its substantive blocker is unchanged and is a maintainer question — see the decision card on #15939." That is no longer true. The decision was made: the director seat recorded Ruling A on #15939 at 2026-09-11T14:08Z (batch #115), carrying the maintainer's own 「同意」. ⇒ this PR is not waiting on a maintainer. It is waiting on work, and that work is now filed. The red is still by construction — that part is unchanged
What actually blocks it nowRuling A ordered the remediation first, per file, with this gate landing last into a tree it already reads as clean — which is the recommendation this PR's own body made. That is now executed:
18 renames + 3 describe-only = the full delta this PR enumerated. #15939 is now
⇒ This PR's disposition, stated plainlyParked, draft, and correctly so — but the reason to write on it from now on is "waiting on #17780–#17786", ⛔ not "waiting on a ruling". When all seven are on
Generated by Claude Code |
…ration-unit-keys-jsdoc
…ration-unit-keys-jsdoc
…ration-unit-keys-jsdoc # Conflicts: # packages/spec/src/system/metrics.test.ts
…ted tree The note's replacement prose was written while the neighbouring `RuntimeConfig.resourceLimits.timeout` rename was still pending, and asserted that key was "inside the gate's census and outside its verdict", that its unit was "not missing, only unpublished", and that it was "a rename waiting on that gate change". #15939 ruling A's per-file remediation has since landed that rename, so all three read false on this tree. The note now repairs the original wrong reason ("outside the gate's population") without re-asserting a landed rename as pending, and points at the neighbour's own entry instead of restating its story. registry.ts regenerated to mirror it. Claude-Session: https://claude.ai/code/session_015c5G6TmpMKgnusmTpD7Ntt Co-authored-by: Claude <noreply@anthropic.com>
…iated tree The changeset was written while all 21 offenders were pending. It claimed a "0 -> 21" population delta on this tree and "two wrongly-recorded reasons repaired". Both now read false: ruling A's seven per-file cards have landed the whole population, so the gate reads zero offenders here, and the metrics.test.ts half of the prose repair was carried by #17783 when it renamed that key. Numbers re-measured on this tree: 0 offenders among 211 duration-shaped numeric keys across 2482 source files. Claude-Session: https://claude.ai/code/session_015c5G6TmpMKgnusmTpD7Ntt Co-authored-by: Claude <noreply@anthropic.com>
Contract review — PR #17635, in-seat clause-② review at CONTRACT_REVIEW_TIERHead-sha: Adopted verbatim from a context-isolated, adversarial review subagent. Its brief carried the card, the recorded rulings and the PR itself — ⛔ not the dispatch order and ⛔ not this seat's own conclusions — and was written to invite rejection. The verdict below is reproduced whole and unedited; ⛔ this seat did not rewrite, abridge or polish any part of it, and the two out-of-scope gaps it found are carried through rather than dropped. Served-tier verification (降档保险丝). Head reviewed: ① Derived judgments Accept-set / public-surface changes the diff produces:
② Semver grading
③ Boundary-flag disposition
PASS — Out of scope (real, must be filed):
epic PM for #15939 · Generated by Claude Code |
Part of #15939
The
check:duration-unit-keyswidening, landing last as ruling A sequenced it. All seven remediation cards (#17780–#17786) have merged, so the population this rule adds is already remediated and the widened gate reads zero offenders on the merged tree.The ruling this executes
Option 2 of #15939, recorded 2026-09-07 (decision batch #65), maintainer 「同意」; sequencing set by ruling A, director seat 2026-09-11, maintainer 「同意」 (decision batch #115):
JSDoc is developer commentary and is not "prose" in the sense of the #14478 rule, so the gate does not start reading JSDoc as a unit channel — option 1 was explicitly not adopted. What it refuses is the divergence: a duration-shaped numeric key whose JSDoc names a unit and whose
.describe()names none (or which has no describe at all).⛔ The JSDoc is read in exactly one direction: to refuse, never to satisfy. A key with no unit in either channel is still listed and still not judged (the #14519 shape, unmoved). Two self-test cases pin that direction and neither was relaxed to reach zero:
What changed while this PR waited
Merged
origin/mainin (⛔ never rebased, never force-pushed — the branch isos-bill's). Merge baseb06b2db5c4, two commits past the6d647858b7the dispatch recorded; both land inpackages/spec/src/and neither introduces an offender.The diff is now 4 files, not 5. #17635's
metrics.test.tshalf was obsolete, not merely conflicted: #17783 rewrote the enclosingit(...), moved the same correction into a narrowed header comment, and renamed the key itself. Verified by content rather than line number — the hunk's exact target text (outside the gate population entirely and keeps its bare name) returns 0 occurrences on the merged tree, while the bare phraseoutside the gate population entirelyreturns 1, now quoted as the superseded reading. Lit controlburn-rate2, dark control (token invented at read time) 0. That half is dropped; nothing is lost.packages/spec/src/migrations/registry.tsis generated and was NOT resolved textually. It text-merged without a conflict, which is exactly the state AGENTS.md §10 says never to trust, so it was regenerated withpnpm gen:migration-registryand proved byte-identical to the merged bytes (blobcef78f2140before and after).check:migration-registrythen read:The only conflict was
metrics.test.ts, resolved to main's side.os-regen-merge.shstopped with "✗ merge stopped on conflicts in NON-generated files — resolve those by hand" — correct for this merge, since the sole conflict really was a hand-written file. The #18047 misclassification did not manifest here.The prose repair — one site, not two
The blindness did not merely miss keys, it produced confident wrong prose about why they were missed. One site still carried it. The replacement text #17635 was carrying had itself gone stale and would have landed already wrong: it described the neighbouring
RuntimeConfig.resourceLimits.timeoutas a key whose "describe names no unit at all", "inside the gate's census and outside its verdict", and "a rename waiting on that gate change". On this tree that key is already renamed —timeoutMs … .describe('Maximum execution time in milliseconds')atplugin-security-advanced.zod.ts:316-317, its tombstone at:320, its retirement prose at:185.The note now repairs the original wrong reason without re-asserting a landed rename as pending, and points at the neighbour's own entry instead of restating its story. It spells no package version, so it does not join the
@objectstack/spec 18class filed as #18040.registry.tsregenerated to mirror it. ⛔ No pin assertion, title or body changed.The second site — the
metrics.test.tsburn-rate pin — was corrected by #17783 when it renamed that key, so nothing is owed there.The gate reading, from the right instrument
⛔
check:duration-unit-keyson plainmainproves nothing here:maincarries the old gate. This is this branch's widened gate on the merged tree:A zero is only as good as the proof the instrument can still say non-zero. Lit control, drawn a different way than the self-test — a real on-disk mutation of a real source file, not a synthetic fixture: the unit was stripped from
resourceLimits.timeoutMs's describe, leaving its JSDoc naming milliseconds. Blob160ab186→b7d06799, deleted anchor0/ injected anchor1. The gate then:The new rule class fires, on the real tree walk, at the exact mutated key, with the census unchanged at 211/2482 — so only the verdict moved. Restored and proved restored by observed state: on-disk blob back to
160ab186,git diff HEADempty, working tree clean. The mutation ran under atrap … EXIT INT TERMwith an absolute path.Population, measured here rather than relayed: 18 renames each carrying a
retiredKey()tombstone entry naming #15939 onorigin/main, plus 3 describe-only corrections = 21. Counted with an instrument independent of the gate (tombstone entry files), lit control 15 (#15678entries), dark control 0.Ruling A's objectui pre-check — inapplicable, and why
Ruling A requires a
git grepofobjectuiat the pinned SHA before landing a published-key rename (AGENTS.md Post-Task Checklist step 4). This PR renames nothing. The 18 renames all landed in the seven remediation cards, each of which owed that check on its own landing. Measured here: every changed line underpackages/spec/src/is a comment — the non-comment changed-line set is empty. So the check is inapplicable rather than done, and it is ⛔ not silently skipped.Verification
Heavy runs through
scripts/pm/os-verify-lock.sh(OS_VERIFY_LOCK_SLOT=issue-15939-dev), verdicts quoted from the lock's ownVERDICT command-exitline, ⛔ never a bare$?.pnpm --filter '@objectstack/spec^...' buildVERDICT command-exit 0— empty closure (No projects matched);packages/spechas no workspace dependencies, so this is a declared no-op, ⛔ not counted as a passpnpm --filter @objectstack/spec buildVERDICT command-exit 0(199s) — run before every gate that readsdist/pnpm --filter @objectstack/spec typecheckVERDICT command-exit 0—tsc --noEmit+check:scripts-typecheck+check:test-typecheckpnpm --filter @objectstack/spec testVERDICT command-exit 0— 476 files / 13578 tests passedcheck:duration-unit-keys --self-testcheck:duration-unit-keys(widened, merged tree)check:migration-registrycheck:generateddispatch-gates.mjs --ranGate family derived after the merge and changeset existed, with
node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack, and reconciled with--rancarrying a recorded exit code per family. Two prerequisites were built rather than counted green (@objectstack/formula,@objectstack/lint,@objectstack/objectql), after whichcheck:doc-formula-expressionsandcheck:lean-entry-closureboth read exit 0.⊘ NOT MEASURED — declared, ⛔ not counted green:
pnpm check:dual-build-cjs-loads— exit 3,PREREQUISITE NOT MET: reads built output for 87 packages and needs a whole-repopnpm build. That is CI'sBuild Core. Nothing was measured; this is neither a pass nor a finding.pnpm check:pm-dispatch-gates— exit 124, my owntimeoutkill at 560s under the container's foreground cap, ⛔ not a gate verdict. Its partial output showed no failing case, but no verdict line was reached. Note the reconciler derives NOT-MEASURED only from exit 3, so it counts this family as run; it is declared here instead.The repo-wide farm (
pnpm lintand the 48 artifact-roster, 11 wide-population and 5 path-scheduled CI families the derivation names as outside its total) is CI's run. This narrowing is declared, not silent.Changeset
.changeset/15939-duration-unit-keys-jsdoc-divergence.md,@objectstack/spec: patch. Rewritten against the merged tree: it had claimed a live "0 → 21 offenders" population delta and "two wrongly-recorded reasons repaired", both false now that the remediation has landed and one of the two sites belongs to #17783. ⛔ No published key, accept set, default or runtime behaviour moves in this diff.Gate declaration for this PR
Why
nois the honest answer for THIS diff, re-measured after the merge: the four files are the gate script (packages/spec/scripts/**, notsrc/**), one comment-only prose repair, the regenerated migration registry mirroring it, and the changeset. Mechanically checked — every changed line underpackages/spec/src/is a comment; the non-comment changed-line set is empty. No new exported symbol, no new key on a published payload, and no accept/reject outcome moved. What the diff does is make an existing gate refuse more inputs, which narrows the accept set; narrowing is the semantic face, never clause ②.check:pm-widening-tellsreads exit 0.packages/spec/), so an in-seat contract review atCONTRACT_REVIEW_TIERis owed before this may turn ready or enqueue. Scheduled, not waived — and this round ⛔ did not turn it ready, ⛔ did not enqueue it, and ⛔ did not touchneeds:contract-review.Prepared by the
os-devseat in sessionsession_015c5G6TmpMKgnusmTpD7Ntt; merge commitd9b3fd5a3d, merge baseb06b2db5c4.Generated by Claude Code