Uh oh!
There was an error while loading. Please reload this page.
fix(pm): three machine-judgeable half-state rows — pm-state ONE-OF, pm:queue rot, contract-review carrier pair (H29/H30/H31) - #11884
Merged
Conversation
…ier pair machine-judgeable (H29/H30/H31) Part of the half-state patrol's rule families: the general pm-state exclusivity row, the pm:queue staleness row, and the dual-carrier contract-review gate comparison. Report-only, no label ever written from the script. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RMTpSRF5CjMmQBFfPtPCwJ
This was referenced Aug 24, 2026
hotlong
marked this pull request as ready for review
August 25, 2026 00:36
Uh oh!
There was an error while loading. Please reload this page.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for freeto join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes#11179
Three new report-only rows on the half-state patrol — the pm-state ONE-OF
invariant,
pm:queuerot, and the contract-review gate's two carriers — plus ameasured verdict on the two families this card proposed that turn out not to
need code. One file,
scripts/pm/check-half-states.mjs.Inventory first: what was already there
Re-verified against
origin/main(4019e16c) before anything was written, asthe re-dispatch ordered:
pm:queuepm:queue+pm:dispatchedpm:awaiting-maintainercarrierpm:queuerotAC1 — H29: the pm state labels are ONE-OF, generally
H3 and H25 are both this invariant, each pinned to the one carrier that was
measured drifting. Between them the vocabulary has six members and fifteen
pairs, and eleven pairs had no reader at all — including the two this card
was filed on (
pm:queue×needs-user-decision, where the queue label's owndefinition is 「无可问之事」; and
pm:queue×pm:blocked, the dual-hang thatsat three days). H29 asks the invariant itself.
Acceptance: a card carrying two pm state labels produces exactly one row,
naming both claims rather than both labels — and the pairs another row already
owns are reported by that row and not here. H3's pair and every pair containing
pm:awaiting-maintainerare excluded, each pinned in both directions(silent here, and the owning row does fire on the same card). A card carrying
three states still reports the pairs nobody else owns rather than going quiet
because one of them fired.
Live specimen on today's board, pinned as a fixture: #11534 carries
needs-user-decision+pm:blocked— a third pair, in a third direction, whichis the argument for asking the invariant instead of pinning pairs one at a time.
The vocabulary is derived from H25's list rather than re-typed
(
PM_EXCLUSIVE_STATE_LABELS = [AWAITING_MAINTAINER_LABEL, ...AWAITING_MAINTAINER_EXCLUSIVE_LABELS]),so the two rows cannot disagree about what a pm state is. It is the third
pm:*label set in this file and it is deliberately none of the other two —H13's visibility set carries
finding/pm:epic/pm:seat, H22's residue setcarries
pm:blockingand dropsneeds-user-decision. All three pairings arepinned in the self-test, the same guard H22 already keeps against H13's set.
AC2 — H30: a
pm:queuecard rotting unclaimedAcceptance: an open
pm:queuecard with no activity past the horizonproduces one report-only row that names the age, names the threshold, and asks
for one explicit transition — never a grade, never a label from the script.
An unreadable
updated_atflags rather than reading as fresh (#4690).The horizon is 3 days, and the argument is a measurement, not a precedent
copy. The aging shape is H11's —
updated_at, report-only, threshold namedin the row. The number is not, and reusing H11's 7 days would have been the
mistake: 7d is calibrated for a parked state, where waiting is legal.
pm:queueis the one active state, where waiting is the defect. Measuredover all 40 open
pm:queuecards on 2026-08-24:At 7d the row cannot fire on today's board at all — a check that cannot fail is
the shape this file exists to catch, not to add. At 1d it reports 43% of the
queue, which is queue depth. 3d clears the ordinary depth, still exceeds the
measured three-day dual-hang this card was filed on, and names 8 of 40 — a
minority a human can walk. Five live rows are pinned as fixtures (three fire,
two stay clean), so the calibration cannot rot silently.
Adjacency pinned: H24 is silent (no assignee), H18 is silent (no
pm:retriage),H11 is silent (
pm:queueis not a parked state) — which is why this needed arow rather than a widening.
AC3 — H31: the contract-review gate carried on ONE of its two carriers
needs:contract-reviewis a dual carrier (maintainer 2026-08-22,「两边都挂好」): hung in one stroke, cleared in one stroke, each carrier written
read-modify-write with a read-back. Two writes, one postcondition, and
nothing ever checked the pair. The measured miss: a PASS verdict was posted, the
PR carrier was cleared, the card carrier was not.
Acceptance: when a delivering open PR exists, the card's carrier and the
PR's carrier are compared and any disagreement produces one row naming the PR.
Both directions are covered, and the sentence says which one it is — the
card-side hang (a gate outliving a passed review) and, the dangerous one, a gate
present on the PR and absent on the card, because 「闸门被剥不是红灯是放行」: a
stripped gate is a green light and 「被剥」 and 「从未挂过」 are
indistinguishable in the evidence. A row comparing carriers is the only reader
that can tell them apart.
Live specimen, pinned: card #11427 carries the gate; its delivering draft PR
#11844 carries
documentation, size/l, dependencies, tests, toolingand notthe gate.
Deliberately NOT landed — the companion "premature hang" rule. The
re-dispatch's inventory mentioned flagging a gated card with no PR carrier. That
shape is the protocol's own prescribed sequence:
references/contract-review.mdsays 「PR 一存在即挂,报告先于 PR 到达则先挂卡侧、ACCEPT 时补齐 PR 侧」. Reporting it would report correct behaviour as a defect.
The other live carrier, #10025 (gated,
pm:blocked, no open PR), is pinnedas a clean case for exactly that reason.
Cost, declared: zero requests. The open-PR listing is already in hand
(H7/H12/H21 list it; H8 already passes it around for the same question), and the
delivery relation is
prDeliversCard— H8's, shared rather than re-derived, sothe two rows can never disagree about which PR delivers which card. Merged PRs
are out of scope by decision: the gate governs the open PR, and a merged carrier
is a closed-out stroke rather than a live half-write. A PR row whose
labelsisnot an array is excluded from the comparison rather than counted as bare —
reading an unreadable carrier as an absent one would manufacture a gate finding
out of a read failure.
AC4 — H14's strip-when-empty: already shipped, recorded not re-landed
The proposal was "recompute the dependent set each patrol and STRIP when it is
empty or every named dependent is closed/404". H14's stale direction is
exactly that and has been on main since before this card:
carries && dependents.length === 0fires, and becausebuildBlockingIndexisbuilt from the open listing, a dependent that has been closed or 404s
contributes no edge and lands in that branch by construction. The shipped row is
also better calibrated than the proposal in two ways the proposal did not
have: it is suspended when the index is known incomplete (unreadable evidence is
not absent evidence), and it states its repo-local boundary instead of claiming
exhaustiveness — cross-repo
Blocked-by:edges are protocol-legal. Re-landingit would have been a duplicate row on one fact. No code; recorded here.
AC5 — the freshness warning: the premise does not hold on this transport
The card proposes: 「结论型评论时间戳早于卡正文最后编辑时间戳」 → warn. The
fact is judgeable only if both stamps are readable, and one of them is not.
This file already carries the measurement, in the
Blocked-by:channel's"Stated boundary" note: an issue's body-edit time is on no REST payload at
all (the issues API carries
created_at/updated_atonly; body edit historylives behind GraphQL
userContentEdits; the timeline endpoint does not recordbody edits either), and this file is REST-only by a standing operational note.
The one proxy in reach,
updated_at, is worse than nothing — it bumps when acomment is posted, so "newer than the body's last edit" is false for every
comment ever written and the check would read nothing while looking like it read.
So the honest options are all decisions, not implementations: move this file off
REST for one row, accept a proxy the file already ruled worse than nothing, or
record the measurement and leave the freshness duty with the seat protocol. No
code was written on a guess — the open question goes back with the report. A
predicate that cannot fail is the shape this file exists to catch.
references/state-machine.md: zero lines, arguedNo doc row is added, and this is a deliberate call rather than an omission. The
file sits at its ratchet ceiling (43, headroom 0), so a row must be paid for by
consolidation — and every line still available for consolidation is inside the
in-flight hunks of PR #11874, which is ACCEPTed and awaiting human merge and
which pays for its own two rows out of the same three stale status notes. Paying
twice from one purse is a guaranteed textual conflict with a PR this card was
told to compose cleanly with. The general ONE-OF rule is already stated in that
file for the
pm:awaiting-maintainercarrier, and that section namescheck-half-states.mjsas the authority for enforcement detail — which is wherethis diff puts it. The header catalogue inside the script is skipped for the
same reason (its H28 entry occupies the identical anchor), following the
existing precedent that H21, H22 and H23 have no catalogue entry either; every
new row carries its full rationale at the predicate site, as H24–H27 do.
Reverse verification
Self-test 1058 → 1149 cases (+91). Three ablations, each anchored on the
exact text intended to change, each in a script carrying an
EXIT INT TERMtrap that restores the file, each with the mutation proven ondisk by counting the injected and the deleted text separately, and each
restore verified byte-identical with
cmpagainst the pre-mutation copy. Nobuild/dist leg applies — this is a plain
.mjsthe self-test imports fromsource.
if (pairs.length === 0)→if (true))15 of 1149 case(s) failed=== 0→if (false))barePrs→[])11 of 1149 case(s) failedAblation 2's green is a real measurement, not a failed run. Removing the
early return does not change any verdict, because the two reporting branches
both require a non-empty PR set: the card-side-first silence is
over-determined. The line stays — it states the protocol rule at the point a
future third branch would otherwise silently violate it — but a reviewer should
know it is documentary rather than load-bearing today, which is why ablation 3
was run against a predicate that is.
A note on the assertion shape. The observation recorded on PR #11874 —
predicate(...).includes(...)turns a null-return regression into aTypeErrorcrash instead of a named case — is honoured for the new blocks: every message
assertion here goes through a helper that checks
typeoffirst and returns adescribing string. It shows in the ablation output, which reads
got "NO MESSAGE (null)", want trueon 20 of the 26 failing rows rather thandying at the first one. The existing H26 block is not restructured — that is
a diff of its own, and this card had to merge cleanly onto PR #11874.
One honest correction to my own proof. Ablation 2's first on-disk check
counted
if (delivering.length === 0) return null;and reported 1 after asuccessful mutation — because H8 carries the identical line. The count was not
anchored tightly enough to be evidence; what actually proved uniqueness was the
mutation script's own
assert count == 1against the full line including itstrailing comment. Ablation 3's proof is anchored on text unique to the target.
Gates
Derived at
b83214bfwithnode scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack(no pathargs — the script took its own change set from the merge base). It printed
"--repo 'objectstack-ai/objectstack' checked against this checkout's 'origin'
remote — it holds" and "change set derived from git — 1 path(s) vs merge base
4019e16", matching 11 families. Exit codes captured before any pipe
(redirect-then-capture). Every line below is the gate's own verdict:
✓ check-half-states self-test: 1149 cases pass.(was 1058)✓ check-agent-test-spelling: 0 violations — 357 file(s) · 3459 bare -- token(s) · 1109 launcher-rooted run(s) · 8 separator(s) JUDGEDOK: 16 package(s) read outside themselves, all declared, and turbo.json hashes every declared glob.(cross-package-test-inputs, both invocations)✓ check:entry-guard: 148 scripts/ file(s) — every entry guard goes through invoked-as.mjs; 105 export bindings, 95 of them inert on import✓ check:parse-guard: 147 scripts/ file(s) — every TypeScript parse goes through ts-parse.mjs.✓ check-partof-closing-keyword self-test: 28 cases pass.check-closing-keyword-parity: OK (3 parsers agree on all 9 keywords and both measured separators; sweep found 5 file(s) carrying the grammar across 6619 tracked file(s), all registered).✓ check:pnpm-filter-targets: 135/168 --filter occurrence(s) across 26 file(s) resolve against 78 workspace package(s)check-nul-bytes: OK (scanned 6614 text file(s) -- 6614 tracked, 0 untracked-not-ignored; skipped 5 binary; no raw ASCII control bytes).— run on the standing clause, not because the derivation named itThe 11th matched row is
node scripts/pm/check-half-states.mjsunderhalf-state-patrol.yml— the live network sweep, not a local gate; itsoffline half is
check:pm-half-statesabove. Invokingscripts/check-partof-closing-keyword.mjsdirectly first reportedNOT WIRED — neither PR_BODY nor PR_NUMBER is set … This is a wiring or usage failure, NOT a verdict; it is green when handed this body, and its self-testform is green above.
skip-changesetapplies — the diff isscripts/pm/**only, so it publishesnothing.
Notes for the reviewer
armed.
this branch is cut from
origin/mainand every insertion point here was chosento avoid its hunks: it inserts after
needsBlockerLivenessand afterh27DeadClaimNoProgress, at the H4 fetch in the label loop, at the H26resolutions loop, and at the end of the H26 self-test section; this one inserts
after
h25AwaitingMaintainerExclusivity, after the H25 call in the label loop,in the H13/H18 unscoped loop, and before the window-arithmetic self-test
section. No shared line is touched, and
references/state-machine.mdis nottouched at all. It takes the next number after its H28.
Measured, not asserted:
git merge-treeof this branch againstorigin/claude/issue-11747-unlock-scan-overhaulreports 0 conflictmarkers, and against the current
origin/main(e75e3438, three commitspast the derivation base, none touching this file) likewise 0.
pm:blockedcards: 32 parkBlocked-by:outside the body (invisible to the unlock scan), 16 wait on already-closed upstreams, 7 name no blocker at all #11747 and fix(pm): ungate the blocker-liveness comment read and pair it with a stale-body-line row (H28) #11874 (the siblingcard and its PR), [Decision] Does
react-blocks.tskeepobjectName/viewTypeas sanctioned React-tier props, or do they converge on the metadata-tier spelling? — the carrier objectui#2890's last phase has never had #11284 (the class-3 measurement this card cites), Whole-text restructuring round of the pm-dispatch protocol face — maintainer-ordered, fires when the skills-lane backlog drains #11086(the restructuring round). All remain open as far as this PR is concerned.
Generated by Claude Code