From 8048faf0257065657fb09dbacc7998a67c77a30b Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 12 Sep 2026 04:50:57 +0000 Subject: [PATCH 1/2] =?UTF-8?q?fix(pm):=20read=20a=20key-initial=20describ?= =?UTF-8?q?ing=20clause-=E2=91=A1=20line=20as=20a=20non-declaration?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `CLAUSE2_KEY_LINE` decides "is this a declaration?" by position, and a standing-rules bullet teaching the spelling puts the key in exactly the position a declaration does. A claim comment whose only key-initial line was such a bullet had its EXPLANATION read as the card's declaration. Two structural tells now demote a matching line to a describing near miss (card state `missing`, a new `describing` reason with its own remedy sentence): the fixed key named more than once on the line, and the key held inside an inline-code span the line goes on talking outside of. A describing line is skipped rather than returned, so a real declaration written below one is now read. Separately, `readValueToken` refuses a value token followed immediately by an alternation: `yes|no` is a menu, not a choice, and it now reads `malformed` alongside the `` placeholder it has always matched. The pre-registered flip case in the #17366 battery is flipped, not deleted. Co-authored-by: Claude Claude-Session: https://claude.ai/code/session_01MCLBsUgfykL74aU716rzVK --- scripts/pm/check-clause2-carriers.mjs | 205 +++++++++++++++++++++++--- 1 file changed, 181 insertions(+), 24 deletions(-) diff --git a/scripts/pm/check-clause2-carriers.mjs b/scripts/pm/check-clause2-carriers.mjs index 92d43d0bb8..0a4791632f 100644 --- a/scripts/pm/check-clause2-carriers.mjs +++ b/scripts/pm/check-clause2-carriers.mjs @@ -672,8 +672,17 @@ export const CLAUSE2_VALUES = Object.freeze(['yes', 'no']); * full-width colon, or the space-separated prose form `Clause ②:` that two of * the three measured cards actually wrote. Those are near misses and are * reported as such below; they are not declarations. + * + * ⭐ Three capture groups, and the first two exist for #17098: whether the key + * was OPENED with a backtick, and whether that backtick CLOSED before the + * colon. The pattern has always tolerated both ticks; what it could not say is + * WHICH of them it consumed — and a span closed around the key (a declaration, + * merely backticked) differs from a span still open at the colon (the VALUE is + * inside quoted text, and the line is a quotation of the spelling) by nothing + * else on the line. ⛔ The tolerated decoration is byte-identical to what it + * was: the groups report the match, they do not widen it. */ -const CLAUSE2_KEY_LINE = /^[ \t]*(?:>[ \t]*)?(?:[-*][ \t]+)?(?:\*\*)?`?Clause-②`?(?:\*\*)?[ \t]*:(.*)$/; +const CLAUSE2_KEY_LINE = /^[ \t]*(?:>[ \t]*)?(?:[-*][ \t]+)?(?:\*\*)?(`?)Clause-②(`?)(?:\*\*)?[ \t]*:(.*)$/; /** * A line that MENTIONS the clause without being the machine declaration — used @@ -756,16 +765,110 @@ function hasInlineClause2Key(line) { * `Clause-②: YES`, `Clause-②: nope` and an empty value all stay MALFORMED, * because none of them opens with the token. The boundary is a character * class, not a judgement. + * + * ⭐ One more shape joins them for #17098: a token followed by an + * ALTERNATION. The class above ends at `[A-Za-z0-9_]` and `|` is not in it, + * so `yes|no` opened with a valid token and returned `yes` — a MENU read as + * a CHOICE, which is how a seat's own spelling instruction became its card's + * judgement. It is refused HERE, alongside `Clause-②: ` and every + * other unfilled template, because it is the same fact about the same slot: + * the value was never chosen. `clause2LineDescribes` states the four axes + * behind putting it here rather than beside the describing tells. + * + * ⛔ The refusal is ADJACENCY, never a scan: only a `|` that is the next + * non-blank character after the token. The reasoning #13914's control shape + * allows may contain a pipe anywhere later — a table column, a shell + * pipeline — and is untouched. */ function readValueToken(raw) { const rest = String(raw ?? '').replace(/^[ \t]+/, ''); // Built from CLAUSE2_VALUES so the closed set is declared once: adding a // third reading would have to be a deliberate edit to that constant. - const token = new RegExp(`^(?:\\*\\*)?(?:\`)?[ \\t]*(${CLAUSE2_VALUES.join('|')})(?![A-Za-z0-9_])`); + const token = new RegExp(`^(?:\\*\\*)?(?:\`)?[ \\t]*(${CLAUSE2_VALUES.join('|')})(?![A-Za-z0-9_])(?![ \\t]*\\|)`); const m = token.exec(rest); return m ? m[1] : null; } +/** + * Does this MATCHING line describe the declaration instead of making one? + * (#17098) + * + * ## The defect, in one line + * + * `CLAUSE2_KEY_LINE` decides "is this a declaration?" by POSITION, and a bullet + * teaching the spelling puts the key in exactly the position a declaration + * does. So a standing-rules bullet quoting both spellings read `declared`, and + * on a claim comment whose only key-initial line was that bullet, the + * EXPLANATION became the card's declaration — measured fail-closed on #16454 + * (a true `no` that hung `needs:contract-review` on both carriers) and measured + * fail-OPEN on #17277 / #17290, where the declaration limb read `yes` from the + * dispatching seat's own boilerplate and `--pair` exited 0, which is a landing + * pre-check's precondition ②. ⭐ The seat that documents the spelling is the + * seat that defeats the check. + * + * ## Two STRUCTURAL tells, and neither is a reading of prose + * + * ⛔ Loosening or tightening the POSITION rule was never available: the header + * one section up states why, and the reporter below it fires only where the key + * is not line-initial. So both tells below are facts about the line's markdown + * STRUCTURE, decided without reading a word of what the seat wrote: + * + * TWICE-NAMED — the fixed key appears more than once on the line. A + * declaration names the key once; a line naming it twice is showing both + * spellings, which is the measured shape of the card's own specimen. + * QUOTED-AND-CONTINUED — the key's inline-code span was opened before the + * key, was NOT closed before the colon, closes later on the line, and the + * line then CONTINUES outside that span. The value is inside a quotation + * and the seat is talking about it. ⭐ The continuation is load-bearing in + * both directions: a line that is only the quoted declaration + * (`` `Clause-②: yes` ``, optionally bolded) is a DECLARATION and stays one + * — that spelling is what this file's own remedy sentence teaches, so + * refusing it would make the gate reject the shape it prescribes. + * + * ## What is NOT a tell here — the alternation, and why + * + * ⚠️ `readValueToken`'s token class ends at `[A-Za-z0-9_]`, so `Clause-②: + * yes|no` opens with a valid token and returned `yes`: a MENU read as a CHOICE. + * That is the same defect, and it is repaired one function down — as + * `malformed`, ⛔ not as a describing near miss, and the four axes agree: + * + * 业务需求 — measured: the live specimen (a bulleted, bolded, backticked + * instruction) already fires QUOTED-AND-CONTINUED, so routing the + * alternation to `malformed` costs nothing on any occurrence on the board. + * The only line where the alternation is the SOLE tell is an undecorated + * `Clause-②: yes|no` — a seat that pasted the template and did not choose. + * 长远合理性 — one state per fact. "The value slot holds a menu" is one fact + * and it already has a state: `Clause-②: ` reads `malformed` + * today, as do `YES`, `nope` and an empty value. A second state for the + * same fact is the dialect direction. + * 防 AI 写错 — the two remedies are not interchangeable. `malformed` names + * the two spellings and says CHOOSE; the describing remedy says ADD a line + * above. For an unfilled template the act that exists is choosing, and + * "add a line above" invites a second, duplicate declaration. Strictness + * is identical either way — both are a C2 row at exit 4. + * 不扩散 — three near-miss reasons where two structural ones carry every + * measured shape is a widened surface with no pull behind it. + * + * @param {string} line — the whole line, for the twice-named count. + * @param {RegExpExecArray} m — this line's `CLAUSE2_KEY_LINE` match. + * @returns {boolean} + */ +function clause2LineDescribes(line, m) { + const s = String(line ?? ''); + // TWICE-NAMED. `indexOf` from the last hit, so an overlap cannot double-count. + let seen = 0; + for (let at = s.indexOf(CLAUSE2_KEY_TEXT); at >= 0; at = s.indexOf(CLAUSE2_KEY_TEXT, at + CLAUSE2_KEY_TEXT.length)) { + if (++seen > 1) return true; + } + // QUOTED-AND-CONTINUED. The span is open at the colon exactly when the key's + // leading tick was consumed and its trailing one was not. + if (m[1] !== '`' || m[2] === '`') return false; + const closesAt = String(m[3] ?? '').indexOf('`'); + if (closesAt < 0) return false; + // Trailing bold and whitespace close the line; anything else continues it. + return !/^[ \t]*(?:\*\*)?[ \t]*$/.test(String(m[3]).slice(closesAt + 1)); +} + /** A quoted line for a finding row — capped, because a claim comment can be long. */ function quoteLine(line, cap = 160) { const s = String(line ?? '').trim().replace(/\s+/g, ' '); @@ -778,36 +881,65 @@ function quoteLine(line, cap = 160) { * @param {string} text * @returns {{ kind: 'declared', value: 'yes'|'no', line: string } * | { kind: 'malformed', value: string, line: string } - * | { kind: 'near-miss', reason: 'inline-key'|'spelling', line: string } + * | { kind: 'near-miss', reason: 'describing'|'inline-key'|'spelling', line: string } * | null} * * Four-valued on purpose. `declared` and `malformed` are different facts about * a line that IS the key; `near-miss` is a fact about a line that is not. Any * collapse of these into "no" is the defect #13914 filed. * - * The near miss carries a REASON because the two shapes owe opposite remedies: - * `spelling` is a line that does not carry the fixed key at all, and `inline-key` - * is a line that carries it exactly right but not at the start of a line. ⛔ The - * reason changes the sentence, never the state — both are near misses, and a - * near miss is not a declaration in either case. + * The near miss carries a REASON because the shapes owe different remedies: + * `spelling` is a line that does not carry the fixed key at all; `inline-key` + * is a line that carries it exactly right but not at the start of a line; and + * `describing` (#17098) is a line that carries it exactly right, at the start + * of a line, and is QUOTING the spelling rather than declaring a value — + * `clause2LineDescribes` holds the two structural tells. ⛔ The reason changes + * the sentence, never the state — all three are near misses, and a near miss + * is not a declaration in any of the three cases. + * + * ⭐ A describing line is SKIPPED, not returned: the scan continues past it. + * That is the half of #17098 the fixture could not see. `readClause2Line` + * returns on the first line that IS a declaration attempt, and a quotation is + * not one — so a claim comment whose real declaration sits BELOW its + * standing-rules bullet is now read from the declaration, where first-match + * previously stopped at the bullet. The describing line is kept only as the + * residue to quote back when nothing else on the body reads. */ export function readClause2Line(text) { const lines = String(text ?? '').split(/\r?\n/); + let read = null; + let describing = null; let nearMiss = null; let inlineKey = null; for (const line of lines) { const m = CLAUSE2_KEY_LINE.exec(line); if (m) { - const value = readValueToken(m[1]); - if (value !== null) return { kind: 'declared', value, line: quoteLine(line) }; - return { kind: 'malformed', value: quoteLine(m[1], 60), line: quoteLine(line) }; + // #17098: a line that QUOTES the spelling is not a declaration attempt, + // so it neither answers nor stops the scan. ⛔ It is not `malformed` + // either — that state sends the seat to fix a value on a line that was + // never making a claim about one. + if (clause2LineDescribes(line, m)) { + if (describing === null) describing = quoteLine(line); + continue; + } + if (read !== null) continue; + const value = readValueToken(m[3]); + read = value !== null + ? { kind: 'declared', value, line: quoteLine(line) } + : { kind: 'malformed', value: quoteLine(m[3], 60), line: quoteLine(line) }; + continue; } if (inlineKey === null && hasInlineClause2Key(line)) inlineKey = quoteLine(line); if (nearMiss === null && CLAUSE2_NEAR_MISS_LINE.test(line)) nearMiss = quoteLine(line); } + if (read !== null) return read; // The correctly-spelled key wins over a vocabulary near miss wherever the two // land in the body: it is the more actionable of the two residues, and reading - // order is not a fact about which one the seat should be sent to. + // order is not a fact about which one the seat should be sent to. By the same + // rule a DESCRIBING line outranks both: it carries the key in the fixed + // spelling AND at the start of a line, so of the three it is the one whose + // remedy is a single line the seat can write without moving anything. + if (describing !== null) return { kind: 'near-miss', reason: 'describing', line: describing }; if (inlineKey !== null) return { kind: 'near-miss', reason: 'inline-key', line: inlineKey }; return nearMiss === null ? null : { kind: 'near-miss', reason: 'spelling', line: nearMiss }; } @@ -1072,7 +1204,7 @@ const CLAUSE2_CORRECTION_KEY_TEXT = 'Clause-②-correction'; * comment rows, or `null` when the thread could NOT be read. * @returns {{ state: 'declared'|'malformed'|'misplaced'|'missing'|'absent'|'unreadable' * |'claim-branch-unparsed', - * value?: 'yes'|'no', detail?: string, nearMissReason?: 'inline-key'|'spelling', + * value?: 'yes'|'no', detail?: string, nearMissReason?: 'describing'|'inline-key'|'spelling', * correctionNote?: string, malformedClaim?: object, governingClaim?: object }} — * `correctionNote` rides alongside for exactly one purpose, the same way * `nearMissReason` does: the rows below print it. ⛔ It is not part of the state @@ -1369,6 +1501,27 @@ function c2Sentence(d, head, fixed, notADecision) { `${CORRECTION_REMEDY} ${NEVER_WRITES}` ); case 'missing': + // #17098: the key is at the start of a line, spelled exactly right, and + // the line is QUOTING the spelling rather than declaring a value. The + // state is `missing` and exits 4 exactly as it always did; what changes + // is that this line USED TO BE READ as the card's declaration, so the + // remedy has to say what the seat is looking at. ⛔ It never tells the + // seat to edit the quoted line: an explanation of the protocol is a + // correct thing to have written. + if (d.nearMissReason === 'describing') { + return ( + `${head} — NO READING on the declaration limb: the thread carries the key in the ` + + `fixed spelling and at the START of a line, on ${JSON.stringify(d.detail)}, but that ` + + 'line QUOTES the spelling rather than declaring a value — it names the key twice, or ' + + 'holds the key inside an inline-code span the line goes on talking outside of. ⛔ A ' + + 'quotation of the protocol is not a judgement about this diff, and the two are told ' + + 'apart by the line\'s markdown structure, never by reading its words. ⛔ There is ' + + 'nothing to fix on the quoted line — it is a correct thing to have written. What is ' + + 'owed is a declaration of its OWN, ABOVE it: this limb reads the first line that IS ' + + `a declaration attempt, so position is the whole remedy — ${fixed}. ` + + `${CORRECTION_REMEDY} ${notADecision} ${NEVER_WRITES}` + ); + } // The key is on the thread, spelled exactly right, and simply not at the // start of a line. The state is unchanged — it was not read, so it is // still `missing` and still exits 4 — but the remedy that ships with the @@ -4205,18 +4358,22 @@ export function selfTest() { t('⭐ criterion 1: the template\'s key with `no` substituted reads DECLARED', readClause2Line('Clause-②: no')?.value === 'no'); t('⭐ …and with `yes` substituted', readClause2Line('Clause-②: yes')?.value === 'yes'); t('…and the template line the seat copies is exactly the fixed key, so the two cannot drift', TEMPLATE_LINE.startsWith('Clause-②:')); - // ⚠️ CONTROL, measured and NOT endorsed: the template line copied WITHOUT - // choosing reads `yes`, because `readValueToken` takes the first token after - // the colon and treats the rest as the seat's argument. That calibration is - // #13914's and is untouched here. + // ⭐ FLIPPED, as the pre-registration above this line required: the template + // line copied WITHOUT choosing used to read `yes`, because `readValueToken` + // took the first token after the colon and treated the rest as the seat's + // argument — so an UNFILLED template read as a judgement. #17098 refuses the + // alternation in `readValueToken`, and the case is kept rather than deleted, + // with its expectation moved: the before-state it recorded is the thing the + // assertion below is now measuring the absence of. // - // ⭐ FLIP TRIGGER, pre-registered: this is a key-INITIAL DESCRIBING line, which - // is exactly the population #17098 is open against. When #17098 lands, this - // reading becomes `kind !== 'declared'` and this case flips WITH it — change - // the expectation in THAT PR and keep the case. ⛔ Do not delete it, and ⛔ do - // not read its green today as an endorsement: it records what the reader does - // now, so that the sibling fix has a measured before-state to move. - t('⚠️ CONTROL (flips with #17098): the UNFILLED template line reads `yes` today — the token is first, the alternative is trailing prose', readClause2Line(TEMPLATE_LINE)?.value === 'yes'); + // ⚠️ It is `malformed` and ⛔ NOT a describing near miss: the line is an + // unfilled TEMPLATE — one key, no inline-code span — so the fact about it is + // that its value slot holds a menu, which is the fact `Clause-②: ` + // has always carried. `clause2LineDescribes` holds the four axes. + t('⭐ the UNFILLED template line is NOT a declaration — a menu is not a choice (#17098)', readClause2Line(TEMPLATE_LINE)?.kind !== 'declared'); + t('…and it carries NO value: ⛔ the first alternative is never taken as the answer', readClause2Line(TEMPLATE_LINE)?.value !== 'yes' && readClause2Line(TEMPLATE_LINE)?.value !== 'no'); + t('…reading MALFORMED, the same state the angle-bracket placeholder has always read', readClause2Line(TEMPLATE_LINE)?.kind === 'malformed' && readClause2Line('Clause-②: ')?.kind === 'malformed'); + t('…and the row quotes the unfilled slot back, so the seat sees what it copied', says(readClause2Line(TEMPLATE_LINE)?.line, 'yes | no')); t('⭐ the C2 rows point at the TEMPLATE rather than at a regex', says(missingLine, '〈模板与表〉') && says(noClaim, '〈模板与表〉')); t('…and tell the seat to COPY it rather than compose one', says(missingLine, 'COPY the template')); t('⛔ and the pointer prescribes no VALUE — the declaration is still the judgement', says(missingLine, 'Do not fill the line in')); From ab572e8f09b524902019ae21fd75ffb310111bba Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 12 Sep 2026 04:55:08 +0000 Subject: [PATCH 2/2] =?UTF-8?q?test(pm):=20finish=20the=20one-sided=20"onl?= =?UTF-8?q?y=20DESCRIBES=20=E2=87=92=20MISSING"=20fixture?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The assertion states a general property and had one case under it: prose before the key, which the line-anchored reader never matched at all, so it passed for a reason narrower than the sentence it was written under. Both halves are now asserted at that sentence, and a new battery carries the mechanism from the measured specimens — the filing card's standing-rules bullet and the dispatch-template bullet a second seat measured, where the defect fired fail-OPEN into a landing pre-check. The battery pins each tell alone so neither can be carrying the other, the skip-not-return property (a declaration below a quotation is now read, which is what a seat had to arrange by hand), the row's three distinct remedies, and controls that the reasoning-after-value allowance and the four-valued reading are both intact. The `pool = claimRows` fallback is pinned as measured-unreachable on both arms rather than rewritten. Co-authored-by: Claude Claude-Session: https://claude.ai/code/session_01MCLBsUgfykL74aU716rzVK --- scripts/pm/check-clause2-carriers.mjs | 150 ++++++++++++++++++++++++-- 1 file changed, 143 insertions(+), 7 deletions(-) diff --git a/scripts/pm/check-clause2-carriers.mjs b/scripts/pm/check-clause2-carriers.mjs index 0a4791632f..aecbb2d866 100644 --- a/scripts/pm/check-clause2-carriers.mjs +++ b/scripts/pm/check-clause2-carriers.mjs @@ -622,6 +622,7 @@ const SELF_TEST_BATTERIES = Object.freeze({ 'the argv contract and the board provenance (#16623)': 42, '#17366: the correction comment — the self-solvable exit, and the three things it is not': 65, '#17149: a claim that parses to ZERO branches — malformed, never absent': 26, + '#17098: a key-INITIAL line that DESCRIBES the spelling — the half the fixture did not cover': 48, }); // DELETING an entry silences that battery's floor exactly as effectively as @@ -629,8 +630,8 @@ const SELF_TEST_BATTERIES = Object.freeze({ // Raised by exactly the one battery #16304 adds, again by exactly the one // #17302 adds, and again by exactly the one #17366 adds, so the roster's // existing slack is preserved rather than tightened or loosened as a side -// effect, and once more by the one #17149 adds. -const SELF_TEST_BATTERY_FLOOR = 18; +// effect, and once more by the one #17149 adds, and by the one #17098 adds. +const SELF_TEST_BATTERY_FLOOR = 19; // 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 @@ -3452,11 +3453,19 @@ export function selfTest() { t('a heading-style claim is NOT a claim comment — the thread reads ABSENT, not missing-a-line', cardDeclaration([{ body: '## Claim — PM loop round R1\nBranch: `claude/issue-13476-unresolvable-engine-403`\nDomain: `domain:engine`', created_at: '2026-08-31T10:00:00Z' }]).state === 'absent'); t('the #13910 shape — a claim comment with no Clause-② line — reads MISSING: the carrier is there, the line is not', cardDeclaration([CLAIM('Domain: `domain:engine`')]).state === 'missing'); t('⛔ ABSENT and MISSING are two readings, never one — one owes a comment, the other a line', cardDeclaration([{ body: 'a triage note, and nothing that begins a line with the claim key', created_at: '2026-08-31T10:00:00Z' }]).state !== cardDeclaration([CLAIM('Domain: x')]).state); - // The substring trap: a claim comment that DESCRIBES the declaration carries - // the key as a fragment inside a sentence, never as a line of its own. The - // reader is line-anchored, so a description is MISSING and never readable. + // A claim comment that DESCRIBES the declaration rather than making one. + // + // ⚠️ This assertion states a GENERAL property and for a long time had ONE + // case under it — prose before the key, which the line-anchored reader never + // matched at all. #17098 measured the other half: with the key FIRST, after + // markdown decoration, the same describing line read `declared`, so the + // sentence was false in general while its own case was green. ⛔ Both halves + // are asserted here, at the sentence that claims them; the #17098 battery + // below carries the mechanism, the measured specimens and the controls. t('a claim comment that only DESCRIBES the line reads MISSING, never declared', cardDeclaration([CLAIM('the dev declares `Clause-②: yes|no` from the diff')]).state === 'missing'); t('…and it carries no value — a fragment inside prose is not a reading of one', cardDeclaration([CLAIM('the dev declares `Clause-②: yes|no` from the diff')]).value === undefined); + t('⭐ …and the same is true KEY-FIRST, which is the half this sentence used to claim without covering', cardDeclaration([CLAIM('- **`Clause-②: yes` / `Clause-②: no`** — the value alone on its line, machine-read.')]).state === 'missing'); + t('⭐ …carrying no value there either — the fail-OPEN half, where a `yes` was invented out of a spelling lesson', cardDeclaration([CLAIM('- **`Clause-②: yes` / `Clause-②: no`** — the value alone on its line, machine-read.')]).value === undefined); t('⛔ neither not-read state is `no`', cardDeclaration([CLAIM('Domain: x')]).state !== 'declared' && cardDeclaration([{ body: 'a triage note, and nothing that begins a line with the claim key', created_at: '2026-08-31T10:00:00Z' }]).state !== 'declared'); t('the line in a NON-claim comment reads MISPLACED, not absent and not declared', cardDeclaration([CLAIM('Domain: x'), { body: 'Clause-②: yes', created_at: '2026-08-31T11:00:00Z' }]).state === 'misplaced'); t('a malformed line in the claim comment reads MALFORMED', cardDeclaration([CLAIM('Clause-②: Yes')]).state === 'malformed'); @@ -4576,6 +4585,132 @@ export function selfTest() { t('⛔ the branch reader was NOT widened — the inline spelling still parses to zero', claimedBranches(INLINE_NEW.body).length === 0); t('⛔ …and the claim marker still matches it, which is what makes the two-anchor split a STATE', CLAIM_COMMENT_MARKER.test(INLINE_NEW.body) === true); + // -- #17098: the key-INITIAL describing line ------------------------------ + // + // The one-sided fixture this battery exists to finish is one section up, in + // the card-level battery: 「a claim comment that only DESCRIBES the line reads + // MISSING, never declared」. Its case put PROSE BEFORE THE KEY, so the line + // never matched `CLAUSE2_KEY_LINE` at all and the assertion passed for a + // reason narrower than the sentence it was written under. The half it did not + // cover — the key FIRST, after markdown decoration — read `declared`, and the + // general property the sentence states was false while its own case was green. + // + // ⭐ Both halves are now pinned, and they are pinned from the MEASURED + // specimens rather than from invented ones: the filing card's standing-rules + // bullet, and the dispatch-template bullet a second seat measured on #17277 / + // #17290 — where this defect fired in the FAIL-OPEN direction, the declaration + // limb reading `yes` from the dispatching seat's own boilerplate while + // `--pair` exited 0 into a landing pre-check. + battery('#17098: a key-INITIAL line that DESCRIBES the spelling — the half the fixture did not cover'); + // The filing card's specimen (#17098 body), and the second seat's (5636056726). + const D_CARD_BULLET = '- **`Clause-②: yes` / `Clause-②: no`** — the value alone on its line, machine-read.'; + const D_TEMPLATE_BULLET = '- **`Clause-②: yes|no` must appear in the PR BODY at column 0.** `Check Changeset` reads it there…'; + t('⭐ the filing card\'s own specimen is NOT a declaration', readClause2Line(D_CARD_BULLET)?.kind !== 'declared'); + t('⭐ …and neither is the second seat\'s measured dispatch-template bullet', readClause2Line(D_TEMPLATE_BULLET)?.kind !== 'declared'); + t('⛔ neither yields a value — the defect was a `yes` invented out of a spelling lesson', readClause2Line(D_CARD_BULLET)?.value === undefined && readClause2Line(D_TEMPLATE_BULLET)?.value === undefined); + t('…both are reasoned DESCRIBING, so the row can say what the seat is looking at', readClause2Line(D_CARD_BULLET)?.reason === 'describing' && readClause2Line(D_TEMPLATE_BULLET)?.reason === 'describing'); + t('⛔ …and NOT `malformed`, which would send the seat to fix a value on a line that claims none', readClause2Line(D_CARD_BULLET)?.kind === 'near-miss' && readClause2Line(D_TEMPLATE_BULLET)?.kind === 'near-miss'); + t('…and each quotes ITS OWN line back, capped, so the residue is actionable', says(readClause2Line(D_CARD_BULLET)?.line, 'machine-read') && says(readClause2Line(D_TEMPLATE_BULLET)?.line, 'column 0')); + // The card level: the general property, now true of BOTH halves. + t('⭐ the card-level reading is MISSING on the key-INITIAL half — the property the sentence states', cardDeclaration([CLAIM(D_CARD_BULLET)]).state === 'missing'); + t('⭐ …and on the second seat\'s bullet too', cardDeclaration([CLAIM(D_TEMPLATE_BULLET)]).state === 'missing'); + t('⛔ …carrying no value in either case — this is the fail-OPEN half, where a `yes` reached exit 0', cardDeclaration([CLAIM(D_CARD_BULLET)]).value === undefined && cardDeclaration([CLAIM(D_TEMPLATE_BULLET)]).value === undefined); + t('…and the prose-FIRST half still reads MISSING, by its own reason — the two halves are one property, not one mechanism', cardDeclaration([CLAIM('the dev declares `Clause-②: yes|no` from the diff')]).state === 'missing' && cardDeclaration([CLAIM('the dev declares `Clause-②: yes|no` from the diff')]).nearMissReason === 'inline-key'); + + // -- the two tells, each pinned ALONE so neither can be carrying the other -- + t('TELL 1 — the fixed key named TWICE on one line is a quotation of the spelling', readClause2Line('- Clause-②: yes, or Clause-②: no — pick one')?.reason === 'describing'); + t('TELL 2 — the key inside an inline-code span the line goes on talking outside of', readClause2Line('- `Clause-②: yes` is what a dev writes when the diff touches the spec')?.reason === 'describing'); + t('⛔ TELL 2 is CONTINUATION, not quoting: the quoted declaration ALONE on its line is a declaration', readClause2Line('`Clause-②: yes`')?.value === 'yes'); + t('⛔ …and bolded around the span too — that spelling is what this file\'s own remedy sentence teaches', readClause2Line('**`Clause-②: no`**')?.value === 'no'); + t('⛔ …while a span closed around the KEY was never the shape at all', readClause2Line('`Clause-②`: no — scripts only')?.value === 'no'); + t('the tells are STRUCTURAL — the same words with the markdown removed declare, and the same markdown with other words describes', readClause2Line('Clause-②: yes is what a dev writes when the diff touches the spec')?.value === 'yes' && readClause2Line('- `Clause-②: no` was yesterday\'s answer')?.reason === 'describing'); + + // -- the alternation, refused in the VALUE reader rather than here --------- + t('⭐ a bare alternation is refused: `yes|no` is a menu, not a choice', readClause2Line('Clause-②: yes|no')?.kind !== 'declared'); + t('…in either order, and spaced', readClause2Line('Clause-②: no|yes')?.kind !== 'declared' && readClause2Line('Clause-②: yes | no')?.kind !== 'declared'); + t('…reading MALFORMED, the state `Clause-②: ` has always read — one fact, one state', readClause2Line('Clause-②: yes|no')?.kind === 'malformed' && readClause2Line('Clause-②: ')?.kind === 'malformed'); + t('⛔ the refusal is ADJACENCY, never a scan: a pipe later in the reasoning is the seat\'s argument', readClause2Line('Clause-②: no — see the table | column two')?.value === 'no'); + + // -- SKIPPED, not returned: the scan continues past a describing line ------ + // + // What a seat had to do BY HAND on three live cards (#17277 · #17290 · #17596) + // was place a real declaration ABOVE the instructional line, because + // first-match-wins made position the whole remedy. A describing line is no + // longer a match, so the order stops mattering. + t('⭐ a real declaration BELOW a describing bullet is read — first-match no longer stops at a quotation', cardDeclaration([CLAIM(`${D_TEMPLATE_BULLET}\nClause-②: no`)]).value === 'no'); + t('…and ABOVE it, which is what the seat had to do by hand', cardDeclaration([CLAIM(`Clause-②: no\n${D_TEMPLATE_BULLET}`)]).value === 'no'); + t('⛔ …and the two orders now read the SAME — the defect was that they did not', cardDeclaration([CLAIM(`${D_CARD_BULLET}\nClause-②: yes`)]).value === cardDeclaration([CLAIM(`Clause-②: yes\n${D_CARD_BULLET}`)]).value); + t('a MALFORMED line below a describing one still reads malformed — skipping a quotation is not skipping a failure', cardDeclaration([CLAIM(`${D_CARD_BULLET}\nClause-②: probably`)]).state === 'malformed'); + + // -- the row: what the seat is told, and what it is NOT told --------------- + const describingRow = c2DeclarationUnreadable(pair({ cardComments: [CLAIM(D_TEMPLATE_BULLET)] })); + t('a claim comment whose only key line is a quotation produces a C2 row', typeof describingRow === 'string'); + t('…that says NO READING, so the state is not dressed up as a verdict', says(describingRow, 'NO READING')); + t('…and names QUOTING as what the line is doing, rather than sending the seat after a typo', says(describingRow, 'QUOTES the spelling rather than declaring a value')); + t('⭐ …and says in as many words that there is nothing to fix on the quoted line', says(describingRow, 'nothing to fix on the quoted line')); + t('⭐ …and that the remedy is a declaration of its OWN, ABOVE it', says(describingRow, 'a declaration of its OWN, ABOVE it')); + t('…and quotes the line, so the seat can see which one it means', says(describingRow, 'column 0')); + t('⛔ …and still refuses to fill the value in on the seat\'s behalf', says(describingRow, 'Do not fill the line in')); + t('⛔ …and is a DIFFERENT sentence from the placement row and from the bare missing row — three residues, three remedies', describingRow !== c2DeclarationUnreadable(pair({ cardComments: [CLAIM('Domain: `domain:cli` · Clause-②: no')] })) && describingRow !== c2DeclarationUnreadable(pair({ cardComments: [CLAIM('Domain: x')] }))); + t('the pair is counted as MISSING in the tally — a not-read declaration, never a clean one', declarationLimbTally([pair({ cardComments: [CLAIM(D_TEMPLATE_BULLET)] })]).missing === 1); + t('⛔ …and is NOT a carrier for a sibling card — a quotation cannot answer another card\'s question', siblingDeclarations(pair({ card: 999, pr: 13910 }), [pair({ card: 999, pr: 13910 }), { pr: 13910, card: 13476, cardComments: [CLAIM(D_TEMPLATE_BULLET)] }]).length === 0); + + // -- CONTROLS: #12297 and #13914 are not undone by any of the above -------- + // + // ⛔ Both are deliberate and both were named as un-undoable by the filing + // card. #12297: reasoning may FOLLOW the value. #13914: the reading is + // FOUR-valued, and no state collapses into another. + t('⛔ CONTROL #12297: the token followed by reasoning is still a declaration', readClause2Line('Clause-②: yes — widens the accept set')?.value === 'yes'); + t('⛔ CONTROL #12297: …including the bold-wrapped parenthesised form seats actually write', readClause2Line('**Clause-②: no**(仅移动 import/注释)')?.value === 'no'); + t('⛔ CONTROL #12297: …and reasoning that itself contains backticks — a span the KEY never opened is not the key\'s span', readClause2Line('Clause-②: yes — `packages/spec` moves')?.value === 'yes'); + t('⛔ CONTROL #12297: …and a parenthesised reason after a bulleted, bolded key', readClause2Line('- **Clause-②: no** (scripts only)')?.value === 'no'); + t('⛔ CONTROL: every decoration the key line has always tolerated still declares', ['Clause-②: yes', '> Clause-②: yes', '- Clause-②: yes', '**Clause-②: yes**', '`Clause-②`: yes', '- **`Clause-②`**: **`yes`**', '> - `Clause-②` : yes'].every((l) => readClause2Line(l)?.value === 'yes')); + t('⛔ CONTROL #13914: all four readings remain reachable and distinct', new Set([ + readClause2Line('Clause-②: yes')?.kind, + readClause2Line('Clause-②: probably not')?.kind, + readClause2Line('## Clause ②: **yes**')?.kind, + String(readClause2Line('Claim: nothing here')), + ]).size === 4); + t('⛔ CONTROL #13914: …and the near miss is still three-reasoned, never collapsed to one', new Set([ + readClause2Line('## Clause ②: **yes**')?.reason, + readClause2Line('Domain: x · Clause-②: no')?.reason, + readClause2Line(D_CARD_BULLET)?.reason, + ]).size === 3); + t('⛔ CONTROL: the accept set moved for DESCRIBING lines only — the two fixed spellings are byte-identical reads', CLAUSE2_VALUES.every((v) => readClause2Line(`Clause-②: ${v}`)?.value === v)); + t('⛔ CONTROL: the near-miss and inline-key reporters are untouched — a mid-line key is still placement, not describing', readClause2Line('Domain: `domain:cli` · Clause-②: no')?.reason === 'inline-key'); + t('⛔ CONTROL: a correction comment\'s own declaration still reads — the describing tells do not reach it', readClause2Correction(FIXED_CORRECTION('no'))?.value === 'no'); + + // -- the `pool = claimRows` fallback: measured UNREACHABLE, left alone ----- + // + // The dispatch pointer asked whether `cardDeclaration`'s + // `governing.length > 0 ? governing : claimRows` fallback — which reads the + // FIRST claim comment by thread order rather than the newest — should be made + // recency-aware. It is measured DEAD after #17149, and a dead branch is a + // report line rather than a rewrite. The two arms, pinned so the measurement + // is re-runnable rather than recalled: + // + // `claim` non-null → it came from a row matching the SAME claim predicate + // `claimRows` filters on, so `governing` always has + // that row in it and is never empty. + // `claim` null → `claimGovernance` returns a null `governing` only + // when no claim row parses a branch, and that same + // condition sets `malformed`, which returns + // `claim-branch-unparsed` ABOVE this line. So a null + // `claim` that reaches here means there were no claim + // comments at all, and `claimRows` is empty too. + t('⭐ arm 1: a governing claim always leaves a non-empty pool, so the fallback cannot fire', cardDeclaration([ + { id: 1, created_at: '2026-08-30T09:00:00Z', body: 'Claim: old\nBranch: `claude/issue-1-old`\nClause-②: yes' }, + { id: 2, created_at: '2026-08-31T09:00:00Z', body: 'Claim: new\nBranch: `claude/issue-1-new`\nClause-②: no' }, + ]).value === 'no'); + t('⭐ arm 2: every claim branchless ⇒ CLAIM-BRANCH-UNPARSED, returned above the pool', cardDeclaration([ + { id: 1, created_at: '2026-08-30T09:00:00Z', body: 'Claim: session_x · claude/issue-1-old\nClause-②: yes' }, + { id: 2, created_at: '2026-08-31T09:00:00Z', body: 'Claim: session_x · claude/issue-1-new\nClause-②: no' }, + ]).state === 'claim-branch-unparsed'); + t('⭐ arm 2: …and NO claim comment at all ⇒ ABSENT, with an empty pool either way', cardDeclaration([{ id: 1, body: 'a triage note', created_at: '2026-08-31T10:00:00Z' }]).state === 'absent'); + t('⛔ …so no thread reaches this limb with a null governing claim AND a non-empty claim set — the fallback is dead code, left as it stands', cardDeclaration([ + { id: 1, created_at: '2026-08-30T09:00:00Z', body: 'Claim: session_x · claude/issue-1-old\nClause-②: yes' }, + ]).state !== 'declared'); + // -- The floor: every declared battery RAN, and ran its cases (#13489) ----- // // Evaluated after every battery has had its chance and BEFORE the verdict, so @@ -4637,8 +4772,9 @@ export function selfTest() { 'spellings held out as negatives, ' + 'the three read paths with their offline reader, the argv contract with its usage and its ' + 'refusal, the board provenance line, the claim whose `Branch:` line parses to ZERO ' - + 'branches — reported as an unresolvable carrier rather than discarded — and the exit ' - + 'register).', + + 'branches — reported as an unresolvable carrier rather than discarded, the key-INITIAL ' + + 'line that QUOTES the spelling held apart from one that declares a value in BOTH halves ' + + 'of that property — and the exit register).', ); selfTestReachedVerdict = true;