Uh oh!
There was an error while loading. Please reload this page.
fix(pm): mask the fixture builders only a self-test can reach - #13930
Conversation
…e bug was live on three files
ACCEPT — The correction landed, and the dev went past what I asked in the right directionI sent this back for one prose fix: the docblock claimed "No self-test in this tree takes a parameter (61 of 61 are
⇒ Stale by roughly a factor of three, in a docblock nobody had reason to doubt. ⭐ And the important half: the two PROPERTIES the anchor actually rests on both HOLD. ⇒ ⭐ That distinction is the whole lesson: a property that still holds and a count that rotted look identical in a comment. Separating them is what makes the docblock survive the next three months. Independently re-derived by this seat, by a different method (a ⭐ An instrument note worth more than the numbersThe dev's first reproduction of my table returned different absolutes — 30/11455, 30/3961, 34/3826, control 28717/28717 — because it composed ⇒ Two compositions of the same measurement gave different absolutes and the same verdict — and the tell that the verdict was robust was that the control read identical in both (28717/28717 and 48044/48044 alike). ⛔ Absolutes are not comparable across compositions; a control that stays identical under both is what makes the direction trustworthy anyway. Recorded, because a reviewer handed only the first table and only the first column could have concluded the two of us disagreed. No behaviour changed in this round — proven, not asserted
⛔ No fixture was added for the three real files, per my instruction — the existing synthetic destructured-signature case already pins the mechanism. The open question — ruled: file itThe dev found, and reported rather than fixed, that this module's own Verified by this seat, with a control: Pre-existing on ⇒ Filed as an observation-class Governed-surface checkDiff is Generated by Claude Code |
Uh oh!
There was an error while loading. Please reload this page.
Fixes#13781
The defect
maskSelfTestsblanks the brace-balanced body of every declaration whose own name matchesSELF_TEST_DECL. A fixture builder that only the self-test calls is named for what it builds —makeSource,buildFixtureTree,stampFor,fixtureCommit— so it never matched, its body survived the mask, and its fixture path literals reachedextractWatchHintsas if they were paths the gate opens.Premise re-derived on this branch's base
73c846687(the card measured it at4301f7846; still live). The card's own command:The four that go are written by
makeSource(L485) into a temp git repo; the three that stay are the population the file really declares.The shape
In-file reachability over top-level declarations, bounded to one file, with the mask applied to callables only.
A top-level
function/classis masked when it is reachable from a self-test body and not reachable from anything else the module does. The roots of "anything else" are the module-body statements outside every declaration (imports, top-level side effects,export { … }lists, the entrypoint guard at the bottom) plus everyexported declaration, which is reachable from outside the file by definition. A self-test body is reached but never traversed — these scripts callselfTest()from module scope, so traversing it would make every helper root-reachable and the predicate vacuous. The second conjunct is the safety half: a helper shared by the self-test and the real gate body stays unmasked.Two mechanics that are not decoration:
The parameter list is skipped before brace counting, and this repairs live files. A destructured default puts braces in the signature, and counting those closes the body before it opens. ⚠ An earlier revision of this PR and of the docblock called that bug latent — "no self-test in this tree takes a parameter (61 of 61 are
selfTest())". That was false, and the correction makes the fix worth more, not less. Three self-test entry points underscripts/carry a brace in their signature onorigin/maintoday:Bytes
maskSelfTestschanges in each file, this module against a staged copy of the one onorigin/main(6193e576d; the three subject files and all three of the module's relative deps are byte-identical between that sha and this branch's base, so the staged copy is an exact control):Thirty bytes is the destructured parameter and nothing else — the entire self-test body was surviving the mask. The control blanking an identical 48044 both ways is what makes those three a reading rather than an artifact. It did not move the census, and that is luck rather than design:
extractWatchHintsreturns identical sets over the three ([],[], and the same 8 hints) because their self-test bodies happen to carry no path literal their module bodies do not already carry. A fourth file with the same signature shape and one fixture path in it would have been a live fabricated lead.The neighbouring
61 of 61 at column 0claim was re-derived rather than assumed: acrossscripts/at6193e576dthere are 169 carrier files and 185 declarations (111function selfTest(, 28export function selfTest(, 19async function selfTest(, 8export async function selfTest(, 19 compound names) — the counts were stale by a factor of three, but the two properties the anchor rests on both hold: 185 of 185 at column 0, and a column-0 scan for the const/arrow spelling finds zero. Both docblock paragraphs now say that, and the paragraph tells the next reader to re-derive the counts rather than quote them. One measured aside recorded there: this module's ownmaskSelfTestsmatchesSELF_TEST_DECL, so the file blanks that function's body when it scans itself — true long before this change, and free only because none of the functions it reaches spells a path.A reference inside a template interpolation is a reference.
scan.interpolationis consulted, and skipping it is not academic:release-rehearsal-clone.mjsnames its own path constant only from inside template literals, so a scan that read${SELF}as string text foundSELFunreferenced and masked away the one hint that file really declares. Measured — it was the first version's behaviour.A declaration whose span cannot be closed is dropped rather than run to end of file, so one unterminated span cannot swallow the declarations after it. The mask-to-end-of-file behaviour a malformed self-test has always had is kept where it lives.
Invariant: the new mask is a strict superset of the old one. Verified positionwise over all 204 scanned scripts: 0 violations, and 158 of the 204 mask byte-identically to
origin/main.Falsification: fleet-wide hint census
Population: every file any discovered family names, plus every first-party module those files import — the exact set
discoverFamiliesrunsextractWatchHintsover. Same tree, same command, onlyscripts/pm/dispatch-gates.mjsdiffering.Covering relation over all 7621 tracked files, per family: 1 covered pair lost, 0 gained. Adjudicated below.
Every removed hint, by the declaration it came from
21 newly masked declarations in 9 files. No removed hint is unattributed.
scripts/pm/release-rehearsal-clone.mjsrunSelfL458,fixtureCommitL470,writeFileL475,makeSourceL485,cloneOfL516.changeset/one.md,.changeset/two.md,.changeset/config.json,.changeset/README.md— all four written bymakeSourceinto a temp reposcripts/check-test-source-alias.mjsfixtureL1950,buildFixtureTreeL1963packages/clocked-decoy,packages/violator,src/thing.ts, …) built under a tempdirscripts/check-type-source-resolution.mjsfixtureL1256,buildFixtureTreeL1273packages/star-trap,packages/unparseable,spec/dist/*, …)scripts/check-skills-token-ratchet.mjsfixtureTreeL547skills/README.md,skills/demo/SKILL.md,skills/demo/rules/nested.md,skills/demo/references/_index.md,skills/demo/evals/deep/deeper/case.md,skills/not-a-skill/notes.mdscripts/check-agent-test-spelling.mjsmakeFixtureTreeL791,baseFixtureFilesL802.claude/agents/os-dev.md,.github/workflows/lint.yml,scripts/keep.sh,skills/x/SKILL.md— names written into the fixture tree, not read from the reposcripts/check-whole-set-label-write.mjswriteTreeL711,baseFilesL720,withTreeL811.github/actions/setup-pnpm/action.yml,.github/workflows/other.ymlpackages/spec/scripts/check-entry-nameability.tswriteFixtureL452packages/spec/scripts/dist/other.d.ts,packages/spec/scripts/dist/other.jsscripts/check-undeclared-dep-imports.mjsfixtureL700,makeTreeL706packages/subject/package.jsonscripts/check-console-injection.mjstmpdirL452,makeSpecPkgL458,makeUnbuiltSpecPkgL477,makeDistL490,stampForL503scripts/assert-console-spec-injection.mjs— thegeneratedByfield of a fabricated stamp objectThree of these read like real paths and are not:
.claude/agents/os-dev.md/.github/workflows/lint.yml(check:agent-test-spelling) are filenames the fixture tree creates under a tempdir. The family's surviving hints still cover both files — 0 covered pairs lost..github/actions/setup-pnpm/action.yml(check-whole-set-label-write.mjs): same, 0 covered pairs lost.scripts/assert-console-spec-injection.mjs(check:console-injection) leaves that file's own hint set empty, but the family keeps the lead through a better-provenanced channel: it is inherited from the imported modulescripts/console-spec-probes.mjs, withhintOriginnaming it. The family's hint set is unchanged.The one covered pair lost, adjudicated
skills/README.mdstops being covered byscripts/check-skills-token-ratchet.mjs. That gate's own source declares the file outside its population, in prose and in a pinned case:The hint came from
fixtureTree(L547), which writesskills/README.mdinto a temp tree precisely to prove the gate ignores it. A card editingskills/README.mdwas being told to run a ratchet that provably does not read it. That is the defect, not a cost of the fix.Zone 2, measured
SELF_TEST_DECL's docblock argues the general case ("a declaration is a thing the language guarantees, a marker comment is a thing an author has to remember"), and it argues it about the anchor, which is what option 1 would replace. A second reason, independent of the quotation: the banner is load-bearing nowhere else, so a missing one reddens nothing — the mask would simply stop reaching, silently, which is the failure family this module exists to refuse.const/let/varwas implemented and measured first. It takes 175 hints from 36 files instead of 104 from 9, and the 71 extra include the declared populations of eight gates:ROOT_DIR_WATCH_HINTS,ROOT_FILE_WATCH_HINTS,ROOT_WATCH_HINTSincheck-doc-anchors,check-driver-conformance,check-doc-authoring,check-doc-formula-expressions,check-cli-command-ids,check-entry-guard,check-corpus-claim-drift,check-examples-live-imports— losing.claude/**,content/**,docs/**,skills/**,packages/drivers/**,examples/**,scripts/**,ARCHITECTURE.md/**. A population declared for this scanner to read is referenced by no executing code; being unreferenced is what such a declaration is, so a reachability rule masks exactly the declarations the extractor exists to see. Value declarations therefore participate in the graph (references through them propagate) but are never masked. The fixture tables this leaves behind (SELF_TEST_CASESand friends) are a deliberate cost: keeping a false hint costs a CI round, dropping a declared population costs a gate.Self-test
13 cases added to
dispatch-gates.mjs's own self-test, in the shape of the existing masking cases. Three of them fail onorigin/mainand pass here; the rest pin the safety half in both directions.Measured against
origin/main's masker, driven directly:Plus: a declaration constant only the self-test names stays a declaration; a reference from inside a template interpolation keeps a helper alive; and the live specimen is pinned in both directions (its fixture changesets are gone,
.changeset/*.mdand its own path survive) so a future edit cannot leave the case green by vacuity.Gates
Derived with
node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --commandsatf1765c325— 14 families, the same 14 as before the docblock correction (diffof the two derivations is empty).comm -23 derived ranis empty; every one was run at that same sha, exit code captured before any pipe.check-test-completeness.mjsexits 3 = PREREQUISITE NOT MET, its own word: it grades a savedturbo run testlog, there is none locally, and its verdict text says explicitly that this is not a red and not a finding. Recorded as NOT MEASURED, not as a pass and not as a failure. CI passes it a log.pnpm check:nul-bytes: OK, 7614 files scanned, 0 raw control bytes. Self-scan of the edited file withgrep -naPover the C0 range: no hits.ESLint, narrowed and declared.
pnpm exec eslint --no-inline-config --format json scripts/pm/dispatch-gates.mjs— 1 file linted, 0 errors, 0 warnings (count read from the JSON, not from a summary line; the file is in the linted population, not ignored). The narrowing is sound because this repo runs oneeslint.config.mjswhich, in its own words at line 328, "never enables type-aware linting (noparserOptions.project, no typed@typescript-eslintrules) for ANY file" — so an edit confined to one file cannot move the verdict on any file it did not touch. CI runs the repo-wide sweep regardless.Changeset
None, deliberately. (The docblock correction is comment-only;
git diff -U0filtered to non-comment lines is empty, and the census re-run atf1765c325is byte-identical to the reviewed one.) The diff is one file underscripts/pm/, publishes nothing from any package, and changes no user-visible behaviour. Precedent surveyed rather than recalled: of the last 12 commits onorigin/mainwhose diff is entirely underscripts/, 12 carried no changeset. Theskip-changesetlabel is applied to this PR so the Check Changeset job reads the opt-out rather than reddening.Scope
maskSelfTestshas four call sites —extractWatchHints(2113),firstPartyImportTargets(2284), the self-test'sINHERITED_POPULATION_MARKERscan (11193), andscripts/pm/bare-root-worklist.mjs(779). All four take source in and offset-preserving blanked source out; the contract is unchanged and all four move in the same direction (fewer fixture literals read as real ones).hintCoversandunreachableReasonare untouched.Generated by Claude Code
Generated by Claude Code
Session: https://claude.ai/code/session_01Pk26oZ12t5N1hwGW1m1MgC
Generated by Claude Code