Skip to content

fix(spec): the agreement shape is an offence — name the unit in two describes and remove the carve-out - #19035

Merged
os-steve merged 5 commits into
mainfrom
claude/issue-18075-agreement-shape-is-an-offence
Sep 18, 2026
Merged

os-steve merged 5 commits into
mainfrom
claude/issue-18075-agreement-shape-is-an-offence

Conversation

@os-steve

Copy link
Copy Markdown
Collaborator

Fixes #18075

Executes batch #158 item 5 · letter A (director seat summon #24, maintainer 「同意」 2026-09-18T11:14Z). The ruling's verbatim scope:

the agreement shape — a unit in the key name, the same unit in the JSDoc, no unit in .describe() — IS an offence: the carve-out !site.jsdocUnits.some((u) => site.keyUnits.includes(u)) is removed, the two live rows get a .describe() that names the unit, the two DEFERRED self-tests turn positive, and the header's shape (b) base refusal is restored

Clause-②: no

The four deliverables

# Deliverable Where
1 The two live rows name their unit in the published channel packages/spec/src/ai/usage.zod.ts latencyMs (had no .describe() at all) and packages/spec/src/system/tenant.zod.ts frequencyHours ('Backup frequency', unit-silent)
2 The carve-out is removed the divergence guard is now site.keyUnits.length > 0 && site.jsdocUnits.length > 0
3 The two DEFERRED self-tests turn positive both now assert unit-in-jsdoc-not-in-describe, relabelled REFUSED (agreement): …
4 The header's shape (b) base refusal is restored the "TWO shapes this branch used to refuse" block now names ONE cost — (a), the retired name list — and records (b) as restored

Landing order is the ruling's: commit 1 adds the two describes, commit 2 removes the carve-out. main is 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 #18075 marker 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 class-(a) sibling the dev found (the instant exemption branch reads proseUnits only, never jsdocUnits, while durationType reads all three) rides the same PR — zero live rows today, a fixture proves it, no separate card.

The EpochMs instant 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: an EpochMs key 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 — not skip-changeset

.describe() text was measured into the published tarball, not assumed. Both new strings appear under paths in packages/spec's files[], with a pre-existing describe ('Computed cost in USD') as the positive control landing the same way:

Wall-clock latency in milliseconds -> dist/ai/index.{js,mjs}, json-schema/ai/AIUsageRecord.json, json-schema/objectstack.json
Backup frequency in hours         -> dist/{browser/,}system/index.{js,mjs}, json-schema/system/{DatabaseLevelIsolationStrategy,TenantIsolationConfig}.json
Computed cost in USD (control)    -> dist/ai/index.{js,mjs}, json-schema/ai/AIUsageRecord.json

A shipped JSON Schema description moves, so a released package publishes a change. patch, Clause-②: no.

The reader-facing half regenerated with it — content/docs/references/system/tenant.mdx stops printing frequencyHours | 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-test 106 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_FLOOR and every entry in SELF_TEST_BATTERIES are 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:

leg mutation result restore
A re-add the carve-out to the guard self-test exit 1, exactly the 2 agreement cases red, 106 still registered blob back to d00c46dfe66b = HEAD, git diff HEAD empty
B1 revert latencyMs's describe gate exit 1, exactly 1 offender: [unit-in-jsdoc-not-in-describe] …/usage.zod.ts:52 latencyMs blob back to d241245cd5ae = HEAD
B2 revert frequencyHours's describe gate exit 1, exactly 1 offender: [unit-in-jsdoc-not-in-describe] …/tenant.zod.ts:603 frequencyHours blob back to 77b5db008946 = HEAD
C delete the instant branch's JSDoc channel self-test exit 1, exactly the 1 new positive control red blob back to ec726de17d8f = HEAD

Leg 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 typecheck exit 0; pnpm --filter @objectstack/spec test exit 0, 489 files / 14229 tests passed; check:generated all 16 artefacts up to date after the merge.

Gate families — scripts/pm/dispatch-gates.mjs derived 103 for these paths from its own change set (never a hand-fed list). 102 ran, all exit 0, reconciled back through --ran with each exit code recorded: 103 derived, 102 run, 0 NOT-MEASURED, 1 UNRUN.

  • NOT MEASURED: 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 grades scripts/pm/dispatch-gates.mjs, which this diff does not touch. CI runs it. ⛔ Not reported as green.
  • Four families first returned 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 at ce75c01bb9: exit 0, no narrowing claimed and none needed.

Merge — origin/main moved 10 commits into packages/spec while this was in flight, including a breaking spec change. Merged through scripts/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:

  • The instant branch and the durationType branch still differ in predicate shape, not in channel coverage: instant refuses a channel only when it names a unit and none of them is ms, while durationType refuses each non-matching unit. A describe naming both ms and 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-608 carries 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

`.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>
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

2 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
  • 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 784366372ec580f470c87ff891f0b533217e55f6 → packageMentionDocs.

Which tree this was computed on

This run read content/docs from 975e19f9c3ff5724c73c3d15c440d85d398f8c98 — the merge of head ce75c01bb9b5fdbe607e41a23d881b5fd4a80b57 into base 784366372ec580f470c87ff891f0b533217e55f6, 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 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

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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

2 participants