mirror of
https://github.com/pbakaus/impeccable.git
synced 2026-09-19 17:46:36 +03:00
Resolve detect DESIGN.md from each target's project, not cwd
The detect CLI loaded DESIGN.md once from process.cwd() and applied it to every scan target. Scanning another project's files from inside a different repo therefore judged them against the wrong project's design system (cross-project contamination observed during eval work: running detect from impeccable-evals against a generated artifact elsewhere applied the evals repo's DESIGN.md). DESIGN.md now resolves by walking up from each scan target's own location to its design root: a directory carrying a DESIGN.md is the root; a directory carrying a project marker (.git / package.json / .impeccable) without a DESIGN.md is a boundary that stops the walk with no design system, so a sibling project never inherits a parent's or cwd's rules. A target with no design root above it falls back to no design system rather than cwd's. Resolution is memoized per root, so a multi-file scan reads each DESIGN.md once, and targets spanning projects each get their own. file:// URLs resolve from their path; remote http(s) URLs get no design system. Adds tests/detect-cli-design-contamination.test.mjs, which spawns the real CLI to prove B's file is not judged by A's DESIGN.md, that a project still governs its own file, that a mixed-project scan resolves per target, and that a marker-less bare file gets no design system. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Fable 5
parent
9f5bbed8b8
commit
70fdc172b8
@@ -0,0 +1,132 @@
|
||||
/**
|
||||
* Regression: `impeccable detect <file-or-dir>` must resolve DESIGN.md from
|
||||
* EACH scan target's own project root, not from process.cwd().
|
||||
*
|
||||
* The bug (found during eval work): running detect from repo A against a file
|
||||
* that lives in repo B applied A's DESIGN.md to B — cross-project contamination.
|
||||
* These tests spawn the real CLI so the fix is exercised end to end.
|
||||
*
|
||||
* Run with: node --test tests/detect-cli-design-contamination.test.mjs
|
||||
*/
|
||||
|
||||
import { describe, it, before, after } from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import fs from 'node:fs';
|
||||
import os from 'node:os';
|
||||
import path from 'node:path';
|
||||
import { spawnSync } from 'node:child_process';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
|
||||
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
||||
const CLI = path.resolve(__dirname, '../cli/bin/cli.js');
|
||||
|
||||
// Verdana is a plain web-safe font: it is not in OVERUSED_FONTS and trips no
|
||||
// standalone rule, so the only way it becomes a `design-system-font` finding is
|
||||
// if a DESIGN.md that forbids it gets applied.
|
||||
const PAGE_HTML =
|
||||
'<!doctype html><html><head><style>.card { font-family: Verdana, sans-serif; }</style></head>' +
|
||||
'<body><div class="card">Hi</div></body></html>';
|
||||
|
||||
// A DESIGN.md whose typography allows only Palatino — Verdana violates it.
|
||||
const DESIGN_MD = `---
|
||||
typography:
|
||||
body:
|
||||
fontFamily: "Palatino, Georgia, serif"
|
||||
---
|
||||
# Project A Design System
|
||||
`;
|
||||
|
||||
const tempRoots = [];
|
||||
|
||||
function mkProject({ withDesign, withMarker = true }) {
|
||||
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'impeccable-detect-contam-'));
|
||||
tempRoots.push(dir);
|
||||
if (withMarker) fs.writeFileSync(path.join(dir, 'package.json'), '{"name":"fixture"}');
|
||||
if (withDesign) fs.writeFileSync(path.join(dir, 'DESIGN.md'), DESIGN_MD);
|
||||
const page = path.join(dir, 'page.html');
|
||||
fs.writeFileSync(page, PAGE_HTML);
|
||||
return { dir, page };
|
||||
}
|
||||
|
||||
// Run the CLI from `cwd`; force the node binary so the HTML/jsdom path never
|
||||
// runs under bun (which is unusably slow).
|
||||
function runDetect(cwd, targets) {
|
||||
const result = spawnSync(process.execPath, [CLI, 'detect', '--json', ...targets], {
|
||||
cwd,
|
||||
encoding: 'utf-8',
|
||||
});
|
||||
let findings = [];
|
||||
try {
|
||||
findings = JSON.parse(result.stdout || '[]');
|
||||
} catch {
|
||||
throw new Error(`Non-JSON CLI output.\nstdout: ${result.stdout}\nstderr: ${result.stderr}`);
|
||||
}
|
||||
return findings;
|
||||
}
|
||||
|
||||
function fontFindingsFor(findings, file) {
|
||||
return findings.filter(
|
||||
(f) => f.antipattern === 'design-system-font' && (!file || f.file === file),
|
||||
);
|
||||
}
|
||||
|
||||
let projA;
|
||||
let projB;
|
||||
|
||||
before(() => {
|
||||
projA = mkProject({ withDesign: true }); // DESIGN.md forbids Verdana
|
||||
projB = mkProject({ withDesign: false }); // its own project, no DESIGN.md
|
||||
});
|
||||
|
||||
after(() => {
|
||||
for (const dir of tempRoots) {
|
||||
try { fs.rmSync(dir, { recursive: true, force: true }); } catch { /* best effort */ }
|
||||
}
|
||||
});
|
||||
|
||||
describe('detect CLI DESIGN.md resolution', () => {
|
||||
it('does NOT apply cwd project A\'s DESIGN.md to project B\'s file (the contamination bug)', () => {
|
||||
const findings = runDetect(projA.dir, [projB.page]);
|
||||
assert.deepEqual(
|
||||
fontFindingsFor(findings, projB.page).map((f) => f.ignoreValue),
|
||||
[],
|
||||
'project B\'s Verdana must not be flagged by project A\'s DESIGN.md',
|
||||
);
|
||||
});
|
||||
|
||||
it('still applies a project\'s own DESIGN.md to its own file (positive control)', () => {
|
||||
const findings = runDetect(projA.dir, [projA.page]);
|
||||
assert.ok(
|
||||
fontFindingsFor(findings, projA.page).some((f) => f.ignoreValue === 'verdana'),
|
||||
'project A\'s own DESIGN.md must flag Verdana in project A\'s file',
|
||||
);
|
||||
});
|
||||
|
||||
it('resolves per target when one scan spans two projects', () => {
|
||||
const findings = runDetect(projA.dir, [projA.page, projB.page]);
|
||||
assert.ok(
|
||||
fontFindingsFor(findings, projA.page).length > 0,
|
||||
'A\'s file should be judged against A\'s DESIGN.md',
|
||||
);
|
||||
assert.equal(
|
||||
fontFindingsFor(findings, projB.page).length,
|
||||
0,
|
||||
'B\'s file should NOT be judged against A\'s DESIGN.md',
|
||||
);
|
||||
});
|
||||
|
||||
it('falls back to no design system for a bare file with no project markers above it', () => {
|
||||
// A lone file whose directory has neither .git, package.json, nor .impeccable.
|
||||
const bareDir = fs.mkdtempSync(path.join(os.tmpdir(), 'impeccable-detect-bare-'));
|
||||
tempRoots.push(bareDir);
|
||||
const barePage = path.join(bareDir, 'page.html');
|
||||
fs.writeFileSync(barePage, PAGE_HTML);
|
||||
|
||||
const findings = runDetect(projA.dir, [barePage]);
|
||||
assert.equal(
|
||||
fontFindingsFor(findings, barePage).length,
|
||||
0,
|
||||
'a project-less file must fall back to no design system, not cwd\'s',
|
||||
);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user