test(spec): a battery roster, floor and verdict handshake for check-duration-unit-keys - #18916
Merged
os-bill merged 2 commits intoSep 18, 2026
Merged
Conversation
… verdict handshake
`packages/spec/scripts/check-duration-unit-keys.ts` predates the self-test
battery discipline: no roster, no per-battery floor, no module-level verdict
flag. Measured on this file, not supposed — emptying `DURATION_ROOTS`
DE-REGISTERS the two per-root module-coupling cases the loop generates, and the
run goes 103 checkmarks to 101, every one of them green. The self-test printed
no case count at all, so even the hand-tally clue was absent.
Copies the three parts from `scripts/check-agent-model-declared.mjs` (⛔ never
imports them — every self-test keeps running standalone), with the TypeScript
surface of the sequential port in `scripts/check-test-typecheck.mts`:
- `SELF_TEST_BATTERIES` — 13 declared batteries, name → minimum case count.
A battery here is a SECTION, opened with `battery('<name>')`; every
`expect()` registers against the one most recently opened, as its first
statement, so the floor asserts REACH rather than success.
- `SELF_TEST_BATTERY_FLOOR` — the roster's own size, so deleting an entry
cannot silence its floor.
- `selfTestReachedVerdict` — set after the verdict prints, refused at the
dispatch in `main()`. An exit code is not a handshake: this file's 0 rides
`selfTest()` → `main()` → `process.exit()` unchanged.
Two vacuous cases now discriminate:
- `compliant by TYPE: name, describe and type all agree` asserted
`rulesOf(...).join() === ''`, which a key OUTSIDE the population satisfies
too — green with the type channel ablated away. It now reads the site:
duration type, type units, key units, prose units, admission and verdict.
- `the declared duration units are units this reader actually knows` was an
`every` over a possibly-empty map — vacuously true in the same ablated run.
It now asserts the vocabulary is non-empty first, and a negative control
pins that the right-hand side can answer false.
The gate's verdicts do not move: the non-`--self-test` leg's output is
byte-identical before and after (203 unit-declaring keys, 0 offenders).
Claude-Session: https://claude.ai/code/session_01JbZnqu8bt6YqfJsr9vaFb3
Co-authored-by: Claude <noreply@anthropic.com>
Contributor
📓 Docs Drift CheckNothing in this diff resolved to a documentable surface (no symbol, route or SDK anchor derived from 0 changed package(s)), so this run has no opinion about the docs. What this run could not see
Coarse fallback — 0 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): |
os-bill
marked this pull request as ready for review
September 18, 2026 05:55
os-bill
deleted the
claude/issue-18512-duration-unit-keys-selftest-floor
branch
September 18, 2026 06:42
This was referenced Sep 18, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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 batterydiscipline: no battery roster, no pinned per-battery minimum, no module-level verdict flag.
Re-run first-hand at
origin/main84ba4a8, not inherited:tsx scripts/check-duration-unit-keys.ts --self-test, untouchedself-test: all cases passDURATION_ROOTSemptiedTwo corrections to the card's own wording, both measured here:
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.
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 importedCopied 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 overObject.freeze,a
battery()opener, anUNATTRIBUTED_BATTERYkey). No symbol is imported from either.SELF_TEST_BATTERIES— 13 declared batteries, name to minimum case count. The pattern fileis 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 everyexpect()afterit registers against that name. Registration is the first statement of
expect(), before theline 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 silenceits floor.
selfTestReachedVerdict— set after the verdict line prints, refused at the dispatch inmain(). Worth spelling out here because the exit code travels further in this file than inthe 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
returnplaced 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 agreeassertedrulesOf(...).join() === ''.A key OUTSIDE the population yields
''too, so with the type channel ablated away it readgreen — "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 spec(gate): check-duration-unit-keys admits by declaration — retire the name-shape token list #18486
already landed twenty lines above it, applied to the case it left behind.
the declared duration units are units this reader actually knowswas aneveryover apossibly-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-objectequality with the HEAD blob plus an emptygit diff HEAD.DURATION_ROOTSemptied (the card's item 3)102 case(s) registeredprintedcompliant by TYPE: name, describe and type all agreethe declared duration units are units this reader actually knowsbattery()opener deleted (de-register a battery)the walk's exclusions, pinned BEHAVIOURALLY (#15682)DID NOT RUN — 0 cases registered, 2 pinnedreturn 0;inserted above the verdictThe floor's text names how many are missing, which is what the card asked for:
DARK — the gate's verdicts do not move
The non-
--self-testleg, run before and after on the same tree, and again after mergingorigin/main: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— thecontrol for the de-vacuumed
everyabove.The other two touched cases are rewrites in place, not additions:
compliant by TYPEandthe declared duration units are units this reader actually knowseach still print one line.The verdict line now prints that number instead of leaving it to a hand-tally:
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 Nbatteries". It moves no verdict.
Acceptance notes
Noted, not filed:
today. Nothing is wrong with the gate; the figure was taken at the filing head.
--self-test-carrying scripts wired inpackages/spec/package.jsonthat live in thispackage, 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
dimensionlessrenderer half (#18500), and every line underpackages/spec/src/.Verification
pnpm --filter @objectstack/spec typecheck— exit 0 (includescheck: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— 54families 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 afailure, NOT MEASURED here, and CI builds before running them.
skip-changesetmeasured rather than assumed:npm pack --dry-runon@objectstack/speclists 275 entries; zero under
scripts/and zero naming this file, with README.md and202
src/**/*.zod.tspresent as the positive control. The tarball does not move.Generated by Claude Code