Repository navigation
fix(spec): the agreement shape is an offence — name the unit in two describes and remove the carve-out - #19035
Conversation
`.describe()` is the only unit channel `content/docs/references/**` publishes; the JSDoc above the key stops at the source file. Both keys carried their unit in the name and in the JSDoc and named none in the published channel, so the reference page printed `frequencyHours | integer | Backup frequency` and left the reader to guess the unit from the key name. Remediation lands before the gate widens, so `main` is never red in between. Claude-Session: https://claude.ai/code/session_01AmH9bKvGoLjiY86Q4Z3og2 Co-authored-by: Claude <noreply@anthropic.com>
The divergence guard carried a second half that spared a key whose JSDoc named the same unit its key name already carries, with the describe silent. Agreement between the key name and a source comment says nothing about the published channel, so the carve-out is removed and the shape is a base refusal again. The two self-test cases that pinned the carve-out are positive controls now; the header's shape (b) returns from deferred to refused. The instant exemption reads the JSDoc channel too, for the same reason and in the same direction as the duration-type branch already did: an `EpochMs` key whose JSDoc says seconds was unrefused while a `DurationMs` key whose JSDoc says seconds was refused. No live row carries the shape; a fixture pair keeps the two branches from drifting apart again. Claude-Session: https://claude.ai/code/session_01AmH9bKvGoLjiY86Q4Z3og2 Co-authored-by: Claude <noreply@anthropic.com>
The message was written when the branch only refused a JSDoc that CONTRADICTED the key name, so it told every offender "the only unit the reader can see is the one the JSDoc disagrees with" — false the moment the two agree, which is now a refusal. It names the silent published channel as the harm instead, and keeps the contradiction reading as the conditional half it always was. Claude-Session: https://claude.ai/code/session_01AmH9bKvGoLjiY86Q4Z3og2 Co-authored-by: Claude <noreply@anthropic.com>
`gen:docs` picks up both describes: `ai/usage.mdx` and `system/tenant.mdx` now print the unit the JSDoc always had. Claude-Session: https://claude.ai/code/session_01AmH9bKvGoLjiY86Q4Z3og2 Co-authored-by: Claude <noreply@anthropic.com>
…reement-shape-is-an-offence
📓 Docs Drift Check2 anchor(s) derived from 1 changed package(s); no hand-written page names any of them, so this run has nothing to list — not a clean bill of health. This check sees only pages that NAME a derived anchor: one that documents this change in prose, or enumerates it in an authoring dialect, names none and stays invisible to it on every run. 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 975e19f9c3ff5724c73c3d15c440d85d398f8c98 && git checkout 975e19f9c3ff5724c73c3d15c440d85d398f8c98
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 784366372ec580f470c87ff891f0b533217e55f6 ce75c01bb9b5fdbe607e41a23d881b5fd4a80b57 && git checkout -B drift-repro 784366372ec580f470c87ff891f0b533217e55f6 && git merge --no-ff ce75c01bb9b5fdbe607e41a23d881b5fd4a80b57
node scripts/docs-audit/affected-docs.mjs --json 784366372ec580f470c87ff891f0b533217e55f6 |
Fixes #18075
Executes batch #158 item 5 · letter A (director seat summon #24, maintainer 「同意」 2026-09-18T11:14Z). The ruling's verbatim scope:
Clause-②: no
The four deliverables
packages/spec/src/ai/usage.zod.tslatencyMs(had no.describe()at all) andpackages/spec/src/system/tenant.zod.tsfrequencyHours('Backup frequency', unit-silent)site.keyUnits.length > 0 && site.jsdocUnits.length > 0unit-in-jsdoc-not-in-describe, relabelledREFUSED (agreement): …Landing order is the ruling's: commit 1 adds the two describes, commit 2 removes the carve-out.
mainis never red in between, and neither is any intermediate commit on this branch — the gate was re-run green after commit 1 alone.Every
DEFERRED to #18075marker is gone from the checker: five occurrences, two cases and three prose passages. The count difference was the card's own warning and it held.The class-(a) sibling rides this PR, as ruled
The
EpochMsinstant exemption now reads the JSDoc channel too, under the same predicate shape the describe half already used (length > 0 && !includes('ms')) — a channel added, not a predicate widened. Two fixtures pin it: anEpochMskey whose JSDoc says seconds is refused, and one whose JSDoc says ms is not.Live rows: 0. The population run is unchanged at zero offenders with the channel added.
One extra edit, and why it is not scope creep
The finding message told every offender "the only unit the reader can see is the one the JSDoc disagrees with" — written when this branch only ever fired on a contradiction. The moment agreement became a refusal that sentence was false for half the class, in a file whose own header says "A gate that cannot see a channel writes falsehoods about it." The message now names the silent published channel as the harm and keeps the contradiction reading as the conditional half it always was.
The card's recorded knock-on is discharged, not reworded
The card recorded that #15939's changeset over-claims — it says the gate refuses a JSDoc unit the describe does not name "(or there is no describe at all)", which was untrue of exactly these two rows. That sentence is now true of the gate. Nothing was edited in place to make it true; the two rows are remediated and the carve-out is gone. The new changeset says so explicitly.
Changeset:
patch, measured — notskip-changeset.describe()text was measured into the published tarball, not assumed. Both new strings appear under paths inpackages/spec'sfiles[], with a pre-existing describe ('Computed cost in USD') as the positive control landing the same way:A shipped JSON Schema
descriptionmoves, so a released package publishes a change.patch,Clause-②: no.The reader-facing half regenerated with it —
content/docs/references/system/tenant.mdxstops printingfrequencyHours | integer | Backup frequency.Verification
All readings on the merged head
ce75c01bb9, exit codes captured before any pipe.The gate, final tree — exit 0:
203 unit-declaring numeric key(s) across 2522 source file(s) … zero offenders, no baseline· self-test106 case(s) across 13 batteries, every battery at or above its pinned floor.The floor did not move. 104 cases before, 106 after: the two DEFERRED cases turned positive without changing the count, and the two new instant fixtures grew a battery above its floor, which the roster documents as ordinary work.
SELF_TEST_BATTERY_FLOORand every entry inSELF_TEST_BATTERIESare untouched.Three ablation legs, each mutated and restored through
scripts/ablation-replace.mjs(anchor must hit; restore proven by blob hash against HEAD, never by exit code), each run from a committed state:d00c46dfe66b= HEAD,git diff HEADemptylatencyMs's describe[unit-in-jsdoc-not-in-describe] …/usage.zod.ts:52 latencyMsd241245cd5ae= HEADfrequencyHours's describe[unit-in-jsdoc-not-in-describe] …/tenant.zod.ts:603 frequencyHours77b5db008946= HEADec726de17d8f= HEADLeg B is the one that matters for the ruling: it shows the widened guard catching the ruled shape on live source, not only on fixtures.
Package obligations —
pnpm --filter @objectstack/spec typecheckexit 0;pnpm --filter @objectstack/spec testexit 0, 489 files / 14229 tests passed;check:generatedall 16 artefacts up to date after the merge.Gate families —
scripts/pm/dispatch-gates.mjsderived 103 for these paths from its own change set (never a hand-fed list). 102 ran, all exit 0, reconciled back through--ranwith each exit code recorded:103 derived, 102 run, 0 NOT-MEASURED, 1 UNRUN.pnpm check:pm-dispatch-gates— it exceeds this environment's ~10-minute foreground cap (killed at 560s with 1840 lines of passing self-test cases and no verdict). It is a whole-tree-declared family that gradesscripts/pm/dispatch-gates.mjs, which this diff does not touch. CI runs it. ⛔ Not reported as green.PREREQUISITE NOT MET(unbuilt workspace packages) and one returned exit 3 on a shallow-clone fixture. Each was cleared by building the named package / fetching the pinned commit and re-run to a real verdict — ⛔ none is reported from the instrument that did not run.Repo-wide lint —
pnpm lint(eslint . --no-inline-config) over the whole population atce75c01bb9: exit 0, no narrowing claimed and none needed.Merge —
origin/mainmoved 10 commits intopackages/specwhile this was in flight, including a breaking spec change. Merged throughscripts/pm/os-regen-merge.sh, reinstalled, rebuilt, and every reading above re-taken on the merged head. Both describes, both reference pages and the removed carve-out were verified present after the merge.Acceptance notes
Noted, not filed — recorded so the next reader does not re-open them:
durationTypebranch still differ in predicate shape, not in channel coverage: instant refuses a channel only when it names a unit and none of them isms, whiledurationTyperefuses each non-matching unit. A describe naming bothmsand another unit is therefore tolerated on an instant and refused on a duration type. Zero live rows either way, and the tolerant reading is arguably right for a sentence with an incidental second unit. Deliberately left alone: the ruling authorized a channel, not a predicate. Handler: whichever PR next touches this file.packages/spec/src/system/tenant.zod.ts:600-608carries trailing whitespace on its blank separator lines. No gate reads it; not touched, because this PR's edit there is one line and a whitespace sweep would bury it. Handler: none needed.Generated by Claude Code