Skip to content

feat(pm): two report-only patrol predicates for the pm:blocking machinery — cache coherence both directions, and oldest-unclaimed age - #9945

Merged
os-warren merged 1 commit into
mainfrom
claude/issue-9935-blocking-patrol-predicates
Aug 19, 2026
Merged

feat(pm): two report-only patrol predicates for the pm:blocking machinery — cache coherence both directions, and oldest-unclaimed age#9945
os-warren merged 1 commit into
mainfrom
claude/issue-9935-blocking-patrol-predicates

Conversation

@os-warren

Copy link
Copy Markdown
Collaborator

Fixes#9935

Two report-only predicates on scripts/pm/check-half-states.mjs, surfaced as anchor rows through the patrol that already runs this script. No workflow change; nothing reddens; the script never writes a label.

H14 — pm:blocking cache coherence, both directions

pm:blocking is not a state a seat sets. The state model makes it a cache the triage sweep derives from the Blocked-by: reverse index (⛔ 不手工挂), and the lane selection order ranks it second only to priority:p0. A cache with a ranking consumer and no coherence check drifts two different ways, and each lies differently:

  • stale — the label carried while no open card's Blocked-by: body line targets it. Worse than an absent label: it boosts a card nothing depends on, with authority.
  • missing — an open card targeted by at least one open card's Blocked-by: line and not carrying the label. A real unblocker the selection order cannot see.

Report-only in the strong sense: the producer is the triage sweep's derivation pass, so the row's prescribed remedy is always a derivation that runs, never a label written from this script.

H15 — oldest unclaimed pm:blocking, with its age

One visibility row naming the oldest open, unassignedpm:blocking card. It carries no threshold constant, deliberately, and the rationale comment says why: every other aged item here answers "has this been abandoned?", where a threshold is what turns silence into an alarm. This row answers "is the lane taking blocking cards first?", and the honest answer is a number every run. A threshold would re-introduce the judgement call the row exists to hand over, and would go quiet on the days the board is worst behaved but still under it.

The age reported is the card's (created_at — the same quantity the selection order's own within-rank tie-break reads), and the row says so rather than leaving the reader to guess: the label's own age would need a per-card timeline fetch this sweep deliberately never makes.

Measured on the live board, 2026-08-19

234 open cards, 17 Blocked-by: body lines, 16 index entries. The shipped functions, driven over the real census:

