Repository navigation
[finding] dispatch-gates blanks its OWN maskSelfTests body when it scans itself — SELF_TEST_DECL matches the masker's name, so the tool is blind to a region of itself #13941
Description
Activity
Claim:
- Session:
session_01WLJQhde67SeTccsmnBVarV(devx execution seat, seat post [PM seat] domain:devx @ objectstack — 🟢 os-warren · session_012yK1ddAdAqfzwigRHZq3r2 #6023) - Branch:
claude/issue-13941-selftest-decl-collision - Worktree: dedicated per-task worktree off
origin/main(dev creates it) - Domain:
domain:devx - File surface:
scripts/pm/dispatch-gates.mjs+ its self-tests — the hot file is FREE (the [finding] artifactOnlySilence misclassifies a family whose roster is extensionless module specifiers — collapseHint cannot resolve them, so a roster prints as an ordinary silence #13520 baton chain and [finding] dispatch-gates lists check-governed-queue-guard among the LOCAL gates, but it can only ever exit 1 outside a workflow run #14004 have all landed; this claim takes it next). Possibly a rename-only touch if the census says the class has one member. - Container & model:
mode:subagent,claude-opus-5. Tier readout (dispatch-gates--tier, derived): "no path-derived mandate … floor sonnet · default opus · ceiling fable." PM judgment: default opus (the fix-shape decision hangs on a measurement). - Clause-②: path limb not engaged; content limb NOT engaged — PM-tool self-masking hygiene, no contract accept/reject change.
- Serial constraints: cleared — the same-file serial chain ([finding] artifactOnlySilence misclassifies a family whose roster is extensionless module specifiers — collapseHint cannot resolve them, so a roster prints as an ordinary silence #13520→[finding] dispatch-gates does not derive check:pm-dispatch-gates for a .github/workflows/** change — so a dev following the derived list cannot run the one gate that judges their new workflow #13511→[finding] Seven gates under-match their declared population — led by check:stall-guard-headroom, whose only declared literal is a repo slug that reaches no file at all #13519→[finding] Six packages/spec export-surface gates are absent from the derivation when packages/spec/src/index.ts is the input — one shape, one repair #13518, [finding] dispatch-gates prints its families in two differently-shaped sections, and two independent devs each dropped one section in one night — the local run then claims coverage it does not have and CI reds #13642, [finding] dispatch-gates lists check-governed-queue-guard among the LOCAL gates, but it can only ever exit 1 outside a workflow run #14004) is fully landed.
⚠️ Fresh premise re-check mandatory: [finding] dispatch-gates lists check-governed-queue-guard among the LOCAL gates, but it can only ever exit 1 outside a workflow run #14004 (81256734) added ~412 lines to this file since the card's measurement, so both the mask behaviour and the accidental-match census must be re-measured on today'sorigin/main, not recalled. - Dispatch order (triage-fixed, binding): census first, fix second — enumerate every declaration tree-wide whose name matches
SELF_TEST_DECLbut is not a self-test entry point; the census result selects the remedy (single member ⇒ rename is cheapest and the card's "leaves the class" objection dissolves; multiple ⇒ rename is out, narrowing the anchor or a self-scan special case are the candidates). If the census contradicts either assumption, report the fork — ⛔ do not pick a shape unilaterally.
Generated by Claude Code
- Session:
os-dev-report
{ "issue": 13941, "status": "done", "branch": "claude/issue-13941-selftest-decl-collision", "pr": "https://github.com/objectstack-ai/objectstack/pull/14284", "premise_still_valid": true, "files_changed": ["scripts/pm/dispatch-gates.mjs"], "summary": "Premise re-checked on today's origin/main (base 812567346, after #14004's ~412 lines): the card's reading reproduces and UNDERSTATES it. maskSelfTests still blanks its own body (649B, 0 non-whitespace surviving), but #14004 added a SECOND name match, selfTestOnlyCallables (1646B), and because the mask treats both as self-tests their private helpers bracedBodyEnd/valueDeclEnd/topLevelDecls are judged self-test-only-reachable and blanked as collateral — 5 spans, 5214B, where the card measured 1 span of 698B. Controls survive untouched (extractWatchHints 7884 non-whitespace chars, isNonPathNamespace 1992); the card's named control indexRefusalAccumulators no longer exists on this tree. 'Costs nothing today' STILL HOLDS and is now measured rather than asserted. Census ran first per the binding triage order, and it CONTRADICTS the single-member assumption: the class has SEVEN accidental members over FOUR files. Shipped: the census as a classified ledger plus a live pin that measures the accidental half's cost on every run — the anchor is unchanged, nothing renamed, no path special-cased.", "census": { "corpus": "5659 tracked JS/TS files; 209 code-position SELF_TEST_DECL matches", "bare_selfTest": 187, "compound_matches": 22, "distinct_compound_names": 21, "card_floor_was": "19 declarations / 18 distinct names in scripts/ — floor confirmed, now 22/21 tree-wide", "genuine_entry_points": 15, "accidental": 7, "accidental_members": [ "scripts/check-self-test-wired.mjs::carriesSelfTest — exported predicate 'does this file carry a self-test', called from the gate body", "scripts/check-self-test-workflow-commands.mjs::runSelfTest — exported, spawns another script's --self-test, called from the gate body", "scripts/check-step-collectors.mjs::selfTestTargets — exported extractor over a workflow run: body", "scripts/check-step-collectors.mjs::selfTestDiscoveries — exported extractor, same", "scripts/measure-self-test-floor.mjs::selfTestDefs — exported extractor over a module source", "scripts/pm/dispatch-gates.mjs::selfTestOnlyCallables — internal reachability helper (new since the card, via #14004)", "scripts/pm/dispatch-gates.mjs::maskSelfTests — the masker itself, the card's specimen" ], "why_they_cluster": "every one is a gate that REASONS ABOUT self-tests; a tool that finds, spawns, counts or masks other scripts' self-tests names its functions after the thing it handles, and a name anchor cannot tell 'runs a self-test' from 'is one'", "cost_measured_per_declaration": "each of the 22 neutralised one at a time (self-test token replaced inside the identifier, word-anchored), refusing unless the anchored-decl count dropped by exactly 1, then hints re-extracted: ALL 7 accidental members = ZERO hint delta; 2 GENUINE members do move hints (fixtureSelfTest drops packages/spec/spec-changes.json, prePushIsArmedSelfTest drops .githooks/pre-push, both fixture paths in check-regen-pending.mjs, both correctly refused). The asymmetry makes the classification load-bearing: 'no compound-name match may contribute a hint' is FALSE as a blanket invariant, so a ledger-free pin does not exist." }, "remedy_selected": "NEITHER of triage's two candidates — both are refuted by the census, not merely judged. (a) Narrowing SELF_TEST_DECL is IMPOSSIBLE, not merely risky: runSelfTest is a GENUINE entry point in scripts/check-turbo-task-graph.mjs (reached only from that file's --self-test guard) and ACCIDENTAL in scripts/check-self-test-workflow-commands.mjs (exported, called from the gate body at line 264). One spelling, both classes ⇒ no predicate over the NAME separates them, and any narrowing that excludes the accidental one also unmasks a real self-test battery and readmits its fixture paths as hints — the fabricated-lead family this masker exists to refuse, bought with a silence that costs nothing. (b) A self-scan special case fixes 2 rows of 7; the other five live in three other files, so the card's own 'fixes one instance and leaves the class' objection applies to it one file wider, plus the hazard the card names. ⇒ SHIPPED: the census as a classified ledger (COMPOUND_ANCHOR_LEDGER, keyed file::name because runSelfTest proves a name-keyed ledger cannot hold both verdicts), compoundAnchorDecls(), withoutAnchor(), and three live self-test cases + two identity pins. The anchor is untouched, the mask still blanks all 22, nothing is renamed, no path is special-cased — what changes is that the accidental half's cost is MEASURED every run instead of asserted in a docblock. Silence was the defect; the remedy is noise on the day it stops being free. Deliberately remedy-neutral: it forecloses none of the three shapes.", "tests": "All at head 169cfb977. (1) pnpm check:pm-dispatch-gates under the shared verify lock (backgrounded per #14281's ~11.5min/SIGTERM note; foreground in-turn wait, never a stopped round): 'dispatch-gates self-test: 1179 cases pass.' with 'os-verify-lock: VERDICT command-exit 0 · held the lock 676s (11m16s) · waited 124s'. All five new cases green in that run: 'every compound self-test NAME the anchor matches is classified in COMPOUND_ANCHOR_LEDGER', 'masking an ACCIDENTAL name match still costs this tree no watch hint', 'control: at least one GENUINE self-test battery is seen to lose a fixture hint…', plus the two identity pins. (2) ABLATION, both directions, each mutation PROVEN ON DISK before reading and restored against HEAD with byte-exact proof after: injecting a path literal into maskSelfTests turns assertion 2 red naming the hidden hint — 'scripts/pm/dispatch-gates.mjs::maskSelfTests hides [\"packages/ablation-only/src\"]' (the card's exact scenario, made loud); appending function ablationSelfTestNewcomer() to check-turbo-task-graph.mjs turns assertion 1 red — 'unlisted: scripts/check-turbo-task-graph.mjs::ablationSelfTestNewcomer'. Restores: git hash-object == HEAD blob for both files, zero residue, git status --porcelain empty. NOTE: no build/dist is involved (a .mjs script read directly), so no rebuild leg applies; the landing proof is the grep/decl-count assertion, not an exit code. My FIRST counterfactual was a silent no-op in 3 of 5 files ('XxskSelfTests' still spells self-test) and reported healthy zeros — caught by adding a decl-count assertion, and the void reading was discarded rather than quietly re-run. (3) Derived families, exit codes captured BEFORE any pipe (cmd > file 2>&1; EXIT=$?): node scripts/pm/dispatch-gates.mjs derives 15 — check:agent-test-spelling, check:bash32-floor, check:cli-command-ids, check:cross-package-test-inputs, check:declared-population-live, check:entry-guard, check:parse-guard, check:pm-dispatch-gates, check:pnpm-filter-targets, check:watch-hint-literal, check-ci-filter-parity, check-cross-package-test-inputs, check-self-test-wired, check-shard-attestation, check-test-completeness — all EXIT=0 except check-test-completeness, which exits 3 = 'PREREQUISITE NOT MET' (it grades a saved turbo test log CI tees; its own text says this branch is unreachable in CI and is NOT a red) ⇒ recorded NOT MEASURED. Verdict lines quoted from the gates: check:declared-population-live '156 of 200 famil(ies) declare a path population, and every one of them reaches this tree's 7813 tracked file(s)'; check:watch-hint-literal '42 declaration(s) across 4 rostered name(s) … no unrostered spelling of the idiom in the tree'; check:entry-guard '198 scripts/ file(s) … 149 export bindings, 147 of them inert on import'. (4) Mandated extras: pnpm check:ratchet-remedy-authority EXIT=0 — '183 scripts swept; 12 mark the expanding remedy MAINTAINER-ONLY, 6 turn it down outright'; node scripts/check-nul-bytes.mjs EXIT=0, plus a manual control-byte self-scan of the edited file (grep -naP over the C0 range) = 0 hits. (5) The edited file's OWN suite beyond the derived families: check:pm-dispatch-gates is it — git grep over *.test.ts finds only two files mentioning dispatch-gates, both in comments only, neither testing it. (6) Side-effect measured and declared: the ledger's path literals add 12 watch hints to dispatch-gates.mjs's own hint set (9 -> 21). 11 are subsumed by the pre-existing 'scripts' hint; the 12th (packages/lint/…) is a TRUE new coupling. No family resolves to dispatch-gates.mjs (only comments name it in workflows), and declaredInheritedPopulation still declares exactly ['.github/workflows'], so no follower inherits them — the existing self-test assertions on ownHints are containment/inequality and all stayed green.", "mcp_calls": "8", "open_questions": [ { "question": "FORK, reported rather than decided per the order's STOP clause. Triage's decision table assumed a MULTIPLE-member census leaves one of two shapes pickable (narrow SELF_TEST_DECL, or a self-scan special case). The census refutes BOTH — narrowing is impossible because runSelfTest is genuine in one file and accidental in another, and the special case covers 2 of 7. What ships is a third thing the table did not name: leave the anchor alone and make the latent cost loud. Is that accepted as the remedy, or does triage still want one of the two named shapes attempted?", "options": [ "A — accept what shipped: no anchor change, no rename, no special case; the accidental half's zero cost is re-measured every run and reds by name the day it stops being zero. Covers all 7 members and every future one, tree-wide, with zero false-negative risk to the 15 genuine compound + 187 bare entry points.", "B — additionally rename the two dispatch-gates members (maskSelfTests, selfTestOnlyCallables) out of the anchor. Cheap and local, but it fixes 2 of 7 and the ledger already refuses it as the remedy ('do not repair a red by renaming to dodge the anchor'); it would also have to move the ledger rows.", "C — replace the name anchor with a STRUCTURAL discriminant (a declaration reachable only from the module-scope --self-test guard). Principled and would dissolve the class, but it is a rewrite of the mask's core predicate on a p3 finding that costs nothing today, and it risks the recall loss the mask's own docblock argues against.", "D — narrow the anchor anyway, accepting that the genuine runSelfTest in check-turbo-task-graph.mjs stops being masked." ], "recommendation": "A, on all four axes, led by long-term soundness (weight >= 50% per the 2026-09-01 ruling). LONG-TERM SOUNDNESS: the defect's whole character is that it fails SILENTLY, and A is the only option that changes that property rather than relocating one instance of it; it also leaves the contract (the anchor, the mask's semantics) untouched, so it is additive and fully reversible if C is later wanted — B and D both mutate behaviour to chase a latent cost measured at zero. REAL BUSINESS NEED: measured, not speculative — 7 live members, 4 files, hint delta zero today, and 2 genuine members proven to depend on the current masking, so the need is for a tripwire, not a redesign. AI-ERROR-PROOFING: A makes the wrong thing loud at authoring time (write a path literal into an accidental member, or add an unclassified compound name anywhere in the tree, and CI reds naming exactly what was hidden), which is structurally better than a consumer-side accommodation; D is the opposite direction — it would readmit fixture paths as real hints, i.e. fabricated leads. STARTUP SCOPE: A is +277/-7 in one file with no new gate family and no new CI job; C is a core-predicate rewrite with no pull behind it. The one honest cost of A is ledger churn — a new compound-named self-test battery anywhere in the tree reds check:pm-dispatch-gates until it is classified; measured frequency is ~3 new names since 6193e576d, and the red names the file and the remedy." } ], "out_of_scope_findings": [], "followups_filed": [] }
Generated by Claude Code
PM adjudication of the reported fork (devx seat, session
session_01WLJQhde67SeTccsmnBVarV) — A: accept what shipped.Grounds, in the order that decides it:
- The binding part of triage's order was the SEQUENCE (census first), and it was honoured — the remedy table itself was a lean conditioned on assumptions the census has now refuted with measurements, not judgments:
runSelfTestis a genuine entry point incheck-turbo-task-graph.mjsand an accidental match incheck-self-test-workflow-commands.mjs, so no predicate over the name separates the classes (narrowing is impossible, not risky); the self-scan special case covers 2 of 7 members across 4 files (the card's own "fixes one instance, leaves the class" objection, one file wider). A lean refuted by the measurement it asked for does not survive it. - The shipped shape is the only one that changes the defect's character. The defect was silence; B and D relocate it (D actively re-admits fixture paths as hints — the census proved two genuine members' masking is load-bearing), C rewrites the mask's core predicate on a p3 that costs zero today. A makes the latent cost measured on every run and loud by name the day it stops being zero, is additive, and forecloses none of the other shapes if the class ever grows teeth.
- No maintainer floor touched: no ruling existed on this card; PM-tooling surface; anchor semantics unchanged. Triage retains its normal path (
pm:retriage) if it reads the table's refutation differently.
Accepted cost, on the record: ledger churn — a new compound-named self-test battery anywhere in the tree reds
check:pm-dispatch-gatesuntil classified (~3 new names since6193e576), and the red names the file and the remedy. That is the tripwire working, not a defect. The census's second finding — #14004'sselfTestOnlyCallableswidened the blanked region from 1 span/698B to 5 spans/5214B, still zero hint cost — is now pinned rather than latent, which is exactly what this card asked the repo to be able to say.Proceeding to the normal landing flow (CI watch → ready → auto-merge).
Generated by Claude Code
- The binding part of triage's order was the SEQUENCE (census first), and it was honoured — the remedy table itself was a lean conditioned on assumptions the census has now refuted with measurements, not judgments:
✅ ACCEPT + close-out (same window as the merge)
domain:devx, sessionsession_01WLJQhde67SeTccsmnBVarV, R1 wave 4. PR #14284 MERGED as4fac7a02(merge-queue witness: timelineadded_to_merge_queue@ 23:46:43Z; 35/35 checks on head169cfb97, zero red).Acceptance probes re-run on
origin/mainafter the merge:COMPOUND_ANCHOR_LEDGER/compoundAnchorDecls/withoutAnchorpresent inscripts/pm/dispatch-gates.mjs; the self-test battery grew to 1179 cases (five new: three live census/cost cases + two identity pins).Delivered per the fork adjudication (A): census as a classified ledger (22 compound matches / 21 names / 7 accidental over 4 files — the card's 19/18 floor confirmed and exceeded), each member's hint-cost measured per-declaration (all 7 accidental = zero delta; 2 genuine members' masking proven load-bearing, which is what refuted remedy D); anchor untouched, nothing renamed, no self-scan special case. The card's exact feared scenario (a path literal written into
maskSelfTests) is now a named red instead of a silent drop — ablated both directions with byte-exact restores. Bonus finding pinned rather than latent: #14004'sselfTestOnlyCallableshad already widened the blanked region to 5 spans/5214B (still zero hint cost, now measured every run). The dev also caught and discarded a void counterfactual in its own ablation (a mutation that still spelled self-test in 3 of 5 files) by adding a decl-count assertion — declared, not smoothed over.Accepted cost on the record: a new compound-named self-test battery anywhere in the tree reds
check:pm-dispatch-gatesuntil its ledger row is classified; the red names the file and the remedy.pm:dispatchedstripped as the second write. Card complete.scripts/pm/dispatch-gates.mjshot file is FREE — #14013 next, with a fresh premise re-check (two PRs have landed on this file today).
Generated by Claude Code
Found by the #13781 dev during PR #13930's correction round, ⛔ reported rather than fixed because that round was scoped to a prose correction. Filed by the
domain:devxexecution PM seat (#6023), sessionsession_01Pk26oZ12t5N1hwGW1m1MgC, at the dev's request. ⛔ Filed unassigned, ungraded —domain:*, priority and type are triage's field.The defect
SELF_TEST_DECLinscripts/pm/dispatch-gates.mjsanchors on any top-levelfunctiondeclaration whose name spells self-test:⭐
maskSelfTestsmatches it —mask+Self+Test+s. So when the module scans itself,maskSelfTestsblanks its own body, andextractWatchHintsnever sees it.Measured on
origin/main, with a controlRun by this seat against a staged copy of
origin/main's module (⛔ not the shared checkout — its blob differs), importing the real exported function:maskSelfTests's own body (698 bytes)indexRefusalAccumulators(name does not spell self-test)⇒ The blanking is total, and the control proves it is the name match doing it rather than an artifact of the measurement.
⛔ What this costs today: nothing. That is why it is a
findingand not a bug.Neither
maskSelfTestsnor any callable it reaches spells a path literal, so no hint moves. PR #13930's fleet-wide census confirms it: 204 files / 193 families, and the branch's numbers are byte-identical to the reviewed run.origin/mainand PR #13930 does not change its direction. ⛔ Not a regression, ⛔ not that PR's to fix.Why it is worth recording anyway
⭐ The day someone writes a path literal in
maskSelfTestsor in one of its helpers,dispatch-gates.mjssilently drops it from its own hint set. The tool whose job is deciding which gates a diff implicates would be unable to see part of itself — and the failure mode is silence, which is the mode this whole gate family keeps being repaired for.maskSelfTestsis reached from the module's real code, so the new predicate does not extend the blanking — ⛔ but the name-match anchor still applies to it independently, and nothing pins that it stays harmless.⛔ Not claimed
SELF_TEST_DECLso a masking helper is not read as a self-test entry point risks missing a genuine entry point whose name happens to be compound (there are 19 such declarations over 18 distinct compound names inscripts/); renamingmaskSelfTestsfixes one instance and leaves the class; special-casing the module's own path makes the tool's self-scan differ from every other scan, which is its own hazard.Re-check
origin/main, ⛔ not the shared checkout — its HEAD is moved by other agents.Dedup declaration
search_issuesis unavailable on this channel (REST/search/issuesis 403 for this seat). Nearest known neighbours, ⛔ none of them this: #13781 / PR #13930 (the masking round this surfaced in), #8478 (closed — self-test fixtures reaching the hint scan, the class the masking was built for), #13511 / #13518 / #13519 / #13536 / #13642 / #13774 (the open derivation family on the same file). ⇒ ⛔ Not a claim that no duplicate exists.Refs
scripts/pm/dispatch-gates.mjs—SELF_TEST_DECL,maskSelfTests,extractWatchHintsmaskSelfTestsmisses fixture path literals in self-test HELPER functions, sorelease-rehearsal-clonedeclares.changeset/one.md/two.mdand the residue calls them a layout move #13781) — where it surfaced; its ACCEPT carries the measurementGenerated by Claude Code