Skip to content

fix(pm): check-half-states reads every Blocked-by: directive on a line, and every ref in one - #19455

Merged
os-steve merged 1 commit into
mainfrom
claude/issue-19439-blocked-by-reader-every-directive
Sep 21, 2026
Merged

os-steve merged 1 commit into
mainfrom
claude/issue-19439-blocked-by-reader-every-directive

Conversation

@os-steve

Copy link
Copy Markdown
Collaborator

Fixes #19439

Clause-②: no

check-half-states.mjs read 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)

shape before after
objectui#8347 body — NINE backticked ·-separated directives on one line directiveValues 1 value, blockedByTargets 1 target (#8345) — H19 printed 「1 of 1 … CLOSED」 9 values, 9 targets (#8345, #8648 … #8655)
objectui#8093 body — one directive whose remainder is prose naming three numbers 1 target, objectui#7434 — a possessive that was never a blocker; both real blockers dropped 3 targets, objectui#7434, #8090, objectui#8065 — both real blockers read
DARK control — one directive 1 target 1 target
DARK control — prose naming #9 and #10 outside any directive 0 0

⚠️ One correction to the reading this was dispatched on: the seat measured 「nine directives on TWO lines → 2 targets」 from the card's quoted (line-wrapped) rendering. objectui#8347's live body carries all nine on ONE line, so the reading there is 1 target, not 2 — which is exactly the 「1 of 1」 H19 published. The defect is one notch worse than the claim stated.

What lands

