Las 49 redeclaraciones verbatim de id/ref en 7 providers pasan a heredar de WithRefOpts (44) / ProviderOpts (5 raices con ref opcional). Guard de censo por linea cruda CASE-SENSITIVE en opts-census.test.ts (un parser de llaves se traga los extends con genericos inline; menubar tiene `ref: state<...>` en un call-site que un scan insensible flagearia), verificado en rojo y en verde. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>alpha-0.1-dir-prefs
parent
30e2a027fe
commit
337dde8575
@ -0,0 +1,75 @@
|
||||
import { readdirSync, readFileSync } from 'node:fs';
|
||||
import { join, relative } from 'node:path';
|
||||
import { describe, expect, it } from 'vitest';
|
||||
|
||||
/**
|
||||
* The census guard of the opts axis: no opts interface inside
|
||||
* `soma/components` re-declares `id` / `ref` in its own body — those two
|
||||
* keys come from `WithRefOpts` / `ProviderOpts` (`provider.svelte.ts`),
|
||||
* the single place that says what they are. History: 49 interfaces across
|
||||
* 7 files had drifted into verbatim copies before the 2026-08-05 merge.
|
||||
*
|
||||
* The scan is a raw-line, CASE-SENSITIVE match — deliberately not a
|
||||
* parser. The brace-matching parser that produced the first census count
|
||||
* missed 16 interfaces whose `extends` clause carries inline literal
|
||||
* generics (`StateProps<{ open: boolean }>`); a line match has no such
|
||||
* blind spot, and it also covers `type XOpts = { … }` literal bodies.
|
||||
* Case matters: `menubar-provider.svelte.ts` composes an internal menu
|
||||
* with `ref: state<HTMLElement | null>(null)` — the FUNCTION `state` at a
|
||||
* call-site — which a case-insensitive scan would flag as a false positive.
|
||||
*/
|
||||
|
||||
const COMPONENTS_DIR = join(__dirname, '..', 'components');
|
||||
|
||||
/**
|
||||
* Signed exceptions — relative path → reason. Growing this list is a
|
||||
* decision, not a convenience.
|
||||
*/
|
||||
const EXCEPTIONS: Record<string, string> = {};
|
||||
|
||||
// Prefix matches on purpose: a widened `id: Active<string | undefined>` or
|
||||
// a narrowed `ref: State<HTMLElementX …>` is also a deviation from the base.
|
||||
const ID_DECL = /^\s*id\s*:\s*Active<string>/;
|
||||
const REF_DECL = /^\s*ref\s*\??\s*:\s*State<HTMLElement/;
|
||||
|
||||
function scanTsFiles(dir: string): string[] {
|
||||
const out: string[] = [];
|
||||
for (const entry of readdirSync(dir, { withFileTypes: true })) {
|
||||
const path = join(dir, entry.name);
|
||||
if (entry.isDirectory()) out.push(...scanTsFiles(path));
|
||||
else if (entry.name.endsWith('.ts') && !entry.name.endsWith('.test.ts')) out.push(path);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
describe('opts census — id/ref come from the base, never re-declared', () => {
|
||||
const files = scanTsFiles(COMPONENTS_DIR);
|
||||
|
||||
it('finds the catalogue (sanity: the census is not scanning an empty tree)', () => {
|
||||
expect(files.length).toBeGreaterThan(200);
|
||||
});
|
||||
|
||||
it('no file in components/ re-declares the canonical id/ref lines', () => {
|
||||
const offenders: string[] = [];
|
||||
for (const file of files) {
|
||||
const rel = relative(COMPONENTS_DIR, file).replace(/\\/g, '/');
|
||||
if (rel in EXCEPTIONS) continue;
|
||||
const lines = readFileSync(file, 'utf-8').split('\n');
|
||||
for (let i = 0; i < lines.length; i++) {
|
||||
if (ID_DECL.test(lines[i]) || REF_DECL.test(lines[i])) {
|
||||
offenders.push(`${rel}:${i + 1}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
expect(
|
||||
offenders,
|
||||
'id/ref re-declared verbatim — extend WithRefOpts / ProviderOpts instead'
|
||||
).toEqual([]);
|
||||
});
|
||||
|
||||
it('the exceptions are real files that still exist', () => {
|
||||
const relSet = new Set(files.map((f) => relative(COMPONENTS_DIR, f).replace(/\\/g, '/')));
|
||||
const stale = Object.keys(EXCEPTIONS).filter((rel) => !relSet.has(rel));
|
||||
expect(stale, 'these exceptions point at files that no longer exist — retire them').toEqual([]);
|
||||
});
|
||||
});
|
||||
Loading…
Reference in new issue