From 209f5c59645d46b0c2b45f0a50ad542f67243274 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 4 Sep 2026 10:36:03 +0000 Subject: [PATCH] fix(pm): dispatch-gates' --changed --commands control accepts the no-diff refusal MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The case asserts that `--changed` combines with a stdout-shape flag — that the combination PARSES and REACHES the derivation. It demanded `status === 0`, which is a property of the tree the run stands in and not of the combination: where HEAD equals origin/main the derivation refuses by design with exit 2 and the "changes nothing against ... nothing to derive" sentence, so the case went red with nothing wrong with the tool. That shape is `main`'s own push run of lint.yml, which has been red at the gate step on every push since the case landed, while the merge_group run of the same head stayed green. The predicate now accepts either outcome, matching the refusal on the constant the print site interpolates rather than on a retyped copy of it, and a positive control puts an ILLEGAL combination (`--changed` with a path — refused by argv before any derivation, at the same exit 2 with a different sentence) through the SAME predicate and requires it to fail, so widening the case cannot empty it out. Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_012zGPuVVX3deAx9LdjK8jCk --- scripts/pm/dispatch-gates.mjs | 58 +++++++++++++++++++++++++++++++++-- 1 file changed, 55 insertions(+), 3 deletions(-) diff --git a/scripts/pm/dispatch-gates.mjs b/scripts/pm/dispatch-gates.mjs index 41508a171a..5ec8448acc 100644 --- a/scripts/pm/dispatch-gates.mjs +++ b/scripts/pm/dispatch-gates.mjs @@ -19065,10 +19065,50 @@ function selfTest() { // CONTROL: --changed really does combine with a stdout-shape flag — the // usage-line fix above would otherwise be cosmetic on a refusal that does // not exist. + // + // ⛔ The claim is about the COMBINATION — that it PARSES and REACHES the + // derivation — so it must not be hostage to whether the tree this run + // happens to stand in has a diff. The first spelling demanded + // `status === 0`, which made the verdict a property of the caller's tree: + // where HEAD equals origin/main the derivation refuses BY DESIGN (exit 2, + // the NO_DIFF_REFUSAL sentence) and the case went red with nothing wrong + // with the tool. That is not only the fresh-worktree baseline it was found + // on. `main`'s own `push` run of lint.yml has exactly that shape — the + // pushed head IS origin/main — so every push to main was red at this + // file's gate step from the commit that added this case until this one, + // while the `merge_group` run of the same head stayed green because a + // queue branch HAS a diff (#15278). + // + // Both outcomes prove the claim, and nothing else does: exit 0 with a + // command list, or the exit-2 refusal that ONLY the derivation reaches. An + // illegal combination never gets that far — argv refuses it first, with + // its own sentence at the same exit code — which is why the predicate + // matches on the sentence and never on the code alone, and why the + // POSITIVE CONTROL below puts an illegal run through the SAME predicate + // and requires it to FAIL. A case widened to accept a second outcome is + // one reading away from accepting every outcome; that reading is made + // here, mechanically, instead of being left to the next author's eye. + const reachedDerivation = (run) => + (run.status === 0 && (run.stdout ?? '').length > 0) + || (run.status === 2 && (run.stderr ?? '').includes(NO_DIFF_REFUSAL)); const changedCommandsRun = runCli(['--changed', '--commands']); t( - 'CONTROL: --changed --commands is legal and answers, so the moved usage line describes a real combination', - changedCommandsRun.status === 0 && (changedCommandsRun.stdout ?? '').length > 0, + 'CONTROL: --changed --commands parses and REACHES the derivation, so the moved usage line describes a real combination' + + ' — exit 0 with commands, or the exit-2 no-diff refusal on a tree that has nothing to derive (#15278)', + reachedDerivation(changedCommandsRun), + ); + // POSITIVE CONTROL: the predicate above still REJECTS a combination the + // argv chain refuses before any derivation runs. `--changed` with a path is + // that combination — the two input modes answer different questions — and + // its refusal carries a different sentence at the same exit 2, which is + // exactly the confusion the predicate has to survive. + const changedPathRun = runCli(['--changed', '--commands', seamCard]); + t( + '…and that same predicate REJECTS the illegal --changed-with-a-path combination, so no parse failure can satisfy the control above', + !reachedDerivation(changedPathRun) + && changedPathRun.status === 2 + && (changedPathRun.stderr ?? '').includes('--changed derives the paths itself') + && !(changedPathRun.stderr ?? '').includes(NO_DIFF_REFUSAL), ); } @@ -19562,6 +19602,18 @@ const invokedDirectly = isEntrypoint(import.meta.url); * where `changedPathsFromGit()` refuses, and a pin that cannot be run in the * self-test is not a pin. */ +/** + * The invariant half of the refusal `--changed` prints on a tree with NO diff. + * + * A CONSTANT for USAGE_LINE's reason and for one more that is this file's own + * subject. The self-test's `--changed --commands` CONTROL has to tell that + * refusal — which only the derivation can print — apart from an argv-parse + * refusal, and BOTH exit 2. Matching a retyped copy of the sentence would pin + * the case to a memory of the message rather than to the message, so the case + * and the print site read the same constant (#15278). + */ +const NO_DIFF_REFUSAL = 'changes nothing against'; + const USAGE_LINE = 'usage: node scripts/pm/dispatch-gates.mjs' + ' [--tier | [--residue] [--commands | --json | --ran ]]' @@ -19762,7 +19814,7 @@ if (invokedDirectly) { // and "no gates" is the most expensive thing this tool could say wrongly // (#4690: an unreadable input must never look like an empty answer). console.error( - `dispatch-gates: this branch changes nothing against '${derived.base}' (merge base ${derived.mergeBase.slice(0, 9)}) — ` + + `dispatch-gates: this branch ${NO_DIFF_REFUSAL} '${derived.base}' (merge base ${derived.mergeBase.slice(0, 9)}) — ` + 'nothing to derive. On the base branch already, or in the wrong checkout? Pass explicit paths to ask about a hypothetical surface.', ); process.exit(2);