fix(pm): check-half-states reads every Blocked-by: directive on a line, and every ref in one - #19455
Conversation
…in one `check-half-states.mjs` took ONE target from a line stating several, and for a directive whose remainder is prose it took whichever number the ref walk reached first. Both readings under-report a block that H19's row then releases: a live body with nine `·`-separated backticked directives resolved 1 target and printed 「1 of 1 CLOSED」, and a body whose directive opens with a possessive resolved that possessive — a card that was never a blocker — and dropped both real blockers. Two halves, in the two places they belong: - the shared decorated-directive reader now splits a line at every FURTHER key that follows a separator run, so each directive yields its own value and the whole directive family gains it. A key after a WORD is still prose. - the `Blocked-by:` walk now takes EVERY ref in a directive's value instead of the leading run. The run was taken to keep a context `#N` out of the index, and it did not: a prose-opening value filed a phantom AND dropped the real blockers. An extra target can only withhold a discharge; a dropped one releases a live block. Self-test: the two measured live shapes as fixtures (9 targets, both real blockers), the two dark controls (one directive is one target; prose outside a directive invents nothing), the family pin on a non-`Blocked-by:` key, and the end-to-end count 「1 of 2」 from a two-directive body line. Claude-Session: https://claude.ai/code/session_017ETYWqMQD4qMtZzAGovWNi Co-authored-by: Claude <noreply@anthropic.com>
Contract reviewServed-tier: ① Derived judgments
② Semver levelNone — nothing published; ③ Boundary flags
Implemented-by: VERDICT: PASS Generated by Claude Code |
Fixes #19439
Clause-②: no
check-half-states.mjsread one target from a line stating several, and for a directive whose remainder is prose it read whichever number the ref walk reached first. Both readings under-report a block, and H19's row is what the unlock scan's release act fires from.The two measured shapes, before and after (live bodies, read this pass)
·-separated directives on one linedirectiveValues1 value,blockedByTargets1 target (#8345) — H19 printed 「1 of 1 … CLOSED」#8345, #8648 … #8655)objectui#7434— a possessive that was never a blocker; both real blockers droppedobjectui#7434, #8090, objectui#8065— both real blockers read#9and#10outside any directiveWhat lands
Two halves, in the two places they belong.
scripts/pm/check-half-states.mjsonly, net +79 lines (101 added / 22 removed), including 16 self-test cases.directiveValues) yields EVERY directive on a line. A line splits at every further key that follows a separator run — decoration, spaces and one of·•|,;、, and nothing else. ⛔ A key that follows a WORD is still prose, so the anchoring contract that keeps 「seats park theBlocked-by: #1line in comments」 out of the index is unchanged. Every directive family reads through this one reader —Blocked-by,Restart-when(H9),Unlock-action,Maintainer-action,Prior-rulings,Reviewed-by,Seam— so all of them gain the multiplicity, and the pin that they invent nothing from prose is asserted on a NON-Blocked-by:key.Blocked-by:walk takes EVERY ref in a directive's value, not the leading run.The prose-remainder decision, on the four axes
The card offered two readings for a directive whose remainder is prose: every
#Nin the value is a target, or the value is reported UNJUDGED with the line quoted. Every#Nis a target.{repo, number}refs; H19'sunresolvedstate is produced by the RESOLVER, so feeding it from the extractor means a new return shape throughbuildBlockingIndex,blockerTargetsForandblockerChannelKeys— and the card ⛔ forbids changing H19's UNJUDGED arm.#Nout of the index, and it does not buy that: on objectui#8093 it filed the possessive as a dependent AND dropped both real blockers. So it files a phantom either way; what it ADDS is substitution, and a block whose real blockers are invisible reads as expired. Reading every ref can only ADD a target, and an extra target can only WITHHOLD a discharge (H19 counts it open, names it on the row, and says PARTIAL) — ⛔ never found a release. That is this file's standing posture on an ambiguity (never resolve one in the direction of release), applied to the value instead of to the resolution.#Nin a directive's prose becomes a target. On these boards that is 13 new reverse-index edges (see below) and, where a comment narrates in a line-initialBlocked-by:sentence, a row that says PARTIAL where it used to say 「every target it names is closed」. The row still fires and names the extra target, and — unlike the comment archive this file refuses to found a verdict on — the line carrying it is rewritable by the seat that wrote it.The census the card asks for as the first act
Population: 140 open
pm:blockedcards on both boards (objectstack 68, objectui 72), body AND comments (140 comment sets fetched), read 2026-09-20T23:10Z. Both readers run over the same texts; the old one is objectstack at57ceb9d.Blocked-by:directive (objectstack 62, objectui 54) — 167 directive values under the old reader (89 + 78).pm:blocking(objectstack#13457), 3 are openpm:blockedcards that really are realtime: no WS/SSE transport — service-realtime is in-process only and clients long-poll; big-screen/command-center scenarios need real push #8347's blockers (objectui#8649, 【能力需求】plugin-approvals 审批请求可见性:对业务记录有读权者应可只读查看审批动态(或提供可见性 hook) #8652, finding:check-changeset-no-major --self-testgoes red repo-wide after a release exits pre mode — its control requires major-declaring changesets AND apre.jsonthat no longer exists #8654 — a correct missing-cache row that was invisible before), and 1 is objectstack#11333, named as the parent in a triage audit comment — the single edge whose blocker status is a judgement call.#9/#10outside a directive → 0 targets under BOTH readers; one directive → 1 under both.pm:blockedcards only. objectui#8093 — triage's substitution specimen — is NOT in this population: it carriespm:queuetoday, released since that reading. So the substitution class is under-counted here by construction, not absent.Verification
node scripts/pm/check-half-states.mjs --self-test:: exit 0 — 5160 cases pass (16 added), every battery above its pin;SELF_TEST_BATTERY_FLOORuntouched (8), no new battery opened.da8c088:node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack(no paths) → 42 commands, 42 run, every one exit 0, reconciled with--ranover a record carrying each exit code: 「42 derived, 42 run, 0 NOT-MEASURED, 0 UNRUN … a DERIVED zero」.pnpm check:pm-dispatch-gatestook 962.5s on this box and is in that 42. Every exit code was captured before any pipe.Restart-whenplus its prose control, the URL-fragment control, and the end-to-end count — a two-directive body line with one closed and one open target reads 「1 of 2」, ⛔ never 「1 of 1」.Blocked-by: #123 (see #456 for the background)now reads123,456, with the reason beside it.维护者速读(草稿)
Blocked-by:的两处修正——一行上写了几条指令就读几条;一条指令的值里有几个#N就算几个阻塞目标。objectui#7434's当成了阻塞方,两个真阻塞被丢掉。Acceptance notes
noted, not filed: four of the changed texts are narration lines in comments that begin with a decoratedBlocked-by:key and then describe a re-point in prose (「Blocked-by:re-pointed#14096→#15195…」). The line-anchored reader counts them as directives — that is pre-existing and deliberate (it models the literal grep the unlock scan runs), and it is what makes their prose numbers reachable at all. Narrowing it is a change to WHICH LINES are read, which this card ⛔ does not authorise. Carrier: the next card that touchesdirectiveValues' anchoring.noted, not filed: a cross-repo qualifier separated from its#by decoration (objectui**#7434**) still reads as a LOCAL ref, before and after — the ref grammar is unchanged in that respect. Carrier: none today; no live specimen on either board in this census.Generated by Claude Code