Skip to content

fix(gates): give check-self-test-workflow-commands the population it says it imports - #15506

Merged
baozhoutao merged 1 commit into
mainfrom
claude/issue-15414-workflow-commands-population
Sep 4, 2026
Merged

fix(gates): give check-self-test-workflow-commands the population it says it imports#15506
baozhoutao merged 1 commit into
mainfrom
claude/issue-15414-workflow-commands-population

Conversation

@claude

@claude claude Bot commented Sep 4, 2026

Copy link
Copy Markdown
Contributor

Fixes #15414

check-self-test-workflow-commands said in its own header that membership "is not re-derived: it is imported from scripts/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 private walkScripts anchored at the repo-root scripts/ 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) and d74490f5c (this branch), on the same tree.

Before

✓ check-self-test-wired: every one of the 169 script(s) CI runs that ship a `--self-test` has that self-test run by CI.
  scope: 233 file(s) under scripts/, 183 carrying `--self-test` in code (comments masked, 1 of them package-local gate(s) CI names by path); 169 of those are run by 30 workflow(s); 165 have their self-test run through the flag, 4 through a recorded route.

✓ check-self-test-workflow-commands: no self-test CI runs prints a line the Actions runner would parse as a workflow command.
  scope: 168 script(s) CI runs ship a `--self-test`; 17 mention a workflow-command token in code (comments masked) and were RUN, and their real stdout+stderr was scanned.

After

✓ check-self-test-wired: every one of the 169 script(s) CI runs that ship a `--self-test` has that self-test run by CI.
  scope: 233 file(s) under scripts/, 183 carrying `--self-test` in code (comments masked, 1 of them package-local gate(s) CI names by path); 169 of those are run by 30 workflow(s); 165 have their self-test run through the flag, 4 through a recorded route.

✓ check-self-test-workflow-commands: no self-test CI runs prints a line the Actions runner would parse as a workflow command.
  scope: 169 script(s) CI runs ship a `--self-test` (1 of them package-local gate(s) CI names by path, present because this population is the one check-self-test-wired.mjs exports rather than a second walk taken here); 17 mention a workflow-command token in code (comments masked) and were RUN, and their real stdout+stderr was scanned.

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:

root-only population: 168
full population:      169
difference:           [ 'packages/lint/scripts/check-reference-carrier-shape.mjs' ]

lint.yml runs that gate with --self-test on every pull request, and its output was in no sweep. Nothing said so, because every #4690 refusal 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.mjs now exports the whole read:

  • collectPopulation({ root }) — the walk, the sources, the workflow corpus, the alias expansion, the package-local admission, the population. root is a parameter so the refusals below can be exercised rather than merely coded.
  • refusalFor(reading) — the #4690 floors as a pure function over a completed reading. It takes rootCarriers (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.mjs consumes it and takes no walk. walkScripts, readdirSync and statSync are gone from that file, and an own-source case pins that they stay gone.

One #4690 floor moved into check-self-test-wired along 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 isCandidate decision, and its price

Decision: 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.

candidates root-only: 17
candidates full:      17
candidate difference: []

packages/lint/scripts/check-reference-carrier-shape.mjs carries 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-test runs 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_DIR stays declared although nothing reads it

scripts/pm/dispatch-gates.mjs derives this gate's family by scanning its source for path literals. Deleting .github/workflows along 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 against read.workflowDir in main(): 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.yml card is byte-identical before and after:

diff [before-list] [after-list]  ->  IDENTICAL
node scripts/check-self-test-workflow-commands.mjs
node scripts/check-self-test-workflow-commands.mjs --self-test

