feat(pm): two report-only patrol rows — the decision card's four-facet block, and finding outliving its grade - #18033
Conversation
…acet block The `os-decision-facets` marker is declared machine-findable by references/decision-analysis.md 〈四棱卡面块固定形状〉 and, measured on origin/main, is read by nothing: one hit in the whole tree, its own declaration, against a positive control (needs-user-decision reaches seven files under scripts/). This adds the first reader. H62 is report-only: an OPEN card carrying the decision label whose BODY carries no marker, in either spelling. Presence is a literal containment test over the raw body, which is the extraction rule the shape names for itself (「提取按字面 grep」); code-stripping and a positional test are both refused by name because each produces 「注释形读不到永不读作无四棱块」, the one direction of error the shape forbids. The prior-ruling line the shape puts after the four facets is read as a SECOND reading on the same row — present / unresolved / absent — and never as a second row. Its writer is check-prior-rulings.mjs, coupled here by source text rather than by import: both import directions were measured and neither runs (static is a TDZ cycle, dynamic deadlocks against this module's own top-level await). Population is the unscoped open listing, for H31's reason; the row costs no request and writes nothing. H4 / H13 / H14 / H22 / H29 are byte-unchanged. Claude-Session: https://claude.ai/code/session_01DAcomhvR9kKizeYgg89Vo8 Co-authored-by: Claude <noreply@anthropic.com>
…ire the sweep's self-test in CI The state model makes `finding` transient — 「观察类记录,恒 = 待首次定级;定级即 离标」 — and the population has been re-measured and hand-cleared three times (385 of 395, then 185 of 226, then 38 by hand, then the one-time sweep to 0 on both boards). It refills because the strip has to be passed by the caller on every grading write. The write-path half is its own card; this is the read half: H63 reports an open card carrying the marker beside a grading state or any `priority:*`, so the refill is visible on the patrol instead of being re-measured by whoever next looks. The rule is REUSED, not copied — and the import can only point one way, which was measured rather than assumed: the sweep already imports `PM_STATE_LABELS` from the patrol and derives module-level constants from it, so the patrol importing the sweep throws `ReferenceError: Cannot access 'PM_STATE_LABELS' before initialization` the moment the patrol is the entry point, which it is on every run. So the screen moves down into the module that owns the vocabulary, and the sweep re-exports it under the names its callers and its 91-case self-test already use — unchanged, and pinned by source text from the patrol's own self-test so a copy cannot re-grow. The row does not contradict H13 (its set carries the marker, which is why H13 is silent on a graded carrier), H22 (open-only) or H29 (a card KIND, not a position); the docblock names each. CI wiring the D1 report left unfired: `check:pm-stale-finding` in package.json and a self-test step in lint.yml. `check-self-test-wired` population 208 → 209, green both directions — the tool shipped a self-test nothing ran. Claude-Session: https://claude.ai/code/session_01DAcomhvR9kKizeYgg89Vo8 Co-authored-by: Claude <noreply@anthropic.com>
Wiring `check:pm-stale-finding` into package.json makes this tool a DECLARING family for `dispatch-gates`, and `check:declared-population-live` reds on it: the module-level run fixture's repo slug is the only path-shaped literal in the file, so the derivation read it as a declared population of one path that names no tracked file — the same output a gate with no literals produces. Both halves of the gate's own remedy: the `no-path-population` marker, which is true of this tool (its whole input is the GitHub API), and the fixture moved into the self-test body, where the module-body mask reaches it. Top-level VALUE declarations are deliberately never masked — being unreferenced at module scope is what a declared population IS — so the comment on the moved fixture says ⛔ do not hoist it. `check:declared-population-live`: 250 declaring families with 1 unreachable → 249, all reaching. The sweep's 91-case self-test is unchanged. Claude-Session: https://claude.ai/code/session_01DAcomhvR9kKizeYgg89Vo8 Co-authored-by: Claude <noreply@anthropic.com>
|
CI red, being worked — skills seat (session Generated by Claude Code |
CI red at `Lint & Repo Gates` step 32 (run 34759389219): `dispatch-gates self-test: 1 of 1682 case(s) failed` — 「every gate CI runs whose own source sweeps the repo root is DECLARED whole-tree, declared path-less, derivable by path, or a justified row in ROOT_WALK_RESIDUE_LEDGER — listed but no longer a member: check:pm-half-states」. Root cause, measured: this PR's self-test reads two sibling sources as program text — `sweep-stale-finding.mjs`, to pin that it still ALIASES the one stale-`finding` screen rather than re-growing a copy, and `check-prior-rulings. mjs`, to pin that the prior-ruling line's writer still prints the key the patrol greps. The derivation reads those literals and now places the family BY PATH (`dispatch-gates scripts/pm/sweep-stale-finding.mjs` ⇢ 「program text read by scripts/pm/check-half-states.mjs」), so it left the residue population — and the ledger reds on a listed family that is no longer a member, by its own contract: a stale exclusion is an exclusion nobody measures. The repair is the ledger's, ⛔ not the self-test's: the row goes, with a note saying why the family left and why this placement is the OPPOSITE of the #15753 one the row was written about — that one came from a noise-floor constant and said the opposite of what the constant declares, while these two literals are exactly what the self-test reads. A card touching either sibling now really does owe this gate. `dispatch-gates --self-test`: 1682 of 1682 pass, exit 0. Claude-Session: https://claude.ai/code/session_01DAcomhvR9kKizeYgg89Vo8 Co-authored-by: Claude <noreply@anthropic.com>
…trol-rows-facets-and-stale-finding
Contract reviewHead: ① derived judgments — two report-only patrol rows and the CI wiring the D1 left unfired:
Seat measurements on the head tree ( ② semver: nothing published; ③ boundary flags: Implemented-by: Verdict: PASS — both D2 halves as claimed (5653347020, 5653347348): two report-only rows, one rule for the stale- Generated by Claude Code |
Fixes #17009
Fixes #16904
Both cards' D2 halves. #17009's D1 landed as
ea1da645(PR #17515) and #16904's asae1b2588(PR #17994, plus the seat's one-time sweep, readings 5652947318); these two rows are the last deliverable on each card, so merging this closes both.Two report-only rows in
scripts/pm/check-half-states.mjs, one per card, plus the CI wiring #16904's D1 report left unfired. No--writepath anywhere, no label written, no gate added.H62 (#17009) — a decision card whose face carries no four-facet block
An open
needs-user-decisioncard whose body carries noos-decision-facetsmarker, in either spelling (plain-text line or the HTML-comment form). Presence is a literal containment test over the raw body, which is the extraction rule the shape names for itself (「提取按字面 grep」); code-stripping and a positional read are refused by name in the docblock because each produces 「注释形读不到永不读作无四棱块」, the one direction of error the shape forbids. Body only: 「落卡即带」 makes the block part of the card face, so a marker in a comment is not the shape, and the row buys no thread.The
Prior rulings read:line is a second reading on the same row —present/unresolved/absent, in the row text, never a second row. Its writer ischeck-prior-rulings.mjs, coupled here by source text: both import directions were measured and neither runs (static is a TDZ cycle; dynamic deadlocks against this module's own top-level await). The docblock records both measurements so nobody retries them.H63 (#16904) — a graded card still wearing
findingAn open card carrying
findingbeside a grading state or anypriority:*. The rule is reused by import, never copied — but the arrow is forced, not chosen: the sweep already importsPM_STATE_LABELSfrom the patrol and derives module-level constants from it, so the patrol importing the sweep throwsReferenceError: Cannot access 'PM_STATE_LABELS' before initializationthe moment the patrol is the entry point, which it is on every run and every self-test. So the screen moved down into the module that owns the vocabulary, andsweep-stale-finding.mjsre-exports it under the names its callers and its 91-case self-test already use. The patrol's own self-test pins by source text that the sweep still aliases that definition and has not re-grown a copy.The row does not contradict the pins it sits beside, and the docblock names each: H13's set carries
finding(which is exactly why H13 is silent on a graded carrier — the card is visible, to the wrong reader); H22 drops it and this row is open-only; H29 drops it as a card KIND rather than a position, and this row explicitly does not claim otherwise.Wiring:
check:pm-stale-findinginpackage.jsonand a self-test step inlint.yml.check-self-test-wiredpopulation 208 → 209, green in both directions — the tool shipped a 91-case self-test that nothing in CI ran.Acceptance greps, both directions
H62 (#17009)/H63 (#16904)incheck-half-states.mjsDecision-box dependency flags (instruction ④)os-decision-facetsreader incheck-half-states.mjscheck:pm-stale-findinginpackage.jsoncheck:pm-stale-findinginlint.ymlcheck-half-states --self-testcasessweep-stale-finding --self-testcasescheck-self-test-wiredscripts CI runscheck:declared-population-livedeclaring families with an unreachable populationUntouched, proved by md5 of the whole exported function body — H4
1d13e919…, H13fa79039c…, H141e88cafd…, H22a45048e0…, H29aaa6ea92…: identical before and after. The instruction-④ section,.claude/**and the write-path half offindingstripping are untouched.Live readings, read-only, taken from this tree
H63: the landed sweep's own dry run against this board — 33 open cards carry
finding, 0 stale, 33 genuinely ungraded, 0 unjudged; 1 listing, 0 writes,rate_limitcore 14747 → 14746. Zero is the expected reading right after the seat's one-time sweep (19 stripped here, 36 on objectui), and it is what the row exists to watch refill.H62: the decision inbox is 1 open card today, not the 27 the D1 measured — it fires on that one, second reading⚠️ No live specimen carries the marker, so the clean direction has no lit control on today's board; its controls are the self-test's fixtures, in both spellings.
absent.Gates
Derived on the merged head
1d52dd97withdispatch-gates --commands --repo objectstack-ai/objectstack(no paths): 74 families, 74 run, every one exit 0. Reconciled with--rancarrying a recorded exit code per family: 「74 derived famil(ies) accounted for — 74 run, 0 NOT-MEASURED (a DERIVED zero — all 74 recorded an exit code and none of them is 3)」.Five build-reading families first answered
PREREQUISITE NOT MET(exit 3) in a fresh worktree — recorded as NOT MEASURED, never as findings — and all five exit 0 afterpnpm buildunder the shared verify lock (VERDICT command-exit 0 · held 391s · waited 0s, 73/73 tasks). One of the five,check:type-check-debt, refused a second time on a heap ceiling my ownNODE_OPTIONSimposed (its own line says so: 「tsc runs under --max-old-space-size=4096 MB — the caller's NODE_OPTIONS, which is tighter」) and exits 0 at the CI-shaped 6144.pnpm lintrun repo-wide rather than narrowed:eslint --no-inline-config . --format jsonexit 0, 6723 files linted, 0 with findings — no narrowing claimed. Control-character sweep over every touched file: no match.origin/mainmoved again after this merge; the derivation says the tree is 1 commit behind on 3 files it derives from. The merge queue rebuilds and re-runs on that generation, which is where that half is measured.Deviations, declared
scripts/pm/sweep-stale-finding.mjsis edited to hand the screen down, because ruling ② mandates reuse by import and the only import direction that runs points the other way (measured; the crash is reproduced in the docblock). Its public API, its behaviour and its 91-case self-test are unchanged.package.jsonmakes it a declaring family fordispatch-gates, andcheck:declared-population-livereds on the module-level fixture's repo slug. Fixed by the gate's own remedy: theno-path-populationmarker (true — the tool's whole input is the GitHub API) plus moving the fixture into the self-test body, where the module-body mask reaches it.dispatch-gates --self-testthen red on one case of 1682 (CI run 34759389219,Lint & Repo Gatesstep 32): 「listed but no longer a member: check:pm-half-states」. The patrol's two sibling source-text pins make the family placeable by path (dispatch-gates scripts/pm/sweep-stale-finding.mjs⇢ 「program text read by scripts/pm/check-half-states.mjs」), so it left the root-walk residue population and itsROOT_WALK_RESIDUE_LEDGERrow went stale — which that ledger reds on by its own contract. The row is removed with a note; the placement is true and is the opposite of theextractWatchHintsreads a noise-floor EXCLUSION constant as a watch hint:.changesetderivescheck:pm-half-statesonto every changeset-adding card #15753 one the row was written about.origin/mainmerged (1d52dd97) because the derivation warned the tree was stale on a file it derives from — and it was load-bearing: the mergedcheck-clause2-carriers.mjsaddscheck:pm-clause2-carriersto this card's family list (73 → 74).skip-changeset:scripts/pm/**, a rootpackage.jsonscript and a workflow publish nothing from any released package.Authored in Claude Code session
session_01DAcomhvR9kKizeYgg89Vo8.Generated by Claude Code