From 0062f6d033c892f64be121cfc93cb4aca10c1c5a Mon Sep 17 00:00:00 2001 From: Tom Boucher Date: Sat, 22 Aug 2026 06:39:48 -0400 Subject: [PATCH] fix(#2845): stop the dimension parity guard counting a back-reference (#3752) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit next went red on dacae9273 against documentation that was correct. parseDeclaredCounts read any ' ... dimensions' collocation as a claim about the gsd-ui-checker dimension TOTAL. The how-to sentence 'Dimension 7 is a rule gsd-ui-checker follows, the same as the other six dimensions' refers to the other members of a seven-member set; the guard counted it as that document declaring six, and reported a count-mismatch on prose that was accurate. A drift guard that fires on correct prose is a false positive, which is how guards end up switched off. A negative lookbehind now excludes a numeral introduced by 'other' or 'remaining'. It is declared once as NOT_A_BACK_REFERENCE and shared by both English scans rather than written at each site — the same two-surface divergence class this suite exists to catch. Both scans apply it case-insensitively; the first cut of this fix had the digit scan case-sensitive and the word scan not, so a sentence-initial 'Other 6 dimensions' still slipped through. The exclusion is word-anchored, so 'another six dimensions' — a real claim about a second set — still counts. parseDeclaredCounts takes an excludeBackReferences opt-out so a test can prove against the REAL shipped how-to that the exclusion is load-bearing: with it off the file reports [6,7], with it on [7]. That replaces a raw substring match on prose, which the suite's own header forbids, with a typed before/after. The docs prose is deliberately unchanged. It is the only instance of the pattern in the tree, so keeping it means the real-tree assertion exercises the path this fix exists for instead of asserting only on fixtures. Regression tests cover both polarities: eight back-reference shapes including all four sentence-initial cases, five real count claims that must still count, the 'another' word-boundary case, and the shipped how-to itself. Known limits recorded in the code: the exclusion is English-only, because translated docs here are corrected to match English rather than authored, so there is no instance to model the grammar on; and it cannot distinguish a back-reference from a genuine total opening with the same word ('Other 6 dimensions were added'), where the false negative is the safer side of the trade. Why it reached next at all: the docs PR (#3746) was green. A doc-only diff inert-skips the test matrix in the PR lane, so the guard that reads docs never ran against the docs change that broke it — it fired on push to next, after merge. Co-authored-by: sim --- tests/ui-spec-inventory-provenance.test.cjs | 93 +++++++++++++++++++-- 1 file changed, 88 insertions(+), 5 deletions(-) diff --git a/tests/ui-spec-inventory-provenance.test.cjs b/tests/ui-spec-inventory-provenance.test.cjs index e934a4f43..fe0d46919 100644 --- a/tests/ui-spec-inventory-provenance.test.cjs +++ b/tests/ui-spec-inventory-provenance.test.cjs @@ -137,18 +137,44 @@ const WORD_NUMERAL = Object.freeze({ 여섯: 6, 일곱: 7, 여덟: 8, }); -/** Every numeric "N dimensions" claim in `text`, in any of the three shipped languages. - * Returns plain numbers — the typed IR the parity function consumes. */ -function parseDeclaredCounts(text) { +/** A numeral introduced by "other" or "remaining" is a BACK-REFERENCE to the rest of a set + * — "the same as the other six dimensions" — never a claim about the set's SIZE. Counting + * one turned `next` red on dacae9273 against entirely correct documentation, and a drift + * guard that fires on accurate prose is a false positive, which is how guards end up + * disabled. + * + * Declared once and shared by both English scans so the two can never drift apart — the + * same divergence class this whole suite exists to catch. `\b` anchors it to the whole + * word, so "another six dimensions" (a real claim about a second set) still counts, and + * both scans apply it case-insensitively: a sentence-initial "Other six dimensions…" is + * the same back-reference as a mid-sentence one. + * + * Known limits: the exclusion is English-only — the ja/zh/ko/pt patterns have no + * equivalent, because translated docs here are CORRECTED to match English rather than + * authored, so there is no instance to model the grammar on. And it cannot tell a + * back-reference from a genuine total that happens to open with the same word ("Other 6 + * dimensions were added"); prose alone does not disambiguate those, and the false-negative + * is the safer side of that trade for a guard whose failure mode is being switched off. */ +const NOT_A_BACK_REFERENCE = String.raw`(? { if (Number.isInteger(v)) found.push(v); }; const scan = (re, take) => { for (const m of body.matchAll(re)) take(m); }; + const guard = excludeBackReferences ? NOT_A_BACK_REFERENCE : ''; // "6 dimensions", "6 design quality dimensions", "6 Validation Dimensions" - scan(/(\d+)\s+(?:[A-Za-z][A-Za-z-]*\s+){0,3}?[Dd]imensions?\b/g, (m) => push(Number(m[1]))); + scan(new RegExp(`${guard}(\\d+)\\s+(?:[A-Za-z][A-Za-z-]*\\s+){0,3}?[Dd]imensions?\\b`, 'gi'), + (m) => push(Number(m[1]))); // "six dimensions", "six quality dimensions" - scan(/\b(five|six|seven|eight|nine|ten)\s+(?:[A-Za-z][A-Za-z-]*\s+){0,3}?dimensions\b/gi, + scan(new RegExp(`${guard}\\b(five|six|seven|eight|nine|ten)\\s+(?:[A-Za-z][A-Za-z-]*\\s+){0,3}?dimensions\\b`, 'gi'), (m) => push(WORD_NUMERAL[m[1].toLowerCase()])); // "Dimensions: 6/6 passed" scan(/Dimensions:\s*(\d+)\/(\d+)/g, (m) => { push(Number(m[1])); push(Number(m[2])); }); @@ -455,6 +481,63 @@ describe('#2845 — parsers are total and newline-agnostic', () => { }); }); +describe('#2845 — a back-reference is not a count claim (regression: `next` red on dacae9273)', () => { + // The how-to gained "Dimension 7 is a rule gsd-ui-checker follows, the same as the + // other six dimensions" — correct prose — and the guard read "six dimensions" as that + // document claiming the checker has six. `next` went red on documentation that was + // right. + // + // Worth recording WHY it reached `next`: the docs PR was green. A doc-only diff + // inert-skips the test matrix in the PR lane, so the guard that reads docs never ran + // against the docs change that broke it. It fired on push to `next` — after merge. + + test('"the other N dimensions" / "the remaining N dimensions" are not counted', () => { + for (const phrase of [ + 'Dimension 7 is a rule the checker follows, the same as the other six dimensions.', + 'the same as the other 6 dimensions', + 'the remaining six dimensions are unchanged', + 'the remaining 6 dimensions are unchanged', + // Sentence-initial: the digit scan was case-SENSITIVE while the word scan was not, + // so these two slipped through the first version of this fix. + 'Other six dimensions apply.', + 'Other 6 dimensions apply.', + 'Remaining six dimensions are unaffected.', + 'Remaining 6 dimensions are unaffected.', + ]) { + assert.deepEqual(parseDeclaredCounts(phrase), [], `counted a back-reference: ${phrase}`); + } + }); + + test('a real count claim is still counted — the fix must not blind the guard', () => { + assert.deepEqual(parseDeclaredCounts('validates the spec across six dimensions'), [6]); + assert.deepEqual(parseDeclaredCounts('System MUST validate against 7 dimensions'), [7]); + assert.deepEqual(parseDeclaredCounts('All 7 dimensions evaluated'), [7]); + assert.deepEqual(parseDeclaredCounts('**7 Validation Dimensions:**'), [7]); + assert.deepEqual(parseDeclaredCounts('gsd-ui-checker seven quality dimensions'), [7]); + }); + + test('the exclusion is anchored to the whole word, not a substring', () => { + // "another" contains "other" but has no word boundary before it, so a genuine claim + // about a second set must survive. + assert.deepEqual(parseDeclaredCounts('another six dimensions'), [6]); + }); + + test('the exclusion is load-bearing on the real shipped how-to, not just on fixtures', () => { + // Typed proof rather than a substring match on prose: run the matcher over the real + // file with the exclusion OFF and then ON. Off, it must report a 6 — that 6 is the + // back-reference, and is literally what turned `next` red. On, only 7s survive. + // If the docs prose is ever reworded away, the first assertion fails loudly rather + // than this guard quietly ceasing to exercise the path it exists for. + const howto = readShipped(SURFACE.HOWTO); + const withoutExclusion = [...new Set(parseDeclaredCounts(howto, { excludeBackReferences: false }))].sort(); + const withExclusion = [...new Set(parseDeclaredCounts(howto))].sort(); + + assert.deepEqual(withoutExclusion, [6, 7], + 'vacuous unless the how-to still carries the back-reference this guard exists for'); + assert.deepEqual(withExclusion, [7]); + }); +}); + describe('#2845 — Dimension 7 contract text', () => { const dim7 = () => sectionContaining(checker, `## Dimension 7: ${DIMENSION_7_LABEL}`); const dim6 = () => sectionContaining(checker, '## Dimension 6: Registry Safety');