Skip to content

Commit 7f7b855

Browse files
os-justinclaude
andauthored
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

File tree

‎scripts/bump-objectui.selftest.sh‎

Lines changed: 1 addition & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -47,13 +47,7 @@
4747
# the real `objectui-changeset-digest.mjs` (a byte copy, like the script under
4848
# test) through a throwaway objectui repo with a real changeset commit.
4949
#
50-
# dispatch-gates: no-path-population -- every path this file writes or reads
51-
# lives inside a disposable checkout under mktemp -d (a throwaway objectui and
52-
# a throwaway FRAMEWORK_ROOT, both destroyed by the EXIT trap below), so no
53-
# quoted literal in this file names a path this repo tracks. Case 5's fixture
54-
# changeset filename is assembled from CHANGESET_NAME by interpolation
55-
# everywhere it is used, deliberately never spelled as one bare quoted token,
56-
# so it does not read as a declared population here either.
50+
# dispatch-gates: no-path-population -- every path this file writes or reads lives inside a disposable checkout under mktemp -d (a throwaway objectui and a throwaway FRAMEWORK_ROOT, both destroyed by the EXIT trap below), so no quoted literal in this file names a path this repo tracks. Case 5's fixture changeset filename is assembled from CHANGESET_NAME by interpolation everywhere it is used, deliberately never spelled as one bare quoted token, so it does not read as a declared population here either.
5751

5852
set -euo pipefail
5953

0 commit comments

Comments
 (0)