* fix(#3426): anchor rule 3 gate tests on heading structure not token scan * chore(#3426): add changeset * chore(#3426): link changeset fragment to pr 3489 --------- Co-authored-by: sim <sim@local>
This commit is contained in:
5
.changeset/nimble-wasps-jump.md
Normal file
5
.changeset/nimble-wasps-jump.md
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
---
|
||||||
|
type: Fixed
|
||||||
|
pr: 3489
|
||||||
|
---
|
||||||
|
package-legitimacy-gate tests now locate the executor RULE 3 section by its heading inside the deviation rules block, so unrelated prompt edits can no longer redirect or silently defeat the package-install guardrail assertions
|
||||||
@@ -110,6 +110,28 @@ function extractXmlElement(text, tag) {
|
|||||||
return text.slice(start, end + tag.length + 3);
|
return text.slice(start, end + tag.length + 3);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Structural RULE locator (#3426): anchor on the `**RULE N:**` heading inside
|
||||||
|
// <deviation_rules> — never on incidental word proximity elsewhere in the file,
|
||||||
|
// which let unrelated edits re-anchor the old first-token-scan-hit window.
|
||||||
|
// Returns { heading, lines } for the section spanning the heading up to (not
|
||||||
|
// including) the next `**RULE N:**` heading, or null when <deviation_rules> is
|
||||||
|
// missing or the requested rule has zero or duplicated headings.
|
||||||
|
function extractRuleSection(text, ruleNumber) {
|
||||||
|
const block = extractXmlElement(text, 'deviation_rules');
|
||||||
|
if (!block) return null;
|
||||||
|
const lines = block.split(/\r?\n/);
|
||||||
|
const headingRe = /^\*\*RULE (\d+):/;
|
||||||
|
const headings = lines
|
||||||
|
.map((line, i) => ({ rule: Number((line.match(headingRe) || [])[1]), lineIndex: i }))
|
||||||
|
.filter((h) => Number.isInteger(h.rule));
|
||||||
|
const starts = headings.filter((h) => h.rule === ruleNumber);
|
||||||
|
if (starts.length !== 1) return null;
|
||||||
|
const start = starts[0].lineIndex;
|
||||||
|
const next = headings.find((h) => h.lineIndex > start);
|
||||||
|
const end = next ? next.lineIndex : lines.length;
|
||||||
|
return { heading: lines[start], lines: lines.slice(start, end) };
|
||||||
|
}
|
||||||
|
|
||||||
function normalizeTokens(text) {
|
function normalizeTokens(text) {
|
||||||
return text
|
return text
|
||||||
.toLowerCase()
|
.toLowerCase()
|
||||||
@@ -432,10 +454,10 @@ describe('gsd-executor.md — package installs excluded from RULE 3 auto-fix', (
|
|||||||
});
|
});
|
||||||
|
|
||||||
test('RULE 3 section explicitly excludes package-manager installs', () => {
|
test('RULE 3 section explicitly excludes package-manager installs', () => {
|
||||||
const rule3Line = lineIndexes(model.lines, (line) => hasAllTokens(line, ['rule', '3']))[0];
|
const section = extractRuleSection(model.text, 3);
|
||||||
assert.notEqual(rule3Line, undefined, 'executor must contain RULE 3 section');
|
assert.ok(section, 'executor must contain exactly one RULE 3 section inside <deviation_rules>');
|
||||||
|
|
||||||
const window = model.lines.slice(rule3Line, rule3Line + 35);
|
const window = section.lines;
|
||||||
|
|
||||||
const hasInstallCommands =
|
const hasInstallCommands =
|
||||||
anyLineHasAll(window, ['npm', 'install']) ||
|
anyLineHasAll(window, ['npm', 'install']) ||
|
||||||
@@ -451,10 +473,10 @@ describe('gsd-executor.md — package installs excluded from RULE 3 auto-fix', (
|
|||||||
});
|
});
|
||||||
|
|
||||||
test('failed package installs surface checkpoint:human-verify', () => {
|
test('failed package installs surface checkpoint:human-verify', () => {
|
||||||
const rule3Line = lineIndexes(model.lines, (line) => hasAllTokens(line, ['rule', '3']))[0];
|
const section = extractRuleSection(model.text, 3);
|
||||||
assert.notEqual(rule3Line, undefined, 'executor must contain RULE 3 section');
|
assert.ok(section, 'executor must contain exactly one RULE 3 section inside <deviation_rules>');
|
||||||
|
|
||||||
const window = model.lines.slice(rule3Line, rule3Line + 50);
|
const window = section.lines;
|
||||||
const hasFailureLanguage =
|
const hasFailureLanguage =
|
||||||
anyLineHasAll(window, ['failed', 'install']) ||
|
anyLineHasAll(window, ['failed', 'install']) ||
|
||||||
anyLineHasAll(window, ['install', 'fails']) ||
|
anyLineHasAll(window, ['install', 'fails']) ||
|
||||||
@@ -468,6 +490,66 @@ describe('gsd-executor.md — package installs excluded from RULE 3 auto-fix', (
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// #3426: the RULE 3 tests must anchor on the `**RULE N:**` heading inside
|
||||||
|
// <deviation_rules>, never on incidental word proximity — a decoy line
|
||||||
|
// elsewhere in the file used to silently re-anchor the old token scan
|
||||||
|
// (first line containing both `rule` and `3` tokens), so unrelated edits
|
||||||
|
// could redirect or defeat the guardrail assertions.
|
||||||
|
test('RULE 3 locator anchors on the heading, not word proximity (#3426)', () => {
|
||||||
|
const fixture = [
|
||||||
|
'## Unrelated guidance',
|
||||||
|
'Apply each Rule in order: (1) fix bugs, (2) add tests, (3) verify.',
|
||||||
|
'',
|
||||||
|
'<deviation_rules>',
|
||||||
|
'Deviation Rule 3 applies only after steps (1) and (2) are exhausted.',
|
||||||
|
'',
|
||||||
|
'**RULE 1: Auto-fix bugs**',
|
||||||
|
'',
|
||||||
|
'---',
|
||||||
|
'',
|
||||||
|
'**RULE 3: Auto-fix blocking issues**',
|
||||||
|
'',
|
||||||
|
'**EXCLUDED from RULE 3 — package manager installs:**',
|
||||||
|
'Running `npm install <pkg>` is **NOT** auto-fixable.',
|
||||||
|
'If a referenced package fails to install, return a `checkpoint:human-verify` task.',
|
||||||
|
'',
|
||||||
|
'---',
|
||||||
|
'',
|
||||||
|
'**RULE 4: Ask about architectural changes**',
|
||||||
|
'',
|
||||||
|
'</deviation_rules>',
|
||||||
|
].join('\n');
|
||||||
|
const fixtureLines = fixture.split('\n');
|
||||||
|
|
||||||
|
// Documents the #3390 failure mode: the pre-#3426 token scan anchors on
|
||||||
|
// the decoy numbered list above the real heading (index 1, not 10).
|
||||||
|
const legacyAnchor = lineIndexes(fixtureLines, (line) => hasAllTokens(line, ['rule', '3']))[0];
|
||||||
|
assert.equal(legacyAnchor, 1, 'token-scan locator is expected to hit the decoy line');
|
||||||
|
|
||||||
|
const section = extractRuleSection(fixture, 3);
|
||||||
|
assert.ok(section, 'structural locator must find RULE 3 inside <deviation_rules> despite decoys');
|
||||||
|
assert.match(section.heading, /^\*\*RULE 3:/);
|
||||||
|
assert.equal(
|
||||||
|
section.lines.includes(fixtureLines[1]),
|
||||||
|
false,
|
||||||
|
'structural window must not include the out-of-block decoy line'
|
||||||
|
);
|
||||||
|
assert.ok(
|
||||||
|
anyLineHasAll(section.lines, ['checkpoint:human-verify']),
|
||||||
|
'structural window must cover the real RULE 3 content'
|
||||||
|
);
|
||||||
|
|
||||||
|
// Missing rule (0 heading matches) and duplicated rule (2 matches) must
|
||||||
|
// both fail loudly rather than anchor on a guess.
|
||||||
|
assert.equal(extractRuleSection(fixture, 2), null, 'missing rule must return null');
|
||||||
|
assert.equal(
|
||||||
|
extractRuleSection(fixture.replace('</deviation_rules>', '**RULE 3: Duplicate**\n\n</deviation_rules>'), 3),
|
||||||
|
null,
|
||||||
|
'duplicated rule heading must return null'
|
||||||
|
);
|
||||||
|
assert.equal(extractRuleSection(fixture.replace('<deviation_rules>', '<other>'), 3), null, 'missing <deviation_rules> must return null');
|
||||||
|
});
|
||||||
|
|
||||||
test('auto mode does not auto-approve package-legitimacy checkpoints', () => {
|
test('auto mode does not auto-approve package-legitimacy checkpoints', () => {
|
||||||
const autoModeLine = lineIndexes(model.lines, (line) => hasAllTokens(line, ['auto-mode', 'checkpoint', 'behavior']))[0];
|
const autoModeLine = lineIndexes(model.lines, (line) => hasAllTokens(line, ['auto-mode', 'checkpoint', 'behavior']))[0];
|
||||||
assert.notEqual(autoModeLine, undefined, 'executor must define auto-mode checkpoint behavior');
|
assert.notEqual(autoModeLine, undefined, 'executor must define auto-mode checkpoint behavior');
|
||||||
|
|||||||
Reference in New Issue
Block a user