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>
Architectural correction. Morfo was importing Svelte runes, $adom, and
$uix/sema — all violations of "morfo is pure declarative DNA". The
runtime that interprets a CompiledMorfo lives where it executes: soma.
Move:
- src/uix/morfo/runtime.svelte.ts → src/uix/soma/runtime.svelte.ts
- src/uix/morfo/runtime.svelte.test.ts → src/uix/soma/runtime.svelte.test.ts
Rename (consistent with the move; "soma's runtime", per the doctrine
"morfo declares, soma executes"):
- class MorfoRuntime → SomaRuntime
- createMorfoRuntime() → createSomaRuntime()
- type MorfoRuntimeSources → SomaRuntimeSources
- error prefixes [morfo-runtime] → [soma-runtime]
Sema is now genuinely ornamental:
- ActiveUix.semantic returns EngineSemantic | undefined (no longer
throws when no engine is configured). Components in SSR / headless
tests / audio-disabled environments stay functional.
- SomaRuntime.trigger() skips the emit step AND the target-resolution
check when no engine is present — only requires a DOM target when
something will actually be emitted.
- Soma.semantic getter returns | undefined too (was non-optional).
Consumers updated:
- src/uix/active-uix/active-uix.svelte.ts — imports createSomaRuntime
from $soma; semantic getter returns | undefined
- src/uix/active-uix/types.ts — interface SomaRuntime, semantic | undefined
- 8 providers (toggle, switch, collapsible, dialog, drawer, popover,
toast, avatar) — type SomaRuntime, import path $soma
- src/uix/soma/index.ts — re-exports createSomaRuntime + types
- src/uix/morfo/index.ts — drops the runtime re-exports (morfo is pure)
- src/uix/sema/signal.ts — JSDoc reference updated
- src/uix/soma/core/soma.svelte.ts — semantic getter | undefined
Test added: SomaRuntime.trigger() succeeds without a semantic engine
(skips emit + target check, runs handler directly).
Docs aligned to the new shape:
- active_architecture.md — runtime locations, sema as ornamental
- soma/SOMA_ARCHITECTURE.md, soma/README.md — name + import paths
- morfo/README.md — file listing now flags morfo as PURE TS, points
at soma/runtime.svelte.ts for the interpreter
- sema/README.md — SomaRuntime references
- README.md — root architecture diagram
- CLAUDE.md — hand-off note
Tests 218/218 green. npm run check holds at the 39-error baseline.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds a single convenience method on `ActiveUix` so soma providers stop
duplicating the `dom: this.soma.dom, semantic: this.soma.semantic`
boilerplate at every `createMorfoRuntime` callsite.
Before (8 providers, identical lines):
this.runtime = createMorfoRuntime(dialogMorfo, {
dom: this.soma.dom, // ← duplicated everywhere
semantic: this.soma.semantic, // ← duplicated everywhere
states: { … },
props: { … },
events: { … }
})
After:
this.runtime = this.soma.uix.runtime(dialogMorfo, {
states: { … },
props: { … },
events: { … }
})
The implementation is a thin convenience — internally `uix.runtime()`
calls `createMorfoRuntime(morfo, { dom: this.dom, semantic: this.semantic,
…sources })`. No registry, no scope ids, no per-instance lifecycle to
track. Per-provider runtime ownership unchanged. The user explicitly
chose this minimal fix over a "single shared runtime + scope handles"
design after weighing the tradeoffs.
Migrated callsites: avatar, collapsible, dialog, drawer, popover,
switch, toast, toggle (8 providers, ~10 callsites including toast's
two — root + item). The `createMorfoRuntime` named import dropped from
each (only `MorfoRuntime` type kept where the field is annotated).
3 new tests on `active-uix.svelte.test.ts`:
- returns a MorfoRuntime with auto-injected services
- caller does not pass dom or semantic
- attach mode uses the external app's dom + semantic
Verified: vitest 17/17 active-uix (was 14), 211/211 UIX, smoke 68/68,
morfo-check 66/66.
Earlier design mistake: `createActiveUix(options)` was internally
calling `createActiveApp(...)` to compose the UIX services. That
violated the contract — active-uix was supposed to be either a
substitute for active-app (when no app exists) or a complement (when
an external app injects services).
Fix: standalone mode now instantiates each UIX service directly via
the canonical factories — no `active-app` involved:
- core: createEngineLogger, createEngineBus, createActiveTimers,
createActivePrefs (with empty schema)
- lang: createActiveLang(schema, defaultLocale, fallbackChain)
- dom: createActiveDom
- frontend: createActiveFrontend (auto-applies to <html>)
- format: createActiveFormat (clock from timers)
- semantic: new EngineSemantic(opts)
active-uix manages every owned lifecycle: dispose() tears them down
in reverse dependency order. Attach mode is unchanged — wraps an
external app, validates UIX services, doesn't touch app.dispose().
Type changes:
- `ActiveUix.app: ActiveApp | undefined`. Present only in attach
mode (it's the underlying app); `undefined` in standalone (no
app exists).
- `ActiveUix.bus: EngineBus` (generic envelope). Standalone gets
a fresh bus, attach gets the App's bus with its event map. UIX
components don't care about typed events; consumers that do
re-narrow at the call site.
- Removed `prefs?` and `extraServices?` from `ActiveUixOptions`.
Apps that need a custom prefs schema or services beyond UIX
should use attach mode (compose `createActiveApp` themselves).
New tests cover both modes: standalone (8 tests including service
disposal verification, idempotent dispose, format opt-out,
componentLangs registration toggle), attach (6 tests including
missing-services error, identity-equal service proxying, no
disposal of external app), and cross-mode parity (1 test).
Verified: vitest 15 files / 211 tests, smoke 68/68, morfo-check 66/66.
Decouples the UIX layer (soma / morfo runtime / sema / eidos) from
the App composition layer (`active-app`). UIX components no longer
import `\$active-app` directly — they read services through
`getActiveUix()`. Two boot paths produce the same `ActiveUix`:
- **Standalone** — `createActiveUix({ lang, semantic, … })`
bootstraps an `ActiveApp` internally with the UIX services plus
any `extraServices`. Owns the app — `dispose()` propagates.
- **Attach** — `attachActiveUix(externalApp)` wraps an app the
application composed itself (typically when it has services
beyond UIX: cache, session, http, …). The external app must
declare the UIX services; helper `defineUixServices(options)`
returns the slice ready to spread into
`createActiveApp({ services })`. Does NOT own the app.
In both cases components see only `ActiveUix`. They have no way to
tell which boot path was used.
Module:
src/uix/active-uix/
├── index.ts barrel ($active-uix)
├── types.ts ActiveUix, ActiveUixOptions, AttachActiveUixOptions
├── services.ts defineUixServices() + UIX_REQUIRED_SERVICES
├── active-uix.svelte.ts createActiveUix() + attachActiveUix() + ActiveUixImpl
├── components/uix.svelte <Uix uix={...}> wrapper (sets context + creates Soma)
└── svelte/context.svelte.ts setActiveUix() / getActiveUix() / tryGetActiveUix()
Surface exposed by `ActiveUix`:
- UIX services: lang, frontend, format (optional), dom, semantic
- Core: logger, bus, timers, prefs
- Config: somaPortalTo
- `app` escape hatch for advanced integration code (NOT for UIX
components)
Wiring done by active-uix at boot:
- `App.lang.extend('components', componentLangs)` — soma component
translations always available without per-app boilerplate.
- Frontend service auto-configured with `applyDom: true` +
`target: () => document.documentElement` so theme/dir/density
land on `<html>` automatically.
Migration:
- `Soma.create()` reads `getActiveUix()` instead of `getActiveApp()`.
Soma class no longer imports `\$active-app`.
- 3 layouts (test/eidos, test/sium, test/soma) replace
`createActiveApp + setActiveApp + <Soma>` with
`createActiveUix + <Uix>`. Boilerplate drops ~20 lines per layout.
- 7 sium pages replace `getActiveApp() + app.lang.setLocale(…)`
with `getActiveUix() + uix.lang.setLocale(…)`.
Validation:
- `attachActiveUix(app)` throws `ActiveUixMissingServiceError` if
the external app didn't declare lang/frontend/dom/semantic.
- `getActiveUix()` throws `ActiveUixNoContextError` if no `<Uix>`
ancestor mounted.
`\$active-uix` alias wired in svelte.config.js + vite.config.ts (both
client and server projects).
Verified: vitest 197/197, smoke 68/68, morfo-check 66/66, playwright
probe (4 routes including dialog with sound) zero page/console errors.