Skip to content

fix(pm): a population declaration's reason is whole, or the declaration is red - #18660

Merged
os-justin merged 1 commit into
mainfrom
claude/issue-18422-dispatch-gates-whole-reason
Sep 17, 2026
Merged

os-justin merged 1 commit into
mainfrom
claude/issue-18422-dispatch-gates-whole-reason

Conversation

@os-justin

Copy link
Copy Markdown
Collaborator

Fixes #18422

The defect

The three population markers in scripts/pm/dispatch-gates.mjsNO_PATH_POPULATION_MARKER, WHOLE_TREE_POPULATION_MARKER and WIDE_POPULATION_MARKER — each captured 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. Nothing refused it: read line by line, wholeTreePopulationRefusal checks 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_MARKER included — measured, not assumed), so the contract lands on all three.

Contract, before and after

before after
capture (\S.*)$ under m — ends at the first newline unchanged, and pinned as the defect
a wrapped reason reaches the seat as line one REFUSED, naming the declaration, the file and the line that continues it
what terminates a declaration nothing — the capture just stopped a blank line, a blank comment line, a non-comment line, EOF, or another dispatch-gates: declaration
consumers of the reason the fragment, silently the whole reason, or a refusal — alwaysRunsPopulationLines and the --json refused field for whole-tree and wide, the undetermined listing for no-path