Two halves, in the two places they belong. scripts/pm/check-half-states.mjs only, net +79 lines (101 added / 22 removed), including 16 self-test cases.

  1. The shared decorated-directive reader (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 the Blocked-by: #1 line 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.
  2. The 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 #N in the value is a target, or the value is reported UNJUDGED with the line quoted. Every #N is a target.

  • 实际业务需求 — measured, not assumed. The prose-remainder shape is live on both boards (8 of the 116 blocked cards that carry a directive), and in 4 of them the old reader extracted zero targets from a directive a seat really wrote: objectstack#14570, objectstack#13458, objectui#9273, objectui#5987. A stated block that the machine turns into nothing is the same silence this item exists to end.
  • 项目长远合理性 — one rule ("a directive's value names blockers; every reference in it is one") instead of two ("the leading run, unless…"). The UNJUDGED reading needs a second verdict channel out of a parser that returns {repo, number} refs; H19's unresolved state is produced by the RESOLVER, so feeding it from the extractor means a new return shape through buildBlockingIndex, blockerTargetsFor and blockerChannelKeys — and the card ⛔ forbids changing H19's UNJUDGED arm.
  • 防 AI 写错 — decisive. The leading run was taken to keep a context #N out 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.
  • 创业阶段不扩散 — no new surface, no new verdict, no new flag, no config. The declared file surface only.

⚠️ The price, measured rather than asserted: a context #N in 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-initial Blocked-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:blocked cards 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 at 57ceb9d.

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_FLOOR untouched (8), no new battery opened.
  • The derived union, taken at the final commit da8c088: node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack (no paths) → 42 commands, 42 run, every one exit 0, reconciled with --ran over a record carrying each exit code: 「42 derived, 42 run, 0 NOT-MEASURED, 0 UNRUN … a DERIVED zero」. pnpm check:pm-dispatch-gates took 962.5s on this box and is in that 42. Every exit code was captured before any pipe.
  • New self-test cases: the two measured live shapes as fixtures (the realtime: no WS/SSE transport — service-realtime is in-process only and clients long-poll; big-screen/command-center scenarios need real push #8347 line byte-identical to the live body), both dark controls, the family pin on Restart-when plus 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」.
  • The pin this overturns is rewritten in place, not deleted: Blocked-by: #123 (see #456 for the background) now reads 123,456, with the reason beside it.

维护者速读(草稿)

  • 改了什么:巡检脚本读 Blocked-by: 的两处修正——一行上写了几条指令就读几条;一条指令的值里有几个 #N 就算几个阻塞目标。
  • 为什么改:H19 的行是「解锁放行」的触发器。它在 objectui#8347 上打印「1 of 1 已关闭」,而该卡九个阻塞里三个还开着、卡面自己写着「未裁决完不得派发」;另一张卡把句中的所有格 objectui#7434's 当成了阻塞方,两个真阻塞被丢掉。
  • 风险与代价(含回滚):代价已实测——指令值里的「顺带提到的卡号」也会算作目标,两板共新增 13 条反向索引边(其中 3 条是本来就该有的真阻塞),H19 可能把「全部关闭」改口为「部分关闭」并点名多出来的那张卡。方向只会扣住放行、不会造成放行。回滚 = revert 本 PR 的单文件改动。
  • 席位意见:(留空,待席位定稿)
  • 你要做的:确认「宁可扣住放行,也不接受读错后放行」这条取舍;若要改为「读不准就报 UNJUDGED」,那是另一张卡(需要改 H19 的第三态入口,本卡明令不动)。

Acceptance notes

  • noted, not filed: four of the changed texts are narration lines in comments that begin with a decorated Blocked-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 touches directiveValues' 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

…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>
@os-steve os-steve added the skip-changeset PR has no user-facing published change; bypasses the changeset gate label Sep 20, 2026 — with Claude

Copy link
Copy Markdown
Collaborator Author

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: da8c088ab04facf345070f5b3d281563098dc720

① Derived judgments

  • Published accept set / public surface: none moves — one PM patrol reader (scripts/pm/check-half-states.mjs, +101 / −22, net +79 against the ≤ +80 budget): the shared decorated-directive reader directiveValues, blockedByTargets, their docblocks and 16 self-test cases; H4's presence test, H19's UNJUDGED arm and the channels read untouched; SELF_TEST_BATTERY_FLOOR 8 untouched, no new battery. check-governed-merges --pr 19455: NOT governed — ordinary queue landing. Judged correct.
  • The fix, read line by line: (1) directiveValues splits a line at every FURTHER key that follows a separator run (DIRECTIVE_SEPARATOR: decoration, spaces, one of · • | , ; 、), each directive's own opening decoration captured and stripped per directive; a key after a WORD stays prose — the mid-sentence-mention contract pinned on a non-Blocked-by: family too; (2) blockedByTargets takes EVERY ref in a directive's value (BLOCKED_BY_REF, a qualifier must touch its #, no match may start inside a path-like token — a URL fragment names nothing) instead of the leading run. The prose-remainder decision is the card's first arm, argued on the four axes and accepted by the seat: the leading run already indexed a phantom (the possessive on objectui#8093) and ADDED substitution — real blockers invisible, a live block reading as expired; every ref can only ADD a target, and an extra target can only WITHHOLD a discharge (H19 counts it open, names it, says PARTIAL) — this file's own check:react-declaration-parity 是唯一没接进任何 workflow 的源码审计门禁,且无 MANIFEST 时静默 skip 退出 0 —— 它现在永远不可能红 #4690 posture applied to the value; the UNJUDGED arm was correctly rejected as needing a new return shape through the resolver, which the card forbids. One measured correction to the seat's own claim, accepted: objectui#8347's nine directives sit on ONE line in the live body, so the old reading was 1 target (exactly H19's 「1 of 1」), not the 2 the seat read from the card's wrapped quotation. Judged correct.
  • Evidence, seat-run on this head in a detached worktree: --self-test exit 0 — 5160 cases (5144 on origin/main, +16); the seat's own probes on this head: the nine-on-one-line body → 9 targets, the sys_user_preference update reports droppedFields:[id] for a key the caller never sent — every org switch pops a user-facing warning toast #8093 prose remainder → #8090, #7434, #8065, one directive → 1, prose mentioning #9 / #10 outside a directive → 0, a mid-sentence key → not a directive; merge-tree vs origin/main 8e368dc: 0 conflicts; CI on da8c088: 35 check runs, 4 still running, 0 red. The dev's census is the first act the card asked for, with both controls: 140 open pm:blocked cards on both boards, 116 carrying a directive, 3 one-line multi-directive lines (all objectui#8347), 10 texts on 9 cards whose target set changes, 2 of 140 whose JUDGED set changes (objectui#8347 1 → 9, objectui#9273 0 → 1), H14 reverse index +13 edges (8 name closed targets, 1 already pm:blocking, 3 real blockers of realtime: no WS/SSE transport — service-realtime is in-process only and clients long-poll; big-screen/command-center scenarios need real push #8347, 1 judgement call objectstack#11333), 4 cards whose directives the old reader turned into ZERO targets; the before column measured by running the 57ceb9d reader on the live bodies in the same process. The 42-family derived union 42 / 42 exit 0 with --ran reconciled, check:pm-dispatch-gates exit 0.

② Semver level

None — nothing published; skip-changeset on the PR is the correct declaration. Judged correct.

③ Boundary flags

open_questions empty. All four mechanism assumptions confirmed (the one-value-per-line reader quoted; nothing touched the file since the base; no new battery; NOT governed). Deviations read and accepted (the detached slow gate blocked on by PID; base kept at 57ceb9d with two unrelated commits since — the seat's merge test on today's main is clean; a ## 维护者速读(草稿) on an ungoverned PR because a pinned reading decision is overturned — the seat carries that decision into this record rather than the decision box: it is a patrol-reader trade-off inside the lane's own file, argued and pinned, and the maintainer may veto it on the row it first produces). Two noted, not filed observations with named carriers (narration comment lines opening with a decorated key are read as directives — pre-existing and deliberate; a cross-repo qualifier separated from its # by decoration reads as local — no live specimen).

Implemented-by: claude/issue-19439-blocked-by-reader-every-directive
Reviewed-by: session_017ETYWqMQD4qMtZzAGovWNi

VERDICT: PASS


Generated by Claude Code

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size/m skip-changeset PR has no user-facing published change; bypasses the changeset gate

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant