fix(#3312): require static frontend evidence before ui-plan-gate blocks (#3451)

* fix(#3312): require static frontend evidence before ui-plan-gate blocks

* fix(#3312): fill changeset pr reference

* fix(#3312): align regression assertions with section heading and legacy artifact shape

---------

Co-authored-by: sim <sim@local>
This commit is contained in:
Tom Boucher
2026-08-14 09:27:48 -04:00
committed by GitHub
parent 43475a2e0f
commit 29c27e948b
11 changed files with 457 additions and 23 deletions

View File

@@ -295,19 +295,71 @@ describe('check ui.plan-gate — frontend phase, no UI-SPEC', () => {
].join('\n'),
'utf8',
);
// Static frontend evidence (#3312): block requires token match AND structural
// corroboration — this fixture is a real frontend repo.
fs.mkdirSync(path.join(tmpDir, 'src'), { recursive: true });
fs.writeFileSync(path.join(tmpDir, 'src', 'App.tsx'), 'export function App() { return null; }\n', 'utf8');
// No UI-SPEC.md in phase dir
});
after(() => cleanup(tmpDir));
test('[happy] frontend phase + no UI-SPEC: block:true, frontend:true, hasUiSpec:false, exit 0', () => {
test('[happy] frontend phase + evidence + no UI-SPEC: block:true, frontend:true, hasUiSpec:false, exit 0', () => {
const result = runTools(['check', 'ui.plan-gate', '1', '--raw'], tmpDir);
assert.strictEqual(result.status, 0, `exit non-zero. stderr=${result.stderr?.slice(0, 300)}`);
const out = parseEnvelope(result, 'ui-plan-gate-no-spec');
assert.strictEqual(out.frontend, true, 'frontend must be true for frontend-keyword phase');
assert.strictEqual(out.hasFrontendEvidence, true, 'src/App.tsx is frontend evidence');
assert.strictEqual(out.hasUiSpec, false, 'hasUiSpec must be false when no spec file exists');
assert.strictEqual(out.block, true, 'block must be true (frontend && !hasUiSpec)');
assert.strictEqual(out.block, true, 'block must be true (frontend && hasFrontendEvidence && !hasUiSpec)');
assert.strictEqual(out.uiSpecPath, null, 'uiSpecPath must be null when spec absent');
// matchedToken is the FIRST match in the phase section — the "## Phase 1:
// Dashboard" heading precedes the body, so the first match is `dashboard`.
assert.strictEqual(out.matchedToken, 'dashboard', 'matchedToken surfaces what tripped the sniffer');
});
});
// ─── 6b. check ui.plan-gate: #3312 — token match without frontend evidence ────
describe('check ui.plan-gate — #3312 proper-noun token match, no frontend evidence', () => {
let tmpDir;
before(() => {
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'plan-pre-ui-gate-3312-'));
const planningDir = path.join(tmpDir, '.planning');
fs.mkdirSync(path.join(planningDir, 'phases', '01-infra'), { recursive: true });
fs.writeFileSync(path.join(planningDir, 'config.json'), '{}', 'utf8');
fs.writeFileSync(
path.join(planningDir, 'ROADMAP.md'),
[
'# Project Roadmap',
'',
'## Phase 1: Infra',
'',
'Fix broken references in dashboard-financeiro, update .env.tpl and CI workflows.',
'',
].join('\n'),
'utf8',
);
// Markdown/bash/config repo by design: no src/, no package.json, no component files.
});
after(() => cleanup(tmpDir));
test('[regression] dashboard-financeiro mention in non-frontend repo: frontend:true but block:false', () => {
const result = runTools(['check', 'ui.plan-gate', '1', '--raw'], tmpDir);
assert.strictEqual(result.status, 0, `exit non-zero. stderr=${result.stderr?.slice(0, 300)}`);
const out = parseEnvelope(result, 'ui-plan-gate-3312');
assert.strictEqual(out.frontend, true,
'sniffer still matches `dashboard` at the hyphen boundary (micro-frontend rule)');
assert.strictEqual(out.hasFrontendEvidence, false,
'markdown/bash repo has no component files and no UI-framework dependency');
assert.strictEqual(out.block, false,
'#3312: token match without structural evidence must NOT block planning');
assert.strictEqual(out.matchedToken, 'dashboard', 'matchedToken surfaces the proper-noun trigger');
assert.ok(
typeof out.matchedLine === 'string' && out.matchedLine.includes('dashboard-financeiro'),
'matchedLine surfaces the line for one-second operator triage',
);
});
});