|
|
/**
|
|
|
* 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);
|