fix(scripts/pm): H52 stands down on a seat ANSWER a held card can actually write - #19436
Conversation
…form H52 stood down on exactly three acts: close the card, hang `needs-user-decision`, or post a newer `os-dev-report` with an empty `open_questions`. A card no dev is dispatched to can perform none of the three, so the row re-fires forever on a correctly held card. Two live carriers on the objectui board each answered the questions on the thread, NEWER than the report, and re-fired on every sweep. Add a fourth exit beside the other three: `H52_ANSWERED_HEADING` on a NON-report comment newer than the latest report. The shape is MEASURED from those two live answers, which carry it unedited — one ATX heading line naming `open_questions` and saying ANSWERED. `latestDevReport` is untouched; the new reader is a SIBLING that calls the same `latestMarkedComment` / `releaseAnswersClaim` rules for ordering, so a seat answer OLDER than the report still fires and a report carrying the heading still governs by its own array. Claude-Session: https://claude.ai/code/session_017ETYWqMQD4qMtZzAGovWNi Co-authored-by: Claude <noreply@anthropic.com>
…ntom it was Ablating the guard left the suite green: the newest `os-dev-report` is BY CONSTRUCTION at least as new as any report carrying the answered heading, so `releaseAnswersClaim` already answers false and the guard could never change an answer. Removed, with the proof recorded in the docblock; the pin that asserts a report never clears now rides the ordering rule, which the ordering ablation reds. Claude-Session: https://claude.ai/code/session_017ETYWqMQD4qMtZzAGovWNi Co-authored-by: Claude <noreply@anthropic.com>
…anch Region-disjoint on scripts/pm/check-half-states.mjs — H59 :14208–:14500, H52 :11906–:12260 plus the H52 self-test band. 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 #19160
Clause-②: no
The defect
H52 stood down on exactly three acts: close the card, hang
needs-user-decision, or post a neweros-dev-reportwhoseopen_questionsis 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-hangingneeds-user-decisionfor a residual raised while executing a ruling, and no newer report can exist because apm:on-holdcard is dispatched to nobody. The one act a seat CAN perform — answering the questions on the thread — was invisible, becauselatestDevReportreads 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:
5737496254### The two open questions, answered5736885341## Half-state patrol H52 — the two `open_questions` on this card are ANSWERED, and this comment is the record that stands the row downLINE-anchored (
m) whereOS_DEV_REPORT_MARKERis 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:
488f4f5)a978f79)pm:on-hold, 14 comments)pm:on-hold, 7 comments)Neither card needs an edit: the answers they already carry are the record. Population unchanged, the
needs-user-decisionrefusal unchanged,latestDevReportunchanged.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_questionsitself. Long-term: excludingpm:on-holdwould delete the row's whole population argument (the census measured 14 on-hold carriers and 52 of 63 carriers outsidepm: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) andreleaseAnswersClaim(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-reportis 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 verifiedblob == HEADandgit diff HEADempty).releaseAnswersClaim(answer, report)totrueDirection 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-testexit 0 — 5142 cases on the merged tree (5124 at the branch point, +9 mine, +9 from the H59 row merged in).SELF_TEST_BATTERY_FLOORuntouched at 8; all eight batteries unchanged.The derived union —
node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstackata978f79, 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
+60lines inscripts/pm/check-half-states.mjs(72 added / 12 deleted vsorigin/main), inside the dispatch's+60budget, self-test cases included.skip-changeset:scripts/pm/**publishes nothing from any released package — the fast-track path, no measurement owed.origin/mainwas merged once, after the H59 row (PR fix(scripts/pm): H59 readsstate_reasonso a deliberately REOPENED card is not called a platform failure #19427) landed on it; the two H-row regions are disjoint and the merge auto-resolved with no conflict.markerMatchesundecorates 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