fix(pm): a population declaration's reason is whole, or the declaration is red - #18660
Merged
Merged
Conversation
…n is red The three population markers in `scripts/pm/dispatch-gates.mjs` capture their reason with `(\S.*)$` under the `m` flag, so the capture ends at the first newline. A reason an author wraps across two or three comment lines was captured as line one only, and no refusal ever asked whether it ended where the author did: `wholeTreePopulationRefusal` checks that a reason EXISTS and that a root walk BACKS it. The seat was handed a sentence that simply stops — and the reason is the one thing a seat reads off that row. The marker grammar now has one spelling (`populationMarkerPattern`), and `populationReasonContinuation` reads, off the same text, the comment line that continues a reason. A continued reason is refused by `populationReasonCutRefusal`, which names the declaration, the file and the line; the whole-tree and wide refusals delegate to it before the checks that read the reason text, and the no-path renderer prints a cut declaration as REFUSED rather than as the fragment it was cut down to. Live derivation over the tree: 27 declarations, one of them cut — `check:objectui-bump` reached the seat as "every path this file writes or reads". Its comment is made whole on one line here, verbatim, so no refusal lands red on main. Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Gqi43smmqjJ5sUrhfoPeKu
This was referenced Sep 17, 2026
os-justin
marked this pull request as ready for review
September 17, 2026 13:24
os-justin
deleted the
claude/issue-18422-dispatch-gates-whole-reason
branch
September 17, 2026 13:46
This was referenced Sep 17, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes #18422
The defect
The three population markers in
scripts/pm/dispatch-gates.mjs—NO_PATH_POPULATION_MARKER,WHOLE_TREE_POPULATION_MARKERandWIDE_POPULATION_MARKER— each captured their reason with(\S.*)$under themflag, so the capture ends at the first newline; a reason an author wraps across two or three comment lines was captured as line one only. Nothing refused it: read line by line,wholeTreePopulationRefusalchecks that a reason EXISTS, that no sibling marker contradicts it, and that a root walk BACKS it — never that it is whole, and there was no other caller that did. The row rendered as a sentence that merely stops, with nothing red at author time or at read time, and the reason is the one thing a seat reads off that row when deciding whether a family belongs on its card. Measured on #17472 / PR #18414; measured again live in this tree, below.All three markers shared the shape (
WIDE_POPULATION_MARKERincluded — measured, not assumed), so the contract lands on all three.Contract, before and after
(\S.*)$underm— ends at the first newlinedispatch-gates:declarationalwaysRunsPopulationLinesand the--jsonrefusedfield for whole-tree and wide, the undetermined listing for no-pathThe grammar now has ONE spelling (
populationMarkerPattern), because the continuation reading has to agree with the capture about what a marker line is, down to the comment form; two spellings of one grammar drift silently.populationReasonContinuationreads, off the same text, the comment line that continues a reason;readPopulationDeclarationrecords the reason and its continuation from ONE source and ONE file, so a refusal can never grade file A's reason against file B's continuation;populationReasonCutRefusalis the refusal, shared by all three channels, and the whole-tree and wide refusals delegate to it.Ordering, deliberate and pinned both ways: the cut is refused after the two-marker pair refusals (a declaration that contradicts a sibling is refused for THAT, in the words a reader has been getting for it) and before the whole-tree walk check and the wide hint check — both of those grade the declaration against a reason this reading says is only part of one, so a hint named in the wrapped half would read as unaccounted for and the refusal would name the wrong defect in confident words.
Shape B, and why — the four axes
Shape B (refuse at author time) over shape A (consume a comment block).
check:route-envelope), and exactly one wrapped and being cut today. The one-line reason is what this convention already IS; the pull is for the one declaration to be made whole, not for a multi-line grammar nobody in this tree writes.No axis conflicts, so there is no trade-off to hand up. The triage boundary (5713131801) is held: this is the capture domain and its validation, ⛔ not a marker that matches arbitrary multi-line text.
The pins
The file's
--self-testregisters cases witht(name, cond, detail)into one flatcasesroster and holds theSELF_TEST_VERDICThandshake the dispatch refuses without; perdocs/audits/2026-09-self-test-shape-census.md:377its floor isNONEand its handshake isHELD— a recorded shape, unchanged here.Registered: 25 new cases (fixture battery
A declaration's reason is WHOLE, or the declaration is RED (#18422), plus three live-half pairs). Whole file after:✓ dispatch-gates self-test: 1771 cases pass.What they pin, clause by clause:
git add -Aover a worktree inside the checkout stages a mode-160000 gitlink at exit 0 with a warning — no repo-side gate refuses a gitlink that has no.gitmodulesrow #17472 first-draft shape as a case — the capture still ends at the first newline (pinned as the defect, not as a claim it went away), and the wholeness reading names the continuation line;dispatch-gates:key ⇒ not a continuation (it is a second declaration; the pair refusals grade that shape);#line under a//declaration is not a comment in that language;The live derivation, over the tree at this head
Every declaration the three markers match today, run through the new contract:
One declaration reds — and it was being cut on
mainright now, not hypothetically:check:objectui-bumpreached the seat asevery path this file writes or reads, 36 characters of a 466-character reason wrapped over six comment lines. A refusal cannot land red onmain, so the declaration is made whole on ONE line in this same PR:scripts/bump-objectui.selftest.sh:50— comment-only edit, the author's words joined verbatim at the wrap points (the join is asserted byte-for-byte against the original lines, not retyped).pnpm check:objectui-bumpexit 0 after it.After the repair: 27 declarations, 0 refused. The count is not vacuous — the refusal fired on this tree before the repair (above), and three live-half cases in the self-test re-fire it per channel by putting a continuation on a live entry and asserting the refusal names it.
Ablation, from the committed fix
Mutation: the refusal deleted —
populationReasonCutRefusalreturns null once it has a declaration, which is the pre-fix state exactly (the cut is detectable and nothing is red).Predicted direction before the run: 转红. Observed: 转红, 8 cases, and they are exactly the ones the refusal buys —
The controls stayed GREEN under the same mutation, which is what makes these an instrument rather than a restatement: the two-marker pair refusals,
a WHOLE reason is refused nothing on any of the three, every continuation-reading case (the detector still works — it is the refusal that was deleted, which is the card's whole point: the cut is knowable and nothing is red), and the threeevery live … reason ENDS on its own marker linecases (0 cuts in the tree either way).Run under
trap '<restore>' EXIT INT TERMwith an absoluteREPO_ROOT, restored withgit checkout HEAD -- <path>(never the bare form, which takes the mutation back out of the index), and the restore proven by blob hash AND by an emptygit diff HEAD— not by an exit code.Gates
Derived from the tree with
node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack(no hand-fed path list; the tool takes its own change set off the merge base), every command run, each exit code captured redirect-then-$?, reconciled with--ran.30 derived, 30 run, 0 NOT-MEASURED, 0 UNRUN — every one exit 0.
pnpm check:pm-dispatch-gatesis this file's own--self-testand is HEAVY:✓ dispatch-gates self-test: 1771 cases pass., 633.9s, run DETACHED per this file's own header (#14281) and underscripts/pm/os-verify-lock.sh(VERDICT command-exit 0 · held the lock 635s · waited 0s). It exceeds the ~10-minute foreground cap, which is why the header says to detach it.origin/mainmoved three commits under this branch mid-run, two of them touching gate scripts, so the derivation was re-run from a scratch worktree atorigin/main84ad2e139against these two paths: the family list came back byte-identical to the one above — no family was added by the drift.pnpm lintrepo-wide, not narrowed: exit 0 in 84s (node --stack-size=4000 eslint . --no-inline-config, at3f4bf6b01). The narrowing this lane has been using was not needed on this run.skip-changeset:scripts/pm/**andscripts/bump-objectui.selftest.share in no package'sfiles[]— nothing published moves.Out-of-scope findings (⛔ not fixed here)
Both are the same CLASS as this card — an author writes a declaration and the consumer silently drops part or all of it — and both are OUTSIDE the file surface triage drew (5713131801), so they are named here for the seat rather than repaired in this PR.
(?:\/\/|#), so a declaration written inside a BLOCK comment parses as nothing at all. Two live specimens:scripts/symbol-anchors.mjs:180(/* dispatch-gates: no-path-population -- …) andscripts/release-verify-npm.mjs:110(* dispatch-gates: no-path-population -- …). Measured:declaredNoPathPopulationreturnsnullfor both, both families sit inundeterminedwithhints=0, and their authors' examined-and-explained status is dropped with no tell — the residue counts them with the families nobody has looked at, which is the exact bucket the marker exists to split. Dedupe words:block comment marker,no-path-population unparsed,symbol-anchors declaration,release-verify-npm population,comment form alternation.NO_CHECK_FAMILIES_MARKER(workflow-level, read bydeclaredNoCheckFamiliesReason) andINHERITED_POPULATION_MARKER(module-level, itsreasonhalf). Neither is cut in the tree today (measured: the three liveno-check-familiesdeclarations and bothinherited-populationdeclarations are one-liners followed by a blank line), so this is exposure, not a live defect. The repair is one line each: register the key inPOPULATION_MARKER_KEYS/POPULATION_DECLARATION_FIELDSand read the continuation — the helper this PR adds is generic and was deliberately built so the class closes in one move. Dedupe words:no-check-families reason cut,inherited-population reason,population marker wholeness,first newline capture,dispatch-gates marker roster.Acceptance notes
noted, not filed: this file's--self-testhas no per-battery floor roster (AGENTS.md 「Writing a--self-test」 asks for battery name → minimum case count); it registers into one flatcasesroster. Already recorded —docs/audits/2026-09-self-test-shape-census.md:377grades it floorNONE, handshakeHELD. Not a finding, a censused state. 承接者: whoever works that census row.🤖 Generated with Claude Code
https://claude.ai/code/session_01Gqi43smmqjJ5sUrhfoPeKu
Generated by Claude Code