Skip to content

feat(pm): reconcile the gate families a dev DERIVED against the ones it RAN - #14306

Merged
baozhoutao merged 1 commit into
mainfrom
claude/issue-13774-run-reconciliation-mode
Sep 2, 2026
Merged

feat(pm): reconcile the gate families a dev DERIVED against the ones it RAN#14306
baozhoutao merged 1 commit into
mainfrom
claude/issue-13774-run-reconciliation-mode

Conversation

@baozhoutao

@baozhoutao baozhoutao commented Sep 2, 2026

Copy link
Copy Markdown
Contributor

Fixes #13774

Adds a --ran FILE verdict mode to scripts/pm/dispatch-gates.mjs: it takes a record of what was run and names the exact set difference against this tree's derivation. One file changed, scripts/pm/dispatch-gates.mjs, plus its self-test.

Follows the triage ruling on that card: 「⭐ 分诊裁定:走生产侧(选项 2),⛔ 不以消费侧纪律收场」 — the normalization is owned once, in a place that can be tested.

Premise re-derivation — what already existed, and what was still missing

⚠️ Four deliveries landed on this file after the card and all of its comments were written, so the gap was re-derived on today's origin/main (a59f78df3) before anything was designed.

landed what it added which link it closes
#13642 / PR #14221 familyReconciliation + familyReconciliationLines — a TOTAL, printed on every human run, with the arithmetic tying it to both sections printed ⟶ harvested
#14004 / PR #14280 ciOnlyMeasurement / ciOnlyCommandSet — CI-measured-only families classified and subtracted from --commands, with the omission stated where it happens printed ⟶ harvested
#13941 / PR #14284 the compound-anchor ledger unrelated to this chain

The card's gap is open. Everything that landed is print-side: it makes a SHORT HARVEST detectable by stating a number the reader can assert against. Nothing in the file accepts a record of what was executed, and no code path compares two lists. Verified on today's tree: no --ran, no run-record reader, no set difference anywhere; familyReconciliation's inputs are matchedRows and kindGroups only.

That is the card's own discriminating test, unchanged by the drift: «a better list does not make anyone run it». The count line answers did you harvest the whole list; this answers did you run it, and one does not imply the other. premise_still_valid: true.

The run-record format, and why it makes the escape clause moot

The ruling attached a condition: 「若实现时发现日志命名无法被工具端可靠归一(例如各 dev 的日志名形状不受控),回报而不是自行退回消费侧」.

The answer is not a better normalizer — it is that there is nothing to normalize. The record's lines are the exact strings --commands emits, copied by the runner as it goes:

node scripts/pm/dispatch-gates.mjs --commands > gates.list
while IFS= read -r cmd; do
  eval "$cmd" > "logs/$n" 2>&1
  printf '%s\n' "$cmd" >> ran.list      # what RAN — pass or fail
done < gates.list
node scripts/pm/dispatch-gates.mjs --ran ran.list

Both sides of the comparison are therefore produced by one expression in one file and are identical in shape by construction. ⛔ The record is never built from log FILE NAMES — that is the step that needs a slug, and the slug is where the first hand-built reconciliation went wrong.

Everything the parser does to a line is line-ending decoding, and it is stated as such: one trailing \r is dropped because a CRLF file terminates its lines with two bytes; blank, whitespace-only and # lines carry no command; nothing else is touched. Leading and trailing spaces inside a line are content, and trimming them would be a normalization applied to one side of the comparison only — the fuzzy shape wearing a smaller hat.

The three measured failure mechanisms, and what defeats each