The 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. populationReasonContinuation reads, off the same text, the comment line that continues a reason; readPopulationDeclaration records 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; populationReasonCutRefusal is 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).

  • 实际业务需求 — measured over the tree at this head: 27 declarations across 25 gate files, 26 already writing the whole reason on one line (up to 1215 characters of it, 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.
  • 项目长远合理性 — B is contract-first: a declaration has ONE spelling and the refusal states it. A adds a second grammar whose central question — is this following comment a continuation, or the next paragraph — nothing in the text can answer. This file refuses coin tosses everywhere else ("a derivation that picked either would be placing the family by a coin toss"); A is that coin toss, one channel further along.
  • 防 AI 写错元数据 — B is 契约收紧 with a loud refusal at author time naming the file and the line. A is 消费端宽容: it would silently make an unrelated comment part of a seat-facing reason — the same class of defect this card was filed on, pointed the other way, and unfalsifiable because there would be nothing to be red.
  • 创业阶段不扩散 — B is the smaller change and stays inside the refusals that already exist; A changes what a marker IS and grows the grammar for a spelling with no caller.

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-test registers cases with t(name, cond, detail) into one flat cases roster and holds the SELF_TEST_VERDICT handshake the dispatch refuses without; per docs/audits/2026-09-self-test-shape-census.md:377 its floor is NONE and its handshake is HELD — 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:

  • the [finding] git add -A over 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 .gitmodules row #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;
  • a one-line reason ⇒ unchanged — not continued by the code under it, nor by a blank line, a blank comment line, or EOF;
  • a following comment line that starts a NEW dispatch-gates: key ⇒ not a continuation (it is a second declaration; the pair refusals grade that shape);
  • the comment form must match — a # line under a // declaration is not a comment in that language;
  • the shell spelling read the same way, continuation and all;
  • a cut reason is REFUSED on each of the three channels, and the refusal names the declaration, the file and the line; a whole reason is refused nothing on any of them;
  • the two-marker pair refusals are unchanged by a cut reason — all three pairs still refuse as the contradiction they are;
  • the cut is named before the walk and before the hints;
  • the always-runs renderer prints a cut declaration as a REFUSED row rather than as the fragment it was cut down to;
  • the discovery keeps the FIRST declaration a family meets and the cut travels with it from the same file;
  • an unknown marker key is refused by both readings, and the field roster and the marker roster name the same three channels — neither can grow one alone.

The live derivation, over the tree at this head

Every declaration the three markers match today, run through the new contract:

LIVE DERIVATION: declarations=27 refused=1
RED check:objectui-bump [no-path-population] files=scripts/bump-objectui.selftest.sh
    declares no-path-population and its reason does not END on the marker line:
    scripts/bump-objectui.selftest.sh:51 continues it with "lives inside a disposable
    checkout under mktemp -d (a throwaway objectui and". …

One declaration reds — and it was being cut on main right now, not hypothetically: check:objectui-bump reached the seat as every path this file writes or reads, 36 characters of a 466-character reason wrapped over six comment lines. A refusal cannot land red on main, 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-bump exit 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 — populationReasonCutRefusal returns null once it has a declaration, which is the pre-fix state exactly (the cut is detectable and nothing is red).

HEAD blob: 75fe87e3d6930a223fb97e034e3ca9c8496213dc
injected marker count: 1
mutated blob: f3f37ce1cce932dc73b4af5991d093a6063fcdcc
ABLATION RUN EXIT: 1
✗ dispatch-gates self-test: 8 of 1771 case(s) failed.   (633.3s under the shared lock)
restored blob: 75fe87e3d6930a223fb97e034e3ca9c8496213dc
RESTORE VERIFIED: blob == HEAD blob, git diff HEAD empty

Predicted direction before the run: 转红. Observed: 转红, 8 cases, and they are exactly the ones the refusal buys —

✗ a cut WHOLE-TREE reason is refused, and the refusal names the declaration, the file and the line that continues it
✗ a cut WIDE reason is refused through the same shared reading, naming its own channel
✗ and a cut NO-PATH reason is refused too — the channel with no refusal function of its own is not the channel without the contract
✗ but a cut reason IS named before the walk that would back it and before the hints it would be graded against
✗ the always-runs renderer prints a cut declaration as a REFUSED row rather than as the fragment it was cut down to
✗ and that reading is not vacuous over this tree: a live entry reds the moment a continuation is put on it            (no-path)
✗ and not vacuously: a live whole-tree entry reds the moment a continuation is put on it
✗ and not vacuously: a live wide-population entry reds the moment a continuation is put on it

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 three every live … reason ENDS on its own marker line cases (0 cuts in the tree either way).

Run under trap '<restore>' EXIT INT TERM with an absolute REPO_ROOT, restored with git 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 empty git 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.

0  node scripts/check-ci-filter-parity.mjs                      0  pnpm check:agent-test-spelling
0  node scripts/check-closing-keyword-parity.mjs                0  pnpm check:bash32-floor
0  node scripts/check-closing-keyword-parity.mjs --self-test    0  pnpm check:cli-command-ids
0  node scripts/check-comment-mask-corpus.mjs                   0  pnpm check:cross-package-test-inputs
0  node scripts/check-declaration-mirrors.mjs                   0  pnpm check:declared-population-live
0  node scripts/check-declaration-mirrors.mjs --self-test       0  pnpm check:driver-memory-census
0  node scripts/check-scripts-symbol-anchors.mjs                0  pnpm check:entry-guard
0  node scripts/check-scripts-symbol-anchors.mjs --self-test    0  pnpm check:nul-bytes
0  node scripts/check-self-test-wired.mjs                       0  pnpm check:objectui-bump
0  node scripts/check-self-test-wired.mjs --self-test           0  pnpm check:parse-guard
0  node scripts/check-self-test-workflow-commands.mjs           0  pnpm check:pm-dispatch-gates
0  node scripts/check-self-test-workflow-commands.mjs --self-test  0  pnpm check:pnpm-filter-targets
0  node scripts/check-whole-set-label-write.mjs                 0  pnpm check:ratchet-remedy-authority
0  node scripts/check-whole-set-label-write.mjs --self-test     0  pnpm check:refd-timer-probe
0  node scripts/pm/bare-root-worklist.mjs --self-test           0  pnpm check:watch-hint-literal
✓ dispatch-gates --ran: 30 derived famil(ies) accounted for — 30 run, 0 NOT-MEASURED
  (a DERIVED zero — all 30 recorded an exit code and none of them is 3).

pnpm check:pm-dispatch-gates is this file's own --self-test and is HEAVY: ✓ dispatch-gates self-test: 1771 cases pass., 633.9s, run DETACHED per this file's own header (#14281) and under scripts/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/main moved three commits under this branch mid-run, two of them touching gate scripts, so the derivation was re-run from a scratch worktree at origin/main 84ad2e139 against these two paths: the family list came back byte-identical to the one above — no family was added by the drift.

pnpm lint repo-wide, not narrowed: exit 0 in 84s (node --stack-size=4000 eslint . --no-inline-config, at 3f4bf6b01). The narrowing this lane has been using was not needed on this run.

skip-changeset: scripts/pm/** and scripts/bump-objectui.selftest.sh are in no package's files[] — 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.

  1. To file — the marker's comment-form alternation is (?:\/\/|#), 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 -- …) and scripts/release-verify-npm.mjs:110 ( * dispatch-gates: no-path-population -- …). Measured: declaredNoPathPopulation returns null for both, both families sit in undetermined with hints=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.
  2. To file — the same first-newline capture is carried by the two markers OUTSIDE this card's three: NO_CHECK_FAMILIES_MARKER (workflow-level, read by declaredNoCheckFamiliesReason) and INHERITED_POPULATION_MARKER (module-level, its reason half). Neither is cut in the tree today (measured: the three live no-check-families declarations and both inherited-population declarations 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 in POPULATION_MARKER_KEYS / POPULATION_DECLARATION_FIELDS and 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-test has no per-battery floor roster (AGENTS.md 「Writing a --self-test」 asks for battery name → minimum case count); it registers into one flat cases roster. Already recorded — docs/audits/2026-09-self-test-shape-census.md:377 grades it floor NONE, handshake HELD. 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

…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
@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 13:24
@os-justin
os-justin added this pull request to the merge queue Sep 17, 2026
Merged via the queue into main with commit 7f7b855 Sep 17, 2026
37 checks passed
@os-justin
os-justin deleted the claude/issue-18422-dispatch-gates-whole-reason branch September 17, 2026 13:46
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