Repository navigation
Commit 7f7b855
fix(pm): a population declaration's reason is whole, or the declaration is red (#18660)
Fixes #18422
## The defect
The three population markers in `scripts/pm/dispatch-gates.mjs` —
`NO_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 **#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: 75fe87e
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: 75fe87e
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.com/claude-code)
https://claude.ai/code/session_01Gqi43smmqjJ5sUrhfoPeKu
---
_Generated by [Claude
Code](https://claude.ai/code/session_01Gqi43smmqjJ5sUrhfoPeKu)_
Co-authored-by: Claude <noreply@anthropic.com>1 parent c049e74 commit 7f7b855
2 files changed
Lines changed: 485 additions & 20 deletions
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
47 | 47 | | |
48 | 48 | | |
49 | 49 | | |
50 | | - | |
51 | | - | |
52 | | - | |
53 | | - | |
54 | | - | |
55 | | - | |
56 | | - | |
| 50 | + | |
57 | 51 | | |
58 | 52 | | |
59 | 53 | | |
| |||
0 commit comments