# mechanism (measured, on the card) what defeats it here
1 the list was harvested and never used as a checklist — 19 of 62 families run, one CI red out of 43 unrun by luck the comparison is a MODE with an exit code: --ran exits 1 while any derived family is unaccounted for. A report can paraphrase prose; it cannot paraphrase a nonzero exit
2 a prefix matcher paired names that did not correspond and reported unreconciled 0 where the raw comm reported 36 (bash tr -c left a trailing underscore the Python slug did not reproduce) every comparison is Set.has on the untouched string. Prefix, substring, truncation and whitespace relations all pair with nothing — pinned by cases. A whitespace-only difference is reported and the family stays UNRUN: the diagnostic can never move a verdict, which is the whole lesson of that incident
3 arithmetic over a self-maintained counter: 57 + 1 = 58 balanced perfectly because the missing family had left the numerator and the denominator in the same operation the denominator is commandsFor's union, recomputed in THIS process from THIS tree — the same expression --commands prints — and the record can only ever subtract from a total it did not produce. Pinned at the limit: an empty record over a non-empty derivation reports every family unrun, never a balanced nothing

The classes, and why the remainder is the tool's job

#13711's row on this card is the warning: a remainder of 18 was accepted because the dev named every entry in prose, and a reconciliation whose output is routinely non-empty trains readers to wave it through. Its 18 were 16 CI-owned job steps and 2 spelling duplicates. Neither can appear here, and neither is classified away by a rule: derived is commandsFor's union, which never contained the always-runs tail (that is a listing about the REPO, printed under its own heading) and which deduplicates the two spellings of one family into one command before the reconciliation sees them.

What the tool still meets in a record it classifies itself, byte-exactly, against sets the same derivation produced: CI-measured-only families (ciOnlyCommandSet, subtracted from --commands by design) and pending-changeset families. Neither owes the runner an explanation.

What is left is only what the tool genuinely cannot know — a gate that refused with its own prerequisite:

  • NOT-MEASURED is its own class, reported apart from ran because this tool did not measure it and must not imply it did. It costs a stated reason (NOT-MEASURED <command> :: <reason>); a marked line without one leaves its family UNRUN. That is comment 5477620344's lesson made mechanical: the category exists for a gate that refuses with its own prerequisite, and it is exactly where a family you merely did not FINISH running goes to hide — a cap kill (exit 143) leaves no verdict at all, and the rendering says so beside the class.

Live demonstration — derived on this PR's own diff, run, fed back

node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --commands on this branch derived 15 families; all 15 were run under the shared verify lock, plus pnpm check:ratchet-remedy-authority from the dispatch order. The record was captured by construction, one printf '%s\n' "$cmd" per completed command.

1 — the record as captured (every command recorded as run):

Run reconciliation — 15 derived, 15 run, 0 NOT-MEASURED, 0 UNRUN.
  Outside this card's derivation (1) — recorded, and named by nothing this run derived. Not an error: a run beyond the union costs nothing.
    - pnpm check:ratchet-remedy-authority
✓ dispatch-gates --ran: 15 derived famil(ies) accounted for — 15 run, 0 NOT-MEASURED.        exit 0

2 — the honest record. node scripts/check-test-completeness.mjs exited 3 on this tree with PREREQUISITE NOT MET (it grades a saved turbo run test log and none was named; its own message says to record it as NOT MEASURED). Recorded as the refusal it is:

Run reconciliation — 15 derived, 14 run, 1 NOT-MEASURED, 0 UNRUN.
  NOT-MEASURED (1) — the RUNNER's claim, recorded with a reason. ⛔ This tool did not measure them and cannot verify the reason:
    - node scripts/check-test-completeness.mjs   [the gate REFUSED with its own stated prerequisite (exit 3, PREREQUISITE NOT MET) …]
    ⚠️ NOT-MEASURED is for a gate that REFUSES with its own stated prerequisite. A run the OS killed is not that — a cap kill (exit 143) leaves no verdict and the family is simply unrun.
✓ dispatch-gates --ran: 15 derived famil(ies) accounted for — 14 run, 1 NOT-MEASURED.        exit 0

3 — one line removed from that same record:

Run reconciliation — 15 derived, 13 run, 1 NOT-MEASURED, 1 UNRUN.
  ⛔ UNRUN (1) — derived for these paths, and the record does not account for them:
    - pnpm check:watch-hint-literal   [absent from the run record]
✗ dispatch-gates --ran: 1 of 15 derived famil(ies) UNRUN.                                    exit 1

All three classes on one real card, from one real run.

Self-tests

53 new cases (51 assertions, one of them a three-input loop), inside the tool's own --self-test. Whole suite: ✓ dispatch-gates self-test: 1232 cases pass, 0 failures.

They cover each of the three mechanisms above, both malformed-record shapes, the class partition asserted over 0/1/5-family derivations, the closure check that refuses rather than printing an untrustworthy verdict (#4690), the argv split (a two-token flag's value must not fall through into the path list — the hazard --repo's own docblock predicted, which came true the moment a second value-taking flag existed), and the message that would otherwise LIE about which argument is wrong.

⭐ The end-to-end half is the one that matters: it runs the real CLI, takes --commands output on a real card, writes it to a file unmodified, and reconciles against it — green, exit 0 — then removes one line and asserts exit 1 naming exactly that command. That measures the by-construction property the whole design rests on; every unit case above it stays green if --ran is never wired into the CLI.

⚠️ Cost, stated: check:pm-dispatch-gates measured 759s on this branch under the shared verify lock (shared-box seconds — the lock excludes other locked runs, not unlocked sibling work). The three CLI derivations in the new end-to-end block are ~90s of that. #14281 already owns that gate's runtime and its cap-kill behaviour; this PR does not make that problem, it adds to it, and the end-to-end cases are the ones that cannot be replaced by fixtures.

Gates run — 16 of 16, at 51801a456

Derived families re-derived from the merge base after the final commit (node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack), then reconciled with the mode this PR adds — see the demonstration above, unrun 0.

exit command
0 node scripts/check-ci-filter-parity.mjs
0 node scripts/check-cross-package-test-inputs.mjs
0 node scripts/check-self-test-wired.mjs
0 node scripts/check-shard-attestation.mjs
3 node scripts/check-test-completeness.mjs — NOT MEASURED by the gate's own declaration, not a red
0 pnpm check:agent-test-spelling
0 pnpm check:bash32-floor
0 pnpm check:cli-command-ids
0 pnpm check:cross-package-test-inputs
0 pnpm check:declared-population-live
0 pnpm check:entry-guard
0 pnpm check:parse-guard
0 pnpm check:pm-dispatch-gates (759s)
0 pnpm check:pnpm-filter-targets
0 pnpm check:watch-hint-literal
0 pnpm check:ratchet-remedy-authority (named by the dispatch order; outside the derived union)

Every exit code captured with cmd > log 2>&1; status=$? — redirected before capture, never read after a pipe.

Changeset

skip-changeset: the diff is one file under scripts/pm/** and publishes nothing from any package.

⛔ Not done here


Dispatched session: https://claude.ai/code/session_01WLJQhde67SeTccsmnBVarV

Generated by Claude Code


Generated by Claude Code

…it RAN

Add a --ran <file> verdict mode to scripts/pm/dispatch-gates.mjs. It takes a
run record whose lines are the exact strings --commands emits and reports an
exact set difference against this tree's derivation, recomputed in the same
process.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WLJQhde67SeTccsmnBVarV
@claude claude Bot added the skip-changeset PR has no user-facing published change; bypasses the changeset gate label Sep 2, 2026
@github-actions github-actions Bot added the size/l label Sep 2, 2026
@baozhoutao
baozhoutao marked this pull request as ready for review September 2, 2026 02:08
@baozhoutao
baozhoutao added this pull request to the merge queue Sep 2, 2026
Merged via the queue into main with commit 72adb7f Sep 2, 2026
39 checks passed
@baozhoutao
baozhoutao deleted the claude/issue-13774-run-reconciliation-mode branch September 2, 2026 02:34
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

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

Projects

None yet

2 participants