Commit 7572329
fix(pm): dispatch-gates reads a population declaration written in a block comment, and refuses one it cannot read (#18784)
Fixes #18661
Clause-②: no
`scripts/pm/dispatch-gates.mjs`'s three population markers
(`no-path-population`, `whole-tree-population`, `wide-population`) built
their grammar from one head whose comment-form alternation listed `//`
and `#` and nothing else, so a declaration written in a file's own
BLOCK-comment idiom parsed as NOTHING — not refused, not printed, not
counted, and therefore byte-identical in every channel this tool has to
a gate that declares nothing at all. Two live gates were writing one.
The author of each believed they had explained why their gate reads no
population; every reader of the residue saw their family in the
unexamined pile; neither side had anything to check against, which is
what makes this class expensive rather than merely wrong.
## The two before-readings (re-derived on this branch, not taken from
the card)
Taken on `origin/main` `95b21b33be` before any edit:
1. **The population reader answers `null` over both files.**
`declaredNoPathPopulation` returns `null` for
`scripts/symbol-anchors.mjs` and for `scripts/release-verify-npm.mjs`,
and `populationReasonContinuation` returns `null` for both as well — so
not even the #18422 wholeness refusal had anything to grade. Nothing was
refused because nothing was read.
2. **Both families sit in `undetermined` with `hints=0` and no
annotation.** `node scripts/pm/dispatch-gates.mjs --residue --repo
objectstack-ai/objectstack scripts/pm/dispatch-gates.mjs` placed 309
families — 23 matched, 43 undetermined, 225 silent, 7 always-runs, 11
declared-wide — and reported `9 of those 43 undetermined famil(ies)
DECLARE that they have no path population`. `node
scripts/release-verify-npm.mjs --self-test` and `node
scripts/symbol-anchors.mjs --self-test` are both printed in the
undetermined block with no `names:` suffix and no `↳ declared no path
population` line under them.
One line number on the card was stale and is corrected here:
`scripts/symbol-anchors.mjs`'s declaration is at `:207` on today's tip,
not `:180`; the text is unchanged. `scripts/release-verify-npm.mjs:110`
is where the card says.
## ① The grammar: one roster, five forms, and where a reason ENDS in
each
`MARKER_COMMENT_FORMS` replaces the inline alternation. It is the single
place a form is added, the head both marker builders and the
continuation reading are derived from, and it classifies each form into
one of two KINDS, because the two kinds answer the wholeness question
differently:
| kind | forms | where the reason ends |
|---|---|---|
| `line` | `//`, `#` | on the marker line. A comment line under it in
the SAME form, carrying text that is not a new `dispatch-gates:` key, is
a CUT and is refused — #18422, unchanged byte for byte |
| `block` | `/**`, `/*`, `*` | at the first of: the closing delimiter, a
blank star-only line, the next star-`@tag` line, another
`dispatch-gates:` key, or EOF. The star-prefixed lines between are
JOINED into the reason |
The block form's own wholeness question is answered in the header rather
than left to be found. Two adjacent `//` lines are two separate comments
and nothing in the text says whether the second belongs to the first —
that is why #18660 refuses there, and that refusal is untouched. A `/*
... */` block is ONE comment whose internal newlines are formatting, so
its next star line is a continuation *by construction* and the join is
decidable from the text rather than guessed. What the block form cannot
do is cross any of the three places a block-comment author signals a new
thought; each is pinned. A line INSIDE the block carrying text with no
star prefix is none of the five endings — it is reason text the walk
cannot read — so it is recorded as the block form's CUT and refused with
its own remedy ("give that line the block's star prefix, or end the
reason before it with a blank star line"), never with the line forms'
advice, which would send that author to the wrong half of their
declaration.
The one residual asymmetry is named in the header rather than hidden: a
second sentence on the very next star line, with no blank line between,
IS swallowed. That is OVER-inclusion and it reaches a seat as a reason
that says too much — visible on the row. The truncation #18422 refused
is UNDER-inclusion and reaches a seat as a sentence that merely ends
oddly — invisible. The block idiom's own paragraph break is the text
that separates the two, and it is what a block-comment author already
writes.
⛔ What a population declaration MEANS is unchanged, and no refusal PR
#18660 added is loosened: the line forms' capture, their continuation
reading and their refusal text are all identical, and a self-test case
asserts that refusal text still reads "Put the WHOLE reason on the
marker line".
## ② The sound: a line that reads like a declaration and did not parse
is never silent
`unparsedPopulationMarkers` + `unparsedPopulationMarkerRefusal`, RED at
author time through this file's own `--self-test` live half, which
sweeps every gate source the discovery reads (250 sources on this tree)
and names each finding by FILE, LINE and the FORM it was written in —
exactly the three things the old output withheld. Two causes,
deliberately one finding, because the author's experience is identical:
an unrecognised comment form, and a recognised form with no `-- REASON`
tail.
The chosen place is the refusal rather than the derivation output,
because the refusal fires before any row can be printed: an unparsed
declaration cannot reach `main`, so a second rendering of a state that
cannot exist would be unreachable code, which this file's own rule
refuses.
Two boundaries, both measured rather than assumed. A line may carry AT
MOST ONE comment opener — a line with the docblock's own star plus a
second opener is an EXAMPLE of a declaration written inside a comment
about declarations, which is how every example in this file is written,
and the grammar reads one opener too, so probe and grammar agree about
what documentation looks like. And a QUOTE is not a comment opener:
every self-test in this family builds its fixtures out of string
literals, this file's own included, and the grammar already ignores them
for the same reason.
## Census — the whole tree, report-only, no state written
Swept over all 8842 tracked files at `95b21b33be`:
| reading | count |
|---|---|
| lines carrying the literal text `dispatch-gates:` | 137 |
| of those, lines carrying a POPULATION key | 69 |
| parse as a declaration under the OLD form set (`//`, `#`) | 25, across
25 files |
| parse under the NEW form set (+ `/**`, `/*`, `*`) | 27, across 27
files |
| NEWLY parsing | 2 — `scripts/release-verify-npm.mjs:110`,
`scripts/symbol-anchors.mjs:207` |
| declaration-SHAPED and unparsed on `main` (what ② would have flagged)
| 2 — the same two |
| still unparsed after ① | **0** |
So ②'s only live cases were the two ① repairs, and the census is its
coverage statement: the remaining 67 population-key lines are prose
mentions (5 of them, all backticked or mid-sentence), this file's own
docblock examples, and string-literal fixtures — none is
declaration-shaped, and the live sweep over the tip returns zero. The
pins for ② are therefore synthetic plus one live non-vacuity leg that
takes a real declaration, re-spells its opener in an unlisted form, and
asserts exactly that line is found.
## The family before/after — nothing else moves
Same command, same probe path, before at `95b21b33be` and after at
`98d0b1dca4`; the full listings differ by 28 lines and every movement is
named:
- `+ ↳ declared no path population — ...` under `node
scripts/release-verify-npm.mjs --self-test` — this change
- `+ ↳ declared no path population — ...` under `node
scripts/symbol-anchors.mjs --self-test`, with the whole six-line reason
joined — this change
- `9 of those 43 undetermined` becomes `11 of those 43` — this change
- `check-dev-prereqs` 70 declared literals becomes 71, and its `names:`
list grows — NOT this change: `scripts/check-dev-prereqs.mjs` moved on
`origin/main` in the merge this branch carries
- `8842 tracked file(s)` becomes `8844` (two places) — NOT this change:
the merge added `.changeset/amplifiers-linked-packages.md` and
`scripts/measure-markdown-ts-blocks.mjs`
- the derivation header's commit sha
The five bucket counts are byte-identical: 23 matched, 43 undetermined,
225 silent, 7 always-runs, 11 declared-wide. No family changed bucket.
`no-path-population` annotates WITHIN `undetermined` rather than moving
a family out of it, which is what the channel does.
## The ledger row the readable declaration graduates
`ROOT_WALK_RESIDUE_LEDGER` carried a hand-written row for
`scripts/symbol-anchors.mjs --self-test` — a repo-root walker that
declares neither marker and cannot be placed by path. It declared
neither marker only because its declaration could not be read. With the
form set widened it leaves that population by DECLARING, which the
table's own header names as the outcome it exists to push toward, and
`check:pm-dispatch-gates` reds on a stale exclusion by design. The row
is deleted in the same landing, with a `⚖️` note in the style the table
already uses for its one previous graduation. This is the card's defect
priced in a second currency: it had cost a hand-maintained exclusion
row, carrying by hand the reading the gate's own source already carried.
## Ablation (from the committed fix, restored under a trap)
Mutation: the three `kind: 'block'` rows deleted from
`MARKER_COMMENT_FORMS`, proven on disk by blob hash —
`e72c4d48d33ef670c7baa16f3929cf3f4bf958f5` (== the HEAD blob) becomes
`1c2128d800e60ffa93c81cbad76313c381f69c92`.
| ablated reading | result |
|---|---|
| `declaredNoPathPopulation` over the two live files | both back to
`null` |
| residue's documented-no-population count | 11 back to 9 |
| `pnpm check:pm-dispatch-gates` | **exit 1** — `11 of 1809 case(s)
failed` |
| the 8 block-form grammar pins | all RED |
| the live pin "the live tree's BLOCK-form declarations are READ and not
dropped" | RED, `found 0: none` |
| the SOUND, over the real tree | RED, and it names both specimens:
`scripts/release-verify-npm.mjs:110 declares no-path-population in form
*` and `scripts/symbol-anchors.mjs:207 declares no-path-population in
form /*` |
| the ledger case | RED in the OPPOSITE direction — `unlisted:
scripts/symbol-anchors.mjs --self-test` — so the row's deletion is
coupled to the fix by construction |
| the other 1798 cases | green, unchanged |
Restore: `git checkout HEAD -- scripts/pm/dispatch-gates.mjs` under an
`EXIT INT TERM` trap, verified by hash equality with the HEAD blob AND
an empty `git diff HEAD`, not by an exit code.
## Self-test — measured, not NOT MEASURED
`pnpm check:pm-dispatch-gates` run DETACHED with its output to a file
and read from the file, never under a foreground timeout: **exit 0**, `✓
dispatch-gates self-test: 1809 cases pass`, battery 748.0s on this box,
at `71aacfb886`.
## Derived gates
`node scripts/pm/dispatch-gates.mjs --commands --repo
objectstack-ai/objectstack` from the worktree, no hand-fed path list: 1
path in the change set, 28 commands derived. Every one run, each exit
code captured by redirect-then-`$?`, and reconciled:
`✓ dispatch-gates --ran: 28 derived famil(ies) accounted for — 28 run, 0
NOT-MEASURED (a DERIVED zero — all 28 recorded an exit code and none of
them is 3)`
All 28 exit 0. Repo-wide `pnpm lint` (`eslint . --no-inline-config`):
exit 0. The derivation at `71aacfb886` prints a STALE TREE note naming
one file that moved on `origin/main` afterwards,
`scripts/pm/post-stamped.mjs`; the derived command list is
byte-identical before and after that move, and the queue rebuilds this
PR on the current `main` regardless.
## Scope
⛔ #18662 is NOT folded in. It asks for the REASON-WHOLENESS reading (the
cut refusal) to be extended to `no-check-families` and
`inherited-population`; this change gives neither of them one, and
`NO_CHECK_FAMILIES_MARKER` is a separate `#`-only regex for YAML that is
untouched. One interaction is worth recording: `pathListMarkerPattern`
shares the one head with the population markers by construction
(#18673), so `inherited-population` and `self-test-reads` now also
accept the block spellings. There are zero live block-form declarations
of either key — both live ones are `//` — so nothing moves, and their
reason-wholeness exposure is exactly what #18662 describes, neither
widened nor narrowed here.
`skip-changeset`: the diff is one file under `scripts/pm/**`, a PM loop
tool that no package's `files[]` ships.
---
_Generated by [Claude
Code](https://claude.ai/code/session_01Gqi43smmqjJ5sUrhfoPeKu)_
---------
Co-authored-by: Claude <noreply@anthropic.com>1 parent 94b3f37 commit 7572329
1 file changed
Lines changed: 592 additions & 58 deletions
0 commit comments