Skip to content

Commit 23f1de0

Browse files
os-steveclaude
andauthored
fix(scripts/pm): H52 stands down on a seat ANSWER a held card can actually write (#19436)
Fixes #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 #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>
1 parent b71d9e7 commit 23f1de0

1 file changed

Lines changed: 72 additions & 12 deletions

File tree

‎scripts/pm/check-half-states.mjs‎

Lines changed: 72 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -11987,17 +11987,22 @@ export function h50ThreadReadMismatch(issue, commentRows, sinceMs = Date.parse(T
1198711987
// current statement of what is open: a later round that closes a question out
1198811988
// writes `open_questions: []`, and that empty array is the record that stands
1198911989
// this row down. ⚠️ The known and deliberate cost of that choice is stated
11990-
// rather than hidden: a question ANSWERED in the thread — by a ruling comment,
11991-
// a chat reply recorded on the card — with no newer report and no label still
11992-
// reads as open here. That is an over-report, and it is the direction this row
11993-
// chooses on purpose, because the two repairs it asks for are both repairs the
11994-
// board needs anyway: route the question if it is open, and record the answer
11995-
// in a report or on the label if it is not. ⛔ The opposite direction — teach
11996-
// the row to recognise a ruling — was refused: `isTriageRulingComment`'s anchor
11997-
// set is H33's population, the live answer that motivated this row (「Ruling
11998-
// recorded — the three residuals …」) matches none of its five anchors, and
11999-
// widening them would be a change to H33 rather than an addition beside it.
12000-
//
11990+
// rather than hidden: a question ANSWERED in the thread — by a ruling comment, a
11991+
// chat reply recorded on the card — with no newer report and no label still reads
11992+
// as open here. ⛔ Teaching the row to recognise a RULING was refused:
11993+
// `isTriageRulingComment`'s anchor set is H33's population, the live answer that
11994+
// motivated this row (「Ruling recorded — the three residuals …」) matches none of
11995+
// its five anchors, and widening them would be a change to H33 rather than an
11996+
// addition beside it. ⚠️ But that over-report has NO floor on a card no dev is
11997+
// dispatched to (#19160): closing is forbidden
11998+
// (「缺陷卡 ⛔ 不藏进 hold 也不自行关闭」), re-hanging `needs-user-decision` is
11999+
// forbidden by this row's own remedy for a residual, and no newer report can
12000+
// exist — a `pm:on-hold` card is dispatched to nobody. Two live carriers
12001+
// (objectui#8348, objectui#9868) answered on the thread NEWER than the report and
12002+
// re-fired every sweep: the 「无机制可唤醒的卡 ⛔ 不 hold」 shape the charter
12003+
// refuses one layer down, for a CARD. So a FOURTH exit — `H52_ANSWERED_HEADING`
12004+
// on a NON-report comment newer than the report, a shape the answer DECLARES and
12005+
// ⛔ never a ruling: the ruling comment stays a FIRING case, pinned.
1200112006
// ## Population — measured, not assumed
1200212007
//
1200312008
// The first census is what set it. The row was first written over open
@@ -12229,6 +12234,42 @@ export function latestDevReport(commentRows) {
1222912234
return { row, ...devReportOpenQuestions(row?.body) };
1223012235
}
1223112236

12237+
/**
12238+
* A seat's ANSWER to the report's questions, as a HEADING of its comment.
12239+
*
12240+
* MEASURED, ⛔ not invented: the only two live answers carry it UNEDITED —
12241+
* objectui#8348 `5737496254` 「### The two open questions, answered」 and
12242+
* objectui#9868 `5736885341` 「## Half-state patrol H52 — the two `open_questions`
12243+
* on this card are ANSWERED …」. ONE heading line naming the array and saying
12244+
* ANSWERED is all they share, and that pair IS the pin. LINE-anchored (`m`) where
12245+
* `OS_DEV_REPORT_MARKER` is body-anchored, because one of the two carries its
12246+
* heading 32 lines into a seat review; the ATX heading is what keeps that safe,
12247+
* so ⛔ a blockquoted `> ###` (a QUOTE of another card's answer) and the same
12248+
* words in prose are both refused. ⛔ No `g` flag.
12249+
*/
12250+
export const H52_ANSWERED_HEADING =
12251+
/^#{1,6}[ \t]+(?=[^\n]*\bopen[\s_`*]*questions?\b)(?=[^\n]*\banswered\b)/im;
12252+
12253+
/**
12254+
* Does the thread carry a seat ANSWER NEWER than its newest `os-dev-report`?
12255+
*
12256+
* A SIBLING of `latestDevReport`, ⛔ never a change to it — that reader is shared
12257+
* with other rows and still returns exactly what it returned. Ordering is CALLED
12258+
* rather than restated: `latestMarkedComment` is the newest-of rule,
12259+
* `releaseAnswersClaim` the "is this the later record" one — and that pair is ALSO
12260+
* what refuses a REPORT, so ⛔ no second guard excludes one. The newest report is
12261+
* BY CONSTRUCTION at least as new as any report carrying the heading, so ordering
12262+
* already answers false; a guard there was written, ABLATED (the suite stayed
12263+
* green without it) and removed as the phantom check it was.
12264+
*/
12265+
export function h52AnswerStandsDown(commentRows) {
12266+
const rows = Array.isArray(commentRows) ? commentRows : [];
12267+
const report = latestMarkedComment(rows, OS_DEV_REPORT_MARKER);
12268+
const answer = latestMarkedComment(rows, H52_ANSWERED_HEADING);
12269+
if (!report || !answer) return false;
12270+
return releaseAnswersClaim(answer, report);
12271+
}
12272+
1223212273
/**
1223312274
* Which cards this row can speak about AT ALL — exported for the
1223412275
* counting-policy reason every such predicate is: the predicate that decides
@@ -12266,6 +12307,9 @@ export function h52OpenQuestionsUnrouted(issue, commentRows) {
1226612307
if (!report.parsed) return null;
1226712308
const questions = report.questions.filter((q) => q !== null);
1226812309
if (report.questions.length === 0) return null;
12310+
// The FOURTH exit (#19160) — a seat ANSWER newer than that report, the only
12311+
// clearing act a card no dev is dispatched to can write.
12312+
if (h52AnswerStandsDown(commentRows)) return null;
1226912313
const reportId = commentIdText(report.row?.id);
1227012314
const named = reportId ? `comment ${reportId}` : 'a comment carrying no readable id';
1227112315
const stamped = report.row?.created_at ?? 'unstamped';
@@ -12291,7 +12335,9 @@ export function h52OpenQuestionsUnrouted(issue, commentRows) {
1229112335
're-flagged card leaves the inbox unable to say WHICH question is open and invites a reader to re-present ' +
1229212336
'a ruling that has already been executed, and because this card\'s close would take the label, and the ' +
1229312337
'question\'s only visibility, with it. If the question has already been ANSWERED, say so in the next ' +
12294-
'report, whose empty `open_questions` is the record that stands this row down. ⛔ Nothing here changes ' +
12338+
'report, whose empty `open_questions` is the record that stands this row down — or, on a card no dev is ' +
12339+
'dispatched to and which therefore can never get one, answer them on the thread in a comment NEWER than ' +
12340+
'that report, under a HEADING that names `open_questions` and says ANSWERED. ⛔ Nothing here changes ' +
1229512341
'the report contract: the array is the right place for the question. Report-only patrol INPUT: nothing is ' +
1229612342
'blocked and no label is written.'
1229712343
);
@@ -33568,6 +33614,20 @@ Doubles as the fire's **write self-check** (step 0). \`201\` is not the reading.
3356833614
t('H52 order: ⛔ a ruling comment does NOT stand the row down — the documented over-report', typeof h52([cm52(1, report52(Q52), T52), cm52(2, 'Ruling recorded — A, A, A.', '2026-09-08T09:00:00Z')]), 'string');
3356933615
t('H52 order: …and H33 cannot see that ruling either, which is why it was not reused', isTriageRulingComment('Ruling recorded — the three residuals: A, A, A'), false);
3357033616

33617+
// The FOURTH exit (#19160) — both live headings are fixtures VERBATIM: measured, ⛔ not invented.
33618+
const A8348 = '### The two open questions, answered';
33619+
const A9868 = '## Half-state patrol H52 — the two `open_questions` on this card are ANSWERED, and this comment is the record that stands the row down';
33620+
const answered52 = (answer, at = '2026-09-08T09:00:00Z') => [cm52(1, report52(Q52_TWO), T52), cm52(2, answer + '\n\nQ1 and Q2 taken by name.', at)];
33621+
t('H52 answer: objectui#8348\'s live heading, verbatim, stands the row down', h52(answered52(A8348)), null);
33622+
t('H52 answer: …and objectui#9868\'s, verbatim — both carry the shape UNEDITED', h52(answered52(A9868)), null);
33623+
t('H52 answer: ⛔ an answer OLDER than the report does not clear — the report re-raised them', typeof h52(answered52(A8348, '2026-09-06T09:00:00Z')), 'string');
33624+
t('H52 answer: ⛔ nor the same words in PROSE, with no heading', typeof h52(answered52('The two open questions are answered above.')), 'string');
33625+
t('H52 answer: ⛔ nor a QUOTE of another card\'s answer', typeof h52(answered52('> ' + A8348)), 'string');
33626+
t('H52 answer: ⛔ a REPORT carrying the heading never clears — ordering refuses it, its own array governs', typeof h52([cm52(1, report52(Q52_TWO), T52), cm52(2, 'os-dev-report\n\n' + A8348 + '\n\n```json\n{"open_questions":[{"question":"q"}]}\n```', '2026-09-08T09:00:00Z')]), 'string');
33627+
t('H52 answer: an answer with NO report is not a clearing, and ⛔ the marker carries no `g` flag', h52AnswerStandsDown([cm52(1, A8348, T52)]) === false && H52_ANSWERED_HEADING.global === false, true);
33628+
t('H52 answer: a SIBLING reader — `latestDevReport` still names the report row', latestDevReport(answered52(A8348)).row.id, 1);
33629+
t('H52 sentence: the remedy names the thread route beside the report one', h52row(OPEN52).includes('under a HEADING that names `open_questions` and says ANSWERED'), true);
33630+
3357133631
// Population — every OPEN card, and ⛔ deliberately not one `pm:*` state.
3357233632
t('H52 population: an open `pm:dispatched` card is in', h52SpeaksAbout(DISPATCHED52), true);
3357333633
t('H52 population: a `pm:queue` card is IN — the census\'s LARGEST bucket at 19 carriers', h52SpeaksAbout(card52(['pm:queue'])), true);

0 commit comments

Comments
 (0)