fix(scripts): doc-authoring self-test pins its batteries by NAME, so a battery that stops running is named rather than uncounted - #13487
Merged
os-project-manager merged 2 commits intoAug 30, 2026
Conversation
… by a bare pass
`selfTest()` decided success as `failures.length === 0` with no assertion-count
floor anywhere, so "every case held" and "the cases never ran" produced the same
output: an early return at the top of `selfTestRule3()` left that whole battery
unrun and the script still exited 0, printing the verdict line asserting those
cases hold — in the gate that exists to catch prose claiming more than the code
delivers.
Every battery now declares itself with `battery('<name>')`, every `expect()` is
attributed to the battery most recently opened, and the floor requires the
OPENED set to equal the DECLARED set with each battery at or above its own
pinned count. A set difference names WHICH battery stopped running; a count says
only that something did. A bare non-zero total would not have caught the 40->3
case the card warns about, and a pinned TOTAL rots as soon as a sibling grows.
`main()` no longer does `return selfTest()`: `selfTest()` returns a verdict
sentinel only after the floor has been evaluated and the success line printed,
so a `return` that leaves the function early is refused instead of exiting 0
with no output at all.
Audited before designing: 16 batteries, 198 assertions, and every static
`expect()` call site in the file executes today — no battery was already dark.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Pk26oZ12t5N1hwGW1m1MgC
…ery's count Deleting an entry from SELF_TEST_BATTERIES silences that battery's floor exactly as effectively as zeroing its count, so the registry size is pinned too — the same shrink-only shape, with the same MAINTAINER-ONLY refusal beside it. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Pk26oZ12t5N1hwGW1m1MgC
os-project-manager
marked this pull request as ready for review
August 30, 2026 14:49
os-project-manager
enabled auto-merge
August 30, 2026 14:49
os-project-manager
deleted the
claude/issue-13173-doc-authoring-self-test-floor
branch
August 30, 2026 15:13
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 #13173
selfTest()inscripts/check-doc-authoring.mjsdecided success asfailures.length === 0with no assertion-count floor anywhere, so the successcondition and the total-failure condition produced identical output — in the
gate that exists to catch prose claiming more than the code delivers.
The mechanism, re-measured on current
main(not quoted from the card)The file changed three times the day the card was filed, so the experiment was
re-run rather than cited. On
8fcd0816, an earlyreturninjected at the topof
selfTestRule3():Exit 0, with the full success line asserting those cases hold. Premise confirmed.
The audit, done before anything was designed
The card recorded that no audit of the current registrations had ever been made.
One was made here, by instrumenting
expectto log its call site and diffingevery executed call site against every static one:
selfTest72 ·selfTestRule3108 ·selfTestPackagesProse18)expect()call sitesThe one static hit that does not execute is
expect(issue.message).toContain(...)inside a fixture string — text, not a call site.
So the PM hypothesis that at least one battery was already dark is FALSIFIED:
every battery runs today. That is what let the floor be pinned to today's real
counts rather than to a guess.
The repair: registered NAMES, per battery
Not a count. A count answers "something moved"; the card's own 40-to-3 warning
needs "which battery moved", and a pinned TOTAL rots the moment a sibling
grows. So this takes the shape the sibling
check-dispatcher-error-vocabularygate landed hours earlier — the registered names, held as a set:
SELF_TEST_BATTERIESdeclares 16 batteries, each with its own case floor.battery('...'); everyexpect()is attributed to thebattery most recently opened. An assertion with no battery open is filed under
a key that is not declared, so it reds by the same set difference instead of
inflating whichever battery ran last.
battery at or above its own count, and it is evaluated before the verdict —
so that line can only be printed by a run in which every battery ran.
exactly as effectively as zeroing it.
main()no longer doesreturn selfTest().selfTest()returns a verdictsentinel only after the floor has been evaluated and the success line printed,
so a
returnthat leaves the function early is refused rather than exiting 0with no output at all. Same defect class, one level up; named here because it
is beyond the literal "assertion-count floor" the card asked for.
Counts are a floor, not an equality: adding cases must never red. Falling
below one means cases stopped running — the battery is the bug, and the failure
text says so, with lowering a floor marked
⛔ MAINTAINER-ONLYbeside theRATCHET_AUTHORITY_MARKERthis file already carries.The ablations — the floor NAMES the battery, five ways
Every mutation proven on disk by
git hash-objectagainst the HEAD blob (neverby an editor's exit code), every restore proven byte-identical with
git diff HEADempty.returnat the top ofselfTestRule3()DID NOT RUN — 0 cases registered, N pinnedbattery "published-catalog id precision" registered 14 case(s), below its pinned floor of 15mustPasscut from 12 entries to 1registered 4 case(s), below its pinned floor of 15returnat the top ofselfTest()itselfselfTest() returned without reaching its verdictdeclares 15 batteries, below the pinned 16andbattery "spec text: precision" registered 18 case(s) but is not declaredBefore this PR every one of those exits 0 with the full success line.
Verification
Both legs, on the final commit
30cae64d— the CI step is--self-test &&themain run, so both are quoted:
Gate family derived from the real changeset with
node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack(13path-matched + 2 convention-triggered for editing a gate script), all run on
30cae64d, all exit 0:check:doc-authoring·check:agent-test-spelling·check:bash32-floor·check:cli-command-ids·check:cross-package-test-inputs·check:entry-guard·check:parse-guard·check:pnpm-filter-targets·check:watch-hint-literal·check-ci-filter-parity·check-cross-package-test-inputs·check-shard-attestation·pm/bare-root-worklist --self-test·check:pm-dispatch-gatesAdded beyond the derivation because this diff plainly implicates them, both
exit 0:
check:nul-bytes—OK (scanned 7451 text file(s) ... no raw ASCII control bytes),plus a direct
grep -naPover the edited file: no control bytes.check:ratchet-remedy-authority— this file is classifiedmarkedin thatgate's control corpus, and the new refusal text is a second marked offer:
OK check-ratchet-remedy-authority: 170 scripts swept; 10 mark the expanding remedy ⛔ MAINTAINER-ONLY ... Control corpus: 24 hand-classified scripts, set-equality audited both ways.check-test-completeness.mjsexits 3PREREQUISITE NOT METlocally by design(it grades a saved
turbo run testlog). Recorded NOT MEASURED — neitherred nor green.
No test suite in the repo pins this script (
git grepover*.test.ts/*.spec.tsforcheck-doc-authoring: zero hits), so no pin suite is owed.Lint, narrowed with the narrowing proved rather than run repo-wide:
what counts — the file resolves to a real config and is linted, not ignored.
--format json: 1 file, 0 errors, 0 warnings.eslint.config.mjswhichnever enables type-aware linting for any file (no
parserOptions.project,no typed
@typescript-eslintrules) — stated in that config's own headerwith a measured positive control. A change confined to one file therefore
cannot move any untouched file's verdict.
CI runs the full farm regardless; 166 of the 187 discovered families sit behind
no path filter at all.
Not in scope
Whether other
--self-testimplementations inscripts/share this shape isstill an open survey, deliberately untouched here — that is the class card's
question, not this one's, and this PR does not address it.
No changeset:
scripts/-only, publishing nothing from any package, matching thetwo most recent PRs to this same file, which both carried
skip-changesetandno changeset file.
Authored by Claude Code, dispatch session
session_01Pk26oZ12t5N1hwGW1m1MgC(https://claude.ai/code/session_01Pk26oZ12t5N1hwGW1m1MgC) — recorded in prose
because a body edit rewrites the session-URL footer to its bare form.
Generated by Claude Code