Skip to content

fix(pm): give the two dispatch-gates markers outside the population roster the #18422 reason-wholeness reading - #18822

Merged
os-justin merged 3 commits into
mainfrom
claude/issue-18662-marker-reason-wholeness
Sep 17, 2026
Merged

os-justin merged 3 commits into
mainfrom
claude/issue-18662-marker-reason-wholeness

Conversation

@os-justin

Copy link
Copy Markdown
Collaborator

Fixes #18662

Clause-②: no

The defect

NO_CHECK_FAMILIES_MARKER and INHERITED_POPULATION_MARKER's reason half captured their reason with the same (\S.*)$-under-m shape that #18422 (PR #18660) repaired for the three population markers: the capture ends at the FIRST NEWLINE, so a reason an author wraps onto the comment line below is read as line ONE and nothing refuses it. Neither marker was in POPULATION_MARKER_KEYS, so neither populationReasonContinuation nor populationReasonCutRefusal could even be asked about them — both threw unknown population marker key. No live declaration is cut in this tree today (the census below), so this is exposure, not a live cut; the cost lands on the next author who wraps one and the seat who reads the sentence that stops.

The before-reading, re-derived on main (not taken from the card)

Measured against origin/main 034f5a3afd, feeding each marker a reason wrapped over two comment lines — the same wrapped shape #18422 measured for the population markers.

no-check-families, consumer declaredNoCheckFamiliesReason (the capture at scripts/pm/dispatch-gates.mjs:2707, return m ? m[1].trim() : null; at :2708):

reason read      : "steps are an install/build/boot pipeline, and the verdict is"
author wrote     : "steps are an install/build/boot pipeline, and the verdict is whether the
                    scaffolded app boots at all, which no named local check family covers"
consumer sounds? : checkFamilyCoverageGaps -> []   (empty = accepted, silently)
wholeness reading: THROWS -- unknown population marker key 'no-check-families'

inherited-population, consumer declaredInheritedPopulation (the reason at :4154, const reason = m[3].trim();):

population read  : [".github/workflows"]
reason read      : "the workflow directory this module readdirs, and the verdict is"
consumer sounds? : no throw, no refusal, no row — the call returned normally
wholeness reading: THROWS -- unknown population marker key 'inherited-population'