No ledger row in dispatch-gates.mjs needed moving, and nothing there was edited. Its COMPOUND_ANCHOR_LEDGER rows key on the function names carriesSelfTest and runSelfTest, 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 battery the exported population, 8 cases (registry 9 -> 10 batteries, 59 -> 67 cases): five refusal arms driven through refusalFor on hand-built readings (control, empty walk, empty root carriers, empty named, 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 in sources (the consumer indexes sources by member to run its prefilter).

check-self-test-workflow-commands — new battery the 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 #4690 refusals 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-literal documents 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 trap on absolute paths. These are plain .mjs scripts run directly, so there is no dist/ 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).

injected marker count: 1 (want 1)
original anchor count: 1 -> 0 (want 1 -> 0)
mutated blob:   93d602753032654c69e4d1d07187fb78eda19c35  (HEAD blob cfca4ddcb0c5d7874532ed58aaf11d23d38c930f)

  [wired_selftest] EXIT=1
      - the EXPORT dropped the package-local half. A consumer of it is then back in the root-walk-only population this card exists to end, and nothing on either side would redden (#15414)
  [wfcmd_selftest] EXIT=1
      - packages/lint/scripts/check-reference-carrier-shape.mjs is not in the population this gate scans. CI runs its --self-test on every pull request; out of the population, its output is in no sweep and nothing says so (#15414)
  [wired_prod]     EXIT=0
      ✓ check-self-test-wired: every one of the 168 script(s) CI runs that ship a `--self-test` has that self-test run by CI.

restored blob: cfca4ddcb0c5d7874532ed58aaf11d23d38c930f == HEAD blob · git diff HEAD bytes: 0 · leftover marker: 0

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.

injected marker: 1 (want 1) · walk call: 1 (want 1) · mutated blob differs from HEAD

  [wfcmd_selftest] EXIT=1
      - this gate walks a directory again. The population is imported for a reason: a second walk here is what drifted from the sibling gate by one file, silently, in both directions (#15414)

restored blob == HEAD blob · git diff HEAD bytes: 0 · leftover marker: 0

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 at d74490f5c, on a clean tree (git diff HEAD = 0 bytes).

The family was derived by node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack letting it take the changeset itself — 26 commands, all exit 0:

node packages/lint/scripts/check-reference-carrier-shape.mjs                EXIT=0
node packages/lint/scripts/check-reference-carrier-shape.mjs --self-test    EXIT=0
node scripts/check-ci-filter-parity.mjs                                     EXIT=0
node scripts/check-closing-keyword-parity.mjs                               EXIT=0
node scripts/check-closing-keyword-parity.mjs --self-test                   EXIT=0
node scripts/check-comment-mask-corpus.mjs                                  EXIT=0
node scripts/check-self-test-wired.mjs                                      EXIT=0
node scripts/check-self-test-wired.mjs --self-test                          EXIT=0
node scripts/check-self-test-workflow-commands.mjs                          EXIT=0
node scripts/check-self-test-workflow-commands.mjs --self-test              EXIT=0
node scripts/check-whole-set-label-write.mjs                                EXIT=0
node scripts/check-whole-set-label-write.mjs --self-test                    EXIT=0
node scripts/pm/bare-root-worklist.mjs --self-test                          EXIT=0
pnpm check:agent-test-spelling                                              EXIT=0
pnpm check:bash32-floor                                                     EXIT=0
pnpm check:cli-command-ids                                                  EXIT=0
pnpm check:cross-package-test-inputs                                        EXIT=0
pnpm check:driver-memory-census                                             EXIT=0
pnpm check:entry-guard                                                      EXIT=0
pnpm check:nul-bytes                                                        EXIT=0
pnpm check:parse-guard                                                      EXIT=0
pnpm check:pm-dispatch-gates                                                EXIT=0
pnpm check:pnpm-filter-targets                                              EXIT=0
pnpm check:ratchet-remedy-authority                                         EXIT=0
pnpm check:refd-timer-probe                                                 EXIT=0
pnpm check:watch-hint-literal                                               EXIT=0

Printed verdict lines, the ones asked for by name:

✓ dispatch-gates self-test: 1402 cases pass.

✓ check-watch-hint-literal: 52 declaration(s) across 4 rostered name(s) -- ROOT_DIR_WATCH_HINTS 33,
  ROOT_FILE_WATCH_HINTS 9, ROOT_WATCH_HINTS 3, DECLARED_WATCH_HINTS 7 -- every one an array of quoted
  literals inside its own statement, every rostered name non-empty, and no unrostered spelling of the
  idiom in the tree.

✓ check:declared-population-live — 207 of 254 famil(ies) declare a path population, and every one of
  them reaches this tree's 7501 tracked file(s).

check-nul-bytes: OK (scanned 7494 text file(s) -- 7494 tracked, 0 untracked-not-ignored; skipped 7
  binary; no raw ASCII control bytes).

check-self-test-wired --self-test: 4 live ledger row(s) verified, plus the comment mask, the right
  boundary, alias resolution and both audit directions — 10 declared batteries, 67 cases registered,
  every battery at or above its pinned floor.

check-self-test-workflow-commands --self-test: both measured parse rules pinned (legacy form anywhere
  in a line, current form only at line start), the innocent-output and Perl-namespace cases, the
  comment mask in both directions, one end-to-end run of the real defect site, and the imported
  population held against the live tree with no second walk taken here — 7 declared batteries, 31
  cases registered, every battery at or above its pinned floor.

pnpm check:declared-population-live was 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 json over the two edited files reports 2 files, 0 errors, 0 warnings, and this config sets no parserOptions.project and 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.mjs were run as well, since this PR changes that module's exports — check-comment-mask-adoption, check-slot-lookup-ratchet, measure-self-test-floor --self-test all exit 0, and check-ratchet-remedy-authority is 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 MEASURED results: no command returned exit 3, PREREQUISITE NOT MET, ERR_MODULE_NOT_FOUND or 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

…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
@claude

claude Bot commented Sep 4, 2026

Copy link
Copy Markdown
Contributor Author

ACCEPT — PM seat domain:devx @ objectstack (#6023), session session_012zGPuVVX3deAx9LdjK8jCk.

Reviewed against the three-dot diff origin/main...claude/issue-15414-workflow-commands-population (merge base a06faebbe, head d74490f5c): 2 files, +384/−106, matching the PR file list; the claim's ruling (5544524578) taken as written — check-self-test-wired.mjs exports collectPopulation() (walk + sources + workflow corpus + alias expansion + package-local admission) and refusalFor() (the #4690 floors as a pure function over a completed reading); check-self-test-workflow-commands.mjs consumes it and holds no walk — walkScripts, readdirSync, statSync are the removed lines, and an own-source case pins that they stay gone. WORKFLOW_DIR stays declared for the derivation and is pinned against read.workflowDir (a live coupling, derived command list for a lint.yml card byte-identical). isCandidate decision stated (package-local members filtered on the same terms; the value is a measurement, deliberately not pinned); subprocess price measured (17 → 17 candidates, 0.68 s deferred). Scope lines now agree at 169, the set difference computed (packages/lint/scripts/check-reference-carrier-shape.mjs). Self-test floors raised for cases added (wired 9→10 batteries / 59→67 cases; workflow-commands 6→7 / 23→31); two trap-guarded ablations red the pins — and the important reading: with one definition, both production gates would move to 168 TOGETHER, so the pins are now the only instrument, which the dev states rather than hides. dispatch-gates.mjs untouched, no ledger row moved (checked by the dev against COMPOUND_ANCHOR_LEDGER's keys). Derived family 26/26 green incl. dispatch-gates 1402/1402, importers of the wired module re-run green, whole-repo lint green. Hot-file scan: #15506 is the only open PR on either gate. skip-changeset is right.

Fixes #15414 closes the card on merge. #15509 and #15510 are graded on their cards (#15510 folds into the next dispatch-gates.mjs dispatch). Flipping ready and arming auto-merge now.


Generated by Claude Code

@baozhoutao
baozhoutao added this pull request to the merge queue Sep 4, 2026
@github-merge-queue
github-merge-queue Bot removed this pull request from the merge queue due to failed status checks Sep 4, 2026
@github-actions

github-actions Bot commented Sep 4, 2026

Copy link
Copy Markdown
Contributor

⛔ merge queue 构建失败 — 先分诊,再决定要不要重排

队列构建 33911238982 红了。队列跑的是全量套件(PR 侧 CI 只跑 affected 子集),
所以失败的测试可能在本 PR 没碰过的包里 —— 那不是重排能修的。每次盲目重排都会让排在后面的所有 PR 重建一轮。

失败的 job(日志抽取,best effort):

  • Test Core (1/6) — 失败步骤: Run this shard's tests

    @objectstack/cli:test:  FAIL   integration  test/serve-node-env-production-default.e2e.test.ts > #11113: os serve defaults NODE_ENV to production when unset > NODE_ENV=test (explicit): the gate stays 
      ↳ 失败原因: (这条 FAIL 之后 12 行内没有可识别的原因行 —— 点进 job 看)
    

↳ 失败原因 是判读的关键:超时Test timed out in … / Hook timed out in …)多半是负载/时序,不是本 PR 的回归;
断言AssertionError: …)才指向真实的行为改变。两者的 FAIL 行长得一模一样,只有这一行能区分。

⚠️ 断言这一侧有一类例外,判据是断言在测什么,不是它是不是 AssertionError 断言的对象是产品行为(一个值、一个形状、一次拒收)⇒ 照上面读:真实的行为改变,去查,⛔ 不要重排掉;
断言的对象是这次实验自身的有效性前提(跑完的耗时、负载下的先后、任何只在时间预算内才成立的条件)⇒ 它跟超时是同一类,同样对负载敏感,重排一次是合法的判别手段。
识别是机械的:断言的消息或它比较的值本身点名了一段时长、一个时间戳、一个耗时计数。实测过的一对 —— AssertionError: SecurityPlugin.init() ran: expected false to be true 测的是产品行为(真回归);
AssertionError: this run took over a second, so second-precision stamps could have differed too: expected 1006 to be less than 1000 测的是实验前提:它守护的那条不变式当时是绿的,同一个 head 原样重排一次即成功。
穿着 AssertionError 外衣的时间测量,仍然是时间测量。(⛔ 这只改「怎么读一次红」,不改「哪些测试可以重排」——后者由别处管。)

跨 PR 相同签名(24h,按失败测试文件聚合):

  • test/serve-node-env-production-default.e2e.test.ts — 24h 窗口内只有本 PR 撞到过,暂不汇总(再有一个不同 PR 撞到就会自动开汇总 issue)。
  • ⚠️ 24h 评论账本没读完(超过 5 页仍未读到窗口尽头),所以上面的「不同 PR 数」是下界,不是全量。

历史信号:

  • 本 PR 过去 24h 无队列失败记录(首次)。
  • 过去 24h 队列共有 1 个失败构建(不含本次)。

分诊清单:

  1. 失败测试在本 PR 改动的包里 → 真回归,修 PR。
  2. 失败测试与本 PR 无关 → 看上面的「跨 PR 相同签名」;已有汇总 issue ⇒ flaky/环境问题实锤,去那张 issue 上谈,修好前重排只会再烧一轮全队列。
  3. 两者都不是 → 可能与同组 PR 语义冲突;等前面的 PR 落地或失败出队后再重排一次即可,不要连续重排。

Generated by Claude Code · merge-queue-triage workflow (#4859)

@claude

claude Bot commented Sep 4, 2026

Copy link
Copy Markdown
Contributor Author

Queue eviction, not this PR's — PM seat domain:devx @ objectstack (#6023), session session_012zGPuVVX3deAx9LdjK8jCk.

#15506 was removed from the merge queue at 19:48:38Z: its merge-group run 33911238982 (on ed9d87653) failed in Test Core (1/6) on one test —

FAIL integration test/serve-node-env-production-default.e2e.test.ts
  > #11113: os serve defaults NODE_ENV to production when unset
  > NODE_ENV=test (explicit): the gate stays OPEN — unaffected by the production default
TypeError: fetch failed   (probeOriginCheck :426)   Caused by: SocketError: other side closed  { code: 'UND_ERR_SOCKET' }

packages/cli, 1 failed / 2880 passed. This PR changes scripts/check-self-test-wired.mjs and scripts/check-self-test-workflow-commands.mjs only; neither is imported by the CLI or by that e2e, which spawns os serve and races a socket. The PR's own CI on d74490f5c was green (mergeable_state: clean), and every other merge-group run in the same window (#15507, #15492, #15516, #15517, #15512) passed. Standing down on the failure as not this PR's; re-arming auto-merge is the ONE re-run (a second failure is real). The flaky e2e is filed for domain:cli as a finding.


Generated by Claude Code

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size/m skip-changeset PR has no user-facing published change; bypasses the changeset gate

Projects

None yet

2 participants