fix(scripts/pm): H59 reads state_reason so a deliberately REOPENED card is not called a platform failure - #19427
Conversation
…card is not called a platform failure H59's FALSE OPEN row said 「GitHub did not perform the closure the PR declared」 for every open card a merged PR bound a closing keyword to — including a card that was closed and then REOPENED on purpose. The discriminator is already on the open-listing payload the row holds (`state_reason: 'reopened'`), so the arm costs no request. Keeps the row (a declared closure that is not in force is still worth reading) and changes the sentence for that one case: it names the reopen as the act to read, declares that it buys no timeline and therefore does not place the reopen against the merge, and points the remedy at the reopen's own reason rather than at GitHub. Every other `state_reason`, null included, keeps today's sentence. 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 #19177
Clause-②: no
H59's FALSE OPEN row asserted a cause the payload it already holds contradicts: for a card that
is
opencarryingstate_reason: reopened, it printed the sentence naming a platform failure.The discriminator rides the same open-listing row the sweep already has, so the arm costs zero
requests. Region H59 only;
SELF_TEST_BATTERY_FLOORuntouched (8). Net +29 lines (29 added,0 removed, 1 file) against the PM's ceiling of +30.
Before / after, on the filing card's live shape
Probe: a merged PR binding a closing keyword to a card that is open with
state_reason: reopened.Both quotes are fenced so no keyword in this body binds anything.
Before (identical bytes for
reopenedand fornull— the row is blind to the field):After, for
state_reason: reopenedonly:null,completedandnot_plannedkeep today's sentence, byte for byte.The disposition, on the four axes
The card named two dispositions and picked neither: suppress the row on
reopened, or keepit and change the sentence. This PR keeps it and changes the sentence.
consecutive sweeps and could be cleared by no legal act on the card. The population is live
today: scanning the same
state=openlisting this row already holds, objectstack#17147 andobjectui#7844 / objectui#6596 carry
state_reason: reopenedright now (920 open issues readacross the two boards in this act). So the case is real. And the audit still wants the row: a
merged PR whose body declares a closure that is not in force reads as done to everyone who
finds it, whichever way the card got back to open. Suppression deletes that from the board.
⛔ never call an unjudged thing clean" (its own clause prints "Rows are a LOWER BOUND").
Suppression would add the one silent drop in this row that no counter reports. The defect is a
sentence asserting a cause its data does not establish; the fix belongs at the producer of that
sentence, not in a consumer that learns to ignore the row.
sentence sends it to look for a platform failure that did not happen and hands it a remedy
("close it if the work landed") that is wrong for a deliberate reopen: a seat that follows it
closes a card somebody reopened on purpose. Suppression removes the false instruction but also
removes the true information, and a verifier that silently degrades is worse than no verifier.
The new arm removes the false accusation, declares the limit of what it read, and redirects the
remedy at the reopen's own reason — declared, loud, and correct.
no new constant, no new row, no new battery, no new count key. This is the narrowest arm that
fixes the defect, and it is a correction of an existing sentence rather than an expansion.
No axis conflict: axis 4 mildly prefers the smaller change and the chosen arm is not larger than
suppression on any measure. Suppression is refused on axes 1 and 3.
state_reason: reopenedproves the card was closed and reopened; it does ⛔ not prove WHICH closure, so itcannot rule out "GitHub never fired on an already-reopened card". Placing the reopen against the
merge needs a timeline page, and direction (b) is the half that costs no request — buying one
here would change what this arm is. So the row reports what the payload settles and names what it
did not read, which is the same discipline direction (a) already applies to its band leg.
The fixture extension
The direction (b) fixture
card59Openis a plain object literal, ⛔ not a helper, so the casesbuild the variants with a one-line factory over a spread of it and the literal is left untouched
— every existing (b) assertion still reads the plain arm. Nine cases added beside the existing
ones (⛔ never at
selfTest()'s tail): the platform-failure sentence absent, the field quoted,the closed-and-reopened reading, the no-timeline declaration, both halves of the remedy, the row
still firing, the angle-bracket pin, and one case holding
null/undefined/completed/not_plannedon today's sentence.Verification
node scripts/pm/check-half-states.mjs --self-test— 5118 cases at base a0e62e6, 5127 after,exit 0 both times. Batteries unchanged,
SELF_TEST_BATTERY_FLOORstill 8.Reverse verification (ablation), run from the committed state through
scripts/ablation-replace.mjsso the mutation is proven on disk and the restore is provenagainst HEAD. Predicted direction before running: 转红. Mutation: the new predicate neutered to
if (false && ...).7 of the 9 new cases go red. The 2 survivors are exactly the 2 that are ⛔ not about the reopened
sentence — the angle-bracket pin, and the control case that holds every OTHER
state_reasonontoday's sentence, which reads the plain arm either way. That asymmetry is the evidence the cases
bind to this predicate rather than to the row in general.
Derived gate union —
node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstackat head30caf75, no paths passed (the tool derives its own changeset: 1 path, 29 changed lines, merge base a0e62e6). It printed 41 commands; 40 ran green in the
foreground, each exit captured before any pipe;
pnpm check:pm-dispatch-gatesran detached withits own exit captured. Reconciled with
--ran. The tool's own NOT MEASURED classes (8 familiestaking a value from the workflow, 1 CI-measured-only, 10 pending a changeset that this card does
not write, 46 artifact rosters, 11 wide-population families, 1 path-scheduled CI job) are CI's
and are named as unmeasured here rather than read as clearances.
node scripts/pm/check-governed-merges.mjs --pr N—scripts/pm/**is ⛔ not a governed surface.Acceptance notes
Off-path observations from this region, ⛔ not filed and ⛔ not fixed here:
h59LinkageClause) counts a reopened row as an ordinary declared-closure rowand does not break the two arms out. Nothing it prints is false — it says a declared target
still on the open listing is a FALSE OPEN row, which stays true — so this is an enhancement,
⛔ not one of the three filable classes. It would also need a new
SWEEP_COUNT_KEYSentry andthe clause's own pins, well past this card's ceiling. 承接者: whoever next widens H59's census.
:25702–:25718; at a0e62e6 thosefixtures are at
:25902–:25918, and a fifth spelling of the field lives at:29968in H24'sbattery. All five are self-test fixtures, so the file had no production reader of
state_reasonbefore this PR. Drift in a dispatch pointer, ⛔ not a defect in the file. 承接者: none.
Generated by Claude Code