Control, the same wrapped shape on a population marker (#18422 repaired):

populationReasonContinuation(..., 'wide-population', 'scripts/x.mjs')
  -> {"file":"scripts/x.mjs","line":2,"text":"the count of packages whose manifest this gate refuses","kind":"line"}

That is the red this change turns green.

The registration, and where the refusal prints

One grammar, no second pattern, no hand-rolled continuation reader.

  • MARKER_KEY_FORMS / markerLineHead(key) — the shared head, built from MARKER_COMMENT_FORMS FILTERED to the forms that key's language has.
  • REASON_TAIL_MARKER_KEYS = [...POPULATION_MARKER_KEYS, 'no-check-families']no-check-families now comes out of populationMarkerPattern instead of the fourth hand-written copy of the same grammar it was. The builder keeps its name because population* is what this machinery is called where it is exported; the roster, not the name, is the authority on which keys it serves, and the docblock says so.
  • MARKER_REASON_GRAMMARS — which builder reads each key and which capture group holds its reason, derived BY CONSTRUCTION from the two builders' own key rosters rather than hand-listed. That is what closed the class: a key cannot be added to either builder without the wholeness reading arriving with it. It is also why self-test-reads — the sixth reason-bearing key, same grammar, same defect, filed under [finding] dispatch-gates does not derive check:pm-expected-skips for a SKILL.md change, though that self-test pins a literal in SKILL.md — a SKILL.md PR passed 19 derived gates and went red on the 20th in CI #18673 and NOT named on this card — is covered in the same line rather than becoming a third card on this file. See "Scope" below.
  • readPopulationMarker reads its reason out of the group the roster names, and returns the raw match so a path-list reader takes its path list off the SAME read its reason came from.
  • markerReasonCutRefusal(markerKey, cut) — the refusal TEXT, one copy, five keys. populationReasonCutRefusal(entry, markerKey) is now that function reached through a discovery entry, and its output is byte-identical to what it was before the split (pinned).

Where it prints: the three population channels carry their declaration into a discovery entry that IS rendered, so their refusal is a printed row. These markers have no such row (see the next section), so the READ refuses — refuseCutMarkerReason throws, the CLI catches it at its catch (err) and prints dispatch-gates: derivation failed — followed by the refusal text, exit 2. That is already how both path-list markers refuse an invented path. The message names the file, the line, the marker and the continuation it saw:

dispatch-gates: .github/workflows/scaffold-e2e.yml declares no-check-families and its reason does not
END on the marker line: .github/workflows/scaffold-e2e.yml:7 continues it with "whether the scaffolded
app boots at all, which no named local check family covers". The capture stops at the FIRST NEWLINE, so
the seat is handed the declaration cut off mid-sentence — ... Put the WHOLE reason on the marker line,
however long it runs ... ⛔ Never widen the marker to swallow the next line instead ...

The second surface is this file's own --self-test live census, below — the same place the population markers' live half reds.

⛔ What either declaration MEANS is unchanged: the inherited-population path list is untouched, its invented-path refusal is unchanged and pinned independently, no-check-families still exempts exactly the workflows it exempted, and no refusal PR #18660 / PR #18784 added is loosened.

The card's 「not measured」, answered: NO, neither consumer renders the reason

Read to the surface the text lands on, at 034f5a3afd:

  • declaredNoCheckFamiliesReason has exactly ONE consumer — checkFamilyCoverageGaps at :4298, if (declaredNoCheckFamiliesReason(text)) continue;. The return value is read as a BOOLEAN. The reason string reaches no row, no log and no seat.
  • declaredInheritedPopulation has three production call sites (discoverFamilies' hintsOfModule, and two in this file's own self-test) plus the governed-read census; every one of them reads .population. The only read of .reason in the tree is a self-test case asserting it is non-empty.

So a cut costs nothing to a RENDERING today. The refusal is owed anyway and is stated in both docblocks rather than hidden: wholeness is a property of the declaration, not of today's consumer, and the next reader of either reason is the seat that greps the workflow or the module for it — which is exactly the reader the population markers' repair was written for. The cost is that this refusal buys a seat's future read, not a row on today's output.

PR #18784's block-comment forms, measured against these two

Measured both directions, one form per row, on 034f5a3afd and on this branch.

BEFORE — MARKER_COMMENT_FORMS reached no-check-families in NO form at all: its pattern was a hand-written regex that never consulted the roster, so # matched because it was hard-coded there and the other four did not. It reached inherited-population in ALL FIVE forms already (that marker is built on the shared head since #18673/#18661) — but its reason was m[3].trim(), so a block-form reason kept the closing delimiter and a wrapped block reason was cut at the first newline:

BEFORE  no-check-families:      #  -> "a reason */"   //  -> null   /*  -> null   /** -> null   *  -> null
BEFORE  inherited-population:   #  -> "a reason */"   //  -> "a reason */"   /*  -> "a reason */"
        block WRAPPED reason -> "the workflow directory this module readdirs, and"      (cut)

AFTER — no-check-families is built from the roster FILTERED to #, and the four other forms stay null. ⛔ That is not a widening withheld; it is the answer to the question: the block form cannot apply to a YAML # marker. # is the only comment syntax YAML has — a // or slash-star line in a workflow is document content, not a remark, so reading a declaration off one would be reading it off text the workflow's own parser never treats as a comment. The inherited-population rows are unchanged in form coverage; only its block-form reason now ends where the block ends:

AFTER   no-check-families:      #  -> "a reason */"   //  -> null   /*  -> null   /** -> null   *  -> null
AFTER   inherited-population:   #  -> "a reason */"   //  -> "a reason */"   /*  -> "a reason"
        block WRAPPED reason -> "the workflow directory this module readdirs, and every other literal
                                 here is a join base"                                    (whole)

Scope — and the sixth key

The card names two markers. The registration is derived from the two grammar builders' key rosters, so it also reaches self-test-reads (#18673), the sixth reason-bearing key. That is deliberate, and it is the bounded in-place fix rather than scope creep — the four conditions, answered: (1) same defect class as this card, the identical first-newline capture; (2) mechanical, and the shape is pinned by the two markers this card names; (3) no other claim holds this file (28 open PRs' file lists read at dispatch time — none touches it; #18536's machine-side half is queued behind this card, not claimed); (4) same gate family, no new verification surface. The alternative was a roster that hand-lists five of six reason-bearing keys — a second copy of "which keys have a reason", wrong in the silent direction the moment a seventh arrives, which is exactly how these two sat outside #18422 for two cards.

#18661 / PR #18784 is read and not re-opened: its form roster and its dropped-declaration sound are untouched. ⛔ #18536's machine-side half is not folded in — nothing in this diff touches half-state or Clause-② machinery.

Pins — 26 new cases in --self-test

Per marker (no-check-families, inherited-population, self-test-reads):

  • a wrapped-reason case REFUSED by name, asserting the message carries the file, the file:line of the continuation, the marker key and the continuation's text — the refusal text pinned, not paraphrased;
  • a whole-reason control that still reads its reason (and, for no-check-families, that the workflow is still not a coverage gap) — the repair refuses a cut, it does not refuse the marker.

Plus: the refusal reaching checkFamilyCoverageGaps, the marker's one consumer; a blank-line-separated comment still not a continuation (the terminator all six live declarations write); inherited-population's invented-path refusal pinned as INDEPENDENT of the cut refusal; self-test-reads' missing-read-set refusal still firing first; the #-only restriction both ways, plus the restriction narrowing-only and its drift guard driven through a bad table; the wholeness roster pinned equal to the two builders' rosters and named at all six keys; the reason GROUP pinned per builder; and populationReasonCutRefusal pinned byte-equal to markerReasonCutRefusal so the split cannot drift.

The population markers' existing pins are byte-unchanged.

The census — the five (six) live declarations, measured whole

Taken in --self-test against the real tree at the head on every run, not printed once into this body. Each row is NAMED, never counted:

.github/workflows/merged-branch-reaper.yml:212  no-check-families
.github/workflows/os-create-smoke.yml:48        no-check-families
.github/workflows/scaffold-e2e.yml:23           no-check-families
scripts/cli-build-prerequisite.mjs:111          inherited-population
scripts/pm/check-expected-skips.mjs:131         self-test-reads
scripts/pm/dispatch-gates.mjs:702               inherited-population

The card's five, plus the self-test-reads declaration the sixth key brings. All six are one-liners followed by a blank line — no reason is cut in this tree today, which is the card's own reading, re-taken. The case asserts three things separately: the roster is exactly those six, every reason ENDS on its marker line, and every reason is non-empty. A non-vacuity control puts a continuation under a LIVE declaration and asserts that exact file is refused by name, so the census cannot pass by measuring nothing.

Ablation

From the COMMITTED fix, the registration removed on disk — the three refuseCutMarkerReason(...) calls, which is the mutation shape this file already names for this family ("deleting the populationReasonCutRefusal call from either refusal"). Mutation proven by blob hash before the run, restored by hash under a trap ... EXIT INT TERM after it:

head           : 05389952ee
head blob      : 1d4c95393c3749e68129dc2688e8df8c351aaa9b
on-disk before : 1d4c95393c3749e68129dc2688e8df8c351aaa9b
anchor count before: 3   (expect 3)
anchor count after : 0   (expect 0)
on-disk after  : 5ea16c9a050769027d876e39f8b7a0d21c439a03
MUTATION: PROVEN — blob hash moved and 3 anchor line(s) left the file

✗ dispatch-gates self-test: 5 of 1835 case(s) failed.
os-verify-lock: VERDICT command-exit 1 · held the lock 745s (12m25s) · waited 1s

RESTORE: hash-on-disk=1d4c95393c3749e68129dc2688e8df8c351aaa9b head-blob=1d4c95393c3749e68129dc2688e8df8c351aaa9b
RESTORE: OK — byte-identical to HEAD
RESTORE: git diff HEAD (must be empty):        [empty]

The five, and only the five, are the new refusal pins — one per marker, plus the consumer-reaching case and the census non-vacuity control:

✗ a no-check-families reason that does not END on the marker line is REFUSED, naming the workflow, the line, the marker and the continuation
✗ and the refusal reaches the ONE consumer this marker has — the boolean read in checkFamilyCoverageGaps refuses rather than accepting half a sentence
✗ an inherited-population reason that does not END on the marker line is REFUSED, naming the module, the line, the marker and the continuation
✗ the census is not vacuous over this tree: put a continuation under a LIVE declaration and exactly that file is refused, by name
✗ a self-test-reads reason that does not END on the marker line is REFUSED too, in the same words and naming the same four things

Nothing pre-existing reds. The continuation READING, the whole-reason controls, the roster pins and every population-marker pin stay green under the ablation — which is the point: this change registers a refusal, it does not change how a reason is read.

The self-test line

Run DETACHED with output to a file and waited on in the foreground (tail --pid on the detached pid), never under a foreground timeout, and under the shared verify lock:

✓ dispatch-gates self-test: 1835 cases pass.
os-verify-lock: VERDICT command-exit 0 · held the lock 734s (12m14s) · waited 0s

1809 before this change, 1835 after — 26 new cases. Shared-box seconds, not idle-box figures.

Derived gates, each with its exit code

Derived from the worktree with node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack (no hand-fed path list), every command run with the exit code captured by redirect-then-$?, then reconciled with --ran.

node scripts/check-ci-filter-parity.mjs                        :: exit 0
node scripts/check-closing-keyword-parity.mjs                  :: exit 0
node scripts/check-closing-keyword-parity.mjs --self-test      :: exit 0
node scripts/check-comment-mask-corpus.mjs                     :: exit 0
node scripts/check-declaration-mirrors.mjs                     :: exit 0
node scripts/check-declaration-mirrors.mjs --self-test         :: exit 0
node scripts/check-scripts-symbol-anchors.mjs                  :: exit 0
node scripts/check-scripts-symbol-anchors.mjs --self-test      :: exit 0
node scripts/check-self-test-wired.mjs                         :: exit 0
node scripts/check-self-test-wired.mjs --self-test             :: exit 0
node scripts/check-self-test-workflow-commands.mjs             :: exit 0
node scripts/check-self-test-workflow-commands.mjs --self-test :: exit 0
node scripts/check-whole-set-label-write.mjs                   :: exit 0
node scripts/check-whole-set-label-write.mjs --self-test       :: exit 0
pnpm check:agent-test-spelling                                 :: exit 0
pnpm check:bash32-floor                                        :: exit 0
pnpm check:cli-command-ids                                     :: exit 0
pnpm check:cross-package-test-inputs                           :: exit 0
pnpm check:declared-population-live                            :: exit 0
pnpm check:driver-memory-census                                :: exit 0
pnpm check:entry-guard                                         :: exit 0
pnpm check:nul-bytes                                           :: exit 0
pnpm check:parse-guard                                         :: exit 0
pnpm check:pm-dispatch-gates                                   :: exit 0   (the detached run above)
pnpm check:pnpm-filter-targets                                 :: exit 0
pnpm check:ratchet-remedy-authority                            :: exit 0
pnpm check:refd-timer-probe                                    :: exit 0
pnpm check:watch-hint-literal                                  :: exit 0
✓ 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).

