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

192 lines
6.4 KiB

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

/**
* 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. README_PENDING is a self-cleaning
* exemption set (empty now — every art is documented, ethereal included):
* if an exempted art later gains a README the guard demands it be removed
* from the set, so the exemption can never outlive the gap it covers.
* 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 (error) — the `src/arts/README.md` Map lists every art. An art
* that never reaches the index is invisible to readers (ethereal was: a
* full positioning engine absent from the Map, the aliases and the deps).
*
* 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 ─────────────────────────────
/** Self-cleaning exemption for arts whose README is not authored yet. Empty:
* every art is documented (ethereal's README landed with its Map/alias wiring). */
const README_PENDING = new Set<string>();
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(
'error',
'A-index',
join(ARTS, 'README.md'),
1,
`art '${art}' is not listed in the arts index Map table (src/arts/README.md)`
);
}
}
// ── 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.