Skip to content

fix(pm): the --ran and --commands lanes read the one OUTSIDE enumeration - #17028

Merged
yinlianghui merged 2 commits into
mainfrom
claude/issue-16795-outside-list-other-lanes
Sep 9, 2026
Merged

fix(pm): the --ran and --commands lanes read the one OUTSIDE enumeration#17028
yinlianghui merged 2 commits into
mainfrom
claude/issue-16795-outside-list-other-lanes

Conversation

@yinlianghui

Copy link
Copy Markdown
Collaborator

Fixes #16795

What this changes

The list of blocks that sit OUTSIDE a card's runnable total is one claim. #16398
(PR #16802) built it once for the human lane, as outsideBlockNames in
familyReconciliationLines. The other two output lanes kept short prose copies of
their own, and both were wrong in the way the enumeration is always wrong:

lane before after
--ran, runReconciliationLines named 3 of 5 — the always-runs tail, the unreachable listing and the pending-changeset families reads outsideBlockNames
--commands / --json stderr accounting, machineReadableOutput named 1 — the always-runs tail reads outsideBlockNames
human default, familyReconciliationLines correct since #16802, except the third name reads the same exported expression

outsideBlockNames and outsideBlocksPhrase are exported now and all three lanes call
them. Nothing routes differently and nothing derives differently: --commands stdout is
byte-identical before and after (diff clean), and the only moved bytes on any lane are
in the closing disclaimer.

The third name went conditional in the same pass (pointer 5580275213). The
enumeration still spelled "the pending-changeset families" unconditionally while
pendingChangesetLines returns nothing at zero — so on a card with no pending family the
sentence pointed a reader below at a heading that is not there. It is now sized from
pending.length, the length of the very array that renders the block, like the two names
above it. familyReconciliation carries the third block size (pendingChangeset) beside
the two #16398 added, on the same rule: a count read from the array that RENDERS a block
cannot disagree with it.

The card's acceptance baseline, re-run on this branch

The card's repro needs a change set, so it is run with the card's own file as an explicit
path. Before, at ee2cb6b4:

$ node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack scripts/pm/dispatch-gates.mjs 2> commands.err
$ grep -c 'WIDE' commands.err               # 0
$ grep -c 'Artifact rosters' commands.err   # 1

After, at 47e1d710, the tip of this branch:

$ grep -c 'WIDE' commands.err               # 1
$ grep -c 'Artifact rosters' commands.err   # 1

The line that moved:

  ⛔ Not a complete account of what CI runs on this PR: the 47 artifact-roster famil(ies),
  the 10 declared WIDE-population famil(ies), the 13 pending-changeset famil(ies), the
  unreachable listing and the always-runs tail are each OUTSIDE the 30 command(s) on
  stdout. Run without --commands/--json to see every one of them named.

--ran, same tree, same three counts, from the same arrays:

  ⛔ This answers ONE link: what this card DERIVES against what you RAN. It is not a
  complete account of what CI runs on the PR — the 47 artifact-roster famil(ies), the 10
  declared WIDE-population famil(ies), the 13 pending-changeset famil(ies), the
  unreachable listing and the always-runs tail are each outside the derived total, each
  printed under its own heading by a run without --ran.

And the third name really is conditional — a plain human run with a changeset path in the
change set drops it, because pendingChangesetLines prints nothing for it:

  ⛔ 40 is what THIS CARD owes by path and kind — NOT a complete account of what CI runs
  on the PR. The 47 artifact-roster famil(ies), the 10 declared WIDE-population
  famil(ies), the unreachable listing and the always-runs tail below are each OUTSIDE it,
  each with its own count.

Tests

28 new self-test cases, 1552 to 1580, all green. Pinned NAME BY NAME with a negative
beside every positive on both lanes, because the weak shape is what failed before: the
human lane's case used to ask only for the substring always-runs tail, so the sentence
could name three of five and stay green for the whole time a harvester following it was
missing two.

  • --ran: 5 positives (each of the five names), 1 order/phrase pin, 2 negatives (names
    none of the three conditional blocks at zero counts; still names the two that print
    unconditionally).
  • --commands: driven through machineReadableOutput itself with console.error
    captured, not through the helper — the defect was in what that function PRINTS, and a
    case against the helper would have stayed green through exactly it. 5 positives, the
    card's own grep 'WIDE' reading as a case, 2 negatives, plus a control that the
    enumeration never reaches stdout (stdout stays commands and nothing else).
  • Human lane: the existing five-name loop and phrase pin updated for the counted third
    name, a new negative that the pending block is dropped at zero, and a control that a run
    with ONLY pending families names exactly that one — so the negative cannot be passing
    because the name went away for good.

Ablation — the lane-2 enumeration reverted to the old hard-coded sentence

Direction predicted before the run: turn red. Run from the committed state, mutation
proved on disk before the battery, restore proved by blob hash.

HEAD blob                                        41584fa2df0305fe0d073786912483d5d575e7dd
mutated blob                                     e7dc8a8d7d9211f6acfd83f1d496c458382e30ed
injected old sentence, grep -c                   1
removed new expression, grep -c                  0
git diff HEAD --stat                             1 file changed, 1 insertion(+), 5 deletions(-)

pnpm check:pm-dispatch-gates  =>  exit 1, "6 of 1580 case(s) failed"
  ✗ --commands' closing disclaimer names "the 2 artifact-roster famil(ies)" …
  ✗ --commands' closing disclaimer names "the 1 declared WIDE-population famil(ies)" …
  ✗ --commands' closing disclaimer names "the 3 pending-changeset famil(ies)" …
  ✗ --commands' closing disclaimer names "the unreachable listing" …
  ✗ ...so the stream a --commands consumer reads carries the token 'WIDE' at all
  ✗ ...while still naming the two that print unconditionally

restore: git checkout HEAD -- scripts/pm/dispatch-gates.mjs
restored blob                                    41584fa2df0305fe0d073786912483d5d575e7dd  (== HEAD blob)
git diff HEAD                                    empty
pnpm check:pm-dispatch-gates  =>  exit 0, "1580 cases pass."

One reading worth keeping: the case for the always-runs tail stayed green under the
ablation, and so did the --commands negative — the old hard-coded sentence names exactly
that one block and none of the three conditional ones. That is the whole argument for
pinning each name as its own assertion: a negative alone, and a positive for the one name
that never moved, cannot see this defect.

Gates

Derived on this branch from the real change set, not from a list:
node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack at
47e1d710 prints 30 families. All 30 were run; exit code captured to a file before any
pipe, and the verdict read from each gate's own line.

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

Reconciled by the tool's own --ran lane, against a record built from that same
--commands output:

Run reconciliation — 30 derived, 30 run, 0 NOT-MEASURED, 0 UNRUN.
✓ dispatch-gates --ran: 30 derived famil(ies) accounted for — 30 run, 0 NOT-MEASURED.

Beyond the derived set:

  • pnpm lint (eslint . --no-inline-config) — the whole repo, run in the foreground,
    exit 0. No narrowing was needed, so no narrowing is claimed.
  • The tool script's own suite is --self-test, which check:pm-dispatch-gates runs.
    scripts/pm/ holds no *.test.ts; a repo-wide git grep for this script under test
    files returns 9 files, and every hit is prose in a docblock — the one that names the
    full path, packages/cli/test/serve-node-env-production-default.e2e.test.ts, mentions it
    in a comment and does not execute it.
  • No .changeset entry: scripts/pm/** publishes nothing from any released package.
    skip-changeset.

A PM mechanism assumption this run falsified, and a correction to the card body

Falsified (dispatch mechanism assumption 1). The dispatch expected that
runReconciliationLines(recon) "gets the same recon". It does not: --ran renders the
object returned by runReconciliation (a record-against-derived closure), which is a
different structure from familyReconciliation's and carries no block sizes. Handled by
the dispatch's own fallback — the enumeration is a small exported function taking counts,
and each lane hands it what it holds. The counts arrive at --ran as a second parameter to
the RENDERER rather than as terms of runReconciliation: a block size is not a term of
that reconciliation's arithmetic, and adding one to an object whose closure is asserted
would put a non-term inside an invariant. familyReconciliation did gain the third block
size, because that object already carries block sizes for exactly this purpose and the two
beside it were placed there by #16398.

Card body, corrected here. #16795 states of the --commands lane that the declared
WIDE population "is never mentioned at all — not as a block, not as a count, and not in the
closing disclaimer". The middle third is measured false on ee2cb6b4: the stderr
accounting does print a count line for it,
+ 10 famil(ies) DECLARE a population too wide to place and are NOT above …. What holds is
the load-bearing part and the repro: the token WIDE appears nowhere in the stream
(grep -c reads 0, because that line spells "wide" in prose), and the closing disclaimer
names exactly one block. The count line is also sized on the RUNNABLE subset
(wideRunnable), while the block it points at renders every row including refused ones —
so the new name is sized off widePopulationRows.length, the array widePopulationLines
actually renders, and not off the subset this lane happens to summarise.

H17 riders — report-only, nothing built

scripts/pm/dispatch-gates.mjs is a Restart-touch on three devx-lane cards. Per the
dispatch these are read and reported, not worked.

#14290 (pm:on-hold, hold 5556473243) — its re-check re-run on this branch:

$ node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --residue scripts/objectui-changeset-digest.mjs
exit 0

  - pnpm check:objectui-changeset   [lint.yml]   matched via scripts/objectui-changeset-digest.mjs ⇢ gate script 'scripts/objectui-changeset-digest.mjs'

The family entry is reached by its own gate script and by nothing else: it still
inherits none of the hints scripts/bump-objectui.sh spells, which is the defect #14290
reports, unchanged by this diff. Two further readings taken while there, offered to the
devx seat and acted on by nobody here: the hold's first Restart-when
(closed objectstack-ai/objectstack#16132) is now satisfied#16132 is closed; its
second (git grep -l -E "(execFileSync|spawnSync)\('bash', \[join\(" -- 'scripts/**' reads
2 or more) still reads 1 on origin/main.

#12797 (extensionless-target residue) — no. This diff touches neither
unreachableReason nor hintCovers nor any part of the residue's reason selection; the
only functions edited render the OUTSIDE enumeration.

#12808 (the docblock single-source declaration in
scripts/check-cross-package-test-inputs.mjs) — no. That file is not in this diff, and
nothing here changes extractWatchHints or the single-segment hint rule its sentence is
about.

Risk

Output text on three lanes; no routing, no derivation, no new data. The --commands
stdout stream is byte-identical, which is the property the mode exists for. Rollback is
reverting the one commit pair on this branch.


Generated by Claude Code

… lanes

The list of blocks that sit outside a card's runnable total is one claim.
#16398 built it once for the human lane, as `outsideBlockNames`. The other
two output lanes kept their own short prose copies: `--ran` named three of
the five blocks a plain run prints, and the `--commands` / `--json` stderr
accounting named exactly one — on the very channel this file's own header
tells consumers to use INSTEAD of harvesting the prose.

`outsideBlockNames` / `outsideBlocksPhrase` are exported now and all three
lanes read them. The third name ("the pending-changeset families") was still
spelled unconditionally while `pendingChangesetLines` returns nothing at
zero, so it is conditional on its own count too, from the array that renders
the block — like the two beside it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HxLw5aKDPR5RJgyUR7Exkd
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HxLw5aKDPR5RJgyUR7Exkd
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

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

Projects

None yet

2 participants