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/eidos-lint-all.ts

176 lines
6.0 KiB

/**
* Lint every eidos component CSS against its morfo contract.
*
* Usage: npx tsx scripts/eidos-lint-all.ts
*
* Prints a per-component summary so the drift between eidos and the
* morfo declarations is visible at a glance.
*
* Also validates the `data-event*` VALUES against the morfo catalogue and
* the sema canon — see `scripts/eidos-event-vocabulary.ts`.
*/
import { existsSync, readFileSync, readdirSync } from 'node:fs';
import { resolve } from 'node:path';
import { compileMorfo } from '../src/uix/morfo/compile';
import { lintEidosCss } from '../src/uix/eidos/lint';
import type { Morfo } from '../src/uix/morfo/types';
import {
formatEventFindings,
lintEventValues,
loadEventVocabulary
} from './eidos-event-vocabulary';
// Shared with morfo-check so the two instruments cannot drift (P0 fase A,
// audit 2026-08-26) — the list lives in ./eidos-only-attrs.ts.
import { EIDOS_ONLY_ATTRS } from './eidos-only-attrs';
const RUNTIME_SIGNAL_ATTRS = new Set([
'data-disabled',
'data-invalid',
'data-last-action',
'data-orientation',
'data-readonly',
'data-side',
'data-state'
]);
const EIDOS_ONLY_PART_SUFFIXES = [
'footer',
'handle',
'header',
'indicator',
'label',
'main',
// waveform F3: the two copies of the wave path (base + progress-clipped) —
// decorative svg primitives, rule-of-4 compliant.
'played',
'remaining',
'row',
'row-label',
'status'
] as const;
async function main() {
const eidosDir = resolve('src/uix/eidos/components');
const cssFiles = readdirSync(eidosDir, { withFileTypes: true }).flatMap((entry) => {
if (entry.isFile() && entry.name.endsWith('.css')) {
const name = entry.name.replace(/\.css$/, '');
return [{ name, path: resolve(eidosDir, entry.name) }];
}
if (entry.isDirectory()) {
const path = resolve(eidosDir, entry.name, `${entry.name}.css`);
return existsSync(path) ? [{ name: entry.name, path }] : [];
}
return [];
});
console.log('component morfo eidos invalid unused');
console.log('─────────────────────────────────────────────────────');
let totalInvalid = 0;
let totalEidosOnly = 0;
let totalUnused = 0;
const driftHotspots: { name: string; eidosOnly: string[]; invalid: string[] }[] = [];
for (const file of cssFiles) {
const name = file.name;
const morfoPath = resolve(`src/uix/morfo/components/${name}.ts`);
try {
readFileSync(morfoPath);
} catch {
console.log(`${name.padEnd(22)}(no morfo file — skipped)`);
continue;
}
const mod = (await import(`../src/uix/morfo/components/${name}`)) as Record<string, unknown>;
const exportName = `${name.replace(/-([a-z])/g, (_, c: string) => c.toUpperCase())}Morfo`;
const morfo = mod[exportName] as Morfo | undefined;
if (!morfo) {
console.log(`${name.padEnd(22)}(${exportName} not exported — skipped)`);
continue;
}
const compiled = compileMorfo(morfo);
const partMarkers = new Set(Object.values(compiled.parts.attrs));
const css = readFileSync(file.path, 'utf-8');
const report = lintEidosCss(css, compiled);
console.log(
`${name.padEnd(22)}${String(report.counts.morfoBacked).padEnd(7)}${String(report.counts.eidosOnly).padEnd(7)}${String(report.counts.invalid).padEnd(9)}${report.unusedFromContract.length}`
);
totalInvalid += report.counts.invalid;
totalEidosOnly += report.counts.eidosOnly;
totalUnused += report.unusedFromContract.length;
// Filter eidos-only that look like drift (data-* on a part where the morfo
// never declared the attr — vs design tokens like data-color/-size/-variant).
const drift = report.findings.filter(
(f) =>
f.status === 'eidos-only' &&
(f.eidosOnlyAttrs ?? f.attrs).some(
(a) =>
a.attr.startsWith('data-') &&
!partMarkers.has(a.attr) &&
!isAllowedEidosOnlyAttr(a.attr)
)
);
const invalid = report.findings.filter((f) => f.status === 'invalid');
if (drift.length || invalid.length) {
driftHotspots.push({
name,
eidosOnly: drift.map((f) => f.rule),
invalid: invalid.map((f) => `${f.rule} — ${f.reason}`)
});
}
}
console.log('─────────────────────────────────────────────────────');
console.log(
`totals${' '.repeat(16)} ${totalEidosOnly} ${totalInvalid} ${totalUnused}`
);
if (driftHotspots.length) {
console.log('\nDrift hotspots (eidos uses data-* attrs the morfo does not declare):');
for (const h of driftHotspots) {
if (!h.eidosOnly.length && !h.invalid.length) continue;
console.log(`\n ${h.name}`);
for (const r of h.invalid) console.log(` ✗ ${r}`);
for (const r of h.eidosOnly) console.log(` · ${r}`);
}
} else {
console.log('\nNo drift hotspots beyond documented Eidos-only attrs/parts.');
}
// `data-event*` VALUE check. Its own pass over the same files: it needs no
// compiled morfo, so it also covers the CSS skipped above for lack of one.
const vocabulary = await loadEventVocabulary();
let deadEvents = 0;
const eventBlocks: string[] = [];
for (const file of cssFiles) {
const findings = lintEventValues(readFileSync(file.path, 'utf-8'), vocabulary, file.name);
if (!findings.length) continue;
deadEvents += findings.filter((f) => f.level === 'error').length;
eventBlocks.push(` ${file.name}\n${formatEventFindings(findings, ' ')}`);
}
if (eventBlocks.length) {
console.log('\ndata-event* vocabulary (value must exist in a morfo / the canon):');
for (const block of eventBlocks) console.log(`\n${block}`);
} else {
console.log('\nEvery data-event* value in a recipe exists in the morfo catalogue.');
}
if (totalInvalid > 0 || deadEvents > 0) process.exit(1);
}
function isAllowedEidosOnlyAttr(attr: string): boolean {
if (EIDOS_ONLY_ATTRS.has(attr)) return true;
if (RUNTIME_SIGNAL_ATTRS.has(attr)) return true;
return EIDOS_ONLY_PART_SUFFIXES.some((suffix) => attr.endsWith(`-${suffix}`));
}
main().catch((err) => {
console.error(err);
process.exit(2);
});

Powered by TurnKey Linux.