|
|
|
|
// @vitest-environment jsdom
|
|
|
|
|
|
|
|
|
|
import { existsSync, readdirSync, readFileSync, statSync } from 'node:fs';
|
|
|
|
|
import { basename, dirname, join } 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 { 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,
|
Theming: add `scaling` zoom axis (Radix parity), separate from density
Introduce a global zoom axis independent of density, in parity with Radix
Themes' `scaling` (90/95/100/105/110%). Scaling zooms px metrics INCLUDING
typography (font-size, icon-size, space, control-height); density only moves
layout rhythm + control height and leaves text fixed. The two axes compose
multiplicatively.
- config-types: SCALING_KEYS / ScalingKey / DEFAULT_SCALING; DensityPrimitiveSet
drops the dead `scale` + `contentScale` (kept spaceScale, controlScale).
- primitives/static: STATIC_SCALING (0.9..1.1).
- render-css: appendScaledMetricDeclarations wraps metrics in
calc(<raw>[ * var(--density-x-scale)] * var(--scaling)); appendScalingDeclarations
emits --scaling-{key} + --scaling default; renderScalingBlocks emits
[data-scaling] blocks. line-height/radius/border/shadow excluded.
- config + contract: prune the removed density scalars.
- active-eidos: `scaling` / `scalingSource` options, getScaling() on the
preference source, data-scaling projection + dispose cleanup.
- docs: THEMING.md section 23 + 20.1 reconcile; README density/scaling; SCALING_RFC.md.
Verified: npm run check (0 new errors), vitest eidos (0 new regressions),
browser cascade at 90/100/110 scales font/space/control x0.9/x1.1 and leaves
radius/border fixed.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
4 months ago
|
|
|
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')}`)
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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]);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
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.
|
|
|
|
|
const SOMA_INTERNAL_PRIMITIVES = new Set(['picker-shell']);
|
|
|
|
|
|
|
|
|
|
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
|
|
|
|
|
// (their morfo/eidos are still being authored). They are brought into
|
|
|
|
|
// compliance when their track lands; until then the catalogue guards skip them.
|
|
|
|
|
const ACTIVE_DEV_TRACK = new Set(['words', 'palabras', 'chronos', 'gradient-builder']);
|
|
|
|
|
|
|
|
|
|
// 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();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 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();
|
|
|
|
|
}
|
|
|
|
|
|
fix(eidos): THEME-SYS-1 — consolidate overlay z-index into a named scale (+ guard + docs)
The ~12 overlay recipes hardcoded an ad-hoc parallel z-index scale (raw integers
60–99/1200) that duplicated nothing reusable and had drifted out of order
(tooltip 76 < dropdown 80 — a tooltip painted BEHIND a dropdown). They are now a
named scale.
- `STATIC_Z_INDEX_OVERLAY` (static.ts) → emitted as `--z-index-overlay-{inline,
backdrop,content,floating,tooltip,detached,toast}`. A SEPARATE scale from the
global `--z-index-*` ladder (which orders the depth planes) — overlays portal
to <body> as siblings of modals, so they share one flat low band where each
rung sits just above the modal scrim. Mapping them to the 300–900 ladder would
hide a dropdown/select/popover opened INSIDE a dialog (dropdown 300 < modal
700); the combobox recipe already warned about this. `tooltip` now sits above
`floating` (fixes the inversion); `toast` stays above the soma FloatPanel band.
- Every overlay recipe token (`content-z`/`overlay-z`/`inline-z`/`toaster-z`/
`preview-z`) now references `var(--z-index-overlay-*)` — zero raw integers.
dialog/drawer gain an explicit `content-z` rung (drops the `calc(... + 1)`).
- Guard (contracts.test.ts, "overlay z-index against raw integers"): a recipe
`*-z` token must reference the scale, never a bare integer. Proven to catch
drift (a raw `'76'` makes it fail). Local `z-index: 0..5` (avatar/tabs/sticky)
is intra-component relative stacking — out of scope, stays.
- Docs: THEMING.md §35 rewritten to describe the consolidated scale + the
flat-band rationale + the guard; token table gains `--z-index-overlay-*`;
testing-and-tooling.md documents the catalogue guards (VG-8/SYS-1/A31/A30/
THEME-SYS-1).
Stacking order verified from the resolved CSS (deterministic z compare: content
70 < floating 80 < tooltip 90 < toast 1200; dropdown-in-dialog preserved). A
live browser check was blocked by a port conflict with another session's server.
check: 0 new type errors; the 5 guards green.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
3 months ago
|
|
|
// 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', () => {
|
|
|
|
|
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 });
|
fix: address architectural audit findings + sync ecosystem docs
Audit: src/audit-opus-4-6-26.md. All non-words findings remediated.
Code:
- E1: navigation-menu indicator data-state visible -> open (render bug; the
active underline was permanently invisible). Clears the only invalid lint selector.
- SO1 + E2: raw `new ResizeObserver` -> ActiveDom.observeResize in carousel-provider
and the canvas-text useContainerWidth hook (+ s-text / s-text-virtual-list pass
eidos.dom). iframe/popup-safe, lifecycle-tracked.
- A1: defineUixServices now registers `motion`, so attach-mode app.motion is real
and the active-uix fallback becomes the true edge case (test guard updated).
- A2/A4: contracts.ts pins `motion` + `announce` in ActiveUixServiceContract +
publicSurface; dispose() comment corrected.
- SO2: carousel drops the hardcoded `transform 300ms ease-out` (the recipe already
handles it via [data-dragging]); also fixed the recipe's undefined `--duration-base`
token -> `--duration-slow` (it was masked by the inline).
- S1: HapticChannel reduced-motion via an injected ActiveDom port (mirrors
SoundChannelDom) instead of global matchMedia.
- T1: 10 sites repointed `$libs/dom` -> `$adom` (sema x5 + its tests x3, active-uix
value import, arts/prefs).
- M2 / S2 / S3: dead code removed (button `states:['idle','loading']`,
SemaRuntimeChannelId, SEMA_VALENCED_FAMILY_LIST).
Docs:
- Motion-as-service reflected across the ecosystem: CLAUDE.md (aliases + arch +
service note), arts/README, active_architecture, soma SOMA_ARCHITECTURE, eidos
README, eidos-motion.md.
- X1: CLAUDE.md "5 canonical channels" (false) -> the single canonical narrative
(8 book channels; Sema runs 2 + visual meta-channel, Eidos materializes 5).
- X2/X3/X4: sema/README (8 families + intentRequirement/intentGuidance split),
types.ts JSDoc (SEMA_INTENT_POLICY -> SEMA_FAMILY_POLICY), engine.ts cascade 6->5,
alias table ($frontend out, $lang->$langs, +$clipboard).
- E4: codex_audit.md HISTORICO banner.
Deferred: SU2 (test-only layering, not a build violation); Words M1/T5/E3 (WIP).
Verify: npm run check -> 1 pre-existing error (grafito), 0 new; sema 152/152,
contracts 31/32 (1 pre-existing words), carousel 4/4, motion 22/22.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
4 months ago
|
|
|
expect(Object.keys(services).sort()).toEqual([
|
|
|
|
|
'clipboard',
|
|
|
|
|
'dom',
|
|
|
|
|
'events',
|
|
|
|
|
'format',
|
|
|
|
|
'langs',
|
|
|
|
|
'motion'
|
|
|
|
|
]);
|
|
|
|
|
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);
|
fix: address architectural audit findings + sync ecosystem docs
Audit: src/audit-opus-4-6-26.md. All non-words findings remediated.
Code:
- E1: navigation-menu indicator data-state visible -> open (render bug; the
active underline was permanently invisible). Clears the only invalid lint selector.
- SO1 + E2: raw `new ResizeObserver` -> ActiveDom.observeResize in carousel-provider
and the canvas-text useContainerWidth hook (+ s-text / s-text-virtual-list pass
eidos.dom). iframe/popup-safe, lifecycle-tracked.
- A1: defineUixServices now registers `motion`, so attach-mode app.motion is real
and the active-uix fallback becomes the true edge case (test guard updated).
- A2/A4: contracts.ts pins `motion` + `announce` in ActiveUixServiceContract +
publicSurface; dispose() comment corrected.
- SO2: carousel drops the hardcoded `transform 300ms ease-out` (the recipe already
handles it via [data-dragging]); also fixed the recipe's undefined `--duration-base`
token -> `--duration-slow` (it was masked by the inline).
- S1: HapticChannel reduced-motion via an injected ActiveDom port (mirrors
SoundChannelDom) instead of global matchMedia.
- T1: 10 sites repointed `$libs/dom` -> `$adom` (sema x5 + its tests x3, active-uix
value import, arts/prefs).
- M2 / S2 / S3: dead code removed (button `states:['idle','loading']`,
SemaRuntimeChannelId, SEMA_VALENCED_FAMILY_LIST).
Docs:
- Motion-as-service reflected across the ecosystem: CLAUDE.md (aliases + arch +
service note), arts/README, active_architecture, soma SOMA_ARCHITECTURE, eidos
README, eidos-motion.md.
- X1: CLAUDE.md "5 canonical channels" (false) -> the single canonical narrative
(8 book channels; Sema runs 2 + visual meta-channel, Eidos materializes 5).
- X2/X3/X4: sema/README (8 families + intentRequirement/intentGuidance split),
types.ts JSDoc (SEMA_INTENT_POLICY -> SEMA_FAMILY_POLICY), engine.ts cascade 6->5,
alias table ($frontend out, $lang->$langs, +$clipboard).
- E4: codex_audit.md HISTORICO banner.
Deferred: SU2 (test-only layering, not a build violation); Words M1/T5/E3 (WIP).
Verify: npm run check -> 1 pre-existing error (grafito), 0 new; sema 152/152,
contracts 31/32 (1 pre-existing words), carousel 4/4, motion 22/22.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
4 months ago
|
|
|
expect('motion' 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',
|
|
|
|
|
'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', () => {
|
|
|
|
|
expect(collectPublicSomaComponentDirs()).toEqual(collectMorfoComponentNamesByScope('soma'));
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
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 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 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([]);
|
|
|
|
|
});
|
|
|
|
|
|
fix(eidos): THEME-SYS-1 — consolidate overlay z-index into a named scale (+ guard + docs)
The ~12 overlay recipes hardcoded an ad-hoc parallel z-index scale (raw integers
60–99/1200) that duplicated nothing reusable and had drifted out of order
(tooltip 76 < dropdown 80 — a tooltip painted BEHIND a dropdown). They are now a
named scale.
- `STATIC_Z_INDEX_OVERLAY` (static.ts) → emitted as `--z-index-overlay-{inline,
backdrop,content,floating,tooltip,detached,toast}`. A SEPARATE scale from the
global `--z-index-*` ladder (which orders the depth planes) — overlays portal
to <body> as siblings of modals, so they share one flat low band where each
rung sits just above the modal scrim. Mapping them to the 300–900 ladder would
hide a dropdown/select/popover opened INSIDE a dialog (dropdown 300 < modal
700); the combobox recipe already warned about this. `tooltip` now sits above
`floating` (fixes the inversion); `toast` stays above the soma FloatPanel band.
- Every overlay recipe token (`content-z`/`overlay-z`/`inline-z`/`toaster-z`/
`preview-z`) now references `var(--z-index-overlay-*)` — zero raw integers.
dialog/drawer gain an explicit `content-z` rung (drops the `calc(... + 1)`).
- Guard (contracts.test.ts, "overlay z-index against raw integers"): a recipe
`*-z` token must reference the scale, never a bare integer. Proven to catch
drift (a raw `'76'` makes it fail). Local `z-index: 0..5` (avatar/tabs/sticky)
is intra-component relative stacking — out of scope, stays.
- Docs: THEMING.md §35 rewritten to describe the consolidated scale + the
flat-band rationale + the guard; token table gains `--z-index-overlay-*`;
testing-and-tooling.md documents the catalogue guards (VG-8/SYS-1/A31/A30/
THEME-SYS-1).
Stacking order verified from the resolved CSS (deterministic z compare: content
70 < floating 80 < tooltip 90 < toast 1200; dropdown-in-dialog preserved). A
live browser check was blocked by a port conflict with another session's server.
check: 0 new type errors; the 5 guards green.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
3 months ago
|
|
|
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', () => {
|
|
|
|
|
const violations = grepSources(
|
|
|
|
|
join(HERE, 'soma'),
|
|
|
|
|
/\.setAttribute\(|\.removeAttribute\(|\.toggleAttribute\(|\.classList\.|\.style\.setProperty\(|\.style\.[a-zA-Z]+\s*=/
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
expect(violations).toEqual([]);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('guards Soma component timers through the injected scheduler', () => {
|
|
|
|
|
const allowedFallback = join(HERE, 'soma', 'components', 'announce', 'global.svelte.ts');
|
|
|
|
|
const violations = listSourceFiles(join(HERE, 'soma', 'components')).filter((file) => {
|
|
|
|
|
if (file === allowedFallback) return false;
|
|
|
|
|
return /\bsetTimeout\(|\bclearTimeout\(|\bsetInterval\(|\bclearInterval\(/.test(
|
|
|
|
|
stripComments(readFileSync(file, 'utf8'))
|
|
|
|
|
);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
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', () => {
|
|
|
|
|
const violations = grepSources(
|
|
|
|
|
join(HERE, 'soma', 'components'),
|
|
|
|
|
/\.(addEventListener|removeEventListener)\(/
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
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);
|
|
|
|
|
expect(factory.serviceDependencies).toEqual(['dom']);
|
|
|
|
|
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([]);
|
|
|
|
|
});
|
|
|
|
|
|
uix: rename morfo.translations → morfo.texts (idlangref discipline)
Morfo now declares text slots as absolute idlangrefs in `texts: Record<string, LangRef>`,
not literal multilingual records. Multilingual content moves out of morfo entirely
into per-component catalogs under `src/uix/langs/components/{kebab}.ts`, merged
globally by `ActiveUix` via the new `componentLangs` barrel. Morfo stays a pure
abstract surface; localization lives in its own layer.
- Type: `translations?: LangNode` → `texts?: Record<string, LangRef>` in
`src/uix/morfo/types.ts`. Doc and test fixtures updated.
- Catalog: new `src/uix/langs/` directory with `common.ts` (was `langs.ts`),
`components/` (one file per migrated morfo, plus a barrel), and an aggregate
`uixLangs`. `ActiveUix` registers both namespaces at boot.
- Registry: `registerMorfo` no longer carries a translations fingerprint and
no longer extends `ActiveLangs` dynamically. `connectMorfoTranslations` and
`MorfoTranslationTarget` deleted from the public morfo barrel.
- Validation: schema.ts drops the relative-translationRef-in-morfo.translations
check; catalog presence is enforced by the new `scripts/translations-check.ts`
(wired as `npm run translations:check`).
- Audit: `scripts/component-audit.ts` rule A-1.3 now validates `texts.label`
idlangref shape (`#?components.{kebab}.label|Fallback`) and explicitly errors
on any surviving legacy `translations:` field.
- Migration: `scripts/migrate-translations-to-texts.ts` is the one-off Node
script that produced the 42 morfo rewrites and 42 catalog files.
- Tests: `morfo/registry.test.ts` rewritten around the simpler contract;
`active-uix.svelte.test.ts` reflects boot-time catalog availability instead
of lazy per-morfo extension.
Verification: `npm run check` (0 errors), `npm run translations:check`
(0/0), `npm run morfo:check` (36/36 routed). The 4 pre-existing failures
in `test` are unrelated (date-picker types, css regen, range-calendar
clear, color contract).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
5 months ago
|
|
|
it('Morfo declares text slots as absolute idlangrefs (texts:) — catalog lives outside the morfo', () => {
|
|
|
|
|
_resetMorfoRegistryForTesting();
|
|
|
|
|
|
|
|
|
|
try {
|
uix: rename morfo.translations → morfo.texts (idlangref discipline)
Morfo now declares text slots as absolute idlangrefs in `texts: Record<string, LangRef>`,
not literal multilingual records. Multilingual content moves out of morfo entirely
into per-component catalogs under `src/uix/langs/components/{kebab}.ts`, merged
globally by `ActiveUix` via the new `componentLangs` barrel. Morfo stays a pure
abstract surface; localization lives in its own layer.
- Type: `translations?: LangNode` → `texts?: Record<string, LangRef>` in
`src/uix/morfo/types.ts`. Doc and test fixtures updated.
- Catalog: new `src/uix/langs/` directory with `common.ts` (was `langs.ts`),
`components/` (one file per migrated morfo, plus a barrel), and an aggregate
`uixLangs`. `ActiveUix` registers both namespaces at boot.
- Registry: `registerMorfo` no longer carries a translations fingerprint and
no longer extends `ActiveLangs` dynamically. `connectMorfoTranslations` and
`MorfoTranslationTarget` deleted from the public morfo barrel.
- Validation: schema.ts drops the relative-translationRef-in-morfo.translations
check; catalog presence is enforced by the new `scripts/translations-check.ts`
(wired as `npm run translations:check`).
- Audit: `scripts/component-audit.ts` rule A-1.3 now validates `texts.label`
idlangref shape (`#?components.{kebab}.label|Fallback`) and explicitly errors
on any surviving legacy `translations:` field.
- Migration: `scripts/migrate-translations-to-texts.ts` is the one-off Node
script that produced the 42 morfo rewrites and 42 catalog files.
- Tests: `morfo/registry.test.ts` rewritten around the simpler contract;
`active-uix.svelte.test.ts` reflects boot-time catalog availability instead
of lazy per-morfo extension.
Verification: `npm run check` (0 errors), `npm run translations:check`
(0/0), `npm run morfo:check` (36/36 routed). The 4 pre-existing failures
in `test` are unrelated (date-picker types, css regen, range-calendar
clear, color contract).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
5 months ago
|
|
|
const sample: Morfo = {
|
|
|
|
|
name: 'ContractSample',
|
|
|
|
|
kebab: 'contract-sample',
|
|
|
|
|
scope: ['soma'],
|
uix: rename morfo.translations → morfo.texts (idlangref discipline)
Morfo now declares text slots as absolute idlangrefs in `texts: Record<string, LangRef>`,
not literal multilingual records. Multilingual content moves out of morfo entirely
into per-component catalogs under `src/uix/langs/components/{kebab}.ts`, merged
globally by `ActiveUix` via the new `componentLangs` barrel. Morfo stays a pure
abstract surface; localization lives in its own layer.
- Type: `translations?: LangNode` → `texts?: Record<string, LangRef>` in
`src/uix/morfo/types.ts`. Doc and test fixtures updated.
- Catalog: new `src/uix/langs/` directory with `common.ts` (was `langs.ts`),
`components/` (one file per migrated morfo, plus a barrel), and an aggregate
`uixLangs`. `ActiveUix` registers both namespaces at boot.
- Registry: `registerMorfo` no longer carries a translations fingerprint and
no longer extends `ActiveLangs` dynamically. `connectMorfoTranslations` and
`MorfoTranslationTarget` deleted from the public morfo barrel.
- Validation: schema.ts drops the relative-translationRef-in-morfo.translations
check; catalog presence is enforced by the new `scripts/translations-check.ts`
(wired as `npm run translations:check`).
- Audit: `scripts/component-audit.ts` rule A-1.3 now validates `texts.label`
idlangref shape (`#?components.{kebab}.label|Fallback`) and explicitly errors
on any surviving legacy `translations:` field.
- Migration: `scripts/migrate-translations-to-texts.ts` is the one-off Node
script that produced the 42 morfo rewrites and 42 catalog files.
- Tests: `morfo/registry.test.ts` rewritten around the simpler contract;
`active-uix.svelte.test.ts` reflects boot-time catalog availability instead
of lazy per-morfo extension.
Verification: `npm run check` (0 errors), `npm run translations:check`
(0/0), `npm run morfo:check` (36/36 routed). The 4 pre-existing failures
in `test` are unrelated (date-picker types, css regen, range-calendar
clear, color contract).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
5 months ago
|
|
|
texts: {
|
|
|
|
|
label: '#?components.contract-sample.label|Contract'
|
|
|
|
|
},
|
|
|
|
|
parts: [
|
|
|
|
|
{
|
|
|
|
|
name: 'Provider',
|
|
|
|
|
kebab: 'provider',
|
|
|
|
|
kind: 'public',
|
|
|
|
|
defaultElement: 'div',
|
|
|
|
|
optional: false,
|
|
|
|
|
data: [],
|
|
|
|
|
aria: [
|
|
|
|
|
{
|
|
|
|
|
attr: 'aria-label',
|
uix: rename morfo.translations → morfo.texts (idlangref discipline)
Morfo now declares text slots as absolute idlangrefs in `texts: Record<string, LangRef>`,
not literal multilingual records. Multilingual content moves out of morfo entirely
into per-component catalogs under `src/uix/langs/components/{kebab}.ts`, merged
globally by `ActiveUix` via the new `componentLangs` barrel. Morfo stays a pure
abstract surface; localization lives in its own layer.
- Type: `translations?: LangNode` → `texts?: Record<string, LangRef>` in
`src/uix/morfo/types.ts`. Doc and test fixtures updated.
- Catalog: new `src/uix/langs/` directory with `common.ts` (was `langs.ts`),
`components/` (one file per migrated morfo, plus a barrel), and an aggregate
`uixLangs`. `ActiveUix` registers both namespaces at boot.
- Registry: `registerMorfo` no longer carries a translations fingerprint and
no longer extends `ActiveLangs` dynamically. `connectMorfoTranslations` and
`MorfoTranslationTarget` deleted from the public morfo barrel.
- Validation: schema.ts drops the relative-translationRef-in-morfo.translations
check; catalog presence is enforced by the new `scripts/translations-check.ts`
(wired as `npm run translations:check`).
- Audit: `scripts/component-audit.ts` rule A-1.3 now validates `texts.label`
idlangref shape (`#?components.{kebab}.label|Fallback`) and explicitly errors
on any surviving legacy `translations:` field.
- Migration: `scripts/migrate-translations-to-texts.ts` is the one-off Node
script that produced the 42 morfo rewrites and 42 catalog files.
- Tests: `morfo/registry.test.ts` rewritten around the simpler contract;
`active-uix.svelte.test.ts` reflects boot-time catalog availability instead
of lazy per-morfo extension.
Verification: `npm run check` (0 errors), `npm run translations:check`
(0/0), `npm run morfo:check` (36/36 routed). The 4 pre-existing failures
in `test` are unrelated (date-picker types, css regen, range-calendar
clear, color contract).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
5 months ago
|
|
|
value: v.translationRef('#?components.contract-sample.label|Contract')
|
|
|
|
|
}
|
|
|
|
|
]
|
|
|
|
|
}
|
|
|
|
|
]
|
uix: rename morfo.translations → morfo.texts (idlangref discipline)
Morfo now declares text slots as absolute idlangrefs in `texts: Record<string, LangRef>`,
not literal multilingual records. Multilingual content moves out of morfo entirely
into per-component catalogs under `src/uix/langs/components/{kebab}.ts`, merged
globally by `ActiveUix` via the new `componentLangs` barrel. Morfo stays a pure
abstract surface; localization lives in its own layer.
- Type: `translations?: LangNode` → `texts?: Record<string, LangRef>` in
`src/uix/morfo/types.ts`. Doc and test fixtures updated.
- Catalog: new `src/uix/langs/` directory with `common.ts` (was `langs.ts`),
`components/` (one file per migrated morfo, plus a barrel), and an aggregate
`uixLangs`. `ActiveUix` registers both namespaces at boot.
- Registry: `registerMorfo` no longer carries a translations fingerprint and
no longer extends `ActiveLangs` dynamically. `connectMorfoTranslations` and
`MorfoTranslationTarget` deleted from the public morfo barrel.
- Validation: schema.ts drops the relative-translationRef-in-morfo.translations
check; catalog presence is enforced by the new `scripts/translations-check.ts`
(wired as `npm run translations:check`).
- Audit: `scripts/component-audit.ts` rule A-1.3 now validates `texts.label`
idlangref shape (`#?components.{kebab}.label|Fallback`) and explicitly errors
on any surviving legacy `translations:` field.
- Migration: `scripts/migrate-translations-to-texts.ts` is the one-off Node
script that produced the 42 morfo rewrites and 42 catalog files.
- Tests: `morfo/registry.test.ts` rewritten around the simpler contract;
`active-uix.svelte.test.ts` reflects boot-time catalog availability instead
of lazy per-morfo extension.
Verification: `npm run check` (0 errors), `npm run translations:check`
(0/0), `npm run morfo:check` (36/36 routed). The 4 pre-existing failures
in `test` are unrelated (date-picker types, css regen, range-calendar
clear, color contract).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
5 months ago
|
|
|
};
|
|
|
|
|
|
uix: rename morfo.translations → morfo.texts (idlangref discipline)
Morfo now declares text slots as absolute idlangrefs in `texts: Record<string, LangRef>`,
not literal multilingual records. Multilingual content moves out of morfo entirely
into per-component catalogs under `src/uix/langs/components/{kebab}.ts`, merged
globally by `ActiveUix` via the new `componentLangs` barrel. Morfo stays a pure
abstract surface; localization lives in its own layer.
- Type: `translations?: LangNode` → `texts?: Record<string, LangRef>` in
`src/uix/morfo/types.ts`. Doc and test fixtures updated.
- Catalog: new `src/uix/langs/` directory with `common.ts` (was `langs.ts`),
`components/` (one file per migrated morfo, plus a barrel), and an aggregate
`uixLangs`. `ActiveUix` registers both namespaces at boot.
- Registry: `registerMorfo` no longer carries a translations fingerprint and
no longer extends `ActiveLangs` dynamically. `connectMorfoTranslations` and
`MorfoTranslationTarget` deleted from the public morfo barrel.
- Validation: schema.ts drops the relative-translationRef-in-morfo.translations
check; catalog presence is enforced by the new `scripts/translations-check.ts`
(wired as `npm run translations:check`).
- Audit: `scripts/component-audit.ts` rule A-1.3 now validates `texts.label`
idlangref shape (`#?components.{kebab}.label|Fallback`) and explicitly errors
on any surviving legacy `translations:` field.
- Migration: `scripts/migrate-translations-to-texts.ts` is the one-off Node
script that produced the 42 morfo rewrites and 42 catalog files.
- Tests: `morfo/registry.test.ts` rewritten around the simpler contract;
`active-uix.svelte.test.ts` reflects boot-time catalog availability instead
of lazy per-morfo extension.
Verification: `npm run check` (0 errors), `npm run translations:check`
(0/0), `npm run morfo:check` (36/36 routed). The 4 pre-existing failures
in `test` are unrelated (date-picker types, css regen, range-calendar
clear, color contract).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
5 months ago
|
|
|
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();
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
});
|