fix(gates): give check-self-test-workflow-commands the population it says it imports - #15506
Conversation
…says it imports Its header promised "one definition, two gates", but what it imported was the EXTRACTION (collectInvocations, carriesSelfTest, codeOf) -- not the population. It built its own from a private walkScripts anchored at the repo-root scripts/ dir, while check-self-test-wired grew a second population source (the package-local gate lane CI names by path). Two answers to one question, drifting by exactly one file: check-self-test-wired 169 of those are run by 30 workflow(s) check-self-test-workflow-commands 168 script(s) CI runs ship a `--self-test` The missing member is packages/lint/scripts/check-reference-carrier-shape.mjs, which lint.yml runs with --self-test on every pull request. Its output was in no sweep, and nothing said so: every #4690 refusal in that gate fires on an EMPTY population or an empty candidate set, so a population that is complete-minus-one refuses nothing and prints a confident scope line. check-self-test-wired now exports the whole read as collectPopulation() -- the walk, the sources, the workflow corpus, the alias expansion, the package-local admission -- plus refusalFor(), the #4690 floors as a pure function over a completed reading. The workflow-commands gate consumes it and takes NO walk of its own; walkScripts, readdirSync and statSync are gone from that file, and an own-source case pins that they stay gone. A re-derivation that agrees today is one that can stop agreeing with nothing going red on either side, which is precisely how 169/168 got here. Two decisions recorded in the code: isCandidate treats package-local members exactly like root ones -- no lane of its own. The predicate's subject is the file's bytes; where the file sits says nothing about whether its code can print a workflow command, and a lane-specific arm would be a second matching rule with no measurement behind it. Measured cost: the one package-local member carries neither form, so the candidate set is 17 before and 17 after and this change spawns ZERO extra subprocesses today. Deferred price if it ever gains a token: 0.68s wall for that self-test, against ~14s for the 17 already selected. WORKFLOW_DIR stays declared in the workflow-commands gate although nothing there reads it any more. dispatch-gates derives that gate's family by scanning its source for path literals, so deleting it would drop the gate off every card that edits a workflow while its verdict still moves with those files. It is pinned against read.workflowDir in main(), so it is a live coupling rather than a decoration. Derivation for a .github/workflows/lint.yml card is byte-identical before and after. Floors raised for cases ADDED, never lowered: wired 9 -> 10 batteries (59 -> 67 cases), workflow-commands 6 -> 7 (23 -> 31). Fixes #15414 Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012zGPuVVX3deAx9LdjK8jCk
|
ACCEPT — PM seat Reviewed against the three-dot diff
Generated by Claude Code |
⛔ merge queue 构建失败 — 先分诊,再决定要不要重排队列构建 33911238982 红了。队列跑的是全量套件(PR 侧 CI 只跑 affected 子集), 失败的 job(日志抽取,best effort):
跨 PR 相同签名(24h,按失败测试文件聚合):
历史信号:
分诊清单:
Generated by Claude Code · merge-queue-triage workflow (#4859) |
|
Queue eviction, not this PR's — PM seat #15506 was removed from the merge queue at 19:48:38Z: its merge-group run 33911238982 (on — Generated by Claude Code |
Fixes #15414
check-self-test-workflow-commandssaid in its own header that membership "is not re-derived: it is imported fromscripts/check-self-test-wired.mjs... one definition, two gates". What it actually imported was the extraction (collectInvocations,carriesSelfTest,codeOf). The population it built itself, from a privatewalkScriptsanchored at the repo-rootscripts/dir — while the sibling gate had grown a second population source (the package-local gate lane CI names by path, #15342). Two answers to one question, and they drifted.Before / after, both gates' own scope lines
Measured at
a06faebbe(base) andd74490f5c(this branch), on the same tree.Before
After
169 vs 168 becomes 169 vs 169. The one member that was missing is
packages/lint/scripts/check-reference-carrier-shape.mjs, computed as the exact set difference, not inferred from the counts:lint.ymlruns that gate with--self-teston every pull request, and its output was in no sweep. Nothing said so, because every#4690refusal in that gate fires on an empty population or an empty candidate set — a population that is complete-minus-one refuses nothing and prints a confident scope line.What changed
scripts/check-self-test-wired.mjsnow exports the whole read:collectPopulation({ root })— the walk, the sources, the workflow corpus, the alias expansion, the package-local admission, the population.rootis a parameter so the refusals below can be exercised rather than merely coded.refusalFor(reading)— the#4690floors as a pure function over a completed reading. It takesrootCarriers(the root walk's own answer) and not the combined set, deliberately: a tree whose root walk stopped finding carriers has a broken reader even when the package-local lane still produced one, and the combined set is what would hide that.scripts/check-self-test-workflow-commands.mjsconsumes it and takes no walk.walkScripts,readdirSyncandstatSyncare gone from that file, and an own-source case pins that they stay gone.One
#4690floor moved intocheck-self-test-wiredalong with the population: the empty-population refusal, which previously only existed in the workflow-commands gate. That direction costs nothing — it cannot fire on a tree where any script CI runs ships a--self-test, and on one where none does, a confident green was the old behaviour.The
isCandidatedecision, and its priceDecision: package-local members are filtered on exactly the same terms as root ones. No lane of their own.
The predicate's subject is the file's bytes — "could this code print a workflow command" is a property of the source, and where the file sits says nothing about it. A lane-specific arm would be a second matching rule with no measurement behind it, in a gate whose whole design note is that its verdict comes from real output because matching rules rot in silence. Recorded in the code above
isCandidate.Price, measured on this tree at this commit: zero extra subprocesses.
packages/lint/scripts/check-reference-carrier-shape.mjscarries neither form in its code — no##[, and no::that could reach the start of a printed line — so it enters the population and is not selected by the prefilter. The price is deferred, not waived, and it is small: that gate's--self-testruns in 0.68s wall (measured on a shared box), against roughly 14s for the 17 already selected. If it ever gains a token in code it joins the candidate set on the same terms as any root script.Why
WORKFLOW_DIRstays declared although nothing reads itscripts/pm/dispatch-gates.mjsderives this gate's family by scanning its source for path literals. Deleting.github/workflowsalong with the read would have dropped the gate off every card that edits a workflow — while its verdict still moves with those files, since a workflow is what decides which self-tests are in the population at all. So the constant stays, and it is pinned againstread.workflowDirinmain(): a live coupling, not a decoration. The day the shared reader's corpus root moves, this gate refuses out loud instead of quietly declaring a directory it no longer depends on.Verified, not assumed — the derived command list for a
.github/workflows/lint.ymlcard is byte-identical before and after:No ledger row in
dispatch-gates.mjsneeded moving, and nothing there was edited. ItsCOMPOUND_ANCHOR_LEDGERrows key on the function namescarriesSelfTestandrunSelfTest, both unchanged; its self-test case about "a walk seeded at a bounded subtree" runs on a hand-written fixture string, not on this file.Self-test cases added
Floors were raised for cases added, never lowered.
check-self-test-wired— new batterythe exported population, 8 cases (registry 9 -> 10 batteries, 59 -> 67 cases): five refusal arms driven throughrefusalForon hand-built readings (control, empty walk, empty root carriers, emptynamed, empty population), plus three live ones — the reading is readable at all, the package-local half survives the export, and every population member has an entry insources(the consumer indexessourcesby member to run its prefilter).check-self-test-workflow-commands— new batterythe population is imported, never re-walked, 8 cases (registry 6 -> 7 batteries, 23 -> 31 cases): the live population contains the package-local specimen; the prefilter reaches it (its value is a measurement of that file and is deliberately not pinned); the declared corpus root is the one the shared reader used; two own-source pins — no directory walk in this file, and the population arrives from the gate that owns it; and the#4690refusals still fire, in both the pure arm and the disk arm.The own-source needles are assembled at run time. Spelled out, they would be found in their own fixture and the pin would red on itself forever — the hazard
check-watch-hint-literaldocuments for the same shape.Ablation — the pins are instruments, and the production runs are not
Both mutations were taken from the committed state, each confirmed on disk before the readings and each restored byte for byte afterwards, with a
trapon absolute paths. These are plain.mjsscripts run directly, so there is nodist/leg to rebuild; what is proved instead is that the bytes changed and changed back.Ablation 1 — neutralise the package-local admission in
collectPopulation(the pre-#15342 shape, one layer down).Note the third line, which is the reason these cases had to be written. With one definition, both production gates go to 168 together and agree — green, confident, and wrong. The 169/168 disagreement that made this card findable is no longer available as an accidental signal, so the self-test pins are now the only instrument.
Ablation 2 — re-add a private walk to the workflow-commands gate.
Verification
Every exit code captured before any pipe (
cmd > log 2>&1; EXIT=$?), and the verdict quoted is the one the gate itself printed. The union below was run atd74490f5c, on a clean tree (git diff HEAD= 0 bytes).The family was derived by
node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstackletting it take the changeset itself — 26 commands, all exit 0:Printed verdict lines, the ones asked for by name:
pnpm check:declared-population-livewas run explicitly in addition to the derived family (it is not in it).pnpm lint— the full repo-wide run, not a narrowing:eslint . --no-inline-config, exit 0, 91s under the shared verify lock. The narrowed reading was taken too and agrees:eslint --no-inline-config --format jsonover the two edited files reports 2 files, 0 errors, 0 warnings, and this config sets noparserOptions.projectand enables no typed rules, so nothing in this diff can move an untouched file's verdict.The four other modules that name
check-self-test-wired.mjswere run as well, since this PR changes that module's exports —check-comment-mask-adoption,check-slot-lookup-ratchet,measure-self-test-floor --self-testall exit 0, andcheck-ratchet-remedy-authorityis in the derived family above. Neither edited script has a vitest suite; their instrument is their own--self-test, both of which are in the union.No
NOT MEASUREDresults: no command returned exit 3,PREREQUISITE NOT MET,ERR_MODULE_NOT_FOUNDor a queue timeout in the readings above.Scope
Two files. No changeset — nothing is published from a package. No
content/docs/releases/edit, no force-push, no rebase.🤖 Generated with Claude Code
https://claude.ai/code/session_012zGPuVVX3deAx9LdjK8jCk
Generated by Claude Code