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.
svelte-kit-vice/scripts/packs-check.ts

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();

Powered by TurnKey Linux.