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/src/uix/contracts.test.ts

1282 lines
52 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.

// @vitest-environment jsdom
import { existsSync, readdirSync, readFileSync, statSync } from 'node:fs';
import { basename, dirname, join, sep } from 'node:path';
import { fileURLToPath } from 'node:url';
import { describe, expect, it } from 'vitest';
import { createActiveApp, type ActiveApp } from '$active-app';
import { createActiveDom } from '$adom';
import { createActivePrefs, createActivePrefsDomProjection, standardPrefsDimensions } from '$prefs';
import type { LangNode } from '$libs/langs';
import { state } from '$libs/reactive';
import {
ActiveUixMissingServiceError,
attachActiveUix,
createActiveUix
} from './active-uix/active-uix.svelte';
import { defineUixServices, UIX_REQUIRED_SERVICES } from './active-uix/services';
import { createActiveEidos, ActiveEidosConfigError } from './eidos/active-eidos.svelte';
import {
_resetMorfoRegistryForTesting,
registerMorfo,
type Morfo,
v
} from './morfo';
import { toggleMorfo } from './morfo/components/toggle';
import { EngineSemantic, defineEngineSemantic } from './sema';
import { SemaConfigError } from './sema/errors';
import { SEMA_FAMILY_POLICY } from './sema/types';
import { createSomaRuntime } from './soma/runtime.svelte';
import { UIX_LAYER_CONTRACTS } from './contracts';
import { commonLangs } from './langs';
const minimalLang = { schema: {} satisfies LangNode, defaultLocale: 'es' as const };
const prefsProjectionSchema = standardPrefsDimensions({
languages: ['es-ES', 'ar-EG'],
locales: ['es-ES', 'ar-EG'],
currencies: ['EUR'],
defaults: {
language: 'es-ES',
locale: 'es-ES',
currency: 'EUR'
}
});
const HERE = dirname(fileURLToPath(import.meta.url));
const REPO_ROOT = join(HERE, '..', '..');
const SOURCE_EXTENSIONS = ['.ts', '.svelte'] as const;
function preferencesHarness() {
return {
getTheme: () => 'base',
getMode: () => 'light' as const,
getDensity: () => 'comfortable' as const,
getScaling: () => '100' as const,
onPreferenceChange: () => () => {}
};
}
function listSourceFiles(root: string): string[] {
const stat = statSync(root);
if (stat.isFile()) {
return SOURCE_EXTENSIONS.some((ext) => root.endsWith(ext)) ? [root] : [];
}
return readdirSync(root)
.flatMap((entry) => listSourceFiles(join(root, entry)))
.filter(
(path) => !path.endsWith('.test.ts') && !path.includes(`${join('components', 'icon', 'lib')}`)
)
// Active-dev-track components are excluded from the audited catalogue
// (same doctrine as collectPublicSomaComponentDirs) — their files are
// skipped by every file-scanning guard until their track lands.
.filter(
(path) =>
![...ACTIVE_DEV_TRACK].some(
(name) =>
path.includes(`${join('components', name)}${sep}`) ||
path.endsWith(`${join('components', `${name}.ts`)}`)
)
);
}
function grepSources(root: string, pattern: RegExp): string[] {
return listSourceFiles(root).filter((file) => pattern.test(readFileSync(file, 'utf8')));
}
function stripComments(source: string): string {
return source.replace(/\/\*[\s\S]*?\*\//g, '').replace(/(^|[^:])\/\/.*$/gm, '$1');
}
function collectKnownMorfoDataAttrs(): Set<string> {
const attrs = new Set<string>();
// Public morfos (`morfo/components/*.ts`) AND internal primitives
// (`morfo/internal/*.ts`) — the internal directory holds picker-shell
// and any future shared chrome. Their `data-{name}` markers are part
// of the catalogue too (see audit-codex P1 #5).
const internalRoot = join(HERE, 'morfo', 'internal');
const morfoFiles = [
...listSourceFiles(join(HERE, 'morfo', 'components')),
...(existsSync(internalRoot) ? listSourceFiles(internalRoot) : [])
];
for (const file of morfoFiles) {
const source = stripComments(readFileSync(file, 'utf8'));
const component = /kebab:\s*'([^']+)'/.exec(source)?.[1];
if (!component) continue;
const partRe = /kebab:\s*'([^']+)'/g;
let partMatch: RegExpExecArray | null;
while ((partMatch = partRe.exec(source))) {
const part = partMatch[1];
attrs.add(part === 'provider' ? `data-${component}` : `data-${component}-${part}`);
}
const dataRe = /attr:\s*'([^']+)'/g;
let dataMatch: RegExpExecArray | null;
while ((dataMatch = dataRe.exec(source))) {
if (dataMatch[1].startsWith('data-')) attrs.add(dataMatch[1]);
}
}
// Compiler-emitted from every part's `archetype` declaration — a known
// name catalogue-wide. Composition overrides (a field-overlay trigger
// stamped over the composed Popover trigger's generic archetype) write it
// manually with the value their OWN morfo part declares.
attrs.add('data-archetype');
return attrs;
}
function collectDataAttrs(source: string): Set<string> {
const clean = stripComments(source);
const attrs = new Set<string>();
const stringRe = /['"`](data-[a-zA-Z0-9_-]+)['"`]/g;
const inlineRe = /\b(data-[a-zA-Z0-9_-]+)(?=(?:=|\s|>|\/|}|\]))/g;
let match: RegExpExecArray | null;
while ((match = stringRe.exec(clean))) attrs.add(match[1]);
while ((match = inlineRe.exec(clean))) attrs.add(match[1]);
return attrs;
}
function toPascalName(kebab: string): string {
return kebab
.split('-')
.map((segment) => `${segment.charAt(0).toUpperCase()}${segment.slice(1)}`)
.join('');
}
// Soma component directories that are internal primitives, not public
// components. They ship under `soma/components/{name}/` because they
// have eidos chrome + a provider, but their morfo lives in
// `morfo/internal/` rather than `morfo/components/` (see audit-codex P1
// #5). Audit guards must skip them when comparing soma <-> morfo
// catalogues, otherwise the asymmetry produces a false positive.
// Soma modules that are NOT public component namespaces: shared chrome or
// behaviour bridges consumed by other layers, with no Provider/parts API of
// their own (each documents the split in its index.ts — the eidos root owns
// state + render; the bridge owns runtime emission / keyboard dispatch / pure
// nav maths). They are excluded from BOTH sides of the dir↔scope equality:
// a bridge may truthfully declare 'soma' in its morfo scope (it implements
// soma behaviour) without being a public soma component namespace.
// picker-shell — the pickers' shared footer chrome (no morfo of its own)
// metrics — live-update signal bridge (scope ['sema','eidos'])
// menu-dial — keyboard dispatch + linear nav maths + emission bridge
// onion-menu — keyboard dispatch + radial nav maths + emission bridge
const SOMA_INTERNAL_PRIMITIVES = new Set(['picker-shell', 'metrics', 'menu-dial', 'onion-menu']);
function collectPublicSomaComponentDirs(): string[] {
const root = join(HERE, 'soma', 'components');
return readdirSync(root, { withFileTypes: true })
.filter((entry) => entry.isDirectory())
.map((entry) => entry.name)
.filter(
(name) =>
name !== 'internal' &&
!SOMA_INTERNAL_PRIMITIVES.has(name) &&
!ACTIVE_DEV_TRACK.has(name) &&
existsSync(join(root, name, 'index.ts'))
)
.sort();
}
function collectMorfoComponentNamesByScope(scope: string): string[] {
const root = join(HERE, 'morfo', 'components');
return readdirSync(root)
.filter((file) => file.endsWith('.ts') && !file.endsWith('.test.ts'))
.filter((file) => {
const source = stripComments(readFileSync(join(root, file), 'utf8'));
return new RegExp(`scope:\\s*\\[[^\\]]*'${scope}'`).test(source);
})
.map((file) => file.replace(/\.ts$/, ''))
.filter((name) => !ACTIVE_DEV_TRACK.has(name))
.sort();
}
// Active-dev-track components, excluded from the audited/maintained catalogue.
//
// EMPTY since 2026-08-10 (author directive): `palabras` and `chronos` were the
// two entries, exempted while their tracks were in flight. The exemption is
// lifted — they answer to the same catalogue guards as everybody else, and a
// violation of theirs is a violation like any other. The set stays (typed, and
// consulted by six guards) because a future in-flight track is a legitimate use;
// what is not legitimate is leaving an entry in it after its track lands.
const ACTIVE_DEV_TRACK = new Set<string>([]);
// MOR-4 enumeration guard (clean-room 2026-07-10). The acceptance machinery
// (`component-audit`, `eidos-lint-all`) enumerates BY MORFO FILE — an eidos
// component dir with no morfo is invisible to every gate (card-group shipped
// that way). A morfo may live in `morfo/components/` (public catalogue) or
// `morfo/internal/` (shared layers, e.g. picker-shell). Exemptions, each with
// its own invariant:
// - shared internal layer still without a contract: spin-field (its CSS
// discipline is covered by recipe-css-contract; the foundation-scope
// R-* work owns the rest);
// - pure API aliases / glyph primitives with ZERO own surface (no css —
// the composed primitive's morfo IS the contract): icon-button (Button
// alias), svg (glyph atoms). The guard asserts the category stays true:
// the moment one ships a stylesheet it needs a morfo like everyone else.
const MORFO_EXEMPT_SHARED_LAYERS = new Set(['spin-field']);
const MORFO_EXEMPT_PURE_ALIASES = new Set(['icon-button', 'svg']);
function collectMorfoEnumerationViolations(): string[] {
const eidosRoot = join(HERE, 'eidos', 'components');
const morfoRoots = [join(HERE, 'morfo', 'components'), join(HERE, 'morfo', 'internal')];
const violations: string[] = [];
for (const entry of readdirSync(eidosRoot, { withFileTypes: true })) {
if (!entry.isDirectory()) continue;
const kebab = entry.name;
if (ACTIVE_DEV_TRACK.has(kebab) || MORFO_EXEMPT_SHARED_LAYERS.has(kebab)) continue;
if (MORFO_EXEMPT_PURE_ALIASES.has(kebab)) {
const css = readdirSync(join(eidosRoot, kebab)).filter((f) => f.endsWith('.css'));
if (css.length > 0)
violations.push(
`${kebab}: pure-alias exemption broken — ships ${css.join(', ')} → needs a morfo`
);
continue;
}
if (!morfoRoots.some((root) => existsSync(join(root, `${kebab}.ts`))))
violations.push(`${kebab}: no morfo — invisible to component-audit / eidos-lint`);
}
return violations.sort();
}
// SYS-1 scope-drift guard. `scope` enumerates the layers that IMPLEMENT a
// component, so a component shipping an eidos recipe (a wrapper `.svelte` or a
// `.css` under `eidos/components/{kebab}/` — a README-only dir does NOT count)
// MUST list `'eidos'` in its morfo `scope`. The 2026-06-26 audit found this
// drifted across 62 components; this guard keeps it from drifting back.
function collectScopeDriftViolations(): string[] {
const internalRoot = join(HERE, 'morfo', 'internal');
const eidosRoot = join(HERE, 'eidos', 'components');
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;
// The morfo-level scope is the only `scope: [ ... ]` ARRAY (part/event
// scope is a string literal), so the first array match is it.
const scopeBody = /\bscope:\s*\[([^\]]*)\]/.exec(source)?.[1];
if (!kebab || scopeBody === undefined) continue;
const eidosDir = join(eidosRoot, kebab);
const hasEidosRecipe =
existsSync(eidosDir) &&
readdirSync(eidosDir).some((entry) => entry.endsWith('.svelte') || entry.endsWith('.css'));
if (hasEidosRecipe && !/'eidos'/.test(scopeBody)) {
violations.push(`${kebab}: eidos recipe exists but morfo scope omits 'eidos'`);
}
}
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();
}
// D9: an event a morfo declares and no provider emits is INERT — it reads like
// a promise and does nothing. `book-deviations.md` types the sibling defect (a
// channel signature the activation does not include) as «mecánicamente
// comprobable y merece guard»; this is the same shape one level up, and it is
// the class that produced D1 (AlertDialog mute), D3 (`commit-reset` in five
// pickers), D4 (ColorPicker) and D9 (chronos' whole carry).
//
// The test is EMISSION, not `trigger('literal')`: chronos dispatches through a
// variable (`emitOnChip`), so a literal-call grep reports a false negative — it
// did, twice, during the audit. What holds is weaker and robust: the name has to
// appear as a string literal SOMEWHERE the provider can reach. That means the
// component's own soma dir plus the shared libraries a provider dispatches from
// — `$libs/selection` is where combobox's and select's `commit-select` lives,
// and without it they read as inert when they are not.
const INERT_EVENT_SCOPE = ['libs', join('uix', 'soma', 'datetime'), join('uix', 'soma', 'layers')];
// EMPTY since 2026-08-13 — the 7-entry queue measured on 2026-08-12 (ledger
// D13) resolved one by one under M1(ii): tooltip ×3 WIRED (the F4 recipe),
// virtual-list/grid ×3 declared `emission: 'host'` (the provider's written
// decision, now expressible), gradient-picker commit-reset RETIRED (S-14 —
// its transaction is delegated to the composed generic Picker). Kept as a
// set so the next real instance has a signed place to land.
const INERT_EVENT_DEBT = new Set<string>([]);
function sourceFilesUnder(root: string): string[] {
if (!existsSync(root)) return [];
return listSourceFiles(root).filter((f) => !f.includes('.test.'));
}
/**
* Two properties of this scan, both MEASURED (census over the 255 declared
* events, 2026-08-13) — do not "tighten" either without re-running it:
*
* - The match is a plain quoted-name lookup, not `trigger('name')`, because the
* name very often reaches the call through a VARIABLE: `const eventName =
* next ? 'commit-select' : 'commit-unselect'; trigger(eventName)`. Requiring
* the literal inside the call turns 27 real emissions into violations
* (checkbox, accordion, calendar, select, combobox, listbox, grid-list,
* tag-group, range-calendar, chronos, aura, palabras…). Its price: a name
* quoted in a COMMENT satisfies the check — that is how onion-menu's two
* drill events passed until their factory docblock was reworded.
* - The blob covers the component's EIDOS directory as well as its soma one.
* For the view components (metrics, menu-dial, onion-menu) there is no
* provider class: the eidos view registers the parts and emits, so scanning
* soma alone reported 4 live events as inert.
*/
function collectInertEventViolations(): string[] {
const morfoDir = join(HERE, 'morfo', 'components');
const somaDir = join(HERE, 'soma', 'components');
const eidosDir = join(HERE, 'eidos', 'components');
const shared = INERT_EVENT_SCOPE.flatMap((rel) =>
sourceFilesUnder(join(REPO_ROOT, 'src', rel))
)
.map((f) => readFileSync(f, 'utf8'))
.join('\n');
const violations: string[] = [];
const fixed: string[] = [];
for (const file of listSourceFiles(morfoDir)) {
const component = basename(file, '.ts');
const source = readFileSync(file, 'utf8');
// `semantic:` following `name:` is what discriminates an EVENT from a
// part (parts have `name:` too). The optional middle line is `emission`
// (D.2) — without it in the pattern, an event carrying the flag between
// the two anchors would fall out of the census entirely: invisible, not
// exempt. Measured with the probe that added the flag to a real event.
const matches = [
...source.matchAll(
/name:\s*'([a-z]+-[a-z0-9-]+)',\s*\n(?:\s*emission:\s*'[a-z-]+',\s*\n)?\s*semantic:/g
)
];
if (matches.length === 0) continue;
const dir = join(somaDir, component);
if (!existsSync(dir)) continue;
const blob =
[...sourceFilesUnder(dir), ...sourceFilesUnder(join(eidosDir, component))]
.map((f) => readFileSync(f, 'utf8'))
.join('\n') + shared;
for (const [i, match] of matches.entries()) {
const name = match[1];
const id = `${component}.${name}`;
// D.2: a declared `emission` other than 'runtime' says WHO fires the
// event instead of this repo's providers — declared exemption, not
// debt. The chunk runs to the next event declaration, so the flag is
// read from the event it belongs to.
const chunk = source.slice(match.index, matches[i + 1]?.index);
const emission =
/emission:\s*'(runtime|host|external|declared-only)'/.exec(chunk)?.[1] ?? 'runtime';
if (emission !== 'runtime') {
if (INERT_EVENT_DEBT.has(id)) fixed.push(id);
continue;
}
const emitted = blob.includes(`'${name}'`) || blob.includes(`"${name}"`);
if (emitted) {
if (INERT_EVENT_DEBT.has(id)) fixed.push(id);
continue;
}
if (INERT_EVENT_DEBT.has(id)) continue;
violations.push(`${id}: declared, never emitted`);
}
}
for (const id of fixed.sort()) violations.push(`${id}: FIXED — remove it from INERT_EVENT_DEBT`);
return violations.sort();
}
// D8: `defaultElement` is a CONTRACT — `assertContract`, the eidos recipes and
// anyone reading the morfo take it as the tag that lands in the DOM. Nothing
// enforced it, and a census on 2026-08-12 found 32 parts where it lied. Seven
// were the whole `arrow` family (all declaring `div`; five render the shared
// primitive's `<span>`, two an `<svg>` of their own) and five were field labels
// declaring `label` while rendering `<div>`. Both are fixed; the rest are in
// `DEFAULT_ELEMENT_DEBT` below because each needs a per-row decision about
// WHICH side is wrong, and several are a11y calls (a declared `li` rendered as
// a `div` loses the list semantics APG expects). Ledger: D8 / D11.
const DEFAULT_ELEMENT_DEBT = new Set<string>([
// Each of these needs a change with CONSEQUENCES, not a declaration edit.
// Ledger D11 carries the reasoning per row.
// EMPTY, and it should stay that way. The 28 rows the census found were
// closed on 2026-08-12; the last four each needed a change with consequences
// rather than a declaration edit, and got one:
//
// chronos.event-chip the morfo was right (`div` + `role='button'`,
// because the chip CONTAINS the resize handle and
// a button admits no nested control). The
// affordances a real button gives for free —
// `tabindex` and Enter/Space activation — are
// built now, so the element could follow.
// tooltip.trigger a role-less focusable `div` became the `<button>`
// its morfo always declared, with the `type`
// literal so it cannot submit a form.
// stepper.item a generic wrapper between `role='tablist'` and
// `role='tab'` broke the owns relationship; it is
// `role='presentation'` now, like an `<li>` inside
// a menu.
// color-picker.channel-input declared `input` while composing
// `ColorField.Input`, which is `div` + `role='group'`.
// No composition could have satisfied it.
//
// A new entry here is a decision to ship a contract that lies. Fix the side
// that is wrong instead: the morfo when the component renders the more
// correct element, the component when the morfo does.
]);
/**
* The tag a soma part component renders as the PART element — the one that
* spreads the merged props. Follows one level of indirection (`<Arrow …>` →
* `components/internal/arrow.svelte`), so parts that delegate to a shared
* primitive are covered rather than silently skipped: that hole is what let the
* whole `arrow` family drift unnoticed.
*
* The rule is the SPREAD, not "the tag in the last `{:else}`". The first draft
* used the latter and went FALSE-GREEN: `internal/arrow.svelte`'s last `{:else}`
* holds a decorative `<svg {width} {height}>` with no spread, so the match
* failed, the tag came back `undefined`, and an injected regression on
* `popover.arrow` passed the guard. Caught by injecting one before trusting it.
*/
const PART_ELEMENT = /<([A-Za-z][A-Za-z0-9]*)\s+\{\.\.\.\w*[Pp]rops/g;
/**
* ALL of them, not the first: a part can be polymorphic by design —
* `media-player.media` renders `<audio>` or `<video>` by media kind,
* `breadcrumb.link` a `<span>` for the current page and an `<a>` otherwise.
* `defaultElement` is the DEFAULT, so the contract holds when the declared tag
* is among the branches; reading only the first would flag both as lies.
*/
function renderedTags(file: string, depth = 0): string[] {
if (!existsSync(file)) return [];
const source = readFileSync(file, 'utf8');
const out: string[] = [];
for (const [, tag] of source.matchAll(PART_ELEMENT)) {
if (tag[0] === tag[0].toLowerCase()) {
out.push(tag);
continue;
}
if (depth > 0) continue;
const spec = new RegExp(`import\\s+${tag}\\s+from\\s+'([^']+)'`).exec(source)?.[1];
if (spec) out.push(...renderedTags(join(dirname(file), spec), depth + 1));
}
return [...new Set(out)];
}
function collectDefaultElementViolations(): string[] {
const morfoDir = join(HERE, 'morfo', 'components');
const somaDir = join(HERE, 'soma', 'components');
const violations: string[] = [];
const fixed: string[] = [];
for (const file of listSourceFiles(morfoDir)) {
const component = basename(file, '.ts');
const source = stripComments(readFileSync(file, 'utf8'));
const parts = /kebab:\s*'([a-z0-9-]+)'[\s\S]{0,400}?defaultElement:\s*'([a-z0-9]+)'/g;
let match: RegExpExecArray | null;
while ((match = parts.exec(source))) {
const [, part, declared] = match;
const rendered = renderedTags(
join(somaDir, component, 'components', `${component}-${part}.svelte`)
);
if (rendered.length === 0) continue;
const id = `${component}.${part}`;
if (rendered.includes(declared)) {
if (DEFAULT_ELEMENT_DEBT.has(id)) fixed.push(id);
continue;
}
if (DEFAULT_ELEMENT_DEBT.has(id)) continue;
const shape = rendered.map((t) => `<${t}>`).join(' | ');
violations.push(`${id}: morfo says <${declared}>, component renders ${shape}`);
}
}
// A debt list that outlives its debt is a lie in the other direction.
for (const id of fixed.sort()) {
violations.push(`${id}: FIXED — remove it from DEFAULT_ELEMENT_DEBT`);
}
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();
}
// A30: a child must register its `inputId` with the parent Field via a DIRECT
// constructor assign, never inside a `$effect` — an effect for a one-shot id
// registration is wasteful + order-fragile (the 12 field-family providers that
// do it right assign directly; date/time/color-field drifted into an effect).
//
// The source MUST be `opts.<x>.current` — a prop that is already available at
// construction, so the effect is pure overhead. A reactive source set LATER by
// a child part (file-upload's `this.hiddenInputId`, '' until the hidden-input
// child mounts) genuinely needs the effect to observe it; that is NOT the
// anti-pattern and the `opts.*` requirement correctly leaves it green.
function collectA30Violations(): string[] {
const root = join(HERE, 'soma', 'components');
// `$effect(() => { … inputId.current = opts.X.current … })`, tempered so the
// assign must live INSIDE this effect's body (stop at the `});` that closes it).
const re =
/\$effect\s*\(\s*\(\s*\)\s*=>\s*\{(?:(?!\}\);)[\s\S]){0,400}?inputId\.current\s*=\s*opts\.\w+\.current/;
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, '')}: inputId registered inside a $effect (A30)`)
.sort();
}
// THEME-SYS-1: overlay z-index lives in a named scale, never a raw integer.
// A recipe `*-z` token must reference `var(--z-index-overlay-*)` (the flat
// overlay micro-band) or `var(--z-index-*)` (the depth-plane ladder) — a bare
// integer is an ad-hoc parallel scale that drifts out of order (the audit found
// tooltip 76 < dropdown 80). Local `z-index: 0..5` in component CSS is relative
// stacking, NOT this — it stays (THEMING.md §35). This guards the recipe defs.
function collectMagicZViolations(): string[] {
const base = stripComments(readFileSync(join(HERE, 'eidos', 'lib', 'recipes', 'base.ts'), 'utf8'));
const violations: string[] = [];
const re = /'([\w-]*-z)':\s*'(\d[^']*)'/g;
let match: RegExpExecArray | null;
while ((match = re.exec(base))) {
violations.push(
`recipes/base.ts: '${match[1]}': '${match[2]}' is a raw z-index — use var(--z-index-overlay-*)`
);
}
return violations.sort();
}
function collectComponentLangPathViolations(source: string, file: string): string[] {
const clean = stripComments(source);
const violations: string[] = [];
const refRe = /#\?components\.([^|'"`\s]+)/g;
let match: RegExpExecArray | null;
while ((match = refRe.exec(clean))) {
const path = match[1];
const segments = path.split('.');
if (segments.some((segment) => /[A-Z]/.test(segment))) {
violations.push(`${file.replace(REPO_ROOT, '')}: components.${path}`);
}
}
return violations;
}
function collectLangCatalogKeyViolations(node: unknown, prefix = 'components'): string[] {
if (!node || typeof node !== 'object') return [];
return Object.entries(node as Record<string, unknown>).flatMap(([key, value]) => {
const path = `${prefix}.${key}`;
const ownViolation = /[A-Z]/.test(key) ? [path] : [];
return [...ownViolation, ...collectLangCatalogKeyViolations(value, path)];
});
}
function collectBrokenSomaComponentReadmeLinks(): string[] {
const root = join(HERE, 'soma', 'components');
return collectPublicSomaComponentDirs().flatMap((dir) => {
const file = join(root, dir, 'README.md');
const source = readFileSync(file, 'utf8');
const violations: string[] = [];
const linkRe = /\]\((\.\.\/[^)]+README\.md)\)/g;
let match: RegExpExecArray | null;
while ((match = linkRe.exec(source))) {
const target = join(root, dir, match[1]);
if (!existsSync(target)) {
violations.push(`${dir}/README.md -> ${match[1]}`);
}
}
return violations;
});
}
describe('UIX layer contracts', () => {
// S-19(ii) — ONE sink, materialized by the root. Only the composition root
// holds both ends (the shared live-region sink and the engine), so it
// registers the announce channel with a late-bound closure; an app-written
// wiring could only reach the channel's self-owned fallback — a SECOND
// live pair, the thing AUX-1 forbids.
it('registers the announce channel on the SHARED sink (S-19 ii)', async () => {
const uix = createActiveUix({ langs: minimalLang });
try {
expect(uix.events?.getChannel('announce')).toBeDefined();
const target = document.createElement('div');
document.body.appendChild(target);
await uix.events!.emit({
target,
name: 'signal-announce',
family: 'signal',
intent: 'threat',
message: 'Peligro'
});
// The message lands in uix's OWN assertive region…
expect(document.getElementById('uix-announce-assertive')?.textContent).toContain('Peligro');
// …and the channel never grew its fallback pair: ONE alert region total.
expect(document.querySelectorAll("[role='alert']").length).toBe(1);
target.remove();
} finally {
uix.dispose();
}
});
it('honours the explicit announce opt-out at the root', () => {
const uix = createActiveUix({ langs: minimalLang, events: { announce: false } });
try {
expect(uix.events?.getChannel('announce')).toBeUndefined();
} finally {
uix.dispose();
}
});
it('pins ActiveUix public service names and rejects legacy aliases', () => {
const uix = createActiveUix({ langs: minimalLang });
try {
for (const key of UIX_LAYER_CONTRACTS.activeUix.publicSurface) {
expect(key in uix, key).toBe(true);
}
for (const key of UIX_LAYER_CONTRACTS.activeUix.forbiddenPublicSurface) {
expect(key in uix, key).toBe(false);
}
expect(uix.ownsApp).toBe(true);
expect(uix.app).toBeUndefined();
expect(uix.events).toBeInstanceOf(EngineSemantic);
} finally {
uix.dispose();
}
});
it('keeps attach mode as consumption-only: required services are langs + dom', () => {
expect(UIX_REQUIRED_SERVICES).toEqual(UIX_LAYER_CONTRACTS.activeUix.attachRequires);
const app = createActiveApp({ services: {} });
try {
attachActiveUix(app as unknown as ActiveApp);
expect.fail('attachActiveUix should reject an app without UIX services');
} catch (err) {
if (!(err instanceof ActiveUixMissingServiceError)) throw err;
expect(err.missing).toEqual(['langs', 'dom']);
expect(err.missing).not.toContain('events');
} finally {
app.dispose();
}
});
it('defineUixServices publishes events, never semantic, and cannot attach without dom', () => {
const services = defineUixServices({ langs: minimalLang });
expect(Object.keys(services).sort()).toEqual([
'clipboard',
'dom',
'events',
'format',
'langs',
'motion',
'scene',
'sound'
]);
expect('semantic' in services).toBe(false);
expect('lang' in services).toBe(false);
expect('frontend' in services).toBe(false);
const noDomServices = defineUixServices({ langs: minimalLang, dom: false });
expect('dom' in noDomServices).toBe(false);
expect('events' in noDomServices).toBe(false);
expect('motion' in noDomServices).toBe(false);
expect('sound' in noDomServices).toBe(false);
const app = createActiveApp({ services: noDomServices });
try {
expect(() => attachActiveUix(app as unknown as ActiveApp)).toThrow(
ActiveUixMissingServiceError
);
} finally {
app.dispose();
}
});
it('guards ActiveUix as composition root, not Soma/Eidos bootstrapper', () => {
const source = readFileSync(join(HERE, 'active-uix', 'active-uix.svelte.ts'), 'utf8');
expect(source).not.toMatch(/from ['"].*\/soma/);
expect(source).not.toMatch(/from ['"].*\/eidos/);
expect(source).not.toMatch(/\bSoma\.create\(/);
expect(source).not.toMatch(/\bActiveEidos\.create\(/);
expect(source).not.toMatch(/\bEidos\.create\(/);
});
it('prefs DOM projection owns only cross-modal attrs', () => {
expect(UIX_LAYER_CONTRACTS.prefsDomProjection.implementation).toBe(
'$prefs.createActivePrefsDomProjection'
);
expect(UIX_LAYER_CONTRACTS.prefsDomProjection.requires).toEqual(['prefs', 'dom']);
expect(UIX_LAYER_CONTRACTS.prefsDomProjection.ownsAttrs).toEqual([
'dir',
'lang',
'data-motion',
'data-sound',
'data-haptic'
]);
expect(UIX_LAYER_CONTRACTS.prefsDomProjection.forbiddenAttrs).toEqual([
'data-theme',
'data-mode',
'data-density'
]);
});
it('implements cross-modal projection in prefs, not ActiveUix', () => {
const dom = createActiveDom();
const prefs = createActivePrefs({ schema: prefsProjectionSchema });
const target = document.createElement('div');
const projection = createActivePrefsDomProjection({ prefs, dom, target });
try {
for (const attr of UIX_LAYER_CONTRACTS.prefsDomProjection.ownsAttrs) {
expect(target.hasAttribute(attr), attr).toBe(true);
}
for (const attr of UIX_LAYER_CONTRACTS.prefsDomProjection.forbiddenAttrs) {
expect(target.hasAttribute(attr), attr).toBe(false);
}
} finally {
projection.dispose();
prefs.dispose();
dom.dispose();
}
});
it('SomaRuntime has one public part API and only depends on injected services', async () => {
const dom = createActiveDom();
try {
const runtime = createSomaRuntime(toggleMorfo, { dom });
for (const key of UIX_LAYER_CONTRACTS.somaRuntime.publicSurface) {
expect(key in runtime, key).toBe(true);
}
for (const key of UIX_LAYER_CONTRACTS.somaRuntime.forbiddenPublicSurface) {
expect(key in runtime, key).toBe(false);
}
const id = state('toggle-contract');
runtime.part('provider', { id });
expect(runtime.partProps('provider')).toMatchObject({
id: 'toggle-contract',
'data-toggle': ''
});
} finally {
dom.dispose();
}
});
it('guards Soma components from legacy runtime registration APIs', () => {
const violations = grepSources(
join(HERE, 'soma', 'components'),
/registerContract\(|registerPart\(/
);
expect(violations).toEqual([]);
});
it('guards Soma component barrel exports all public component modules', () => {
const indexSource = readFileSync(join(HERE, 'soma', 'components', 'index.ts'), 'utf8');
const missingExports = collectPublicSomaComponentDirs()
.map((dir) => ({
dir,
name: toPascalName(dir),
exportRe: new RegExp(`export \\* as ${toPascalName(dir)} from ['"]\\./${dir}['"];`)
}))
.filter(({ exportRe }) => !exportRe.test(indexSource))
.map(({ dir, name }) => `${dir} -> ${name}`);
expect(missingExports).toEqual([]);
});
it('guards Soma Provider exports use the canonical root wrapper filename', () => {
const violations = collectPublicSomaComponentDirs()
.filter((dir) => {
const source = readFileSync(join(HERE, 'soma', 'components', dir, 'exports.ts'), 'utf8');
const expected = `export { default as Provider } from './components/${dir}.svelte';`;
return !source.includes(expected);
})
.map((dir) => `${dir}/exports.ts`);
expect(violations).toEqual([]);
});
it('guards Date/Time field roots from swallowing provider DOM attrs', () => {
for (const dir of ['date-field', 'time-field']) {
const source = readFileSync(
join(HERE, 'soma', 'components', dir, 'components', `${dir}.svelte`),
'utf8'
);
expect(source, dir).toContain('ref = $bindable(null)');
expect(source, dir).toContain('...restProps');
expect(source, dir).toContain('const mergedProps');
expect(source, dir).toContain('<div {...mergedProps}>');
expect(source, dir).not.toContain('child({ props: {} })');
}
});
it('guards picker roots are DOM-passthrough composition shells', () => {
// Pickers are DOM-passthrough composition shells: they render a
// `<div>` (so morfo runtime / sema can latch onto the root),
// expose `ref` + `child` + DOM attrs via `WithChild`, and accept
// children as composition slot. The wrapper must merge restProps
// into the rendered element and offer both the child-snippet
// branch and the children branch.
const roots = [
['date-picker', 'DatePickerProviderProps'],
['date-range-picker', 'DateRangePickerProviderProps'],
['time-picker', 'TimePickerProviderProps'],
['time-range-picker', 'TimeRangePickerProviderProps']
] as const;
for (const [dir, typeName] of roots) {
const types = readFileSync(join(HERE, 'soma', 'components', dir, 'types.ts'), 'utf8');
const wrapper = readFileSync(
join(HERE, 'soma', 'components', dir, 'components', `${dir}.svelte`),
'utf8'
);
// Match both shapes:
// `WithChild<{ ... }>;`
// `WithChild<Omit<PrimitiveDivAttributes, ...> & { ... }>;`
// per audit-codex P0/P1 refactor of picker ProviderProps to
// extend PrimitiveDivAttributes.
const rootType =
new RegExp(`export type ${typeName}\\s*=\\s*WithChild<[\\s\\S]*?\\}>;`).exec(types)?.[0] ?? '';
expect(rootType, `${dir}.types.ts uses WithChild<{...}>`).not.toBe('');
expect(rootType, dir).toContain('children?: Snippet;');
expect(wrapper, dir).toContain('ref = $bindable(null)');
expect(wrapper, dir).toContain('...restProps');
expect(wrapper, dir).toContain('<div {...mergedProps}>');
expect(wrapper, dir).toContain('{@render children?.()}');
expect(wrapper, dir).not.toContain('child({ props: {} })');
}
});
it('guards Soma public barrels from provider implementation filenames', () => {
const violations = collectPublicSomaComponentDirs()
.map((dir) => join(HERE, 'soma', 'components', dir, 'exports.ts'))
.filter((file) =>
/from ['"][^'"]*provider\.svelte(?:\.ts)?['"]/.test(readFileSync(file, 'utf8'))
)
.map((file) => file.replace(REPO_ROOT, ''));
expect(violations).toEqual([]);
});
it('guards Soma public barrels from shared lib facades', () => {
const violations = collectPublicSomaComponentDirs()
.map((dir) => join(HERE, 'soma', 'components', dir, 'exports.ts'))
.filter((file) => /from ['"]\$libs\//.test(readFileSync(file, 'utf8')))
.map((file) => file.replace(REPO_ROOT, ''));
expect(violations).toEqual([]);
});
it('guards Soma public component modules against Morfo soma scope', () => {
// Internal bridges may declare 'soma' truthfully (they implement soma
// behaviour) without being public namespaces — subtract them so the
// dir↔scope equality compares public surfaces on both sides.
const publicSomaScoped = collectMorfoComponentNamesByScope('soma').filter(
(name) => !SOMA_INTERNAL_PRIMITIVES.has(name)
);
expect(collectPublicSomaComponentDirs()).toEqual(publicSomaScoped);
});
it("guards every component with an eidos recipe declares 'eidos' in morfo scope (SYS-1)", () => {
const violations = collectScopeDriftViolations();
expect(
violations,
`scope-drift — add 'eidos' to the morfo scope of:\n ${violations.join('\n ')}`
).toEqual([]);
});
it('guards every eidos component dir is enumerable by the acceptance machinery (MOR-4)', () => {
const violations = collectMorfoEnumerationViolations();
expect(
violations,
`morfo-enumeration holes:\n ${violations.join('\n ')}`
).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([]);
});
// S-34: `SEMA_FAMILY_POLICY` is declared the ONE source of the intent policy
// ("editing it reshapes the discriminated unions"), and the TYPE side honours
// that — `IntentRequiredFamily` derives from it. The RUNTIME side does not:
// the sium schema that validates all 172 morfos re-writes the same partition
// as fixed literals, so editing the const would move the compile-time gate
// and leave the runtime one on the old partition, silently.
//
// Deriving the schema from the const instead would buy a VALUE import
// morfo → sema, and that edge is deliberately type-only today (morfo is "the
// contract / DNA; pure TypeScript, no runtime" — theming/channels.md §3).
// So the two sides are tied HERE, in the cross-layer contract test, which
// already imports both.
it('guards the morfo runtime schema against the intent policy const (S-34)', () => {
const source = readFileSync(join(HERE, 'morfo', 'schema.ts'), 'utf8');
// Each `const X = union(...)` in the schema, expanded through references
// to other unions declared above it.
const unions = new Map<string, string[]>();
for (const m of source.matchAll(/const (sema\w*FamilySchema) = union\(([\s\S]*?)\);/g)) {
const members = [...m[2].matchAll(/literal\('([a-z-]+)'\)|\b(sema\w*FamilySchema)\b/g)];
unions.set(
m[1],
members.flatMap(([, lit, ref]) => (lit ? [lit] : (unions.get(ref) ?? [])))
);
}
const fromPolicy = (required: boolean) =>
Object.entries(SEMA_FAMILY_POLICY)
.filter(([, p]) => (p.intentRequirement === 'required') === required)
.map(([family]) => family)
.sort();
const drift = 'intent policy drift — the schema partition no longer matches SEMA_FAMILY_POLICY';
expect([...(unions.get('semaIntentRequiredFamilySchema') ?? [])].sort(), drift).toEqual(
fromPolicy(true)
);
expect([...(unions.get('semaIntentOptionalFamilySchema') ?? [])].sort(), drift).toEqual(
fromPolicy(false)
);
});
it('guards declared events against the ones a provider actually emits (D9)', () => {
const violations = collectInertEventViolations();
expect(
violations,
`declared and never emitted — inert, mute, and reading as if they did something:\n ${violations.join('\n ')}\n\n` +
'Emit it, or retire the declaration. Do not widen INERT_EVENT_DEBT to pass.'
).toEqual([]);
});
it('guards defaultElement against the tag the soma part actually renders (D8)', () => {
const violations = collectDefaultElementViolations();
expect(
violations,
`defaultElement lies about the rendered tag:\n ${violations.join('\n ')}\n\n` +
'Fix the side that is wrong — the morfo when the component renders the more\n' +
'correct element, the component when the morfo does. Do not widen the\n' +
'allow-list to make this pass.'
).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 inputId Field registration against $effect wrapping (A30)', () => {
const violations = collectA30Violations();
expect(
violations,
`A30 — assign inputId directly in the constructor, not in a $effect:\n ${violations.join('\n ')}`
).toEqual([]);
});
it('guards overlay z-index against raw integers in recipes (THEME-SYS-1)', () => {
const violations = collectMagicZViolations();
expect(
violations,
`magic z-index — reference the overlay scale:\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'))
);
expect(missingReadmes).toEqual([]);
});
it('guards Soma component README links to existing component docs', () => {
expect(collectBrokenSomaComponentReadmeLinks()).toEqual([]);
});
it('guards Soma from external re-export facades', () => {
expect(existsSync(join(HERE, 'soma', 'external'))).toBe(false);
const violations = grepSources(join(HERE, 'soma'), /external\/(?:dates|colors)/);
expect(violations).toEqual([]);
});
it('guards Soma root barrel as scope-only', () => {
const source = readFileSync(join(HERE, 'soma', 'index.ts'), 'utf8');
expect(source).toContain("from './core/soma.svelte'");
expect(source).toContain("from './errors'");
expect(existsSync(join(HERE, 'soma', 'core', 'langs.ts'))).toBe(false);
expect(source).not.toMatch(
/from ['"]\.\/(?:provider|props|keyboard|typeahead|css|id|types|runtime\.svelte|components|layers|datetime|color)/
);
});
it('guards Soma internals from importing their own public alias', () => {
const violations = listSourceFiles(join(HERE, 'soma'))
.filter((file) => {
const source = stripComments(readFileSync(file, 'utf8'));
return /from ['"]\$soma(?:['"]|\/)/.test(source);
})
.map((file) => file.replace(REPO_ROOT, ''));
expect(violations).toEqual([]);
});
it('guards Soma from direct mutable DOM writes', () => {
// The rule is about the LIVE document: a UIX-managed attr must travel
// through `dom.apply` so one owner writes it. A file that only ever
// touches a DETACHED buffer is outside that concern — there is nothing
// to coordinate, and no `ActiveDom` to route through.
//
// `palabras/engine/operations/parse-html.ts` is the paste pipeline: it
// works on an inert `DOMParser` document (its own header: "it's a parse
// buffer (inert document / detached container), never live"), marking
// Word's fake-list paragraphs before regrouping them. The marker never
// reaches the page; `normalizeDocument` consumes and drops it.
const detachedBufferOnly = join(
HERE,
'soma',
'components',
'palabras',
'engine',
'operations',
'parse-html.ts'
);
const violations = grepSources(
join(HERE, 'soma'),
/\.setAttribute\(|\.removeAttribute\(|\.toggleAttribute\(|\.classList\.|\.style\.setProperty\(|\.style\.[a-zA-Z]+\s*=/
).filter((file) => file !== detachedBufferOnly);
expect(violations).toEqual([]);
});
it('guards Soma timers through the injected scheduler (raw only as the no-timers fallback)', () => {
// Widened from soma/components to ALL of soma (SOM-3, clean-room
// 2026-07-10: layers/ + datetime/ escaped the old scope and held raw
// timers for months). Evidence-based instead of a file allowlist: a
// file may keep a raw setTimeout ONLY as the documented no-timers
// fallback of the scheduler-preferred shape (semaDelay / dismissal /
// safe-polygon / announce-global / announcer / image-provider), which
// the same file evidences by ALSO calling `.schedule(` on the injected
// service. A raw timer with no scheduler path anywhere in the file is
// a violation.
const violations = listSourceFiles(join(HERE, 'soma')).filter((file) => {
const source = stripComments(readFileSync(file, 'utf8'));
if (!/\bsetTimeout\(|\bsetInterval\(/.test(source)) return false;
return !/\.schedule\(|\.interval\(/.test(source);
});
expect(violations).toEqual([]);
});
it('guards Soma components through the shared typeahead helper', () => {
const violations = grepSources(
join(HERE, 'soma', 'components'),
/class\s+Typeahead|typeaheadBuffer|typeaheadTimerKey|clearTypeaheadTimer/
);
expect(violations).toEqual([]);
});
it('guards Soma component listeners through ActiveDom', () => {
// media-provider.ts is the pluggable playback-engine PORT — Svelte-
// agnostic BY DESIGN (its header: "no runes, no $adom"): third-party
// engines (hls.js adapters, embeds) implement the same contract, so it
// must not depend on the component runtime. Its listeners live on the
// engine's own element and are cleaned in destroy().
const allowedPort = join(HERE, 'soma', 'components', 'media-player', 'media-provider.ts');
const violations = grepSources(
join(HERE, 'soma', 'components'),
/\.(addEventListener|removeEventListener)\(/
).filter((file) => file !== allowedPort);
expect(violations).toEqual([]);
});
it('guards hardcoded Soma component data attrs with morfo contracts', () => {
const knownAttrs = collectKnownMorfoDataAttrs();
const violations = listSourceFiles(join(HERE, 'soma', 'components')).flatMap((file) => {
const attrs = collectDataAttrs(readFileSync(file, 'utf8'));
return [...attrs]
.filter((attr) => !knownAttrs.has(attr))
.map((attr) => `${file.replace(REPO_ROOT, '')}: ${attr}`);
});
expect(violations).toEqual([]);
});
it('guards component translation namespaces as kebab-case', () => {
const sourceRoots = [join(HERE, 'soma', 'components'), join(HERE, 'morfo', 'components')];
const pathViolations = sourceRoots.flatMap((root) =>
listSourceFiles(root).flatMap((file) =>
collectComponentLangPathViolations(readFileSync(file, 'utf8'), file)
)
);
expect(pathViolations).toEqual([]);
expect(collectLangCatalogKeyViolations(commonLangs, 'common')).toEqual([]);
});
it('events/sema uses events as service name and requires dom or projector for visual projection', () => {
const factory = defineEngineSemantic({ visual: false });
expect(factory.name).toBe(UIX_LAYER_CONTRACTS.events.publicServiceName);
// Derived from the table, never hardcoded: `sound` is in there because the
// factory forwards it as `soundEngine` (single AudioContext in attach).
expect(factory.serviceDependencies).toEqual(UIX_LAYER_CONTRACTS.events.serviceDependencies);
expect(UIX_LAYER_CONTRACTS.events.morfoPayloadName).toBe('semantic');
expect(UIX_LAYER_CONTRACTS.events.requiresOneOfForVisualProjection).toEqual([
'dom',
'projector'
]);
expect(() => new EngineSemantic()).toThrow(SemaConfigError);
expect(() => new EngineSemantic({ visual: false })).not.toThrow();
});
it('guards Sema production code from direct global listeners and console output', () => {
const violations = listSourceFiles(join(HERE, 'sema'))
.filter((file) => {
const source = stripComments(readFileSync(file, 'utf8'));
return /(?:document|window)\.(?:addEventListener|removeEventListener)\(|console\./.test(
source
);
})
.map((file) => file.replace(REPO_ROOT, ''));
expect(violations).toEqual([]);
});
it('ActiveEidos is the visual runtime and does not recreate an engine layer', () => {
expect(UIX_LAYER_CONTRACTS.activeEidos.publicRuntime).toBe('ActiveEidos');
expect(existsSync(new URL('./eidos/engine', import.meta.url))).toBe(false);
expect(existsSync(new URL('./eidos/core/headless', import.meta.url))).toBe(false);
expect(UIX_LAYER_CONTRACTS.activeEidos.constructionOptionalServices).toEqual([
'langs',
'format',
'prefs'
]);
expect(UIX_LAYER_CONTRACTS.activeEidos.requiredAccessors).toEqual(['langs', 'prefs']);
expect(UIX_LAYER_CONTRACTS.activeEidos.optionalAccessors).toEqual(['format']);
expect(() => createActiveEidos({ preferences: preferencesHarness() })).toThrow(
ActiveEidosConfigError
);
const eidos = createActiveEidos({
preferences: preferencesHarness(),
applyDom: false
});
expect(eidos.renderStaticCss()).toContain('--size-md-control-height');
expect(() => eidos.prefs).toThrow(ActiveEidosConfigError);
expect(eidos.format).toBeUndefined();
eidos.dispose();
});
it('guards Eidos public surface from wrapper/engine reintroduction', () => {
const eidosRoot = join(HERE, 'eidos');
const violations = grepSources(
eidosRoot,
/\bEngineEidos\b|\bclass\s+Eidos\b|\bEidos\.create\(|from ['"].*\/engine/
);
expect(violations).toEqual([]);
});
it('guards UIX docs shell from writing visual prefs through ActivePrefs', () => {
const violations = grepSources(
join(REPO_ROOT, 'web', 'routes', 'uix'),
/prefs\.theme|setIntent\(['"]theme['"]/
);
expect(violations).toEqual([]);
});
it('Morfo declares text slots as absolute idlangrefs (texts:) — catalog lives outside the morfo', () => {
_resetMorfoRegistryForTesting();
try {
const sample: Morfo = {
name: 'ContractSample',
kebab: 'contract-sample',
scope: ['soma'],
texts: {
label: '#?components.contract-sample.label|Contract'
},
parts: [
{
name: 'Provider',
kebab: 'provider',
kind: 'public',
defaultElement: 'div',
optional: false,
data: [],
aria: [
{
attr: 'aria-label',
value: v.translationRef('#?components.contract-sample.label|Contract')
}
]
}
]
};
registerMorfo(sample);
// Morfo owns the abstract surface (text slots as idlangrefs); content
// lives in src/uix/langs/components/{kebab}.ts and is merged by
// ActiveUix into the components.* namespace.
expect(sample.texts?.label).toBe('#?components.contract-sample.label|Contract');
expect(sample.texts?.label.startsWith('#?')).toBe(true);
} finally {
_resetMorfoRegistryForTesting();
}
});
});

Powered by TurnKey Linux.