You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
121 lines
3.8 KiB
121 lines
3.8 KiB
/**
|
|
* packs-check — the mechanical guard of the pack tier's P contract
|
|
* (docs/architecture/packs.md). Small on purpose: packs are exempt from the
|
|
* acceptance matrix, NOT from discipline.
|
|
*
|
|
* Checks (all error-level):
|
|
* P-3 no raw platform APIs in `src/packs/` — everything goes through the
|
|
* injected ports (`SceneDom` etc.). Raw `requestAnimationFrame`,
|
|
* `new ResizeObserver/IntersectionObserver/MutationObserver`,
|
|
* `addEventListener`, `setTimeout/setInterval`, `matchMedia` = error.
|
|
* S-1 no scoped `<style>` blocks in pack `.svelte` files — the consumer
|
|
* styles the host via class/style; the pack never competes with eidos.
|
|
* R-1 every `effects/*.ts` module registers its effect AND merges its
|
|
* params type (`registerAmbientEffect` + `declare module`), so the
|
|
* typed registry can't drift from the runtime one.
|
|
*
|
|
* P-1/P-2/P-5/P-6 are enforced structurally by `EngineScene` (type-required
|
|
* `reduce`, handle teardown, aria-hidden canvas, IO pause + budget) — no
|
|
* static check needed here.
|
|
*/
|
|
|
|
import { readdirSync, readFileSync, statSync } from 'node:fs';
|
|
import { join, relative } from 'node:path';
|
|
|
|
const REPO = process.cwd();
|
|
const PACKS_DIR = join(REPO, 'src', 'packs');
|
|
|
|
interface Violation {
|
|
rule: string;
|
|
file: string;
|
|
line: number;
|
|
detail: string;
|
|
}
|
|
|
|
const RAW_API_RE =
|
|
/\brequestAnimationFrame\s*\(|\bcancelAnimationFrame\s*\(|new\s+(ResizeObserver|IntersectionObserver|MutationObserver)\b|\baddEventListener\s*\(|\bsetTimeout\s*\(|\bsetInterval\s*\(|\bmatchMedia\s*\(/;
|
|
|
|
function walk(dir: string, out: string[] = []): string[] {
|
|
for (const entry of readdirSync(dir)) {
|
|
const path = join(dir, entry);
|
|
if (statSync(path).isDirectory()) walk(path, out);
|
|
else if (/\.(ts|svelte)$/.test(entry) && !/\.test\./.test(entry)) out.push(path);
|
|
}
|
|
return out;
|
|
}
|
|
|
|
function main(): void {
|
|
const violations: Violation[] = [];
|
|
let files: string[] = [];
|
|
try {
|
|
files = walk(PACKS_DIR);
|
|
} catch {
|
|
console.error('packs-check: cannot scan src/packs/');
|
|
process.exitCode = 1;
|
|
return;
|
|
}
|
|
if (files.length === 0) {
|
|
console.error('packs-check: empty source corpus under src/packs/');
|
|
process.exitCode = 1;
|
|
return;
|
|
}
|
|
|
|
for (const file of files) {
|
|
const rel = relative(REPO, file);
|
|
const src = readFileSync(file, 'utf8');
|
|
const lines = src.split('\n');
|
|
|
|
lines.forEach((line, i) => {
|
|
// Comments may NAME the raw APIs (docs of what the port replaces).
|
|
const code = line.replace(/\/\/.*$/, '').replace(/^\s*\*.*$/, '');
|
|
if (RAW_API_RE.test(code)) {
|
|
violations.push({
|
|
rule: 'P-3',
|
|
file: rel,
|
|
line: i + 1,
|
|
detail: `raw platform API — use the injected port (${code.trim().slice(0, 60)}…)`
|
|
});
|
|
}
|
|
});
|
|
|
|
if (file.endsWith('.svelte') && /<style\b/.test(src)) {
|
|
violations.push({
|
|
rule: 'S-1',
|
|
file: rel,
|
|
line: lines.findIndex((l) => /<style\b/.test(l)) + 1,
|
|
detail: 'scoped <style> in a pack component — style the host via class/style'
|
|
});
|
|
}
|
|
|
|
if (/[\\/]effects[\\/][^\\/]+\.ts$/.test(file)) {
|
|
if (!src.includes('registerAmbientEffect(')) {
|
|
violations.push({
|
|
rule: 'R-1',
|
|
file: rel,
|
|
line: 1,
|
|
detail: 'effect module does not register its effect (registerAmbientEffect)'
|
|
});
|
|
}
|
|
if (!src.includes('declare module')) {
|
|
violations.push({
|
|
rule: 'R-1',
|
|
file: rel,
|
|
line: 1,
|
|
detail: 'effect module does not merge its params type (declare module …/registry)'
|
|
});
|
|
}
|
|
}
|
|
}
|
|
|
|
if (violations.length > 0) {
|
|
for (const v of violations) {
|
|
console.error(`ERROR [${v.rule}] ${v.file}:${v.line} — ${v.detail}`);
|
|
}
|
|
console.error(`\npacks-check: ${violations.length} error(s) across ${files.length} files`);
|
|
process.exit(1);
|
|
}
|
|
console.log(`packs-check: 0 error(s) across ${files.length} files`);
|
|
}
|
|
|
|
main();
|