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 |
…ually write (objectstack-ai#19436) Fixes objectstack-ai#19160 Clause-②: no ## The defect H52 stood down on exactly three acts: close the card, hang `needs-user-decision`, or post a newer `os-dev-report` whose `open_questions` is empty. A card no dev is dispatched to can perform **none of the three** — closing a defect card is forbidden (「缺陷卡 ⛔ 不藏进 hold 也不自行关闭」), the row's own remedy forbids re-hanging `needs-user-decision` for a residual raised while executing a ruling, and no newer report can exist because a `pm:on-hold` card is dispatched to nobody. The one act a seat CAN perform — answering the questions on the thread — was invisible, because `latestDevReport` reads only report comments. That is the 「无机制可唤醒的卡 ⛔ 不 hold」 shape the charter refuses for a CARD, one layer up: a patrol row with no act that can ever clear it. ## The answer shape — MEASURED from the two live answers, ⛔ not invented Both live carriers already carry it, **unedited**. The pin is one ATX heading line that names the array and says ANSWERED: | card | answering comment | the heading line, verbatim | |:--|:--|:--| | objectui#8348 | `5737496254` | `### The two open questions, answered` | | objectui#9868 | `5736885341` | ``## Half-state patrol H52 — the two `open_questions` on this card are ANSWERED, and this comment is the record that stands the row down`` | ```js export const H52_ANSWERED_HEADING = /^#{1,6}[ \t]+(?=[^\n]*\bopen[\s_`*]*questions?\b)(?=[^\n]*\banswered\b)/im; ``` LINE-anchored (`m`) where `OS_DEV_REPORT_MARKER` is body-anchored, because objectui#8348 carries its heading 32 lines into a seat review, so a body anchor would have read only one of the two. The ATX heading is what keeps that safe: the same words in **prose** are refused, and so is a **blockquoted** heading (a QUOTE of another card's answer). Both refusals are pinned, and both are ablated below. ## Before / after, measured on the two REAL threads Not a fixture — the live comment rows, fetched and handed to both versions of the predicate: | card | pre-fix (`488f4f5`) | post-fix (`a978f79`) | |:--|:--|:--| | objectui#8348 (`pm:on-hold`, 14 comments) | H52 fires | **clears** | | objectui#9868 (`pm:on-hold`, 7 comments) | H52 fires | **clears** | Neither card needs an edit: the answers they already carry are the record. Population unchanged, the `needs-user-decision` refusal unchanged, `latestDevReport` unchanged. ## The four axes, per design choice **1. A fourth null-exit, ⛔ not a state-based exclusion.** *Business need*: the two live instances are correctly held and correctly answered — what is missing is a reader, exactly as the row's own filing said of `open_questions` itself. *Long-term*: excluding `pm:on-hold` would delete the row's whole population argument (the census measured 14 on-hold carriers and 52 of 63 carriers outside `pm:dispatched`) and would silence a card whose question really is open. *AI-error*: an exit keyed on a DECLARED act keeps 「declared = enforced」; a state exclusion makes silence depend on a label nobody wrote for this purpose. *Startup focus*: no new surface — one reader beside an existing one. **2. The shape is measured from the two live answers, ⛔ not a fourth prose rule.** *Business need*: both answers satisfy it with zero edits, so the fleet pays nothing and the shape is already in use. *Long-term*: a rule nobody has written yet would have no corpus behind it and would drift. *AI-error*: the ATX heading is a loud, structural declaration — the seat SAYS the questions are answered; prose that merely reads like an answer is refused, which is the strict-contract direction rather than lenient matching. *Startup focus*: one regex, no new protocol. **3. Ordering by the same rule the family already uses.** *Business need*: a seat answer OLDER than the report does not clear — the report re-raised the questions after it. *Long-term*: `latestMarkedComment` (newest-of) and `releaseAnswersClaim` (is this the later record) are CALLED, not restated, so "which of two is newer" stays answered in one place for every marker. *AI-error*: a second hand-rolled comparator is where the next tie-break bug lives. *Startup focus*: zero new machinery. **4. A SIBLING reader, ⛔ never a change to `latestDevReport`.** That reader is shared by other rows and returns exactly what it returned; the new exit is additive. **5. ⭐ A guard I wrote, ablated, and REMOVED as the phantom it was.** The first draft excluded a report from being the answer with an explicit test. Ablating it left the suite green: the newest `os-dev-report` is BY CONSTRUCTION at least as new as any report carrying the heading, so the ordering rule already answers false and the guard could never change an answer. *AI-error axis, decisively*: a guard that can never fire teaches the next reader that the exclusion is enforced there, and the pin behind it was vacuous. Removed, with the proof recorded in the docblock; the pin now rides the ordering rule and the ordering ablation reds it. ## Ablation — every behavioural pin proved non-vacuous Each leg via `scripts/ablation-replace.mjs` (anchor must HIT; on-disk blob before/after; restore verified `blob == HEAD` and `git diff HEAD` empty). | leg | mutation | result | |:--|:--|:--| | 1 | delete the fourth exit from the predicate | RED — both live-heading pins fail | | 2 | delete the report-exclusion guard | GREEN ⇒ phantom; guard removed from the diff | | 3 | `releaseAnswersClaim(answer, report)` to `true` | RED — the OLDER pin and the REPORT pin fail | | 4 | drop the ATX anchor from the shape | RED — the PROSE pin and the QUOTE pin fail | Direction observed: **turned red**, on the legs that own each pin — leg 2 is the one that came back green, and that green is what removed a line of code rather than weakening a gate. ## Verification `node scripts/pm/check-half-states.mjs --self-test` exit 0 — **5142 cases** on the merged tree (5124 at the branch point, +9 mine, +9 from the H59 row merged in). `SELF_TEST_BATTERY_FLOOR` untouched at 8; all eight batteries unchanged. The derived union — `node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack` at `a978f79`, no paths — listed 41 commands. Every one was run with its exit captured before any pipe; the union run and this reading are taken at that head. ## Acceptance notes - Net `+60` lines in `scripts/pm/check-half-states.mjs` (72 added / 12 deleted vs `origin/main`), inside the dispatch's `+60` budget, self-test cases included. - `skip-changeset`: `scripts/pm/**` publishes nothing from any released package — the fast-track path, no measurement owed. - `origin/main` was merged once, after the H59 row (PR objectstack-ai#19427) landed on it; the two H-row regions are disjoint and the merge auto-resolved with no conflict. - noted, not filed: `markerMatches` undecorates backticks and asterisks but not a leading blockquote marker, which is exactly why the QUOTE refusal holds. That is load-bearing for this row and is now pinned here; 承接者: the next author of an undecoration spelling, who will find the pin. --- _Generated by [Claude Code](https://claude.ai/code/session_017ETYWqMQD4qMtZzAGovWNi)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
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