From 386f61f9dd2f9b6b07ece2fca761c96ea8ad9d61 Mon Sep 17 00:00:00 2001 From: os-justin Date: Thu, 17 Sep 2026 22:39:14 +0000 Subject: [PATCH 1/2] fix(pm): a decorated `Claim:` enters the governing-claim pool, through the sibling's one reading The governing-claim pool and the retraction indexer each tested the imported `CLAIM_COMMENT_MARKER` against the RAW comment body, so a claim written `**Claim:**` or `` `Claim:` `` was never a candidate for governance -- not superseded, not listed, not named. `claimGovernance`, imported from the same sibling, has read both ownership markers through `markerMatches` since #18680, so one function held both answers at once. Both raw tests now read through `markerMatches(CLAIM_COMMENT_MARKER, body)`, imported from `check-half-states.mjs`: the bare marker first (provably additive), then the shared stripper per line with the list item refused. The constant is not widened and no second undecorator is written here. Claude-Session: https://claude.ai/code/session_01Gqi43smmqjJ5sUrhfoPeKu Co-authored-by: Claude --- scripts/pm/check-clause2-carriers.mjs | 93 ++++++++++++++++++++++++--- 1 file changed, 84 insertions(+), 9 deletions(-) diff --git a/scripts/pm/check-clause2-carriers.mjs b/scripts/pm/check-clause2-carriers.mjs index ccd5ad7c9a0..1a7f8bff3aa 100644 --- a/scripts/pm/check-clause2-carriers.mjs +++ b/scripts/pm/check-clause2-carriers.mjs @@ -683,6 +683,7 @@ import { isGateSemanticLabel, labelNames, latestMarkedComment, + markerMatches, prDeliversCard, proxyRearmPlan, resolveSweepRepo, @@ -1756,7 +1757,7 @@ export function claimRetractions(commentRows) { }); const out = new Map(); for (const claim of indexed) { - if (!CLAIM_COMMENT_MARKER.test(String(claim.row?.body ?? ''))) continue; + if (!markerMatches(CLAIM_COMMENT_MARKER, String(claim.row?.body ?? ''))) continue; // Fail closed, twice: an unattributable claim cannot be matched against an // author, and an unattributable candidate cannot be the seat that wrote it. if (claim.author === null) continue; @@ -1791,12 +1792,82 @@ export function claimRetractions(commentRows) { */ export const CLAIM_SELECTION_RULE = 'the GOVERNING claim — the NEWEST comment whose body carries a line beginning `Claim:`/`Claimed:` ' + + '(read through the sibling reader\'s ONE reading, `markerMatches`: the bare marker first, then the ' + + 'shared stripper per line, with a markdown LIST ITEM refused — so a decorated `**Claim:**` is the ' + + 'same record as a bare one and ⛔ the two readers of this thread cannot answer differently) ' + 'AND whose `Branch:` line parses at least one protocol-shaped branch (newest by `created_at`; an ' + 'unreadable stamp or a tie falls back to thread order, later row wins). The pool is every claim ' + 'comment sharing that `created_at`; when NO claim names a branch at all, every claim comment is ' + 'the pool. ⛔ Not earliest, ⛔ not a session match, ⛔ not the one whose body mentions the key. ' + `MEMBERSHIP comes first: ${CLAIM_RETRACTION_RULE}`; +// --------------------------------------------------------------------------- +// #18764 — a DECORATED claim ENTERS the pool. ONE reading, and it is the +// sibling's. +// +// The pool and the retraction indexer each tested `CLAIM_COMMENT_MARKER` +// against the RAW body. The constant anchors the bare word at line start and +// tolerates leading whitespace and one `>` — nothing else — so a claim a seat +// wrote as `**Claim:** …` or `` `Claim:` … `` was not SUPERSEDED here, it was +// never a candidate: not listed, not rejected, not named anywhere in the +// record. Meanwhile `claimGovernance`, imported from the same sibling, had +// already been reading both markers through `markerMatches` since #18680, so +// ONE FUNCTION held both answers at once — governance saw the bolded claim and +// the pool beside it did not. +// +// Measured on the two live records the #18764 escalation named (read +// 2026-09-17T22:02Z; ⚠️ both cards have since left that state and the reading +// is stated with its time for that reason), replayed through `--pair-json`: +// +// a bolded/backticked claim carrying its own `Branch:` and `Clause-②: no` +// → `claim.selected: none — no comment on this thread carries a line +// beginning `Claim:``, the limb read MISPLACED and `--pair` exited 4, +// prescribing a `Clause-②-correction:` for a line the seat had already +// written in the right place. +// a BARE older claim declaring `yes` beneath a DECORATED newer one declaring +// `no` → the OLD claim governed the declaration, the reading answered +// `DECLARED yes`, and the newer record was not even listed as rejected. +// ⭐ That is the expensive direction: not a missing reading but a WRONG +// value, reported with every appearance of having been read. +// +// ⛔ The repair is NOT a `\*\*` added to `CLAIM_COMMENT_MARKER`. Decoration is +// an OPEN set (#18680 settled that), so admitting one spelling buys exactly +// that spelling and replays this card on the next one — and the constant is +// the PROTOCOL's spelling, which is why every reader imports it rather than +// restating it. ⛔ Nor is it a second undecorator written here: this file +// would then own a definition of "decorated" that the sibling could drift +// from, which is the very failure the card names. What both raw tests do +// instead is READ THROUGH `markerMatches` — the sibling's one reading, bare +// test first (so the change is provably additive: no body that matched +// yesterday stops matching) and the shared stripper after it, with the list +// item refused there and the near-miss vocabulary +// (`OWNERSHIP_MARKER_NEAR_MISS_FORMS`) kept where it lives. ⛔ That vocabulary +// is the sibling's and is not re-declared here. +// +// ## The OTHER undecorator in this file stays where it is, and that is MEASURED +// +// `undecorateRetractionLine` looks like the same job one section up, and the +// card asked whether the file should end up with one undecoration path. It +// should not, and the two paths were run over one fixture set to find out: +// 5 of 12 fixtures read DIFFERENTLY. `undecorateRetractionLine` strips `_` and +// then every leading non-letter/non-digit character, so it reads +// `__Claim:__`, `- Claim:`, `* Claim:`, `## Claim:` and `🚨 Claim:` as the +// directive; `markerMatches` reads none of those, deliberately — the list item +// and the heading are NAMED refusals with pins behind them, and the underscore +// form is a named near miss. +// +// ⭐ The divergence is load-bearing in BOTH directions, which is why unifying +// by hand would be a regression whichever way it went. The #18373 retraction +// this file's own battery replays opens `🚨 **撤回上一条认领…`, and the leading +// sigil is exactly what the prose channel must strip before the anchor can +// OPEN the line — narrowing the retraction stripper to the shared one would +// stop reading the measured specimen #18719 landed for. Widening the shared +// one the other way would make `- Claim:` a claim, which H20 pins as not one. +// ⇒ two strippers, two jobs, and the difference is stated here rather than +// discovered again. ⛔ Not unified from here: it is filed, with the fixture +// table, for the seat to route. +// --------------------------------------------------------------------------- + /** * WHICH claim comment this reading is built from, and which it is not — the * input half of the declaration limb (#18456). @@ -1837,7 +1908,7 @@ export function claimCarrierSelection(commentRows) { // ⭐ The full claim listing KEEPS the retracted rows: the record names them // RETRACTED below. A pool that silently shrank would replace one invisible // fact with another. - const claims = commentRows.filter((row) => CLAIM_COMMENT_MARKER.test(String(row?.body ?? ''))); + const claims = commentRows.filter((row) => markerMatches(CLAIM_COMMENT_MARKER, String(row?.body ?? ''))); const live = claims.filter((row) => !retracted.has(row)); const governing = governance.governing; const matched = governing ? live.filter((row) => (row?.created_at ?? null) === governing.createdAt) : []; @@ -1898,13 +1969,17 @@ const CLAUSE2_CORRECTION_KEY_TEXT = 'Clause-②-correction'; * because the older claim happened to agree. * * The predicate that separates them is `CLAIM_COMMENT_MARKER`, imported rather - * than restated: a claim comment is one whose body carries a LINE BEGINNING - * `Claim:` (or `Claimed:`, optionally blockquoted), and that one spelling is - * the whole set — so a heading-style claim (`## Claim — …`) is not a claim - * comment here, however complete the reasoning under it, and its thread reads - * `absent`. ⛔ Widening the predicate is not this file's to do: it is the - * sibling's constant precisely so the two readers cannot drift, and the remedy - * for a thread that reads `absent` is a comment in the fixed spelling. + * than restated, and OFFERED through `markerMatches` — the sibling's one + * reading (#18764), imported for the same reason the constant is. A claim + * comment is one whose body carries a LINE BEGINNING `Claim:` (or `Claimed:`, + * optionally blockquoted), read bare first and then with the shared + * decoration stripped, so `**Claim:**` and `` `Claim:` `` are that same line + * and ⛔ not a second spelling this file admits on its own. A heading-style + * claim (`## Claim — …`) is still not a claim comment here, however complete + * the reasoning under it, and its thread reads `absent`. ⛔ Widening the + * predicate is not this file's to do: the constant AND the reading are the + * sibling's precisely so the two readers cannot drift, and the remedy for a + * thread that reads `absent` is a comment in the fixed spelling. * * @param {{ body?: string, created_at?: string }[]|null} commentRows — the REST * comment rows, or `null` when the thread could NOT be read. From 776c18771315cbd3a2622bcbdc9ba664a7d7bf67 Mon Sep 17 00:00:00 2001 From: os-justin Date: Thu, 17 Sep 2026 22:41:37 +0000 Subject: [PATCH 2/2] test(pm): pin that a decorated claim enters the pool, governs, and is retractable MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A new self-test battery (24 cases, the roster floor raised by exactly the one battery it adds) pins: a bold and a backticked claim ENTER the pool and GOVERN with their `Branch:` and `Clause-②` lines read from them; a decorated NEWER claim supersedes a BARE older one and the value the limb reads moves with it; the retraction index sees a decorated claim as retractable by its own author; the input record names the decorated row it selected. The controls are the other half: the constant itself still refuses those bodies, a list item / heading / underscore-emphasis line is still not a claim, the `Clause-②-correction:` comment does not enter the pool through the new door, and the two undecoration paths this file holds are pinned as the two jobs they are -- the sigil-led #18373 retraction is read by one and not by the other. Claude-Session: https://claude.ai/code/session_01Gqi43smmqjJ5sUrhfoPeKu Co-authored-by: Claude --- scripts/pm/check-clause2-carriers.mjs | 85 ++++++++++++++++++++++++++- 1 file changed, 82 insertions(+), 3 deletions(-) diff --git a/scripts/pm/check-clause2-carriers.mjs b/scripts/pm/check-clause2-carriers.mjs index 1a7f8bff3aa..f7ee45d4ef7 100644 --- a/scripts/pm/check-clause2-carriers.mjs +++ b/scripts/pm/check-clause2-carriers.mjs @@ -782,6 +782,7 @@ const SELF_TEST_BATTERIES = Object.freeze({ '#18701: ONE thread set -- what the template STATES is what the queue guard READS': 16, '#18719: a RETRACTED claim leaves the pool — a withdrawn claim never governs': 36, '#18683: the card-comment read pages to a cap — past 100 is UNJUDGED, ⛔ never a truncated pool': 27, + '#18764: a DECORATED claim ENTERS the pool — ONE reading, and it is the sibling\'s': 24, }); // DELETING an entry silences that battery's floor exactly as effectively as @@ -793,8 +794,8 @@ const SELF_TEST_BATTERIES = Object.freeze({ // one #17915 adds, by the one #17959 adds, by the one #18042 adds, and by the // one #18174 adds, and by the one #18141 adds, and by the one #17919 adds, and // by the one #16833 adds, and by the one #18456 adds, and by the one #18719 -// adds, and by the one #18683 adds. -const SELF_TEST_BATTERY_FLOOR = 30; +// adds, and by the one #18683 adds, and by the one #18764 adds. +const SELF_TEST_BATTERY_FLOOR = 31; // The key an assertion is filed under when no battery is open. It is not a // declared battery, so it reds by the same set difference rather than silently @@ -8183,6 +8184,82 @@ export async function selfTest() { })()); t('⛔ CONTROL: the diagnosis KEY is unchanged, so every sentence already keyed to `comments` still finds it', readDiagnosisKey('comments', 770017) === 'comments:770017'); + // -- #18764: a DECORATED claim ENTERS the pool ----------------------------- + // + // The two live records the escalation named, replayed OFFLINE and ⛔ never + // re-graded: they are another repository's cards and another seat's work. + // Ids, stamps and the load-bearing LINES are the real ones, read from the + // REST rows at 2026-09-17T22:02Z — ⚠️ both cards have since left the state + // they were read in, which is why the reading carries its time. The bodies + // below ADD the `Branch:` / `Clause-②` lines the real comments did not carry: + // without them the thread reads `claim-branch-unparsed` on BOTH sides of this + // change (measured), and the defect this battery pins is the one that only + // shows once a claim is otherwise complete. + battery('#18764: a DECORATED claim ENTERS the pool — ONE reading, and it is the sibling\'s'); + const D64 = (id, at, body, login = 'os-sales') => ({ id, created_at: at, user: { login }, body }); + const D64_BOLD = D64(5721120402, '2026-09-17T20:57:09Z', [ + '**Claim:** card objectui#9660, by the `domain:spec` @ objectui execution seat, session `session_01UanLVj6xvbS6puBCewLr8L`.', + 'Branch: `claude/issue-9660-named-test-invocation`', + 'Clause-②: no', + ].join('\n')); + const D64_TICK = D64(5720184809, '2026-09-17T19:41:36Z', [ + '`Claim:` card objectui#9717, by the `domain:spec` @ objectui execution seat, session `session_01UanLVj6xvbS6puBCewLr8L`.', + 'Branch: `claude/issue-9717-doc-component-types`', + 'Clause-②: no', + ].join('\n')); + // The older BARE claim, declaring the OPPOSITE value — so "the wrong carrier + // governs" is a WRONG VALUE here and not merely a missing one. + const D64_BARE_OLD = D64(5700000001, '2026-09-17T18:00:00Z', [ + 'Claim: the older BARE claim', + 'Branch: `claude/issue-9660-older-bare`', + 'Clause-②: yes', + ].join('\n')); + const D64_POOL = (rows) => claimCarrierSelection(rows).pool.map((r) => r.id).join(','); + const D64_RECORD = (rows) => { + const rec = pairInputRecord({ pr: 9999, card: 9660, cardComments: rows, headSha: 'offline' }); + return { selected: [rec['claim.selected']].flat().join('\n'), rejected: [rec['claim.rejected']].flat().join('\n') }; + }; + + t('⭐ a BOLD claim ENTERS the pool — the state it could not reach at all before', D64_POOL([D64_BOLD]) === String(D64_BOLD.id), D64_POOL([D64_BOLD])); + t('…and GOVERNS: the branch resolved is the one IT names', (claimCarrierSelection([D64_BOLD]).governing?.branches ?? []).join() === 'claude/issue-9660-named-test-invocation'); + t('…and the declaration limb reads the `Clause-②` line OFF IT', cardDeclaration([D64_BOLD]).state === 'declared' && cardDeclaration([D64_BOLD]).value === 'no'); + t('⛔ CONTROL — the CONSTANT is NOT widened: the raw marker still refuses that same body', CLAIM_COMMENT_MARKER.test(D64_BOLD.body) === false); + t('⭐ the BACKTICKED spelling is the same record, by the same reading', D64_POOL([D64_TICK]) === String(D64_TICK.id)); + t('…with its own `Branch:` line resolved', (claimCarrierSelection([D64_TICK]).governing?.branches ?? []).join() === 'claude/issue-9717-doc-component-types'); + t('…and its own declaration read', cardDeclaration([D64_TICK]).value === 'no'); + t('⛔ CONTROL: the raw marker refuses the backticked body too', CLAIM_COMMENT_MARKER.test(D64_TICK.body) === false); + t('⭐ a DECORATED NEWER claim SUPERSEDES a BARE older one — the pool is the newest, not the readable one', D64_POOL([D64_BARE_OLD, D64_BOLD]) === String(D64_BOLD.id), D64_POOL([D64_BARE_OLD, D64_BOLD])); + t('…and the older record is LISTED, as SUPERSEDED rather than dropped', says(D64_RECORD([D64_BARE_OLD, D64_BOLD]).rejected, 'a SUPERSEDED claim') && says(D64_RECORD([D64_BARE_OLD, D64_BOLD]).rejected, String(D64_BARE_OLD.id))); + t('⭐ …and the VALUE the limb reads is the newer one', cardDeclaration([D64_BARE_OLD, D64_BOLD]).value === 'no'); + t('⛔ CONTROL: the older record declares the OPPOSITE, so selecting the wrong carrier is a WRONG value, ⛔ not a missing one', cardDeclaration([D64_BARE_OLD]).value === 'yes'); + t('the input record NAMES the decorated row it selected — by id and by date', says(D64_RECORD([D64_BOLD]).selected, String(D64_BOLD.id)) && says(D64_RECORD([D64_BOLD]).selected, '2026-09-17T20:57:09Z')); + t('⛔ CONTROL: the same thread read the other way says nobody claimed at all', says(D64_RECORD([D64(1, '2026-09-17T20:57:09Z', 'no claim on this line')]).selected, 'no comment on this thread carries')); + + // The retraction index reads the SAME predicate, so a decorated claim is + // retractable by its own author — ⛔ never a record that can be written but + // never withdrawn. + const D64_RELEASE = D64(5721120999, '2026-09-17T21:30:00Z', 'Release: session `session_01UanLVj6xvbS6puBCewLr8L` — 去向 `pm:queue`'); + t('⭐ the retraction index SEES a decorated claim — it is retractable by its own author', claimRetractions([D64_BOLD, D64_RELEASE]).has(D64_BOLD)); + t('…and the pool then says every claim on the thread is RETRACTED, ⛔ not that none was written', says(D64_RECORD([D64_BOLD, D64_RELEASE]).selected, 'RETRACTED')); + t('⛔ CONTROL: a DIFFERENT author\'s release retracts nothing, decorated or not', claimRetractions([D64_BOLD, { ...D64_RELEASE, user: { login: 'os-other' } }]).size === 0); + + // The refusals are the SIBLING's and are pinned here as still-refused: this + // file admits no spelling of its own, so a form the sibling names as a NEAR + // MISS must not become a claim by arriving through this door. + t('⛔ a markdown LIST ITEM is still not a claim — the shape the shared reading refuses to undecorate through', D64_POOL([D64(2, '2026-09-17T20:00:00Z', '- Claim: seat.\nBranch: `claude/issue-1-x`')]) === ''); + t('⛔ a HEADING-style claim is still not one', D64_POOL([D64(3, '2026-09-17T20:00:00Z', '## Claim: seat.\nBranch: `claude/issue-1-x`')]) === ''); + t('⛔ UNDERSCORE emphasis is still a NAMED near miss, ⛔ not a claim', markerMatches(CLAIM_COMMENT_MARKER, '__Claim:__ seat.') === false); + t('⛔ and the `Clause-②-correction:` comment does not enter the pool through the new door either', D64_POOL([FIXED_CORRECTION('no')]) === '' && markerMatches(CLAIM_COMMENT_MARKER, FIXED_CORRECTION('no').body) === false); + t('⛔ provably ADDITIVE: a bare claim this file already read reads exactly as before', claimCarrierSelection([CLAIM('Clause-②: no')]).pool.length === 1); + + // ⭐ The file keeps TWO undecoration paths, and that is a measurement rather + // than an oversight: `undecorateRetractionLine` strips `_` and then every + // leading non-letter/non-digit character, which the shared stripper does not. + // 5 of 12 fixtures read differently. Both directions are load-bearing, so the + // pair below pins the divergence instead of quietly closing it. + t('⭐ the RETRACTION path still reads the sigil-led #18373 line — narrowing it to the shared reading would unland #18719', claimRetractions(RTX_18373).has(RTX_18373.find((r) => r.id === RTX_WITHDRAWN))); + t('⭐ …while the SHARED reading does not strip a leading sigil, so the two paths are ⛔ NOT interchangeable', markerMatches(CLAIM_COMMENT_MARKER, '🚨 Claim: PM loop round 1') === false); + // -- The floor: every declared battery RAN, and ran its cases (#13489) ----- // // Evaluated after every battery has had its chance and BEFORE the verdict, so @@ -8251,7 +8328,9 @@ export async function selfTest() { + 'line that QUOTES the spelling held apart from one that declares a value in BOTH halves ' + 'of that property, the input record whose field roster is the same on exit 0, on exit 4 ' + 'and on a refusal — with the selected carrier, its body fingerprint and the rejected ' - + 'candidates each stated — and the exit register).', + + 'candidates each stated, the DECORATED claim that enters the pool and governs through ' + + 'the sibling\'s one reading — the constant unwidened, and the file\'s two undecoration ' + + 'paths pinned as the two jobs they are — and the exit register).', ); selfTestReachedVerdict = true;