H14 #9919 [MISSING] targeted by #9249
H14 #9832 [MISSING] targeted by #9650
H14 #9689 [MISSING] targeted by #9784
H14 #9652 [MISSING] targeted by #9482
H14 #9255 [MISSING] targeted by #9592
H14 #7276 [STALE] carries pm:blocking, nothing targets it
H14 totals: 1 stale, 5 missing
H15: no row (the board's only pm:blocking card is assigned)

Not one coherent pairing existed at that reading — the cache had no reader checking it and had drifted to 0% agreement with the index it is derived from. That is the finding the predicate was specified to make visible, and it was true the moment it could first be measured.

Reverse verification — direction predicted before running, all five held

casepredictedobserved
#7276 — carries the label, live index has no entryfinding, stale armfinding, stale arm
#9832 — targeted by #9650, no labelfinding, missing arm, naming #9650finding, missing arm, naming #9650
#7917's Blocked-by: objectstack-ai/objectui#4356no local index entry for #4356no entry; local #4356 reads clean
H15 over the live boardnull — the one blocking card is assignednull
same board, that card's assignee removedrow, roughly 220hrow, open ~220h

Both legs were run against the shipped exported functions over the real 234-card census, not only against fixtures, and independently cross-checked by a separate throwaway implementation that agreed on 1 stale / 5 missing. No rebuild leg applies: this script is executed directly from source and has no dist/, so there is no stale artifact for an ablation to read.

One case failed honestly on the first run and is worth recording, because the failure was in the fixture, not the predicate: H15: absent created_at was green against a 3-day-old card, because the fixture helper's created = daysAgo(3) default fills an undefined. It is now built longhand so it tests the absent field, and the comment says why.

Design notes worth a reviewer's eye

  • This is the file's first parser for the Blocked-by: line. H4 only asks "is there such a line" and extracts nothing; no other script in the repo parses these lines at all (the unlock scan is a seat procedure over a grep). So there was no second parser to converge on, and H4 keeps its cheaper question.
  • Code is NOT stripped, unlike H7/H8/H13. Those read prose and must protect the careful author who quotes a spelling in backticks. This one models a machine reader: the unlock scan greps the literal, so a fenced line really does fire the live machinery. Stripping here would report coherence against an index nothing uses. Pinned as a case so the choice reads as deliberate.
  • Only the leading ref run is taken. Real lines carry trailing prose, and prose can name a second card that is context rather than a blocker. Scanning the whole value would manufacture a dependent for it, and the cost would land on a third card as a phantom row against someone who did nothing wrong.
  • Cross-repo refs are dropped, and it is load-bearing rather than tidy: objectstack-ai/objectui#4356 is a real line on this board, and reading its number as local would invent a dependent for whatever this repo's fix(objectql,spec): filter folds to where on EVERY engine method; top/limit joins the #3795 slot table (#4346) #4356 happens to be.
  • H15 attaches its one summary row to the card it names, so it links, sorts and truncates exactly like every other row and neither renderer needs a special case. Both new items are pinned in both media, and both are pinned as not loud, so a P0-SUSPECT row still outranks them at the fold.
  • The self-reference trap is pinned too: this sweeper's own rendered rows land in an issue body and quote the literal, so there is a case proving a rendered finding row does not parse back as a Blocked-by: line.

Gates

Derived with node scripts/pm/dispatch-gates.mjs (no args, real diff), all run at 2906b65b, each quoting the gate's own verdict line:

  • pnpm check:pm-half-states✓ check-half-states self-test: 273 cases pass. (202 on origin/main, +71)
  • pnpm check:partof-closing-keyword✓ check-partof-closing-keyword self-test: 28 cases pass. (it imports h7PartOfWithClosingKeyword from this file, so it is also the import-compatibility witness)
  • pnpm check:cross-package-test-inputsOK: 12 package(s) read outside themselves, all declared, and turbo.json hashes every declared glob.
  • pnpm check:nul-bytescheck-nul-bytes: OK (scanned 6312 text file(s) ... no raw ASCII control bytes).

The sixth family the derivation names, node scripts/pm/check-half-states.mjs from half-state-patrol.yml, is the live sweep and could not be run in this container. Worth recording precisely, because the script's own probe says otherwise: the injected token is the agent proxy's 14-character placeholder, and the proxy answers /rate_limit itself with 200 and a 14871 quota while every real endpoint returns 403 with GitHub access is not enabled for this session. So --probe reports "transport prerequisite met — the token authenticates" and the first listing page then 403s. That is a container class the file's classifier does not cover, filed separately rather than addressed here.

No changeset

scripts/pm/** is internal PM tooling and publishes nothing — consistent with every prior PR touching this file, most recently #9929. The skip-changeset label is the PM's to set.

Generated by Claude Code


Generated by Claude Code

…nery
H14 — `pm:blocking` cache coherence, both directions, off the `Blocked-by:`
reverse index the unscoped pass already has the bodies for. `pm:blocking` is a
triage-sweep-derived cache, never hand-applied, and the lane selection order
ranks it second only to `priority:p0`; a cache with a ranking consumer and no
coherence check drifts two ways. A card carrying the label that nothing targets
is boosted on the authority of a dependency that does not exist; a card that is
targeted and lacks it is a real unblocker the order cannot see.
H15 — one visibility row naming the oldest open, UNASSIGNED `pm:blocking` card
and its age. No threshold: the row reports unconditionally, because a threshold
would re-introduce the judgement call the row exists to hand to the reader.
Both stay report-only, both render as ordinary rows in both media, and the
remedy for either direction is always the triage sweep's derivation pass — this
script never writes a label. Measured on the live board 2026-08-19 (234 open
cards, 17 `Blocked-by:` body lines): 1 stale, 5 missing, 0 coherent pairings.
Self-test 202 -> 273 cases, all green.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AeA3nU1B5Q2pgxqxgUrexd
@github-actions

Copy link
Copy Markdown
Contributor

⛔ merge queue 构建失败 — 先分诊,再决定要不要重排

队列构建 32238338331 红了。队列跑的是全量套件(PR 侧 CI 只跑 affected 子集),
所以失败的测试可能在本 PR 没碰过的包里 —— 那不是重排能修的。每次盲目重排都会让排在后面的所有 PR 重建一轮。

失败的 job(日志抽取,best effort):

  • Console Pin Gate — 失败步骤: Verify pnpm version

    AssertionError [ERR_ASSERTION]: The expression evaluated to a falsy value:
    

历史信号:

  • 本 PR 过去 24h 无队列失败记录(首次)。
  • 过去 24h 队列共有 8 个失败构建(不含本次)。

分诊清单:

  1. 失败测试在本 PR 改动的包里 → 真回归,修 PR。
  2. 失败测试与本 PR 无关 → 在其他 PR 的同类评论里搜同名测试;出现过 ⇒ flaky 实锤,开 issue 修/隔离那条测试。修好前重排只会再烧一轮全队列。
  3. 两者都不是 → 可能与同组 PR 语义冲突;等前面的 PR 落地或失败出队后再重排一次即可,不要连续重排。

Generated by Claude Code · merge-queue-triage workflow (#4859)

Merged via the queue into main with commit 301bf26Aug 19, 2026
25 checks passed
@os-warren
os-warren deleted the claude/issue-9935-blocking-patrol-predicates branch August 19, 2026 09:41
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size/mskip-changesetPR has no user-facing published change; bypasses the changeset gate

Projects

None yet

Development

Successfully merging this pull request may close these issues.

patrol: two report-only predicates for the pm:blocking machinery — cache coherence both directions, and oldest-unclaimed-blocking age

2 participants

@os-warren@claude