mirror of
https://github.com/pbakaus/impeccable.git
synced 2026-09-12 22:26:38 +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:
@@ -1,37 +0,0 @@
|
||||
/**
|
||||
* Convert a live-config glob pattern to a RegExp.
|
||||
*
|
||||
* Supports `**` across path segments, `*` within one segment, and `?` for one
|
||||
* character. Callers normalize project-relative paths to forward slashes.
|
||||
*/
|
||||
export function livePathGlobToRegex(pattern) {
|
||||
let re = '';
|
||||
let i = 0;
|
||||
while (i < pattern.length) {
|
||||
const c = pattern[i];
|
||||
if (c === '*') {
|
||||
if (pattern[i + 1] === '*') {
|
||||
if (pattern[i + 2] === '/') {
|
||||
re += '(?:.*/)?';
|
||||
i += 3;
|
||||
} else {
|
||||
re += '.*';
|
||||
i += 2;
|
||||
}
|
||||
} else {
|
||||
re += '[^/]*';
|
||||
i += 1;
|
||||
}
|
||||
} else if (c === '?') {
|
||||
re += '[^/]';
|
||||
i += 1;
|
||||
} else if (/[.+^${}()|[\]\\]/.test(c)) {
|
||||
re += `\\${c}`;
|
||||
i += 1;
|
||||
} else {
|
||||
re += c;
|
||||
i += 1;
|
||||
}
|
||||
}
|
||||
return new RegExp(`^${re}$`);
|
||||
}
|
||||
@@ -1,139 +0,0 @@
|
||||
/**
|
||||
* Project detector waivers for the live overlay (issue #639, hardened in the
|
||||
* PR #645 follow-up). One place decides what the /live.js prelude serializes
|
||||
* as window.__IMPECCABLE_PROJECT_IGNORES__:
|
||||
*
|
||||
* ignoreRules detector.ignoreRules, unioned across every live root.
|
||||
* ignoreValues detector.ignoreValues entries ({rule, value, files?}),
|
||||
* deduped across roots; createdAt/reason stay local.
|
||||
* ignoreFiles detector.ignoreFiles globs, unioned across roots, so a
|
||||
* wholly waived page scans to zero findings in the overlay
|
||||
* just as it reports nothing through the CLI and the hook.
|
||||
* roots served-root prefixes derived from the inject config's own
|
||||
* `files` globs. Never derived from the ignore globs: one
|
||||
* entry scoped to prototype/library/** would lend
|
||||
* prototype/library/ as a candidate prefix to every page,
|
||||
* and that rule would suppress site-wide (issue #639).
|
||||
* pageFiles the inject config's `files` expanded to real project
|
||||
* files, so the browser can resolve a URL to the one file it
|
||||
* actually serves instead of trying every root (PR #645
|
||||
* review: with src/ and public/ both served, /foo.html must
|
||||
* not borrow src/foo.html's waivers while actually serving
|
||||
* public/foo.html).
|
||||
*
|
||||
* Config is read from every root the live session spans: the appRoot the
|
||||
* server chdir'd onto, plus contextRoot and repoRoot when they differ. The
|
||||
* edit hook keys the same config at the session cwd (the repo root in a
|
||||
* monorepo, via resolveCacheCwd), and `impeccable detect` reads it from its
|
||||
* invocation cwd, so reading only the appRoot silently dropped every waiver
|
||||
* in exactly the monorepo layouts the roots manifest exists for. Reading is
|
||||
* additive across roots, matching readConfig's own union of config.json and
|
||||
* config.local.json.
|
||||
*
|
||||
* In a monorepo, roots and pageFiles are serialized repo-relative (the
|
||||
* appRoot's path inside the repo is prefixed), so waivers spelled from
|
||||
* either root match through the resolver's suffix expansion.
|
||||
*/
|
||||
import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
import { readConfig } from '../hook-lib.mjs';
|
||||
import { resolveFiles } from '../live-inject.mjs';
|
||||
import { resolveLiveConfigPath } from '../lib/impeccable-paths.mjs';
|
||||
|
||||
// Serializing thousands of page identities into every /live.js response
|
||||
// helps nobody; past this cap pageFiles is omitted and the resolver falls
|
||||
// back to the served-root common ancestor, which is correct, just less
|
||||
// precise about cross-root duplicates.
|
||||
const PAGE_FILES_CAP = 500;
|
||||
|
||||
export function collectProjectDetectorIgnores({ appRoot, contextRoot, repoRoot, scriptsDir } = {}) {
|
||||
const configRoots = [];
|
||||
for (const dir of [appRoot, contextRoot, repoRoot]) {
|
||||
if (typeof dir !== 'string' || !dir) continue;
|
||||
const resolved = path.resolve(dir);
|
||||
if (!configRoots.includes(resolved)) configRoots.push(resolved);
|
||||
}
|
||||
if (configRoots.length === 0) configRoots.push(process.cwd());
|
||||
|
||||
const ignoreRules = new Set();
|
||||
const ignoreFiles = new Set();
|
||||
const valueEntries = new Map();
|
||||
for (const dir of configRoots) {
|
||||
// readConfig merges config.json with the gitignored config.local.json
|
||||
// and type-checks both, exactly as the edit hook reads the same pair.
|
||||
const config = readConfig(dir);
|
||||
for (const rule of Array.isArray(config.ignoreRules) ? config.ignoreRules : []) {
|
||||
if (typeof rule === 'string' && rule.trim()) ignoreRules.add(rule);
|
||||
}
|
||||
for (const glob of Array.isArray(config.ignoreFiles) ? config.ignoreFiles : []) {
|
||||
if (typeof glob === 'string' && glob.trim()) ignoreFiles.add(glob);
|
||||
}
|
||||
for (const entry of Array.isArray(config.ignoreValues) ? config.ignoreValues : []) {
|
||||
if (!entry || typeof entry !== 'object') continue;
|
||||
// readConfig already normalized rule/value and folded `file` into
|
||||
// `files`; serve only what the browser matches on.
|
||||
const serialized = {
|
||||
rule: entry.rule,
|
||||
value: entry.value,
|
||||
...(Array.isArray(entry.files) && entry.files.length > 0 ? { files: entry.files } : {}),
|
||||
};
|
||||
const key = JSON.stringify([serialized.rule, serialized.value,
|
||||
Array.isArray(serialized.files) ? [...serialized.files].sort() : []]);
|
||||
if (!valueEntries.has(key)) valueEntries.set(key, serialized);
|
||||
}
|
||||
}
|
||||
|
||||
const served = readLiveServedPages({ appRoot: configRoots[0], repoRoot, scriptsDir });
|
||||
return {
|
||||
ignoreRules: [...ignoreRules],
|
||||
ignoreValues: [...valueEntries.values()],
|
||||
ignoreFiles: [...ignoreFiles],
|
||||
roots: served.roots,
|
||||
pageFiles: served.pageFiles,
|
||||
};
|
||||
}
|
||||
|
||||
function readLiveServedPages({ appRoot, repoRoot, scriptsDir }) {
|
||||
let live = null;
|
||||
try {
|
||||
const configPath = resolveLiveConfigPath({ cwd: appRoot, scriptsDir });
|
||||
live = JSON.parse(fs.readFileSync(configPath, 'utf-8'));
|
||||
} catch {
|
||||
// No readable inject config: the browser matches URL paths as-is.
|
||||
return { roots: [], pageFiles: [] };
|
||||
}
|
||||
const files = Array.isArray(live?.files)
|
||||
? live.files.filter((glob) => typeof glob === 'string' && glob)
|
||||
: [];
|
||||
|
||||
// A monorepo appRoot serializes identities repo-relative, so waivers
|
||||
// spelled from either root match through the resolver's suffix expansion.
|
||||
let prefix = '';
|
||||
if (typeof repoRoot === 'string' && repoRoot) {
|
||||
const rel = path.relative(path.resolve(repoRoot), path.resolve(appRoot)).split(path.sep).join('/');
|
||||
if (rel && !rel.startsWith('..') && !path.isAbsolute(rel)) prefix = `${rel}/`;
|
||||
}
|
||||
|
||||
const roots = [...new Set(files.map((glob) => {
|
||||
const wildcardAt = glob.search(/[*?{]/);
|
||||
const head = wildcardAt === -1 ? glob : glob.slice(0, wildcardAt);
|
||||
const cut = head.lastIndexOf('/');
|
||||
return prefix + (cut > -1 ? head.slice(0, cut + 1) : '');
|
||||
}))];
|
||||
|
||||
let pageFiles = [];
|
||||
try {
|
||||
pageFiles = resolveFiles(appRoot, { ...live, files })
|
||||
.filter((rel) => {
|
||||
// resolveFiles passes literal entries through even when they do not
|
||||
// exist; a missing file is nobody's identity.
|
||||
try { return fs.statSync(path.join(appRoot, rel)).isFile(); } catch { return false; }
|
||||
})
|
||||
.map((rel) => prefix + rel);
|
||||
} catch {
|
||||
pageFiles = [];
|
||||
}
|
||||
if (pageFiles.length > PAGE_FILES_CAP) pageFiles = [];
|
||||
|
||||
return { roots, pageFiles };
|
||||
}
|
||||
Reference in New Issue
Block a user