|
126 | 126 | * way H4 tolerates it (a leading blockquote, a list bullet, backticks, bold), |
127 | 127 | * because that is markdown a seat writes without meaning anything by it; the |
128 | 128 | * KEY and the VALUE are literal. A near-miss spelling is reported IN the row so |
129 | | - * the residue is actionable, and it still does not count as a declaration. |
| 129 | + * the residue is actionable, and it still does not count as a declaration. So |
| 130 | + * is the other near miss, the one where the key is spelled exactly right and |
| 131 | + * sits mid-line after another field: that line is quoted back with a remedy |
| 132 | + * naming PLACEMENT rather than spelling, and it is not read as a declaration |
| 133 | + * either — what moved is the sentence, never the accept set. |
130 | 134 | * |
131 | 135 | * **It writes nothing.** No label, ever — hanging or clearing a review gate |
132 | 136 | * from a checker would be issuing the review verdict, which is 自查放行 and is |
@@ -528,6 +532,62 @@ const CLAUSE2_KEY_LINE = /^[ \t]*(?:>[ \t]*)?(?:[-*][ \t]+)?(?:\*\*)?`?Clause- |
528 | 532 | */ |
529 | 533 | const CLAUSE2_NEAR_MISS_LINE = /^[ \t]*(?:>[ \t]*)?(?:[-*#][ \t]*)*(?:\*\*)?`?\s*Clause[ \t-]*(?:②|2|two)(?![\w]).*$/i; |
530 | 534 |
|
| 535 | +/** |
| 536 | + * The SECOND near-miss shape, and the one both patterns above are blind to: the |
| 537 | + * key in the FIXED spelling, on a line that starts with something else. |
| 538 | + * |
| 539 | + * `Domain: \`domain:cli\` · Clause-②: no` is a natural way to write a compact |
| 540 | + * claim header, and it reaches neither pattern above — both anchor at `^` and |
| 541 | + * tolerate only line-start decoration before the key. So the line was invisible |
| 542 | + * TWICE: not read as a declaration (correct), and not quoted back as a near miss |
| 543 | + * either (the whole job of the mechanism above). What the seat was told instead |
| 544 | + * was that the SPELLING was wrong, on a line spelled exactly right. |
| 545 | + * |
| 546 | + * ⛔ This is a REPORTER, never a reader. It changes what this file SAYS about a |
| 547 | + * line it does not read; it changes nothing about what it ACCEPTS. |
| 548 | + * `CLAUSE2_KEY_LINE` is untouched, and must stay untouched: 「a predicate that |
| 549 | + * reads prose is a heuristic, and the measured terminus of that direction is a |
| 550 | + * check that can barely fail」. |
| 551 | + * |
| 552 | + * ⭐ And the reason that red line is structural rather than stylistic: this |
| 553 | + * reader decides "is this line a declaration?" by POSITION ALONE. Loosening the |
| 554 | + * position rule to catch the shape above would, by the same stroke, promote more |
| 555 | + * merely-DESCRIBING prose into candidate declarations — the opposite direction, |
| 556 | + * measured on the same regex. So the detector below deliberately fires only |
| 557 | + * where the key is NOT at the start of a line, and a key-initial line reaches it |
| 558 | + * never: whatever a key-initial line reads as, this file does not move it. |
| 559 | + * |
| 560 | + * The prefix set is the decoration the two patterns above already tolerate |
| 561 | + * (whitespace, a blockquote `>`, a list bullet, `#`, backtick/bold wrapping). A |
| 562 | + * line whose key is preceded by only that is a DECORATION near miss and keeps |
| 563 | + * the spelling remedy; a line whose key is preceded by anything else is a |
| 564 | + * PLACEMENT near miss and gets a remedy that names placement. |
| 565 | + */ |
| 566 | +const CLAUSE2_KEY_TEXT = 'Clause-②'; |
| 567 | +const CLAUSE2_KEY_COLON = /^`?(?:\*\*)?[ \t]*:/; |
| 568 | +const CLAUSE2_LINE_START_DECORATION = /^[ \t>\-*#`]*$/; |
| 569 | + |
| 570 | +/** |
| 571 | + * Does this line carry the fixed key, followed by its colon, at a position no |
| 572 | + * line-start decoration can explain? |
| 573 | + * |
| 574 | + * @param {string} line |
| 575 | + * @returns {boolean} |
| 576 | + */ |
| 577 | +function hasInlineClause2Key(line) { |
| 578 | + const s = String(line ?? ''); |
| 579 | + let from = 0; |
| 580 | + for (;;) { |
| 581 | + const at = s.indexOf(CLAUSE2_KEY_TEXT, from); |
| 582 | + if (at < 0) return false; |
| 583 | + from = at + CLAUSE2_KEY_TEXT.length; |
| 584 | + // The key alone is not the shape; it is the key AND its colon, so a bare |
| 585 | + // mention of `Clause-②` in a sentence is left to the pattern above. |
| 586 | + if (!CLAUSE2_KEY_COLON.test(s.slice(from))) continue; |
| 587 | + if (!CLAUSE2_LINE_START_DECORATION.test(s.slice(0, at))) return true; |
| 588 | + } |
| 589 | +} |
| 590 | + |
531 | 591 | /** |
532 | 592 | * The value token, read immediately after the colon. |
533 | 593 | * |
@@ -567,26 +627,38 @@ function quoteLine(line, cap = 160) { |
567 | 627 | * @param {string} text |
568 | 628 | * @returns {{ kind: 'declared', value: 'yes'|'no', line: string } |
569 | 629 | * | { kind: 'malformed', value: string, line: string } |
570 | | - * | { kind: 'near-miss', line: string } |
| 630 | + * | { kind: 'near-miss', reason: 'inline-key'|'spelling', line: string } |
571 | 631 | * | null} |
572 | 632 | * |
573 | 633 | * Four-valued on purpose. `declared` and `malformed` are different facts about |
574 | 634 | * a line that IS the key; `near-miss` is a fact about a line that is not. Any |
575 | 635 | * collapse of these into "no" is the defect #13914 filed. |
| 636 | + * |
| 637 | + * The near miss carries a REASON because the two shapes owe opposite remedies: |
| 638 | + * `spelling` is a line that does not carry the fixed key at all, and `inline-key` |
| 639 | + * is a line that carries it exactly right but not at the start of a line. ⛔ The |
| 640 | + * reason changes the sentence, never the state — both are near misses, and a |
| 641 | + * near miss is not a declaration in either case. |
576 | 642 | */ |
577 | 643 | export function readClause2Line(text) { |
578 | 644 | const lines = String(text ?? '').split(/\r?\n/); |
579 | 645 | let nearMiss = null; |
| 646 | + let inlineKey = null; |
580 | 647 | for (const line of lines) { |
581 | 648 | const m = CLAUSE2_KEY_LINE.exec(line); |
582 | 649 | if (m) { |
583 | 650 | const value = readValueToken(m[1]); |
584 | 651 | if (value !== null) return { kind: 'declared', value, line: quoteLine(line) }; |
585 | 652 | return { kind: 'malformed', value: quoteLine(m[1], 60), line: quoteLine(line) }; |
586 | 653 | } |
| 654 | + if (inlineKey === null && hasInlineClause2Key(line)) inlineKey = quoteLine(line); |
587 | 655 | if (nearMiss === null && CLAUSE2_NEAR_MISS_LINE.test(line)) nearMiss = quoteLine(line); |
588 | 656 | } |
589 | | - return nearMiss === null ? null : { kind: 'near-miss', line: nearMiss }; |
| 657 | + // The correctly-spelled key wins over a vocabulary near miss wherever the two |
| 658 | + // land in the body: it is the more actionable of the two residues, and reading |
| 659 | + // order is not a fact about which one the seat should be sent to. |
| 660 | + if (inlineKey !== null) return { kind: 'near-miss', reason: 'inline-key', line: inlineKey }; |
| 661 | + return nearMiss === null ? null : { kind: 'near-miss', reason: 'spelling', line: nearMiss }; |
590 | 662 | } |
591 | 663 |
|
592 | 664 | /** |
@@ -619,7 +691,7 @@ export function readClause2Line(text) { |
619 | 691 | * @param {{ body?: string, created_at?: string }[]|null} commentRows — the REST |
620 | 692 | * comment rows, or `null` when the thread could NOT be read. |
621 | 693 | * @returns {{ state: 'declared'|'malformed'|'misplaced'|'missing'|'absent'|'unreadable', |
622 | | - * value?: 'yes'|'no', detail?: string }} |
| 694 | + * value?: 'yes'|'no', detail?: string, nearMissReason?: 'inline-key'|'spelling' }} |
623 | 695 | */ |
624 | 696 | export function cardDeclaration(commentRows) { |
625 | 697 | if (!Array.isArray(commentRows)) return { state: 'unreadable' }; |
@@ -657,7 +729,16 @@ export function cardDeclaration(commentRows) { |
657 | 729 | // exists at all. `claimRows` is non-empty exactly when some comment matched |
658 | 730 | // the imported claim predicate, and `pool` is derived from it — so this asks |
659 | 731 | // the same question the reading above asked and cannot answer it differently. |
660 | | - return { state: claimRows.length > 0 ? 'missing' : 'absent', detail: nearMiss?.line }; |
| 732 | + // `nearMissReason` rides alongside the quoted line for exactly one purpose: |
| 733 | + // the row below picks its REMEDY sentence from it. ⛔ It is not part of the |
| 734 | + // state union and no verdict, count or exit reads it — a near miss with a |
| 735 | + // correctly-spelled key is the same `missing` this function has always |
| 736 | + // returned, and #12409's boundary moves by not one character. |
| 737 | + return { |
| 738 | + state: claimRows.length > 0 ? 'missing' : 'absent', |
| 739 | + detail: nearMiss?.line, |
| 740 | + nearMissReason: nearMiss?.reason, |
| 741 | + }; |
661 | 742 | } |
662 | 743 |
|
663 | 744 | // --------------------------------------------------------------------------- |
@@ -810,6 +891,24 @@ export function c2DeclarationUnreadable(pair) { |
810 | 891 | `${NEVER_WRITES}` |
811 | 892 | ); |
812 | 893 | case 'missing': |
| 894 | + // The key is on the thread, spelled exactly right, and simply not at the |
| 895 | + // start of a line. The state is unchanged — it was not read, so it is |
| 896 | + // still `missing` and still exits 4 — but the remedy that ships with the |
| 897 | + // sentence below sends the seat to inspect SPELLING, which for this shape |
| 898 | + // is a search for a typo that is not there. Naming the placement is the |
| 899 | + // whole of the change; ⛔ nothing here accepts the line. |
| 900 | + if (d.nearMissReason === 'inline-key') { |
| 901 | + return ( |
| 902 | + `${head} — NO READING on the declaration limb, and the PLACEMENT of the key is what is ` + |
| 903 | + `wrong, NOT its spelling: the thread carries the key in the fixed spelling, on ` + |
| 904 | + `${JSON.stringify(d.detail)}, but not at the START of a line. This limb is read ` + |
| 905 | + 'line-anchored — only whitespace, a blockquote `>`, a list bullet and backtick/bold ' + |
| 906 | + 'wrapping may precede the key — so a key that follows another field on a shared line is ' + |
| 907 | + 'not read, however correctly it is spelled. ⛔ There is no typo to find on that line. ' + |
| 908 | + 'Remedy: put the `Clause-②: yes|no` line on a line of its OWN in the card\'s claim ' + |
| 909 | + `comment, unchanged otherwise — ${fixed}. ${notADecision} ${NEVER_WRITES}` |
| 910 | + ); |
| 911 | + } |
813 | 912 | return ( |
814 | 913 | `${head} — NO READING on the declaration limb, and the DECLARATION LINE is what is ` + |
815 | 914 | `missing: the card's claim comment is there and carries no \`Clause-②:\` line in the ` + |
@@ -2254,6 +2353,21 @@ export function selfTest() { |
2254 | 2353 | t('a very long claim line is quoted back CAPPED, so one row cannot swamp the report', (readClause2Line(`Clause-②: maybe ${'x'.repeat(400)}`)?.line ?? '').length < 200); |
2255 | 2354 | t('a card that never mentions the clause reads null', readClause2Line('Claim: whatever\nBranch: x') === null); |
2256 | 2355 | t('⛔ the reader never invents a value from an adjacent word', readClause2Line('this card is clause 2 yes in substance')?.kind !== 'declared'); |
| 2356 | + // The inline-key near miss. Each case below pins ONE line and says only what |
| 2357 | + // that line shows: the measured shape is `Domain: … · Clause-②: no`, which is |
| 2358 | + // how a compact claim header is written and which reached NEITHER pattern. |
| 2359 | + t('this exact shared-line header — `Domain: `domain:cli` · Clause-②: no` — is reported as a near miss', readClause2Line('Domain: `domain:cli` · Clause-②: no')?.kind === 'near-miss'); |
| 2360 | + t('…reasoned INLINE-KEY, so the row that quotes it can name placement instead of spelling', readClause2Line('Domain: `domain:cli` · Clause-②: no')?.reason === 'inline-key'); |
| 2361 | + t('…and ⛔ NOT read as a declaration: this line is exactly as unread as it was before', readClause2Line('Domain: `domain:cli` · Clause-②: no')?.kind !== 'declared'); |
| 2362 | + t('…and the row quotes THAT line, not the key alone', says(readClause2Line('Domain: `domain:cli` · Clause-②: no')?.line, 'Domain:')); |
| 2363 | + // ⛔ The reporter fires only where the key is NOT at the start of a line. The |
| 2364 | + // key-INITIAL direction is a different card's (#17098) and is not moved here; |
| 2365 | + // this pins the property of THIS change — a key-initial line never reaches the |
| 2366 | + // inline-key reason — rather than pinning what that direction currently reads. |
| 2367 | + t('⛔ a key-INITIAL line is never reasoned inline-key — decoration before the key is not placement', readClause2Line('## Clause-②: yes')?.reason === 'spelling'); |
| 2368 | + t('…nor is a bulleted, blockquoted or backticked key: `> - `Clause-②` : yes` still reads DECLARED, untouched', readClause2Line('> - `Clause-②` : yes')?.kind === 'declared'); |
| 2369 | + t('⛔ a mid-line mention with NO colon is not the inline shape — the reporter looks for the key AND its colon', readClause2Line('Domain: x · Clause-② is not touched here') === null); |
| 2370 | + t('the inline-key residue wins over a vocabulary near miss written ABOVE it — reading order is not a fact about the remedy', readClause2Line('## Clause ②: **yes**\nDomain: x · Clause-②: no')?.reason === 'inline-key'); |
2257 | 2371 |
|
2258 | 2372 | // -- the card-level declaration: every state, none collapsed into another -- |
2259 | 2373 | battery('the card-level declaration: every state, none collapsed into another'); |
@@ -2342,6 +2456,25 @@ export function selfTest() { |
2342 | 2456 | const nearMiss = c2DeclarationUnreadable(pair({ cardComments: [CLAIM('## Clause ②: **yes**')] })); |
2343 | 2457 | t('a prose declaration still produces the C2 row — prose is not a reading', typeof nearMiss === 'string'); |
2344 | 2458 | t('…and the row quotes what WAS there, so the residue is actionable', says(nearMiss, 'Clause ②')); |
| 2459 | + // The inline-key row. The measured claim comment is a compact header — the |
| 2460 | + // key correctly spelled, after another field, on a shared line. |
| 2461 | + const INLINE = 'Domain: `domain:cli` · Clause-②: no'; |
| 2462 | + const inlineRow = c2DeclarationUnreadable(pair({ cardComments: [CLAIM(INLINE)] })); |
| 2463 | + t('a claim comment whose only Clause-② key sits mid-line produces a C2 row', typeof inlineRow === 'string'); |
| 2464 | + t('…and that row names PLACEMENT as what is wrong', says(inlineRow, 'PLACEMENT of the key is what is wrong')); |
| 2465 | + t('…and says in as many words that the spelling is NOT it', says(inlineRow, 'NOT its spelling') && says(inlineRow, 'no typo to find')); |
| 2466 | + t('…and quotes the offending line, which is what makes the residue actionable', says(inlineRow, 'Domain: `domain:cli`')); |
| 2467 | + t('…and its remedy is to put the line on one of its OWN, not to hunt a misspelling', says(inlineRow, 'line of its OWN')); |
| 2468 | + t('⛔ it is a DIFFERENT sentence from the row a claim comment with no key at all gets', inlineRow !== missingLine); |
| 2469 | + t('⛔ and the state behind it is still MISSING — the sentence moved, the accept set did not', cardDeclaration([CLAIM(INLINE)]).state === 'missing'); |
| 2470 | + t('⛔ …with no value read off that line', cardDeclaration([CLAIM(INLINE)]).value === undefined); |
| 2471 | + t('⛔ and the sweep still counts it as one NOT-READ card, exactly as before', declarationLimbTally([pair({ cardComments: [CLAIM(INLINE)] })]).missing === 1); |
| 2472 | + t('…and the row still carries the standing refusal to relax the spelling', says(inlineRow, 'do not relax the') && says(inlineRow, 'Do not fill the line in')); |
| 2473 | + // A thread with no claim comment owes the COMMENT, so that remedy does not |
| 2474 | + // change; what it gains is the quotation it never had. |
| 2475 | + const inlineNoClaim = c2DeclarationUnreadable(pair({ cardComments: [{ body: INLINE, created_at: '2026-08-31T10:00:00Z' }] })); |
| 2476 | + t('a NON-claim comment carrying the inline key still owes the CLAIM COMMENT', says(inlineNoClaim, 'CLAIM COMMENT is what is missing')); |
| 2477 | + t('…and now quotes that line as the nearest thing on the thread', says(inlineNoClaim, 'Domain: `domain:cli`')); |
2345 | 2478 | const misplaced = c2DeclarationUnreadable(pair({ cardComments: [CLAIM('Domain: x'), { body: 'Clause-②: yes', created_at: '2026-08-31T11:00:00Z' }] })); |
2346 | 2479 | t('a declaration outside the claim comment reads MISPLACED, with its own sentence', says(misplaced, 'MISPLACED')); |
2347 | 2480 | t('…and says the thinking was done, only in the wrong carrier', says(misplaced, 'not in a place') || says(misplaced, 'place the predicate does not look')); |
|
0 commit comments