Repository navigation
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 was referenced Aug 31, 2026
baozhoutao
pushed a commit
that referenced
this pull request
Sep 3, 2026
Batch 1b of #13799: the PR #13487 roster-floor shape transplanted onto the `scripts/**` self-tests whose assertion sink is NOT a block-bodied helper inside the self-test body -- a concise arrow, or a module-scope function -- so batch 1 (PR #14851) could not transplant it verbatim. Per the batch-1 review ruling, the roster machinery lives at MODULE scope (SELF_TEST_BATTERIES / SELF_TEST_BATTERY_FLOOR / UNATTRIBUTED_BATTERY / battery() / registerCase() / selfTestFloorFailures()), and each file's existing assertion sink is given a minimal block body that calls registerCase() and returns the original expression unchanged. No case is rewritten, none is reordered, and no assertion changes meaning: all 18 self-tests exit with the same code and byte-identical output before and after. What is pinned is the registered NAMES, not a total: every existing section banner opens a battery, every assertion is attributed to the battery most recently opened, the floor requires the OPENED set to equal the DECLARED set with each battery at or above its own count, and the roster's own size is pinned so deleting an entry cannot silence a floor quietly. One battery is pinned at its structural invariant rather than at today's count, with the reason written over the entry: check-plugin-teardown-shape's exclusions battery runs exactly one case per DELIBERATELY_EXCLUDED row, and promoting a name onto the teardown roster is a legitimate edit that shrinks that list. Census (`node scripts/measure-self-test-floor.mjs --json`): ROSTER 3 -> 21, NONE 158 -> 140; the set of files whose class changed equals this worklist exactly. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015k1DVWthZyPS7xi1Q72YpK
baozhoutao
pushed a commit
that referenced
this pull request
Sep 3, 2026
Batch 1b of #13799: the PR #13487 roster-floor shape transplanted onto the `scripts/**` self-tests whose assertion sink is NOT a block-bodied helper inside the self-test body -- a concise arrow, or a module-scope function -- so batch 1 (PR #14851) could not transplant it verbatim. Per the batch-1 review ruling, the roster machinery lives at MODULE scope (SELF_TEST_BATTERIES / SELF_TEST_BATTERY_FLOOR / UNATTRIBUTED_BATTERY / battery() / registerCase() / batteryFloorFailures()), and each file's existing assertion sink is given a minimal block body that calls registerCase() and returns the original expression unchanged. No case is rewritten, none is reordered, and no assertion changes meaning: all 18 self-tests exit with the same code and byte-identical output before and after. What is pinned is the registered NAMES, not a total: every existing section banner opens a battery, every assertion is attributed to the battery most recently opened, the floor requires the OPENED set to equal the DECLARED set with each battery at or above its own count, and the roster's own size is pinned so deleting an entry cannot silence a floor quietly. None of the introduced helpers is named with a self-test spelling. That is deliberate and recorded beside them: `check:pm-dispatch-gates` anchors on a top-level declaration whose NAME spells self-test, and every such name owes a row in that gate's COMPOUND_ANCHOR_LEDGER. These helpers hold no fixtures to mask and read no path literal, so the accurate name is the one that says `battery`. One battery is pinned at its structural invariant rather than at today's count, with the reason written over the entry: check-plugin-teardown-shape's exclusions battery runs exactly one case per DELIBERATELY_EXCLUDED row, and promoting a name onto the teardown roster is a legitimate edit that shrinks that list. Census (`node scripts/measure-self-test-floor.mjs --json`): ROSTER 3 -> 21, NONE 158 -> 140; the set of files whose class changed equals this worklist exactly. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015k1DVWthZyPS7xi1Q72YpK
baozhoutao
pushed a commit
that referenced
this pull request
Sep 3, 2026
Batch 1c of #13799: PR #13487's roster-and-floor shape, exactly as batch 1 landed it, transplanted onto the `scripts/**` self-tests that already carry its shape -- a block-bodied assertion helper inside the self-test body, at least two named section banners, and a body-level verdict site. Per file: a module-level frozen `SELF_TEST_BATTERIES` roster of NAMES with a per-battery floor, `SELF_TEST_BATTERY_FLOOR` pinning the roster's own size, `UNATTRIBUTED_BATTERY`, a `battery('<banner text>')` opener at each existing section banner, one `registerCase()` at the head of the existing assertion helper, and the floor evaluated immediately before the existing verdict so its messages enter the file's own failure sink. 432 batteries, 2591 cases. No case is rewritten, none reordered and no assertion changes meaning: every file's `--self-test` exits 0 with byte-identical stdout AND stderr before and after. The roster block goes after the import block in every file, which is TDZ-safe by construction. 15 of the 45 files the dispatch named do not meet that criterion when measured against the tree and are left for a later batch rather than improvised; the reason per file is in the PR body. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01YWBXxmS9sXHoqnozSNwHnE
baozhoutao
pushed a commit
that referenced
this pull request
Sep 3, 2026
Part of #13799 (batch 2, Tier B — the batch-1 shape). PR #13487's roster-and-floor shape as batches 1/1b/1c landed it, transplanted onto four `scripts/**` self-tests that already carry that shape. ⛔ No case is rewritten, none reordered, no assertion changes meaning. ⛔ No TOTAL is pinned — the roster pins registered NAMES. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012zGPuVVX3deAx9LdjK8jCk
This was referenced Sep 3, 2026
akarma-synetal
pushed a commit
to akarma-synetal/framework
that referenced
this pull request
Sep 9, 2026
…f-tests (objectstack-ai#14851) * test(scripts): pin a battery roster floor on 40 self-tests `failures.length === 0` was the only success condition in these gates' self-tests, so "every case held" and "the cases never ran" printed the same line. Transplants the PR objectstack-ai#13487 shape validated on check-doc-authoring and carried to check-self-test-wired / check-self-test-workflow-commands in PR objectstack-ai#13797: every section opens with `battery('<name>')`, every assertion is attributed to the battery most recently opened, and a floor evaluated before the verdict requires the OPENED set to equal the DECLARED set with each battery at or above its own case count. The roster's own size is pinned too, so deleting an entry cannot silence a floor. Registered NAMES are what is pinned, never a total: a set difference says WHICH battery stopped, a count says only that something did. No case was rewritten — the only edits inside each self-test are the roster wiring (a `battery()` opener per existing section banner, one `registerCase()` line in the assertion helper, and the floor block before the verdict). Floors are the per-battery counts measured on this tree. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018Swfxm6gT9ESmjSS7pqfVw * test(scripts): pin the structural invariant, not today's count, on two ledger-driven batteries Two batteries register one case per row of a list that is meant to shrink: `KNOWN_NUMBER_COLLISIONS` in check-adr-anchors (a resolved collision is deleted, and a stale entry already fails) and `ALLOW` in check-single-authz-resolver (an exemption that exempts nothing is dead weight). A floor at today's count would redden every legitimate removal and train the next author to edit the floor — the one habit these floors exist to prevent. Pinned instead is the part that does not move with the list: the structural cases ran AND at least one row was audited, said in place over each entry. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018Swfxm6gT9ESmjSS7pqfVw * test(scripts): reword the two ledger notes so neither reads as a ratchet-remedy offer The first spelling put `ALLOW` and the words "shrink-only" inside `anchorFor`'s window in check-single-authz-resolver, which flipped that file from `excluded` to `unmarked` under check:ratchet-remedy-authority (and then MISCLASSIFIED against its CONTROL row). Measured, both findings; the gate is green again with the same fact stated without the token. The adr-anchors note is reflowed for readability only. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018Swfxm6gT9ESmjSS7pqfVw * test(scripts): type the roster wiring in the .mts gate scripts/check-test-typecheck.mts is the one .mts file in the batch and it is reached by the root `tsc --noEmit`, so the untyped `battery()` / `registerCase()` / `floorFailure()` insertions added 7 raw errors and drifted the frozen DEBT entry for @objectstack/spec-monorepo upward (26 -> 33, measured). Typing the wiring and widening the roster to `Readonly<Record<string, number>>` puts the count back at its recorded 26 with check:type-check-debt green. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018Swfxm6gT9ESmjSS7pqfVw --------- Co-authored-by: Claude <noreply@anthropic.com> Co-authored-by: claude[bot] <209825114+claude[bot]@users.noreply.github.com>
akarma-synetal
pushed a commit
to akarma-synetal/framework
that referenced
this pull request
Sep 9, 2026
…bjectstack-ai#14872) Batch 1b of objectstack-ai#13799: the PR objectstack-ai#13487 roster-floor shape transplanted onto the `scripts/**` self-tests whose assertion sink is NOT a block-bodied helper inside the self-test body -- a concise arrow, or a module-scope function -- so batch 1 (PR objectstack-ai#14851) could not transplant it verbatim. Per the batch-1 review ruling, the roster machinery lives at MODULE scope (SELF_TEST_BATTERIES / SELF_TEST_BATTERY_FLOOR / UNATTRIBUTED_BATTERY / battery() / registerCase() / batteryFloorFailures()), and each file's existing assertion sink is given a minimal block body that calls registerCase() and returns the original expression unchanged. No case is rewritten, none is reordered, and no assertion changes meaning: all 18 self-tests exit with the same code and byte-identical output before and after. What is pinned is the registered NAMES, not a total: every existing section banner opens a battery, every assertion is attributed to the battery most recently opened, the floor requires the OPENED set to equal the DECLARED set with each battery at or above its own count, and the roster's own size is pinned so deleting an entry cannot silence a floor quietly. None of the introduced helpers is named with a self-test spelling. That is deliberate and recorded beside them: `check:pm-dispatch-gates` anchors on a top-level declaration whose NAME spells self-test, and every such name owes a row in that gate's COMPOUND_ANCHOR_LEDGER. These helpers hold no fixtures to mask and read no path literal, so the accurate name is the one that says `battery`. One battery is pinned at its structural invariant rather than at today's count, with the reason written over the entry: check-plugin-teardown-shape's exclusions battery runs exactly one case per DELIBERATELY_EXCLUDED row, and promoting a name onto the teardown roster is a legitimate edit that shrinks that list. Census (`node scripts/measure-self-test-floor.mjs --json`): ROSTER 3 -> 21, NONE 158 -> 140; the set of files whose class changed equals this worklist exactly. Claude-Session: https://claude.ai/code/session_015k1DVWthZyPS7xi1Q72YpK Co-authored-by: Claude <noreply@anthropic.com> Co-authored-by: claude[bot] <209825114+claude[bot]@users.noreply.github.com>
akarma-synetal
pushed a commit
to akarma-synetal/framework
that referenced
this pull request
Sep 9, 2026
…jectstack-ai#14896) Batch 1c of objectstack-ai#13799: PR objectstack-ai#13487's roster-and-floor shape, exactly as batch 1 landed it, transplanted onto the `scripts/**` self-tests that already carry its shape -- a block-bodied assertion helper inside the self-test body, at least two named section banners, and a body-level verdict site. Per file: a module-level frozen `SELF_TEST_BATTERIES` roster of NAMES with a per-battery floor, `SELF_TEST_BATTERY_FLOOR` pinning the roster's own size, `UNATTRIBUTED_BATTERY`, a `battery('<banner text>')` opener at each existing section banner, one `registerCase()` at the head of the existing assertion helper, and the floor evaluated immediately before the existing verdict so its messages enter the file's own failure sink. 432 batteries, 2591 cases. No case is rewritten, none reordered and no assertion changes meaning: every file's `--self-test` exits 0 with byte-identical stdout AND stderr before and after. The roster block goes after the import block in every file, which is TDZ-safe by construction. 15 of the 45 files the dispatch named do not meet that criterion when measured against the tree and are left for a later batch rather than improvised; the reason per file is in the PR body. Claude-Session: https://claude.ai/code/session_01YWBXxmS9sXHoqnozSNwHnE Co-authored-by: claude[bot] <209825114+claude[bot]@users.noreply.github.com> Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
akarma-synetal
pushed a commit
to akarma-synetal/framework
that referenced
this pull request
Sep 9, 2026
…jectstack-ai#15003) * test(scripts): battery roster and floor for four more self-tests Part of objectstack-ai#13799 (batch 2, Tier B — the batch-1 shape). PR objectstack-ai#13487's roster-and-floor shape as batches 1/1b/1c landed it, transplanted onto four `scripts/**` self-tests that already carry that shape. ⛔ No case is rewritten, none reordered, no assertion changes meaning. ⛔ No TOTAL is pinned — the roster pins registered NAMES. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012zGPuVVX3deAx9LdjK8jCk * test(scripts): battery roster and floor for five more self-tests Part of objectstack-ai#13799 (batch 2, Tier B — the batch-1 shape). ⛔ No case is rewritten, none reordered, no assertion changes meaning. ⛔ No TOTAL is pinned — the roster pins registered NAMES. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012zGPuVVX3deAx9LdjK8jCk * test(scripts): battery roster and floor for four more PM self-tests Part of objectstack-ai#13799 (batch 2, Tier B — the batch-1 shape). ⛔ No case is rewritten, none reordered, no assertion changes meaning. ⛔ No TOTAL is pinned — the roster pins registered NAMES. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012zGPuVVX3deAx9LdjK8jCk * test(scripts): re-read the multi-repo battery floor after the hotcrm merge `origin/main`'s objectstack-ai#14867 change (`hotcrm` added to `GOVERNED_REPOS`) grows the `multi-repo scope (objectstack-ai#9619)` battery from 15 cases to 17: one assertion becomes two, and the new register row adds one more through the section's own loop. Re-read from a probe run rather than assumed — the declared set still equals the opened set (22/22), and the file's self-test output stays byte-identical to `origin/main`'s version of it. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012zGPuVVX3deAx9LdjK8jCk --------- Co-authored-by: Claude <noreply@anthropic.com>
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