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/arts-check.ts

191 lines
6.3 KiB

/**
* arts-check — guard for the `src/arts/` ecosystem invariants (arts docs
* reconciliation, batch A0). Ships the net that batches A1–A3 / B3 land
* against, so the drift-fix and language passes cannot silently regress the
* structural promises the arts corpus makes.
*
* The canon these invariants encode is `src/arts/README.md` §"naming
* conventions" + the "Map" index:
*
* A-readme (error) — every `src/arts/{name}/` has a README.md. A missing
* README is a hole in the corpus. `ethereal` is the one known gap (it only
* ships PERF.md); it is exempt until batch A3 authors its README. The
* exemption is self-cleaning: once the file lands, the guard demands the
* art be removed from README_PENDING.
* A-naming (error) — the `Engine*` / `Active*` split holds:
* · No `Engine*` root is DECLARED in a `.svelte.ts` file. Engines are
* pure factories over private/no state (README §7-9) — they never own
* `$state`, so they never need the runes extension. (A `.svelte.ts`
* that merely *wraps* an engine is fine; only a declaration trips it.)
* · Every reactive `class Active*` root lives in a `.svelte.ts` file
* (README §12) — a plain `.ts` cannot own module `$state`. Error
* classes (`Active…Error`) are not reactive roots and are exempt.
* A-index (warn) — the `src/arts/README.md` Map lists every art. WARN (not
* error) because closing the four current gaps (bus, orca, prefs, ethereal)
* is batch B3's job; the warnings are B3's worklist and drive to zero there.
*
* Mirrors scripts/docs-check.ts (textual invariants, no Svelte-graph imports).
* Exit code 1 when any error-level finding exists.
*/
import { readFileSync, readdirSync, statSync, existsSync } from 'node:fs';
import { join, resolve, relative, sep } from 'node:path';
const REPO = resolve(import.meta.dirname, '..');
const ARTS = join(REPO, 'src', 'arts');
interface Finding {
severity: 'error' | 'warn';
invariant: string;
file: string;
line: number;
message: string;
}
const findings: Finding[] = [];
const notes: string[] = [];
function report(
severity: 'error' | 'warn',
invariant: string,
file: string,
line: number,
message: string
): void {
findings.push({
severity,
invariant,
file: relative(REPO, file).split(sep).join('/'),
line,
message
});
}
function read(file: string): string {
return readFileSync(file, 'utf-8');
}
/** The arts = the immediate subdirectories of `src/arts/`. */
const arts = readdirSync(ARTS)
.filter((entry) => statSync(join(ARTS, entry)).isDirectory())
.sort();
/** Collect `.ts` sources under an art, excluding tests. */
function walkTs(dir: string, out: string[] = []): string[] {
for (const entry of readdirSync(dir)) {
const full = join(dir, entry);
if (statSync(full).isDirectory()) {
if (entry !== 'node_modules') walkTs(full, out);
} else if (/\.ts$/.test(entry) && !/\.(test|spec)\./.test(entry)) {
out.push(full);
}
}
return out;
}
const tsFiles = walkTs(ARTS);
// ── Invariant A-readme — every art has a README ─────────────────────────────
/** ethereal only ships PERF.md today; batch A3 authors its README. */
const README_PENDING = new Set(['ethereal']);
for (const art of arts) {
const readme = join(ARTS, art, 'README.md');
const exists = existsSync(readme);
if (!exists) {
if (README_PENDING.has(art)) {
notes.push(`A-readme: '${art}/README.md' absent — exempt pending batch A3`);
} else {
report('error', 'A-readme', readme, 1, `art '${art}' has no README.md`);
}
} else if (README_PENDING.has(art)) {
// Self-cleaning: the exemption must not outlive the gap it covers.
report(
'error',
'A-readme',
readme,
1,
`art '${art}' now has a README — remove '${art}' from README_PENDING in scripts/arts-check.ts`
);
}
}
// ── Invariant A-naming — the Engine*/Active* split holds ────────────────────
/** An EXPORTED declaration of an `Engine*` root (class or create/define factory). */
const ENGINE_DECL =
/^\s*export\s+(?:async\s+)?(?:abstract\s+)?(?:class\s+Engine\w+|function\s+(?:create|define)Engine\w+|const\s+(?:create|define)Engine\w+\s*=)/;
/** An EXPORTED reactive `class Active*` root (not an `Active…Error`). */
const ACTIVE_CLASS = /^\s*export\s+(?:abstract\s+)?class\s+(Active\w+)/;
for (const file of tsFiles) {
const isSvelte = file.endsWith('.svelte.ts');
const lines = read(file).split('\n');
lines.forEach((line, i) => {
if (isSvelte && ENGINE_DECL.test(line)) {
report(
'error',
'A-naming',
file,
i + 1,
'an Engine* root is declared in a .svelte.ts file — engines are stateless (arts/README §naming); reactive roots are Active* only'
);
}
if (!isSvelte) {
const m = line.match(ACTIVE_CLASS);
if (m && !/Error$/.test(m[1])) {
report(
'error',
'A-naming',
file,
i + 1,
`reactive root class ${m[1]} is declared in a plain .ts — Active* roots own $state and must live in a .svelte.ts (arts/README §naming)`
);
}
}
});
}
// ── Invariant A-index — the arts index Map lists every art (WARN) ───────────
const indexSrc = read(join(ARTS, 'README.md'));
const indexed = new Set([...indexSrc.matchAll(/\.\/([a-z][\w-]*)\/README\.md/g)].map((m) => m[1]));
for (const art of arts) {
if (!indexed.has(art)) {
report(
'warn',
'A-index',
join(ARTS, 'README.md'),
1,
`art '${art}' is not listed in the arts index Map table — batch B3 adds it`
);
}
}
// ── Report ──────────────────────────────────────────────────────────────────
findings.sort(
(a, b) =>
a.invariant.localeCompare(b.invariant) || a.file.localeCompare(b.file) || a.line - b.line
);
const errors = findings.filter((f) => f.severity === 'error');
const warns = findings.filter((f) => f.severity === 'warn');
for (const f of findings) {
console.log(
`${f.severity.toUpperCase().padEnd(5)} [${f.invariant}] ${f.file}:${f.line} — ${f.message}`
);
}
for (const n of notes) console.log(`NOTE ${n}`);
console.log(
`\narts-check: ${errors.length} error(s), ${warns.length} warning(s) across ${arts.length} arts`
);
process.exit(errors.length > 0 ? 1 : 0);

Powered by TurnKey Linux.