|
58 | 58 | // inside its message, so the literal 24-character token appears in the source |
59 | 59 | // ONLY at the `const` declaration — nowhere near the offer text it governs. |
60 | 60 | // |
| 61 | +// Presence is nonetheless checked in AUTHOR-FACING text, never raw source. A gate |
| 62 | +// whose header merely discusses the convention has told the author nothing, and |
| 63 | +// counting that as compliance was a real hole: it was found by stripping the token |
| 64 | +// from a real gate and watching this detector stay green, because that file's own |
| 65 | +// header names the token in a comment. See {@link carriesAuthorityToken}. |
| 66 | +// |
61 | 67 | // ── The control corpus is part of the deliverable ────────────────────────── |
62 | 68 | // |
63 | 69 | // A first cut of the prototype behind #8540 spelled its gap class `[^.;]`, which |
@@ -479,6 +485,27 @@ export function offerIsRefused(offer) { |
479 | 485 | return REFUSAL_BOUND.test(offer.context) || REFUSAL_PREDICATION.test(offer.context); |
480 | 486 | } |
481 | 487 |
|
| 488 | +/** |
| 489 | + * Does this gate actually CARRY the authority token — in the text an author |
| 490 | + * reads, not merely somewhere in the file? |
| 491 | + * |
| 492 | + * ⛔ The distinction is the whole assertion, and a raw `src.includes(…)` gets it |
| 493 | + * wrong. Measured: strip the token from check-role-word.mjs's marker const and a |
| 494 | + * source-wide search still finds it, because the file's own header COMMENT |
| 495 | + * mentions the convention by name. The gate would then have gone silent for every |
| 496 | + * author who trips it while this detector reported the farm clean — the exact |
| 497 | + * shape of failure the convention exists to prevent, reproduced by its detector. |
| 498 | + * |
| 499 | + * Comments are maintainer-facing. A gate that discusses the token has not told |
| 500 | + * the author anything; a gate that puts it in a string has. |
| 501 | + * |
| 502 | + * @param {string} src |
| 503 | + * @returns {boolean} |
| 504 | + */ |
| 505 | +export function carriesAuthorityToken(src) { |
| 506 | + return authorFacingMessages(src).some((m) => m.includes(RATCHET_AUTHORITY_MARKER)); |
| 507 | +} |
| 508 | + |
482 | 509 | // ── Classification ────────────────────────────────────────────────────────── |
483 | 510 |
|
484 | 511 | /** |
@@ -510,7 +537,7 @@ export function classify(src) { |
510 | 537 | return { verdict: refused.length > 0 ? 'refused' : 'excluded', live, refused, anchors }; |
511 | 538 | } |
512 | 539 | return { |
513 | | - verdict: src.includes(RATCHET_AUTHORITY_MARKER) ? 'marked' : 'unmarked', |
| 540 | + verdict: carriesAuthorityToken(src) ? 'marked' : 'unmarked', |
514 | 541 | live, |
515 | 542 | refused, |
516 | 543 | anchors, |
@@ -897,6 +924,24 @@ function selfTest() { |
897 | 924 | + `longer reaches: ${unreached.join(', ') || '(none)'}`, |
898 | 925 | unreached.length === 0); |
899 | 926 |
|
| 927 | + // (18) Compliance is carried in AUTHOR-FACING text, not in commentary. Found by |
| 928 | + // reverse verification: stripping the token from a real gate left this detector |
| 929 | + // green, because that gate's header mentions the token in a comment. |
| 930 | + const commentaryOnly = `// this gate marks the path ${RATCHET_AUTHORITY_MARKER} per #8435\n` |
| 931 | + + `const m = 'Fix it properly. Or ${ADD} a MEASURED entry to the baseline saying why not. That baseline is shrink-only.';`; |
| 932 | + expect('compliance — a gate that only MENTIONS the token in a comment does not count as carrying ' |
| 933 | + + 'it (a source-wide search reads a gate\'s own commentary as compliance, so a gate could go ' |
| 934 | + + 'silent for authors while this detector reported the farm clean)', |
| 935 | + classify(commentaryOnly).verdict === 'unmarked'); |
| 936 | + |
| 937 | + // (19) …and the mirror: the token in a string literal DOES count. Paired with |
| 938 | + // (18) by construction, so exactly one of the two can fire on a broken check. |
| 939 | + const inLiteral = `const T = '${RATCHET_AUTHORITY_MARKER}';\n` |
| 940 | + + `const m = 'Fix it properly. Or ${ADD} a MEASURED entry to the baseline saying why not. That baseline is shrink-only.';`; |
| 941 | + expect('compliance — the token declared as a string literal DOES count as carrying it, which is ' |
| 942 | + + 'how all six instrumented gates spell it', |
| 943 | + classify(inLiteral).verdict === 'marked'); |
| 944 | + |
900 | 945 | // (17) This gate must not be an instance of its own convention. |
901 | 946 | expect('self-classification — this gate is NOT an instance of the convention it enforces (its ' |
902 | 947 | + 'control is a declaration registry, and its offer-shaped quotes live in comments)', |
|
0 commit comments