mirror of
https://github.com/pbakaus/impeccable.git
synced 2026-09-20 10:06:54 +03:00
Rebase reconciliation: fold main's post-freeze work into the swapped tree
The rebase onto origin/main brought changes whose JS engine halves left the tree with the swap. This commit reconciles what survives: - Suite map: register main's comp-fidelity unit tests (build-phase, comp-diff, font-match, hero-checks) in the core suite and live-browser-ignores in the live suite. - Payload guard: the skill scripts payload now allowlists the comp-fidelity build pipeline (comp-spec/comp-diff/build-phase/font-match and their libs), the one Node toolchain that has not moved into the engine. - Drop skill/scripts/live/project-ignores.mjs, lib/live-path-globs.mjs, and their test: they import hook-lib/live-inject/impeccable-paths, which the swap deleted, and their consumer (the JS live server) is the engine now. - skill text: the comp pipeline's calls to engine verbs (generate-image, embed-prompt) use the launcher spelling. - Oracle: re-record 17 detect goldens over the fixture set main changed (oklch #592, color-mix #578, 1D grid #615, the two comp-fidelity rules) and record the gap in DELTAS.md; those JS rule changes are not yet ported to the engine, and the goldens pin its current behavior. bun run test (oracle included) and bun run build are green on this tree. AI-assisted change: implemented with Claude Code. Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WaJv2c4oN8wS7Ttq4XRqyx
This commit is contained in:
+19
-2
@@ -393,8 +393,25 @@ describe('skill scripts payload', () => {
|
||||
}
|
||||
});
|
||||
|
||||
test('ships no Node entry points and no bundled detector', () => {
|
||||
expect([...names].filter((n) => n.endsWith('.mjs') || n.startsWith('detector/') || n.startsWith('lib/'))).toEqual([]);
|
||||
test('ships no engine entry points and no bundled detector', () => {
|
||||
// The engine verbs live in the binary; the only Node scripts allowed in
|
||||
// the payload are the comp-fidelity build pipeline and its libs, which
|
||||
// have not moved into the engine yet.
|
||||
const allowedNodeScripts = new Set([
|
||||
'build-phase.mjs',
|
||||
'comp-diff.mjs',
|
||||
'comp-spec.mjs',
|
||||
'font-match.mjs',
|
||||
'lib/font-fingerprint.mjs',
|
||||
'lib/font-index.mjs',
|
||||
'lib/hero-checks.mjs',
|
||||
'lib/image-metrics.mjs',
|
||||
'lib/png.mjs',
|
||||
'lib/raster.mjs',
|
||||
]);
|
||||
const stray = [...names].filter((n) =>
|
||||
(n.endsWith('.mjs') || n.startsWith('detector/') || n.startsWith('lib/')) && !allowedNodeScripts.has(n));
|
||||
expect(stray).toEqual([]);
|
||||
});
|
||||
|
||||
test('never reads platform binaries as source', () => {
|
||||
|
||||
@@ -1,145 +0,0 @@
|
||||
import { describe, it, 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 { fileURLToPath } from 'node:url';
|
||||
import { collectProjectDetectorIgnores } from '../skill/scripts/live/project-ignores.mjs';
|
||||
|
||||
const REPO_ROOT = path.join(path.dirname(fileURLToPath(import.meta.url)), '..');
|
||||
const SCRIPTS_DIR = path.join(REPO_ROOT, 'skill', 'scripts');
|
||||
|
||||
const tempDirs = [];
|
||||
function makeTemp() {
|
||||
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'impeccable-project-ignores-'));
|
||||
tempDirs.push(dir);
|
||||
return dir;
|
||||
}
|
||||
after(() => {
|
||||
for (const dir of tempDirs) {
|
||||
try { fs.rmSync(dir, { recursive: true, force: true }); } catch { /* best effort */ }
|
||||
}
|
||||
});
|
||||
|
||||
function write(root, rel, content) {
|
||||
const filePath = path.join(root, rel);
|
||||
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
||||
fs.writeFileSync(filePath, content);
|
||||
}
|
||||
|
||||
function writeDetectorConfig(root, detector) {
|
||||
write(root, '.impeccable/config.json', JSON.stringify({ detector }, null, 2));
|
||||
}
|
||||
|
||||
function writeLiveConfig(root, files) {
|
||||
write(root, '.impeccable/live/config.json', JSON.stringify({
|
||||
files,
|
||||
insertBefore: '</body>',
|
||||
commentSyntax: 'html',
|
||||
}, null, 2));
|
||||
}
|
||||
|
||||
describe('collectProjectDetectorIgnores', () => {
|
||||
it('collects waivers, roots, and pageFiles from a single-root project', () => {
|
||||
const app = makeTemp();
|
||||
write(app, 'package.json', '{"name":"single","private":true}\n');
|
||||
writeDetectorConfig(app, {
|
||||
ignoreRules: ['ai-color-palette'],
|
||||
ignoreFiles: ['prototype/legacy/**'],
|
||||
ignoreValues: [
|
||||
{ rule: 'gradient-text', value: '*', files: ['prototype/library/**'], reason: 'stays local' },
|
||||
],
|
||||
});
|
||||
writeLiveConfig(app, ['prototype/index.html', 'prototype/library/buttons.html']);
|
||||
write(app, 'prototype/index.html', '<html></html>');
|
||||
write(app, 'prototype/library/buttons.html', '<html></html>');
|
||||
|
||||
const out = collectProjectDetectorIgnores({ appRoot: app, scriptsDir: SCRIPTS_DIR });
|
||||
assert.deepEqual(out.ignoreRules, ['ai-color-palette']);
|
||||
assert.deepEqual(out.ignoreFiles, ['prototype/legacy/**']);
|
||||
// createdAt/reason stay local; only rule/value/files ride to the browser.
|
||||
assert.deepEqual(out.ignoreValues, [
|
||||
{ rule: 'gradient-text', value: '*', files: ['prototype/library/**'] },
|
||||
]);
|
||||
assert.deepEqual(out.roots.sort(), ['prototype/', 'prototype/library/']);
|
||||
assert.deepEqual(out.pageFiles.sort(), ['prototype/index.html', 'prototype/library/buttons.html']);
|
||||
});
|
||||
|
||||
it('reads waivers keyed at the repo root, where the hook and the CLI put them', () => {
|
||||
// The monorepo shape from the PR #645 review: the live server chdirs
|
||||
// onto the child appRoot, while resolveCacheCwd keys the hook's config
|
||||
// at the session cwd, which is the repo root.
|
||||
const repo = makeTemp();
|
||||
const app = path.join(repo, 'site');
|
||||
fs.mkdirSync(path.join(repo, '.git'), { recursive: true });
|
||||
write(app, 'package.json', '{"name":"site","private":true}\n');
|
||||
writeDetectorConfig(repo, {
|
||||
ignoreRules: ['ai-color-palette'],
|
||||
ignoreValues: [{ rule: 'overused-font', value: 'space grotesk' }],
|
||||
});
|
||||
writeLiveConfig(app, ['prototype/index.html']);
|
||||
write(app, 'prototype/index.html', '<html></html>');
|
||||
|
||||
const out = collectProjectDetectorIgnores({ appRoot: app, repoRoot: repo, scriptsDir: SCRIPTS_DIR });
|
||||
assert.deepEqual(out.ignoreRules, ['ai-color-palette']);
|
||||
assert.deepEqual(out.ignoreValues, [{ rule: 'overused-font', value: 'space grotesk' }]);
|
||||
// Identities serialize repo-relative so waivers spelled from either root
|
||||
// match through the resolver's suffix expansion.
|
||||
assert.deepEqual(out.roots, ['site/prototype/']);
|
||||
assert.deepEqual(out.pageFiles, ['site/prototype/index.html']);
|
||||
});
|
||||
|
||||
it('unions configs across roots and dedupes identical value entries', () => {
|
||||
const repo = makeTemp();
|
||||
const app = path.join(repo, 'site');
|
||||
fs.mkdirSync(path.join(repo, '.git'), { recursive: true });
|
||||
write(app, 'package.json', '{"name":"site","private":true}\n');
|
||||
writeDetectorConfig(repo, {
|
||||
ignoreRules: ['ai-color-palette'],
|
||||
ignoreValues: [{ rule: 'overused-font', value: 'space grotesk' }],
|
||||
});
|
||||
writeDetectorConfig(app, {
|
||||
ignoreRules: ['gradient-text', 'ai-color-palette'],
|
||||
ignoreValues: [{ rule: 'overused-font', value: 'space grotesk' }],
|
||||
});
|
||||
writeLiveConfig(app, ['prototype/index.html']);
|
||||
write(app, 'prototype/index.html', '<html></html>');
|
||||
|
||||
const out = collectProjectDetectorIgnores({ appRoot: app, repoRoot: repo, scriptsDir: SCRIPTS_DIR });
|
||||
assert.deepEqual(out.ignoreRules.sort(), ['ai-color-palette', 'gradient-text']);
|
||||
assert.deepEqual(out.ignoreValues, [{ rule: 'overused-font', value: 'space grotesk' }]);
|
||||
});
|
||||
|
||||
it('expands glob file entries to existing files and drops missing literals', () => {
|
||||
const app = makeTemp();
|
||||
write(app, 'package.json', '{"name":"globs","private":true}\n');
|
||||
writeLiveConfig(app, ['prototype/**/*.html', 'prototype/not-created-yet.html']);
|
||||
write(app, 'prototype/index.html', '<html></html>');
|
||||
write(app, 'prototype/library/buttons.html', '<html></html>');
|
||||
|
||||
const out = collectProjectDetectorIgnores({ appRoot: app, scriptsDir: SCRIPTS_DIR });
|
||||
assert.deepEqual(out.pageFiles.sort(), ['prototype/index.html', 'prototype/library/buttons.html']);
|
||||
assert.deepEqual(out.roots.sort(), ['prototype/']);
|
||||
});
|
||||
|
||||
it('degrades to empty arrays when nothing is configured', () => {
|
||||
const app = makeTemp();
|
||||
write(app, 'package.json', '{"name":"bare","private":true}\n');
|
||||
const out = collectProjectDetectorIgnores({ appRoot: app, scriptsDir: SCRIPTS_DIR });
|
||||
assert.deepEqual(out, { ignoreRules: [], ignoreValues: [], ignoreFiles: [], roots: [], pageFiles: [] });
|
||||
});
|
||||
|
||||
it('survives a malformed detector config without throwing', () => {
|
||||
const app = makeTemp();
|
||||
write(app, 'package.json', '{"name":"broken","private":true}\n');
|
||||
write(app, '.impeccable/config.json', '{"detector":{"ignoreRules":"foo","ignoreValues":[null,7],"ignoreFiles":{}}}');
|
||||
writeLiveConfig(app, ['prototype/index.html']);
|
||||
write(app, 'prototype/index.html', '<html></html>');
|
||||
|
||||
const out = collectProjectDetectorIgnores({ appRoot: app, scriptsDir: SCRIPTS_DIR });
|
||||
assert.deepEqual(out.ignoreRules, []);
|
||||
assert.deepEqual(out.ignoreValues, []);
|
||||
assert.deepEqual(out.ignoreFiles, []);
|
||||
assert.deepEqual(out.pageFiles, ['prototype/index.html']);
|
||||
});
|
||||
});
|
||||
@@ -33,3 +33,18 @@ origin but not `'wasm-unsafe-eval'` still refuses to compile it. The JS
|
||||
`patchCspMeta` predates the wasm bundle and appended only the origin.
|
||||
|
||||
- `live-inject-csp-meta-no-connect-src`: the patched `<meta http-equiv="Content-Security-Policy">` reads `script-src 'self' http://localhost:8412 'wasm-unsafe-eval'` (was `script-src 'self' http://localhost:8412`). The `data-impeccable-csp-original` marker, the `connect-src` and `img-src` additions, idempotence, and the revert on unpatch are unchanged. `live-inject-vite-csp-meta` and `live-inject-next-jsx` carry meta tags the patch does not touch, so their goldens did not move.
|
||||
|
||||
## Recorded 2026-08-31: main's post-freeze fixture changes, goldens re-recorded from the engine
|
||||
|
||||
The rebase onto main brought fixture updates whose paired JS rule changes have
|
||||
not been ported to the engine yet. The detect goldens below are re-recorded
|
||||
from the binary, so they pin the engine's current behavior on the new fixture
|
||||
content; the entries name the upstream JS change each one still owes. Until a
|
||||
rule ships in the engine and its golden is re-recorded, the golden is the pin
|
||||
of the gap, not an endorsement of it.
|
||||
|
||||
- `detect-fixture-json-color-html`, `detect-fixture-text-color-html`: the fixture gained the color-mix nested-hex column (upstream 54440319, #578, with explicit sizes from 7426af44); the engine still reads hex codes inside `color-mix(...)` when measuring gradient contrast, so its readings on the reshaped fixture differ from the JS engine's.
|
||||
- `detect-fixture-json-oklch-neon-text-html`, `detect-fixture-text-oklch-neon-text-html`: new fixture for oklch parsing in visual-contrast and neon-text (upstream 1b7da15b, #592, columns from 8347d77f); the engine does not parse oklch there yet, so the flag column's neon-text goes unflagged and a mis-read low-contrast is recorded.
|
||||
- `detect-fixture-json-codex-grid-1d-pass-html`, `detect-fixture-text-codex-grid-1d-pass-html`: new pass-case fixture for 1D dashed rules (upstream a236137b/7ddcd533, #615); the engine still flags the 1D line-field as `codex-grid-background`, which is the pre-fix behavior the fixture exists to retire.
|
||||
- `detect-fixture-json-organic-clip-path-html`, `detect-fixture-text-organic-clip-path-html`, `detect-fixture-json-buried-raster-html`, `detect-fixture-text-buried-raster-html`: fixtures for the two comp-fidelity rules (upstream 58561610: organic-clip-path, buried-raster); neither rule exists in the engine, so only incidental findings (or none) are recorded.
|
||||
- `detect-dir-json-all-fixtures`, `detect-dir-text-all-fixtures`, `detect-dir-quiet-all-fixtures`, `detect-scope-type`, `detect-scope-both`, `detect-no-advisory-json`, `detect-no-advisory-text`: directory-wide sweeps over `tests/fixtures/antipatterns/`; re-recorded because the fixture set above grew and changed, shifting counts and orderings.
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"stdout": "",
|
||||
"stderr": "392 anti-patterns found.\n2 advisory notes (not counted).\n",
|
||||
"stderr": "400 anti-patterns found.\n2 advisory notes (not counted).\n",
|
||||
"exit": 2,
|
||||
"signal": null,
|
||||
"files": {}
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"stdout": "[]\n",
|
||||
"stderr": "",
|
||||
"exit": 0,
|
||||
"signal": null,
|
||||
"files": {}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"stdout": "[\n {\n \"antipattern\": \"codex-grid-background\",\n \"name\": \"Decorative grid-line background\",\n \"description\": \"A decorative grid or line-field background drawn with hairline linear-gradient layers tiled by a fixed pixel cell is a recurring generated-UI signature. Reserve grid overlays for actual canvas, map, blueprint, or measurement surfaces; elsewhere use product structure or a plain surface.\",\n \"severity\": \"advisory\",\n \"category\": \"slop\",\n \"file\": \"<REPO>/tests/fixtures/antipatterns/codex-grid-1d-pass.html\",\n \"line\": 0,\n \"snippet\": \"px-tiled hairline line-field background\"\n }\n]\n",
|
||||
"stderr": "",
|
||||
"exit": 2,
|
||||
"signal": null,
|
||||
"files": {}
|
||||
}
|
||||
File diff suppressed because one or more lines are too long
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"stdout": "[\n {\n \"antipattern\": \"low-contrast\",\n \"name\": \"Low contrast text\",\n \"description\": \"Text does not meet WCAG AA contrast requirements (4.5:1 for body, 3:1 for large text). Increase the contrast between text and background.\",\n \"severity\": \"warning\",\n \"category\": \"quality\",\n \"file\": \"<REPO>/tests/fixtures/antipatterns/oklch-neon-text.html\",\n \"line\": 0,\n \"snippet\": \"1.3:1 (need 4.5:1) — text #00f4f6 on #f5f5f5\"\n },\n {\n \"antipattern\": \"flat-type-hierarchy\",\n \"name\": \"Flat type hierarchy\",\n \"description\": \"Font sizes are too close together — no clear visual hierarchy. Use fewer sizes with more contrast (aim for at least a 1.25 ratio between steps).\",\n \"severity\": \"warning\",\n \"category\": \"slop\",\n \"file\": \"<REPO>/tests/fixtures/antipatterns/oklch-neon-text.html\",\n \"line\": 0,\n \"snippet\": \"Sizes: 13px, 16px, 18px (ratio 1.4:1)\"\n }\n]\n",
|
||||
"stderr": "",
|
||||
"exit": 2,
|
||||
"signal": null,
|
||||
"files": {}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"stdout": "[\n {\n \"antipattern\": \"low-contrast\",\n \"name\": \"Low contrast text\",\n \"description\": \"Text does not meet WCAG AA contrast requirements (4.5:1 for body, 3:1 for large text). Increase the contrast between text and background.\",\n \"severity\": \"warning\",\n \"category\": \"quality\",\n \"file\": \"<REPO>/tests/fixtures/antipatterns/organic-clip-path.html\",\n \"line\": 0,\n \"snippet\": \"1.2:1 (need 4.5:1) — text #000000 on #1a1a1a\"\n },\n {\n \"antipattern\": \"low-contrast\",\n \"name\": \"Low contrast text\",\n \"description\": \"Text does not meet WCAG AA contrast requirements (4.5:1 for body, 3:1 for large text). Increase the contrast between text and background.\",\n \"severity\": \"warning\",\n \"category\": \"quality\",\n \"file\": \"<REPO>/tests/fixtures/antipatterns/organic-clip-path.html\",\n \"line\": 0,\n \"snippet\": \"4.1:1 (need 4.5:1) — text #000000 on #cc3333\"\n },\n {\n \"antipattern\": \"low-contrast\",\n \"name\": \"Low contrast text\",\n \"description\": \"Text does not meet WCAG AA contrast requirements (4.5:1 for body, 3:1 for large text). Increase the contrast between text and background.\",\n \"severity\": \"warning\",\n \"category\": \"quality\",\n \"file\": \"<REPO>/tests/fixtures/antipatterns/organic-clip-path.html\",\n \"line\": 0,\n \"snippet\": \"1.3:1 (need 4.5:1) — text #000000 on #222222\"\n },\n {\n \"antipattern\": \"low-contrast\",\n \"name\": \"Low contrast text\",\n \"description\": \"Text does not meet WCAG AA contrast requirements (4.5:1 for body, 3:1 for large text). Increase the contrast between text and background.\",\n \"severity\": \"warning\",\n \"category\": \"quality\",\n \"file\": \"<REPO>/tests/fixtures/antipatterns/organic-clip-path.html\",\n \"line\": 0,\n \"snippet\": \"1.7:1 (need 4.5:1) — text #000000 on #333333\"\n }\n]\n",
|
||||
"stderr": "",
|
||||
"exit": 2,
|
||||
"signal": null,
|
||||
"files": {}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"stdout": "",
|
||||
"stderr": "",
|
||||
"exit": 0,
|
||||
"signal": null,
|
||||
"files": {}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"stdout": "",
|
||||
"stderr": "\n<REPO>/tests/fixtures/antipatterns/codex-grid-1d-pass.html\n [codex-grid-background] px-tiled hairline line-field background\n → A decorative grid or line-field background drawn with hairline linear-gradient layers tiled by a fixed pixel cell is a recurring generated-UI signature. Reserve grid overlays for actual canvas, map, blueprint, or measurement surfaces; elsewhere use product structure or a plain surface.\n\n1 anti-pattern found.\n",
|
||||
"exit": 2,
|
||||
"signal": null,
|
||||
"files": {}
|
||||
}
|
||||
File diff suppressed because one or more lines are too long
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"stdout": "",
|
||||
"stderr": "\n<REPO>/tests/fixtures/antipatterns/oklch-neon-text.html\n [low-contrast] 1.3:1 (need 4.5:1) — text #00f4f6 on #f5f5f5\n → Text does not meet WCAG AA contrast requirements (4.5:1 for body, 3:1 for large text). Increase the contrast between text and background.\n [flat-type-hierarchy] Sizes: 13px, 16px, 18px (ratio 1.4:1)\n → Font sizes are too close together — no clear visual hierarchy. Use fewer sizes with more contrast (aim for at least a 1.25 ratio between steps).\n\n2 anti-patterns found.\n",
|
||||
"exit": 2,
|
||||
"signal": null,
|
||||
"files": {}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"stdout": "",
|
||||
"stderr": "\n<REPO>/tests/fixtures/antipatterns/organic-clip-path.html\n [low-contrast] 1.2:1 (need 4.5:1) — text #000000 on #1a1a1a\n → Text does not meet WCAG AA contrast requirements (4.5:1 for body, 3:1 for large text). Increase the contrast between text and background.\n [low-contrast] 4.1:1 (need 4.5:1) — text #000000 on #cc3333\n → Text does not meet WCAG AA contrast requirements (4.5:1 for body, 3:1 for large text). Increase the contrast between text and background.\n [low-contrast] 1.3:1 (need 4.5:1) — text #000000 on #222222\n → Text does not meet WCAG AA contrast requirements (4.5:1 for body, 3:1 for large text). Increase the contrast between text and background.\n [low-contrast] 1.7:1 (need 4.5:1) — text #000000 on #333333\n → Text does not meet WCAG AA contrast requirements (4.5:1 for body, 3:1 for large text). Increase the contrast between text and background.\n\n4 anti-patterns found.\n",
|
||||
"exit": 2,
|
||||
"signal": null,
|
||||
"files": {}
|
||||
}
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user