test(uix): add VG-8 + A31 regression guards; fix the toolbar A31 the guard found

Two more catalogue invariants in contracts.test.ts, locking in audit fixes:

- VG-8: every component morfo must be declared `as const satisfies Morfo`,
  never `: Morfo` (the annotation widens the literal so the schema can't check
  it — how alert-dialog-001 shipped). Green; proven to catch drift (annotating
  toggle as `: Morfo` makes it fail).

- A31: a per-item membership predicate (`isSelected` / `isItemPressed` / …) must
  not do `.current.includes(...)` — that is O(N²) per render. Lift a Set, use
  `.has()`.

The A31 guard immediately surfaced a real violation the human audit missed:
`toolbar-provider` ToolbarGroupProvider.isItemPressed did
`this.opts.value.current.includes(value)`, called from the per-item `isPressed`
$derived — the exact O(N²) pattern, in a component not on the audit's 9-item
A31 list. Fixed with the same lift (`pressedSet` + `.has()`). That catch IS the
guard's proof.

Both guards exclude the active-dev-track set so they stay green for the
maintained catalogue. check: 0 new type errors; toolbar provider tests 3/3.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
active-uix
dev 3 months ago
parent e378e14a62
commit ec100c4bb8

@ -1,7 +1,7 @@
// @vitest-environment jsdom
import { existsSync, readdirSync, readFileSync, statSync } from 'node:fs';
import { dirname, join } from 'node:path';
import { basename, dirname, join } from 'node:path';
import { fileURLToPath } from 'node:url';
import { describe, expect, it } from 'vitest';
@ -202,6 +202,52 @@ function collectScopeDriftViolations(): string[] {
return violations.sort();
}
// VG-8: a component morfo MUST be declared `as const satisfies Morfo`, never
// `: Morfo`. The annotation widens the literal object to the interface, so the
// compiler stops checking the contract against the actual literals — that is
// exactly how alert-dialog-001 shipped a morfo the schema couldn't validate.
function collectMorfoDeclarationViolations(): string[] {
const internalRoot = join(HERE, 'morfo', 'internal');
const morfoFiles = [
...listSourceFiles(join(HERE, 'morfo', 'components')),
...(existsSync(internalRoot) ? listSourceFiles(internalRoot) : [])
];
const violations: string[] = [];
for (const file of morfoFiles) {
const source = stripComments(readFileSync(file, 'utf8'));
const kebab = /kebab:\s*'([^']+)'/.exec(source)?.[1];
if (kebab && ACTIVE_DEV_TRACK.has(kebab)) continue;
if (!/export const \w+Morfo\b/.test(source)) continue;
const name = file.replace(REPO_ROOT, '');
if (/export const \w+Morfo\s*:\s*Morfo\b/.test(source)) {
violations.push(`${name}: ': Morfo' widens the literal — use 'as const satisfies Morfo'`);
} else if (!/as const satisfies Morfo\b/.test(source)) {
violations.push(`${name}: missing 'as const satisfies Morfo'`);
}
}
return violations.sort();
}
// A31: a per-item membership predicate (`isSelected` / `isItemChecked` /
// `isExpanded` / `isItemPressed` / …) must NOT read a reactive array with
// `.current.includes(...)` / `.indexOf(...)` — called once per item that is
// O(N²) and the documented "hangs at 30+" hazard. Lift a `Set` in a provider
// `$derived` and do O(1) `.has(value)` (the 9 components the audit fixed all do).
function collectA31Violations(): string[] {
const root = join(HERE, 'soma', 'components');
const re =
/\bis\w*(?:Selected|Checked|Expanded|Pressed|Highlighted|Active)\b[^{};]*\{[^{}]*\.current\.(?:includes|indexOf)\b/;
return listSourceFiles(root)
.filter((file) => file.endsWith('.svelte.ts'))
.filter((file) => !ACTIVE_DEV_TRACK.has(basename(dirname(file))))
.filter((file) => re.test(stripComments(readFileSync(file, 'utf8'))))
.map((file) => `${file.replace(REPO_ROOT, '')}: per-item membership uses .current.includes (A31 O(N²))`)
.sort();
}
function collectComponentLangPathViolations(source: string, file: string): string[] {
const clean = stripComments(source);
const violations: string[] = [];
@ -502,6 +548,19 @@ describe('UIX layer contracts', () => {
).toEqual([]);
});
it("guards every morfo is declared 'as const satisfies Morfo', never ': Morfo' (VG-8)", () => {
const violations = collectMorfoDeclarationViolations();
expect(violations, `morfo declaration drift:\n ${violations.join('\n ')}`).toEqual([]);
});
it('guards per-item membership predicates against O(N²) .current.includes (A31)', () => {
const violations = collectA31Violations();
expect(
violations,
`A31 — lift a Set + use .has() in:\n ${violations.join('\n ')}`
).toEqual([]);
});
it('guards Soma public component modules with local README docs', () => {
const missingReadmes = collectPublicSomaComponentDirs().filter(
(dir) => !existsSync(join(HERE, 'soma', 'components', dir, 'README.md'))

@ -261,8 +261,11 @@ export class ToolbarGroupProvider {
});
}
/** Lifted pressed set (A31): per-item `isItemPressed` does O(1) `.has()`, not O(N) `.includes()`. */
readonly pressedSet = $derived.by(() => new Set(this.opts.value.current));
isItemPressed(value: string): boolean {
return this.opts.value.current.includes(value);
return this.pressedSet.has(value);
}
toggleItem(value: string) {

Loading…
Cancel
Save

Powered by TurnKey Linux.