next went red on dacae9273 against documentation that was correct.
parseDeclaredCounts read any '<numeral> ... 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 <sim@local>
This commit is contained in:
@@ -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`(?<!\b(?:other|remaining)\s)`;
|
||||
|
||||
/** Every numeric "N dimensions" claim in `text`, in any of the shipped languages.
|
||||
* Returns plain numbers — the typed IR the parity function consumes.
|
||||
*
|
||||
* `excludeBackReferences: false` reproduces the pre-fix behavior. It exists so a test can
|
||||
* prove, against the real shipped files, that the exclusion is load-bearing rather than
|
||||
* decorative — see the dacae9273 regression block. */
|
||||
function parseDeclaredCounts(text, { excludeBackReferences = true } = {}) {
|
||||
const body = lf(text);
|
||||
const found = [];
|
||||
const push = (v) => { 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');
|
||||
|
||||
Reference in New Issue
Block a user