Repo-wide pnpm lint (eslint . --no-inline-config): exit 0.

skip-changeset: the diff is one file under scripts/pm/, which no package's files[] ships — nothing published moves.

Acceptance notes

To file (class (c), a declaration the tool silently drops), NOT fixed here — different defect class from this card, so the bounded in-place exemption does not apply: the dropped-declaration probe unparsedPopulationMarkers (#18661) is keyed on POPULATION_MARKER_KEYS alone, so a line that READS as a no-check-families, inherited-population or self-test-reads declaration and does not PARSE as one makes no sound at all — no refusal, no row, no count — while the identical shape on a population key is reported. Measured on 034f5a3afd, three dropped spellings returned [] from the probe with a population-key control lighting on the same opener. The self-test-reads instance re-opens #18673's own hole: a dropped declaration takes a family out of the derived set, and the --ran reconciliation then says "0 NOT-MEASURED" about a set that no longer contains it. Dedupe words: unparsed marker probe roster · dropped declaration silent · POPULATION_MARKER_LOOKALIKE keys · inherited-population dropped · self-test-reads dropped.

Noted, not filed: routing inherited-population's block-form read through blockFormReason also drops the closing delimiter that m[3].trim() used to leave in the reason ("a reason */" becomes "a reason"). No live declaration is written in a block form, so nothing moved in this tree; it is measured in the block-form table above rather than left to be found. Next reader of this file: #18536's machine-side half.


Generated by Claude Code

`no-check-families` and `inherited-population` captured their reason with
the same `(\S.*)$`-under-`m` shape #18422 repaired for the three population
markers, so the capture ended at the first newline and a wrapped reason
reached its reader as a sentence that stops. Both now come out of the shared
grammar and are graded by the shared wholeness reading; a continued reason is
refused by name (file, line, marker, the continuation it saw).

The registration is by construction over the two grammar builders rather
than hand-listed, so `self-test-reads` — the sixth reason-bearing key, same
defect, same file — is closed in the same line instead of becoming a third
card on this file.

`no-check-families` is restricted to the `#` comment form: it is read out of
workflow YAML, where `#` is the only comment syntax, so the block forms
#18661 added cannot apply to it.

Claude-Session: https://claude.ai/code/session_01Gqi43smmqjJ5sUrhfoPeKu
Co-authored-by: Claude <noreply@anthropic.com>
Per marker: a wrapped reason REFUSED by name (file, line, marker, the
continuation it saw) with the refusal text pinned, and a whole-reason control
that still reads back. The `#`-only restriction on `no-check-families` is
pinned in both directions, as a filter of the shared roster rather than a
second grammar. The wholeness roster is pinned equal to the two grammar
builders' own key rosters, so a seventh key cannot arrive without the reading.

The live census reads the six declarations in the tree at the head and
asserts each one whole, named rather than counted, with a non-vacuity control
that puts a continuation under a live declaration and watches that file get
refused.

Claude-Session: https://claude.ai/code/session_01Gqi43smmqjJ5sUrhfoPeKu
Co-authored-by: Claude <noreply@anthropic.com>
@os-justin os-justin added the skip-changeset PR has no user-facing published change; bypasses the changeset gate label Sep 17, 2026 — with Claude
@os-justin
os-justin marked this pull request as ready for review September 17, 2026 23:21
@os-justin
os-justin added this pull request to the merge queue Sep 17, 2026
Merged via the queue into main with commit fbe5e1e Sep 17, 2026
37 checks passed
@os-justin
os-justin deleted the claude/issue-18662-marker-reason-wholeness branch September 17, 2026 23:45
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

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

Projects

None yet

2 participants