fix(pm): reconcile dispatch-gates' family sections with a total a harvest can be asserted against - #14221

Merged
baozhoutao merged 2 commits into
mainfrom
claude/issue-13642-family-print-reconciliation
Sep 1, 2026
Merged

fix(pm): reconcile dispatch-gates' family sections with a total a harvest can be asserted against#14221
baozhoutao merged 2 commits into
mainfrom
claude/issue-13642-family-print-reconciliation

Conversation

@claude

@claudeclaudeBot commented Sep 1, 2026

Copy link
Copy Markdown
Contributor

Fixes#13642

Two differently-shaped sections carry a card's runnable answer in dispatch-gates' human rendering, and nothing in that rendering stated their union. Two independent devs each harvested one section, ran it green, and reddened CI on a family the other section had named. This adds the missing control: a total a harvest can be asserted against, derived from the same structure the sections print from.

Premise re-check (the card predates PR #14207's +603 lines)

The card describes "two structurally different sections". Re-derived on 24b66352, the human block now has seven sections, of which exactly two carry this card's runnable answer:

sectionshapein the runnable answer?
Local gates for this card - CMD [wf] matched via ...yes — path-derived
spelling footerN families — P pnpm, K direct node.it is the count for the block above
Convention-triggered gates - CMD — why, under a per-kind headingyes — kind-derived
Once a changeset exists, N more... - CMD [wf] would match ...no, and deliberately so
Unreachable — ...standing repo factno
Always runs — ... - [wf · job] stepno
Residue — ...bucket countsno

So the card's premise holds, and its taxonomy was two-thirds of the picture. The authority for "what this card owes" is commandsFor(...) — matched UNION convention, deduped — which is exactly what --commands and --json already render.

The measurement that made the defect reproducible today

On 24b66352, for the card packages/spec/src/foo.test.ts:

matched block rows ......... 40
--commands ................. 44 (40 by path, 6 by kind, 2 reached both ways)
the spelling footer said ... "40 families — 28 pnpm, 12 direct node."

⭐ The only count line adjacent to the harvestable block stated a subtotal in the vocabulary of a total. A reader who harvested the matched block, counted 40, and read the footer got a reconciliation that agreed — on the wrong list. Both incidents on the card lost the convention block specifically, and check:engine-double-contract — occurrence 2's missing gate — is in the convention block on this very input.

What this changes (one file, scripts/pm/dispatch-gates.mjs)

  1. familyReconciliation — the total and its parts, built from the same two expressions commandsFor unions, not from a second traversal. Add a family to either input and both the section and its term move together. The identity matched + convention − both === total then holds by set algebra, and is asserted anyway: a mismatch throws and refuses rather than printing a total that cannot be trusted (check:react-declaration-parity 是唯一没接进任何 workflow 的源码审计门禁,且无 MANIFEST 时静默 skip 退出 0 —— 它现在永远不可能红 #4690).
  2. familyReconciliationLines — printed on every completed derivation, including at zero (an absent number is not assertable; its absence would mean "this card owes nothing" and "this build has no reconciliation" at once — the argument derive already makes for the tier verdict). Carries the arithmetic, the short-harvest warning where a section really is droppable, the --commands escape hatch inline, and a disclaimer that the total is not what CI runs.
  3. spellingFooterLines — no longer spells its matched-block subtotal as N families. It says N matched families, and when families sit outside its block it forward-points to the total, at the harvest site, where a consumer who never scrolls further still meets it.

⛔ Explicitly not done, per the triage ruling: no new output mode (--json and --commands both already exist), no ALL FAMILIES: block, no "please read more carefully" prose, and nothing in dispatch-order or teaching material — that limb is skills-lane governed territory.

Rendered output, on the input measured above

40 matched families — 28 pnpm, 12 direct node.
⛔ Two spellings, deliberately ... takes 28 of the 40 and reports nothing missing.
⇒ Harvest with --commands ...
Holding captured text already? ...
⛔ ...and a harvest of this block is 40 of the 44 this card owes, whichever spelling it takes: 4 more
famil(ies) are named by change KIND and print under their own heading below, outside every harvest
of THIS block — the published snippet above included.
The Reconciliation line under that heading carries the 44. Assert your list against THAT number, never against this one.
Convention-triggered gates (this change KIND moves them; no path derivation can name them):
... 6 rows, check:engine-double-contract among them ...
Reconciliation — 44 famil(ies): this card's WHOLE runnable answer, and the number to assert a harvest against.
40 named by PATH (the matched block) + 6 named by change KIND (the convention block), 2 of them the same
family reached both ways ⇒ 44 distinct.
⛔ A harvest that ends at ONE section is SHORT and reports nothing missing: the matched block alone is 40
of the 44, the convention block alone is 6 of the 44. Two cards lost the convention block exactly this
way, hours apart, and CI found it both times.
⇒ Skip the arithmetic: --commands prints exactly these 44, one runnable command per line, nothing else on stdout.
⛔ 44 is what THIS CARD owes by path and kind — NOT a complete account of what CI runs on the PR.

Verification

Union re-run on the final commit 1644b978.

pnpm check:pm-dispatch-gates — the tool's own battery, which is also the self-test surface for this file:

✓ dispatch-gates self-test: 1140 cases pass.
os-verify-lock: VERDICT command-exit 0 · held the lock 535s

26 new cases, all green, including the end-to-end pair that only a real run can hold:

✓ the reconciliation total IS the commandsFor union, not a recount of it
✓ adding a convention family moves the convention term AND the total
✓ suppressing the convention section moves the convention term AND the total
✓ the total and its parts close over EVERY overlap of the two inputs — 64 of 64
✓ the DEFAULT run prints the reconciliation — no flag, because a control behind a flag is worth nothing
✓ and its total is exactly what --commands returns for the same card
✓ the PATH term equals the rows the published snippet harvests from the matched block
✓ and the change-KIND term equals the distinct runnable rows the convention block really printed
✓ CONTROL: a matched-block-only harvest of this card is STILL short — the defect is real and untouched
✓ CONTROL: and the reconciliation total DETECTS that harvest as short rather than agreeing with it

Ablations — both legs proven on disk, both restored byte-identical

A — remove the call site from derive. Mutation confirmed on disk (call-site occurrences 1 → 0, injected marker 1; blob 6dbebbe2b0c6ed8c), then the real battery run on the mutated tree:

✗ the DEFAULT run prints the reconciliation — no flag, because a control behind a flag is worth nothing
✗ and its total is exactly what --commands returns for the same card
✗ the PATH term equals the rows the published snippet harvests from the matched block
✗ and the change-KIND term equals the distinct runnable rows the convention block really printed
✗ the matched footer forward-points to that total from inside the block being harvested
✗ dispatch-gates self-test: 5 of 1140 case(s) failed.

⭐ That ablation also found a vacuous assertion in my own pin: with the line absent, reconTotal is NaN, and NaN !== convBlock.length is TRUE — so the CONTROL case asserting the total detects a short harvest passed while the remedy was gone. That is this card's own defect shape inside the pin for it. Fixed in 1644b978 by requiring Number.isInteger(reconTotal) first.

B — break the closure identity (replace the overlap computation with an independent 0). Declared narrowing: the assert is in a pure function one call fully exercises, so this leg drove the function directly rather than the 9-minute battery. Mutation confirmed on disk (anchor 1 → 0, marker 1; blob differs from HEAD), control call on the unmutated tree returned a closing reconciliation first:

REFUSED, as designed:
dispatch-gates: the family reconciliation does not close — 1 matched + 1 convention − 0 both ≠ 1 distinct. ...

and the real human run exited 2 and printed ZERO reconciliation lines — it refuses rather than printing a wrong total.

Restore proven for both, not read off an exit code: git diff HEAD empty, git status clean, blob hash equal to the HEAD blob, 0 ablation markers left in the file.

Gate family, re-derived from the actual diff on 1644b978

node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack (no paths — the script takes the change set off the merge base itself) derives 15 families, and its own new line reconciles them: Reconciliation — 15 famil(ies) ... 15 named by PATH + 0 named by change KIND ⇒ 15 distinct. All 15 run, all green:

gateexit
pnpm check:pm-dispatch-gates0 — 1140 cases
pnpm check:declared-population-live0
pnpm check:watch-hint-literal0
pnpm check:parse-guard0
pnpm check:entry-guard0
pnpm check:agent-test-spelling0
pnpm check:bash32-floor0
pnpm check:cli-command-ids0
pnpm check:pnpm-filter-targets0
pnpm check:cross-package-test-inputs0
node scripts/check-cross-package-test-inputs.mjs0
node scripts/check-self-test-wired.mjs0
node scripts/check-ci-filter-parity.mjs0
node scripts/check-shard-attestation.mjs0
node scripts/check-test-completeness.mjsNOT MEASURED

check-test-completeness exits 3 = PREREQUISITE NOT MET, its own words: it grades a saved turbo run test log, there is none locally, and its message says the derived family names it with no argument and "the local reading for this gate is NOT MEASURED. ⛔ It is not a red." The other two exited 3 for the same class (yaml not installed in a fresh worktree) and were re-run to a real 0 after pnpm install.

Also run, per the dispatch: node scripts/pm/bare-root-worklist.mjs --self-test (0), node scripts/check-ratchet-remedy-authority.mjs (0), pnpm check:nul-bytes (0, plus a direct grep -naP control-byte scan of the changed file — none).

Every exit code above was captured before any pipe (cmd > log 2>&1; EXIT=$?), and each verdict is quoted from the gate's own line.

ESLint — a declared, measured narrowing, not a skip

The repo-wide pnpm lint is CI's run. Locally this was narrowed to the one changed file, with the three things that make a narrowing a measurement:

  1. Population from eslint's own config — one root eslint.config.mjs, resolved by eslint itself; the run used the same --no-inline-config the root lint script uses.
  2. Count from --format json — 1 file linted, 0 errors, 0 warnings.
  3. Invariance for untouched files — the config's own declaration (eslint.config.mjs, the QUERY_OPTIONS_TEST_GLOBS header) states this repo "runs one eslint.config.mjs, which never enables type-aware linting (no parserOptions.project, no typed @typescript-eslint rules) for ANY file", measured there with a positive control. A one-file .mjs edit therefore cannot move any untouched file's verdict.

Stale-tree warning, checked rather than carried

The derivation warns that scripts/check-stack-collection-maps.mjs changed on origin/main after this branch point. Read: the literals its new version names are its own file path, which does not cover scripts/pm/dispatch-gates.mjs (a bare scripts word is refused as too generic), so it does not change this card's family. Recorded as examined, not as an unread warning.

Notes for review

  • skip-changeset — the diff is scripts/pm/** only and publishes nothing from any package.
  • The reconciliation line names --commands inline. The file header already says it, but the header is not where a harvesting consumer looks, and the triage's own diagnosis is that adoption — not availability — is what failed. Flagged as a deliberate choice rather than a silent one.
  • Placement is below the convention block: at the top it would state a figure before the sections it reconciles had printed, and under the residue it would sit past the point a harvesting reader stops.

Generated by Claude Code


Generated by Claude Code

…vest can be asserted against
The human rendering places a card's runnable answer in two differently
shaped sections — a path-derived matched block and a kind-derived
convention block — and nothing in it stated the union. Two independent
devs each harvested one section, ran it green, and reddened CI on a
family the other named; a third reader misread the same output a third
way. Both machine-readable modes already existed, so the gap was not a
missing mode but a missing control: a partial harvest was plausible
rather than detectable.
- familyReconciliation derives the total and its parts from the SAME two
expressions commandsFor unions, so a family added to either input moves
the section and the count together. The closure identity is asserted
and refuses rather than printing an untrustworthy total.
- familyReconciliationLines prints it on every completed derivation,
including at zero, with the arithmetic tying it to both sections and
the --commands escape hatch named inline.
- spellingFooterLines no longer spells its matched-block subtotal as
`N families`: that was the line a dropped-section harvest reconciled
against, successfully, on the wrong list. It now names its scope and
forward-points to the total when families sit outside its block.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WLJQhde67SeTccsmnBVarV
…fore claiming detection
Measured while ablating the call site out of derive(): with no line printed
reconTotal is NaN, and `NaN !== convBlock.length` is TRUE — so the case
asserting that the total detects a short harvest passed while the remedy
was absent. That is the instrument-cannot-fail-toward-its-target shape this
card is about, inside the pin for it.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WLJQhde67SeTccsmnBVarV
@claudeclaudeBot added the skip-changeset PR has no user-facing published change; bypasses the changeset gate label Sep 1, 2026
@baozhoutao
baozhoutao marked this pull request as ready for review September 1, 2026 14:30
@baozhoutao
baozhoutao added this pull request to the merge queueSep 1, 2026
Merged via the queue into main with commit 66ecc50Sep 1, 2026
34 checks passed
@baozhoutao
baozhoutao deleted the claude/issue-13642-family-print-reconciliation branch September 1, 2026 14:53
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size/mskip-changesetPR has no user-facing published change; bypasses the changeset gate

Projects

None yet

2 participants

@baozhoutao@claude
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Skip to content

fix(pm): reconcile dispatch-gates' family sections with a total a harvest can be asserted against - #14221

Merged
baozhoutao merged 2 commits into
mainfrom
claude/issue-13642-family-print-reconciliation
Sep 1, 2026
Merged

fix(pm): reconcile dispatch-gates' family sections with a total a harvest can be asserted against#14221
baozhoutao merged 2 commits into
mainfrom
claude/issue-13642-family-print-reconciliation

Conversation

@claude

@claudeclaudeBot commented Sep 1, 2026

Copy link
Copy Markdown
Contributor

Fixes#13642

Two differently-shaped sections carry a card's runnable answer in dispatch-gates' human rendering, and nothing in that rendering stated their union. Two independent devs each harvested one section, ran it green, and reddened CI on a family the other section had named. This adds the missing control: a total a harvest can be asserted against, derived from the same structure the sections print from.

Premise re-check (the card predates PR #14207's +603 lines)

The card describes "two structurally different sections". Re-derived on 24b66352, the human block now has seven sections, of which exactly two carry this card's runnable answer:

sectionshapein the runnable answer?
Local gates for this card - CMD [wf] matched via ...yes — path-derived
spelling footerN families — P pnpm, K direct node.it is the count for the block above
Convention-triggered gates - CMD — why, under a per-kind headingyes — kind-derived
Once a changeset exists, N more... - CMD [wf] would match ...no, and deliberately so
Unreachable — ...standing repo factno
Always runs — ... - [wf · job] stepno
Residue — ...bucket countsno

So the card's premise holds, and its taxonomy was two-thirds of the picture. The authority for "what this card owes" is commandsFor(...) — matched UNION convention, deduped — which is exactly what --commands and --json already render.

The measurement that made the defect reproducible today

On 24b66352, for the card packages/spec/src/foo.test.ts:

matched block rows ......... 40
--commands ................. 44 (40 by path, 6 by kind, 2 reached both ways)
the spelling footer said ... "40 families — 28 pnpm, 12 direct node."

⭐ The only count line adjacent to the harvestable block stated a subtotal in the vocabulary of a total. A reader who harvested the matched block, counted 40, and read the footer got a reconciliation that agreed — on the wrong list. Both incidents on the card lost the convention block specifically, and check:engine-double-contract — occurrence 2's missing gate — is in the convention block on this very input.

What this changes (one file, scripts/pm/dispatch-gates.mjs)

  1. familyReconciliation — the total and its parts, built from the same two expressions commandsFor unions, not from a second traversal. Add a family to either input and both the section and its term move together. The identity matched + convention − both === total then holds by set algebra, and is asserted anyway: a mismatch throws and refuses rather than printing a total that cannot be trusted (check:react-declaration-parity 是唯一没接进任何 workflow 的源码审计门禁,且无 MANIFEST 时静默 skip 退出 0 —— 它现在永远不可能红 #4690).
  2. familyReconciliationLines — printed on every completed derivation, including at zero (an absent number is not assertable; its absence would mean "this card owes nothing" and "this build has no reconciliation" at once — the argument derive already makes for the tier verdict). Carries the arithmetic, the short-harvest warning where a section really is droppable, the --commands escape hatch inline, and a disclaimer that the total is not what CI runs.
  3. spellingFooterLines — no longer spells its matched-block subtotal as N families. It says N matched families, and when families sit outside its block it forward-points to the total, at the harvest site, where a consumer who never scrolls further still meets it.

⛔ Explicitly not done, per the triage ruling: no new output mode (--json and --commands both already exist), no ALL FAMILIES: block, no "please read more carefully" prose, and nothing in dispatch-order or teaching material — that limb is skills-lane governed territory.

Rendered output, on the input measured above

40 matched families — 28 pnpm, 12 direct node.
⛔ Two spellings, deliberately ... takes 28 of the 40 and reports nothing missing.
⇒ Harvest with --commands ...
Holding captured text already? ...
⛔ ...and a harvest of this block is 40 of the 44 this card owes, whichever spelling it takes: 4 more
famil(ies) are named by change KIND and print under their own heading below, outside every harvest
of THIS block — the published snippet above included.
The Reconciliation line under that heading carries the 44. Assert your list against THAT number, never against this one.
Convention-triggered gates (this change KIND moves them; no path derivation can name them):
... 6 rows, check:engine-double-contract among them ...
Reconciliation — 44 famil(ies): this card's WHOLE runnable answer, and the number to assert a harvest against.
40 named by PATH (the matched block) + 6 named by change KIND (the convention block), 2 of them the same
family reached both ways ⇒ 44 distinct.
⛔ A harvest that ends at ONE section is SHORT and reports nothing missing: the matched block alone is 40
of the 44, the convention block alone is 6 of the 44. Two cards lost the convention block exactly this
way, hours apart, and CI found it both times.
⇒ Skip the arithmetic: --commands prints exactly these 44, one runnable command per line, nothing else on stdout.
⛔ 44 is what THIS CARD owes by path and kind — NOT a complete account of what CI runs on the PR.

Verification

Union re-run on the final commit 1644b978.

pnpm check:pm-dispatch-gates — the tool's own battery, which is also the self-test surface for this file:

✓ dispatch-gates self-test: 1140 cases pass.
os-verify-lock: VERDICT command-exit 0 · held the lock 535s

26 new cases, all green, including the end-to-end pair that only a real run can hold:

✓ the reconciliation total IS the commandsFor union, not a recount of it
✓ adding a convention family moves the convention term AND the total
✓ suppressing the convention section moves the convention term AND the total
✓ the total and its parts close over EVERY overlap of the two inputs — 64 of 64
✓ the DEFAULT run prints the reconciliation — no flag, because a control behind a flag is worth nothing
✓ and its total is exactly what --commands returns for the same card
✓ the PATH term equals the rows the published snippet harvests from the matched block
✓ and the change-KIND term equals the distinct runnable rows the convention block really printed
✓ CONTROL: a matched-block-only harvest of this card is STILL short — the defect is real and untouched
✓ CONTROL: and the reconciliation total DETECTS that harvest as short rather than agreeing with it

Ablations — both legs proven on disk, both restored byte-identical

A — remove the call site from derive. Mutation confirmed on disk (call-site occurrences 1 → 0, injected marker 1; blob 6dbebbe2b0c6ed8c), then the real battery run on the mutated tree:

✗ the DEFAULT run prints the reconciliation — no flag, because a control behind a flag is worth nothing
✗ and its total is exactly what --commands returns for the same card
✗ the PATH term equals the rows the published snippet harvests from the matched block
✗ and the change-KIND term equals the distinct runnable rows the convention block really printed
✗ the matched footer forward-points to that total from inside the block being harvested
✗ dispatch-gates self-test: 5 of 1140 case(s) failed.

⭐ That ablation also found a vacuous assertion in my own pin: with the line absent, reconTotal is NaN, and NaN !== convBlock.length is TRUE — so the CONTROL case asserting the total detects a short harvest passed while the remedy was gone. That is this card's own defect shape inside the pin for it. Fixed in 1644b978 by requiring Number.isInteger(reconTotal) first.

B — break the closure identity (replace the overlap computation with an independent 0). Declared narrowing: the assert is in a pure function one call fully exercises, so this leg drove the function directly rather than the 9-minute battery. Mutation confirmed on disk (anchor 1 → 0, marker 1; blob differs from HEAD), control call on the unmutated tree returned a closing reconciliation first:

REFUSED, as designed:
dispatch-gates: the family reconciliation does not close — 1 matched + 1 convention − 0 both ≠ 1 distinct. ...

and the real human run exited 2 and printed ZERO reconciliation lines — it refuses rather than printing a wrong total.

Restore proven for both, not read off an exit code: git diff HEAD empty, git status clean, blob hash equal to the HEAD blob, 0 ablation markers left in the file.

Gate family, re-derived from the actual diff on 1644b978

node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack (no paths — the script takes the change set off the merge base itself) derives 15 families, and its own new line reconciles them: Reconciliation — 15 famil(ies) ... 15 named by PATH + 0 named by change KIND ⇒ 15 distinct. All 15 run, all green:

gateexit
pnpm check:pm-dispatch-gates0 — 1140 cases
pnpm check:declared-population-live0
pnpm check:watch-hint-literal0
pnpm check:parse-guard0
pnpm check:entry-guard0
pnpm check:agent-test-spelling0
pnpm check:bash32-floor0
pnpm check:cli-command-ids0
pnpm check:pnpm-filter-targets0
pnpm check:cross-package-test-inputs0
node scripts/check-cross-package-test-inputs.mjs0
node scripts/check-self-test-wired.mjs0
node scripts/check-ci-filter-parity.mjs0
node scripts/check-shard-attestation.mjs0
node scripts/check-test-completeness.mjsNOT MEASURED

check-test-completeness exits 3 = PREREQUISITE NOT MET, its own words: it grades a saved turbo run test log, there is none locally, and its message says the derived family names it with no argument and "the local reading for this gate is NOT MEASURED. ⛔ It is not a red." The other two exited 3 for the same class (yaml not installed in a fresh worktree) and were re-run to a real 0 after pnpm install.

Also run, per the dispatch: node scripts/pm/bare-root-worklist.mjs --self-test (0), node scripts/check-ratchet-remedy-authority.mjs (0), pnpm check:nul-bytes (0, plus a direct grep -naP control-byte scan of the changed file — none).

Every exit code above was captured before any pipe (cmd > log 2>&1; EXIT=$?), and each verdict is quoted from the gate's own line.

ESLint — a declared, measured narrowing, not a skip

The repo-wide pnpm lint is CI's run. Locally this was narrowed to the one changed file, with the three things that make a narrowing a measurement:

  1. Population from eslint's own config — one root eslint.config.mjs, resolved by eslint itself; the run used the same --no-inline-config the root lint script uses.
  2. Count from --format json — 1 file linted, 0 errors, 0 warnings.
  3. Invariance for untouched files — the config's own declaration (eslint.config.mjs, the QUERY_OPTIONS_TEST_GLOBS header) states this repo "runs one eslint.config.mjs, which never enables type-aware linting (no parserOptions.project, no typed @typescript-eslint rules) for ANY file", measured there with a positive control. A one-file .mjs edit therefore cannot move any untouched file's verdict.

Stale-tree warning, checked rather than carried

The derivation warns that scripts/check-stack-collection-maps.mjs changed on origin/main after this branch point. Read: the literals its new version names are its own file path, which does not cover scripts/pm/dispatch-gates.mjs (a bare scripts word is refused as too generic), so it does not change this card's family. Recorded as examined, not as an unread warning.

Notes for review

  • skip-changeset — the diff is scripts/pm/** only and publishes nothing from any package.
  • The reconciliation line names --commands inline. The file header already says it, but the header is not where a harvesting consumer looks, and the triage's own diagnosis is that adoption — not availability — is what failed. Flagged as a deliberate choice rather than a silent one.
  • Placement is below the convention block: at the top it would state a figure before the sections it reconciles had printed, and under the residue it would sit past the point a harvesting reader stops.

Generated by Claude Code


Generated by Claude Code

…vest can be asserted against
The human rendering places a card's runnable answer in two differently
shaped sections — a path-derived matched block and a kind-derived
convention block — and nothing in it stated the union. Two independent
devs each harvested one section, ran it green, and reddened CI on a
family the other named; a third reader misread the same output a third
way. Both machine-readable modes already existed, so the gap was not a
missing mode but a missing control: a partial harvest was plausible
rather than detectable.
- familyReconciliation derives the total and its parts from the SAME two
expressions commandsFor unions, so a family added to either input moves
the section and the count together. The closure identity is asserted
and refuses rather than printing an untrustworthy total.
- familyReconciliationLines prints it on every completed derivation,
including at zero, with the arithmetic tying it to both sections and
the --commands escape hatch named inline.
- spellingFooterLines no longer spells its matched-block subtotal as
`N families`: that was the line a dropped-section harvest reconciled
against, successfully, on the wrong list. It now names its scope and
forward-points to the total when families sit outside its block.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WLJQhde67SeTccsmnBVarV
…fore claiming detection
Measured while ablating the call site out of derive(): with no line printed
reconTotal is NaN, and `NaN !== convBlock.length` is TRUE — so the case
asserting that the total detects a short harvest passed while the remedy
was absent. That is the instrument-cannot-fail-toward-its-target shape this
card is about, inside the pin for it.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WLJQhde67SeTccsmnBVarV
@claudeclaudeBot added the skip-changeset PR has no user-facing published change; bypasses the changeset gate label Sep 1, 2026
@baozhoutao
baozhoutao marked this pull request as ready for review September 1, 2026 14:30
@baozhoutao
baozhoutao added this pull request to the merge queueSep 1, 2026
Merged via the queue into main with commit 66ecc50Sep 1, 2026
34 checks passed
@baozhoutao
baozhoutao deleted the claude/issue-13642-family-print-reconciliation branch September 1, 2026 14:53
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size/mskip-changesetPR has no user-facing published change; bypasses the changeset gate

Projects

None yet

2 participants

@baozhoutao@claude
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

fix(pm): reconcile dispatch-gates' family sections with a total a harvest can be asserted against - #14221

Merged
baozhoutao merged 2 commits into
mainfrom
claude/issue-13642-family-print-reconciliation
Sep 1, 2026
Merged

fix(pm): reconcile dispatch-gates' family sections with a total a harvest can be asserted against#14221
baozhoutao merged 2 commits into
mainfrom
claude/issue-13642-family-print-reconciliation

Conversation

@claude

@claudeclaudeBot commented Sep 1, 2026

Copy link
Copy Markdown
Contributor

Fixes#13642

Two differently-shaped sections carry a card's runnable answer in dispatch-gates' human rendering, and nothing in that rendering stated their union. Two independent devs each harvested one section, ran it green, and reddened CI on a family the other section had named. This adds the missing control: a total a harvest can be asserted against, derived from the same structure the sections print from.

Premise re-check (the card predates PR #14207's +603 lines)

The card describes "two structurally different sections". Re-derived on 24b66352, the human block now has seven sections, of which exactly two carry this card's runnable answer:

sectionshapein the runnable answer?
Local gates for this card - CMD [wf] matched via ...yes — path-derived
spelling footerN families — P pnpm, K direct node.it is the count for the block above
Convention-triggered gates - CMD — why, under a per-kind headingyes — kind-derived
Once a changeset exists, N more... - CMD [wf] would match ...no, and deliberately so
Unreachable — ...standing repo factno
Always runs — ... - [wf · job] stepno
Residue — ...bucket countsno

So the card's premise holds, and its taxonomy was two-thirds of the picture. The authority for "what this card owes" is commandsFor(...) — matched UNION convention, deduped — which is exactly what --commands and --json already render.

The measurement that made the defect reproducible today

On 24b66352, for the card packages/spec/src/foo.test.ts:

matched block rows ......... 40
--commands ................. 44 (40 by path, 6 by kind, 2 reached both ways)
the spelling footer said ... "40 families — 28 pnpm, 12 direct node."

⭐ The only count line adjacent to the harvestable block stated a subtotal in the vocabulary of a total. A reader who harvested the matched block, counted 40, and read the footer got a reconciliation that agreed — on the wrong list. Both incidents on the card lost the convention block specifically, and check:engine-double-contract — occurrence 2's missing gate — is in the convention block on this very input.

What this changes (one file, scripts/pm/dispatch-gates.mjs)

  1. familyReconciliation — the total and its parts, built from the same two expressions commandsFor unions, not from a second traversal. Add a family to either input and both the section and its term move together. The identity matched + convention − both === total then holds by set algebra, and is asserted anyway: a mismatch throws and refuses rather than printing a total that cannot be trusted (check:react-declaration-parity 是唯一没接进任何 workflow 的源码审计门禁,且无 MANIFEST 时静默 skip 退出 0 —— 它现在永远不可能红 #4690).
  2. familyReconciliationLines — printed on every completed derivation, including at zero (an absent number is not assertable; its absence would mean "this card owes nothing" and "this build has no reconciliation" at once — the argument derive already makes for the tier verdict). Carries the arithmetic, the short-harvest warning where a section really is droppable, the --commands escape hatch inline, and a disclaimer that the total is not what CI runs.
  3. spellingFooterLines — no longer spells its matched-block subtotal as N families. It says N matched families, and when families sit outside its block it forward-points to the total, at the harvest site, where a consumer who never scrolls further still meets it.

⛔ Explicitly not done, per the triage ruling: no new output mode (--json and --commands both already exist), no ALL FAMILIES: block, no "please read more carefully" prose, and nothing in dispatch-order or teaching material — that limb is skills-lane governed territory.

Rendered output, on the input measured above

40 matched families — 28 pnpm, 12 direct node.
⛔ Two spellings, deliberately ... takes 28 of the 40 and reports nothing missing.
⇒ Harvest with --commands ...
Holding captured text already? ...
⛔ ...and a harvest of this block is 40 of the 44 this card owes, whichever spelling it takes: 4 more
famil(ies) are named by change KIND and print under their own heading below, outside every harvest
of THIS block — the published snippet above included.
The Reconciliation line under that heading carries the 44. Assert your list against THAT number, never against this one.
Convention-triggered gates (this change KIND moves them; no path derivation can name them):
... 6 rows, check:engine-double-contract among them ...
Reconciliation — 44 famil(ies): this card's WHOLE runnable answer, and the number to assert a harvest against.
40 named by PATH (the matched block) + 6 named by change KIND (the convention block), 2 of them the same
family reached both ways ⇒ 44 distinct.
⛔ A harvest that ends at ONE section is SHORT and reports nothing missing: the matched block alone is 40
of the 44, the convention block alone is 6 of the 44. Two cards lost the convention block exactly this
way, hours apart, and CI found it both times.
⇒ Skip the arithmetic: --commands prints exactly these 44, one runnable command per line, nothing else on stdout.
⛔ 44 is what THIS CARD owes by path and kind — NOT a complete account of what CI runs on the PR.

Verification

Union re-run on the final commit 1644b978.

pnpm check:pm-dispatch-gates — the tool's own battery, which is also the self-test surface for this file:

✓ dispatch-gates self-test: 1140 cases pass.
os-verify-lock: VERDICT command-exit 0 · held the lock 535s

26 new cases, all green, including the end-to-end pair that only a real run can hold:

✓ the reconciliation total IS the commandsFor union, not a recount of it
✓ adding a convention family moves the convention term AND the total
✓ suppressing the convention section moves the convention term AND the total
✓ the total and its parts close over EVERY overlap of the two inputs — 64 of 64
✓ the DEFAULT run prints the reconciliation — no flag, because a control behind a flag is worth nothing
✓ and its total is exactly what --commands returns for the same card
✓ the PATH term equals the rows the published snippet harvests from the matched block
✓ and the change-KIND term equals the distinct runnable rows the convention block really printed
✓ CONTROL: a matched-block-only harvest of this card is STILL short — the defect is real and untouched
✓ CONTROL: and the reconciliation total DETECTS that harvest as short rather than agreeing with it

Ablations — both legs proven on disk, both restored byte-identical

A — remove the call site from derive. Mutation confirmed on disk (call-site occurrences 1 → 0, injected marker 1; blob 6dbebbe2b0c6ed8c), then the real battery run on the mutated tree:

✗ the DEFAULT run prints the reconciliation — no flag, because a control behind a flag is worth nothing
✗ and its total is exactly what --commands returns for the same card
✗ the PATH term equals the rows the published snippet harvests from the matched block
✗ and the change-KIND term equals the distinct runnable rows the convention block really printed
✗ the matched footer forward-points to that total from inside the block being harvested
✗ dispatch-gates self-test: 5 of 1140 case(s) failed.

⭐ That ablation also found a vacuous assertion in my own pin: with the line absent, reconTotal is NaN, and NaN !== convBlock.length is TRUE — so the CONTROL case asserting the total detects a short harvest passed while the remedy was gone. That is this card's own defect shape inside the pin for it. Fixed in 1644b978 by requiring Number.isInteger(reconTotal) first.

B — break the closure identity (replace the overlap computation with an independent 0). Declared narrowing: the assert is in a pure function one call fully exercises, so this leg drove the function directly rather than the 9-minute battery. Mutation confirmed on disk (anchor 1 → 0, marker 1; blob differs from HEAD), control call on the unmutated tree returned a closing reconciliation first:

REFUSED, as designed:
dispatch-gates: the family reconciliation does not close — 1 matched + 1 convention − 0 both ≠ 1 distinct. ...

and the real human run exited 2 and printed ZERO reconciliation lines — it refuses rather than printing a wrong total.

Restore proven for both, not read off an exit code: git diff HEAD empty, git status clean, blob hash equal to the HEAD blob, 0 ablation markers left in the file.

Gate family, re-derived from the actual diff on 1644b978

node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack (no paths — the script takes the change set off the merge base itself) derives 15 families, and its own new line reconciles them: Reconciliation — 15 famil(ies) ... 15 named by PATH + 0 named by change KIND ⇒ 15 distinct. All 15 run, all green:

gateexit
pnpm check:pm-dispatch-gates0 — 1140 cases
pnpm check:declared-population-live0
pnpm check:watch-hint-literal0
pnpm check:parse-guard0
pnpm check:entry-guard0
pnpm check:agent-test-spelling0
pnpm check:bash32-floor0
pnpm check:cli-command-ids0
pnpm check:pnpm-filter-targets0
pnpm check:cross-package-test-inputs0
node scripts/check-cross-package-test-inputs.mjs0
node scripts/check-self-test-wired.mjs0
node scripts/check-ci-filter-parity.mjs0
node scripts/check-shard-attestation.mjs0
node scripts/check-test-completeness.mjsNOT MEASURED

check-test-completeness exits 3 = PREREQUISITE NOT MET, its own words: it grades a saved turbo run test log, there is none locally, and its message says the derived family names it with no argument and "the local reading for this gate is NOT MEASURED. ⛔ It is not a red." The other two exited 3 for the same class (yaml not installed in a fresh worktree) and were re-run to a real 0 after pnpm install.

Also run, per the dispatch: node scripts/pm/bare-root-worklist.mjs --self-test (0), node scripts/check-ratchet-remedy-authority.mjs (0), pnpm check:nul-bytes (0, plus a direct grep -naP control-byte scan of the changed file — none).

Every exit code above was captured before any pipe (cmd > log 2>&1; EXIT=$?), and each verdict is quoted from the gate's own line.

ESLint — a declared, measured narrowing, not a skip

The repo-wide pnpm lint is CI's run. Locally this was narrowed to the one changed file, with the three things that make a narrowing a measurement:

  1. Population from eslint's own config — one root eslint.config.mjs, resolved by eslint itself; the run used the same --no-inline-config the root lint script uses.
  2. Count from --format json — 1 file linted, 0 errors, 0 warnings.
  3. Invariance for untouched files — the config's own declaration (eslint.config.mjs, the QUERY_OPTIONS_TEST_GLOBS header) states this repo "runs one eslint.config.mjs, which never enables type-aware linting (no parserOptions.project, no typed @typescript-eslint rules) for ANY file", measured there with a positive control. A one-file .mjs edit therefore cannot move any untouched file's verdict.

Stale-tree warning, checked rather than carried

The derivation warns that scripts/check-stack-collection-maps.mjs changed on origin/main after this branch point. Read: the literals its new version names are its own file path, which does not cover scripts/pm/dispatch-gates.mjs (a bare scripts word is refused as too generic), so it does not change this card's family. Recorded as examined, not as an unread warning.

Notes for review

  • skip-changeset — the diff is scripts/pm/** only and publishes nothing from any package.
  • The reconciliation line names --commands inline. The file header already says it, but the header is not where a harvesting consumer looks, and the triage's own diagnosis is that adoption — not availability — is what failed. Flagged as a deliberate choice rather than a silent one.
  • Placement is below the convention block: at the top it would state a figure before the sections it reconciles had printed, and under the residue it would sit past the point a harvesting reader stops.

Generated by Claude Code


Generated by Claude Code

…vest can be asserted against
The human rendering places a card's runnable answer in two differently
shaped sections — a path-derived matched block and a kind-derived
convention block — and nothing in it stated the union. Two independent
devs each harvested one section, ran it green, and reddened CI on a
family the other named; a third reader misread the same output a third
way. Both machine-readable modes already existed, so the gap was not a
missing mode but a missing control: a partial harvest was plausible
rather than detectable.
- familyReconciliation derives the total and its parts from the SAME two
expressions commandsFor unions, so a family added to either input moves
the section and the count together. The closure identity is asserted
and refuses rather than printing an untrustworthy total.
- familyReconciliationLines prints it on every completed derivation,
including at zero, with the arithmetic tying it to both sections and
the --commands escape hatch named inline.
- spellingFooterLines no longer spells its matched-block subtotal as
`N families`: that was the line a dropped-section harvest reconciled
against, successfully, on the wrong list. It now names its scope and
forward-points to the total when families sit outside its block.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WLJQhde67SeTccsmnBVarV
…fore claiming detection
Measured while ablating the call site out of derive(): with no line printed
reconTotal is NaN, and `NaN !== convBlock.length` is TRUE — so the case
asserting that the total detects a short harvest passed while the remedy
was absent. That is the instrument-cannot-fail-toward-its-target shape this
card is about, inside the pin for it.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WLJQhde67SeTccsmnBVarV
@claudeclaudeBot added the skip-changeset PR has no user-facing published change; bypasses the changeset gate label Sep 1, 2026
@baozhoutao
baozhoutao marked this pull request as ready for review September 1, 2026 14:30
@baozhoutao
baozhoutao added this pull request to the merge queueSep 1, 2026
Merged via the queue into main with commit 66ecc50Sep 1, 2026
34 checks passed
@baozhoutao
baozhoutao deleted the claude/issue-13642-family-print-reconciliation branch September 1, 2026 14:53
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size/mskip-changesetPR has no user-facing published change; bypasses the changeset gate

Projects

None yet

2 participants

@baozhoutao@claude
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

fix(pm): reconcile dispatch-gates' family sections with a total a harvest can be asserted against - #14221

Merged
baozhoutao merged 2 commits into
mainfrom
claude/issue-13642-family-print-reconciliation
Sep 1, 2026
Merged

fix(pm): reconcile dispatch-gates' family sections with a total a harvest can be asserted against#14221
baozhoutao merged 2 commits into
mainfrom
claude/issue-13642-family-print-reconciliation

Conversation

@claude

@claudeclaudeBot commented Sep 1, 2026

Copy link
Copy Markdown
Contributor

Fixes#13642

Two differently-shaped sections carry a card's runnable answer in dispatch-gates' human rendering, and nothing in that rendering stated their union. Two independent devs each harvested one section, ran it green, and reddened CI on a family the other section had named. This adds the missing control: a total a harvest can be asserted against, derived from the same structure the sections print from.

Premise re-check (the card predates PR #14207's +603 lines)

The card describes "two structurally different sections". Re-derived on 24b66352, the human block now has seven sections, of which exactly two carry this card's runnable answer:

sectionshapein the runnable answer?
Local gates for this card - CMD [wf] matched via ...yes — path-derived
spelling footerN families — P pnpm, K direct node.it is the count for the block above
Convention-triggered gates - CMD — why, under a per-kind headingyes — kind-derived
Once a changeset exists, N more... - CMD [wf] would match ...no, and deliberately so
Unreachable — ...standing repo factno
Always runs — ... - [wf · job] stepno
Residue — ...bucket countsno

So the card's premise holds, and its taxonomy was two-thirds of the picture. The authority for "what this card owes" is commandsFor(...) — matched UNION convention, deduped — which is exactly what --commands and --json already render.

The measurement that made the defect reproducible today

On 24b66352, for the card packages/spec/src/foo.test.ts:

matched block rows ......... 40
--commands ................. 44 (40 by path, 6 by kind, 2 reached both ways)
the spelling footer said ... "40 families — 28 pnpm, 12 direct node."

⭐ The only count line adjacent to the harvestable block stated a subtotal in the vocabulary of a total. A reader who harvested the matched block, counted 40, and read the footer got a reconciliation that agreed — on the wrong list. Both incidents on the card lost the convention block specifically, and check:engine-double-contract — occurrence 2's missing gate — is in the convention block on this very input.

What this changes (one file, scripts/pm/dispatch-gates.mjs)

  1. familyReconciliation — the total and its parts, built from the same two expressions commandsFor unions, not from a second traversal. Add a family to either input and both the section and its term move together. The identity matched + convention − both === total then holds by set algebra, and is asserted anyway: a mismatch throws and refuses rather than printing a total that cannot be trusted (check:react-declaration-parity 是唯一没接进任何 workflow 的源码审计门禁,且无 MANIFEST 时静默 skip 退出 0 —— 它现在永远不可能红 #4690).
  2. familyReconciliationLines — printed on every completed derivation, including at zero (an absent number is not assertable; its absence would mean "this card owes nothing" and "this build has no reconciliation" at once — the argument derive already makes for the tier verdict). Carries the arithmetic, the short-harvest warning where a section really is droppable, the --commands escape hatch inline, and a disclaimer that the total is not what CI runs.
  3. spellingFooterLines — no longer spells its matched-block subtotal as N families. It says N matched families, and when families sit outside its block it forward-points to the total, at the harvest site, where a consumer who never scrolls further still meets it.

⛔ Explicitly not done, per the triage ruling: no new output mode (--json and --commands both already exist), no ALL FAMILIES: block, no "please read more carefully" prose, and nothing in dispatch-order or teaching material — that limb is skills-lane governed territory.

Rendered output, on the input measured above

40 matched families — 28 pnpm, 12 direct node.
⛔ Two spellings, deliberately ... takes 28 of the 40 and reports nothing missing.
⇒ Harvest with --commands ...
Holding captured text already? ...
⛔ ...and a harvest of this block is 40 of the 44 this card owes, whichever spelling it takes: 4 more
famil(ies) are named by change KIND and print under their own heading below, outside every harvest
of THIS block — the published snippet above included.
The Reconciliation line under that heading carries the 44. Assert your list against THAT number, never against this one.
Convention-triggered gates (this change KIND moves them; no path derivation can name them):
... 6 rows, check:engine-double-contract among them ...
Reconciliation — 44 famil(ies): this card's WHOLE runnable answer, and the number to assert a harvest against.
40 named by PATH (the matched block) + 6 named by change KIND (the convention block), 2 of them the same
family reached both ways ⇒ 44 distinct.
⛔ A harvest that ends at ONE section is SHORT and reports nothing missing: the matched block alone is 40
of the 44, the convention block alone is 6 of the 44. Two cards lost the convention block exactly this
way, hours apart, and CI found it both times.
⇒ Skip the arithmetic: --commands prints exactly these 44, one runnable command per line, nothing else on stdout.
⛔ 44 is what THIS CARD owes by path and kind — NOT a complete account of what CI runs on the PR.

Verification

Union re-run on the final commit 1644b978.

pnpm check:pm-dispatch-gates — the tool's own battery, which is also the self-test surface for this file:

✓ dispatch-gates self-test: 1140 cases pass.
os-verify-lock: VERDICT command-exit 0 · held the lock 535s

26 new cases, all green, including the end-to-end pair that only a real run can hold:

✓ the reconciliation total IS the commandsFor union, not a recount of it
✓ adding a convention family moves the convention term AND the total
✓ suppressing the convention section moves the convention term AND the total
✓ the total and its parts close over EVERY overlap of the two inputs — 64 of 64
✓ the DEFAULT run prints the reconciliation — no flag, because a control behind a flag is worth nothing
✓ and its total is exactly what --commands returns for the same card
✓ the PATH term equals the rows the published snippet harvests from the matched block
✓ and the change-KIND term equals the distinct runnable rows the convention block really printed
✓ CONTROL: a matched-block-only harvest of this card is STILL short — the defect is real and untouched
✓ CONTROL: and the reconciliation total DETECTS that harvest as short rather than agreeing with it

Ablations — both legs proven on disk, both restored byte-identical

A — remove the call site from derive. Mutation confirmed on disk (call-site occurrences 1 → 0, injected marker 1; blob 6dbebbe2b0c6ed8c), then the real battery run on the mutated tree:

✗ the DEFAULT run prints the reconciliation — no flag, because a control behind a flag is worth nothing
✗ and its total is exactly what --commands returns for the same card
✗ the PATH term equals the rows the published snippet harvests from the matched block
✗ and the change-KIND term equals the distinct runnable rows the convention block really printed
✗ the matched footer forward-points to that total from inside the block being harvested
✗ dispatch-gates self-test: 5 of 1140 case(s) failed.

⭐ That ablation also found a vacuous assertion in my own pin: with the line absent, reconTotal is NaN, and NaN !== convBlock.length is TRUE — so the CONTROL case asserting the total detects a short harvest passed while the remedy was gone. That is this card's own defect shape inside the pin for it. Fixed in 1644b978 by requiring Number.isInteger(reconTotal) first.

B — break the closure identity (replace the overlap computation with an independent 0). Declared narrowing: the assert is in a pure function one call fully exercises, so this leg drove the function directly rather than the 9-minute battery. Mutation confirmed on disk (anchor 1 → 0, marker 1; blob differs from HEAD), control call on the unmutated tree returned a closing reconciliation first:

REFUSED, as designed:
dispatch-gates: the family reconciliation does not close — 1 matched + 1 convention − 0 both ≠ 1 distinct. ...

and the real human run exited 2 and printed ZERO reconciliation lines — it refuses rather than printing a wrong total.

Restore proven for both, not read off an exit code: git diff HEAD empty, git status clean, blob hash equal to the HEAD blob, 0 ablation markers left in the file.

Gate family, re-derived from the actual diff on 1644b978

node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack (no paths — the script takes the change set off the merge base itself) derives 15 families, and its own new line reconciles them: Reconciliation — 15 famil(ies) ... 15 named by PATH + 0 named by change KIND ⇒ 15 distinct. All 15 run, all green:

gateexit
pnpm check:pm-dispatch-gates0 — 1140 cases
pnpm check:declared-population-live0
pnpm check:watch-hint-literal0
pnpm check:parse-guard0
pnpm check:entry-guard0
pnpm check:agent-test-spelling0
pnpm check:bash32-floor0
pnpm check:cli-command-ids0
pnpm check:pnpm-filter-targets0
pnpm check:cross-package-test-inputs0
node scripts/check-cross-package-test-inputs.mjs0
node scripts/check-self-test-wired.mjs0
node scripts/check-ci-filter-parity.mjs0
node scripts/check-shard-attestation.mjs0
node scripts/check-test-completeness.mjsNOT MEASURED

check-test-completeness exits 3 = PREREQUISITE NOT MET, its own words: it grades a saved turbo run test log, there is none locally, and its message says the derived family names it with no argument and "the local reading for this gate is NOT MEASURED. ⛔ It is not a red." The other two exited 3 for the same class (yaml not installed in a fresh worktree) and were re-run to a real 0 after pnpm install.

Also run, per the dispatch: node scripts/pm/bare-root-worklist.mjs --self-test (0), node scripts/check-ratchet-remedy-authority.mjs (0), pnpm check:nul-bytes (0, plus a direct grep -naP control-byte scan of the changed file — none).

Every exit code above was captured before any pipe (cmd > log 2>&1; EXIT=$?), and each verdict is quoted from the gate's own line.

ESLint — a declared, measured narrowing, not a skip

The repo-wide pnpm lint is CI's run. Locally this was narrowed to the one changed file, with the three things that make a narrowing a measurement:

  1. Population from eslint's own config — one root eslint.config.mjs, resolved by eslint itself; the run used the same --no-inline-config the root lint script uses.
  2. Count from --format json — 1 file linted, 0 errors, 0 warnings.
  3. Invariance for untouched files — the config's own declaration (eslint.config.mjs, the QUERY_OPTIONS_TEST_GLOBS header) states this repo "runs one eslint.config.mjs, which never enables type-aware linting (no parserOptions.project, no typed @typescript-eslint rules) for ANY file", measured there with a positive control. A one-file .mjs edit therefore cannot move any untouched file's verdict.

Stale-tree warning, checked rather than carried

The derivation warns that scripts/check-stack-collection-maps.mjs changed on origin/main after this branch point. Read: the literals its new version names are its own file path, which does not cover scripts/pm/dispatch-gates.mjs (a bare scripts word is refused as too generic), so it does not change this card's family. Recorded as examined, not as an unread warning.

Notes for review

  • skip-changeset — the diff is scripts/pm/** only and publishes nothing from any package.
  • The reconciliation line names --commands inline. The file header already says it, but the header is not where a harvesting consumer looks, and the triage's own diagnosis is that adoption — not availability — is what failed. Flagged as a deliberate choice rather than a silent one.
  • Placement is below the convention block: at the top it would state a figure before the sections it reconciles had printed, and under the residue it would sit past the point a harvesting reader stops.

Generated by Claude Code


Generated by Claude Code

…vest can be asserted against
The human rendering places a card's runnable answer in two differently
shaped sections — a path-derived matched block and a kind-derived
convention block — and nothing in it stated the union. Two independent
devs each harvested one section, ran it green, and reddened CI on a
family the other named; a third reader misread the same output a third
way. Both machine-readable modes already existed, so the gap was not a
missing mode but a missing control: a partial harvest was plausible
rather than detectable.
- familyReconciliation derives the total and its parts from the SAME two
expressions commandsFor unions, so a family added to either input moves
the section and the count together. The closure identity is asserted
and refuses rather than printing an untrustworthy total.
- familyReconciliationLines prints it on every completed derivation,
including at zero, with the arithmetic tying it to both sections and
the --commands escape hatch named inline.
- spellingFooterLines no longer spells its matched-block subtotal as
`N families`: that was the line a dropped-section harvest reconciled
against, successfully, on the wrong list. It now names its scope and
forward-points to the total when families sit outside its block.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WLJQhde67SeTccsmnBVarV
…fore claiming detection
Measured while ablating the call site out of derive(): with no line printed
reconTotal is NaN, and `NaN !== convBlock.length` is TRUE — so the case
asserting that the total detects a short harvest passed while the remedy
was absent. That is the instrument-cannot-fail-toward-its-target shape this
card is about, inside the pin for it.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WLJQhde67SeTccsmnBVarV
@claudeclaudeBot added the skip-changeset PR has no user-facing published change; bypasses the changeset gate label Sep 1, 2026
@baozhoutao
baozhoutao marked this pull request as ready for review September 1, 2026 14:30
@baozhoutao
baozhoutao added this pull request to the merge queueSep 1, 2026
Merged via the queue into main with commit 66ecc50Sep 1, 2026
34 checks passed
@baozhoutao
baozhoutao deleted the claude/issue-13642-family-print-reconciliation branch September 1, 2026 14:53
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size/mskip-changesetPR has no user-facing published change; bypasses the changeset gate

Projects

None yet

2 participants

@baozhoutao@claude
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content

fix(pm): reconcile dispatch-gates' family sections with a total a harvest can be asserted against - #14221

Merged
baozhoutao merged 2 commits into
mainfrom
claude/issue-13642-family-print-reconciliation
Sep 1, 2026
Merged

fix(pm): reconcile dispatch-gates' family sections with a total a harvest can be asserted against#14221
baozhoutao merged 2 commits into
mainfrom
claude/issue-13642-family-print-reconciliation

Conversation

@claude

@claudeclaudeBot commented Sep 1, 2026

Copy link
Copy Markdown
Contributor

Fixes#13642

Two differently-shaped sections carry a card's runnable answer in dispatch-gates' human rendering, and nothing in that rendering stated their union. Two independent devs each harvested one section, ran it green, and reddened CI on a family the other section had named. This adds the missing control: a total a harvest can be asserted against, derived from the same structure the sections print from.

Premise re-check (the card predates PR #14207's +603 lines)

The card describes "two structurally different sections". Re-derived on 24b66352, the human block now has seven sections, of which exactly two carry this card's runnable answer:

sectionshapein the runnable answer?
Local gates for this card - CMD [wf] matched via ...yes — path-derived
spelling footerN families — P pnpm, K direct node.it is the count for the block above
Convention-triggered gates - CMD — why, under a per-kind headingyes — kind-derived
Once a changeset exists, N more... - CMD [wf] would match ...no, and deliberately so
Unreachable — ...standing repo factno
Always runs — ... - [wf · job] stepno
Residue — ...bucket countsno

So the card's premise holds, and its taxonomy was two-thirds of the picture. The authority for "what this card owes" is commandsFor(...) — matched UNION convention, deduped — which is exactly what --commands and --json already render.

The measurement that made the defect reproducible today

On 24b66352, for the card packages/spec/src/foo.test.ts:

matched block rows ......... 40
--commands ................. 44 (40 by path, 6 by kind, 2 reached both ways)
the spelling footer said ... "40 families — 28 pnpm, 12 direct node."

⭐ The only count line adjacent to the harvestable block stated a subtotal in the vocabulary of a total. A reader who harvested the matched block, counted 40, and read the footer got a reconciliation that agreed — on the wrong list. Both incidents on the card lost the convention block specifically, and check:engine-double-contract — occurrence 2's missing gate — is in the convention block on this very input.

What this changes (one file, scripts/pm/dispatch-gates.mjs)

  1. familyReconciliation — the total and its parts, built from the same two expressions commandsFor unions, not from a second traversal. Add a family to either input and both the section and its term move together. The identity matched + convention − both === total then holds by set algebra, and is asserted anyway: a mismatch throws and refuses rather than printing a total that cannot be trusted (check:react-declaration-parity 是唯一没接进任何 workflow 的源码审计门禁,且无 MANIFEST 时静默 skip 退出 0 —— 它现在永远不可能红 #4690).
  2. familyReconciliationLines — printed on every completed derivation, including at zero (an absent number is not assertable; its absence would mean "this card owes nothing" and "this build has no reconciliation" at once — the argument derive already makes for the tier verdict). Carries the arithmetic, the short-harvest warning where a section really is droppable, the --commands escape hatch inline, and a disclaimer that the total is not what CI runs.
  3. spellingFooterLines — no longer spells its matched-block subtotal as N families. It says N matched families, and when families sit outside its block it forward-points to the total, at the harvest site, where a consumer who never scrolls further still meets it.

⛔ Explicitly not done, per the triage ruling: no new output mode (--json and --commands both already exist), no ALL FAMILIES: block, no "please read more carefully" prose, and nothing in dispatch-order or teaching material — that limb is skills-lane governed territory.

Rendered output, on the input measured above

40 matched families — 28 pnpm, 12 direct node.
⛔ Two spellings, deliberately ... takes 28 of the 40 and reports nothing missing.
⇒ Harvest with --commands ...
Holding captured text already? ...
⛔ ...and a harvest of this block is 40 of the 44 this card owes, whichever spelling it takes: 4 more
famil(ies) are named by change KIND and print under their own heading below, outside every harvest
of THIS block — the published snippet above included.
The Reconciliation line under that heading carries the 44. Assert your list against THAT number, never against this one.
Convention-triggered gates (this change KIND moves them; no path derivation can name them):
... 6 rows, check:engine-double-contract among them ...
Reconciliation — 44 famil(ies): this card's WHOLE runnable answer, and the number to assert a harvest against.
40 named by PATH (the matched block) + 6 named by change KIND (the convention block), 2 of them the same
family reached both ways ⇒ 44 distinct.
⛔ A harvest that ends at ONE section is SHORT and reports nothing missing: the matched block alone is 40
of the 44, the convention block alone is 6 of the 44. Two cards lost the convention block exactly this
way, hours apart, and CI found it both times.
⇒ Skip the arithmetic: --commands prints exactly these 44, one runnable command per line, nothing else on stdout.
⛔ 44 is what THIS CARD owes by path and kind — NOT a complete account of what CI runs on the PR.

Verification

Union re-run on the final commit 1644b978.

pnpm check:pm-dispatch-gates — the tool's own battery, which is also the self-test surface for this file:

✓ dispatch-gates self-test: 1140 cases pass.
os-verify-lock: VERDICT command-exit 0 · held the lock 535s

26 new cases, all green, including the end-to-end pair that only a real run can hold:

✓ the reconciliation total IS the commandsFor union, not a recount of it
✓ adding a convention family moves the convention term AND the total
✓ suppressing the convention section moves the convention term AND the total
✓ the total and its parts close over EVERY overlap of the two inputs — 64 of 64
✓ the DEFAULT run prints the reconciliation — no flag, because a control behind a flag is worth nothing
✓ and its total is exactly what --commands returns for the same card
✓ the PATH term equals the rows the published snippet harvests from the matched block
✓ and the change-KIND term equals the distinct runnable rows the convention block really printed
✓ CONTROL: a matched-block-only harvest of this card is STILL short — the defect is real and untouched
✓ CONTROL: and the reconciliation total DETECTS that harvest as short rather than agreeing with it

Ablations — both legs proven on disk, both restored byte-identical

A — remove the call site from derive. Mutation confirmed on disk (call-site occurrences 1 → 0, injected marker 1; blob 6dbebbe2b0c6ed8c), then the real battery run on the mutated tree:

✗ the DEFAULT run prints the reconciliation — no flag, because a control behind a flag is worth nothing
✗ and its total is exactly what --commands returns for the same card
✗ the PATH term equals the rows the published snippet harvests from the matched block
✗ and the change-KIND term equals the distinct runnable rows the convention block really printed
✗ the matched footer forward-points to that total from inside the block being harvested
✗ dispatch-gates self-test: 5 of 1140 case(s) failed.

⭐ That ablation also found a vacuous assertion in my own pin: with the line absent, reconTotal is NaN, and NaN !== convBlock.length is TRUE — so the CONTROL case asserting the total detects a short harvest passed while the remedy was gone. That is this card's own defect shape inside the pin for it. Fixed in 1644b978 by requiring Number.isInteger(reconTotal) first.

B — break the closure identity (replace the overlap computation with an independent 0). Declared narrowing: the assert is in a pure function one call fully exercises, so this leg drove the function directly rather than the 9-minute battery. Mutation confirmed on disk (anchor 1 → 0, marker 1; blob differs from HEAD), control call on the unmutated tree returned a closing reconciliation first:

REFUSED, as designed:
dispatch-gates: the family reconciliation does not close — 1 matched + 1 convention − 0 both ≠ 1 distinct. ...

and the real human run exited 2 and printed ZERO reconciliation lines — it refuses rather than printing a wrong total.

Restore proven for both, not read off an exit code: git diff HEAD empty, git status clean, blob hash equal to the HEAD blob, 0 ablation markers left in the file.

Gate family, re-derived from the actual diff on 1644b978

node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack (no paths — the script takes the change set off the merge base itself) derives 15 families, and its own new line reconciles them: Reconciliation — 15 famil(ies) ... 15 named by PATH + 0 named by change KIND ⇒ 15 distinct. All 15 run, all green:

gateexit
pnpm check:pm-dispatch-gates0 — 1140 cases
pnpm check:declared-population-live0
pnpm check:watch-hint-literal0
pnpm check:parse-guard0
pnpm check:entry-guard0
pnpm check:agent-test-spelling0
pnpm check:bash32-floor0
pnpm check:cli-command-ids0
pnpm check:pnpm-filter-targets0
pnpm check:cross-package-test-inputs0
node scripts/check-cross-package-test-inputs.mjs0
node scripts/check-self-test-wired.mjs0
node scripts/check-ci-filter-parity.mjs0
node scripts/check-shard-attestation.mjs0
node scripts/check-test-completeness.mjsNOT MEASURED

check-test-completeness exits 3 = PREREQUISITE NOT MET, its own words: it grades a saved turbo run test log, there is none locally, and its message says the derived family names it with no argument and "the local reading for this gate is NOT MEASURED. ⛔ It is not a red." The other two exited 3 for the same class (yaml not installed in a fresh worktree) and were re-run to a real 0 after pnpm install.

Also run, per the dispatch: node scripts/pm/bare-root-worklist.mjs --self-test (0), node scripts/check-ratchet-remedy-authority.mjs (0), pnpm check:nul-bytes (0, plus a direct grep -naP control-byte scan of the changed file — none).

Every exit code above was captured before any pipe (cmd > log 2>&1; EXIT=$?), and each verdict is quoted from the gate's own line.

ESLint — a declared, measured narrowing, not a skip

The repo-wide pnpm lint is CI's run. Locally this was narrowed to the one changed file, with the three things that make a narrowing a measurement:

  1. Population from eslint's own config — one root eslint.config.mjs, resolved by eslint itself; the run used the same --no-inline-config the root lint script uses.
  2. Count from --format json — 1 file linted, 0 errors, 0 warnings.
  3. Invariance for untouched files — the config's own declaration (eslint.config.mjs, the QUERY_OPTIONS_TEST_GLOBS header) states this repo "runs one eslint.config.mjs, which never enables type-aware linting (no parserOptions.project, no typed @typescript-eslint rules) for ANY file", measured there with a positive control. A one-file .mjs edit therefore cannot move any untouched file's verdict.

Stale-tree warning, checked rather than carried

The derivation warns that scripts/check-stack-collection-maps.mjs changed on origin/main after this branch point. Read: the literals its new version names are its own file path, which does not cover scripts/pm/dispatch-gates.mjs (a bare scripts word is refused as too generic), so it does not change this card's family. Recorded as examined, not as an unread warning.

Notes for review

  • skip-changeset — the diff is scripts/pm/** only and publishes nothing from any package.
  • The reconciliation line names --commands inline. The file header already says it, but the header is not where a harvesting consumer looks, and the triage's own diagnosis is that adoption — not availability — is what failed. Flagged as a deliberate choice rather than a silent one.
  • Placement is below the convention block: at the top it would state a figure before the sections it reconciles had printed, and under the residue it would sit past the point a harvesting reader stops.

Generated by Claude Code


Generated by Claude Code

…vest can be asserted against
The human rendering places a card's runnable answer in two differently
shaped sections — a path-derived matched block and a kind-derived
convention block — and nothing in it stated the union. Two independent
devs each harvested one section, ran it green, and reddened CI on a
family the other named; a third reader misread the same output a third
way. Both machine-readable modes already existed, so the gap was not a
missing mode but a missing control: a partial harvest was plausible
rather than detectable.
- familyReconciliation derives the total and its parts from the SAME two
expressions commandsFor unions, so a family added to either input moves
the section and the count together. The closure identity is asserted
and refuses rather than printing an untrustworthy total.
- familyReconciliationLines prints it on every completed derivation,
including at zero, with the arithmetic tying it to both sections and
the --commands escape hatch named inline.
- spellingFooterLines no longer spells its matched-block subtotal as
`N families`: that was the line a dropped-section harvest reconciled
against, successfully, on the wrong list. It now names its scope and
forward-points to the total when families sit outside its block.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WLJQhde67SeTccsmnBVarV
…fore claiming detection
Measured while ablating the call site out of derive(): with no line printed
reconTotal is NaN, and `NaN !== convBlock.length` is TRUE — so the case
asserting that the total detects a short harvest passed while the remedy
was absent. That is the instrument-cannot-fail-toward-its-target shape this
card is about, inside the pin for it.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WLJQhde67SeTccsmnBVarV
@claudeclaudeBot added the skip-changeset PR has no user-facing published change; bypasses the changeset gate label Sep 1, 2026
@baozhoutao
baozhoutao marked this pull request as ready for review September 1, 2026 14:30
@baozhoutao
baozhoutao added this pull request to the merge queueSep 1, 2026
Merged via the queue into main with commit 66ecc50Sep 1, 2026
34 checks passed
@baozhoutao
baozhoutao deleted the claude/issue-13642-family-print-reconciliation branch September 1, 2026 14:53
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size/mskip-changesetPR has no user-facing published change; bypasses the changeset gate

Projects

None yet

2 participants

@baozhoutao@claude
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

fix(pm): reconcile dispatch-gates' family sections with a total a harvest can be asserted against - #14221

Merged
baozhoutao merged 2 commits into
mainfrom
claude/issue-13642-family-print-reconciliation
Sep 1, 2026
Merged

fix(pm): reconcile dispatch-gates' family sections with a total a harvest can be asserted against#14221
baozhoutao merged 2 commits into
mainfrom
claude/issue-13642-family-print-reconciliation

Conversation

@claude

@claudeclaudeBot commented Sep 1, 2026

Copy link
Copy Markdown
Contributor

Fixes#13642

Two differently-shaped sections carry a card's runnable answer in dispatch-gates' human rendering, and nothing in that rendering stated their union. Two independent devs each harvested one section, ran it green, and reddened CI on a family the other section had named. This adds the missing control: a total a harvest can be asserted against, derived from the same structure the sections print from.

Premise re-check (the card predates PR #14207's +603 lines)

The card describes "two structurally different sections". Re-derived on 24b66352, the human block now has seven sections, of which exactly two carry this card's runnable answer:

sectionshapein the runnable answer?
Local gates for this card - CMD [wf] matched via ...yes — path-derived
spelling footerN families — P pnpm, K direct node.it is the count for the block above
Convention-triggered gates - CMD — why, under a per-kind headingyes — kind-derived
Once a changeset exists, N more... - CMD [wf] would match ...no, and deliberately so
Unreachable — ...standing repo factno
Always runs — ... - [wf · job] stepno
Residue — ...bucket countsno

So the card's premise holds, and its taxonomy was two-thirds of the picture. The authority for "what this card owes" is commandsFor(...) — matched UNION convention, deduped — which is exactly what --commands and --json already render.

The measurement that made the defect reproducible today

On 24b66352, for the card packages/spec/src/foo.test.ts:

matched block rows ......... 40
--commands ................. 44 (40 by path, 6 by kind, 2 reached both ways)
the spelling footer said ... "40 families — 28 pnpm, 12 direct node."

⭐ The only count line adjacent to the harvestable block stated a subtotal in the vocabulary of a total. A reader who harvested the matched block, counted 40, and read the footer got a reconciliation that agreed — on the wrong list. Both incidents on the card lost the convention block specifically, and check:engine-double-contract — occurrence 2's missing gate — is in the convention block on this very input.

What this changes (one file, scripts/pm/dispatch-gates.mjs)

  1. familyReconciliation — the total and its parts, built from the same two expressions commandsFor unions, not from a second traversal. Add a family to either input and both the section and its term move together. The identity matched + convention − both === total then holds by set algebra, and is asserted anyway: a mismatch throws and refuses rather than printing a total that cannot be trusted (check:react-declaration-parity 是唯一没接进任何 workflow 的源码审计门禁,且无 MANIFEST 时静默 skip 退出 0 —— 它现在永远不可能红 #4690).
  2. familyReconciliationLines — printed on every completed derivation, including at zero (an absent number is not assertable; its absence would mean "this card owes nothing" and "this build has no reconciliation" at once — the argument derive already makes for the tier verdict). Carries the arithmetic, the short-harvest warning where a section really is droppable, the --commands escape hatch inline, and a disclaimer that the total is not what CI runs.
  3. spellingFooterLines — no longer spells its matched-block subtotal as N families. It says N matched families, and when families sit outside its block it forward-points to the total, at the harvest site, where a consumer who never scrolls further still meets it.

⛔ Explicitly not done, per the triage ruling: no new output mode (--json and --commands both already exist), no ALL FAMILIES: block, no "please read more carefully" prose, and nothing in dispatch-order or teaching material — that limb is skills-lane governed territory.

Rendered output, on the input measured above

40 matched families — 28 pnpm, 12 direct node.
⛔ Two spellings, deliberately ... takes 28 of the 40 and reports nothing missing.
⇒ Harvest with --commands ...
Holding captured text already? ...
⛔ ...and a harvest of this block is 40 of the 44 this card owes, whichever spelling it takes: 4 more
famil(ies) are named by change KIND and print under their own heading below, outside every harvest
of THIS block — the published snippet above included.
The Reconciliation line under that heading carries the 44. Assert your list against THAT number, never against this one.
Convention-triggered gates (this change KIND moves them; no path derivation can name them):
... 6 rows, check:engine-double-contract among them ...
Reconciliation — 44 famil(ies): this card's WHOLE runnable answer, and the number to assert a harvest against.
40 named by PATH (the matched block) + 6 named by change KIND (the convention block), 2 of them the same
family reached both ways ⇒ 44 distinct.
⛔ A harvest that ends at ONE section is SHORT and reports nothing missing: the matched block alone is 40
of the 44, the convention block alone is 6 of the 44. Two cards lost the convention block exactly this
way, hours apart, and CI found it both times.
⇒ Skip the arithmetic: --commands prints exactly these 44, one runnable command per line, nothing else on stdout.
⛔ 44 is what THIS CARD owes by path and kind — NOT a complete account of what CI runs on the PR.

Verification

Union re-run on the final commit 1644b978.

pnpm check:pm-dispatch-gates — the tool's own battery, which is also the self-test surface for this file:

✓ dispatch-gates self-test: 1140 cases pass.
os-verify-lock: VERDICT command-exit 0 · held the lock 535s

26 new cases, all green, including the end-to-end pair that only a real run can hold:

✓ the reconciliation total IS the commandsFor union, not a recount of it
✓ adding a convention family moves the convention term AND the total
✓ suppressing the convention section moves the convention term AND the total
✓ the total and its parts close over EVERY overlap of the two inputs — 64 of 64
✓ the DEFAULT run prints the reconciliation — no flag, because a control behind a flag is worth nothing
✓ and its total is exactly what --commands returns for the same card
✓ the PATH term equals the rows the published snippet harvests from the matched block
✓ and the change-KIND term equals the distinct runnable rows the convention block really printed
✓ CONTROL: a matched-block-only harvest of this card is STILL short — the defect is real and untouched
✓ CONTROL: and the reconciliation total DETECTS that harvest as short rather than agreeing with it

Ablations — both legs proven on disk, both restored byte-identical

A — remove the call site from derive. Mutation confirmed on disk (call-site occurrences 1 → 0, injected marker 1; blob 6dbebbe2b0c6ed8c), then the real battery run on the mutated tree:

✗ the DEFAULT run prints the reconciliation — no flag, because a control behind a flag is worth nothing
✗ and its total is exactly what --commands returns for the same card
✗ the PATH term equals the rows the published snippet harvests from the matched block
✗ and the change-KIND term equals the distinct runnable rows the convention block really printed
✗ the matched footer forward-points to that total from inside the block being harvested
✗ dispatch-gates self-test: 5 of 1140 case(s) failed.

⭐ That ablation also found a vacuous assertion in my own pin: with the line absent, reconTotal is NaN, and NaN !== convBlock.length is TRUE — so the CONTROL case asserting the total detects a short harvest passed while the remedy was gone. That is this card's own defect shape inside the pin for it. Fixed in 1644b978 by requiring Number.isInteger(reconTotal) first.

B — break the closure identity (replace the overlap computation with an independent 0). Declared narrowing: the assert is in a pure function one call fully exercises, so this leg drove the function directly rather than the 9-minute battery. Mutation confirmed on disk (anchor 1 → 0, marker 1; blob differs from HEAD), control call on the unmutated tree returned a closing reconciliation first:

REFUSED, as designed:
dispatch-gates: the family reconciliation does not close — 1 matched + 1 convention − 0 both ≠ 1 distinct. ...

and the real human run exited 2 and printed ZERO reconciliation lines — it refuses rather than printing a wrong total.

Restore proven for both, not read off an exit code: git diff HEAD empty, git status clean, blob hash equal to the HEAD blob, 0 ablation markers left in the file.

Gate family, re-derived from the actual diff on 1644b978

node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack (no paths — the script takes the change set off the merge base itself) derives 15 families, and its own new line reconciles them: Reconciliation — 15 famil(ies) ... 15 named by PATH + 0 named by change KIND ⇒ 15 distinct. All 15 run, all green:

gateexit
pnpm check:pm-dispatch-gates0 — 1140 cases
pnpm check:declared-population-live0
pnpm check:watch-hint-literal0
pnpm check:parse-guard0
pnpm check:entry-guard0
pnpm check:agent-test-spelling0
pnpm check:bash32-floor0
pnpm check:cli-command-ids0
pnpm check:pnpm-filter-targets0
pnpm check:cross-package-test-inputs0
node scripts/check-cross-package-test-inputs.mjs0
node scripts/check-self-test-wired.mjs0
node scripts/check-ci-filter-parity.mjs0
node scripts/check-shard-attestation.mjs0
node scripts/check-test-completeness.mjsNOT MEASURED

check-test-completeness exits 3 = PREREQUISITE NOT MET, its own words: it grades a saved turbo run test log, there is none locally, and its message says the derived family names it with no argument and "the local reading for this gate is NOT MEASURED. ⛔ It is not a red." The other two exited 3 for the same class (yaml not installed in a fresh worktree) and were re-run to a real 0 after pnpm install.

Also run, per the dispatch: node scripts/pm/bare-root-worklist.mjs --self-test (0), node scripts/check-ratchet-remedy-authority.mjs (0), pnpm check:nul-bytes (0, plus a direct grep -naP control-byte scan of the changed file — none).

Every exit code above was captured before any pipe (cmd > log 2>&1; EXIT=$?), and each verdict is quoted from the gate's own line.

ESLint — a declared, measured narrowing, not a skip

The repo-wide pnpm lint is CI's run. Locally this was narrowed to the one changed file, with the three things that make a narrowing a measurement:

  1. Population from eslint's own config — one root eslint.config.mjs, resolved by eslint itself; the run used the same --no-inline-config the root lint script uses.
  2. Count from --format json — 1 file linted, 0 errors, 0 warnings.
  3. Invariance for untouched files — the config's own declaration (eslint.config.mjs, the QUERY_OPTIONS_TEST_GLOBS header) states this repo "runs one eslint.config.mjs, which never enables type-aware linting (no parserOptions.project, no typed @typescript-eslint rules) for ANY file", measured there with a positive control. A one-file .mjs edit therefore cannot move any untouched file's verdict.

Stale-tree warning, checked rather than carried

The derivation warns that scripts/check-stack-collection-maps.mjs changed on origin/main after this branch point. Read: the literals its new version names are its own file path, which does not cover scripts/pm/dispatch-gates.mjs (a bare scripts word is refused as too generic), so it does not change this card's family. Recorded as examined, not as an unread warning.

Notes for review

  • skip-changeset — the diff is scripts/pm/** only and publishes nothing from any package.
  • The reconciliation line names --commands inline. The file header already says it, but the header is not where a harvesting consumer looks, and the triage's own diagnosis is that adoption — not availability — is what failed. Flagged as a deliberate choice rather than a silent one.
  • Placement is below the convention block: at the top it would state a figure before the sections it reconciles had printed, and under the residue it would sit past the point a harvesting reader stops.

Generated by Claude Code


Generated by Claude Code

…vest can be asserted against
The human rendering places a card's runnable answer in two differently
shaped sections — a path-derived matched block and a kind-derived
convention block — and nothing in it stated the union. Two independent
devs each harvested one section, ran it green, and reddened CI on a
family the other named; a third reader misread the same output a third
way. Both machine-readable modes already existed, so the gap was not a
missing mode but a missing control: a partial harvest was plausible
rather than detectable.
- familyReconciliation derives the total and its parts from the SAME two
expressions commandsFor unions, so a family added to either input moves
the section and the count together. The closure identity is asserted
and refuses rather than printing an untrustworthy total.
- familyReconciliationLines prints it on every completed derivation,
including at zero, with the arithmetic tying it to both sections and
the --commands escape hatch named inline.
- spellingFooterLines no longer spells its matched-block subtotal as
`N families`: that was the line a dropped-section harvest reconciled
against, successfully, on the wrong list. It now names its scope and
forward-points to the total when families sit outside its block.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WLJQhde67SeTccsmnBVarV
…fore claiming detection
Measured while ablating the call site out of derive(): with no line printed
reconTotal is NaN, and `NaN !== convBlock.length` is TRUE — so the case
asserting that the total detects a short harvest passed while the remedy
was absent. That is the instrument-cannot-fail-toward-its-target shape this
card is about, inside the pin for it.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WLJQhde67SeTccsmnBVarV
@claudeclaudeBot added the skip-changeset PR has no user-facing published change; bypasses the changeset gate label Sep 1, 2026
@baozhoutao
baozhoutao marked this pull request as ready for review September 1, 2026 14:30
@baozhoutao
baozhoutao added this pull request to the merge queueSep 1, 2026
Merged via the queue into main with commit 66ecc50Sep 1, 2026
34 checks passed
@baozhoutao
baozhoutao deleted the claude/issue-13642-family-print-reconciliation branch September 1, 2026 14:53
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size/mskip-changesetPR has no user-facing published change; bypasses the changeset gate

Projects

None yet

2 participants

@baozhoutao@claude
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

fix(pm): reconcile dispatch-gates' family sections with a total a harvest can be asserted against - #14221

Merged
baozhoutao merged 2 commits into
mainfrom
claude/issue-13642-family-print-reconciliation
Sep 1, 2026
Merged

fix(pm): reconcile dispatch-gates' family sections with a total a harvest can be asserted against#14221
baozhoutao merged 2 commits into
mainfrom
claude/issue-13642-family-print-reconciliation

Conversation

@claude

@claudeclaudeBot commented Sep 1, 2026

Copy link
Copy Markdown
Contributor

Fixes#13642

Two differently-shaped sections carry a card's runnable answer in dispatch-gates' human rendering, and nothing in that rendering stated their union. Two independent devs each harvested one section, ran it green, and reddened CI on a family the other section had named. This adds the missing control: a total a harvest can be asserted against, derived from the same structure the sections print from.

Premise re-check (the card predates PR #14207's +603 lines)

The card describes "two structurally different sections". Re-derived on 24b66352, the human block now has seven sections, of which exactly two carry this card's runnable answer:

sectionshapein the runnable answer?
Local gates for this card - CMD [wf] matched via ...yes — path-derived
spelling footerN families — P pnpm, K direct node.it is the count for the block above
Convention-triggered gates - CMD — why, under a per-kind headingyes — kind-derived
Once a changeset exists, N more... - CMD [wf] would match ...no, and deliberately so
Unreachable — ...standing repo factno
Always runs — ... - [wf · job] stepno
Residue — ...bucket countsno

So the card's premise holds, and its taxonomy was two-thirds of the picture. The authority for "what this card owes" is commandsFor(...) — matched UNION convention, deduped — which is exactly what --commands and --json already render.

The measurement that made the defect reproducible today

On 24b66352, for the card packages/spec/src/foo.test.ts:

matched block rows ......... 40
--commands ................. 44 (40 by path, 6 by kind, 2 reached both ways)
the spelling footer said ... "40 families — 28 pnpm, 12 direct node."

⭐ The only count line adjacent to the harvestable block stated a subtotal in the vocabulary of a total. A reader who harvested the matched block, counted 40, and read the footer got a reconciliation that agreed — on the wrong list. Both incidents on the card lost the convention block specifically, and check:engine-double-contract — occurrence 2's missing gate — is in the convention block on this very input.

What this changes (one file, scripts/pm/dispatch-gates.mjs)

  1. familyReconciliation — the total and its parts, built from the same two expressions commandsFor unions, not from a second traversal. Add a family to either input and both the section and its term move together. The identity matched + convention − both === total then holds by set algebra, and is asserted anyway: a mismatch throws and refuses rather than printing a total that cannot be trusted (check:react-declaration-parity 是唯一没接进任何 workflow 的源码审计门禁,且无 MANIFEST 时静默 skip 退出 0 —— 它现在永远不可能红 #4690).
  2. familyReconciliationLines — printed on every completed derivation, including at zero (an absent number is not assertable; its absence would mean "this card owes nothing" and "this build has no reconciliation" at once — the argument derive already makes for the tier verdict). Carries the arithmetic, the short-harvest warning where a section really is droppable, the --commands escape hatch inline, and a disclaimer that the total is not what CI runs.
  3. spellingFooterLines — no longer spells its matched-block subtotal as N families. It says N matched families, and when families sit outside its block it forward-points to the total, at the harvest site, where a consumer who never scrolls further still meets it.

⛔ Explicitly not done, per the triage ruling: no new output mode (--json and --commands both already exist), no ALL FAMILIES: block, no "please read more carefully" prose, and nothing in dispatch-order or teaching material — that limb is skills-lane governed territory.

Rendered output, on the input measured above

40 matched families — 28 pnpm, 12 direct node.
⛔ Two spellings, deliberately ... takes 28 of the 40 and reports nothing missing.
⇒ Harvest with --commands ...
Holding captured text already? ...
⛔ ...and a harvest of this block is 40 of the 44 this card owes, whichever spelling it takes: 4 more
famil(ies) are named by change KIND and print under their own heading below, outside every harvest
of THIS block — the published snippet above included.
The Reconciliation line under that heading carries the 44. Assert your list against THAT number, never against this one.
Convention-triggered gates (this change KIND moves them; no path derivation can name them):
... 6 rows, check:engine-double-contract among them ...
Reconciliation — 44 famil(ies): this card's WHOLE runnable answer, and the number to assert a harvest against.
40 named by PATH (the matched block) + 6 named by change KIND (the convention block), 2 of them the same
family reached both ways ⇒ 44 distinct.
⛔ A harvest that ends at ONE section is SHORT and reports nothing missing: the matched block alone is 40
of the 44, the convention block alone is 6 of the 44. Two cards lost the convention block exactly this
way, hours apart, and CI found it both times.
⇒ Skip the arithmetic: --commands prints exactly these 44, one runnable command per line, nothing else on stdout.
⛔ 44 is what THIS CARD owes by path and kind — NOT a complete account of what CI runs on the PR.

Verification

Union re-run on the final commit 1644b978.

pnpm check:pm-dispatch-gates — the tool's own battery, which is also the self-test surface for this file:

✓ dispatch-gates self-test: 1140 cases pass.
os-verify-lock: VERDICT command-exit 0 · held the lock 535s

26 new cases, all green, including the end-to-end pair that only a real run can hold:

✓ the reconciliation total IS the commandsFor union, not a recount of it
✓ adding a convention family moves the convention term AND the total
✓ suppressing the convention section moves the convention term AND the total
✓ the total and its parts close over EVERY overlap of the two inputs — 64 of 64
✓ the DEFAULT run prints the reconciliation — no flag, because a control behind a flag is worth nothing
✓ and its total is exactly what --commands returns for the same card
✓ the PATH term equals the rows the published snippet harvests from the matched block
✓ and the change-KIND term equals the distinct runnable rows the convention block really printed
✓ CONTROL: a matched-block-only harvest of this card is STILL short — the defect is real and untouched
✓ CONTROL: and the reconciliation total DETECTS that harvest as short rather than agreeing with it

Ablations — both legs proven on disk, both restored byte-identical

A — remove the call site from derive. Mutation confirmed on disk (call-site occurrences 1 → 0, injected marker 1; blob 6dbebbe2b0c6ed8c), then the real battery run on the mutated tree:

✗ the DEFAULT run prints the reconciliation — no flag, because a control behind a flag is worth nothing
✗ and its total is exactly what --commands returns for the same card
✗ the PATH term equals the rows the published snippet harvests from the matched block
✗ and the change-KIND term equals the distinct runnable rows the convention block really printed
✗ the matched footer forward-points to that total from inside the block being harvested
✗ dispatch-gates self-test: 5 of 1140 case(s) failed.

⭐ That ablation also found a vacuous assertion in my own pin: with the line absent, reconTotal is NaN, and NaN !== convBlock.length is TRUE — so the CONTROL case asserting the total detects a short harvest passed while the remedy was gone. That is this card's own defect shape inside the pin for it. Fixed in 1644b978 by requiring Number.isInteger(reconTotal) first.

B — break the closure identity (replace the overlap computation with an independent 0). Declared narrowing: the assert is in a pure function one call fully exercises, so this leg drove the function directly rather than the 9-minute battery. Mutation confirmed on disk (anchor 1 → 0, marker 1; blob differs from HEAD), control call on the unmutated tree returned a closing reconciliation first:

REFUSED, as designed:
dispatch-gates: the family reconciliation does not close — 1 matched + 1 convention − 0 both ≠ 1 distinct. ...

and the real human run exited 2 and printed ZERO reconciliation lines — it refuses rather than printing a wrong total.

Restore proven for both, not read off an exit code: git diff HEAD empty, git status clean, blob hash equal to the HEAD blob, 0 ablation markers left in the file.

Gate family, re-derived from the actual diff on 1644b978

node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack (no paths — the script takes the change set off the merge base itself) derives 15 families, and its own new line reconciles them: Reconciliation — 15 famil(ies) ... 15 named by PATH + 0 named by change KIND ⇒ 15 distinct. All 15 run, all green:

gateexit
pnpm check:pm-dispatch-gates0 — 1140 cases
pnpm check:declared-population-live0
pnpm check:watch-hint-literal0
pnpm check:parse-guard0
pnpm check:entry-guard0
pnpm check:agent-test-spelling0
pnpm check:bash32-floor0
pnpm check:cli-command-ids0
pnpm check:pnpm-filter-targets0
pnpm check:cross-package-test-inputs0
node scripts/check-cross-package-test-inputs.mjs0
node scripts/check-self-test-wired.mjs0
node scripts/check-ci-filter-parity.mjs0
node scripts/check-shard-attestation.mjs0
node scripts/check-test-completeness.mjsNOT MEASURED

check-test-completeness exits 3 = PREREQUISITE NOT MET, its own words: it grades a saved turbo run test log, there is none locally, and its message says the derived family names it with no argument and "the local reading for this gate is NOT MEASURED. ⛔ It is not a red." The other two exited 3 for the same class (yaml not installed in a fresh worktree) and were re-run to a real 0 after pnpm install.

Also run, per the dispatch: node scripts/pm/bare-root-worklist.mjs --self-test (0), node scripts/check-ratchet-remedy-authority.mjs (0), pnpm check:nul-bytes (0, plus a direct grep -naP control-byte scan of the changed file — none).

Every exit code above was captured before any pipe (cmd > log 2>&1; EXIT=$?), and each verdict is quoted from the gate's own line.

ESLint — a declared, measured narrowing, not a skip

The repo-wide pnpm lint is CI's run. Locally this was narrowed to the one changed file, with the three things that make a narrowing a measurement:

  1. Population from eslint's own config — one root eslint.config.mjs, resolved by eslint itself; the run used the same --no-inline-config the root lint script uses.
  2. Count from --format json — 1 file linted, 0 errors, 0 warnings.
  3. Invariance for untouched files — the config's own declaration (eslint.config.mjs, the QUERY_OPTIONS_TEST_GLOBS header) states this repo "runs one eslint.config.mjs, which never enables type-aware linting (no parserOptions.project, no typed @typescript-eslint rules) for ANY file", measured there with a positive control. A one-file .mjs edit therefore cannot move any untouched file's verdict.

Stale-tree warning, checked rather than carried

The derivation warns that scripts/check-stack-collection-maps.mjs changed on origin/main after this branch point. Read: the literals its new version names are its own file path, which does not cover scripts/pm/dispatch-gates.mjs (a bare scripts word is refused as too generic), so it does not change this card's family. Recorded as examined, not as an unread warning.

Notes for review

  • skip-changeset — the diff is scripts/pm/** only and publishes nothing from any package.
  • The reconciliation line names --commands inline. The file header already says it, but the header is not where a harvesting consumer looks, and the triage's own diagnosis is that adoption — not availability — is what failed. Flagged as a deliberate choice rather than a silent one.
  • Placement is below the convention block: at the top it would state a figure before the sections it reconciles had printed, and under the residue it would sit past the point a harvesting reader stops.

Generated by Claude Code


Generated by Claude Code

…vest can be asserted against
The human rendering places a card's runnable answer in two differently
shaped sections — a path-derived matched block and a kind-derived
convention block — and nothing in it stated the union. Two independent
devs each harvested one section, ran it green, and reddened CI on a
family the other named; a third reader misread the same output a third
way. Both machine-readable modes already existed, so the gap was not a
missing mode but a missing control: a partial harvest was plausible
rather than detectable.
- familyReconciliation derives the total and its parts from the SAME two
expressions commandsFor unions, so a family added to either input moves
the section and the count together. The closure identity is asserted
and refuses rather than printing an untrustworthy total.
- familyReconciliationLines prints it on every completed derivation,
including at zero, with the arithmetic tying it to both sections and
the --commands escape hatch named inline.
- spellingFooterLines no longer spells its matched-block subtotal as
`N families`: that was the line a dropped-section harvest reconciled
against, successfully, on the wrong list. It now names its scope and
forward-points to the total when families sit outside its block.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WLJQhde67SeTccsmnBVarV
…fore claiming detection
Measured while ablating the call site out of derive(): with no line printed
reconTotal is NaN, and `NaN !== convBlock.length` is TRUE — so the case
asserting that the total detects a short harvest passed while the remedy
was absent. That is the instrument-cannot-fail-toward-its-target shape this
card is about, inside the pin for it.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WLJQhde67SeTccsmnBVarV
@claudeclaudeBot added the skip-changeset PR has no user-facing published change; bypasses the changeset gate label Sep 1, 2026
@baozhoutao
baozhoutao marked this pull request as ready for review September 1, 2026 14:30
@baozhoutao
baozhoutao added this pull request to the merge queueSep 1, 2026
Merged via the queue into main with commit 66ecc50Sep 1, 2026
34 checks passed
@baozhoutao
baozhoutao deleted the claude/issue-13642-family-print-reconciliation branch September 1, 2026 14:53
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size/mskip-changesetPR has no user-facing published change; bypasses the changeset gate

Projects

None yet

2 participants

@baozhoutao@claude
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content

fix(pm): reconcile dispatch-gates' family sections with a total a harvest can be asserted against - #14221

Merged
baozhoutao merged 2 commits into
mainfrom
claude/issue-13642-family-print-reconciliation
Sep 1, 2026
Merged

fix(pm): reconcile dispatch-gates' family sections with a total a harvest can be asserted against#14221
baozhoutao merged 2 commits into
mainfrom
claude/issue-13642-family-print-reconciliation

Conversation

@claude

@claudeclaudeBot commented Sep 1, 2026

Copy link
Copy Markdown
Contributor

Fixes#13642

Two differently-shaped sections carry a card's runnable answer in dispatch-gates' human rendering, and nothing in that rendering stated their union. Two independent devs each harvested one section, ran it green, and reddened CI on a family the other section had named. This adds the missing control: a total a harvest can be asserted against, derived from the same structure the sections print from.

Premise re-check (the card predates PR #14207's +603 lines)

The card describes "two structurally different sections". Re-derived on 24b66352, the human block now has seven sections, of which exactly two carry this card's runnable answer:

sectionshapein the runnable answer?
Local gates for this card - CMD [wf] matched via ...yes — path-derived
spelling footerN families — P pnpm, K direct node.it is the count for the block above
Convention-triggered gates - CMD — why, under a per-kind headingyes — kind-derived
Once a changeset exists, N more... - CMD [wf] would match ...no, and deliberately so
Unreachable — ...standing repo factno
Always runs — ... - [wf · job] stepno
Residue — ...bucket countsno

So the card's premise holds, and its taxonomy was two-thirds of the picture. The authority for "what this card owes" is commandsFor(...) — matched UNION convention, deduped — which is exactly what --commands and --json already render.

The measurement that made the defect reproducible today

On 24b66352, for the card packages/spec/src/foo.test.ts:

matched block rows ......... 40
--commands ................. 44 (40 by path, 6 by kind, 2 reached both ways)
the spelling footer said ... "40 families — 28 pnpm, 12 direct node."

⭐ The only count line adjacent to the harvestable block stated a subtotal in the vocabulary of a total. A reader who harvested the matched block, counted 40, and read the footer got a reconciliation that agreed — on the wrong list. Both incidents on the card lost the convention block specifically, and check:engine-double-contract — occurrence 2's missing gate — is in the convention block on this very input.

What this changes (one file, scripts/pm/dispatch-gates.mjs)

  1. familyReconciliation — the total and its parts, built from the same two expressions commandsFor unions, not from a second traversal. Add a family to either input and both the section and its term move together. The identity matched + convention − both === total then holds by set algebra, and is asserted anyway: a mismatch throws and refuses rather than printing a total that cannot be trusted (check:react-declaration-parity 是唯一没接进任何 workflow 的源码审计门禁,且无 MANIFEST 时静默 skip 退出 0 —— 它现在永远不可能红 #4690).
  2. familyReconciliationLines — printed on every completed derivation, including at zero (an absent number is not assertable; its absence would mean "this card owes nothing" and "this build has no reconciliation" at once — the argument derive already makes for the tier verdict). Carries the arithmetic, the short-harvest warning where a section really is droppable, the --commands escape hatch inline, and a disclaimer that the total is not what CI runs.
  3. spellingFooterLines — no longer spells its matched-block subtotal as N families. It says N matched families, and when families sit outside its block it forward-points to the total, at the harvest site, where a consumer who never scrolls further still meets it.

⛔ Explicitly not done, per the triage ruling: no new output mode (--json and --commands both already exist), no ALL FAMILIES: block, no "please read more carefully" prose, and nothing in dispatch-order or teaching material — that limb is skills-lane governed territory.

Rendered output, on the input measured above

40 matched families — 28 pnpm, 12 direct node.
⛔ Two spellings, deliberately ... takes 28 of the 40 and reports nothing missing.
⇒ Harvest with --commands ...
Holding captured text already? ...
⛔ ...and a harvest of this block is 40 of the 44 this card owes, whichever spelling it takes: 4 more
famil(ies) are named by change KIND and print under their own heading below, outside every harvest
of THIS block — the published snippet above included.
The Reconciliation line under that heading carries the 44. Assert your list against THAT number, never against this one.
Convention-triggered gates (this change KIND moves them; no path derivation can name them):
... 6 rows, check:engine-double-contract among them ...
Reconciliation — 44 famil(ies): this card's WHOLE runnable answer, and the number to assert a harvest against.
40 named by PATH (the matched block) + 6 named by change KIND (the convention block), 2 of them the same
family reached both ways ⇒ 44 distinct.
⛔ A harvest that ends at ONE section is SHORT and reports nothing missing: the matched block alone is 40
of the 44, the convention block alone is 6 of the 44. Two cards lost the convention block exactly this
way, hours apart, and CI found it both times.
⇒ Skip the arithmetic: --commands prints exactly these 44, one runnable command per line, nothing else on stdout.
⛔ 44 is what THIS CARD owes by path and kind — NOT a complete account of what CI runs on the PR.

Verification

Union re-run on the final commit 1644b978.

pnpm check:pm-dispatch-gates — the tool's own battery, which is also the self-test surface for this file:

✓ dispatch-gates self-test: 1140 cases pass.
os-verify-lock: VERDICT command-exit 0 · held the lock 535s

26 new cases, all green, including the end-to-end pair that only a real run can hold:

✓ the reconciliation total IS the commandsFor union, not a recount of it
✓ adding a convention family moves the convention term AND the total
✓ suppressing the convention section moves the convention term AND the total
✓ the total and its parts close over EVERY overlap of the two inputs — 64 of 64
✓ the DEFAULT run prints the reconciliation — no flag, because a control behind a flag is worth nothing
✓ and its total is exactly what --commands returns for the same card
✓ the PATH term equals the rows the published snippet harvests from the matched block
✓ and the change-KIND term equals the distinct runnable rows the convention block really printed
✓ CONTROL: a matched-block-only harvest of this card is STILL short — the defect is real and untouched
✓ CONTROL: and the reconciliation total DETECTS that harvest as short rather than agreeing with it

Ablations — both legs proven on disk, both restored byte-identical

A — remove the call site from derive. Mutation confirmed on disk (call-site occurrences 1 → 0, injected marker 1; blob 6dbebbe2b0c6ed8c), then the real battery run on the mutated tree:

✗ the DEFAULT run prints the reconciliation — no flag, because a control behind a flag is worth nothing
✗ and its total is exactly what --commands returns for the same card
✗ the PATH term equals the rows the published snippet harvests from the matched block
✗ and the change-KIND term equals the distinct runnable rows the convention block really printed
✗ the matched footer forward-points to that total from inside the block being harvested
✗ dispatch-gates self-test: 5 of 1140 case(s) failed.

⭐ That ablation also found a vacuous assertion in my own pin: with the line absent, reconTotal is NaN, and NaN !== convBlock.length is TRUE — so the CONTROL case asserting the total detects a short harvest passed while the remedy was gone. That is this card's own defect shape inside the pin for it. Fixed in 1644b978 by requiring Number.isInteger(reconTotal) first.

B — break the closure identity (replace the overlap computation with an independent 0). Declared narrowing: the assert is in a pure function one call fully exercises, so this leg drove the function directly rather than the 9-minute battery. Mutation confirmed on disk (anchor 1 → 0, marker 1; blob differs from HEAD), control call on the unmutated tree returned a closing reconciliation first:

REFUSED, as designed:
dispatch-gates: the family reconciliation does not close — 1 matched + 1 convention − 0 both ≠ 1 distinct. ...

and the real human run exited 2 and printed ZERO reconciliation lines — it refuses rather than printing a wrong total.

Restore proven for both, not read off an exit code: git diff HEAD empty, git status clean, blob hash equal to the HEAD blob, 0 ablation markers left in the file.

Gate family, re-derived from the actual diff on 1644b978

node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack (no paths — the script takes the change set off the merge base itself) derives 15 families, and its own new line reconciles them: Reconciliation — 15 famil(ies) ... 15 named by PATH + 0 named by change KIND ⇒ 15 distinct. All 15 run, all green:

gateexit
pnpm check:pm-dispatch-gates0 — 1140 cases
pnpm check:declared-population-live0
pnpm check:watch-hint-literal0
pnpm check:parse-guard0
pnpm check:entry-guard0
pnpm check:agent-test-spelling0
pnpm check:bash32-floor0
pnpm check:cli-command-ids0
pnpm check:pnpm-filter-targets0
pnpm check:cross-package-test-inputs0
node scripts/check-cross-package-test-inputs.mjs0
node scripts/check-self-test-wired.mjs0
node scripts/check-ci-filter-parity.mjs0
node scripts/check-shard-attestation.mjs0
node scripts/check-test-completeness.mjsNOT MEASURED

check-test-completeness exits 3 = PREREQUISITE NOT MET, its own words: it grades a saved turbo run test log, there is none locally, and its message says the derived family names it with no argument and "the local reading for this gate is NOT MEASURED. ⛔ It is not a red." The other two exited 3 for the same class (yaml not installed in a fresh worktree) and were re-run to a real 0 after pnpm install.

Also run, per the dispatch: node scripts/pm/bare-root-worklist.mjs --self-test (0), node scripts/check-ratchet-remedy-authority.mjs (0), pnpm check:nul-bytes (0, plus a direct grep -naP control-byte scan of the changed file — none).

Every exit code above was captured before any pipe (cmd > log 2>&1; EXIT=$?), and each verdict is quoted from the gate's own line.

ESLint — a declared, measured narrowing, not a skip

The repo-wide pnpm lint is CI's run. Locally this was narrowed to the one changed file, with the three things that make a narrowing a measurement:

  1. Population from eslint's own config — one root eslint.config.mjs, resolved by eslint itself; the run used the same --no-inline-config the root lint script uses.
  2. Count from --format json — 1 file linted, 0 errors, 0 warnings.
  3. Invariance for untouched files — the config's own declaration (eslint.config.mjs, the QUERY_OPTIONS_TEST_GLOBS header) states this repo "runs one eslint.config.mjs, which never enables type-aware linting (no parserOptions.project, no typed @typescript-eslint rules) for ANY file", measured there with a positive control. A one-file .mjs edit therefore cannot move any untouched file's verdict.

Stale-tree warning, checked rather than carried

The derivation warns that scripts/check-stack-collection-maps.mjs changed on origin/main after this branch point. Read: the literals its new version names are its own file path, which does not cover scripts/pm/dispatch-gates.mjs (a bare scripts word is refused as too generic), so it does not change this card's family. Recorded as examined, not as an unread warning.

Notes for review

  • skip-changeset — the diff is scripts/pm/** only and publishes nothing from any package.
  • The reconciliation line names --commands inline. The file header already says it, but the header is not where a harvesting consumer looks, and the triage's own diagnosis is that adoption — not availability — is what failed. Flagged as a deliberate choice rather than a silent one.
  • Placement is below the convention block: at the top it would state a figure before the sections it reconciles had printed, and under the residue it would sit past the point a harvesting reader stops.

Generated by Claude Code


Generated by Claude Code

…vest can be asserted against
The human rendering places a card's runnable answer in two differently
shaped sections — a path-derived matched block and a kind-derived
convention block — and nothing in it stated the union. Two independent
devs each harvested one section, ran it green, and reddened CI on a
family the other named; a third reader misread the same output a third
way. Both machine-readable modes already existed, so the gap was not a
missing mode but a missing control: a partial harvest was plausible
rather than detectable.
- familyReconciliation derives the total and its parts from the SAME two
expressions commandsFor unions, so a family added to either input moves
the section and the count together. The closure identity is asserted
and refuses rather than printing an untrustworthy total.
- familyReconciliationLines prints it on every completed derivation,
including at zero, with the arithmetic tying it to both sections and
the --commands escape hatch named inline.
- spellingFooterLines no longer spells its matched-block subtotal as
`N families`: that was the line a dropped-section harvest reconciled
against, successfully, on the wrong list. It now names its scope and
forward-points to the total when families sit outside its block.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WLJQhde67SeTccsmnBVarV
…fore claiming detection
Measured while ablating the call site out of derive(): with no line printed
reconTotal is NaN, and `NaN !== convBlock.length` is TRUE — so the case
asserting that the total detects a short harvest passed while the remedy
was absent. That is the instrument-cannot-fail-toward-its-target shape this
card is about, inside the pin for it.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WLJQhde67SeTccsmnBVarV
@claudeclaudeBot added the skip-changeset PR has no user-facing published change; bypasses the changeset gate label Sep 1, 2026
@baozhoutao
baozhoutao marked this pull request as ready for review September 1, 2026 14:30
@baozhoutao
baozhoutao added this pull request to the merge queueSep 1, 2026
Merged via the queue into main with commit 66ecc50Sep 1, 2026
34 checks passed
@baozhoutao
baozhoutao deleted the claude/issue-13642-family-print-reconciliation branch September 1, 2026 14:53
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size/mskip-changesetPR has no user-facing published change; bypasses the changeset gate

Projects

None yet

2 participants

@baozhoutao@claude