You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
fix(spec): the agreement shape is an offence — name the unit in two describes and remove the carve-out (#19035)
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](https://claude.ai/code/session_01AmH9bKvGoLjiY86Q4Z3og2)_
---------
Co-authored-by: Claude <noreply@anthropic.com>
0 commit comments