Repository navigation
Commit 02f8e45
test(spec): a battery roster, floor and verdict handshake for check-duration-unit-keys (#18916)
Fixes #18512
Clause-②: no
## The defect, re-measured on this tree before anything was written
`packages/spec/scripts/check-duration-unit-keys.ts` (1471 lines)
predates the self-test battery
discipline: no battery roster, no pinned per-battery minimum, no
module-level verdict flag.
Re-run first-hand at `origin/main` 84ba4a8, not inherited:
| leg | reading |
|---|---|
| `tsx scripts/check-duration-unit-keys.ts --self-test`, untouched | 103
checkmark lines, 0 cross lines, exit 0 |
| its only summary line | `self-test: all cases pass` |
| the same run with `DURATION_ROOTS` emptied | 101 registered — 95
green, 6 red, exit 1 |
Two corrections to the card's own wording, both measured here:
- The card says "the printed count stays non-zero" and "a reader
watching the count would see
101". This self-test printed **no case count at all**. The 103 and 101
are a tally a reader
has to make by hand off the checkmark lines, so the reader did not even
have the clue the
card credits them with. That makes the defect worse, not milder.
- The card pins the census at "185 rows / 0 offenders". Today's census
reads **203
unit-declaring numeric keys across 2520 source files, 0 offenders**. 185
is a stale figure
from the filing date; what this PR holds is the invariant behind it —
the non-self-test
leg's output is byte-identical before and after.
## What landed
### The three parts, copied from
`scripts/check-agent-model-declared.mjs`, never imported
Copied per AGENTS.md's "Copy a landed one, never import one" — every
self-test must keep
running standalone. The TypeScript surface follows the sequential port
in
`scripts/check-test-typecheck.mts` (a readonly string-to-number record
over `Object.freeze`,
a `battery()` opener, an `UNATTRIBUTED_BATTERY` key). No symbol is
imported from either.
- `SELF_TEST_BATTERIES` — 13 declared batteries, name to minimum case
count. The pattern file
is table-driven, so its ROW is its battery; this self-test is a sequence
of `expect()` calls,
so a **section** is the battery: each opens with `battery('name')` and
every `expect()` after
it registers against that name. Registration is the first statement of
`expect()`, before the
line prints and before the failure counter moves, so the floor asserts
REACH — a case that
runs and fails still registers, and only a case that never runs goes
missing.
- `SELF_TEST_BATTERY_FLOOR` — the roster's own size (13), so deleting an
entry cannot silence
its floor.
- `selfTestReachedVerdict` — set after the verdict line prints, refused
at the dispatch in
`main()`. Worth spelling out here because the exit code travels further
in this file than in
the pattern: `selfTest()` returns a number, `main()` returns it,
`process.exit(main(...))`
reports it, and a 0 rides that whole path just as happily when it comes
from a `return`
placed above the verdict.
Two floors are deliberately below their live count, and the roster
comment says why: the two
workspace-direction batteries generate one case per workspace root (11
each today), and adding
or removing a workspace package is ordinary work that must not red this
file. Their floor of 1
pins REACH — the silently-empty enumeration this file's own docblock
names — not the size of
the workspace.
### The two vacuous cases now discriminate
- `compliant by TYPE: name, describe and type all agree` asserted
`rulesOf(...).join() === ''`.
A key OUTSIDE the population yields `''` too, so with the type channel
ablated away it read
green — "compliant" was indistinguishable from "absent". It now reads
the site: duration type,
type units, key units, prose units, admission and verdict. This is the
tightening PR #18486
already landed twenty lines above it, applied to the case it left
behind.
- `the declared duration units are units this reader actually knows` was
an `every` over a
possibly-empty map, vacuously true in exactly the ablated run where the
two coupling cases
stopped existing. Non-emptiness is asserted first; how many there must
be is the floor's job.
A negative control was added beside it, in this file's own convention,
so the right-hand side
is known to be able to answer false.
## LIT — the floor fires, and the cases that were vacuous go red
Every ablation ran from the committed fix, under a trap that restores on
EXIT/INT/TERM, each
mutation proven on disk by occurrence count before the run and each
restore proven afterwards by
`git hash-object` equality with the HEAD blob plus an empty `git diff
HEAD`.
| ablation | on the PRE-FIX file | on this branch |
|---|---|---|
| `DURATION_ROOTS` emptied (the card's item 3) | 95 green + 6 red =
**101 registered**, no floor, no count printed | 94 green + 8 red + **2
floor lines**, `102 case(s) registered` printed |
| — the case `compliant by TYPE: name, describe and type all agree` |
**green** (vacuous) | **red** |
| — the case `the declared duration units are units this reader actually
knows` | **green** (vacuous) | **red** |
| one `battery()` opener deleted (de-register a battery) | n/a | red:
battery `the walk's exclusions, pinned BEHAVIOURALLY (#15682)` **DID NOT
RUN — 0 cases registered, 2 pinned** |
| one roster entry deleted | n/a | red twice: roster **declares 12,
below the pinned 13**, and its cases are **attributed to no declared
battery** |
| a `return 0;` inserted above the verdict | exit **0**, **zero bytes**
of output | exit **1**, the dispatch refusing: "selfTest() returned
without reaching its verdict" |
The floor's text names how many are missing, which is what the card
asked for:
```
self-test floor: self-test battery "the duration vocabulary is really exported, and its units
are ones this reader knows" registered 2 case(s), below its pinned floor of 4 — 2 case(s) that
used to run no longer do.
```
## DARK — the gate's verdicts do not move
The non-`--self-test` leg, run before and after on the same tree, and
again after merging
`origin/main`:
```
✓ check:duration-unit-keys — 203 unit-declaring numeric key(s) across 2520 source file(s) all
carry their unit in the key name (or in a sibling `unit`, or under a declared exemption: 6
declared duration type(s) (`DurationMs`/`DurationSeconds`), 15 declared `EpochMs` instant(s),
13 declared `externalVocabulary` mirror(s), 0 declared `dimensionless` number(s)); zero
offenders, no baseline.
```
All four captures are byte-identical, sha256
`6a62415e9609b578e4228b7336f04a69c25c91d15fa870eec154a9d7c9540ff5`.
## Count leg — 103 to 104, enumerated
The roster is a partition of the cases that already ran; adding it
registered no case and
removed none. Exactly **one** case is new:
- `negative control: a unit this reader does NOT know is absent from
UNIT_SPELLINGS` — the
control for the de-vacuumed `every` above.
The other two touched cases are rewrites in place, not additions:
`compliant by TYPE` and
`the declared duration units are units this reader actually knows` each
still print one line.
The verdict line now prints that number instead of leaving it to a
hand-tally:
```
self-test: all cases pass — 104 case(s) across 13 batteries, every battery at or above its pinned floor
```
Printing the count is a fourth thing the card's item 1 did not name, so
it is argued rather than
slipped in: the pattern being copied prints one on its own verdict line,
so omitting it would
have been a deviation from the copy; AGENTS.md's objection is that a
count is "EVIDENCE, NOT
PROOF", and the proof it asks for lands in the same commit; and a landed
sibling
(`scripts/pm/changeset-deadline-census.mjs`) already prints "all cases
passed across N
batteries". It moves no verdict.
## Acceptance notes
Noted, not filed:
- The card's "185 rows / 0 offenders" is stale: the census reads 203
keys across 2520 files
today. Nothing is wrong with the gate; the figure was taken at the
filing head.
- Sibling gates share the gap, named here for a successor card rather
than touched: of the 13
`--self-test`-carrying scripts wired in `packages/spec/package.json`
that live in this
package, **12 carry neither a roster nor a verdict flag** —
`build-declaration-map.ts`,
`build-export-origins.ts`, `build-migration-registry.ts`,
`check-browser-reachable-entries.ts`,
`check-dual-source-exports.ts`, `check-error-code-provenance.ts`,
`check-exported-any.ts`,
`check-llms-txt.ts`, `check-objectui-pin-citations.ts`,
`check-skill-examples.ts`,
`check-template-manifests.ts`, `check-yaml-examples.ts`. This one is now
the thirteenth.
Untouched, as fenced: the agreement carve-out and its two cases labelled
`DEFERRED to #18075`,
the `dimensionless` renderer half (#18500), and every line under
`packages/spec/src/`.
## Verification
- `pnpm --filter @objectstack/spec typecheck` — exit 0 (includes
`check:scripts-typecheck`,
the program that type-checks this file).
- `pnpm --filter @objectstack/spec test` — exit 0, 487 test files /
14125 tests passed.
- `node scripts/pm/dispatch-gates.mjs --commands --repo
objectstack-ai/objectstack` — 54
families derived, all 54 run with exit codes landed to disk, then
reconciled with `--ran`:
54 accounted for, 0 unrun. 50 exit 0; 4 exit **3 = PREREQUISITE NOT
MET** in a worktree with
no `dist/` (`check:dts-closure`, `check:dual-build-cjs-loads`,
`check:lean-entry-closure`,
`check:sourcemap-no-sources-content`) — by those gates' own words
neither a pass nor a
failure, NOT MEASURED here, and CI builds before running them.
- `skip-changeset` measured rather than assumed: `npm pack --dry-run` on
`@objectstack/spec`
lists 275 entries; **zero** under `scripts/` and zero naming this file,
with README.md and
202 `src/**/*.zod.ts` present as the positive control. The tarball does
not move.
---
_Generated by [Claude
Code](https://claude.ai/code/session_01JbZnqu8bt6YqfJsr9vaFb3)_
Co-authored-by: Claude <noreply@anthropic.com>1 parent 02bdeaa commit 02f8e45
1 file changed
Lines changed: 211 additions & 6 deletions
0 commit comments