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/docs/architecture/morfo.md

1370 lines
83 KiB

---
title: Morfo — the declarative contract
type: reference
audience: human + agent
authority: E1 architecture — the cross-layer contract layer (DNA)
status: current
source: migrated from src/uix/morfo/README.md (2026-07-02, docs-book F7.2)
---
# Morfo
**The cross-layer contract of a component's public DOM surface.**
Morfo is the single source of truth for a component's parts, data-attrs, ARIA contract, keyboard shortcuts, focus policy, and public event contract. The same morfo is consumed by soma (to wire the headless provider), by eidos (to generate CSS selectors), by sema (to bind perceptual channels), and by the docs site (to render part tables).
**One file per component**, at `src/uix/morfo/components/{kebab}.ts`. No prose — that's the component's README. No props — those live in `types.ts` with JSDoc. Component-owned text slots may live in `texts` (idlangrefs) when they are part of ARIA labels, live-region text, or internal functional labels — the multilingual catalog itself lives in `src/uix/langs/components/{kebab}.ts`. Everything else is the machine-readable contract.
## Why morfo exists
Without morfo, a component's structural information used to live in many places:
- Part names in manual attr maps inside the provider.
- Data-attr enums in manual contract registration inside the provider.
- ARIA emission hardcoded in the provider's `$derived.by(...)` props.
- Keyboard handlers scattered across the provider.
- Public event names and their transport split across provider code, docs, and consumers.
- Prose descriptions in the README.
- Selector strings duplicated in eidos CSS, sema `.csem`, docs tables.
Renaming a part (`content` → `panel`) used to mean touching 6+ locations with zero automatic verification. Cross-layer drift (soma emits `data-dialog-content`, eidos styles `data-dialog-panel`) was silent.
With morfo, **every location reads from the same declaration**. Parts, data-attrs, enum values, and component-owned translation keys are authored once. `compileMorfo`, `registerMorfo`, `SomaRuntime`, and the selector helper `createAttrs` consume the morfo directly. A smoke test validates the real DOM against the declaration on every CI run.
---
## What morfo contains
A `Morfo` is a plain TypeScript constant that describes:
- **`name`** — PascalCase display name (`"Dialog"`).
- **`kebab`** — kebab-case identifier (`"dialog"`), matches the public `data-{kebab}` marker.
- **`scope`** — which layers implement this component: `['soma']`, `['soma', 'eidos']`, etc.
- **`apg`** — optional URL to the WAI-ARIA APG pattern when the component implements a formal one.
- **`focus`** — optional focus policy for overlays / composites.
- **`events`** — the component's public event surface: which semantic occurrences it may emit and expose to cross-layer consumers.
docs+fix: old docs quarantined in docs/old-deprecated; STUMBLES doc-class fixes Two user findings from the Knob build exercise (an agent building a new component from the docs alone — STUMBLES.md). Quarantine: the superseded fossils no longer share shelf space with the live corpus. docs/old-deprecated/ (with an index README explaining what lands there and pointing readers at docs/README.md) now holds the executed audits and fix plans: fable_audit, fable-eidos-audit, inherit_audit + inherit_fix_plan, ARCHETYPE_COHERENCE_AUDIT_2026-06-19 (still citable — the component-guide banner and the eidos components README repoint to it), COMPONENT_COHERENCE_AUDIT. pendiente.md (a live pending list, not a fossil) moved to docs/process/. docs-check treats the folder as sealed chronicle (I1/I2 exempt; I6 skips its internal links, as its README promises). Root-level *.md is now: README, CLAUDE, AGENTS + the user's own working files. STUMBLES fixes applied on the spot (the doc-class ones): - #2 kind drift: the REAL enum is 'public' | 'private' | 'virtual' (MorfoPartKind, 671/9/14 uses) — morfo.md omitted 'private', the checklist invented 'internal' (0 uses). Both fixed; I2 gains the phantom-'internal' guard. A-2.1's row now says what the audit script actually checks (kebab only — the archetype may vary, the Toggle provider-IS-trigger doctrine). - #6: the langs catalog SHAPE (flat keys, per-language leaves, named export, index registration) is now shown in morfo.md instead of only its location. - #8: component-audit s0 defines the minimum brief package as an explicit 8-file list. The engineering-class stumbles are registered as plan batches S1-S6 (generated vocabularies appendix, continuous-gesture trigger doctrine, part-absent condition, Gesture.rotate, the soma->eidos CSS-var contract, minor frictions). docs:check 0 errors, 11-warn baseline. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
3 months ago
- **`texts`** — optional component-owned text slots, declared as idlangrefs (`'#?components.{kebab}.{key}|Fallback'`). The multilingual catalog itself lives in `src/uix/langs/components/{kebab}.ts` and is registered by `ActiveUix` under `components.{kebab}.*`. Its shape (flat keys, per-language leaves, named export `{camelKebab}Langs` registered in `langs/components/index.ts`):
```ts
import type { LangNode } from '$libs/langs';
export const knobLangs = {
label: { es: 'Dial', en: 'Knob' }
} satisfies LangNode;
```
docs(uix): la doctrina alcanza a la bolsa única — el corpus deja de enseñar la API muerta La clase exacta que la auditoría flagueó (doc↔código), producida esta vez por NUESTROS propios cambios de P0 fase C y sin corregir hasta ahora: - component-guide §«two sanctioned ways»: reescrita a UNA vía (la bolsa .props resuelve el contrato entero; extras solo lo que el morfo no puede expresar; valores CRUDOS; la inversa «morfo sin value ⇒ soma escribe»). Los 4 snippets con syncAttrs: true, limpiados. - soma-architecture: partProps re-descrito como LA bolsa (identidad + contrato completo, SSR incluido); el párrafo del flag sustituido por «el único escritor imperativo es el prewrite»; snippet limpiado. - soma.md: snippet de accordion sin el flag. - morfo.md §naming: el «(SSR included)» dejó de ser contraste — desde la tubería única TODO plan viaja en la bolsa; lo que distingue a los naming es la PRECEDENCIA (consumer-first via mergeProps). - testing-and-tooling: la postura SSR gana el canario ssr-contract.test, y nace «The gate» (check:gate + ledger menguante, gate, hook — los instrumentos de fase B no estaban documentados). - CLAUDE.md: el paso 4 de trigger ya no dice «effect-driven» (prettier normalizó de paso el fichero entero — solo whitespace, contenido intacto, declarado aquí para que el diff ancho no sea silencioso). Lo histórico (changelog, old-deprecated, audit de julio, process/) se queda como historia. Verificación: docs:check 0 errores sobre 817 docs; cero menciones normativas de la API muerta fuera de referencias históricas. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
1 month ago
- **`parts`** — the part tree (recursive). Each part declares:
docs+fix: old docs quarantined in docs/old-deprecated; STUMBLES doc-class fixes Two user findings from the Knob build exercise (an agent building a new component from the docs alone — STUMBLES.md). Quarantine: the superseded fossils no longer share shelf space with the live corpus. docs/old-deprecated/ (with an index README explaining what lands there and pointing readers at docs/README.md) now holds the executed audits and fix plans: fable_audit, fable-eidos-audit, inherit_audit + inherit_fix_plan, ARCHETYPE_COHERENCE_AUDIT_2026-06-19 (still citable — the component-guide banner and the eidos components README repoint to it), COMPONENT_COHERENCE_AUDIT. pendiente.md (a live pending list, not a fossil) moved to docs/process/. docs-check treats the folder as sealed chronicle (I1/I2 exempt; I6 skips its internal links, as its README promises). Root-level *.md is now: README, CLAUDE, AGENTS + the user's own working files. STUMBLES fixes applied on the spot (the doc-class ones): - #2 kind drift: the REAL enum is 'public' | 'private' | 'virtual' (MorfoPartKind, 671/9/14 uses) — morfo.md omitted 'private', the checklist invented 'internal' (0 uses). Both fixed; I2 gains the phantom-'internal' guard. A-2.1's row now says what the audit script actually checks (kebab only — the archetype may vary, the Toggle provider-IS-trigger doctrine). - #6: the langs catalog SHAPE (flat keys, per-language leaves, named export, index registration) is now shown in morfo.md instead of only its location. - #8: component-audit s0 defines the minimum brief package as an explicit 8-file list. The engineering-class stumbles are registered as plan batches S1-S6 (generated vocabularies appendix, continuous-gesture trigger doctrine, part-absent condition, Gesture.rotate, the soma->eidos CSS-var contract, minor frictions). docs:check 0 errors, 11-warn baseline. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
3 months ago
- `name`, `kebab`, `kind` (`public` — consumer-composed · `private` —
rendered only by soma/eidos defaults but still styled + contract-checked ·
`virtual` — DOM-less coordinator, ignored by the contract validator; the
source is `MorfoPartKind` in `morfo/types.ts`).
- `archetype` — optional cross-component classification (see "Archetypes" below).
- `defaultElement` (advisory), `role` (always-emitted).
- `optional`, `supportsNesting`.
- `states` — the state names this part can be in.
- `data` — data-attributes emitted, with enum values when applicable and optional runtime source metadata when the contract wants to declare where the attr comes from.
feat(morfo)!: P1 — split attrs[]/aria[] y vocabulario ARIA cerrado: cada cubo su territorio El cubo aria[] mentía un 13,5 %: 989 entradas medidas — 847 aria-*, 8 role, y 134 impostores (type ×130, tabindex ×2, for, contenteditable). - MorfoPart.attrs[]: cubo nuevo para atributos HTML genéricos, misma forma de entrada y mismo compilado (un solo builder para ambos cubos). - aria[] se CIERRA por tipo: MorfoAriaAttr = 'role' | unión del spec (53 nombres, DERIVADA del array ARIA_ATTR_VOCABULARY — nunca lista paralela, la lección MorfoElement). aria-grabbed/aria-dropeffect deprecados-en-1.2 con acta (verdad embarcada de drag-drop); braille*/description son 1.3 y entran anotados. Un typo muere en el tipo. - Esquema guarda el territorio en AMBAS direcciones: aria[] con no-aria LANZA sugiriendo attrs[]; attrs[] con aria-*/role o data-* LANZA. - Codemod: 134 entradas movidas en 62 morfos (133 bloques). Orden de emisión MEDIDO y conservado: data → attrs → aria (132/133 bloques declaraban el impostor como prefijo contiguo de aria[]); snapshot de los 921 parts × 168 morfos: 920/921 byte-idénticos — la única divergencia (breadcrumb.ellipsis, type tras un aria-label consumerWins) no cambia la bolsa renderizada. - Consumidores de part.aria censados: compile/resolver/schema actualizados; morfo-check pierde un filtro MUERTO (el cubo ya llega cerrado por tipo+schema); landmark-census y tests de la mitad aria sin cambio. - morfo.md documenta los dos cubos. 13 tests nuevos (8 schema territorio + 5 compile paridad). morfo 262/262 · soma entera 1314/1314 · check 71. Nota de árbol compartido: src/uix/morfo/source-census.test.ts (sin trackear) es el eje SourceMap de vicen-e1 y NO viaja aquí (add -u); su censo re-medirá sobre este árbol asentado, orden acordado por canal. Ejecutado por agente Opus 5, sesión coordinando y verificando (guardas de territorio probadas en vivo, suite re-corrida). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
1 month ago
- `attrs` — generic HTML attributes (`type`, `tabindex`, `for`,
`contenteditable`, …), same shape as `aria`, optional.
- `aria` — ARIA attribute contract (attr + value source + condition), CLOSED
to the ARIA vocabulary.
- `keyboard` — keyboard shortcuts relevant when the part has focus.
- `parts` — nested sub-parts (recursive).
See [`types.ts`](../../src/uix/morfo/types.ts) for the full TypeScript shape.
## What morfo does NOT contain
docs(uix): la doctrina alcanza a la bolsa única — el corpus deja de enseñar la API muerta La clase exacta que la auditoría flagueó (doc↔código), producida esta vez por NUESTROS propios cambios de P0 fase C y sin corregir hasta ahora: - component-guide §«two sanctioned ways»: reescrita a UNA vía (la bolsa .props resuelve el contrato entero; extras solo lo que el morfo no puede expresar; valores CRUDOS; la inversa «morfo sin value ⇒ soma escribe»). Los 4 snippets con syncAttrs: true, limpiados. - soma-architecture: partProps re-descrito como LA bolsa (identidad + contrato completo, SSR incluido); el párrafo del flag sustituido por «el único escritor imperativo es el prewrite»; snippet limpiado. - soma.md: snippet de accordion sin el flag. - morfo.md §naming: el «(SSR included)» dejó de ser contraste — desde la tubería única TODO plan viaja en la bolsa; lo que distingue a los naming es la PRECEDENCIA (consumer-first via mergeProps). - testing-and-tooling: la postura SSR gana el canario ssr-contract.test, y nace «The gate» (check:gate + ledger menguante, gate, hook — los instrumentos de fase B no estaban documentados). - CLAUDE.md: el paso 4 de trigger ya no dice «effect-driven» (prettier normalizó de paso el fichero entero — solo whitespace, contenido intacto, declarado aquí para que el diff ancho no sea silencioso). Lo histórico (changelog, old-deprecated, audit de julio, process/) se queda como historia. Verificación: docs:check 0 errores sobre 817 docs; cero menciones normativas de la API muerta fuera de referencias históricas. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
1 month ago
| Not in morfo | Lives in | Reason |
| ----------------------------------------------- | ---------------------------------- | ---------------------------------------------------------------------------------------------- |
| Summary, overview prose | `{component}/README.md` | Editorial, not contract |
| Comparison vs Radix / Base UI / Bits | `{component}/README.md` | Third-party drift shouldn't pollute the contract |
| Usage examples | `{component}/README.md` | Narrative |
| Props (names, types, defaults) | `{component}/types.ts` with JSDoc | Canonical source is TS + JSDoc |
| Shared/common translations | app/langs catalog under `common.*` | Shared vocabulary should not be duplicated per component |
| Provider-only id constants | optional `{component}/langs.ts` | Constants are code ergonomics; the catalog lives in `langs/components/{kebab}.ts` or app langs |
docs(uix): la doctrina alcanza a la bolsa única — el corpus deja de enseñar la API muerta La clase exacta que la auditoría flagueó (doc↔código), producida esta vez por NUESTROS propios cambios de P0 fase C y sin corregir hasta ahora: - component-guide §«two sanctioned ways»: reescrita a UNA vía (la bolsa .props resuelve el contrato entero; extras solo lo que el morfo no puede expresar; valores CRUDOS; la inversa «morfo sin value ⇒ soma escribe»). Los 4 snippets con syncAttrs: true, limpiados. - soma-architecture: partProps re-descrito como LA bolsa (identidad + contrato completo, SSR incluido); el párrafo del flag sustituido por «el único escritor imperativo es el prewrite»; snippet limpiado. - soma.md: snippet de accordion sin el flag. - morfo.md §naming: el «(SSR included)» dejó de ser contraste — desde la tubería única TODO plan viaja en la bolsa; lo que distingue a los naming es la PRECEDENCIA (consumer-first via mergeProps). - testing-and-tooling: la postura SSR gana el canario ssr-contract.test, y nace «The gate» (check:gate + ledger menguante, gate, hook — los instrumentos de fase B no estaban documentados). - CLAUDE.md: el paso 4 de trigger ya no dice «effect-driven» (prettier normalizó de paso el fichero entero — solo whitespace, contenido intacto, declarado aquí para que el diff ancho no sea silencioso). Lo histórico (changelog, old-deprecated, audit de julio, process/) se queda como historia. Verificación: docs:check 0 errores sobre 817 docs; cero menciones normativas de la API muerta fuera de referencias históricas. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
1 month ago
| Event handlers / runtime wiring, state machines | `{component}-provider.svelte.ts` | Execution logic, not contract data |
| Visual variants / recipes | `src/uix/eidos/` (future) | Layer-specific, not shared |
---
## How morfo gets executed (architecture)
Morfo is **declarative**. By itself it doesn't render, doesn't bind events,
doesn't write to the DOM. The piece that does is `SomaRuntime`, which lives
in `soma/` and consumes a morfo together with the provider's reactive sources.
The closed architecture (post-2026-04-25) has six pieces with disjoint
responsibilities:
```
Morfo declares
SomaRuntime transcribes
Provider supplies sources, targets, handlers
docs(uix): tanda 2 del contraste con el corpus — el modelo de los capitulos alcanza a la tuberia unica La lectura completa del corpus (6 lectores Opus, 6/6 censos, ~4,5M tokens) destapo lo que la tanda 1 no vio, y esta tanda lo cierra: - El modelo «Effects sync attrs» — repetido VERBATIM en cuatro capitulos (morfo, soma-architecture, overview, active-architecture), en sus tablas de piezas, en los cuatro pasos de trigger y en las dos frases-resumen — muere: la pieza es la bolsa de render; ADom aplica solo el prewrite. - morfo.md: partProps re-descrito (bolsa completa), la cadena causal de commits, y las filas de la tabla de ejecutores (data/aria/role → bolsa). - coincident: los DOS docstrings que ensenaban «in-flight» (types.ts y morfo.md) se alinean con la equivalencia FIRMADA de sema.md §slider («emit-then-handler, like pre; declared indivisible») — el hallazgo del informe queda refutado como defecto de runtime y reducido a esto. - glossary: el kind fantasma `internal` (la clase exacta que docs-check:422 mata y su regex no ve en tablas markdown) → `public|private|virtual` real. - El gate entra en la doctrina: check:gate/gate en el loop de verificacion (testing-and-tooling y getting-started), eidos:lint como script npm en su fila, y la tabla de Commands de morfo.md. - Mi propia sobreafirmacion de ayer, calificada: el test SSR es un CANARIO de dos representantes sobre mecanismo compartido, no un censo — el censo por provider queda encolado (P1). morfo:check en getting-started declara su alcance real (data-*; role/aria sin validador DOM). - Supervivientes de C2c: el assert-vacio de cropper (la clase «guard sobre VACIO pasa») retirado con acta; sticky/README deja de citar syncAttrs. - gradient-builder/README: fila data-kind del Track + acotacion mesh v1. - Handoff: DOS fe de erratas — «nunca los vio nadie» era falso (vistos y reducidos 12→3 en cb554ae2e; lo que faltaba era la puerta) y la ley del escritor imperativo gana su excepcion abierta (textarea autosize, con su cierre correcto encolado). Verificacion: docs:check 0/817 · cropper 19/19 · morfo schema+compile 133/133. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
1 month ago
Render bag re-derives attrs from state (Svelte renders them)
EngineSemantic dispatches signals to perceptual channels
VisualChannel materializes the signal in the DOM (data-event*, hold, cleanup)
ADom applies DOM mutations (structural commit)
```
### What each morfo field maps to at runtime
feat(sema)!: la señal precede al commit — emit() devuelve {id, settled} síncrono, el hold no bloquea a nadie, y el motor enacta la reducción 'state' (D-full + sema §3.9) D-full. `EngineSemantic.emit(signal)` deja de ser `Promise<string>` que resolvía tras el hold y pasa a devolver SÍNCRONAMENTE `EmitHandle { id, settled }`: el id se acuña sin await, la proyección sigue siendo síncrona en `replace`, y `settled` cubre cola + proyección + despacho + hold + expresión + limpieza (la limpieza ocurre ANTES de resolver). El runtime de soma, en `pre`/`coincident`, proyecta y despacha ANTES del handler en el mismo tick (ya no espera el hold); en `post`, handler → tick() → emit. `TriggerResult.settled` siempre presente. La a11y del runtime (announce / foco) corre tras el despacho, no tras el hold. El modelo es el de la plataforma (`element.animate()` → `Animation.finished`): el estado cambia en el instante de la acción y la expresión corre en su propio reloj sobre un nodo que retiene Presence. `pre`/`post`/`coincident` describen el ORDEN entre proyección y mutación, nunca una espera; `coincident` pasa a ser literalmente lo que declara. Medido en Chrome real (servidor propio, misma sonda antes/después; Escape → `data-state=closed`): dialog 258.6 → 9.1 ms · drawer 286.6 → 9.1 · popover 1757.8 → 8.5 · float-panel 1807.8 → 49.7 (n=1, pane oculto). Toggle con `regime: 'queue'` conserva su forma (contact 20 · estado 28 · commit encolado 156). En los cuatro el sello `emerge-close` se proyecta ANTES del cambio de estado y `data-ending-style` aparece (Presence). El «hasta 1,5 s» del informe de auditoría era real: el tope de `awaitExpression` se pagaba ante cualquier animación viva en el target (popover, float-panel). Condición 1 (censo, fase 0): 260 eventos; 39 `pre` + 21 `coincident` (0 sin `sequence`). Solo 14 tienen handler registrado (los únicos donde D-full adelanta la mutación; cifra del adversarial, el constructor contó 12); los otros 46 ya mutaban el estado en el mismo tick que un `void trigger` (sidebar.css:363-375 y tooltip-provider:272 lo tenían escrito con medición). Los 6 que desmontan (4 overlays + toast + tooltip) tienen Presence. CERO migraciones necesarias. La regla transversal `[data-event^='emerge-close'] { animation: dismiss-fade }` (base.css) existe en LETRA sobre los cuatro overlays, pero el preset de Presence gana la cascada en todo instante (medido con getComputedStyle): el refutador tenía razón en EFECTO. Condición 2 (carrera nueva del id síncrono): `clear(id)` / `clearTarget` honran ocurrencias EN VUELO (`inFlight` + `pendingClears`): la ocurrencia no se registra como persistente al terminar y limpia como transitoria; `clear` devuelve true. Reproducida en test (hold corriendo ⇒ tras `settled`, sin proyección residual y `active` vacío). `dispose()` vacía ambas. SO4 re-firmado: un target ausente NUNCA salta el handler ni lanza en ninguna secuencia — se omite prewrite+emit, se reporta por logger con `SomaRuntimeTargetError` como payload, y el handler corre («sema es ornamental; una acción del usuario nunca se pierde por un adorno»). sema §3.9. La política perceptual `'state'` bajo reduce pasa al MOTOR: `ReducedMotionFallback` vive en sema (morfo lo importa), la señal lleva `a11y.reducedMotionFallback` (cualquier valor declarado) y el motor, con `motion.get() === 'reduce'` y `'state'`, devuelve el mismo retorno silenciado que `channels: []` (antes de `queue`). Soma deja de forzar `channels: []` (`a11yChannelsOverride` muere) y conserva `'text'`/`'focus'` con `sources.motion`. S5 se conserva como doctrina (la preferencia gana al morfo y a la llamada); cambia el enactor. Cierra el emit DIRECTO sin a11y (~76 en el docs site). Dato: hoy ningún morfo declara `'state'` (9 declaran `'text'`); el valor es contrato y simetría con la háptica, no un síntoma vivo. Prosa re-firmada (CANON intacto, silente a propósito): engine.ts docblock · sema.md (emit contract, promise semantics, lifecycle, One door, overlays, error policy, reductions, migration) · morfo.md · active-architecture.md (walkthrough) · overview.md · soma-architecture.md · chans/types.ts req. 3 · sema/README · book-deviations D.9/D.10/D.12 · morfo/types.ts (MorfoEventSequence, MorfoA11ySemantic) · comentarios del `sequence: 'post'` en dialog/drawer/popover/ float-panel (valor intacto). Verificación (tras la ronda 2): sema 337/337 · runtime 63/63 · alcance del lote 201 ficheros / 2136 tests · suite entera 5366/5366 (458 ficheros, exit 0) · check 89 errores, 0 en src/, ledger intacto (los ~76 emit de web/routes son fire-and-forget y compilan con el handle) · gate VERDE tras la ronda 1 (5359/5359; un flake de gesture.test rotate-detent bajo carga, verde en solitario y dentro del gate) · +20 tests (engine 40→53, runtime 56→63) · 3 mutaciones enrojecen y revertidas byte-idénticas (⚠ diferir la limpieza UN microtask es un no-op observable: la mutación honesta cruza a macrotask) · adversarial Opus independiente (métodos propios, árbol byte-idéntico al terminar): doctrina SANA; 8 defectos, cerrados antes de anclar los 6 que eran del lote — D-1 código (el catch de `settled` vivía en la rama onFulfilled del trigger: un handler que lanzaba en `pre` dejaba la rechazada del canal visual sin manejar; ahora se engancha donde nace el handle, con test de las tres vías) · D-2/D-3 prosa · D-4/D-5 tests (los cuatro valores del fallback, y el par S5 vuelve a discriminar) · D-8 exactitud (`clear`/`clearTarget` no honran ni cuentan transitorias en vuelo). Segunda sonda de Chrome del adversarial: 11.6 · 11.0 · 5.1 · 42.7 ms. D-6 (alcance): `contracts.test.ts` restaurado a HEAD (su diff era 100 % formateo); el reformateo prettier de los docs tocados se queda. D-7 PREEXISTENTE, fila nombrada: `dispose()` durante una ocurrencia en vuelo repuebla `active` después y deja proyección residual (engine.ts, `finally` + `dispose`). Pendiente del autor: mirar el cierre de Dialog y Toast en un Chrome que PINTE (los panes de los agentes estaban ocultos: mutaciones y estilo computado medidos, frame no visto). Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
3 weeks ago
| Morfo field | Runtime executor | Purpose |
| ----------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------- |
| `parts[].data` (with `value`) | Render bag (`partProps`) — Svelte renders and re-derives it | Reactive `data-*` |
| `parts[].aria` | Render bag — NAMING attrs (`aria-label`) are `consumerWins`: `mergeProps` resolves them consumer-first (see Step 4) | Reactive `aria-*` |
| `parts[].role` | Render bag (`staticAttrs`) | Stable role |
| `parts[].keyboard` | The DECLARATION is the single contract (grammar-validated `kind: 'spec'` rows; `'display'` rows are docs prose) — verified by `keyboard-census.test.ts`. EXECUTION converges on SPECIES PRIMITIVES (`soma/keyboard/`: `gridWalk` · `segmentWalk` · `highlightWalk` · `valueStep`, C+ verdict 2026-08-27) with the provider as the thin delegate; `runtime.keydown(part, event, anchor?)` + per-registration `actions` is the opt-in leaf convenience (6 adopters); genuinely unique machinery (palabras) stays hand-rolled with its acta | Key contract + species execution |
| `events[].prewrite` | `trigger()` step 1 | Transient markers |
| `events[].semantic` | `trigger()` step 2 (emit payload) | Perceptual signal |
| `events[].commits` | **Nobody executes**; smoke validates | Documentation |
| `focus` | SIX species, one executor apiece: `trap` → `FocusScope.use({ policy })` (soma) · `roving` → `RovingFocusGroup` (`$adom`), or a SIGNED custom executor where the pattern exceeds the 1D primitive · `walk` → `getDirectionalKeys` (`$soma/keyboard/directional`) + `dom.focus` · `activedescendant` → `highlightWalk` (`$soma/keyboard/highlight-walk`) · `slider` → `valueStep` (`$soma/keyboard/value-step`) · `segments` → `segmentWalk` / `segmentAdvance` / `segmentRetreat` (`$soma/keyboard/segment-walk`) — declaration↔wiring pinned in BOTH directions by `focus-census.test.ts` | Species focus default |
`commits` is **descriptive**, not prescriptive. The actual causal chain is
docs(uix): tanda 2 del contraste con el corpus — el modelo de los capitulos alcanza a la tuberia unica La lectura completa del corpus (6 lectores Opus, 6/6 censos, ~4,5M tokens) destapo lo que la tanda 1 no vio, y esta tanda lo cierra: - El modelo «Effects sync attrs» — repetido VERBATIM en cuatro capitulos (morfo, soma-architecture, overview, active-architecture), en sus tablas de piezas, en los cuatro pasos de trigger y en las dos frases-resumen — muere: la pieza es la bolsa de render; ADom aplica solo el prewrite. - morfo.md: partProps re-descrito (bolsa completa), la cadena causal de commits, y las filas de la tabla de ejecutores (data/aria/role → bolsa). - coincident: los DOS docstrings que ensenaban «in-flight» (types.ts y morfo.md) se alinean con la equivalencia FIRMADA de sema.md §slider («emit-then-handler, like pre; declared indivisible») — el hallazgo del informe queda refutado como defecto de runtime y reducido a esto. - glossary: el kind fantasma `internal` (la clase exacta que docs-check:422 mata y su regex no ve en tablas markdown) → `public|private|virtual` real. - El gate entra en la doctrina: check:gate/gate en el loop de verificacion (testing-and-tooling y getting-started), eidos:lint como script npm en su fila, y la tabla de Commands de morfo.md. - Mi propia sobreafirmacion de ayer, calificada: el test SSR es un CANARIO de dos representantes sobre mecanismo compartido, no un censo — el censo por provider queda encolado (P1). morfo:check en getting-started declara su alcance real (data-*; role/aria sin validador DOM). - Supervivientes de C2c: el assert-vacio de cropper (la clase «guard sobre VACIO pasa») retirado con acta; sticky/README deja de citar syncAttrs. - gradient-builder/README: fila data-kind del Track + acotacion mesh v1. - Handoff: DOS fe de erratas — «nunca los vio nadie» era falso (vistos y reducidos 12→3 en cb554ae2e; lo que faltaba era la puerta) y la ley del escritor imperativo gana su excepcion abierta (textarea autosize, con su cierre correcto encolado). Verificacion: docs:check 0/817 · cropper 19/19 · morfo schema+compile 133/133. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
1 month ago
`handler -> state mutation -> render bag re-derives -> Svelte renders`
(P0 fase C, audit 2026-08-26 — the bag is the single attr pipeline). The
`commits` declaration documents what an external observer will see and is
checked by the smoke suite.
### The `trigger(eventName)` sequence
```
1. prewrite imperative (data-last-action, etc.)
feat(sema)!: la señal precede al commit — emit() devuelve {id, settled} síncrono, el hold no bloquea a nadie, y el motor enacta la reducción 'state' (D-full + sema §3.9) D-full. `EngineSemantic.emit(signal)` deja de ser `Promise<string>` que resolvía tras el hold y pasa a devolver SÍNCRONAMENTE `EmitHandle { id, settled }`: el id se acuña sin await, la proyección sigue siendo síncrona en `replace`, y `settled` cubre cola + proyección + despacho + hold + expresión + limpieza (la limpieza ocurre ANTES de resolver). El runtime de soma, en `pre`/`coincident`, proyecta y despacha ANTES del handler en el mismo tick (ya no espera el hold); en `post`, handler → tick() → emit. `TriggerResult.settled` siempre presente. La a11y del runtime (announce / foco) corre tras el despacho, no tras el hold. El modelo es el de la plataforma (`element.animate()` → `Animation.finished`): el estado cambia en el instante de la acción y la expresión corre en su propio reloj sobre un nodo que retiene Presence. `pre`/`post`/`coincident` describen el ORDEN entre proyección y mutación, nunca una espera; `coincident` pasa a ser literalmente lo que declara. Medido en Chrome real (servidor propio, misma sonda antes/después; Escape → `data-state=closed`): dialog 258.6 → 9.1 ms · drawer 286.6 → 9.1 · popover 1757.8 → 8.5 · float-panel 1807.8 → 49.7 (n=1, pane oculto). Toggle con `regime: 'queue'` conserva su forma (contact 20 · estado 28 · commit encolado 156). En los cuatro el sello `emerge-close` se proyecta ANTES del cambio de estado y `data-ending-style` aparece (Presence). El «hasta 1,5 s» del informe de auditoría era real: el tope de `awaitExpression` se pagaba ante cualquier animación viva en el target (popover, float-panel). Condición 1 (censo, fase 0): 260 eventos; 39 `pre` + 21 `coincident` (0 sin `sequence`). Solo 14 tienen handler registrado (los únicos donde D-full adelanta la mutación; cifra del adversarial, el constructor contó 12); los otros 46 ya mutaban el estado en el mismo tick que un `void trigger` (sidebar.css:363-375 y tooltip-provider:272 lo tenían escrito con medición). Los 6 que desmontan (4 overlays + toast + tooltip) tienen Presence. CERO migraciones necesarias. La regla transversal `[data-event^='emerge-close'] { animation: dismiss-fade }` (base.css) existe en LETRA sobre los cuatro overlays, pero el preset de Presence gana la cascada en todo instante (medido con getComputedStyle): el refutador tenía razón en EFECTO. Condición 2 (carrera nueva del id síncrono): `clear(id)` / `clearTarget` honran ocurrencias EN VUELO (`inFlight` + `pendingClears`): la ocurrencia no se registra como persistente al terminar y limpia como transitoria; `clear` devuelve true. Reproducida en test (hold corriendo ⇒ tras `settled`, sin proyección residual y `active` vacío). `dispose()` vacía ambas. SO4 re-firmado: un target ausente NUNCA salta el handler ni lanza en ninguna secuencia — se omite prewrite+emit, se reporta por logger con `SomaRuntimeTargetError` como payload, y el handler corre («sema es ornamental; una acción del usuario nunca se pierde por un adorno»). sema §3.9. La política perceptual `'state'` bajo reduce pasa al MOTOR: `ReducedMotionFallback` vive en sema (morfo lo importa), la señal lleva `a11y.reducedMotionFallback` (cualquier valor declarado) y el motor, con `motion.get() === 'reduce'` y `'state'`, devuelve el mismo retorno silenciado que `channels: []` (antes de `queue`). Soma deja de forzar `channels: []` (`a11yChannelsOverride` muere) y conserva `'text'`/`'focus'` con `sources.motion`. S5 se conserva como doctrina (la preferencia gana al morfo y a la llamada); cambia el enactor. Cierra el emit DIRECTO sin a11y (~76 en el docs site). Dato: hoy ningún morfo declara `'state'` (9 declaran `'text'`); el valor es contrato y simetría con la háptica, no un síntoma vivo. Prosa re-firmada (CANON intacto, silente a propósito): engine.ts docblock · sema.md (emit contract, promise semantics, lifecycle, One door, overlays, error policy, reductions, migration) · morfo.md · active-architecture.md (walkthrough) · overview.md · soma-architecture.md · chans/types.ts req. 3 · sema/README · book-deviations D.9/D.10/D.12 · morfo/types.ts (MorfoEventSequence, MorfoA11ySemantic) · comentarios del `sequence: 'post'` en dialog/drawer/popover/ float-panel (valor intacto). Verificación (tras la ronda 2): sema 337/337 · runtime 63/63 · alcance del lote 201 ficheros / 2136 tests · suite entera 5366/5366 (458 ficheros, exit 0) · check 89 errores, 0 en src/, ledger intacto (los ~76 emit de web/routes son fire-and-forget y compilan con el handle) · gate VERDE tras la ronda 1 (5359/5359; un flake de gesture.test rotate-detent bajo carga, verde en solitario y dentro del gate) · +20 tests (engine 40→53, runtime 56→63) · 3 mutaciones enrojecen y revertidas byte-idénticas (⚠ diferir la limpieza UN microtask es un no-op observable: la mutación honesta cruza a macrotask) · adversarial Opus independiente (métodos propios, árbol byte-idéntico al terminar): doctrina SANA; 8 defectos, cerrados antes de anclar los 6 que eran del lote — D-1 código (el catch de `settled` vivía en la rama onFulfilled del trigger: un handler que lanzaba en `pre` dejaba la rechazada del canal visual sin manejar; ahora se engancha donde nace el handle, con test de las tres vías) · D-2/D-3 prosa · D-4/D-5 tests (los cuatro valores del fallback, y el par S5 vuelve a discriminar) · D-8 exactitud (`clear`/`clearTarget` no honran ni cuentan transitorias en vuelo). Segunda sonda de Chrome del adversarial: 11.6 · 11.0 · 5.1 · 42.7 ms. D-6 (alcance): `contracts.test.ts` restaurado a HEAD (su diff era 100 % formateo); el reformateo prettier de los docs tocados se queda. D-7 PREEXISTENTE, fila nombrada: `dispose()` durante una ocurrencia en vuelo repuebla `active` después y deja proyección residual (engine.ts, `finally` + `dispose`). Pendiente del autor: mirar el cierre de Dialog y Toast en un Chrome que PINTE (los panes de los agentes estaban ocultos: mutaciones y estilo computado medidos, frame no visto). Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
3 weeks ago
2. semantic.emit(event) → { id, settled }, dispatched, NOT awaited
3. provider's synchronous handler mutates state — same tick
docs(uix): tanda 2 del contraste con el corpus — el modelo de los capitulos alcanza a la tuberia unica La lectura completa del corpus (6 lectores Opus, 6/6 censos, ~4,5M tokens) destapo lo que la tanda 1 no vio, y esta tanda lo cierra: - El modelo «Effects sync attrs» — repetido VERBATIM en cuatro capitulos (morfo, soma-architecture, overview, active-architecture), en sus tablas de piezas, en los cuatro pasos de trigger y en las dos frases-resumen — muere: la pieza es la bolsa de render; ADom aplica solo el prewrite. - morfo.md: partProps re-descrito (bolsa completa), la cadena causal de commits, y las filas de la tabla de ejecutores (data/aria/role → bolsa). - coincident: los DOS docstrings que ensenaban «in-flight» (types.ts y morfo.md) se alinean con la equivalencia FIRMADA de sema.md §slider («emit-then-handler, like pre; declared indivisible») — el hallazgo del informe queda refutado como defecto de runtime y reducido a esto. - glossary: el kind fantasma `internal` (la clase exacta que docs-check:422 mata y su regex no ve en tablas markdown) → `public|private|virtual` real. - El gate entra en la doctrina: check:gate/gate en el loop de verificacion (testing-and-tooling y getting-started), eidos:lint como script npm en su fila, y la tabla de Commands de morfo.md. - Mi propia sobreafirmacion de ayer, calificada: el test SSR es un CANARIO de dos representantes sobre mecanismo compartido, no un censo — el censo por provider queda encolado (P1). morfo:check en getting-started declara su alcance real (data-*; role/aria sin validador DOM). - Supervivientes de C2c: el assert-vacio de cropper (la clase «guard sobre VACIO pasa») retirado con acta; sticky/README deja de citar syncAttrs. - gradient-builder/README: fila data-kind del Track + acotacion mesh v1. - Handoff: DOS fe de erratas — «nunca los vio nadie» era falso (vistos y reducidos 12→3 en cb554ae2e; lo que faltaba era la puerta) y la ley del escritor imperativo gana su excepcion abierta (textarea autosize, con su cierre correcto encolado). Verificacion: docs:check 0/817 · cropper 19/19 · morfo schema+compile 133/133. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
1 month ago
4. the render bag re-derives structural attrs (data-state, aria-*) and
Svelte renders them
```
State is the only source of truth. The DOM is derivative.
feat(sema)!: la señal precede al commit — emit() devuelve {id, settled} síncrono, el hold no bloquea a nadie, y el motor enacta la reducción 'state' (D-full + sema §3.9) D-full. `EngineSemantic.emit(signal)` deja de ser `Promise<string>` que resolvía tras el hold y pasa a devolver SÍNCRONAMENTE `EmitHandle { id, settled }`: el id se acuña sin await, la proyección sigue siendo síncrona en `replace`, y `settled` cubre cola + proyección + despacho + hold + expresión + limpieza (la limpieza ocurre ANTES de resolver). El runtime de soma, en `pre`/`coincident`, proyecta y despacha ANTES del handler en el mismo tick (ya no espera el hold); en `post`, handler → tick() → emit. `TriggerResult.settled` siempre presente. La a11y del runtime (announce / foco) corre tras el despacho, no tras el hold. El modelo es el de la plataforma (`element.animate()` → `Animation.finished`): el estado cambia en el instante de la acción y la expresión corre en su propio reloj sobre un nodo que retiene Presence. `pre`/`post`/`coincident` describen el ORDEN entre proyección y mutación, nunca una espera; `coincident` pasa a ser literalmente lo que declara. Medido en Chrome real (servidor propio, misma sonda antes/después; Escape → `data-state=closed`): dialog 258.6 → 9.1 ms · drawer 286.6 → 9.1 · popover 1757.8 → 8.5 · float-panel 1807.8 → 49.7 (n=1, pane oculto). Toggle con `regime: 'queue'` conserva su forma (contact 20 · estado 28 · commit encolado 156). En los cuatro el sello `emerge-close` se proyecta ANTES del cambio de estado y `data-ending-style` aparece (Presence). El «hasta 1,5 s» del informe de auditoría era real: el tope de `awaitExpression` se pagaba ante cualquier animación viva en el target (popover, float-panel). Condición 1 (censo, fase 0): 260 eventos; 39 `pre` + 21 `coincident` (0 sin `sequence`). Solo 14 tienen handler registrado (los únicos donde D-full adelanta la mutación; cifra del adversarial, el constructor contó 12); los otros 46 ya mutaban el estado en el mismo tick que un `void trigger` (sidebar.css:363-375 y tooltip-provider:272 lo tenían escrito con medición). Los 6 que desmontan (4 overlays + toast + tooltip) tienen Presence. CERO migraciones necesarias. La regla transversal `[data-event^='emerge-close'] { animation: dismiss-fade }` (base.css) existe en LETRA sobre los cuatro overlays, pero el preset de Presence gana la cascada en todo instante (medido con getComputedStyle): el refutador tenía razón en EFECTO. Condición 2 (carrera nueva del id síncrono): `clear(id)` / `clearTarget` honran ocurrencias EN VUELO (`inFlight` + `pendingClears`): la ocurrencia no se registra como persistente al terminar y limpia como transitoria; `clear` devuelve true. Reproducida en test (hold corriendo ⇒ tras `settled`, sin proyección residual y `active` vacío). `dispose()` vacía ambas. SO4 re-firmado: un target ausente NUNCA salta el handler ni lanza en ninguna secuencia — se omite prewrite+emit, se reporta por logger con `SomaRuntimeTargetError` como payload, y el handler corre («sema es ornamental; una acción del usuario nunca se pierde por un adorno»). sema §3.9. La política perceptual `'state'` bajo reduce pasa al MOTOR: `ReducedMotionFallback` vive en sema (morfo lo importa), la señal lleva `a11y.reducedMotionFallback` (cualquier valor declarado) y el motor, con `motion.get() === 'reduce'` y `'state'`, devuelve el mismo retorno silenciado que `channels: []` (antes de `queue`). Soma deja de forzar `channels: []` (`a11yChannelsOverride` muere) y conserva `'text'`/`'focus'` con `sources.motion`. S5 se conserva como doctrina (la preferencia gana al morfo y a la llamada); cambia el enactor. Cierra el emit DIRECTO sin a11y (~76 en el docs site). Dato: hoy ningún morfo declara `'state'` (9 declaran `'text'`); el valor es contrato y simetría con la háptica, no un síntoma vivo. Prosa re-firmada (CANON intacto, silente a propósito): engine.ts docblock · sema.md (emit contract, promise semantics, lifecycle, One door, overlays, error policy, reductions, migration) · morfo.md · active-architecture.md (walkthrough) · overview.md · soma-architecture.md · chans/types.ts req. 3 · sema/README · book-deviations D.9/D.10/D.12 · morfo/types.ts (MorfoEventSequence, MorfoA11ySemantic) · comentarios del `sequence: 'post'` en dialog/drawer/popover/ float-panel (valor intacto). Verificación (tras la ronda 2): sema 337/337 · runtime 63/63 · alcance del lote 201 ficheros / 2136 tests · suite entera 5366/5366 (458 ficheros, exit 0) · check 89 errores, 0 en src/, ledger intacto (los ~76 emit de web/routes son fire-and-forget y compilan con el handle) · gate VERDE tras la ronda 1 (5359/5359; un flake de gesture.test rotate-detent bajo carga, verde en solitario y dentro del gate) · +20 tests (engine 40→53, runtime 56→63) · 3 mutaciones enrojecen y revertidas byte-idénticas (⚠ diferir la limpieza UN microtask es un no-op observable: la mutación honesta cruza a macrotask) · adversarial Opus independiente (métodos propios, árbol byte-idéntico al terminar): doctrina SANA; 8 defectos, cerrados antes de anclar los 6 que eran del lote — D-1 código (el catch de `settled` vivía en la rama onFulfilled del trigger: un handler que lanzaba en `pre` dejaba la rechazada del canal visual sin manejar; ahora se engancha donde nace el handle, con test de las tres vías) · D-2/D-3 prosa · D-4/D-5 tests (los cuatro valores del fallback, y el par S5 vuelve a discriminar) · D-8 exactitud (`clear`/`clearTarget` no honran ni cuentan transitorias en vuelo). Segunda sonda de Chrome del adversarial: 11.6 · 11.0 · 5.1 · 42.7 ms. D-6 (alcance): `contracts.test.ts` restaurado a HEAD (su diff era 100 % formateo); el reformateo prettier de los docs tocados se queda. D-7 PREEXISTENTE, fila nombrada: `dispose()` durante una ocurrencia en vuelo repuebla `active` después y deja proyección residual (engine.ts, `finally` + `dispose`). Pendiente del autor: mirar el cierre de Dialog y Toast en un Chrome que PINTE (los panes de los agentes estaban ocultos: mutaciones y estilo computado medidos, frame no visto). Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
3 weeks ago
**The signal precedes the commit; it does not gate it** (D-full, 2026-09-15).
Step 2 used to be `await semantic.emit(event)`, and the emit only resolved after
the ~240 ms hold: the state changed a quarter of a second after the gesture, and
the wait bought nothing — the stamp and the sound happen BEFORE the hold, and the
node CSS reacts on is retained through its exit by `Presence`, not by the delay.
The hold and the expression now run on their own clock, reachable as
`TriggerResult.settled` for the rare caller that needs them.
### Provider responsibilities
The provider supplies what morfo cannot infer:
```ts
const runtime = createSomaRuntime(morfo, {
dom: this.soma.dom,
feat(motion)!: una fuente de motion, y los cuatro ejes visuales SIEMPRE en prefs — eidos no lee medios, nadie en el árbol uix pregunta al SO, ningún esquema del app deja a eidos sin motor (P2 #5) La preferencia efectiva de motion la decide prefs UNA vez (`resolveMotion`: la intención `allow|reduce` gana al hint del SO, `system` deriva) y la proyección la estampa en `<html data-motion>` — desde e3c0899dd antes del primer pintado. Aun así el árbol la re-derivaba por su cuenta leyendo el SO en crudo en dos capas: A) CSS de eidos (§61 del changelog): 65 at-rules `@media (prefers-reduced-motion: reduce)` en 63 ficheros + 29 emitidas por el generador, conviviendo con 14 + 52 selectores `[data-motion='reduce']` («with or without the JS projection»). Dos fuentes ⇒ un `allow` explícito no llegaba al CSS. Cada bloque pasa a `[data-motion='reduce'] SEL` con la MISMA declaración y orden (paridad a máquina: 63 ficheros, 65 bloques, 89 reglas, 178 pares selector-declaraciones, 0 discrepancias); los 3 ficheros con gemelo idéntico se unifican; ningún bloque usaba `:root`. Los 4 `!important` dentro de bloques migrados QUEDAN con su rival nombrado (navigation-menu ×2 contra el swap direccional (0,3,0); text-focus contra un `style:transition` inline; events.css empate (0,3,0) con las firmas generadas en otra hoja). Generador: los 4 emisores dejan de emitir el media, ink marks gana su gemelo; `generated/base.css` 29 → 0 media, `--nombres` 8034 / 5751 únicos INTACTOS. GUARD nuevo `reduced-motion-media.test.ts` (at-rule, no prosa; anti-vacío > 60 ficheros; mordido por mutación). Cambio de veredicto de cascada MEDIDO en Chrome real por el adversarial: el prefijo suma (0,1,0), una veintena de variantes más específicas que ganaban al media hoy pierden — y bajo la media del SO la forma de HEAD NO paraba el anillo del spinner. `data-motion` tiene TRES dueños (reduce · navigation-menu from/to · tabs fade|slide), valores disjuntos, forma ancestro obligatoria. Prosa que afirmaba «both» barrida por la AFIRMACIÓN (tooltip.css, card.css, recipes/base.ts, skin-media-player, events, float-panel). B) JS (§62): el motor de motion (`engine-motion.ts:131`), el motor de escena (`engine-scene.ts:99`), la háptica de sema (`chans/haptic.ts:129`) y OCHO lecturas en componentes de eidos preguntaban a `ActiveDom.prefersReducedMotion` (el media en crudo). El puerto correcto YA EXISTÍA sin consumidores: `MotionSource = Source<MotionEffective>` (`$libs/motion`). La fuente nace en prefs (`createMotionSourceFromPrefs`, `src/arts/prefs/motion-source.ts`, precedente `createLocaleSourceFromPrefs`) y la construyen UNA vez las tres raíces: `createActiveUix`, `attachActiveUix` (fallbacks desde `app.prefs`) y las fábricas `defineEngineMotion` / `defineEngineScene` / `defineEngineSemantic` (`coreDependencies: ['prefs']`, forma de format y langs). MUEREN de los puertos `MotionDom.prefersReducedMotion`, `SceneDom.prefersReducedMotion` y el interface `HapticChannelDom` entero (un puerto que contesta política es la puerta por donde vuelve el defecto); `MotionRunOptions.reduced` se queda como PIN por ejecución. `ActiveEidos.reducedMotion` con dos puertas: con prefs el efectivo; standalone sigue al SO con acta (sin primer motor no hay segundo, §60). Los ocho sitios de componentes leen `eidos.reducedMotion`. Hallazgo del lote: DOS motores de escena por superficie DOM (aura-indicator, pack Ambient) que se habrían quedado ciegos a la policy `reduce` obligatoria (P-1) EN SILENCIO — reciben la fuente vía `eidos.reducedMotion`. Deuda nombrada: escena lee la fuente en el montaje (paridad con la lectura del media que sustituye). `ActiveDom.prefersReducedMotion` no se toca: es un hecho del SO que alimenta el ENTORNO de prefs y nada más. Soma (adenda firmada «a en B»): el último lector de política que preguntaba al SO, `soma/runtime.svelte.ts:1280` (migración a11y del morfo: `'state'` ⇒ `channels: []` por S5, `'text'` ⇒ región viva, `'focus'` ⇒ foco), lee la MISMA fuente: `SomaRuntimeBaseSources.motion: MotionSource` OBLIGATORIO como `dom` (un runtime que no puede responder «¿reducido?» no puede honrar S5 — lo garantiza el tipo), llenado por `Soma.runtime()` desde `uix.prefs` (esquema sin `motion` ⇒ permitir, como los motores). La decisión mayor (b) — que el MOTOR aplique `'state'` en su pasada de reducción y que un `emit()` directo reciba tratamiento a11y — queda ABIERTA como fila §3.9 de CONTINUE-sema-audit.md, a ejecutar junto a D-full. `ActiveEidos.reducedMotion` distingue «sin prefs» (standalone ⇒ SO, §60) de «prefs sin ranura motion» (⇒ permitir, como los motores): las dos mitades de una UI ya no discrepan bajo un esquema sin la dimensión. Hallazgo de la adenda: la bolsa `sources` se construye en 96 sitios (3 de producción — menu-dial, metrics, onion-menu — que reciben la fuente vía `eidos.reducedMotion`, y 93 harnesses cuyo `as unknown as Soma` CEGABA la comprobación de miembros: 60 tests rojos hasta declarar el `Omit` real; el tipo hizo su trabajo en el código de producción y era ciego justo donde se suponía que bastaba). Test real nuevo `soma/test/reduced-motion-source.svelte.test.ts` (chromium: `Soma.create()` lee contexto Svelte) con `createActiveUix` + `EngineSemantic` reales; mutación (el trigger vuelve al dom) ROJA 3/3. Adversarial dirigido de la adenda: siete defectos cerrados — `src/uix/contracts.ts` declaraba `requires: ['dom']` para la bolsa de soma y el test no lo asertaba (un guard que no inspecciona nada pasa) → `['dom', 'motion']` + aserto; la bolsa #97 (`bag-census.test.ts`) sin `motion` bajo un casteo; el camino `'text'` —el ÚNICO que declaran 9 morfos de producción— sin test (añadido); dos snippets de docs que ya no compilaban (component-guide A1, morfo.md); cifras del §62. C) La raíz GARANTIZA los cuatro ejes visuales (cazado por el autor en el docs site: texto invisible en oscuro). Cuatro layouts congelados componen su propio esquema de prefs SIN `mode/theme/density/scaling`; un esquema del app SUSTITUÍA al de la raíz entero y eidos «degradaba defensivamente» a `mode = 'light'` CONSTANTE ignorando el SO, mientras el shell estampaba su wrapper en oscuro: tinta de tema claro sobre superficies oscuras. Forma: `uixVisualPrefsDimensions()` (pura, en `prefs-schema.ts`; el boot compila la misma) y `createActiveUix` fusiona SIEMPRE `{ ...ejesVisuales, ...esquemaDelApp }` — el app puede REDEFINIR un eje, nunca omitirlo; sin un solo cast. Eidos deja de degradar en silencio: `createPrefsPreferenceSource(prefs, fallbacks, onMissing)` avisa por `uix.logger.warn` por cada ranura ausente (queda solo para attach con prefs ajenas; docs de attach: el app compone `uixVisualPrefsDimensions()`). Tests reales (esquema sin ejes + SO oscuro ⇒ `data-mode="dark"`; app que redefine `theme` gana; prefs ajenas ⇒ 4 avisos por el logger REAL); el viejo test «degrades to the fallbacks» estaba verde POR COINCIDENCIA (defaults = fallbacks) y se sustituye; mutación (sin fusión) 2 rojos y restaurada. Boot regenerado (15134 B), delta cero ×5 intacto. `web/routes/uix/+layout@.svelte` (descongelado por orden del autor, +14/−29): muere el `modeSource` muerto y el `$state` local; el shell LEE la ranura `mode` y el toggle escribe `uix.prefs.setIntent('mode', …)`; migración única de `uix-docs-theme` al sobre de prefs (clave borrada). Ledger 95 → 93 (la entrada de ese fichero desaparece). VERIFICADO EN CHROME por el coordinador: SO oscuro sin clave ⇒ `<html data-mode="dark">`, wrapper dark, tinta `oklch(0.95)`; clave vieja `dark` ⇒ intent `dark`; toggle mueve html, wrapper, tinta e intent en los dos sentidos. Nombrado, no arreglado: el boot compila siempre el esquema por defecto (un app que REDEFINE un eje resuelve distinto que el boot; hoy nadie en web/ usa el boot) · los otros tres layouts congelados recuperan los ejes pero su wrapper sigue en `$state` local · SSR del docs sirve `light` y la hidratación corrige (previo). Verificación: A — vitest eidos+value-channels 43/483, eidos:lint 0, paridad 0 discrepancias, mutación del guard 3/3 y 4/4 (adversarial), prettier solo avisos preexistentes; B — vitest del scope 90/990 y, con la adenda, 242/2395; +26 tests (motor 5 · escena 2 · háptica 2 · raíz real jsdom 7 · ActiveEidos 7 · fábrica 1 · soma 3 — cifras verificadas una a una por el adversarial), 11 dobles re-firmados, 93 harnesses de soma tipados con el `Omit` real, cuatro mutaciones ROJAS (3/2/3 + la de soma 3/3) y restauradas por sha256, más la mutación de TIPO del adversarial (la fuente devuelve la intención ⇒ 2 errores nuevos en src/: el puerto es gate de tipo, no prosa); C — vitest active-uix+eidos+prefs 57/591, boot 8/8 con delta cero, mutación 2 rojos, verificación en Chrome por el coordinador; todos — check src/ 0 (ledger 95 → 93: MENGUA), check:gate OK, docs:check OK, arts:check OK, packs:check OK. Suite completa 457/5336 verde (adversarial A). Adversariales Opus independientes por lote (informes en el handoff). Constructores + adversariales Opus 5; la sesión coordina. ⚠ Lección: la cuenta del brief de A («94 en fuentes») era un fallo de medida del coordinador (`grep -rh | grep -v generated` filtra LÍNEAS con esa palabra, no el directorio); el constructor la re-midió porque el brief lo exigía. ⚠ Dos constructores cortados por límite de sesión y reanudados tras releer su diff entero (ley de la casa). Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
3 weeks ago
// REQUIRED like `dom` (changelog §62 §7): the runtime honours the morfo's
// `a11ySemantic.reducedMotionFallback`, so it must be able to answer «is
// motion reduced?». `this.soma.runtime(...)` injects it from `uix.prefs`;
// through the low-level factory the caller passes a `MotionSource`.
motion,
eventEngine: this.soma.events,
states: { open: () => this.opts.open.current },
props: { disabled: () => this.opts.disabled.current },
parts: { content: () => this.contentId.current },
events: {
feat(morfo,sema,eidos): todo evento dice de que familia es, y el cruce por fin se ve El framework llamaba a la misma cosa de dos maneras: `open` pelado en ocho componentes y `emerge-open` en tres. No era estetica — un preset de movimiento engancha el nombre con `^=`, asi que el dialecto pelado no casaba con ninguna firma y simplemente no animaba, sin romper una sola prueba. Los 40 nombres sin prefijo pasan a `{familia}-{verbo}[-{matiz}]`: 256 eventos, 256 con prefijo, 0 ambiguos. El plan decia 36 y decia `handle-drag-start`; eran 40, y el canon (c25) dice que esos verbos son `pick` y `drop` — `handle-pick` y `handle-drop` ya existian en 10 y 4 componentes. `validateMorfo` cierra la puerta: un `events[].name` que no empiece por su familia ahora lanza. Visto fallar antes con un nombre pelado inyectado. Lo que el renombrado destapo, y va aqui tambien: - La receta del splitter enganchaba `commit-resize`, muerto desde `bd2e40366`. No casaba desde mayo y nadie chillo. Reescrita por FAMILIA, como slider y knob, y `eidos-lint` valida ahora el VALOR de `data-event*` contra el catalogo de morfos — el guard que lo habria cazado en su dia. - La familia `shift` era muda en el canal visual, contra su propia doctrina (c27: el cruce debe percibirse; c34 tipifica el «shift invisible»). Su mapa ya describia la firma que le faltaba y su sonido por defecto es `slide`. Ahora tiene firma direccional: sexto atributo del sello (`data-event-direction`, `forward`|`backward`, por emision) y deslizamiento de 320ms RTL-safe por `:dir()`. Medido: LTR -30px/+30px, RTL los invierte. - El sello de `shift-navigate` pasa del BOTON al `grid` en los cuatro calendarios. Medido: el boton recibia `contact-activate` y 8,5 ms despues —media trama— el `shift-navigate` pisaba la misma ranura y el `press-squeeze` moria sin pintar un fotograma. Una superficie, una ranura (A-36). - 101 contradicciones docs<->morfo adjudicadas con evidencia (git log, docs de decision, el componente vivo). Las docs desfasadas, corregidas; los nueve DEFECTOS de codigo obsoleto quedan abiertos y sin tocar. - `SoundDirection` -> `SoundContour`: era un contorno de tono, no un sentido, y habia tres cosas distintas deletreadas «direction». check en su linea base con 0 errores nuevos por diferencia de conjuntos · docs:check 0/0 · eidos-lint invalid 0 · el censo y las escenas de navegador medidas con raton real y rAF vivo. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2 months ago
'emerge-open': () => {
this.opts.open.current = true;
},
feat(morfo,sema,eidos): todo evento dice de que familia es, y el cruce por fin se ve El framework llamaba a la misma cosa de dos maneras: `open` pelado en ocho componentes y `emerge-open` en tres. No era estetica — un preset de movimiento engancha el nombre con `^=`, asi que el dialecto pelado no casaba con ninguna firma y simplemente no animaba, sin romper una sola prueba. Los 40 nombres sin prefijo pasan a `{familia}-{verbo}[-{matiz}]`: 256 eventos, 256 con prefijo, 0 ambiguos. El plan decia 36 y decia `handle-drag-start`; eran 40, y el canon (c25) dice que esos verbos son `pick` y `drop` — `handle-pick` y `handle-drop` ya existian en 10 y 4 componentes. `validateMorfo` cierra la puerta: un `events[].name` que no empiece por su familia ahora lanza. Visto fallar antes con un nombre pelado inyectado. Lo que el renombrado destapo, y va aqui tambien: - La receta del splitter enganchaba `commit-resize`, muerto desde `bd2e40366`. No casaba desde mayo y nadie chillo. Reescrita por FAMILIA, como slider y knob, y `eidos-lint` valida ahora el VALOR de `data-event*` contra el catalogo de morfos — el guard que lo habria cazado en su dia. - La familia `shift` era muda en el canal visual, contra su propia doctrina (c27: el cruce debe percibirse; c34 tipifica el «shift invisible»). Su mapa ya describia la firma que le faltaba y su sonido por defecto es `slide`. Ahora tiene firma direccional: sexto atributo del sello (`data-event-direction`, `forward`|`backward`, por emision) y deslizamiento de 320ms RTL-safe por `:dir()`. Medido: LTR -30px/+30px, RTL los invierte. - El sello de `shift-navigate` pasa del BOTON al `grid` en los cuatro calendarios. Medido: el boton recibia `contact-activate` y 8,5 ms despues —media trama— el `shift-navigate` pisaba la misma ranura y el `press-squeeze` moria sin pintar un fotograma. Una superficie, una ranura (A-36). - 101 contradicciones docs<->morfo adjudicadas con evidencia (git log, docs de decision, el componente vivo). Las docs desfasadas, corregidas; los nueve DEFECTOS de codigo obsoleto quedan abiertos y sin tocar. - `SoundDirection` -> `SoundContour`: era un contorno de tono, no un sentido, y habia tres cosas distintas deletreadas «direction». check en su linea base con 0 errores nuevos por diferencia de conjuntos · docs:check 0/0 · eidos-lint invalid 0 · el censo y las escenas de navegador medidas con raton real y rAF vivo. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2 months ago
'emerge-close-cancel': () => {
this.opts.open.current = false;
}
}
});
```
Each part-provider then renders only the static identity:
```ts
readonly props = $derived.by(() => runtime.partProps('trigger'));
docs(uix): tanda 2 del contraste con el corpus — el modelo de los capitulos alcanza a la tuberia unica La lectura completa del corpus (6 lectores Opus, 6/6 censos, ~4,5M tokens) destapo lo que la tanda 1 no vio, y esta tanda lo cierra: - El modelo «Effects sync attrs» — repetido VERBATIM en cuatro capitulos (morfo, soma-architecture, overview, active-architecture), en sus tablas de piezas, en los cuatro pasos de trigger y en las dos frases-resumen — muere: la pieza es la bolsa de render; ADom aplica solo el prewrite. - morfo.md: partProps re-descrito (bolsa completa), la cadena causal de commits, y las filas de la tabla de ejecutores (data/aria/role → bolsa). - coincident: los DOS docstrings que ensenaban «in-flight» (types.ts y morfo.md) se alinean con la equivalencia FIRMADA de sema.md §slider («emit-then-handler, like pre; declared indivisible») — el hallazgo del informe queda refutado como defecto de runtime y reducido a esto. - glossary: el kind fantasma `internal` (la clase exacta que docs-check:422 mata y su regex no ve en tablas markdown) → `public|private|virtual` real. - El gate entra en la doctrina: check:gate/gate en el loop de verificacion (testing-and-tooling y getting-started), eidos:lint como script npm en su fila, y la tabla de Commands de morfo.md. - Mi propia sobreafirmacion de ayer, calificada: el test SSR es un CANARIO de dos representantes sobre mecanismo compartido, no un censo — el censo por provider queda encolado (P1). morfo:check en getting-started declara su alcance real (data-*; role/aria sin validador DOM). - Supervivientes de C2c: el assert-vacio de cropper (la clase «guard sobre VACIO pasa») retirado con acta; sticky/README deja de citar syncAttrs. - gradient-builder/README: fila data-kind del Track + acotacion mesh v1. - Handoff: DOS fe de erratas — «nunca los vio nadie» era falso (vistos y reducidos 12→3 en cb554ae2e; lo que faltaba era la puerta) y la ley del escritor imperativo gana su excepcion abierta (textarea autosize, con su cierre correcto encolado). Verificacion: docs:check 0/817 · cropper 19/19 · morfo schema+compile 133/133. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
1 month ago
// returns: { id, ref attachment, 'data-{component}-trigger': '',
// role, aria-*, data-state, ... } — the FULL resolved contract
```
docs(uix): tanda 2 del contraste con el corpus — el modelo de los capitulos alcanza a la tuberia unica La lectura completa del corpus (6 lectores Opus, 6/6 censos, ~4,5M tokens) destapo lo que la tanda 1 no vio, y esta tanda lo cierra: - El modelo «Effects sync attrs» — repetido VERBATIM en cuatro capitulos (morfo, soma-architecture, overview, active-architecture), en sus tablas de piezas, en los cuatro pasos de trigger y en las dos frases-resumen — muere: la pieza es la bolsa de render; ADom aplica solo el prewrite. - morfo.md: partProps re-descrito (bolsa completa), la cadena causal de commits, y las filas de la tabla de ejecutores (data/aria/role → bolsa). - coincident: los DOS docstrings que ensenaban «in-flight» (types.ts y morfo.md) se alinean con la equivalencia FIRMADA de sema.md §slider («emit-then-handler, like pre; declared indivisible») — el hallazgo del informe queda refutado como defecto de runtime y reducido a esto. - glossary: el kind fantasma `internal` (la clase exacta que docs-check:422 mata y su regex no ve en tablas markdown) → `public|private|virtual` real. - El gate entra en la doctrina: check:gate/gate en el loop de verificacion (testing-and-tooling y getting-started), eidos:lint como script npm en su fila, y la tabla de Commands de morfo.md. - Mi propia sobreafirmacion de ayer, calificada: el test SSR es un CANARIO de dos representantes sobre mecanismo compartido, no un censo — el censo por provider queda encolado (P1). morfo:check en getting-started declara su alcance real (data-*; role/aria sin validador DOM). - Supervivientes de C2c: el assert-vacio de cropper (la clase «guard sobre VACIO pasa») retirado con acta; sticky/README deja de citar syncAttrs. - gradient-builder/README: fila data-kind del Track + acotacion mesh v1. - Handoff: DOS fe de erratas — «nunca los vio nadie» era falso (vistos y reducidos 12→3 en cb554ae2e; lo que faltaba era la puerta) y la ley del escritor imperativo gana su excepcion abierta (textarea autosize, con su cierre correcto encolado). Verificacion: docs:check 0/817 · cropper 19/19 · morfo schema+compile 133/133. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
1 month ago
Everything the morfo declares (`role`, `aria-*`, `data-state`, `data-intent`)
resolves into the render bag and Svelte renders it — server-rendered included
(P0 fase C, audit 2026-08-26). There is no per-part imperative attr writer;
the one imperative write left is `trigger`'s declared prewrite.
### Operational rules
docs(uix): tanda 2 del contraste con el corpus — el modelo de los capitulos alcanza a la tuberia unica La lectura completa del corpus (6 lectores Opus, 6/6 censos, ~4,5M tokens) destapo lo que la tanda 1 no vio, y esta tanda lo cierra: - El modelo «Effects sync attrs» — repetido VERBATIM en cuatro capitulos (morfo, soma-architecture, overview, active-architecture), en sus tablas de piezas, en los cuatro pasos de trigger y en las dos frases-resumen — muere: la pieza es la bolsa de render; ADom aplica solo el prewrite. - morfo.md: partProps re-descrito (bolsa completa), la cadena causal de commits, y las filas de la tabla de ejecutores (data/aria/role → bolsa). - coincident: los DOS docstrings que ensenaban «in-flight» (types.ts y morfo.md) se alinean con la equivalencia FIRMADA de sema.md §slider («emit-then-handler, like pre; declared indivisible») — el hallazgo del informe queda refutado como defecto de runtime y reducido a esto. - glossary: el kind fantasma `internal` (la clase exacta que docs-check:422 mata y su regex no ve en tablas markdown) → `public|private|virtual` real. - El gate entra en la doctrina: check:gate/gate en el loop de verificacion (testing-and-tooling y getting-started), eidos:lint como script npm en su fila, y la tabla de Commands de morfo.md. - Mi propia sobreafirmacion de ayer, calificada: el test SSR es un CANARIO de dos representantes sobre mecanismo compartido, no un censo — el censo por provider queda encolado (P1). morfo:check en getting-started declara su alcance real (data-*; role/aria sin validador DOM). - Supervivientes de C2c: el assert-vacio de cropper (la clase «guard sobre VACIO pasa») retirado con acta; sticky/README deja de citar syncAttrs. - gradient-builder/README: fila data-kind del Track + acotacion mesh v1. - Handoff: DOS fe de erratas — «nunca los vio nadie» era falso (vistos y reducidos 12→3 en cb554ae2e; lo que faltaba era la puerta) y la ley del escritor imperativo gana su excepcion abierta (textarea autosize, con su cierre correcto encolado). Verificacion: docs:check 0/817 · cropper 19/19 · morfo schema+compile 133/133. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
1 month ago
- `partProps(part)` returns the full render bag: static identity (id, ref,
marker, dir) plus every resolved morfo plan.
- `dom.apply` is the only writer of mutable attrs.
- Event handlers are synchronous. Async work happens before `trigger()` is called.
- Guards (`if (disabled) return`) live at the call-site, not inside the handler —
if they enter the handler, the perceptual signal already fired.
- `Semantic` may use `Dom` (downward dependency); `Dom` does not know `Semantic`.
See [overview.md](./overview.md) §2.bis for the cross-layer view.
---
fix(sema,morfo,soma): una superficie no admite dos ocurrencias, y el nombre ya no miente `data-event-*` es UNA RANURA por elemento. Tres auditorias independientes encontraron el mismo defecto y ninguna lo cerro: fable S1 (2026-07-01, con el README diciendo «findings executed»), sema S-17 (2026-08-05, midio el estampado del knob muriendo a los 24ms de un hold de 240) y blocks A-36/A-65 (reproducido en navegador, dos estampados a 1,1ms). Lo que faltaba no era diagnostico: nadie podia distinguir una redireccion legitima de una deriva, porque la opcion que redirigia se llamaba `fallbackTarget` y hacia lo contrario de lo que decia. EL NOMBRE PRIMERO. `fallbackTarget` -> `targetOverride` (202 apariciones, 64 ficheros, dirigido con `git ls-files`). Siempre GANO sobre el ref registrado; el nombre honesto es la precondicion para auditarlo. Las cronicas conservan el viejo por diseño. LA PROPIEDAD. `unstampEventAttrs` recibia el signal y lo DESCARTABA, asi que la ocurrencia que terminaba antes borraba a la que tenia la ranura. Ahora comprueba `data-event-id`. Con eso, `replace` deja de ser el comportamiento roto y pasa a ser el suelo correcto. A-36, SIN `regime`. Los cuatro overlays cedian el estampado del `open` al trigger por un workaround de la era `sequence: 'pre'`; con `post` el content ya esta montado. Retirado el override (float-panel migrado `pre`->`post`, ultimo fuera de la doctrina que sema.md escribe). Medido en /blocks/site-header/preview a 375px: contact-activate en el TRIGGER +18,7ms y open en el CONTENT +20,6ms, con press-squeeze y slide-from-right-full corriendo. Antes: los dos en el trigger, press-squeeze jamas. `regime` DEFINIDO — y son dos valores, no cuatro. `replace` (default) y `queue`, este solo para las 4 parejas irreducibles: los tres toggles (el provider ES el boton) y el knob. `collapse` retirado (su caso murio con la propiedad: medido, 0 huecos en 5 emisiones a 72ms) y `lock` retirado (significaria «un cierre que no se anuncia»), con sus 4 declaraciones muertas. El guard de `queue` PASO EN VERDE SIENDO INCORRECTO: con timers falsos no hay animaciones, asi que awaitExpression volvia al instante. El navegador midio el commit encolado llegando 1,6s tarde. `queue` espera el HOLD, no la expresion. Medir la envolvente no es medir la salida. `renderAttrs` — los attrs que no son partes. Vaciar ACTIVE_DEV_TRACK (palabras y chronos ya no estan excluidos) destapo 23 `data-palabras-*` que ningun morfo declaraba y que palabras.css SI estiliza: aterrizan en el arbol que el usuario escribio, asi que no pueden ser partes. Campo nuevo con la linea afilada —si el consumidor puede componerlo, es una PARTE— y 3 consumidores (palabras, waveform, aura). Declarado en types.ts Y en el esquema sium: la leccion de MorfoElement. Ademas: 5 selectores muertos en el predicado del focus-scope de palabras (barrido de todo el repo: aparecian solo ahi) · RTL-1 real en palabras-chrome (inset logico + translate fisico = doble volteo; rtl:check 1->0) · el union inline al `Direction` canonico, que destapo que el censo de direccion grepea el NOMBRE del tipo · deuda de chronos (scope, barrel, README). Y de la cola: S-36 (barrel 68->71 + guard), S-31 (vibrate(NaN)), S5 (la preferencia de reduced-motion del usuario ya gana a los channels del morfo), SO2 (el warn que el JSDoc prometia), SO4 (validar el target antes del prewrite). Todos los guards nuevos vistos fallar antes de arreglar nada. Base: check 74 (= base) · sema+morfo+contracts 454 verdes / 6 ajenos preexistentes · soma navegador 1258/1259 (el rojo es un timeout ajeno, reproducido con los cambios en stash) · docs:check 0/618 · rtl:check 0/177. Handoff: docs/process/CONTINUE-perceptual-surface.md Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2 months ago
## `renderAttrs` — the attrs that are not parts
Some components write `data-{component}-*` onto elements that **are not parts
and never could be**:
- a document engine marks the user's OWN tree — `palabras` stamps block kind,
heading level, syntax tokens and i18n state on `<blockquote>` / `<table>` /
`<figure>` nodes whose shape depends on what the user wrote;
- a component marks its internal render nodes — `waveform`'s two `<path>`s,
which its own source already calls "eidos-only hooks".
They cannot be `parts[]` (the morfo could never enumerate them) and they cannot
be `data-_*` (the private prefix is outside morfo by design, and eidos STYLES
these — `palabras.css` alone has rules for 23). That left a real cross-layer
contract, soma writing and eidos reading, that **nothing declared** — which is
what morfo exists to prevent.
```ts
renderAttrs: [
{ attr: 'data-palabras-block' },
{ attr: 'data-palabras-heading-level' },
{ attr: 'data-palabras-untranslated' }
docs(uix): la doctrina alcanza a la bolsa única — el corpus deja de enseñar la API muerta La clase exacta que la auditoría flagueó (doc↔código), producida esta vez por NUESTROS propios cambios de P0 fase C y sin corregir hasta ahora: - component-guide §«two sanctioned ways»: reescrita a UNA vía (la bolsa .props resuelve el contrato entero; extras solo lo que el morfo no puede expresar; valores CRUDOS; la inversa «morfo sin value ⇒ soma escribe»). Los 4 snippets con syncAttrs: true, limpiados. - soma-architecture: partProps re-descrito como LA bolsa (identidad + contrato completo, SSR incluido); el párrafo del flag sustituido por «el único escritor imperativo es el prewrite»; snippet limpiado. - soma.md: snippet de accordion sin el flag. - morfo.md §naming: el «(SSR included)» dejó de ser contraste — desde la tubería única TODO plan viaja en la bolsa; lo que distingue a los naming es la PRECEDENCIA (consumer-first via mergeProps). - testing-and-tooling: la postura SSR gana el canario ssr-contract.test, y nace «The gate» (check:gate + ledger menguante, gate, hook — los instrumentos de fase B no estaban documentados). - CLAUDE.md: el paso 4 de trigger ya no dice «effect-driven» (prettier normalizó de paso el fichero entero — solo whitespace, contenido intacto, declarado aquí para que el diff ancho no sea silencioso). Lo histórico (changelog, old-deprecated, audit de julio, process/) se queda como historia. Verificación: docs:check 0 errores sobre 817 docs; cero menciones normativas de la API muerta fuera de referencias históricas. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
1 month ago
];
fix(sema,morfo,soma): una superficie no admite dos ocurrencias, y el nombre ya no miente `data-event-*` es UNA RANURA por elemento. Tres auditorias independientes encontraron el mismo defecto y ninguna lo cerro: fable S1 (2026-07-01, con el README diciendo «findings executed»), sema S-17 (2026-08-05, midio el estampado del knob muriendo a los 24ms de un hold de 240) y blocks A-36/A-65 (reproducido en navegador, dos estampados a 1,1ms). Lo que faltaba no era diagnostico: nadie podia distinguir una redireccion legitima de una deriva, porque la opcion que redirigia se llamaba `fallbackTarget` y hacia lo contrario de lo que decia. EL NOMBRE PRIMERO. `fallbackTarget` -> `targetOverride` (202 apariciones, 64 ficheros, dirigido con `git ls-files`). Siempre GANO sobre el ref registrado; el nombre honesto es la precondicion para auditarlo. Las cronicas conservan el viejo por diseño. LA PROPIEDAD. `unstampEventAttrs` recibia el signal y lo DESCARTABA, asi que la ocurrencia que terminaba antes borraba a la que tenia la ranura. Ahora comprueba `data-event-id`. Con eso, `replace` deja de ser el comportamiento roto y pasa a ser el suelo correcto. A-36, SIN `regime`. Los cuatro overlays cedian el estampado del `open` al trigger por un workaround de la era `sequence: 'pre'`; con `post` el content ya esta montado. Retirado el override (float-panel migrado `pre`->`post`, ultimo fuera de la doctrina que sema.md escribe). Medido en /blocks/site-header/preview a 375px: contact-activate en el TRIGGER +18,7ms y open en el CONTENT +20,6ms, con press-squeeze y slide-from-right-full corriendo. Antes: los dos en el trigger, press-squeeze jamas. `regime` DEFINIDO — y son dos valores, no cuatro. `replace` (default) y `queue`, este solo para las 4 parejas irreducibles: los tres toggles (el provider ES el boton) y el knob. `collapse` retirado (su caso murio con la propiedad: medido, 0 huecos en 5 emisiones a 72ms) y `lock` retirado (significaria «un cierre que no se anuncia»), con sus 4 declaraciones muertas. El guard de `queue` PASO EN VERDE SIENDO INCORRECTO: con timers falsos no hay animaciones, asi que awaitExpression volvia al instante. El navegador midio el commit encolado llegando 1,6s tarde. `queue` espera el HOLD, no la expresion. Medir la envolvente no es medir la salida. `renderAttrs` — los attrs que no son partes. Vaciar ACTIVE_DEV_TRACK (palabras y chronos ya no estan excluidos) destapo 23 `data-palabras-*` que ningun morfo declaraba y que palabras.css SI estiliza: aterrizan en el arbol que el usuario escribio, asi que no pueden ser partes. Campo nuevo con la linea afilada —si el consumidor puede componerlo, es una PARTE— y 3 consumidores (palabras, waveform, aura). Declarado en types.ts Y en el esquema sium: la leccion de MorfoElement. Ademas: 5 selectores muertos en el predicado del focus-scope de palabras (barrido de todo el repo: aparecian solo ahi) · RTL-1 real en palabras-chrome (inset logico + translate fisico = doble volteo; rtl:check 1->0) · el union inline al `Direction` canonico, que destapo que el censo de direccion grepea el NOMBRE del tipo · deuda de chronos (scope, barrel, README). Y de la cola: S-36 (barrel 68->71 + guard), S-31 (vibrate(NaN)), S5 (la preferencia de reduced-motion del usuario ya gana a los channels del morfo), SO2 (el warn que el JSDoc prometia), SO4 (validar el target antes del prewrite). Todos los guards nuevos vistos fallar antes de arreglar nada. Base: check 74 (= base) · sema+morfo+contracts 454 verdes / 6 ajenos preexistentes · soma navegador 1258/1259 (el rojo es un timeout ajeno, reproducido con los cambios en stash) · docs:check 0/618 · rtl:check 0/177. Handoff: docs/process/CONTINUE-perceptual-surface.md Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2 months ago
```
**Where the line is drawn, and it is sharp: if a consumer can compose it or
address it, it is a PART.** `renderAttrs` is for what the component renders
internally and the consumer never composes. It buys no runtime writer — the
component emits them itself, as it always did; what it buys is that the
contract is declared, greppable and guarded (`contracts.test.ts` reads this
file, so an attr that is neither a part nor declared here fails).
Added 2026-08-10, when lifting `palabras`' catalogue exemption exposed 23
undeclared ones. Like `MorfoElement`, the vocabulary lives in TWO places —
the TypeScript shape in [`types.ts`](../../src/uix/morfo/types.ts) and the sium
schema in [`schema.ts`](../../src/uix/morfo/schema.ts). Touch both in one edit.
---
## Archetypes — cross-component part classification
Beyond the component-specific `kebab`, a part can declare an `archetype`
that tags it as part of a cross-component category. This is what lets
**eidos** style "all triggers" or "all overlays" with a single transversal
selector instead of enumerating every component.
```ts
{
name: 'Trigger',
kebab: 'trigger',
archetype: 'trigger', // ← cross-component category
role: 'button',
// ...
}
```
The runtime emits `data-archetype="..."` on the part's DOM element via
`partProps`. Static identity (never mutates), so it ships through
`partProps`, not through `dom.apply`.
### Vocabulary
The canonical inventory is the `ARCHETYPE_VOCABULARY` const in
[`types.ts`](../../src/uix/morfo/types.ts) — the single source; do not copy the list into
prose (a copied list here survived at 24 entries while the code grew to
feat(docs): S1 — generated canonical-vocabulary appendix (STUMBLES #1) An agent building a component from the docs-book alone could not assign archetypes / holds / haptic kinds: the closed sets live only in code consts and the corpus (correctly) forbids copying them into prose, so they were invisible from the docs. STUMBLES #1, the top stumble. Fix: docs/canon/vocabularies.md is GENERATED from the consts by 'npm run docs:vocabularies' — the ONE sanctioned place the lists are spelled out (generated = no drift objection), which every other doc links to. It covers part archetypes (with a one-line role each), sema families with their default hold/persistence + per-intent overrides, verbs by family, intents, the perceptual-duration scale, haptic kinds, the 33 palette scales, sizes, variant archetypes, and the shared common.* strings. - scripts/docs-vocabularies.ts imports the consts (robust — no fragile JSDoc/regex parsing) and formats them; the generation is an exported function so docs-check can compare. - Added ARCHETYPE_DESCRIPTIONS to morfo/types.ts (co-located with ARCHETYPE_VOCABULARY, satisfies Record<MorfoArchetype,string>) as the authoritative one-liners the appendix reads — descriptions become first-class data instead of inline union JSDoc a tool would have to parse. - docs-check gains I7: it regenerates in-memory and fails if the committed file drifts from the consts (negative-tested: corrupt -> error, regen -> green). The appendix's own counts pass I1, so I1 doubles as a second guard. - Linked from README (E2 canon row + two 'I want to' shortcuts), CANON.md (doctrine here, enumerated lists there), architecture/morfo.md (pick a part's archetype), and the component-audit minimum-package list (STUMBLES #8). docs:check 0 errors; npm run check unchanged in my files. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
3 months ago
26). The **generated list with a one-line role for each** archetype lives at
[`canon/vocabularies.md`](../canon/vocabularies.md) (from
`ARCHETYPE_DESCRIPTIONS`) — read it to pick a part's archetype. Validated by
the sium schema. Optional field — omit when a part is genuinely unique to its
component (`Slider.Range`, `PinInput.Segment` internals).
### Provider as trigger vs container
When `Provider` IS the interactive element (Toggle, Switch, Checkbox —
where `defaultElement: 'button'` and the user clicks the Provider itself),
its archetype is `'trigger'`. When `Provider` is just a root container
(`defaultElement: 'div'/'section'/'nav'`), archetype is `'provider'`.
Decide per-morfo.
### Adding a new archetype
Only add when at least two existing components share the role with the
same conceptual meaning. The vocabulary stays small on purpose. If only
one component has it, leave the part without an archetype.
---
## The 2-of-3 rule for extending morfo
Morfo is the cross-layer contract between **soma**, **sema**, and **eidos**
— not a convenience repository for soma. An extension to morfo is justified
only when **at least two of the three layers** consume it.
feat(sema)!: la señal precede al commit — emit() devuelve {id, settled} síncrono, el hold no bloquea a nadie, y el motor enacta la reducción 'state' (D-full + sema §3.9) D-full. `EngineSemantic.emit(signal)` deja de ser `Promise<string>` que resolvía tras el hold y pasa a devolver SÍNCRONAMENTE `EmitHandle { id, settled }`: el id se acuña sin await, la proyección sigue siendo síncrona en `replace`, y `settled` cubre cola + proyección + despacho + hold + expresión + limpieza (la limpieza ocurre ANTES de resolver). El runtime de soma, en `pre`/`coincident`, proyecta y despacha ANTES del handler en el mismo tick (ya no espera el hold); en `post`, handler → tick() → emit. `TriggerResult.settled` siempre presente. La a11y del runtime (announce / foco) corre tras el despacho, no tras el hold. El modelo es el de la plataforma (`element.animate()` → `Animation.finished`): el estado cambia en el instante de la acción y la expresión corre en su propio reloj sobre un nodo que retiene Presence. `pre`/`post`/`coincident` describen el ORDEN entre proyección y mutación, nunca una espera; `coincident` pasa a ser literalmente lo que declara. Medido en Chrome real (servidor propio, misma sonda antes/después; Escape → `data-state=closed`): dialog 258.6 → 9.1 ms · drawer 286.6 → 9.1 · popover 1757.8 → 8.5 · float-panel 1807.8 → 49.7 (n=1, pane oculto). Toggle con `regime: 'queue'` conserva su forma (contact 20 · estado 28 · commit encolado 156). En los cuatro el sello `emerge-close` se proyecta ANTES del cambio de estado y `data-ending-style` aparece (Presence). El «hasta 1,5 s» del informe de auditoría era real: el tope de `awaitExpression` se pagaba ante cualquier animación viva en el target (popover, float-panel). Condición 1 (censo, fase 0): 260 eventos; 39 `pre` + 21 `coincident` (0 sin `sequence`). Solo 14 tienen handler registrado (los únicos donde D-full adelanta la mutación; cifra del adversarial, el constructor contó 12); los otros 46 ya mutaban el estado en el mismo tick que un `void trigger` (sidebar.css:363-375 y tooltip-provider:272 lo tenían escrito con medición). Los 6 que desmontan (4 overlays + toast + tooltip) tienen Presence. CERO migraciones necesarias. La regla transversal `[data-event^='emerge-close'] { animation: dismiss-fade }` (base.css) existe en LETRA sobre los cuatro overlays, pero el preset de Presence gana la cascada en todo instante (medido con getComputedStyle): el refutador tenía razón en EFECTO. Condición 2 (carrera nueva del id síncrono): `clear(id)` / `clearTarget` honran ocurrencias EN VUELO (`inFlight` + `pendingClears`): la ocurrencia no se registra como persistente al terminar y limpia como transitoria; `clear` devuelve true. Reproducida en test (hold corriendo ⇒ tras `settled`, sin proyección residual y `active` vacío). `dispose()` vacía ambas. SO4 re-firmado: un target ausente NUNCA salta el handler ni lanza en ninguna secuencia — se omite prewrite+emit, se reporta por logger con `SomaRuntimeTargetError` como payload, y el handler corre («sema es ornamental; una acción del usuario nunca se pierde por un adorno»). sema §3.9. La política perceptual `'state'` bajo reduce pasa al MOTOR: `ReducedMotionFallback` vive en sema (morfo lo importa), la señal lleva `a11y.reducedMotionFallback` (cualquier valor declarado) y el motor, con `motion.get() === 'reduce'` y `'state'`, devuelve el mismo retorno silenciado que `channels: []` (antes de `queue`). Soma deja de forzar `channels: []` (`a11yChannelsOverride` muere) y conserva `'text'`/`'focus'` con `sources.motion`. S5 se conserva como doctrina (la preferencia gana al morfo y a la llamada); cambia el enactor. Cierra el emit DIRECTO sin a11y (~76 en el docs site). Dato: hoy ningún morfo declara `'state'` (9 declaran `'text'`); el valor es contrato y simetría con la háptica, no un síntoma vivo. Prosa re-firmada (CANON intacto, silente a propósito): engine.ts docblock · sema.md (emit contract, promise semantics, lifecycle, One door, overlays, error policy, reductions, migration) · morfo.md · active-architecture.md (walkthrough) · overview.md · soma-architecture.md · chans/types.ts req. 3 · sema/README · book-deviations D.9/D.10/D.12 · morfo/types.ts (MorfoEventSequence, MorfoA11ySemantic) · comentarios del `sequence: 'post'` en dialog/drawer/popover/ float-panel (valor intacto). Verificación (tras la ronda 2): sema 337/337 · runtime 63/63 · alcance del lote 201 ficheros / 2136 tests · suite entera 5366/5366 (458 ficheros, exit 0) · check 89 errores, 0 en src/, ledger intacto (los ~76 emit de web/routes son fire-and-forget y compilan con el handle) · gate VERDE tras la ronda 1 (5359/5359; un flake de gesture.test rotate-detent bajo carga, verde en solitario y dentro del gate) · +20 tests (engine 40→53, runtime 56→63) · 3 mutaciones enrojecen y revertidas byte-idénticas (⚠ diferir la limpieza UN microtask es un no-op observable: la mutación honesta cruza a macrotask) · adversarial Opus independiente (métodos propios, árbol byte-idéntico al terminar): doctrina SANA; 8 defectos, cerrados antes de anclar los 6 que eran del lote — D-1 código (el catch de `settled` vivía en la rama onFulfilled del trigger: un handler que lanzaba en `pre` dejaba la rechazada del canal visual sin manejar; ahora se engancha donde nace el handle, con test de las tres vías) · D-2/D-3 prosa · D-4/D-5 tests (los cuatro valores del fallback, y el par S5 vuelve a discriminar) · D-8 exactitud (`clear`/`clearTarget` no honran ni cuentan transitorias en vuelo). Segunda sonda de Chrome del adversarial: 11.6 · 11.0 · 5.1 · 42.7 ms. D-6 (alcance): `contracts.test.ts` restaurado a HEAD (su diff era 100 % formateo); el reformateo prettier de los docs tocados se queda. D-7 PREEXISTENTE, fila nombrada: `dispose()` durante una ocurrencia en vuelo repuebla `active` después y deja proyección residual (engine.ts, `finally` + `dispose`). Pendiente del autor: mirar el cierre de Dialog y Toast en un Chrome que PINTE (los panes de los agentes estaban ocultos: mutaciones y estilo computado medidos, frame no visto). Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
3 weeks ago
| What | Soma | Sema | Eidos | In morfo? |
| --------------------------------------------- | ----------------------------------------- | ------------------ | -------------------------- | ------------------------------------------------------------------------------------------------------------------------- |
| `parts[].data` + `aria` + `role` | ✅ | — | ✅ | ✅ |
| `parts[].archetype` | ✅ (emit) | ✅ (verbs by role) | ✅ (transversal selectors) | ✅ |
| `parts[].keyboard` | ✅ (dispatch) | — | — | ✅ (was already there) |
| `focus` | ✅ (FocusScope policy + RovingFocusGroup) | — | — | ✅ (author-signed axis 2026-08-26: executor + `focus-census` + docs render — a stronger guard than the grandfather class) |
| `events[].semantic` family/intent | ✅ (signal payload) | ✅ (vocabulary) | ✅ (selector tinting) | ✅ |
| `events[].prewrite` (e.g. `data-last-action`) | ✅ (apply) | ✅ (sequence) | ✅ (tint exit anim) | ✅ |
| `data-starting-style` / `data-ending-style` | ✅ (Presence) | — | ✅ (animations) | ✅ |
| Computed state from N props | — (provider exposes virtual prop) | — | — | ❌ |
| `firstOf` value source (priority chain) | ✅ only | — | — | ❌ |
| `prop-not-nullish` condition | ✅ only | — | — | ❌ |
| Field-context OR'ing | — (provider, virtual prop) | — | — | ❌ |
Soma-only conveniences live in the provider — typically as a "virtual
prop" that the provider exposes via runtime `props` sources, then morfo
reads with `propRef`. The line stays clean: morfo declares structure +
contracts; provider decides logic.
### Disclosure events
Disclosure-style components (`Collapsible`, accordion item panels, row-detail
reveals) are non-evaluative. They should not declare `intent`, color subset or
size semantics just because they animate. The canonical morfo shape is two
directional `emerge` events:
```ts
{
feat(morfo,sema,eidos): todo evento dice de que familia es, y el cruce por fin se ve El framework llamaba a la misma cosa de dos maneras: `open` pelado en ocho componentes y `emerge-open` en tres. No era estetica — un preset de movimiento engancha el nombre con `^=`, asi que el dialecto pelado no casaba con ninguna firma y simplemente no animaba, sin romper una sola prueba. Los 40 nombres sin prefijo pasan a `{familia}-{verbo}[-{matiz}]`: 256 eventos, 256 con prefijo, 0 ambiguos. El plan decia 36 y decia `handle-drag-start`; eran 40, y el canon (c25) dice que esos verbos son `pick` y `drop` — `handle-pick` y `handle-drop` ya existian en 10 y 4 componentes. `validateMorfo` cierra la puerta: un `events[].name` que no empiece por su familia ahora lanza. Visto fallar antes con un nombre pelado inyectado. Lo que el renombrado destapo, y va aqui tambien: - La receta del splitter enganchaba `commit-resize`, muerto desde `bd2e40366`. No casaba desde mayo y nadie chillo. Reescrita por FAMILIA, como slider y knob, y `eidos-lint` valida ahora el VALOR de `data-event*` contra el catalogo de morfos — el guard que lo habria cazado en su dia. - La familia `shift` era muda en el canal visual, contra su propia doctrina (c27: el cruce debe percibirse; c34 tipifica el «shift invisible»). Su mapa ya describia la firma que le faltaba y su sonido por defecto es `slide`. Ahora tiene firma direccional: sexto atributo del sello (`data-event-direction`, `forward`|`backward`, por emision) y deslizamiento de 320ms RTL-safe por `:dir()`. Medido: LTR -30px/+30px, RTL los invierte. - El sello de `shift-navigate` pasa del BOTON al `grid` en los cuatro calendarios. Medido: el boton recibia `contact-activate` y 8,5 ms despues —media trama— el `shift-navigate` pisaba la misma ranura y el `press-squeeze` moria sin pintar un fotograma. Una superficie, una ranura (A-36). - 101 contradicciones docs<->morfo adjudicadas con evidencia (git log, docs de decision, el componente vivo). Las docs desfasadas, corregidas; los nueve DEFECTOS de codigo obsoleto quedan abiertos y sin tocar. - `SoundDirection` -> `SoundContour`: era un contorno de tono, no un sentido, y habia tres cosas distintas deletreadas «direction». check en su linea base con 0 errores nuevos por diferencia de conjuntos · docs:check 0/0 · eidos-lint invalid 0 · el censo y las escenas de navegador medidas con raton real y rAF vivo. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2 months ago
name: 'emerge-expand',
semantic: { family: 'emerge', verb: 'expand', target: v.partRef('content'), sequence: 'post' }
},
{
feat(morfo,sema,eidos): todo evento dice de que familia es, y el cruce por fin se ve El framework llamaba a la misma cosa de dos maneras: `open` pelado en ocho componentes y `emerge-open` en tres. No era estetica — un preset de movimiento engancha el nombre con `^=`, asi que el dialecto pelado no casaba con ninguna firma y simplemente no animaba, sin romper una sola prueba. Los 40 nombres sin prefijo pasan a `{familia}-{verbo}[-{matiz}]`: 256 eventos, 256 con prefijo, 0 ambiguos. El plan decia 36 y decia `handle-drag-start`; eran 40, y el canon (c25) dice que esos verbos son `pick` y `drop` — `handle-pick` y `handle-drop` ya existian en 10 y 4 componentes. `validateMorfo` cierra la puerta: un `events[].name` que no empiece por su familia ahora lanza. Visto fallar antes con un nombre pelado inyectado. Lo que el renombrado destapo, y va aqui tambien: - La receta del splitter enganchaba `commit-resize`, muerto desde `bd2e40366`. No casaba desde mayo y nadie chillo. Reescrita por FAMILIA, como slider y knob, y `eidos-lint` valida ahora el VALOR de `data-event*` contra el catalogo de morfos — el guard que lo habria cazado en su dia. - La familia `shift` era muda en el canal visual, contra su propia doctrina (c27: el cruce debe percibirse; c34 tipifica el «shift invisible»). Su mapa ya describia la firma que le faltaba y su sonido por defecto es `slide`. Ahora tiene firma direccional: sexto atributo del sello (`data-event-direction`, `forward`|`backward`, por emision) y deslizamiento de 320ms RTL-safe por `:dir()`. Medido: LTR -30px/+30px, RTL los invierte. - El sello de `shift-navigate` pasa del BOTON al `grid` en los cuatro calendarios. Medido: el boton recibia `contact-activate` y 8,5 ms despues —media trama— el `shift-navigate` pisaba la misma ranura y el `press-squeeze` moria sin pintar un fotograma. Una superficie, una ranura (A-36). - 101 contradicciones docs<->morfo adjudicadas con evidencia (git log, docs de decision, el componente vivo). Las docs desfasadas, corregidas; los nueve DEFECTOS de codigo obsoleto quedan abiertos y sin tocar. - `SoundDirection` -> `SoundContour`: era un contorno de tono, no un sentido, y habia tres cosas distintas deletreadas «direction». check en su linea base con 0 errores nuevos por diferencia de conjuntos · docs:check 0/0 · eidos-lint invalid 0 · el censo y las escenas de navegador medidas con raton real y rAF vivo. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2 months ago
name: 'emerge-collapse',
semantic: { family: 'emerge', verb: 'collapse', target: v.partRef('content'), sequence: 'post' }
}
```
feat(morfo,sema,eidos): todo evento dice de que familia es, y el cruce por fin se ve El framework llamaba a la misma cosa de dos maneras: `open` pelado en ocho componentes y `emerge-open` en tres. No era estetica — un preset de movimiento engancha el nombre con `^=`, asi que el dialecto pelado no casaba con ninguna firma y simplemente no animaba, sin romper una sola prueba. Los 40 nombres sin prefijo pasan a `{familia}-{verbo}[-{matiz}]`: 256 eventos, 256 con prefijo, 0 ambiguos. El plan decia 36 y decia `handle-drag-start`; eran 40, y el canon (c25) dice que esos verbos son `pick` y `drop` — `handle-pick` y `handle-drop` ya existian en 10 y 4 componentes. `validateMorfo` cierra la puerta: un `events[].name` que no empiece por su familia ahora lanza. Visto fallar antes con un nombre pelado inyectado. Lo que el renombrado destapo, y va aqui tambien: - La receta del splitter enganchaba `commit-resize`, muerto desde `bd2e40366`. No casaba desde mayo y nadie chillo. Reescrita por FAMILIA, como slider y knob, y `eidos-lint` valida ahora el VALOR de `data-event*` contra el catalogo de morfos — el guard que lo habria cazado en su dia. - La familia `shift` era muda en el canal visual, contra su propia doctrina (c27: el cruce debe percibirse; c34 tipifica el «shift invisible»). Su mapa ya describia la firma que le faltaba y su sonido por defecto es `slide`. Ahora tiene firma direccional: sexto atributo del sello (`data-event-direction`, `forward`|`backward`, por emision) y deslizamiento de 320ms RTL-safe por `:dir()`. Medido: LTR -30px/+30px, RTL los invierte. - El sello de `shift-navigate` pasa del BOTON al `grid` en los cuatro calendarios. Medido: el boton recibia `contact-activate` y 8,5 ms despues —media trama— el `shift-navigate` pisaba la misma ranura y el `press-squeeze` moria sin pintar un fotograma. Una superficie, una ranura (A-36). - 101 contradicciones docs<->morfo adjudicadas con evidencia (git log, docs de decision, el componente vivo). Las docs desfasadas, corregidas; los nueve DEFECTOS de codigo obsoleto quedan abiertos y sin tocar. - `SoundDirection` -> `SoundContour`: era un contorno de tono, no un sentido, y habia tres cosas distintas deletreadas «direction». check en su linea base con 0 errores nuevos por diferencia de conjuntos · docs:check 0/0 · eidos-lint invalid 0 · el censo y las escenas de navegador medidas con raton real y rAF vivo. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2 months ago
`emerge-expand` is `post` so Eidos reacts after content exists. `emerge-collapse` is `post`
too (collapsible-NEW-001), so the conceal runs against a content the flip has
not yet hidden. Any visual color,
motion or density response belongs to Eidos recipes, not to the morfo event.
---
## Anatomy of a morfo file
Minimal template:
```ts
// src/uix/morfo/components/dialog.ts
import type { Morfo } from '../types';
import { v } from '../types'; // value builders: v.literal, v.stateRef, v.partRef, v.propRef, v.translationRef, v.commonRef, v.langRef
export const dialogMorfo = {
name: 'Dialog',
kebab: 'dialog',
scope: ['soma'],
apg: 'https://www.w3.org/WAI/ARIA/apg/patterns/dialog/',
focus: {
kind: 'trap',
trap: true,
initial: 'first-focusable',
return: 'trigger',
restore: true
},
texts: {
'content.roledescription': '#?components.dialog.content.roledescription|dialog window'
},
parts: [
{
name: 'Provider',
kebab: 'provider', // special: 'provider' emits data-dialog (no suffix)
kind: 'virtual', // context-only, no DOM
defaultElement: 'none',
optional: false,
data: [],
aria: []
},
{
name: 'Trigger',
kebab: 'trigger',
kind: 'public',
defaultElement: 'button',
role: 'button',
optional: false,
states: ['open', 'closed'],
data: [{ attr: 'data-state', values: ['open', 'closed'] }],
aria: [
{ attr: 'aria-haspopup', value: v.literal('dialog') },
{ attr: 'aria-expanded', value: v.stateRef('open') },
{ attr: 'aria-controls', value: v.partRef('content') }
]
},
{
name: 'Content',
kebab: 'content',
kind: 'public',
defaultElement: 'div',
role: 'dialog',
optional: false,
states: ['open', 'closed'],
data: [
{ attr: 'data-state', values: ['open', 'closed'] },
{
attr: 'data-last-action',
values: ['saved', 'cancelled', 'dismissed', 'failed'],
severity: 'optional'
}
],
aria: [
{
attr: 'aria-roledescription',
value: v.translationRef('content.roledescription', 'dialog window')
},
{
attr: 'aria-labelledby',
value: v.partRef('title'),
condition: { when: 'part-present', part: 'title' },
severity: 'recommended'
}
],
keyboard: [{ key: 'Escape', action: 'close' }]
},
{
name: 'Title',
kebab: 'title',
kind: 'public',
defaultElement: 'div',
role: 'heading',
optional: true,
data: [],
aria: [{ attr: 'aria-level', value: v.propRef('level'), severity: 'recommended' }]
}
]
} as const satisfies Morfo;
```
The `as const satisfies Morfo` pattern is **mandatory**, not cosmetic. It does two things at once:
- **`as const`** preserves the literal types (`kebab: 'dialog'`, not `string`). This is what lets `createAttrs(dialogMorfo)` return `{ provider: 'data-dialog'; trigger: 'data-dialog-trigger'; ... }` with autocomplete and typo detection in every provider that consumes the morfo.
- **`satisfies Morfo`** validates that the object conforms to the `Morfo` interface without widening it. If a field is missing or mistyped, TypeScript reports it at the declaration — same safety as `: Morfo =` annotation, without the type widening.
A morfo annotated `: Morfo =` still works at runtime but yields `createAttrs(...): Record<string, string>` — no autocomplete, `attrs.trigerr` compiles. Every morfo in the codebase use `as const satisfies Morfo`; new morfos must do the same.
---
## Authoring a new morfo
### Step 1 — Create the file
Write `src/uix/morfo/components/{kebab}.ts` exporting a `{camelName}Morfo` const.
**Naming rules**:
- `name`: PascalCase. `"Dialog"`, `"DateRangePicker"`, `"ColorField"`.
- `kebab`: kebab-case. `"dialog"`, `"date-range-picker"`, `"color-field"`. Must match `data-{kebab}` and `createAttrs({component})` in the provider.
- Part `kebab`s must be **unique across the whole morfo** — no nested path namespacing. If a conflict arises, rename (e.g. `item-trigger` instead of `trigger`).
- The orchestrator part uses `kebab: 'provider'` — special-cased to emit `data-{component}` with no suffix. Matches `name: 'Provider'` for naming coherence.
### Step 2 — Declare parts
For each part, decide:
- **`kind`**:
revert: deshacer la auditoría entera — se hizo sin leer la doctrina Revert de los 7 commits de la sesión del 2026-07-29/30: 352ca8bbe style(soma): formato Prettier en el test de gradient-picker 17a1f167b test(soma): chat-list e70397fca test(soma): field-langs y gradient-picker c9b83d025 docs(morfo): qué hace pública una parte + hallazgos retirados feb4a8424 test(soma): los primeros providers que no tenían red f8e35b8fd fix(morfo): el eje intent/color c39170abb fix(uix): la auditoría del sistema `4e785d32b` (stats-band, otra sesión) queda intacto — el revert lo salta. ## Por qué se revierte todo y no una parte La instrucción de partida era «audita el sistema, **para ello previamente lee toda la documentación**». No se leyó. Se auditó primero y se justificó después, y eso contaminó el conjunto, no unos commits concretos: - Tres hallazgos del informe eran FALSOS, todos de la misma forma — heurísticas de una sola vía dadas por hechas sin abrir el código: D-3 `AgentTimersPort` (el puerto sí está satisfecho por el adaptador de `defineActiveAgent`); «5 derivas de scope» (`avatar-group` / `path-trace` / `rotate-align` sí están implementados, co-locados en el directorio del padre); «29 morfos con `kind:'public'` irreal» (medía si existe `<Componente.Parte>` e ignoraba que un primitivo de API plana compone por props y snippets). - `c9b83d025` existe SÓLO para retirar esos hallazgos del commit anterior. - Los 6 tests de provider se montan sobre un `installSomaHarness` que FABRICA cuatro servicios compartidos (`dom`, `langs`, `timers`, `prefs`), violando la primera regla de propiedad de `architecture/active-uix.md`: «Only composition roots create shared services. `morfo`, `soma`, `sema`, `eidos` and components never create `dom`, `langs`, `prefs`, `format`, `clipboard` or equivalents: they receive them from `ActiveUix`.» El arranque real son tres líneas (`createActiveUix` → `setActiveUix` → `Soma.create()`) que estaban escritas. - Y se le pidieron al usuario decisiones cuya respuesta estaba en el corpus (`testing-and-tooling.md` dice que los tests de provider son convención, no guard; la regla 1 zanja el arnés). Triar qué conservar exigía justo el juicio que falló. Se revierte todo, se lee el corpus, y vuelve sólo lo que se pueda sostener citando un documento — o lo que sea defecto MEDIBLE y por tanto no dependa de criterio (el colapso de uniones de props, que hacía compilar `<Avatar color="nonsense">`; que `translations:check` crashease en cada ejecución de su historia). Nada se pierde: los commits siguen en la historia y se recuperan con `cherry-pick`. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2 months ago
- `'public'` when the consumer composes the part (e.g. `<Dialog.Trigger>`).
- `'virtual'` for internal coordinators that have no DOM of their own (context-only providers, focus guards). Use with `defaultElement: 'none'`.
- **`defaultElement`**: the HTML element the wrapper renders by default. Advisory — the consumer can override via `child` snippet. See the `MorfoElement` union in [`types.ts`](../../src/uix/morfo/types.ts).
- **`role`**: always-emitted ARIA role. Declare this even when the element has an implicit role (e.g. `<button>` has `role=button`) — this makes the contract polymorphism-safe: if the consumer uses `<div>` via `child`, the role still applies.
- **`optional`**: `true` if the part may be absent from a valid composition (Title, Description, Close, Overlay, Indicator, Separator). `false` for required parts (root + core).
- **`states`**: only declare if the part carries a `data-state` enum. List the exact values (e.g. `['open', 'closed']`). Required for `stateRef` ARIA values to validate.
- **`supportsNesting`**: `true` if this part can nest inside itself (Dialog inside Dialog, Menu inside Menu). Informational — enables eidos to style nested instances with scoped selectors.
### Step 3 — Declare `data` entries
For each data-attr the provider emits:
```ts
data: [
// Enum-valued: the complete set.
{ attr: 'data-state', values: ['open', 'closed'] },
// Same attr, but now declaring its runtime source too.
{ attr: 'data-state', values: ['open', 'closed'], value: v.stateRef('open') },
// Presence-only flag: emitted only when true, absent otherwise.
{ attr: 'data-disabled', value: v.propRef('disabled'), severity: 'optional' },
// Free value: writes the raw value, not an empty presence attr.
{ attr: 'data-value', value: v.propRef('value'), emit: 'value' },
// Free value written manually by the component, not by SomaRuntime.
{ attr: 'data-form-auto-fields-field', emit: 'value' },
// Conditionally-emitted attr.
{
attr: 'data-starting-style',
severity: 'optional',
condition: { when: 'state-equals', state: 'open', value: 'starting' }
}
];
```
Non-enum `data-*` entries default to `emit: 'presence'`: truthy writes
`data-x=""`, falsy removes the attr. Use `emit: 'value'` only when the attr
must carry a real value, such as `data-value`, `data-min`, `data-max` or a
manual component attr like `data-form-auto-fields-field`. When `value` is
omitted, the attr remains declarative-only: Morfo documents the contract, but
the component/provider is still responsible for writing the value.
**Severity rules**:
- `'required'` (default) — the attr must always be emitted. Missing = error in strict mode.
- `'recommended'` — emitted in most cases. Missing = warning.
- `'optional'` — emitted only when its condition is met (e.g. `data-disabled` only when disabled). Missing ≠ error.
Use `'optional'` for all presence-only flags so morfo-check doesn't flag them as missing when they're legitimately absent.
feat(morfo)!: P1 — split attrs[]/aria[] y vocabulario ARIA cerrado: cada cubo su territorio El cubo aria[] mentía un 13,5 %: 989 entradas medidas — 847 aria-*, 8 role, y 134 impostores (type ×130, tabindex ×2, for, contenteditable). - MorfoPart.attrs[]: cubo nuevo para atributos HTML genéricos, misma forma de entrada y mismo compilado (un solo builder para ambos cubos). - aria[] se CIERRA por tipo: MorfoAriaAttr = 'role' | unión del spec (53 nombres, DERIVADA del array ARIA_ATTR_VOCABULARY — nunca lista paralela, la lección MorfoElement). aria-grabbed/aria-dropeffect deprecados-en-1.2 con acta (verdad embarcada de drag-drop); braille*/description son 1.3 y entran anotados. Un typo muere en el tipo. - Esquema guarda el territorio en AMBAS direcciones: aria[] con no-aria LANZA sugiriendo attrs[]; attrs[] con aria-*/role o data-* LANZA. - Codemod: 134 entradas movidas en 62 morfos (133 bloques). Orden de emisión MEDIDO y conservado: data → attrs → aria (132/133 bloques declaraban el impostor como prefijo contiguo de aria[]); snapshot de los 921 parts × 168 morfos: 920/921 byte-idénticos — la única divergencia (breadcrumb.ellipsis, type tras un aria-label consumerWins) no cambia la bolsa renderizada. - Consumidores de part.aria censados: compile/resolver/schema actualizados; morfo-check pierde un filtro MUERTO (el cubo ya llega cerrado por tipo+schema); landmark-census y tests de la mitad aria sin cambio. - morfo.md documenta los dos cubos. 13 tests nuevos (8 schema territorio + 5 compile paridad). morfo 262/262 · soma entera 1314/1314 · check 71. Nota de árbol compartido: src/uix/morfo/source-census.test.ts (sin trackear) es el eje SourceMap de vicen-e1 y NO viaja aquí (add -u); su censo re-medirá sobre este árbol asentado, orden acordado por canal. Ejecutado por agente Opus 5, sesión coordinando y verificando (guardas de territorio probadas en vivo, suite re-corrida). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
1 month ago
### Step 4 — Declare `attrs` / `aria` entries
Two cubes, ONE shape (`MorfoAttrEntryOf` in `types.ts`) — they differ only in
which attribute names they accept, and each owns its territory (the schema
throws in both directions):
feat(sema)!: la señal precede al commit — emit() devuelve {id, settled} síncrono, el hold no bloquea a nadie, y el motor enacta la reducción 'state' (D-full + sema §3.9) D-full. `EngineSemantic.emit(signal)` deja de ser `Promise<string>` que resolvía tras el hold y pasa a devolver SÍNCRONAMENTE `EmitHandle { id, settled }`: el id se acuña sin await, la proyección sigue siendo síncrona en `replace`, y `settled` cubre cola + proyección + despacho + hold + expresión + limpieza (la limpieza ocurre ANTES de resolver). El runtime de soma, en `pre`/`coincident`, proyecta y despacha ANTES del handler en el mismo tick (ya no espera el hold); en `post`, handler → tick() → emit. `TriggerResult.settled` siempre presente. La a11y del runtime (announce / foco) corre tras el despacho, no tras el hold. El modelo es el de la plataforma (`element.animate()` → `Animation.finished`): el estado cambia en el instante de la acción y la expresión corre en su propio reloj sobre un nodo que retiene Presence. `pre`/`post`/`coincident` describen el ORDEN entre proyección y mutación, nunca una espera; `coincident` pasa a ser literalmente lo que declara. Medido en Chrome real (servidor propio, misma sonda antes/después; Escape → `data-state=closed`): dialog 258.6 → 9.1 ms · drawer 286.6 → 9.1 · popover 1757.8 → 8.5 · float-panel 1807.8 → 49.7 (n=1, pane oculto). Toggle con `regime: 'queue'` conserva su forma (contact 20 · estado 28 · commit encolado 156). En los cuatro el sello `emerge-close` se proyecta ANTES del cambio de estado y `data-ending-style` aparece (Presence). El «hasta 1,5 s» del informe de auditoría era real: el tope de `awaitExpression` se pagaba ante cualquier animación viva en el target (popover, float-panel). Condición 1 (censo, fase 0): 260 eventos; 39 `pre` + 21 `coincident` (0 sin `sequence`). Solo 14 tienen handler registrado (los únicos donde D-full adelanta la mutación; cifra del adversarial, el constructor contó 12); los otros 46 ya mutaban el estado en el mismo tick que un `void trigger` (sidebar.css:363-375 y tooltip-provider:272 lo tenían escrito con medición). Los 6 que desmontan (4 overlays + toast + tooltip) tienen Presence. CERO migraciones necesarias. La regla transversal `[data-event^='emerge-close'] { animation: dismiss-fade }` (base.css) existe en LETRA sobre los cuatro overlays, pero el preset de Presence gana la cascada en todo instante (medido con getComputedStyle): el refutador tenía razón en EFECTO. Condición 2 (carrera nueva del id síncrono): `clear(id)` / `clearTarget` honran ocurrencias EN VUELO (`inFlight` + `pendingClears`): la ocurrencia no se registra como persistente al terminar y limpia como transitoria; `clear` devuelve true. Reproducida en test (hold corriendo ⇒ tras `settled`, sin proyección residual y `active` vacío). `dispose()` vacía ambas. SO4 re-firmado: un target ausente NUNCA salta el handler ni lanza en ninguna secuencia — se omite prewrite+emit, se reporta por logger con `SomaRuntimeTargetError` como payload, y el handler corre («sema es ornamental; una acción del usuario nunca se pierde por un adorno»). sema §3.9. La política perceptual `'state'` bajo reduce pasa al MOTOR: `ReducedMotionFallback` vive en sema (morfo lo importa), la señal lleva `a11y.reducedMotionFallback` (cualquier valor declarado) y el motor, con `motion.get() === 'reduce'` y `'state'`, devuelve el mismo retorno silenciado que `channels: []` (antes de `queue`). Soma deja de forzar `channels: []` (`a11yChannelsOverride` muere) y conserva `'text'`/`'focus'` con `sources.motion`. S5 se conserva como doctrina (la preferencia gana al morfo y a la llamada); cambia el enactor. Cierra el emit DIRECTO sin a11y (~76 en el docs site). Dato: hoy ningún morfo declara `'state'` (9 declaran `'text'`); el valor es contrato y simetría con la háptica, no un síntoma vivo. Prosa re-firmada (CANON intacto, silente a propósito): engine.ts docblock · sema.md (emit contract, promise semantics, lifecycle, One door, overlays, error policy, reductions, migration) · morfo.md · active-architecture.md (walkthrough) · overview.md · soma-architecture.md · chans/types.ts req. 3 · sema/README · book-deviations D.9/D.10/D.12 · morfo/types.ts (MorfoEventSequence, MorfoA11ySemantic) · comentarios del `sequence: 'post'` en dialog/drawer/popover/ float-panel (valor intacto). Verificación (tras la ronda 2): sema 337/337 · runtime 63/63 · alcance del lote 201 ficheros / 2136 tests · suite entera 5366/5366 (458 ficheros, exit 0) · check 89 errores, 0 en src/, ledger intacto (los ~76 emit de web/routes son fire-and-forget y compilan con el handle) · gate VERDE tras la ronda 1 (5359/5359; un flake de gesture.test rotate-detent bajo carga, verde en solitario y dentro del gate) · +20 tests (engine 40→53, runtime 56→63) · 3 mutaciones enrojecen y revertidas byte-idénticas (⚠ diferir la limpieza UN microtask es un no-op observable: la mutación honesta cruza a macrotask) · adversarial Opus independiente (métodos propios, árbol byte-idéntico al terminar): doctrina SANA; 8 defectos, cerrados antes de anclar los 6 que eran del lote — D-1 código (el catch de `settled` vivía en la rama onFulfilled del trigger: un handler que lanzaba en `pre` dejaba la rechazada del canal visual sin manejar; ahora se engancha donde nace el handle, con test de las tres vías) · D-2/D-3 prosa · D-4/D-5 tests (los cuatro valores del fallback, y el par S5 vuelve a discriminar) · D-8 exactitud (`clear`/`clearTarget` no honran ni cuentan transitorias en vuelo). Segunda sonda de Chrome del adversarial: 11.6 · 11.0 · 5.1 · 42.7 ms. D-6 (alcance): `contracts.test.ts` restaurado a HEAD (su diff era 100 % formateo); el reformateo prettier de los docs tocados se queda. D-7 PREEXISTENTE, fila nombrada: `dispose()` durante una ocurrencia en vuelo repuebla `active` después y deja proyección residual (engine.ts, `finally` + `dispose`). Pendiente del autor: mirar el cierre de Dialog y Toast en un Chrome que PINTE (los panes de los agentes estaban ocultos: mutaciones y estilo computado medidos, frame no visto). Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
3 weeks ago
| Cube | Accepts | Example |
| --------- | ------------------------------------------------------------------ | -------------------------------------------- |
| `data[]` | `data-*` (never `data-_*`) | `data-state`, `data-disabled` |
| `attrs[]` | any other HTML attribute | `type`, `tabindex`, `for`, `contenteditable` |
| `aria[]` | `role` + `ARIA_ATTR_VOCABULARY` (the spec's states and properties) | `aria-expanded`, `aria-label` |
feat(morfo)!: P1 — split attrs[]/aria[] y vocabulario ARIA cerrado: cada cubo su territorio El cubo aria[] mentía un 13,5 %: 989 entradas medidas — 847 aria-*, 8 role, y 134 impostores (type ×130, tabindex ×2, for, contenteditable). - MorfoPart.attrs[]: cubo nuevo para atributos HTML genéricos, misma forma de entrada y mismo compilado (un solo builder para ambos cubos). - aria[] se CIERRA por tipo: MorfoAriaAttr = 'role' | unión del spec (53 nombres, DERIVADA del array ARIA_ATTR_VOCABULARY — nunca lista paralela, la lección MorfoElement). aria-grabbed/aria-dropeffect deprecados-en-1.2 con acta (verdad embarcada de drag-drop); braille*/description son 1.3 y entran anotados. Un typo muere en el tipo. - Esquema guarda el territorio en AMBAS direcciones: aria[] con no-aria LANZA sugiriendo attrs[]; attrs[] con aria-*/role o data-* LANZA. - Codemod: 134 entradas movidas en 62 morfos (133 bloques). Orden de emisión MEDIDO y conservado: data → attrs → aria (132/133 bloques declaraban el impostor como prefijo contiguo de aria[]); snapshot de los 921 parts × 168 morfos: 920/921 byte-idénticos — la única divergencia (breadcrumb.ellipsis, type tras un aria-label consumerWins) no cambia la bolsa renderizada. - Consumidores de part.aria censados: compile/resolver/schema actualizados; morfo-check pierde un filtro MUERTO (el cubo ya llega cerrado por tipo+schema); landmark-census y tests de la mitad aria sin cambio. - morfo.md documenta los dos cubos. 13 tests nuevos (8 schema territorio + 5 compile paridad). morfo 262/262 · soma entera 1314/1314 · check 71. Nota de árbol compartido: src/uix/morfo/source-census.test.ts (sin trackear) es el eje SourceMap de vicen-e1 y NO viaja aquí (add -u); su censo re-medirá sobre este árbol asentado, orden acordado por canal. Ejecutado por agente Opus 5, sesión coordinando y verificando (guardas de territorio probadas en vivo, suite re-corrida). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
1 month ago
`aria[]` is CLOSED at the type level (`MorfoAriaAttr` derives from
`ARIA_ATTR_VOCABULARY`), so `aria-lable` does not compile. Two members are
DEPRECATED in ARIA 1.2 and kept on purpose — `aria-grabbed` and
`aria-dropeffect`, embarked truth of `drag-drop`; the vocabulary annotates them
rather than making the declaration lie about the DOM.
`attrs[]` is optional and compiles EXACTLY like `aria[]` — same plans, same
modes, emitted between `data[]` and `aria[]`.
docs(morfo)!: filas 13 y 14 — los TRES estados de una entrada sin fuente, y la regla de alpha deja de vivir solo en prosa FILA 13(c), firmada: una entrada declarada sin `value` tiene tres estados y la diferencia era invisible. (1) con value: la emite la bolsa y la verifican compilador, resolver e instrumento. (2) sin value, required, sin condición: la emite el PROVIDER y morfo-check EXIGE el attr en el elemento — la única forma sin fuente que alguien verifica. (3) sin value con optional o con condición: CONTRATO-ONLY — nadie la emite y nadie la exige; legitima el attr cuando aparece y documenta que el provider puede emitirlo, nada más. El tercer estado se QUEDA porque es la única forma de decir «el provider lo posee Y es condicional» — descartadas (a) exigir siempre (pondría rojo el caso legítimo) y (b) prohibir la condición (quita expresividad honesta). Se escribe donde se lee: el JSDoc del campo, el comentario del pase (d) que prometía más de lo que el código hace, y una tabla en morfo.md. Dato para no optimizar lo minúsculo: en ese pase `severity` es el filtro DOMINANTE y `condition` el marginal. FILA 14(a), firmada: `web/routes/alpha` (TERMINATED) pasa de doctrina en prosa a entrada de .gitignore con su acta. La lección que la motiva es del peer vicen-e1: `git add <directorio>` consulta .gitignore, JAMÁS doctrina — una regla que solo vive en prosa no la hace cumplir nadie, y anoche entró en un commit por eso. De paso deja de ensuciar el status de las tres sesiones. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
1 month ago
In both buckets `value` is OPTIONAL, the symmetry `data[]` always had: an entry
without a source declares that the attr EXISTS and leaves its value to the
provider's bag — the honest shape when the morfo cannot compute the value at
all (`aria-level` ← `depth + 1`). It has three states, and the difference is
invisible unless you know it (author-signed 2026-08-27):
feat(sema)!: la señal precede al commit — emit() devuelve {id, settled} síncrono, el hold no bloquea a nadie, y el motor enacta la reducción 'state' (D-full + sema §3.9) D-full. `EngineSemantic.emit(signal)` deja de ser `Promise<string>` que resolvía tras el hold y pasa a devolver SÍNCRONAMENTE `EmitHandle { id, settled }`: el id se acuña sin await, la proyección sigue siendo síncrona en `replace`, y `settled` cubre cola + proyección + despacho + hold + expresión + limpieza (la limpieza ocurre ANTES de resolver). El runtime de soma, en `pre`/`coincident`, proyecta y despacha ANTES del handler en el mismo tick (ya no espera el hold); en `post`, handler → tick() → emit. `TriggerResult.settled` siempre presente. La a11y del runtime (announce / foco) corre tras el despacho, no tras el hold. El modelo es el de la plataforma (`element.animate()` → `Animation.finished`): el estado cambia en el instante de la acción y la expresión corre en su propio reloj sobre un nodo que retiene Presence. `pre`/`post`/`coincident` describen el ORDEN entre proyección y mutación, nunca una espera; `coincident` pasa a ser literalmente lo que declara. Medido en Chrome real (servidor propio, misma sonda antes/después; Escape → `data-state=closed`): dialog 258.6 → 9.1 ms · drawer 286.6 → 9.1 · popover 1757.8 → 8.5 · float-panel 1807.8 → 49.7 (n=1, pane oculto). Toggle con `regime: 'queue'` conserva su forma (contact 20 · estado 28 · commit encolado 156). En los cuatro el sello `emerge-close` se proyecta ANTES del cambio de estado y `data-ending-style` aparece (Presence). El «hasta 1,5 s» del informe de auditoría era real: el tope de `awaitExpression` se pagaba ante cualquier animación viva en el target (popover, float-panel). Condición 1 (censo, fase 0): 260 eventos; 39 `pre` + 21 `coincident` (0 sin `sequence`). Solo 14 tienen handler registrado (los únicos donde D-full adelanta la mutación; cifra del adversarial, el constructor contó 12); los otros 46 ya mutaban el estado en el mismo tick que un `void trigger` (sidebar.css:363-375 y tooltip-provider:272 lo tenían escrito con medición). Los 6 que desmontan (4 overlays + toast + tooltip) tienen Presence. CERO migraciones necesarias. La regla transversal `[data-event^='emerge-close'] { animation: dismiss-fade }` (base.css) existe en LETRA sobre los cuatro overlays, pero el preset de Presence gana la cascada en todo instante (medido con getComputedStyle): el refutador tenía razón en EFECTO. Condición 2 (carrera nueva del id síncrono): `clear(id)` / `clearTarget` honran ocurrencias EN VUELO (`inFlight` + `pendingClears`): la ocurrencia no se registra como persistente al terminar y limpia como transitoria; `clear` devuelve true. Reproducida en test (hold corriendo ⇒ tras `settled`, sin proyección residual y `active` vacío). `dispose()` vacía ambas. SO4 re-firmado: un target ausente NUNCA salta el handler ni lanza en ninguna secuencia — se omite prewrite+emit, se reporta por logger con `SomaRuntimeTargetError` como payload, y el handler corre («sema es ornamental; una acción del usuario nunca se pierde por un adorno»). sema §3.9. La política perceptual `'state'` bajo reduce pasa al MOTOR: `ReducedMotionFallback` vive en sema (morfo lo importa), la señal lleva `a11y.reducedMotionFallback` (cualquier valor declarado) y el motor, con `motion.get() === 'reduce'` y `'state'`, devuelve el mismo retorno silenciado que `channels: []` (antes de `queue`). Soma deja de forzar `channels: []` (`a11yChannelsOverride` muere) y conserva `'text'`/`'focus'` con `sources.motion`. S5 se conserva como doctrina (la preferencia gana al morfo y a la llamada); cambia el enactor. Cierra el emit DIRECTO sin a11y (~76 en el docs site). Dato: hoy ningún morfo declara `'state'` (9 declaran `'text'`); el valor es contrato y simetría con la háptica, no un síntoma vivo. Prosa re-firmada (CANON intacto, silente a propósito): engine.ts docblock · sema.md (emit contract, promise semantics, lifecycle, One door, overlays, error policy, reductions, migration) · morfo.md · active-architecture.md (walkthrough) · overview.md · soma-architecture.md · chans/types.ts req. 3 · sema/README · book-deviations D.9/D.10/D.12 · morfo/types.ts (MorfoEventSequence, MorfoA11ySemantic) · comentarios del `sequence: 'post'` en dialog/drawer/popover/ float-panel (valor intacto). Verificación (tras la ronda 2): sema 337/337 · runtime 63/63 · alcance del lote 201 ficheros / 2136 tests · suite entera 5366/5366 (458 ficheros, exit 0) · check 89 errores, 0 en src/, ledger intacto (los ~76 emit de web/routes son fire-and-forget y compilan con el handle) · gate VERDE tras la ronda 1 (5359/5359; un flake de gesture.test rotate-detent bajo carga, verde en solitario y dentro del gate) · +20 tests (engine 40→53, runtime 56→63) · 3 mutaciones enrojecen y revertidas byte-idénticas (⚠ diferir la limpieza UN microtask es un no-op observable: la mutación honesta cruza a macrotask) · adversarial Opus independiente (métodos propios, árbol byte-idéntico al terminar): doctrina SANA; 8 defectos, cerrados antes de anclar los 6 que eran del lote — D-1 código (el catch de `settled` vivía en la rama onFulfilled del trigger: un handler que lanzaba en `pre` dejaba la rechazada del canal visual sin manejar; ahora se engancha donde nace el handle, con test de las tres vías) · D-2/D-3 prosa · D-4/D-5 tests (los cuatro valores del fallback, y el par S5 vuelve a discriminar) · D-8 exactitud (`clear`/`clearTarget` no honran ni cuentan transitorias en vuelo). Segunda sonda de Chrome del adversarial: 11.6 · 11.0 · 5.1 · 42.7 ms. D-6 (alcance): `contracts.test.ts` restaurado a HEAD (su diff era 100 % formateo); el reformateo prettier de los docs tocados se queda. D-7 PREEXISTENTE, fila nombrada: `dispose()` durante una ocurrencia en vuelo repuebla `active` después y deja proyección residual (engine.ts, `finally` + `dispose`). Pendiente del autor: mirar el cierre de Dialog y Toast en un Chrome que PINTE (los panes de los agentes estaban ocultos: mutaciones y estilo computado medidos, frame no visto). Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
3 weeks ago
| shape | who emits | who verifies |
| -------------------------------------------- | -------------- | --------------------------------------------- |
| with `value` | the render bag | the compiler, the resolver and `morfo:check` |
| no `value`, `required`, no `condition` | the provider | `morfo:check` DEMANDS the attr on the element |
| no `value`, plus `optional` or a `condition` | the provider | **nobody** — contract-only |
docs(morfo)!: filas 13 y 14 — los TRES estados de una entrada sin fuente, y la regla de alpha deja de vivir solo en prosa FILA 13(c), firmada: una entrada declarada sin `value` tiene tres estados y la diferencia era invisible. (1) con value: la emite la bolsa y la verifican compilador, resolver e instrumento. (2) sin value, required, sin condición: la emite el PROVIDER y morfo-check EXIGE el attr en el elemento — la única forma sin fuente que alguien verifica. (3) sin value con optional o con condición: CONTRATO-ONLY — nadie la emite y nadie la exige; legitima el attr cuando aparece y documenta que el provider puede emitirlo, nada más. El tercer estado se QUEDA porque es la única forma de decir «el provider lo posee Y es condicional» — descartadas (a) exigir siempre (pondría rojo el caso legítimo) y (b) prohibir la condición (quita expresividad honesta). Se escribe donde se lee: el JSDoc del campo, el comentario del pase (d) que prometía más de lo que el código hace, y una tabla en morfo.md. Dato para no optimizar lo minúsculo: en ese pase `severity` es el filtro DOMINANTE y `condition` el marginal. FILA 14(a), firmada: `web/routes/alpha` (TERMINATED) pasa de doctrina en prosa a entrada de .gitignore con su acta. La lección que la motiva es del peer vicen-e1: `git add <directorio>` consulta .gitignore, JAMÁS doctrina — una regla que solo vive en prosa no la hace cumplir nadie, y anoche entró en un commit por eso. De paso deja de ensuciar el status de las tres sesiones. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
1 month ago
The third row is deliberate: it is the only way to declare "the provider owns
this AND it is conditional". Write it knowing no instrument catches its
absence, and prefer the second row whenever the attr is unconditional.
`ariaBoolean` REQUIRES a `value` (schema invariant): asking for boolean
stringification of a value the entry does not declare is incoherent.
feat(morfo)!: P1 — split attrs[]/aria[] y vocabulario ARIA cerrado: cada cubo su territorio El cubo aria[] mentía un 13,5 %: 989 entradas medidas — 847 aria-*, 8 role, y 134 impostores (type ×130, tabindex ×2, for, contenteditable). - MorfoPart.attrs[]: cubo nuevo para atributos HTML genéricos, misma forma de entrada y mismo compilado (un solo builder para ambos cubos). - aria[] se CIERRA por tipo: MorfoAriaAttr = 'role' | unión del spec (53 nombres, DERIVADA del array ARIA_ATTR_VOCABULARY — nunca lista paralela, la lección MorfoElement). aria-grabbed/aria-dropeffect deprecados-en-1.2 con acta (verdad embarcada de drag-drop); braille*/description son 1.3 y entran anotados. Un typo muere en el tipo. - Esquema guarda el territorio en AMBAS direcciones: aria[] con no-aria LANZA sugiriendo attrs[]; attrs[] con aria-*/role o data-* LANZA. - Codemod: 134 entradas movidas en 62 morfos (133 bloques). Orden de emisión MEDIDO y conservado: data → attrs → aria (132/133 bloques declaraban el impostor como prefijo contiguo de aria[]); snapshot de los 921 parts × 168 morfos: 920/921 byte-idénticos — la única divergencia (breadcrumb.ellipsis, type tras un aria-label consumerWins) no cambia la bolsa renderizada. - Consumidores de part.aria censados: compile/resolver/schema actualizados; morfo-check pierde un filtro MUERTO (el cubo ya llega cerrado por tipo+schema); landmark-census y tests de la mitad aria sin cambio. - morfo.md documenta los dos cubos. 13 tests nuevos (8 schema territorio + 5 compile paridad). morfo 262/262 · soma entera 1314/1314 · check 71. Nota de árbol compartido: src/uix/morfo/source-census.test.ts (sin trackear) es el eje SourceMap de vicen-e1 y NO viaja aquí (add -u); su censo re-medirá sobre este árbol asentado, orden acordado por canal. Ejecutado por agente Opus 5, sesión coordinando y verificando (guardas de territorio probadas en vivo, suite re-corrida). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
1 month ago
```ts
attrs: [
{ attr: 'type', value: v.literal('button') },
{ attr: 'for', value: v.partRef('input'), condition: { when: 'part-present', part: 'input' } }
];
```
ARIA values use a tagged union via builders:
```ts
aria: [
{ attr: 'aria-haspopup', value: v.literal('dialog') }, // static literal
{ attr: 'aria-expanded', value: v.stateRef('open') }, // refs states[]
{ attr: 'aria-labelledby', value: v.partRef('title') }, // refs another part's kebab
{ attr: 'aria-level', value: v.propRef('level') }, // refs a consumer prop
{
attr: 'aria-roledescription',
value: v.translationRef('content.roledescription', 'dialog window')
},
{ attr: 'aria-label', value: v.commonRef('buttons.close', 'Close') },
{ attr: 'aria-label', value: v.langRef('app.shell.close', 'Close') }
];
```
**Value kinds**:
- `v.literal(value)` — static string invariant. `aria-haspopup="dialog"`, `aria-modal="true"`.
- `v.stateRef(name)` — the attribute value derives from a named state. Validator requires `name` to be in the containing part's `states[]`.
- `v.partRef(target)` — the attribute value is the id of another part. Validator requires `target` to be an existing kebab in the morfo.
- `v.propRef(prop)` — the attribute value comes from a consumer prop (override, passthrough).
- `v.translationRef(key, fallback?)` — component-relative by default. `v.translationRef('content.roledescription', 'dialog window')` compiles to `#?components.dialog.content.roledescription|dialog window`.
- `v.commonRef(key, fallback?)` — shared UI vocabulary under `common.*`. Use for repeated actions like `close`, `cancel`, `save`, `next`, `previous`. UIX ships default `commonLangs`; integrators can provide their own leaves and `ActiveUix` only fills what is missing.
- `v.langRef(key, fallback?)` — explicit absolute translation path outside the component namespace, e.g. `v.langRef('app.shell.close', 'Close')`.
`translationRef` can also receive a raw absolute idlangref starting with `#?`; the compiler leaves it absolute and only appends the fallback when needed.
feat(morfo,soma): el attr de nombrado es un DEFAULT del contrato, no una imposicion A-85 estaba confirmado desde el ledger de blocks: la demo de site-header escribia `aria-label` en el trigger del cajon y el canon lo pisaba. El censo dio la magnitud real — 132 declaraciones en 59 morfos — y la disposicion escrita (replicar el `prop-truthy` de button en las 132) resulto ser la direccion inversa a la buena: obligaba a 264 entradas de boilerplate y hacia que el texto del consumidor diese una vuelta entera por el grafo reactivo para aterrizar donde ya estaba. Las 132 declaraciones eran CORRECTAS. El defecto era la precedencia. Asi que el contrato gana dos clases, por VOCABULARIO y no por declaracion: contract (role, data-*, aria-expanded, aria-controls, roledescription...) el runtime SIEMPRE gana; un consumidor que los pisara haria mentir al componente, y esa garantia es algo que las librerias user-props-last no tienen. naming (`ARIA_NAMING_ATTRS`, hoy `aria-label`) el morfo da un DEFAULT y el attr explicito del consumidor gana — espejo de la propia cadena de nombre accesible de la plataforma. Tres puntos, uno por capa, y ninguno de los 132 morfos tocado: `compileMorfo` clasifica el plan (`consumerWins`) y deja de hoistear esos literales a staticAttrs; el runtime los saca del efecto `syncAttrs` —el `dom.apply` post-render ERA el pisado— y los embarca en el bag de la parte, el mismo vehiculo que `dir`; y `mergeProps` los resuelve consumer-first (`a ?? b`, para que un `aria-label=""` explicito sobreviva). Efecto medido que no se buscaba: los labels salen ahora en el HTML de SSR. Antes los escribia un efecto, que en servidor no corre — el propio ledger lo tenia medido y nadie habia atado los dos cabos. `aria-labelledby` se queda en contract A PROPOSITO: es cableado `partRef`, y la plataforma ya hace que un labelledby del consumidor gane a cualquier label sin que tengamos que arbitrarlo. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2 months ago
**Two precedence classes (A-85, 2026-08-11).** Every attr the morfo declares is
one of two kinds, and the split is by ATTR NAME, not per-declaration:
- **Contract** — state, wiring and identity (`role`, `data-*`, `aria-expanded`,
`aria-controls`, `aria-roledescription`, …): the runtime always wins. A
consumer overriding these would make the component lie, and that guarantee is
something user-props-last libraries don't have.
- **Naming** (`ARIA_NAMING_ATTRS` in [`types.ts`](../../src/uix/morfo/types.ts)
— today `aria-label`): the morfo's value is a DEFAULT accessible name and the
consumer's explicit attr wins, mirroring the platform's accessible-name
precedence. Declare the default plainly — unconditional `translationRef` is
the canonical shape; no `prop-truthy` dance is needed. The compiler marks
docs(uix): la doctrina alcanza a la bolsa única — el corpus deja de enseñar la API muerta La clase exacta que la auditoría flagueó (doc↔código), producida esta vez por NUESTROS propios cambios de P0 fase C y sin corregir hasta ahora: - component-guide §«two sanctioned ways»: reescrita a UNA vía (la bolsa .props resuelve el contrato entero; extras solo lo que el morfo no puede expresar; valores CRUDOS; la inversa «morfo sin value ⇒ soma escribe»). Los 4 snippets con syncAttrs: true, limpiados. - soma-architecture: partProps re-descrito como LA bolsa (identidad + contrato completo, SSR incluido); el párrafo del flag sustituido por «el único escritor imperativo es el prewrite»; snippet limpiado. - soma.md: snippet de accordion sin el flag. - morfo.md §naming: el «(SSR included)» dejó de ser contraste — desde la tubería única TODO plan viaja en la bolsa; lo que distingue a los naming es la PRECEDENCIA (consumer-first via mergeProps). - testing-and-tooling: la postura SSR gana el canario ssr-contract.test, y nace «The gate» (check:gate + ledger menguante, gate, hook — los instrumentos de fase B no estaban documentados). - CLAUDE.md: el paso 4 de trigger ya no dice «effect-driven» (prettier normalizó de paso el fichero entero — solo whitespace, contenido intacto, declarado aquí para que el diff ancho no sea silencioso). Lo histórico (changelog, old-deprecated, audit de julio, process/) se queda como historia. Verificación: docs:check 0 errores sobre 817 docs; cero menciones normativas de la API muerta fuera de referencias históricas. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
1 month ago
these plans `consumerWins`; like every plan since the render bag became the
single attr pipeline (P0 fase C, audit 2026-08-26) they ship in the bag —
server-rendered included — and what distinguishes them is precedence:
`mergeProps` resolves them consumer-first, while contract plans stay
runtime-authoritative.
feat(morfo,soma): el attr de nombrado es un DEFAULT del contrato, no una imposicion A-85 estaba confirmado desde el ledger de blocks: la demo de site-header escribia `aria-label` en el trigger del cajon y el canon lo pisaba. El censo dio la magnitud real — 132 declaraciones en 59 morfos — y la disposicion escrita (replicar el `prop-truthy` de button en las 132) resulto ser la direccion inversa a la buena: obligaba a 264 entradas de boilerplate y hacia que el texto del consumidor diese una vuelta entera por el grafo reactivo para aterrizar donde ya estaba. Las 132 declaraciones eran CORRECTAS. El defecto era la precedencia. Asi que el contrato gana dos clases, por VOCABULARIO y no por declaracion: contract (role, data-*, aria-expanded, aria-controls, roledescription...) el runtime SIEMPRE gana; un consumidor que los pisara haria mentir al componente, y esa garantia es algo que las librerias user-props-last no tienen. naming (`ARIA_NAMING_ATTRS`, hoy `aria-label`) el morfo da un DEFAULT y el attr explicito del consumidor gana — espejo de la propia cadena de nombre accesible de la plataforma. Tres puntos, uno por capa, y ninguno de los 132 morfos tocado: `compileMorfo` clasifica el plan (`consumerWins`) y deja de hoistear esos literales a staticAttrs; el runtime los saca del efecto `syncAttrs` —el `dom.apply` post-render ERA el pisado— y los embarca en el bag de la parte, el mismo vehiculo que `dir`; y `mergeProps` los resuelve consumer-first (`a ?? b`, para que un `aria-label=""` explicito sobreviva). Efecto medido que no se buscaba: los labels salen ahora en el HTML de SSR. Antes los escribia un efecto, que en servidor no corre — el propio ledger lo tenia medido y nadie habia atado los dos cabos. `aria-labelledby` se queda en contract A PROPOSITO: es cableado `partRef`, y la plataforma ya hace que un labelledby del consumidor gane a cualquier label sin que tengamos que arbitrarlo. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2 months ago
132 shipped declarations were clobbering the consumer's label before the
runtime learnt this split — the declarations were right, the precedence was
wrong. (`aria-labelledby` stays contract on purpose: it is `partRef` wiring,
and the platform's name computation already lets a consumer-supplied
`labelledby` beat any `label` without our arbitration.)
**The default belongs HERE, not in a provider resolver.** The anti-pattern the
two-class split retires is `value: v.propRef('ariaLabel')` paired with a
provider chain that supplies the real default:
```ts
// ❌ the routing dance: the morfo claims "whatever the consumer passed" while
// the emission is actually a framework label the contract never names.
{ attr: 'aria-label', value: v.propRef('ariaLabel'), condition: { when: 'prop-truthy', prop: 'ariaLabel' } }
// provider: opts.ariaLabel.current || soma.langs.ts(XXX_LANGS.LABEL)
// wrapper: destructures `aria-label` out of restProps to feed that chain
// ✅ morfo-first: the contract names the default; the consumer's attr stays in
// restProps and wins by merge policy. No wrapper wiring, no provider chain.
{ attr: 'aria-label', value: v.translationRef('#?components.meter.label|Meter'), severity: 'recommended' }
```
A label that must disappear when the consumer names the part by reference
declares it — `condition: { when: 'prop-falsy', prop: 'ariaLabelledby' }` —
which requires the provider to publish `ariaLabelledby` as a source. A part
with no framework default declares NO entry at all (Button): there is nothing
to default to, and the consumer's attr flows through untouched.
⚠️ **Four kinds of label CANNOT move here** — measured across the 2026-08-11
sweep of the whole catalogue. A provider chain is not automatically a dance;
check against these before deleting one:
docs(uix): la doctrina alcanza a la bolsa única — el corpus deja de enseñar la API muerta La clase exacta que la auditoría flagueó (doc↔código), producida esta vez por NUESTROS propios cambios de P0 fase C y sin corregir hasta ahora: - component-guide §«two sanctioned ways»: reescrita a UNA vía (la bolsa .props resuelve el contrato entero; extras solo lo que el morfo no puede expresar; valores CRUDOS; la inversa «morfo sin value ⇒ soma escribe»). Los 4 snippets con syncAttrs: true, limpiados. - soma-architecture: partProps re-descrito como LA bolsa (identidad + contrato completo, SSR incluido); el párrafo del flag sustituido por «el único escritor imperativo es el prewrite»; snippet limpiado. - soma.md: snippet de accordion sin el flag. - morfo.md §naming: el «(SSR included)» dejó de ser contraste — desde la tubería única TODO plan viaja en la bolsa; lo que distingue a los naming es la PRECEDENCIA (consumer-first via mergeProps). - testing-and-tooling: la postura SSR gana el canario ssr-contract.test, y nace «The gate» (check:gate + ledger menguante, gate, hook — los instrumentos de fase B no estaban documentados). - CLAUDE.md: el paso 4 de trigger ya no dice «effect-driven» (prettier normalizó de paso el fichero entero — solo whitespace, contenido intacto, declarado aquí para que el diff ancho no sea silencioso). Lo histórico (changelog, old-deprecated, audit de julio, process/) se queda como historia. Verificación: docs:check 0 errores sobre 817 docs; cero menciones normativas de la API muerta fuera de referencias históricas. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
1 month ago
| Cannot move | Why | Examples |
| ------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------- |
| **state-dependent** | `mapRef` returns `map[String(raw)]` verbatim and nothing translates that output, so a mapped idlangref would ship as a literal `#?…` | clipboard copy/copied · a visibility toggle's show/hide · a row's expand/collapse |
| **interpolated** | `translationRef` names a key; it takes no params, so a name built from data has nowhere to put them | `Slide 3 of 8` · `Remove {tag}` · a tree node's per-node name |
| **cross-element** | the consumer's prop is declared on a ROOT wrapper that renders a role-less `<div>`, but the name belongs to a CHILD control. Letting it ride restProps would name the `generic` div — which ARIA forbids — and silently leave the real control on its default | textarea → its `<textarea>` · mask-field / search-field / password-field roots → their `<input>` · waveform → the composed Slider |
feat(morfo,soma): el attr de nombrado es un DEFAULT del contrato, no una imposicion A-85 estaba confirmado desde el ledger de blocks: la demo de site-header escribia `aria-label` en el trigger del cajon y el canon lo pisaba. El censo dio la magnitud real — 132 declaraciones en 59 morfos — y la disposicion escrita (replicar el `prop-truthy` de button en las 132) resulto ser la direccion inversa a la buena: obligaba a 264 entradas de boilerplate y hacia que el texto del consumidor diese una vuelta entera por el grafo reactivo para aterrizar donde ya estaba. Las 132 declaraciones eran CORRECTAS. El defecto era la precedencia. Asi que el contrato gana dos clases, por VOCABULARIO y no por declaracion: contract (role, data-*, aria-expanded, aria-controls, roledescription...) el runtime SIEMPRE gana; un consumidor que los pisara haria mentir al componente, y esa garantia es algo que las librerias user-props-last no tienen. naming (`ARIA_NAMING_ATTRS`, hoy `aria-label`) el morfo da un DEFAULT y el attr explicito del consumidor gana — espejo de la propia cadena de nombre accesible de la plataforma. Tres puntos, uno por capa, y ninguno de los 132 morfos tocado: `compileMorfo` clasifica el plan (`consumerWins`) y deja de hoistear esos literales a staticAttrs; el runtime los saca del efecto `syncAttrs` —el `dom.apply` post-render ERA el pisado— y los embarca en el bag de la parte, el mismo vehiculo que `dir`; y `mergeProps` los resuelve consumer-first (`a ?? b`, para que un `aria-label=""` explicito sobreviva). Efecto medido que no se buscaba: los labels salen ahora en el HTML de SSR. Antes los escribia un efecto, que en servidor no corre — el propio ledger lo tenia medido y nadie habia atado los dos cabos. `aria-labelledby` se queda en contract A PROPOSITO: es cableado `partRef`, y la plataforma ya hace que un labelledby del consumidor gane a cualquier label sin que tengamos que arbitrarlo. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2 months ago
The first two are blocked by the value system: leave those chains alone, they
are doing real work.
**Cross-element is NOT blocked — it is declared on the child part.** The name
belongs to the control, so the morfo's naming default belongs there too; only
the consumer's ROOT convenience prop still needs forwarding, and the provider
forwards it with a CONDITIONAL spread:
```ts
// morfo: the default sits on the part that owns the name
{ attr: 'aria-label', value: v.translationRef('#?components.textarea.label|Text area'),
condition: { when: 'prop-falsy', prop: 'fieldLabelled' } }
// provider: forward the root's convenience prop — conditionally.
...(p.opts.ariaLabel.current ? { 'aria-label': p.opts.ariaLabel.current } : {})
```
⚠️ **The spread MUST be conditional.** Writing `'aria-label': p.opts.ariaLabel.current`
unconditionally sets the key to `undefined` when the prop is absent, and that
ERASES the morfo default the bag just supplied — an unconditional key beats an
absent one in a spread. Conditional, the default stands when the prop is absent
and the prop wins when present, which is exactly the A-85 precedence.
**A `Field.Label` is the other half of that class.** It names the control
through `for`/`id`, and `aria-label` WINS over `<label for>` in the
accessible-name computation — so a generic default emitted next to a real
visible label would silently override it. That suppression used to hide in a
provider resolver; it is now declared, reading a virtual prop the provider
publishes (`fieldLabelled: () => Boolean(this.field?.labelId.current)` — the
2-of-3 rule: computed state lives in the provider, the morfo only reads it).
Result: with a `Field.Label` present the attr is genuinely ABSENT rather than
present-and-undefined, so assert it with `not.toHaveProperty('aria-label')`.
**First ask whether the element is YOURS.** The default moves to the morfo only
when the naming element is a part of THIS component. When the name lands on a
docs(uix): la doctrina alcanza a la bolsa única — el corpus deja de enseñar la API muerta La clase exacta que la auditoría flagueó (doc↔código), producida esta vez por NUESTROS propios cambios de P0 fase C y sin corregir hasta ahora: - component-guide §«two sanctioned ways»: reescrita a UNA vía (la bolsa .props resuelve el contrato entero; extras solo lo que el morfo no puede expresar; valores CRUDOS; la inversa «morfo sin value ⇒ soma escribe»). Los 4 snippets con syncAttrs: true, limpiados. - soma-architecture: partProps re-descrito como LA bolsa (identidad + contrato completo, SSR incluido); el párrafo del flag sustituido por «el único escritor imperativo es el prewrite»; snippet limpiado. - soma.md: snippet de accordion sin el flag. - morfo.md §naming: el «(SSR included)» dejó de ser contraste — desde la tubería única TODO plan viaja en la bolsa; lo que distingue a los naming es la PRECEDENCIA (consumer-first via mergeProps). - testing-and-tooling: la postura SSR gana el canario ssr-contract.test, y nace «The gate» (check:gate + ledger menguante, gate, hook — los instrumentos de fase B no estaban documentados). - CLAUDE.md: el paso 4 de trigger ya no dice «effect-driven» (prettier normalizó de paso el fichero entero — solo whitespace, contenido intacto, declarado aquí para que el diff ancho no sea silencioso). Lo histórico (changelog, old-deprecated, audit de julio, process/) se queda como historia. Verificación: docs:check 0 errores sobre 817 docs; cero menciones normativas de la API muerta fuera de referencias históricas. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
1 month ago
_composed_ component's element (Waveform's embedded `Slider`), there is no part
feat(morfo,soma): el attr de nombrado es un DEFAULT del contrato, no una imposicion A-85 estaba confirmado desde el ledger de blocks: la demo de site-header escribia `aria-label` en el trigger del cajon y el canon lo pisaba. El censo dio la magnitud real — 132 declaraciones en 59 morfos — y la disposicion escrita (replicar el `prop-truthy` de button en las 132) resulto ser la direccion inversa a la buena: obligaba a 264 entradas de boilerplate y hacia que el texto del consumidor diese una vuelta entera por el grafo reactivo para aterrizar donde ya estaba. Las 132 declaraciones eran CORRECTAS. El defecto era la precedencia. Asi que el contrato gana dos clases, por VOCABULARIO y no por declaracion: contract (role, data-*, aria-expanded, aria-controls, roledescription...) el runtime SIEMPRE gana; un consumidor que los pisara haria mentir al componente, y esa garantia es algo que las librerias user-props-last no tienen. naming (`ARIA_NAMING_ATTRS`, hoy `aria-label`) el morfo da un DEFAULT y el attr explicito del consumidor gana — espejo de la propia cadena de nombre accesible de la plataforma. Tres puntos, uno por capa, y ninguno de los 132 morfos tocado: `compileMorfo` clasifica el plan (`consumerWins`) y deja de hoistear esos literales a staticAttrs; el runtime los saca del efecto `syncAttrs` —el `dom.apply` post-render ERA el pisado— y los embarca en el bag de la parte, el mismo vehiculo que `dir`; y `mergeProps` los resuelve consumer-first (`a ?? b`, para que un `aria-label=""` explicito sobreviva). Efecto medido que no se buscaba: los labels salen ahora en el HTML de SSR. Antes los escribia un efecto, que en servidor no corre — el propio ledger lo tenia medido y nadie habia atado los dos cabos. `aria-labelledby` se queda en contract A PROPOSITO: es cableado `partRef`, y la plataforma ya hace que un labelledby del consumidor gane a cualquier label sin que tengamos que arbitrarlo. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2 months ago
to declare it on — a `texts` slot resolved by the provider and handed to the
child is the sanctioned shape, and moving it would be wrong. What must still be
audited in that case is the OTHER half: that the consumer's `aria-label` is
stripped from the wrapper's passthrough (`Without<PrimitiveDivAttributes, {
docs(uix): la doctrina alcanza a la bolsa única — el corpus deja de enseñar la API muerta La clase exacta que la auditoría flagueó (doc↔código), producida esta vez por NUESTROS propios cambios de P0 fase C y sin corregir hasta ahora: - component-guide §«two sanctioned ways»: reescrita a UNA vía (la bolsa .props resuelve el contrato entero; extras solo lo que el morfo no puede expresar; valores CRUDOS; la inversa «morfo sin value ⇒ soma escribe»). Los 4 snippets con syncAttrs: true, limpiados. - soma-architecture: partProps re-descrito como LA bolsa (identidad + contrato completo, SSR incluido); el párrafo del flag sustituido por «el único escritor imperativo es el prewrite»; snippet limpiado. - soma.md: snippet de accordion sin el flag. - morfo.md §naming: el «(SSR included)» dejó de ser contraste — desde la tubería única TODO plan viaja en la bolsa; lo que distingue a los naming es la PRECEDENCIA (consumer-first via mergeProps). - testing-and-tooling: la postura SSR gana el canario ssr-contract.test, y nace «The gate» (check:gate + ledger menguante, gate, hook — los instrumentos de fase B no estaban documentados). - CLAUDE.md: el paso 4 de trigger ya no dice «effect-driven» (prettier normalizó de paso el fichero entero — solo whitespace, contenido intacto, declarado aquí para que el diff ancho no sea silencioso). Lo histórico (changelog, old-deprecated, audit de julio, process/) se queda como historia. Verificación: docs:check 0 errores sobre 817 docs; cero menciones normativas de la API muerta fuera de referencias históricas. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
1 month ago
'aria-label'?: string }>`) and forwarded to the child. Waveform shipped without
feat(morfo,soma): el attr de nombrado es un DEFAULT del contrato, no una imposicion A-85 estaba confirmado desde el ledger de blocks: la demo de site-header escribia `aria-label` en el trigger del cajon y el canon lo pisaba. El censo dio la magnitud real — 132 declaraciones en 59 morfos — y la disposicion escrita (replicar el `prop-truthy` de button en las 132) resulto ser la direccion inversa a la buena: obligaba a 264 entradas de boilerplate y hacia que el texto del consumidor diese una vuelta entera por el grafo reactivo para aterrizar donde ya estaba. Las 132 declaraciones eran CORRECTAS. El defecto era la precedencia. Asi que el contrato gana dos clases, por VOCABULARIO y no por declaracion: contract (role, data-*, aria-expanded, aria-controls, roledescription...) el runtime SIEMPRE gana; un consumidor que los pisara haria mentir al componente, y esa garantia es algo que las librerias user-props-last no tienen. naming (`ARIA_NAMING_ATTRS`, hoy `aria-label`) el morfo da un DEFAULT y el attr explicito del consumidor gana — espejo de la propia cadena de nombre accesible de la plataforma. Tres puntos, uno por capa, y ninguno de los 132 morfos tocado: `compileMorfo` clasifica el plan (`consumerWins`) y deja de hoistear esos literales a staticAttrs; el runtime los saca del efecto `syncAttrs` —el `dom.apply` post-render ERA el pisado— y los embarca en el bag de la parte, el mismo vehiculo que `dir`; y `mergeProps` los resuelve consumer-first (`a ?? b`, para que un `aria-label=""` explicito sobreviva). Efecto medido que no se buscaba: los labels salen ahora en el HTML de SSR. Antes los escribia un efecto, que en servidor no corre — el propio ledger lo tenia medido y nadie habia atado los dos cabos. `aria-labelledby` se queda en contract A PROPOSITO: es cableado `partRef`, y la plataforma ya hace que un labelledby del consumidor gane a cualquier label sin que tengamos que arbitrarlo. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2 months ago
that strip — and with the prop misnamed `ariaLabel`, unique in the catalogue —
so a consumer's `aria-label` silently named the role-less root while the real
control kept its default.
uix(navigation-menu): un <ul> no es una región, y por eso el landmark salía sin nombre El morfo declaraba el nombre por defecto de la navegación en la parte que pinta el <ul>, con la razón escrita al lado: «the root <nav> delegates its name to the list it wraps». La delegación no existe. El gesto que un lector de pantalla ofrece para saltar entre las áreas de una página llega al <nav>, y llegaba a un <nav> ANÓNIMO — con el nombre del consumidor puesto una capa más adentro, en un elemento que ninguna navegación por landmarks visita. Sobrevivió tres semanas de tier porque con UNA navegación en la página un landmark anónimo es admisible, y todas las demos del componente y todos los blocks de sitio montan una. Apareció en cuanto el app-shell montó dos ámbitos, que es lo que un shell hace por definición (A-111). La APG es explícita: si una página incluye más de un landmark de navegación, cada uno lleva su nombre. El arreglo NO es el que la ficha proponía. Su paso 1 quería que la lista heredase el nombre por aria-labelledby; el barrido de los morfos hermanos con defaultElement 'nav' dijo otra cosa: breadcrumb (<ol>) y nav-tree (<ul>) declaran aria: [] en su lista y nombran sólo el <nav>. navigation-menu era el único fuera de esa forma, así que la pareja de nombre —el aria-label con su suppression prop-falsy ariaLabelledby, y el aria-labelledby por propRef— sube entera a la parte Provider y la lista se queda con su aria-orientation. En soma, la fuente ariaLabelledby sube al nivel del runtime (la forma de breadcrumb) y el wrapper DEJA de sacar aria-label de restProps: viaja con el resto y gana el merge por política de naming, que es lo que A-85 dejó escrito y lo que este componente no aplicaba. Se van con ello el opt ariaLabel y el reenvío condicional que la List hacía a mano. Y como la regla no estaba escrita en ninguna doctrina —grep de «landmark» en morfo.md, canon/ y guides/ daba cero—, se escribe: architecture/morfo.md §Step 4 gana «A landmark is named on ITS OWN element», y nace el censo morfo/landmark-census.test.ts, que falla si una parte con defaultElement 'nav' no declara un attr de nombre EN SU PARTE. Probado en rojo por mutación tres veces: deshaciendo el arreglo (navigation-menu.provider), retirando la excepción (palabras.breadcrumb) y vaciando el catálogo, que es la mutación que impide que un censo sobre nada pase en verde. El ámbito es 'nav' a propósito: main, header y footer son únicos por página, y section y form sólo son landmark cuando ya tienen nombre. El guard destapó de paso un segundo caso de la misma clase: la parte Breadcrumb de palabras declara un <nav> con aria: [] y nadie la nombra, ni en soma ni en eidos. Queda como A-116 con EXCEPCIÓN FIRMADA por el autor: palabras es el eje de otra sesión y su rama es compartida. La tercera fila del censo obliga a retirar la excepción el día que esa parte declare un nombre. Verificado con el nombre COMPUTADO desde el árbol AX de Chrome por CDP, nunca leyendo el atributo (un aria-label en el DOM prueba el atributo, no el nombre), con servidor recién arrancado y esperando la puerta de hidratación honesta: /blocks/app-shell/preview navigation: ['Navegación principal', 'Menú de la aplicación', 'Migas de pan'] (antes: el 2.º null) /uix/components/navigation-menu (sin label del consumidor) el <nav> computa 'Principal' — el default del morfo, traducido, sobre el landmark; el <ul>, sin nombre Gates: 420/420 en morfo+sema+soma+eidos tocados · component:audit --only navigation-menu PASS · morfo:check PASS · svelte-check 72/62 antes y después · docs:check 0/0. Los avisos de prettier de los ficheros tocados ya fallaban en HEAD (comprobado con git show HEAD:… | prettier --check); el fichero nuevo va formateado. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2 months ago
**A landmark is named on ITS OWN element.** A part whose `defaultElement` is
`nav` (or any other landmark) declares its naming default THERE — never on a
child part, however natural the delegation reads. `NavigationMenu` put the
default on the part that renders the `<ul>`, with the reason written in the
morfo («the root `<nav>` delegates its name to the list it wraps»), and a
`<ul>` is not a region: the gesture a screen reader offers to jump between the
areas of a page reaches the `<nav>` — anonymous. It stayed invisible for as
long as every page mounted ONE navigation, which is admissible; an application
shell mounts two scopes by definition and then it is an APG violation (A-111).
The census `src/uix/morfo/landmark-census.test.ts` holds the `nav` half of
this, with signed exceptions.
Add `condition` when the ARIA is emitted only in some cases:
```ts
condition: 'always'
condition: { when: 'part-present', part: 'title' }
condition: { when: 'part-absent', part: 'label' } // e.g. aria-label only when no Label part
condition: { when: 'state-equals', state: 'open', value: 'true' }
condition: { when: 'prop-truthy', prop: 'modal' }
condition: { when: 'prop-falsy', prop: 'disabled' }
```
### Step 4.5 — Declare component-owned `texts`
If a text slot belongs to the component contract, declare it on the morfo as
an idlangref. The morfo never embeds the literal multilingual record — that
lives in `src/uix/langs/components/{kebab}.ts` and is merged into the active
catalog by `ActiveUix`:
```ts
export const dialogMorfo = {
name: 'Dialog',
kebab: 'dialog',
// ...
texts: {
trigger: '#?components.dialog.trigger|Open dialog',
'content.roledescription': '#?components.dialog.content.roledescription|dialog window'
},
parts: [
{
name: 'Content',
kebab: 'content',
// ...
aria: [
{
attr: 'aria-roledescription',
value: v.translationRef('content.roledescription', 'dialog window')
}
]
}
]
} as const satisfies Morfo;
```
Use relative refs for text owned by this component. Use `v.commonRef` for
shared actions (`close`, `cancel`, `save`, `next`, `previous`) so the same
string is not duplicated across Drawer, Dialog, Popover, Toast, etc. Use
`v.langRef` for an app/system namespace that is deliberately not owned by
the component.
### Step 5 — Optional: keyboard and focus
```ts
keyboard: [
{ key: 'Escape', action: 'close' },
{ key: 'Tab', action: 'focus-next' },
{ key: 'Shift+Tab', action: 'focus-prev' }
]
// Overlay family — executed by FocusScope (the declaration is the SPECIES
// default; the per-instance `modal`/`trapFocus` prop wins, the `dir` pattern):
focus: {
kind: 'trap',
trap: true, // MUST match the wrapper's shipped default (census-compared)
initial: 'first-focusable', // 'first-focusable' | 'trigger' | { partRef: 'content' }
return: 'trigger', // 'trigger' | 'previous' | { partRef: '...' }
restore: true
}
// Composite family — executed by RovingFocusGroup ($adom), or by a SIGNED
// custom executor when the pattern genuinely exceeds the 1D primitive:
focus: {
kind: 'roving',
parts: ['item'], // the roving candidate part(s), by kebab
orientation: 'horizontal', // 'horizontal' | 'vertical' | 'both' | 'grid'
loop: true
// executor: { custom: '2D row/cell traversal — the shared primitive is 1D' }
}
// Directional walk — every listed part is ALREADY a tab stop; the arrows are a
// convenience on top and no `tabindex` is ever written. Executed by
// `getDirectionalKeys` ($soma/keyboard/directional) + `dom.focus`. No
// `orientation` field: both members take the axis from a per-instance prop.
focus: {
kind: 'walk',
parts: ['trigger'] // accordion, navigation-menu
}
// Virtual focus — the DOM stop never moves; the anchor publishes the highlight
// as `aria-activedescendant`. Executed by `highlightWalk`
// ($soma/keyboard/highlight-walk), which moves an index, not a selection.
focus: {
kind: 'activedescendant',
anchor: 'trigger', // the part that keeps the single tab stop
items: ['item'] // select, command, combobox
}
// Slider model — one focusable root, and the arrows move the VALUE, not the
// focus. Executed by `valueStep` ($soma/keyboard/value-step). Bare on purpose:
// no parts to walk, no axis to resolve, no stop to manage.
focus: {
kind: 'slider' // rating-group
}
// Segmented field — every segment is a CONSTANT tab stop (`tabindex: 0`, the
// opposite of a roving stop). Executed by `segmentWalk` / `segmentAdvance` /
// `segmentRetreat` ($soma/keyboard/segment-walk), which also owns the
// auto-advance on type and the retreat on delete.
focus: {
kind: 'segments',
parts: ['segment'] // date-field, time-field, color-field
}
```
**The focus declaration names the SPECIES and the keyboard primitive EXECUTES
it — two contracts, one taxonomy**: `segments`↔`segmentWalk`,
`activedescendant`↔`highlightWalk`, `slider`↔`valueStep`, `walk`↔the
directional walk, `roving`↔`RovingFocusGroup`, `trap`↔`FocusScope`. Two layers
partitioned the same behaviour independently and landed on the same six names;
that agreement is the evidence the partition is the real one, and it is why a
seventh species must be measured into the union rather than parked as a signed
exception.
Declare `focus` for every component with focus behaviour — overlays
(Dialog, Drawer, Popover…), composites (Toolbar, Tabs, Listbox…), walked
disclosures (Accordion, NavigationMenu), virtual-focus listboxes (Select,
Command, Combobox), value-stepping roots (RatingGroup) and segmented fields
(DateField, TimeField, ColorField). `focus-census.test.ts` fails a component
that ships focus machinery without a declaration, a `trap` default that
contradicts the wrapper, a roving component with neither the shared primitive
nor a signed executor, and any declaration whose executor is not wired (one
rule per kind). Plain controls with no focus behaviour omit it.
### Step 5.5 — Optional: semantic events
Events declare what semantic occurrences the component emits. The semantic
vocabulary (families, intents, verbs) is canonical in
[`docs/CANON.md`](../CANON.md); the shape is:
```ts
events: [
{
name: 'commit-toggle',
semantic: {
family: 'commit', // one of 8 SEMA families
verb: 'toggle', // canonical verb (advisory, validated)
target: v.partRef('provider'), // which part receives data-event-*
sequence: 'post', // 'pre' | 'coincident' | 'post'
intent: {
// valenced families only
fromProp: 'intent', // bind to a public prop
default: 'neutral',
supported: ['neutral', 'affirm', 'risk', 'threat']
}
}
}
];
```
Field rules:
feat(morfo,sema,eidos): todo evento dice de que familia es, y el cruce por fin se ve El framework llamaba a la misma cosa de dos maneras: `open` pelado en ocho componentes y `emerge-open` en tres. No era estetica — un preset de movimiento engancha el nombre con `^=`, asi que el dialecto pelado no casaba con ninguna firma y simplemente no animaba, sin romper una sola prueba. Los 40 nombres sin prefijo pasan a `{familia}-{verbo}[-{matiz}]`: 256 eventos, 256 con prefijo, 0 ambiguos. El plan decia 36 y decia `handle-drag-start`; eran 40, y el canon (c25) dice que esos verbos son `pick` y `drop` — `handle-pick` y `handle-drop` ya existian en 10 y 4 componentes. `validateMorfo` cierra la puerta: un `events[].name` que no empiece por su familia ahora lanza. Visto fallar antes con un nombre pelado inyectado. Lo que el renombrado destapo, y va aqui tambien: - La receta del splitter enganchaba `commit-resize`, muerto desde `bd2e40366`. No casaba desde mayo y nadie chillo. Reescrita por FAMILIA, como slider y knob, y `eidos-lint` valida ahora el VALOR de `data-event*` contra el catalogo de morfos — el guard que lo habria cazado en su dia. - La familia `shift` era muda en el canal visual, contra su propia doctrina (c27: el cruce debe percibirse; c34 tipifica el «shift invisible»). Su mapa ya describia la firma que le faltaba y su sonido por defecto es `slide`. Ahora tiene firma direccional: sexto atributo del sello (`data-event-direction`, `forward`|`backward`, por emision) y deslizamiento de 320ms RTL-safe por `:dir()`. Medido: LTR -30px/+30px, RTL los invierte. - El sello de `shift-navigate` pasa del BOTON al `grid` en los cuatro calendarios. Medido: el boton recibia `contact-activate` y 8,5 ms despues —media trama— el `shift-navigate` pisaba la misma ranura y el `press-squeeze` moria sin pintar un fotograma. Una superficie, una ranura (A-36). - 101 contradicciones docs<->morfo adjudicadas con evidencia (git log, docs de decision, el componente vivo). Las docs desfasadas, corregidas; los nueve DEFECTOS de codigo obsoleto quedan abiertos y sin tocar. - `SoundDirection` -> `SoundContour`: era un contorno de tono, no un sentido, y habia tres cosas distintas deletreadas «direction». check en su linea base con 0 errores nuevos por diferencia de conjuntos · docs:check 0/0 · eidos-lint invalid 0 · el censo y las escenas de navegador medidas con raton real y rAF vivo. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2 months ago
- **`name`** — the addressable id used by `runtime.trigger(name)`. The name
**declares the family**: `{family}-{verb}[-{nuance}]` (e.g. `commit-toggle`,
`emerge-dismiss-outside`). `validateMorfo` rejects a name that does not
start with its own `semantic.family`.
- **`semantic.family`** — one of the 8: `contact`, `commit`, `signal`,
`handle`, `emerge`, `shift`, `sustain`, `delegate` (per `SEMA_MAP`).
Whether `intent` is required is set per-family by `SEMA_FAMILY_POLICY`
(`src/uix/sema/types.ts`), not by the valenced/transitional split: only
`commit` and `signal` are `intentRequirement: 'required'`; every other
family makes `intent` optional (literal or fromProp binding).
- **`semantic.verb`** — optional, advisory. Must be in
`SEMA_VERBS[family]` when present.
- **`semantic.target`** — the `partRef` whose DOM element receives the
`data-event-*` attrs during the visual hold. Lives inside `semantic`
feat(morfo,soma): el attr de nombrado es un DEFAULT del contrato, no una imposicion A-85 estaba confirmado desde el ledger de blocks: la demo de site-header escribia `aria-label` en el trigger del cajon y el canon lo pisaba. El censo dio la magnitud real — 132 declaraciones en 59 morfos — y la disposicion escrita (replicar el `prop-truthy` de button en las 132) resulto ser la direccion inversa a la buena: obligaba a 264 entradas de boilerplate y hacia que el texto del consumidor diese una vuelta entera por el grafo reactivo para aterrizar donde ya estaba. Las 132 declaraciones eran CORRECTAS. El defecto era la precedencia. Asi que el contrato gana dos clases, por VOCABULARIO y no por declaracion: contract (role, data-*, aria-expanded, aria-controls, roledescription...) el runtime SIEMPRE gana; un consumidor que los pisara haria mentir al componente, y esa garantia es algo que las librerias user-props-last no tienen. naming (`ARIA_NAMING_ATTRS`, hoy `aria-label`) el morfo da un DEFAULT y el attr explicito del consumidor gana — espejo de la propia cadena de nombre accesible de la plataforma. Tres puntos, uno por capa, y ninguno de los 132 morfos tocado: `compileMorfo` clasifica el plan (`consumerWins`) y deja de hoistear esos literales a staticAttrs; el runtime los saca del efecto `syncAttrs` —el `dom.apply` post-render ERA el pisado— y los embarca en el bag de la parte, el mismo vehiculo que `dir`; y `mergeProps` los resuelve consumer-first (`a ?? b`, para que un `aria-label=""` explicito sobreviva). Efecto medido que no se buscaba: los labels salen ahora en el HTML de SSR. Antes los escribia un efecto, que en servidor no corre — el propio ledger lo tenia medido y nadie habia atado los dos cabos. `aria-labelledby` se queda en contract A PROPOSITO: es cableado `partRef`, y la plataforma ya hace que un labelledby del consumidor gane a cualquier label sin que tengamos que arbitrarlo. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2 months ago
per the doctrinal shape (was at event-level pre-2026-05-08). Which part
it should be follows one rule — the gesture is stamped where the hand
is, the terminal where the value lives: §_Where the stamp lands_ below.
feat(soma,morfo)!: targetOverride BORRADO — el estado ilegal del eje es inexpresable El endgame que §3.0 firmo al abrir F3: con el censo a 0, la opcion de elemento anonimo sale de TriggerOptions, y con ella assertTargetOverride (existia solo para vigilarla; queda assertAnchor, el guard de la puerta que la sustituye). Las dos resoluciones `opts.targetOverride ?? resolveEmitTarget(...)` (emit y foco a11y) quedan en la resolucion unica, y los Omit<..., 'targetOverride'> de los triggers anclados se vuelven vacuos y desaparecen. El tipo lleva LAPIDA doctrinal: la opcion nacio fallbackTarget, se renombro el 2026-08-10 como precondicion del censo, y compensaba un registro sin identidad de instancia — es lo que dejo a tres auditorias (fable S1, sema S-17, blocks A-36/A-65) encontrar la misma deriva sin poder cerrarla. No volver a añadir una opcion de elemento. Y la tesis se probo sola al ejecutarla: el compilador, ya como censo, cazo 9 usos en TESTS que el grep del scratch nunca escaneo. De ellos, 3 eran redundantes (la parte registraba el mismo elemento), 2 del test de metrics migran al registro + trigger plano, y 4 fijaban la conducta borrada — el describe «targetOverride contract» entero y el test del override preferente, retirados con lapida; el guard A-36 de los overlays (morfo sin redireccion + post) se conserva, des-anidado. El scratch del censo, retirado: el censo es `npm run check`. Docs vivos adjudicados: morfo.md §allowedTargets describe la puerta anclada (EventNameTargeting + assertAnchor) y §repeated-part nombra partInstance donde decia targetOverride; los docblocks de las 3 factorias de vistas de eidos dicen la forma nueva; errors.ts deja de ofrecer la opcion muerta como remedio. Las menciones historicas (A-36, «the retired...») se conservan como historia. Verificado: soma navegador 1278✓/1 (el timeout ajeno preexistente de soma-attr-audit, documentado en la base) · runtime+metrics 53/53 · check 69 = base, 0 propios · docs:check 0/623 · prettier limpio en lo que estaba limpio. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2 months ago
- **`semantic.allowedTargets`** — optional list of `partRef`s an ANCHORED
emission may land on (`SomaRuntimePart.trigger` /
`runtime.partInstance(part, el).trigger` — identity against the registry;
the raw-element `targetOverride` option was DELETED 2026-08-13 once the F3
census hit zero). The exact counterpart of `allowedFamilies` on the target
axis: `target` stays the default the runtime resolves; this declares the
OTHER surfaces an emission may land on — the toolbar `command-button` a
format gesture names, the `dropzone` beside a file-upload's trigger.
Declaring it is what lets `pack-census.test.ts` tell a legitimate
redirection from drift: a sema rule may select the declared target or a
part listed here, and nothing else (the TextArea class of bug — rules
aimed at a part the stamp never visits — ran 6×/13× above its written gain
for months without any guard seeing it).
The runtime side is enforced twice: statically, the anchored trigger's name
union (`EventNameTargeting`) only admits events whose `target` or
`allowedTargets` include the anchoring part; and in dev, `assertAnchor`
warns JS callers whose anchor part is off-contract.
docs(uix): la doctrina alcanza a la bolsa única — el corpus deja de enseñar la API muerta La clase exacta que la auditoría flagueó (doc↔código), producida esta vez por NUESTROS propios cambios de P0 fase C y sin corregir hasta ahora: - component-guide §«two sanctioned ways»: reescrita a UNA vía (la bolsa .props resuelve el contrato entero; extras solo lo que el morfo no puede expresar; valores CRUDOS; la inversa «morfo sin value ⇒ soma escribe»). Los 4 snippets con syncAttrs: true, limpiados. - soma-architecture: partProps re-descrito como LA bolsa (identidad + contrato completo, SSR incluido); el párrafo del flag sustituido por «el único escritor imperativo es el prewrite»; snippet limpiado. - soma.md: snippet de accordion sin el flag. - morfo.md §naming: el «(SSR included)» dejó de ser contraste — desde la tubería única TODO plan viaja en la bolsa; lo que distingue a los naming es la PRECEDENCIA (consumer-first via mergeProps). - testing-and-tooling: la postura SSR gana el canario ssr-contract.test, y nace «The gate» (check:gate + ledger menguante, gate, hook — los instrumentos de fase B no estaban documentados). - CLAUDE.md: el paso 4 de trigger ya no dice «effect-driven» (prettier normalizó de paso el fichero entero — solo whitespace, contenido intacto, declarado aquí para que el diff ancho no sea silencioso). Lo histórico (changelog, old-deprecated, audit de julio, process/) se queda como historia. Verificación: docs:check 0 errores sobre 817 docs; cero menciones normativas de la API muerta fuera de referencias históricas. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
1 month ago
feat(morfo,sema,eidos): todo evento dice de que familia es, y el cruce por fin se ve El framework llamaba a la misma cosa de dos maneras: `open` pelado en ocho componentes y `emerge-open` en tres. No era estetica — un preset de movimiento engancha el nombre con `^=`, asi que el dialecto pelado no casaba con ninguna firma y simplemente no animaba, sin romper una sola prueba. Los 40 nombres sin prefijo pasan a `{familia}-{verbo}[-{matiz}]`: 256 eventos, 256 con prefijo, 0 ambiguos. El plan decia 36 y decia `handle-drag-start`; eran 40, y el canon (c25) dice que esos verbos son `pick` y `drop` — `handle-pick` y `handle-drop` ya existian en 10 y 4 componentes. `validateMorfo` cierra la puerta: un `events[].name` que no empiece por su familia ahora lanza. Visto fallar antes con un nombre pelado inyectado. Lo que el renombrado destapo, y va aqui tambien: - La receta del splitter enganchaba `commit-resize`, muerto desde `bd2e40366`. No casaba desde mayo y nadie chillo. Reescrita por FAMILIA, como slider y knob, y `eidos-lint` valida ahora el VALOR de `data-event*` contra el catalogo de morfos — el guard que lo habria cazado en su dia. - La familia `shift` era muda en el canal visual, contra su propia doctrina (c27: el cruce debe percibirse; c34 tipifica el «shift invisible»). Su mapa ya describia la firma que le faltaba y su sonido por defecto es `slide`. Ahora tiene firma direccional: sexto atributo del sello (`data-event-direction`, `forward`|`backward`, por emision) y deslizamiento de 320ms RTL-safe por `:dir()`. Medido: LTR -30px/+30px, RTL los invierte. - El sello de `shift-navigate` pasa del BOTON al `grid` en los cuatro calendarios. Medido: el boton recibia `contact-activate` y 8,5 ms despues —media trama— el `shift-navigate` pisaba la misma ranura y el `press-squeeze` moria sin pintar un fotograma. Una superficie, una ranura (A-36). - 101 contradicciones docs<->morfo adjudicadas con evidencia (git log, docs de decision, el componente vivo). Las docs desfasadas, corregidas; los nueve DEFECTOS de codigo obsoleto quedan abiertos y sin tocar. - `SoundDirection` -> `SoundContour`: era un contorno de tono, no un sentido, y habia tres cosas distintas deletreadas «direction». check en su linea base con 0 errores nuevos por diferencia de conjuntos · docs:check 0/0 · eidos-lint invalid 0 · el censo y las escenas de navegador medidas con raton real y rAF vivo. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2 months ago
- **`semantic.targetFallback`** — ordered `partRef` chain the RUNTIME resolves
when the canonical `target` has **no live element** at emit time: the first
listed part with a registered instance takes the stamp (and the a11y focus
move, when the event declares one — `resolveEmitTarget` is the ONE
resolution both share, so they can never disagree). This is the second axis
of emission targeting, split from `allowedTargets` on the precedent of
`intentRequirement`/`intentGuidance`:
docs(uix): la doctrina alcanza a la bolsa única — el corpus deja de enseñar la API muerta La clase exacta que la auditoría flagueó (doc↔código), producida esta vez por NUESTROS propios cambios de P0 fase C y sin corregir hasta ahora: - component-guide §«two sanctioned ways»: reescrita a UNA vía (la bolsa .props resuelve el contrato entero; extras solo lo que el morfo no puede expresar; valores CRUDOS; la inversa «morfo sin value ⇒ soma escribe»). Los 4 snippets con syncAttrs: true, limpiados. - soma-architecture: partProps re-descrito como LA bolsa (identidad + contrato completo, SSR incluido); el párrafo del flag sustituido por «el único escritor imperativo es el prewrite»; snippet limpiado. - soma.md: snippet de accordion sin el flag. - morfo.md §naming: el «(SSR included)» dejó de ser contraste — desde la tubería única TODO plan viaja en la bolsa; lo que distingue a los naming es la PRECEDENCIA (consumer-first via mergeProps). - testing-and-tooling: la postura SSR gana el canario ssr-contract.test, y nace «The gate» (check:gate + ledger menguante, gate, hook — los instrumentos de fase B no estaban documentados). - CLAUDE.md: el paso 4 de trigger ya no dice «effect-driven» (prettier normalizó de paso el fichero entero — solo whitespace, contenido intacto, declarado aquí para que el diff ancho no sea silencioso). Lo histórico (changelog, old-deprecated, audit de julio, process/) se queda como historia. Verificación: docs:check 0 errores sobre 817 docs; cero menciones normativas de la API muerta fuera de referencias históricas. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
1 month ago
| Axis | Who decides | When | Example |
| ---------------- | ----------------------------- | -------------------------------- | -------------------------------------------------------- |
| `allowedTargets` | the CALLER, per trigger | normal operation, repeated parts | the pressed `day`, the clicked `item` |
feat(morfo,sema,eidos): todo evento dice de que familia es, y el cruce por fin se ve El framework llamaba a la misma cosa de dos maneras: `open` pelado en ocho componentes y `emerge-open` en tres. No era estetica — un preset de movimiento engancha el nombre con `^=`, asi que el dialecto pelado no casaba con ninguna firma y simplemente no animaba, sin romper una sola prueba. Los 40 nombres sin prefijo pasan a `{familia}-{verbo}[-{matiz}]`: 256 eventos, 256 con prefijo, 0 ambiguos. El plan decia 36 y decia `handle-drag-start`; eran 40, y el canon (c25) dice que esos verbos son `pick` y `drop` — `handle-pick` y `handle-drop` ya existian en 10 y 4 componentes. `validateMorfo` cierra la puerta: un `events[].name` que no empiece por su familia ahora lanza. Visto fallar antes con un nombre pelado inyectado. Lo que el renombrado destapo, y va aqui tambien: - La receta del splitter enganchaba `commit-resize`, muerto desde `bd2e40366`. No casaba desde mayo y nadie chillo. Reescrita por FAMILIA, como slider y knob, y `eidos-lint` valida ahora el VALOR de `data-event*` contra el catalogo de morfos — el guard que lo habria cazado en su dia. - La familia `shift` era muda en el canal visual, contra su propia doctrina (c27: el cruce debe percibirse; c34 tipifica el «shift invisible»). Su mapa ya describia la firma que le faltaba y su sonido por defecto es `slide`. Ahora tiene firma direccional: sexto atributo del sello (`data-event-direction`, `forward`|`backward`, por emision) y deslizamiento de 320ms RTL-safe por `:dir()`. Medido: LTR -30px/+30px, RTL los invierte. - El sello de `shift-navigate` pasa del BOTON al `grid` en los cuatro calendarios. Medido: el boton recibia `contact-activate` y 8,5 ms despues —media trama— el `shift-navigate` pisaba la misma ranura y el `press-squeeze` moria sin pintar un fotograma. Una superficie, una ranura (A-36). - 101 contradicciones docs<->morfo adjudicadas con evidencia (git log, docs de decision, el componente vivo). Las docs desfasadas, corregidas; los nueve DEFECTOS de codigo obsoleto quedan abiertos y sin tocar. - `SoundDirection` -> `SoundContour`: era un contorno de tono, no un sentido, y habia tres cosas distintas deletreadas «direction». check en su linea base con 0 errores nuevos por diferencia de conjuntos · docs:check 0/0 · eidos-lint invalid 0 · el censo y las escenas de navegador medidas con raton real y rAF vivo. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2 months ago
| `targetFallback` | the RUNTIME, from mount state | the declared target is unmounted | drawer `emerge-close` → `trigger` once `content` is gone |
It replaces the hand-rolled `content ?? partRef('trigger')` every overlay
provider used to write around `targetOverride` (dialog / drawer / popover /
float-panel `close`; aura's terminals landing on `provider` when the
decorative ring was never composed; chronos' editor commits landing on
`provider` when no chip names them). Declared in the morfo so soma, sema and
eidos read the same truth: `pack-census.test.ts` counts these parts as
stampable, exactly like `allowedTargets` — but note the perceptual
difference: an `allowedTargets` part is stamped in routine use, a
`targetFallback` part only in the degraded mount, so a sound rule that
matches ONLY fallback parts almost never fires.
`validateMorfo` enforces three invariants (all tested): every entry is an
existing part; the canonical `target` may not list itself (it is always
resolved first); no duplicates (order is meaning — a duplicate reads as two
chances where there is one). Anchored emissions (`SomaRuntimePart.trigger` /
`partInstance(...).trigger`) do NOT fall back: an anchored emit stamps ITS
instance or raises a target error, never a silent redirection — and the
anchored name union (`EventNameTargeting`) excludes fallback-only events on
purpose, because anchoring to the degraded surface would force the poor
landing while the primary is mounted.
docs(uix): la doctrina alcanza a la bolsa única — el corpus deja de enseñar la API muerta La clase exacta que la auditoría flagueó (doc↔código), producida esta vez por NUESTROS propios cambios de P0 fase C y sin corregir hasta ahora: - component-guide §«two sanctioned ways»: reescrita a UNA vía (la bolsa .props resuelve el contrato entero; extras solo lo que el morfo no puede expresar; valores CRUDOS; la inversa «morfo sin value ⇒ soma escribe»). Los 4 snippets con syncAttrs: true, limpiados. - soma-architecture: partProps re-descrito como LA bolsa (identidad + contrato completo, SSR incluido); el párrafo del flag sustituido por «el único escritor imperativo es el prewrite»; snippet limpiado. - soma.md: snippet de accordion sin el flag. - morfo.md §naming: el «(SSR included)» dejó de ser contraste — desde la tubería única TODO plan viaja en la bolsa; lo que distingue a los naming es la PRECEDENCIA (consumer-first via mergeProps). - testing-and-tooling: la postura SSR gana el canario ssr-contract.test, y nace «The gate» (check:gate + ledger menguante, gate, hook — los instrumentos de fase B no estaban documentados). - CLAUDE.md: el paso 4 de trigger ya no dice «effect-driven» (prettier normalizó de paso el fichero entero — solo whitespace, contenido intacto, declarado aquí para que el diff ancho no sea silencioso). Lo histórico (changelog, old-deprecated, audit de julio, process/) se queda como historia. Verificación: docs:check 0 errores sobre 817 docs; cero menciones normativas de la API muerta fuera de referencias históricas. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
1 month ago
fix(sema,morfo,soma): una superficie no admite dos ocurrencias, y el nombre ya no miente `data-event-*` es UNA RANURA por elemento. Tres auditorias independientes encontraron el mismo defecto y ninguna lo cerro: fable S1 (2026-07-01, con el README diciendo «findings executed»), sema S-17 (2026-08-05, midio el estampado del knob muriendo a los 24ms de un hold de 240) y blocks A-36/A-65 (reproducido en navegador, dos estampados a 1,1ms). Lo que faltaba no era diagnostico: nadie podia distinguir una redireccion legitima de una deriva, porque la opcion que redirigia se llamaba `fallbackTarget` y hacia lo contrario de lo que decia. EL NOMBRE PRIMERO. `fallbackTarget` -> `targetOverride` (202 apariciones, 64 ficheros, dirigido con `git ls-files`). Siempre GANO sobre el ref registrado; el nombre honesto es la precondicion para auditarlo. Las cronicas conservan el viejo por diseño. LA PROPIEDAD. `unstampEventAttrs` recibia el signal y lo DESCARTABA, asi que la ocurrencia que terminaba antes borraba a la que tenia la ranura. Ahora comprueba `data-event-id`. Con eso, `replace` deja de ser el comportamiento roto y pasa a ser el suelo correcto. A-36, SIN `regime`. Los cuatro overlays cedian el estampado del `open` al trigger por un workaround de la era `sequence: 'pre'`; con `post` el content ya esta montado. Retirado el override (float-panel migrado `pre`->`post`, ultimo fuera de la doctrina que sema.md escribe). Medido en /blocks/site-header/preview a 375px: contact-activate en el TRIGGER +18,7ms y open en el CONTENT +20,6ms, con press-squeeze y slide-from-right-full corriendo. Antes: los dos en el trigger, press-squeeze jamas. `regime` DEFINIDO — y son dos valores, no cuatro. `replace` (default) y `queue`, este solo para las 4 parejas irreducibles: los tres toggles (el provider ES el boton) y el knob. `collapse` retirado (su caso murio con la propiedad: medido, 0 huecos en 5 emisiones a 72ms) y `lock` retirado (significaria «un cierre que no se anuncia»), con sus 4 declaraciones muertas. El guard de `queue` PASO EN VERDE SIENDO INCORRECTO: con timers falsos no hay animaciones, asi que awaitExpression volvia al instante. El navegador midio el commit encolado llegando 1,6s tarde. `queue` espera el HOLD, no la expresion. Medir la envolvente no es medir la salida. `renderAttrs` — los attrs que no son partes. Vaciar ACTIVE_DEV_TRACK (palabras y chronos ya no estan excluidos) destapo 23 `data-palabras-*` que ningun morfo declaraba y que palabras.css SI estiliza: aterrizan en el arbol que el usuario escribio, asi que no pueden ser partes. Campo nuevo con la linea afilada —si el consumidor puede componerlo, es una PARTE— y 3 consumidores (palabras, waveform, aura). Declarado en types.ts Y en el esquema sium: la leccion de MorfoElement. Ademas: 5 selectores muertos en el predicado del focus-scope de palabras (barrido de todo el repo: aparecian solo ahi) · RTL-1 real en palabras-chrome (inset logico + translate fisico = doble volteo; rtl:check 1->0) · el union inline al `Direction` canonico, que destapo que el censo de direccion grepea el NOMBRE del tipo · deuda de chronos (scope, barrel, README). Y de la cola: S-36 (barrel 68->71 + guard), S-31 (vibrate(NaN)), S5 (la preferencia de reduced-motion del usuario ya gana a los channels del morfo), SO2 (el warn que el JSDoc prometia), SO4 (validar el target antes del prewrite). Todos los guards nuevos vistos fallar antes de arreglar nada. Base: check 74 (= base) · sema+morfo+contracts 454 verdes / 6 ajenos preexistentes · soma navegador 1258/1259 (el rojo es un timeout ajeno, reproducido con los cambios en stash) · docs:check 0/618 · rtl:check 0/177. Handoff: docs/process/CONTINUE-perceptual-surface.md Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2 months ago
- **`regime`** — what this event does when it arrives and the target surface
already carries a live occurrence: `replace` (default) or `queue`. The
`data-event-*` projection is ONE SLOT per element. Declare it only for pairs
that genuinely share a node — when the collision comes from a redirection,
retire the redirection instead. Detail:
[`architecture/sema.md` §The surface is ONE SLOT](./sema.md).
feat(sema)!: la señal precede al commit — emit() devuelve {id, settled} síncrono, el hold no bloquea a nadie, y el motor enacta la reducción 'state' (D-full + sema §3.9) D-full. `EngineSemantic.emit(signal)` deja de ser `Promise<string>` que resolvía tras el hold y pasa a devolver SÍNCRONAMENTE `EmitHandle { id, settled }`: el id se acuña sin await, la proyección sigue siendo síncrona en `replace`, y `settled` cubre cola + proyección + despacho + hold + expresión + limpieza (la limpieza ocurre ANTES de resolver). El runtime de soma, en `pre`/`coincident`, proyecta y despacha ANTES del handler en el mismo tick (ya no espera el hold); en `post`, handler → tick() → emit. `TriggerResult.settled` siempre presente. La a11y del runtime (announce / foco) corre tras el despacho, no tras el hold. El modelo es el de la plataforma (`element.animate()` → `Animation.finished`): el estado cambia en el instante de la acción y la expresión corre en su propio reloj sobre un nodo que retiene Presence. `pre`/`post`/`coincident` describen el ORDEN entre proyección y mutación, nunca una espera; `coincident` pasa a ser literalmente lo que declara. Medido en Chrome real (servidor propio, misma sonda antes/después; Escape → `data-state=closed`): dialog 258.6 → 9.1 ms · drawer 286.6 → 9.1 · popover 1757.8 → 8.5 · float-panel 1807.8 → 49.7 (n=1, pane oculto). Toggle con `regime: 'queue'` conserva su forma (contact 20 · estado 28 · commit encolado 156). En los cuatro el sello `emerge-close` se proyecta ANTES del cambio de estado y `data-ending-style` aparece (Presence). El «hasta 1,5 s» del informe de auditoría era real: el tope de `awaitExpression` se pagaba ante cualquier animación viva en el target (popover, float-panel). Condición 1 (censo, fase 0): 260 eventos; 39 `pre` + 21 `coincident` (0 sin `sequence`). Solo 14 tienen handler registrado (los únicos donde D-full adelanta la mutación; cifra del adversarial, el constructor contó 12); los otros 46 ya mutaban el estado en el mismo tick que un `void trigger` (sidebar.css:363-375 y tooltip-provider:272 lo tenían escrito con medición). Los 6 que desmontan (4 overlays + toast + tooltip) tienen Presence. CERO migraciones necesarias. La regla transversal `[data-event^='emerge-close'] { animation: dismiss-fade }` (base.css) existe en LETRA sobre los cuatro overlays, pero el preset de Presence gana la cascada en todo instante (medido con getComputedStyle): el refutador tenía razón en EFECTO. Condición 2 (carrera nueva del id síncrono): `clear(id)` / `clearTarget` honran ocurrencias EN VUELO (`inFlight` + `pendingClears`): la ocurrencia no se registra como persistente al terminar y limpia como transitoria; `clear` devuelve true. Reproducida en test (hold corriendo ⇒ tras `settled`, sin proyección residual y `active` vacío). `dispose()` vacía ambas. SO4 re-firmado: un target ausente NUNCA salta el handler ni lanza en ninguna secuencia — se omite prewrite+emit, se reporta por logger con `SomaRuntimeTargetError` como payload, y el handler corre («sema es ornamental; una acción del usuario nunca se pierde por un adorno»). sema §3.9. La política perceptual `'state'` bajo reduce pasa al MOTOR: `ReducedMotionFallback` vive en sema (morfo lo importa), la señal lleva `a11y.reducedMotionFallback` (cualquier valor declarado) y el motor, con `motion.get() === 'reduce'` y `'state'`, devuelve el mismo retorno silenciado que `channels: []` (antes de `queue`). Soma deja de forzar `channels: []` (`a11yChannelsOverride` muere) y conserva `'text'`/`'focus'` con `sources.motion`. S5 se conserva como doctrina (la preferencia gana al morfo y a la llamada); cambia el enactor. Cierra el emit DIRECTO sin a11y (~76 en el docs site). Dato: hoy ningún morfo declara `'state'` (9 declaran `'text'`); el valor es contrato y simetría con la háptica, no un síntoma vivo. Prosa re-firmada (CANON intacto, silente a propósito): engine.ts docblock · sema.md (emit contract, promise semantics, lifecycle, One door, overlays, error policy, reductions, migration) · morfo.md · active-architecture.md (walkthrough) · overview.md · soma-architecture.md · chans/types.ts req. 3 · sema/README · book-deviations D.9/D.10/D.12 · morfo/types.ts (MorfoEventSequence, MorfoA11ySemantic) · comentarios del `sequence: 'post'` en dialog/drawer/popover/ float-panel (valor intacto). Verificación (tras la ronda 2): sema 337/337 · runtime 63/63 · alcance del lote 201 ficheros / 2136 tests · suite entera 5366/5366 (458 ficheros, exit 0) · check 89 errores, 0 en src/, ledger intacto (los ~76 emit de web/routes son fire-and-forget y compilan con el handle) · gate VERDE tras la ronda 1 (5359/5359; un flake de gesture.test rotate-detent bajo carga, verde en solitario y dentro del gate) · +20 tests (engine 40→53, runtime 56→63) · 3 mutaciones enrojecen y revertidas byte-idénticas (⚠ diferir la limpieza UN microtask es un no-op observable: la mutación honesta cruza a macrotask) · adversarial Opus independiente (métodos propios, árbol byte-idéntico al terminar): doctrina SANA; 8 defectos, cerrados antes de anclar los 6 que eran del lote — D-1 código (el catch de `settled` vivía en la rama onFulfilled del trigger: un handler que lanzaba en `pre` dejaba la rechazada del canal visual sin manejar; ahora se engancha donde nace el handle, con test de las tres vías) · D-2/D-3 prosa · D-4/D-5 tests (los cuatro valores del fallback, y el par S5 vuelve a discriminar) · D-8 exactitud (`clear`/`clearTarget` no honran ni cuentan transitorias en vuelo). Segunda sonda de Chrome del adversarial: 11.6 · 11.0 · 5.1 · 42.7 ms. D-6 (alcance): `contracts.test.ts` restaurado a HEAD (su diff era 100 % formateo); el reformateo prettier de los docs tocados se queda. D-7 PREEXISTENTE, fila nombrada: `dispose()` durante una ocurrencia en vuelo repuebla `active` después y deja proyección residual (engine.ts, `finally` + `dispose`). Pendiente del autor: mirar el cierre de Dialog y Toast en un Chrome que PINTE (los panes de los agentes estaban ocultos: mutaciones y estilo computado medidos, frame no visto). Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
3 weeks ago
- **`semantic.sequence`** — the ORDER between the perceptual signal and the
structural state change, never a wait between them (D-full, 2026-09-15).
Default `'pre'`: the signal goes out first and the mutation follows in the
same tick. Use `'post'` when the celebration belongs after the new state
lands (commit pulses on completed actions, and any event whose target MOUNTS
during the trigger) — there the handler runs, `tick()` flushes, and the signal
stamps the resolved DOM. `'coincident'` is mechanically identical to `'pre'` —
the distinct name DECLARES that signal and mutation are indivisible (drags,
sustain), per the signed equivalence in [`sema.md`](./sema.md) §slider; it is a
semantic marker, not a third runtime timing, and since D-full the word is
literally true.
feat(morfo,sema,eidos): todo evento dice de que familia es, y el cruce por fin se ve El framework llamaba a la misma cosa de dos maneras: `open` pelado en ocho componentes y `emerge-open` en tres. No era estetica — un preset de movimiento engancha el nombre con `^=`, asi que el dialecto pelado no casaba con ninguna firma y simplemente no animaba, sin romper una sola prueba. Los 40 nombres sin prefijo pasan a `{familia}-{verbo}[-{matiz}]`: 256 eventos, 256 con prefijo, 0 ambiguos. El plan decia 36 y decia `handle-drag-start`; eran 40, y el canon (c25) dice que esos verbos son `pick` y `drop` — `handle-pick` y `handle-drop` ya existian en 10 y 4 componentes. `validateMorfo` cierra la puerta: un `events[].name` que no empiece por su familia ahora lanza. Visto fallar antes con un nombre pelado inyectado. Lo que el renombrado destapo, y va aqui tambien: - La receta del splitter enganchaba `commit-resize`, muerto desde `bd2e40366`. No casaba desde mayo y nadie chillo. Reescrita por FAMILIA, como slider y knob, y `eidos-lint` valida ahora el VALOR de `data-event*` contra el catalogo de morfos — el guard que lo habria cazado en su dia. - La familia `shift` era muda en el canal visual, contra su propia doctrina (c27: el cruce debe percibirse; c34 tipifica el «shift invisible»). Su mapa ya describia la firma que le faltaba y su sonido por defecto es `slide`. Ahora tiene firma direccional: sexto atributo del sello (`data-event-direction`, `forward`|`backward`, por emision) y deslizamiento de 320ms RTL-safe por `:dir()`. Medido: LTR -30px/+30px, RTL los invierte. - El sello de `shift-navigate` pasa del BOTON al `grid` en los cuatro calendarios. Medido: el boton recibia `contact-activate` y 8,5 ms despues —media trama— el `shift-navigate` pisaba la misma ranura y el `press-squeeze` moria sin pintar un fotograma. Una superficie, una ranura (A-36). - 101 contradicciones docs<->morfo adjudicadas con evidencia (git log, docs de decision, el componente vivo). Las docs desfasadas, corregidas; los nueve DEFECTOS de codigo obsoleto quedan abiertos y sin tocar. - `SoundDirection` -> `SoundContour`: era un contorno de tono, no un sentido, y habia tres cosas distintas deletreadas «direction». check en su linea base con 0 errores nuevos por diferencia de conjuntos · docs:check 0/0 · eidos-lint invalid 0 · el censo y las escenas de navegador medidas con raton real y rAF vivo. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2 months ago
**Not declarable here: `direction`.** The morfo cannot state the sense of a
traversal, because the same declared event goes backward on one press and
forward on the next — only the emitter knows which. It travels per-call as
`TriggerOptions.direction` (`forward` | `backward`, the `SemaDirection`
vocabulary) and lands as `data-event-direction`, which is what lets the `shift`
family's motion firma slide in the right sense. Omit it where the route has no
clear sense — a month picked from a select is a jump, not a step, and a sense
inferred from comparing dates is not a sense. Contrast with `intent`, the other
per-emission axis, which the morfo CAN declare a default for.
For a comprehensive worked example see the toggle and dialog morfos.
feat(morfo,sema,contracts): M1(i) — el contrato dice QUIEN dispara cada evento (D.2) El morfo no podia decir quien emite un evento declarado: todo se presumia del runtime, y la verdad de las excepciones vivia fuera del contrato — en la lista de deuda del guard D9 y en las excepciones del censo de packs. Las tablas de eventos de los README prometian percepcion que no ocurre (la ficha M1, P1). Ejecuta la decision firmada D.2 (IMPLEMENTATION_CONTRACT): - `emission: 'runtime' | 'host' | 'external' | 'declared-only'` en MorfoEvent + literal en el schema sium. Ausente = 'runtime': las ~250 declaraciones existentes no se tocan y la presuncion sigue siendo la norma. - El guard D9 exime por DECLARACION en vez de por lista: solo los 'runtime' exigen emisor. Y si un evento eximido sigue en INERT_EVENT_DEBT, el guard FALLA con «FIXED — remove it», para que la deuda no sobreviva a su resolucion. - El pack-census gana el chequeo simetrico: una regla cuyo alcance son SOLO eventos 'declared-only' afina una percepcion que jamas estampara — muerta por definicion, sin lista de excepciones. 'host' NO cuenta como muerto: el anfitrion dispara por el runtime y la regla casa normal. Fixture con los dos casos (positivo y negativo con hermano runtime al alcance). - `morfo.md` §Step 5.5 lleva el apendice tecnico en los terminos que D.2 exige: la tabla de los cuatro valores × sus guards, y la nota doctrinal de por que NO es doctrina del libro — marcar un evento inerte como declared-only para callar al guard es ensanchar la deuda con otro nombre. La sonda temporal (flag sobre tooltip.emerge-present → correr → revertir) destapo un agujero real y lo cerro: el flag entre `name` y `semantic` hacia INVISIBLE el evento al regex del guard — ni censado ni exento. El regex tolera ahora la linea opcional, y la sonda termino dando la conducta disenada exacta: exencion + exigencia de limpiar la deuda. NADIE estrena el flag: los 7 de INERT_EVENT_DEBT quedan intactos y son la parte (ii) — siete decisiones perceptuales del autor, una a una (la de tooltip: ¿el componente mas ubicuo merece firma, o silencio declarado?). La instancia original de la ficha (los 5 pickers con close inerte) esta muerta desde la normalizacion de nombres. Verificado: suites 945 ✓ / 6 ajenos · docs 0/624 · check 69 = base aislada, diff VACIO · prettier: mis dos limpios siguen limpios, pack-census formateado (regresion mia), contracts y morfo.md ya fallaban en HEAD. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2 months ago
**`emission` — who fires the event (technical appendix; D.2, signed).** By
default a declared event is presumed emitted by this repo's providers
(`emission: 'runtime'`, the norm — the field is simply absent), and the D9
contract guard fails when none does. Three declared exceptions exist, and they
are metadata about the EMITTER, never a license to declare perception that
does not happen:
docs(uix): la doctrina alcanza a la bolsa única — el corpus deja de enseñar la API muerta La clase exacta que la auditoría flagueó (doc↔código), producida esta vez por NUESTROS propios cambios de P0 fase C y sin corregir hasta ahora: - component-guide §«two sanctioned ways»: reescrita a UNA vía (la bolsa .props resuelve el contrato entero; extras solo lo que el morfo no puede expresar; valores CRUDOS; la inversa «morfo sin value ⇒ soma escribe»). Los 4 snippets con syncAttrs: true, limpiados. - soma-architecture: partProps re-descrito como LA bolsa (identidad + contrato completo, SSR incluido); el párrafo del flag sustituido por «el único escritor imperativo es el prewrite»; snippet limpiado. - soma.md: snippet de accordion sin el flag. - morfo.md §naming: el «(SSR included)» dejó de ser contraste — desde la tubería única TODO plan viaja en la bolsa; lo que distingue a los naming es la PRECEDENCIA (consumer-first via mergeProps). - testing-and-tooling: la postura SSR gana el canario ssr-contract.test, y nace «The gate» (check:gate + ledger menguante, gate, hook — los instrumentos de fase B no estaban documentados). - CLAUDE.md: el paso 4 de trigger ya no dice «effect-driven» (prettier normalizó de paso el fichero entero — solo whitespace, contenido intacto, declarado aquí para que el diff ancho no sea silencioso). Lo histórico (changelog, old-deprecated, audit de julio, process/) se queda como historia. Verificación: docs:check 0 errores sobre 817 docs; cero menciones normativas de la API muerta fuera de referencias históricas. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
1 month ago
| Value | Who fires it | Guards |
| ----------------------------- | --------------------------------------------------------------------- | ----------------------------------------------------------------------- |
| `'runtime'` (default, absent) | a soma provider / eidos view via `runtime.trigger` | D9 requires a live emitter |
| `'host'` | the embedding application, through the runtime | D9 exempt; pack rules stay LIVE (the stamp happens) |
| `'external'` | an outside system consumes the declaration (analytics, external a11y) | D9 exempt; nothing stamps here |
| `'declared-only'` | nobody — contract surface for testing / docs | D9 exempt; a pack rule tuning it is DEAD and the pack census rejects it |
feat(morfo,sema,contracts): M1(i) — el contrato dice QUIEN dispara cada evento (D.2) El morfo no podia decir quien emite un evento declarado: todo se presumia del runtime, y la verdad de las excepciones vivia fuera del contrato — en la lista de deuda del guard D9 y en las excepciones del censo de packs. Las tablas de eventos de los README prometian percepcion que no ocurre (la ficha M1, P1). Ejecuta la decision firmada D.2 (IMPLEMENTATION_CONTRACT): - `emission: 'runtime' | 'host' | 'external' | 'declared-only'` en MorfoEvent + literal en el schema sium. Ausente = 'runtime': las ~250 declaraciones existentes no se tocan y la presuncion sigue siendo la norma. - El guard D9 exime por DECLARACION en vez de por lista: solo los 'runtime' exigen emisor. Y si un evento eximido sigue en INERT_EVENT_DEBT, el guard FALLA con «FIXED — remove it», para que la deuda no sobreviva a su resolucion. - El pack-census gana el chequeo simetrico: una regla cuyo alcance son SOLO eventos 'declared-only' afina una percepcion que jamas estampara — muerta por definicion, sin lista de excepciones. 'host' NO cuenta como muerto: el anfitrion dispara por el runtime y la regla casa normal. Fixture con los dos casos (positivo y negativo con hermano runtime al alcance). - `morfo.md` §Step 5.5 lleva el apendice tecnico en los terminos que D.2 exige: la tabla de los cuatro valores × sus guards, y la nota doctrinal de por que NO es doctrina del libro — marcar un evento inerte como declared-only para callar al guard es ensanchar la deuda con otro nombre. La sonda temporal (flag sobre tooltip.emerge-present → correr → revertir) destapo un agujero real y lo cerro: el flag entre `name` y `semantic` hacia INVISIBLE el evento al regex del guard — ni censado ni exento. El regex tolera ahora la linea opcional, y la sonda termino dando la conducta disenada exacta: exencion + exigencia de limpiar la deuda. NADIE estrena el flag: los 7 de INERT_EVENT_DEBT quedan intactos y son la parte (ii) — siete decisiones perceptuales del autor, una a una (la de tooltip: ¿el componente mas ubicuo merece firma, o silencio declarado?). La instancia original de la ficha (los 5 pickers con close inerte) esta muerta desde la normalizacion de nombres. Verificado: suites 945 ✓ / 6 ajenos · docs 0/624 · check 69 = base aislada, diff VACIO · prettier: mis dos limpios siguen limpios, pack-census formateado (regresion mia), contracts y morfo.md ya fallaban en HEAD. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2 months ago
Doctrinal note (the reason this is an appendix and not book doctrine, per
D.2): presented wrong, this reads as "the system promises events that do not
exist". It is the opposite — the flag makes the emitter explicit so the
README event tables stop promising perception that never occurs, and the
guard can tell a declared surface from an abandoned one. Marking an inert
event `'declared-only'` to silence the guard is the same move as widening its
debt list, and the review treats it the same way.
### Step 6 — Wire the provider
In `src/uix/soma/components/{kebab}/{kebab}-provider.svelte.ts`:
```ts
import { createAttrs } from '$uix/morfo';
import { dialogMorfo } from '../../../morfo/components/dialog';
const attrs = createAttrs(dialogMorfo);
// attrs is typed as
// {
// readonly provider: 'data-dialog';
// readonly trigger: 'data-dialog-trigger';
// readonly content: 'data-dialog-content';
// readonly overlay: 'data-dialog-overlay';
// readonly title: 'data-dialog-title';
// readonly description: 'data-dialog-description';
// readonly close: 'data-dialog-close';
// }
// — literal-typed from the morfo's `as const` shape.
```
Runtime-based providers call `createSomaRuntime(morfo, sources)` or
`soma.runtime(morfo, sources)`. That path calls `registerMorfo(morfo)` for
you, which compiles the morfo and registers the data contract. The strings
catalog (`componentLangs` + `commonLangs`) is registered globally by
`ActiveUix` at boot — morfo no longer publishes strings dynamically.
Only call `registerMorfo(morfo)` manually when a tool or legacy provider
needs the registry side effect without creating a runtime.
That's it. No more inline attr maps, no more manual contract objects. A typo like
`attrs.trigerr` or `attrs.content-wrong` is a compile error, not a runtime
silent-undef.
---
## Validation
Three layers catch three classes of drift:
### 1. Sium schema (build / dev)
`src/uix/morfo/schema.ts` exports `validateMorfo(morfo)` which:
- Checks shape: types, enum discriminators, literal unions.
- Checks cross-field invariants:
- Every part `kebab` is unique in the morfo.
- Every `partRef.target` resolves to an existing part.
- Every `stateRef.state` exists in the containing part's `states[]`.
- Every relative `translationRef('label')` is normalized at compile time
to `#?components.{kebab}.label`. Catalog presence is enforced by
`npm run translations:check`, not by the schema itself.
- Every `state-equals` condition's `state` exists in the containing part.
- `focus.initial.partRef` / `focus.return.partRef` resolve.
- `scope` is non-empty.
- `kebab` matches `/^[a-z][a-z0-9-]*$/`.
Run a morfo through this to catch authoring errors early. See [`components/dialog.test.ts`](../../src/uix/morfo/components/dialog.test.ts) for a reference test.
### 2. Strict mode in `assertContract` (dev runtime)
The provider's `assertProps` walks the emitted data-attrs and checks their values against the registered contract. In dev mode, a value not in the declared `values[]` logs a warning:
```
[soma] dialog.content: "data-state" has value "opening" but contract expects one of: open, closed
```
### 3. Smoke + morfo-check (CI)
Two npm scripts exercise the UI shell and, historically, morfos against the real DOM:
- `npm run smoke` — Playwright walks concrete `+page.svelte` routes under
`web/routes`. Catches `pageerror`, `console.error`, same-origin request
failures, translation-key-not-found, context-not-found and rendered
`__uix_lang_missing__` fallbacks. Not morfo-specific but catches common
regressions. Set `SMOKE_SCOPE=/uix` to restrict the run to the UIX shell.
- `npm run morfo:check` — DOM validator for morfos that have a routed demo
under `/uix/components/{kebab}`. Morfos without a current routed demo are
reported as `SKIP`; they are not treated as failures. Override the prefix
with `MORFO_ROUTE_PREFIX=/some/path` if a local docs shell maps morfos
elsewhere. Its contract:
- Every declared data-attr with `severity: 'required'` is emitted.
- Every emitted data-attr value matches `values[]` if declared.
- No undeclared `data-{component}-*` attrs are emitted (except `data-_*` private
provider state, which is outside morfo).
- `npm run morfo:vocabulary` — Flags `data-state` enums that diverge from canonical vocabularies (`open|closed`, `active|inactive`, `checked|unchecked|indeterminate`, etc.). WARN-level; novel vocabularies may be legitimate but should be reviewed.
Both scripts require `npm run dev` running in another terminal.
---
## The data-attr convention
`createAttrs(morfo)` derives data-attr names from parts:
| Part kebab | Emitted attr |
| -------------- | --------------------------------------------- |
| `'provider'` | `data-{component}` — no suffix (orchestrator) |
| `'trigger'` | `data-{component}-trigger` |
| `'item-group'` | `data-{component}-item-group` |
Never `data-soma-*`, never `data-eidos-*` — always `data-{component}[-{part}]`.
Private attrs for internal debug / state use the reserved `data-_*` prefix and are
intentionally **outside** morfo. `validateMorfo()` rejects `data-_*` in a morfo
declaration; strict-mode tooling skips provider-private attrs when scanning the
real DOM.
---
## Handling polymorphism (consumer renders a different element)
The `child` snippet pattern allows consumers to swap the default element:
```svelte
<Dialog.Trigger>
{#snippet child({ props })}
<a href="/about" {...props}>About</a>
{/snippet}
</Dialog.Trigger>
```
Morfo's `defaultElement` is **advisory** — the provider doesn't enforce it. What IS guaranteed is `role`: the provider always emits the explicit role (e.g. `role="button"` on a Trigger even though `<button>` has it implicitly). When the consumer renders as `<a>`, the role stays correct.
Keyboard handlers should also be element-agnostic: emit `onkeydown` that handles both Enter and Space for "activate" regardless of the underlying element, since `<a>` only handles Enter natively and `<div>` handles neither.
---
## Sema alignment
[Sema](./sema.md) is the perceptual/semantic layer. It consumes the same DOM surface that morfo declares — no extra hooks needed. The morfo authoring rules that support Sema:
- **Transition markers**: components with enter/exit transitions declare `data-starting-style` and `data-ending-style` on the transitioning part.
- **Causal exit states**: components with multiple semantically distinct exit paths (Dialog: saved / cancelled / dismissed / failed; Toast: dismissed / auto-timeout / action) declare `data-last-action` with enumerable `values`. The provider is expected to update `data-last-action` **before** `data-state` changes, so Sema can tint the exit animation per-action. (Tracked by a dedicated MutationObserver timing test — future work.)
- **Cross-component vocabulary consistency**: the `morfo:vocabulary` script groups components by data-attr semantic (disclosure → `open|closed`, lifecycle → `loading|idle|success|error`) and flags divergent vocabularies for review.
These don't change the morfo shape — they're authoring conventions that enable Sema without requiring a Sema-aware provider.
---
feat(morfo,sema,eidos): todo evento dice de que familia es, y el cruce por fin se ve El framework llamaba a la misma cosa de dos maneras: `open` pelado en ocho componentes y `emerge-open` en tres. No era estetica — un preset de movimiento engancha el nombre con `^=`, asi que el dialecto pelado no casaba con ninguna firma y simplemente no animaba, sin romper una sola prueba. Los 40 nombres sin prefijo pasan a `{familia}-{verbo}[-{matiz}]`: 256 eventos, 256 con prefijo, 0 ambiguos. El plan decia 36 y decia `handle-drag-start`; eran 40, y el canon (c25) dice que esos verbos son `pick` y `drop` — `handle-pick` y `handle-drop` ya existian en 10 y 4 componentes. `validateMorfo` cierra la puerta: un `events[].name` que no empiece por su familia ahora lanza. Visto fallar antes con un nombre pelado inyectado. Lo que el renombrado destapo, y va aqui tambien: - La receta del splitter enganchaba `commit-resize`, muerto desde `bd2e40366`. No casaba desde mayo y nadie chillo. Reescrita por FAMILIA, como slider y knob, y `eidos-lint` valida ahora el VALOR de `data-event*` contra el catalogo de morfos — el guard que lo habria cazado en su dia. - La familia `shift` era muda en el canal visual, contra su propia doctrina (c27: el cruce debe percibirse; c34 tipifica el «shift invisible»). Su mapa ya describia la firma que le faltaba y su sonido por defecto es `slide`. Ahora tiene firma direccional: sexto atributo del sello (`data-event-direction`, `forward`|`backward`, por emision) y deslizamiento de 320ms RTL-safe por `:dir()`. Medido: LTR -30px/+30px, RTL los invierte. - El sello de `shift-navigate` pasa del BOTON al `grid` en los cuatro calendarios. Medido: el boton recibia `contact-activate` y 8,5 ms despues —media trama— el `shift-navigate` pisaba la misma ranura y el `press-squeeze` moria sin pintar un fotograma. Una superficie, una ranura (A-36). - 101 contradicciones docs<->morfo adjudicadas con evidencia (git log, docs de decision, el componente vivo). Las docs desfasadas, corregidas; los nueve DEFECTOS de codigo obsoleto quedan abiertos y sin tocar. - `SoundDirection` -> `SoundContour`: era un contorno de tono, no un sentido, y habia tres cosas distintas deletreadas «direction». check en su linea base con 0 errores nuevos por diferencia de conjuntos · docs:check 0/0 · eidos-lint invalid 0 · el censo y las escenas de navegador medidas con raton real y rAF vivo. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2 months ago
## Where the stamp lands — gesture vs terminal
`semantic.target` is a claim about **subject**, not about paint: the part it names is the one the occurrence is _about_. Read across the catalogue, that claim resolves into a single rule in two halves:
> **The gesture is stamped where the hand is. The terminal is stamped where the value lives.**
Seventeen components declare the `handle` family. Twelve put the two halves on different parts:
| Component | Grip (`handle-*`) | Terminal |
| ------------------------------- | ------------------------------------ | --------------------------------------- |
| `rotate-align` | `needle` | `dial` (`handle-drop`) |
| `path-trace` | `token` | `track` (`handle-drop`) |
| `drag-drop` | `draggable` | `droppable` (`handle-drop`) |
| `splitter` | `resize-trigger` | `provider` (`commit-set`) |
| `color-picker` | `area` | `provider` |
| `css-field` · `number-field` | `scrubber` | `provider` |
| `gradient-builder` | `track` | `provider` |
| `cropper` | `selection` · `handle` · `viewport` | `provider` (`commit-crop`) |
| `image-picker` | `preview` | `provider` |
| `virtual-list` · `virtual-grid` | `viewport` | `provider` (`commit-set-resize`) |
| `chronos` | `event-chip` · `event-resize-handle` | `event-chip` (falls back to `provider`) |
| `knob` | `control` | `control` |
| `drawer` · `float-panel` | `content` | `content` |
| `slider` | `provider` | `provider` |
The five that do not separate them are not exceptions — they are the components where grip and value are the **same node**: Chronos' chip _is_ the event, Knob's control _is_ the dial, Drawer's and FloatPanel's content _is_ the position. Slider is the borderline case: it declares a `thumb` part and the provider gates `handle-pick` on it (`isHandleTarget` in `slider-provider.svelte.ts`), but the pointer capture and the whole grabbable track belong to the provider, so the provider is the surface under the hand.
Two consequences worth naming:
- **The terminal is not a synonym for `commit`.** Rotate-align, path-trace and drag-drop terminate on a `handle-drop`. The family says what kind of occurrence it is; the target says whose.
- **A signature that must paint a node other than the stamped one is a descendant selector, never a reason to move the stamp.** The corollary and its worked example live in [`architecture/eidos.md`](./eidos.md) §From sema (DOM).
When a rule doesn't fire, the diagnostic question is always: **is the stamped node the subject of the event?** If it is, the recipe descends. If it isn't, the morfo is wrong.
feat(morfo,soma,eidos): el cruce de mes lo sella la vista paginada, no un mes suelto Con `numberOfMonths=2` el calendario tenia un *shift invisible* a medias: al pulsar «siguiente», junio cambiaba sus fechas quieto y julio cruzaba. Medido — un solo sello, sobre el segundo grid. La causa no es el runtime ni el llamador. `grid` es una parte REPETIDA, y un emit sin ancla resuelve a la instancia viva mas reciente (`resolveEmitTarget`). `shift-navigate` apuntaba a `grid` desde el 2026-08-11, cuando se le saco del boton para que dejara de pisar su `contact-activate` (A-36). Era correcto para un mes y falso para dos, y un destino que solo vale para un valor del parametro no es un destino. La ley que este mismo eje escribio ya traia la respuesta (`morfo.md` §Where the stamp lands): *if it isn't the subject, the morfo is wrong*. El sujeto es la VISTA PAGINADA — lo que cruza como unidad bajo cualquier numero de meses. No existia como parte, asi que se declara: `months`, arquetipo `viewport`, con `targetFallback: [grid]` para que una composicion headless sin ella degrade a lo de antes en vez de callarse. Se descartaron tres alternativas, y por que ------------------------------------------- - Emitir N veces ancladas: acuna N ocurrencias para UN gesto, N ids en el arbitro de dominancia y una repeticion en la memoria de frecuencia (C-2 solo exime a `handle`), y luego hay que silenciar N-1 desde el componente. - Un emit con N sellos: `resolveEmitTarget` es UNA resolucion para TRES lectores «so they can never disagree» — y el movimiento de foco a11y necesita uno. Ademas declararia que cruzaron dos cosas; el cap. 27 dice que cruzo una. - Volver al provider con CSS descendiente a mano: el provider no es el sujeto (la cabecera no se mueve), deshace una correccion medida y duplica el shorthand de animacion en cuatro recetas. De paso, dos partes clandestinas legalizadas -------------------------------------------- `data-calendar-month-panel` lo fabricaban a mano los cuatro demos multi-mes y `calendar.css` lo estilizaba igualmente, sin que ningun morfo lo declarara; el contenedor lo montaban con un `style` en linea que rederivaba el numero de meses en cada pagina, o con clases locales (`.range-months`, `.month-stack`) que apuntaban a un `--calendar-month-gap` que NO EXISTE — el fallback `36px`/`32px` hacia todo el trabajo. Ahora son partes, la disposicion vive en la receta (`grid-auto-flow: column`, sin contar meses) y el token es `--calendar-months-gap`. Correccion de lo que dije al proponerlo: `eidos-lint` NO cazaba esa deriva — clasificaba el selector como `eidos-only`, no como invalido. Lo que gana el cambio es mover 2 selectores de eidos-only a morfo-backed. Quien si lo caza es `morfo:check`, contra el DOM real. Medido ------ Navegador, `numberOfMonths=2`: el sello cae en `[data-calendar-months]` con `data-event-direction=forward`, corre `shift-cross-forward 0.32s`, y los DOS grids se desplazan 10 px a los 40 ms del cruce (antes: uno). En RTL, `--motion-shift-sign` pasa a -1 y los dos van a -10 px. El boton conserva su `contact-activate` en su propia ranura. Consola limpia en los cuatro demos; el date-range-picker, que muestra dos meses por defecto, es donde mas mordia. `morfo:check` contra el DOM: calendar y range-calendar PASS. `eidos-lint`: invalid 0 en ambos, morfo-backed 27→29 y 39→41. `docs:check` 0/623. Verificado sobre el ARBOL INDEXADO en un worktree aparte, no sobre el mio: `check` 57 errores, identico a HEAD, cero nuevos; 2154/2154 en morfo + soma + eidos + sema. Los 6 fallos de `contracts.test.ts` que se ven en mi arbol de trabajo son de la otra sesion (waveform, media-player, audio-player, menubar, aura, radio-group, tabs): en el arbol firmado pasan. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2 months ago
### A repeated part cannot be the subject of one occurrence
feat(soma,morfo)!: targetOverride BORRADO — el estado ilegal del eje es inexpresable El endgame que §3.0 firmo al abrir F3: con el censo a 0, la opcion de elemento anonimo sale de TriggerOptions, y con ella assertTargetOverride (existia solo para vigilarla; queda assertAnchor, el guard de la puerta que la sustituye). Las dos resoluciones `opts.targetOverride ?? resolveEmitTarget(...)` (emit y foco a11y) quedan en la resolucion unica, y los Omit<..., 'targetOverride'> de los triggers anclados se vuelven vacuos y desaparecen. El tipo lleva LAPIDA doctrinal: la opcion nacio fallbackTarget, se renombro el 2026-08-10 como precondicion del censo, y compensaba un registro sin identidad de instancia — es lo que dejo a tres auditorias (fable S1, sema S-17, blocks A-36/A-65) encontrar la misma deriva sin poder cerrarla. No volver a añadir una opcion de elemento. Y la tesis se probo sola al ejecutarla: el compilador, ya como censo, cazo 9 usos en TESTS que el grep del scratch nunca escaneo. De ellos, 3 eran redundantes (la parte registraba el mismo elemento), 2 del test de metrics migran al registro + trigger plano, y 4 fijaban la conducta borrada — el describe «targetOverride contract» entero y el test del override preferente, retirados con lapida; el guard A-36 de los overlays (morfo sin redireccion + post) se conserva, des-anidado. El scratch del censo, retirado: el censo es `npm run check`. Docs vivos adjudicados: morfo.md §allowedTargets describe la puerta anclada (EventNameTargeting + assertAnchor) y §repeated-part nombra partInstance donde decia targetOverride; los docblocks de las 3 factorias de vistas de eidos dicen la forma nueva; errors.ts deja de ofrecer la opcion muerta como remedio. Las menciones historicas (A-36, «the retired...») se conservan como historia. Verificado: soma navegador 1278✓/1 (el timeout ajeno preexistente de soma-attr-audit, documentado en la base) · runtime+metrics 53/53 · check 69 = base, 0 propios · docs:check 0/623 · prettier limpio en lo que estaba limpio. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2 months ago
`resolveEmitTarget` resolves an unanchored emit to the declared target's **newest live instance**. That is exactly right when the occurrence has one subject among many — one row selected, one day, one item — and the framework names it with an ANCHORED emit: the part's own handle (`runtimePart.trigger`, as `table` does for its row) or a registry-identity lookup (`runtime.partInstance('day', el).trigger`, as `calendar` does for its pressed day). Fifty of the catalogue's 252 partRef targets are repeatable parts, and every one of them works this way.
feat(morfo,soma,eidos): el cruce de mes lo sella la vista paginada, no un mes suelto Con `numberOfMonths=2` el calendario tenia un *shift invisible* a medias: al pulsar «siguiente», junio cambiaba sus fechas quieto y julio cruzaba. Medido — un solo sello, sobre el segundo grid. La causa no es el runtime ni el llamador. `grid` es una parte REPETIDA, y un emit sin ancla resuelve a la instancia viva mas reciente (`resolveEmitTarget`). `shift-navigate` apuntaba a `grid` desde el 2026-08-11, cuando se le saco del boton para que dejara de pisar su `contact-activate` (A-36). Era correcto para un mes y falso para dos, y un destino que solo vale para un valor del parametro no es un destino. La ley que este mismo eje escribio ya traia la respuesta (`morfo.md` §Where the stamp lands): *if it isn't the subject, the morfo is wrong*. El sujeto es la VISTA PAGINADA — lo que cruza como unidad bajo cualquier numero de meses. No existia como parte, asi que se declara: `months`, arquetipo `viewport`, con `targetFallback: [grid]` para que una composicion headless sin ella degrade a lo de antes en vez de callarse. Se descartaron tres alternativas, y por que ------------------------------------------- - Emitir N veces ancladas: acuna N ocurrencias para UN gesto, N ids en el arbitro de dominancia y una repeticion en la memoria de frecuencia (C-2 solo exime a `handle`), y luego hay que silenciar N-1 desde el componente. - Un emit con N sellos: `resolveEmitTarget` es UNA resolucion para TRES lectores «so they can never disagree» — y el movimiento de foco a11y necesita uno. Ademas declararia que cruzaron dos cosas; el cap. 27 dice que cruzo una. - Volver al provider con CSS descendiente a mano: el provider no es el sujeto (la cabecera no se mueve), deshace una correccion medida y duplica el shorthand de animacion en cuatro recetas. De paso, dos partes clandestinas legalizadas -------------------------------------------- `data-calendar-month-panel` lo fabricaban a mano los cuatro demos multi-mes y `calendar.css` lo estilizaba igualmente, sin que ningun morfo lo declarara; el contenedor lo montaban con un `style` en linea que rederivaba el numero de meses en cada pagina, o con clases locales (`.range-months`, `.month-stack`) que apuntaban a un `--calendar-month-gap` que NO EXISTE — el fallback `36px`/`32px` hacia todo el trabajo. Ahora son partes, la disposicion vive en la receta (`grid-auto-flow: column`, sin contar meses) y el token es `--calendar-months-gap`. Correccion de lo que dije al proponerlo: `eidos-lint` NO cazaba esa deriva — clasificaba el selector como `eidos-only`, no como invalido. Lo que gana el cambio es mover 2 selectores de eidos-only a morfo-backed. Quien si lo caza es `morfo:check`, contra el DOM real. Medido ------ Navegador, `numberOfMonths=2`: el sello cae en `[data-calendar-months]` con `data-event-direction=forward`, corre `shift-cross-forward 0.32s`, y los DOS grids se desplazan 10 px a los 40 ms del cruce (antes: uno). En RTL, `--motion-shift-sign` pasa a -1 y los dos van a -10 px. El boton conserva su `contact-activate` en su propia ranura. Consola limpia en los cuatro demos; el date-range-picker, que muestra dos meses por defecto, es donde mas mordia. `morfo:check` contra el DOM: calendar y range-calendar PASS. `eidos-lint`: invalid 0 en ambos, morfo-backed 27→29 y 39→41. `docs:check` 0/623. Verificado sobre el ARBOL INDEXADO en un worktree aparte, no sobre el mio: `check` 57 errores, identico a HEAD, cero nuevos; 2154/2154 en morfo + soma + eidos + sema. Los 6 fallos de `contracts.test.ts` que se ven en mi arbol de trabajo son de la otra sesion (waveform, media-player, audio-player, menubar, aura, radio-group, tabs): en el arbol firmado pasan. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2 months ago
It breaks when one occurrence has **N subjects at once**. `calendar.shift-navigate` targeted `grid`; with `numberOfMonths=2` there are two grids and both cross, but only the newest was stamped — measured 2026-08-12: June changed its dates standing still while July crossed. Neither mechanism helps, because both pick one instance, and neither does emitting N times: that mints N occurrences for one gesture, N ids in the dominance arbiter, and a repeat in the frequency memory (C-2 exempts only `handle`).
The fix is not to administer the stamp. **It is to declare the part that is actually the subject** — here `months`, the paginated view, which crosses as a unit under every value of `numberOfMonths`. A target that is only correct for one parameter value is not a target.
So the diagnostic has a second half: **if the target is a repeated part, ask whether the occurrence has one subject or all of them.** One → anchor it. All → the subject is the container, and it needs to exist.
feat(morfo,sema,eidos): todo evento dice de que familia es, y el cruce por fin se ve El framework llamaba a la misma cosa de dos maneras: `open` pelado en ocho componentes y `emerge-open` en tres. No era estetica — un preset de movimiento engancha el nombre con `^=`, asi que el dialecto pelado no casaba con ninguna firma y simplemente no animaba, sin romper una sola prueba. Los 40 nombres sin prefijo pasan a `{familia}-{verbo}[-{matiz}]`: 256 eventos, 256 con prefijo, 0 ambiguos. El plan decia 36 y decia `handle-drag-start`; eran 40, y el canon (c25) dice que esos verbos son `pick` y `drop` — `handle-pick` y `handle-drop` ya existian en 10 y 4 componentes. `validateMorfo` cierra la puerta: un `events[].name` que no empiece por su familia ahora lanza. Visto fallar antes con un nombre pelado inyectado. Lo que el renombrado destapo, y va aqui tambien: - La receta del splitter enganchaba `commit-resize`, muerto desde `bd2e40366`. No casaba desde mayo y nadie chillo. Reescrita por FAMILIA, como slider y knob, y `eidos-lint` valida ahora el VALOR de `data-event*` contra el catalogo de morfos — el guard que lo habria cazado en su dia. - La familia `shift` era muda en el canal visual, contra su propia doctrina (c27: el cruce debe percibirse; c34 tipifica el «shift invisible»). Su mapa ya describia la firma que le faltaba y su sonido por defecto es `slide`. Ahora tiene firma direccional: sexto atributo del sello (`data-event-direction`, `forward`|`backward`, por emision) y deslizamiento de 320ms RTL-safe por `:dir()`. Medido: LTR -30px/+30px, RTL los invierte. - El sello de `shift-navigate` pasa del BOTON al `grid` en los cuatro calendarios. Medido: el boton recibia `contact-activate` y 8,5 ms despues —media trama— el `shift-navigate` pisaba la misma ranura y el `press-squeeze` moria sin pintar un fotograma. Una superficie, una ranura (A-36). - 101 contradicciones docs<->morfo adjudicadas con evidencia (git log, docs de decision, el componente vivo). Las docs desfasadas, corregidas; los nueve DEFECTOS de codigo obsoleto quedan abiertos y sin tocar. - `SoundDirection` -> `SoundContour`: era un contorno de tono, no un sentido, y habia tres cosas distintas deletreadas «direction». check en su linea base con 0 errores nuevos por diferencia de conjuntos · docs:check 0/0 · eidos-lint invalid 0 · el censo y las escenas de navegador medidas con raton real y rAF vivo. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2 months ago
---
## Typed selector builder — `semaSelector`
When a TypeScript consumer needs to construct a CSS selector that targets the morfo's emitted attrs (e.g. `sema/components/*.ts` cascade rules), it MUST use [`semaSelector`](../../src/uix/morfo/selectors.ts) instead of hand-writing strings:
```ts
import { semaSelector } from '$uix/morfo';
import { dialogMorfo } from '$uix/morfo/components/dialog';
// [data-dialog-content][data-event-family="commit"]
semaSelector(dialogMorfo, 'content', { eventFamily: 'commit' });
feat(morfo,sema,eidos): todo evento dice de que familia es, y el cruce por fin se ve El framework llamaba a la misma cosa de dos maneras: `open` pelado en ocho componentes y `emerge-open` en tres. No era estetica — un preset de movimiento engancha el nombre con `^=`, asi que el dialecto pelado no casaba con ninguna firma y simplemente no animaba, sin romper una sola prueba. Los 40 nombres sin prefijo pasan a `{familia}-{verbo}[-{matiz}]`: 256 eventos, 256 con prefijo, 0 ambiguos. El plan decia 36 y decia `handle-drag-start`; eran 40, y el canon (c25) dice que esos verbos son `pick` y `drop` — `handle-pick` y `handle-drop` ya existian en 10 y 4 componentes. `validateMorfo` cierra la puerta: un `events[].name` que no empiece por su familia ahora lanza. Visto fallar antes con un nombre pelado inyectado. Lo que el renombrado destapo, y va aqui tambien: - La receta del splitter enganchaba `commit-resize`, muerto desde `bd2e40366`. No casaba desde mayo y nadie chillo. Reescrita por FAMILIA, como slider y knob, y `eidos-lint` valida ahora el VALOR de `data-event*` contra el catalogo de morfos — el guard que lo habria cazado en su dia. - La familia `shift` era muda en el canal visual, contra su propia doctrina (c27: el cruce debe percibirse; c34 tipifica el «shift invisible»). Su mapa ya describia la firma que le faltaba y su sonido por defecto es `slide`. Ahora tiene firma direccional: sexto atributo del sello (`data-event-direction`, `forward`|`backward`, por emision) y deslizamiento de 320ms RTL-safe por `:dir()`. Medido: LTR -30px/+30px, RTL los invierte. - El sello de `shift-navigate` pasa del BOTON al `grid` en los cuatro calendarios. Medido: el boton recibia `contact-activate` y 8,5 ms despues —media trama— el `shift-navigate` pisaba la misma ranura y el `press-squeeze` moria sin pintar un fotograma. Una superficie, una ranura (A-36). - 101 contradicciones docs<->morfo adjudicadas con evidencia (git log, docs de decision, el componente vivo). Las docs desfasadas, corregidas; los nueve DEFECTOS de codigo obsoleto quedan abiertos y sin tocar. - `SoundDirection` -> `SoundContour`: era un contorno de tono, no un sentido, y habia tres cosas distintas deletreadas «direction». check en su linea base con 0 errores nuevos por diferencia de conjuntos · docs:check 0/0 · eidos-lint invalid 0 · el censo y las escenas de navegador medidas con raton real y rAF vivo. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2 months ago
// [data-dialog-content][data-event="signal-alert-close-fail"]
semaSelector(dialogMorfo, 'content', { eventName: 'signal-alert-close-fail' });
feat(morfo,sema,eidos): todo evento dice de que familia es, y el cruce por fin se ve El framework llamaba a la misma cosa de dos maneras: `open` pelado en ocho componentes y `emerge-open` en tres. No era estetica — un preset de movimiento engancha el nombre con `^=`, asi que el dialecto pelado no casaba con ninguna firma y simplemente no animaba, sin romper una sola prueba. Los 40 nombres sin prefijo pasan a `{familia}-{verbo}[-{matiz}]`: 256 eventos, 256 con prefijo, 0 ambiguos. El plan decia 36 y decia `handle-drag-start`; eran 40, y el canon (c25) dice que esos verbos son `pick` y `drop` — `handle-pick` y `handle-drop` ya existian en 10 y 4 componentes. `validateMorfo` cierra la puerta: un `events[].name` que no empiece por su familia ahora lanza. Visto fallar antes con un nombre pelado inyectado. Lo que el renombrado destapo, y va aqui tambien: - La receta del splitter enganchaba `commit-resize`, muerto desde `bd2e40366`. No casaba desde mayo y nadie chillo. Reescrita por FAMILIA, como slider y knob, y `eidos-lint` valida ahora el VALOR de `data-event*` contra el catalogo de morfos — el guard que lo habria cazado en su dia. - La familia `shift` era muda en el canal visual, contra su propia doctrina (c27: el cruce debe percibirse; c34 tipifica el «shift invisible»). Su mapa ya describia la firma que le faltaba y su sonido por defecto es `slide`. Ahora tiene firma direccional: sexto atributo del sello (`data-event-direction`, `forward`|`backward`, por emision) y deslizamiento de 320ms RTL-safe por `:dir()`. Medido: LTR -30px/+30px, RTL los invierte. - El sello de `shift-navigate` pasa del BOTON al `grid` en los cuatro calendarios. Medido: el boton recibia `contact-activate` y 8,5 ms despues —media trama— el `shift-navigate` pisaba la misma ranura y el `press-squeeze` moria sin pintar un fotograma. Una superficie, una ranura (A-36). - 101 contradicciones docs<->morfo adjudicadas con evidencia (git log, docs de decision, el componente vivo). Las docs desfasadas, corregidas; los nueve DEFECTOS de codigo obsoleto quedan abiertos y sin tocar. - `SoundDirection` -> `SoundContour`: era un contorno de tono, no un sentido, y habia tres cosas distintas deletreadas «direction». check en su linea base con 0 errores nuevos por diferencia de conjuntos · docs:check 0/0 · eidos-lint invalid 0 · el censo y las escenas de navegador medidas con raton real y rAF vivo. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2 months ago
// [data-dialog-content][data-event^="emerge-close"][data-event-family="emerge"]
semaSelector(dialogMorfo, 'content', { eventNamePrefix: 'emerge-close', eventFamily: 'emerge' });
```
### What it guarantees
- **`partKebab`** is typed against `morfo.parts[].kebab`. Renaming a part breaks every consumer at compile-time, not silently in production.
- **`eventName`** is typed against `morfo.events[].name`. Renaming an event has the same compile-time tripwire.
- **`eventFamily` / `eventIntent`** are typed against the canonical unions (`SemaFamily`, `Intent`).
- **Output is plain CSS** — `target.matches(selector)` consumes it unchanged. Zero runtime cost beyond string concatenation.
### What it accepts loose
Plain strings (no compile-time check yet) for:
- **`state` / `aria`** — the data-attr vocabulary is per-component and not yet derived from the morfo's data contract. A future iteration will tighten these too.
- **`ancestor`** — instance / context scoping (`'#delete-confirm-dialog'`, `'[data-form]'`). Ancestors live outside the morfo's contract by design.
- **`pseudo`** — escape hatch for `:hover`, `:focus-visible`, etc.
### When to use it
Any TypeScript / Svelte module that builds a selector pointing at the morfo's emitted attrs:
| Consumer | Status |
| --------------------------------------------------------- | ------------------------------------------------------------------------ |
| `sema/components/*.ts` cascade rules | **MUST use** — hand-written strings are an architecture violation |
| `eidos/components/{x}/*.ts` runtime selector logic (rare) | **MUST use** when targeting morfo-backed attrs |
| Eidos plain `.css` recipes | N/A — CSS files; covered by `scripts/eidos-lint.ts` as opt-in safety net |
| Test assertions / smoke routes | Optional — strings fine, drift is caught by smoke |
The helper is exported from `$uix/morfo`. Implementation lives in [`selectors.ts`](../../src/uix/morfo/selectors.ts).
---
## Files in this package
```
src/uix/morfo/
├── README.md ← (this file) developer guide
├── types.ts ← Morfo interface + v.* builders (pure TypeScript)
├── schema.ts ← sium-based validator + CANONICAL_VOCABULARIES
├── compile.ts ← compileMorfo(morfo) → CompiledMorfo (cached by WeakMap)
├── resolver.ts ← attr resolver — pure, no DOM, no reactivity
├── create-attrs.ts ← derive data-* attr names from morfo.parts
├── contracts.ts ← runtime contract registry + assertContract
├── registry.ts ← registerMorfo + morfo-owned langs registration hooks
├── selectors.ts ← typed selector builder for sema cascade rules
├── PERMUTATION_RUNNER.md ← CI tool spec for state-space validation
├── index.ts ← package barrel
└── components/
├── dialog.ts ← one morfo per component
├── accordion.ts
├── ...
└── dialog.test.ts ← reference test pattern
```
Morfo is **pure declarative TypeScript**. No Svelte runes, no `.svelte.ts`
files, no imports of `$uix/sema` / `$adom` / `$libs/reactive`. The
runtime that interprets a compiled morfo lives in soma — see
[`../soma/runtime.svelte.ts`](../../src/uix/soma/runtime.svelte.ts).
---
## Commands
docs(uix): tanda 2 del contraste con el corpus — el modelo de los capitulos alcanza a la tuberia unica La lectura completa del corpus (6 lectores Opus, 6/6 censos, ~4,5M tokens) destapo lo que la tanda 1 no vio, y esta tanda lo cierra: - El modelo «Effects sync attrs» — repetido VERBATIM en cuatro capitulos (morfo, soma-architecture, overview, active-architecture), en sus tablas de piezas, en los cuatro pasos de trigger y en las dos frases-resumen — muere: la pieza es la bolsa de render; ADom aplica solo el prewrite. - morfo.md: partProps re-descrito (bolsa completa), la cadena causal de commits, y las filas de la tabla de ejecutores (data/aria/role → bolsa). - coincident: los DOS docstrings que ensenaban «in-flight» (types.ts y morfo.md) se alinean con la equivalencia FIRMADA de sema.md §slider («emit-then-handler, like pre; declared indivisible») — el hallazgo del informe queda refutado como defecto de runtime y reducido a esto. - glossary: el kind fantasma `internal` (la clase exacta que docs-check:422 mata y su regex no ve en tablas markdown) → `public|private|virtual` real. - El gate entra en la doctrina: check:gate/gate en el loop de verificacion (testing-and-tooling y getting-started), eidos:lint como script npm en su fila, y la tabla de Commands de morfo.md. - Mi propia sobreafirmacion de ayer, calificada: el test SSR es un CANARIO de dos representantes sobre mecanismo compartido, no un censo — el censo por provider queda encolado (P1). morfo:check en getting-started declara su alcance real (data-*; role/aria sin validador DOM). - Supervivientes de C2c: el assert-vacio de cropper (la clase «guard sobre VACIO pasa») retirado con acta; sticky/README deja de citar syncAttrs. - gradient-builder/README: fila data-kind del Track + acotacion mesh v1. - Handoff: DOS fe de erratas — «nunca los vio nadie» era falso (vistos y reducidos 12→3 en cb554ae2e; lo que faltaba era la puerta) y la ley del escritor imperativo gana su excepcion abierta (textarea autosize, con su cierre correcto encolado). Verificacion: docs:check 0/817 · cropper 19/19 · morfo schema+compile 133/133. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
1 month ago
| Command | Purpose |
| ------------------------------ | ---------------------------------------------------------------------------------- |
| `npm run check` | TypeScript type-check across the repo (catches shape errors in morfos). |
docs: el repositorio, el formato y los guards de texto entran en el corpus; cuatro afirmaciones caducas mueren (documentación del cierre) El plan de cierre cambió cosas de un nivel que el corpus no cubría: la forma del repositorio, la política de formato, el inventario de lo generado y la clase de guard que lee TEXTO fuente. Un inventario previo de todo `docs/**` (más los README de raíz, `src/**` y `apps/**`) midió qué había: los temas de capa estaban cubiertos, y este nivel no. NUEVO - `docs/repository.md` (E0): las zonas y quién escribe en cada una; la LEY de `web/routes/` congelado y sus dos consecuencias (los validadores de navegador siguen manuales; el formato no llega ahí); un solo install y un solo workspace (el argumento de la copia única de Svelte); un solo mapa de importación con el orden como contrato; qué debe cero y qué debe un ledger que solo mengua. Enlazada desde el mapa, el README de la raíz, getting-started y AGENTS.md. - `docs/testing-and-tooling.md` §Format policy: `.prettierignore` enumerado y justificado (cinco clases), el commit único de formato, el `git config blame.ignoreRevsFile` que hay que ejecutar a mano y que Gitea no lo lee. - `docs/testing-and-tooling.md` §Guards that read source text: la doctrina que faltaba. Un guard de texto está acoplado al formateador; el positivo se pone ROJO y te enteras, el NEGATIVO pasa en VERDE sin inspeccionar nada. Los cinco síntomas medidos en el formateo de una sola vez, seis reglas para escribir uno que no dependa del formato, y los cuatro pasos antes de commitear un formateo masivo (neutralidad compilada, la vista de los guards, los validadores que NO están en el gate, un commit puro). CORREGIDO (afirmaciones vivas y falsas) - `README.md` de la raíz: era una plantilla vacía que mandaba `npm install vicen` con el repositorio `private: true` y sin paquete. Ahora es una puerta. - `AGENTS.md`: su pre-flight INVIOLABLE mandaba leer dos guías del árbol congelado (el canónico está migrado), su ejemplo de test apuntaba a `src/lib/ling/`, borrado en el refactor, y describía cuatro librerías que no existen. Además decía que los comentarios en castellano valen, contra CLAUDE.md. - `docs/theming/guide.md`: los cuatro sitios que llamaban `eidos.listThemes()` DENTRO de `hooks.server.ts`, donde no hay instancia; ahora `THEME_IDS` derivado con `listEidosThemes(config)` del módulo que la raíz también importa. - `docs/architecture/active-uix.md`: la regla 6 decía que la raíz no proyecta preferencias; hoy standalone proyecta por defecto (`projectPrefs`, `@default true`) y attach es opt-in. Su ejemplo de arranque montaba una SEGUNDA proyección a mano. - `announce`: el opt-in queda calificado (motor desnudo) frente al cableado por defecto de las raíces, en `book-deviations.md`, `channels.md:58` y el docblock del canal. - `docs/getting-started.md` y `docs/architecture/morfo.md`: la política de `check:gate` también cubre `scripts/`. - `docs/canon/direction-contract.md`: los dueños literales de la marca (`boot`, `projection-<n>`) pasan a la prosa, citables por un guard. - `src/uix/eidos/components/README.md`: regla 8 — un bindable se reenvía con `bind:`, nunca por el spread del resto (el proxy de rest props no lleva `set`, así que el tipo promete lo que no ata). Con el `ref` en la superficie del `Button` y el gap RESUELTO en `cookie-consent`. `docs/canon/vocabularies.md` y `src/libs/emoji/data.ts` aparecen por fin como artefactos generados, con su comando. Ledger: L-133 · L-142 · L-143 · L-154 a ARREGLADO; L-152 conserva los 532 ficheros pero ya con doctrina escrita; nuevas L-161…L-165 (la última, DIFERIDA: nadie obliga aún a que un guard de texto falle con el corpus vacío). Verificación: `npm run gate` exit 0 en 542 s — lint limpio · check:gate OK (89 de web/ en el ledger) · docs:check 0/0 en 822 docs · suite 466/466 ficheros, 5445/5445 tests · apps:check verde. `component:audit` exit 0 con PASS 161 / NEEDS-WORK 5, las cifras de antes. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
3 weeks ago
| `npm run check:gate` | `check` with policy: `src/` and `scripts/` owe zero; `web/` vs the ledger. |
docs(uix): tanda 2 del contraste con el corpus — el modelo de los capitulos alcanza a la tuberia unica La lectura completa del corpus (6 lectores Opus, 6/6 censos, ~4,5M tokens) destapo lo que la tanda 1 no vio, y esta tanda lo cierra: - El modelo «Effects sync attrs» — repetido VERBATIM en cuatro capitulos (morfo, soma-architecture, overview, active-architecture), en sus tablas de piezas, en los cuatro pasos de trigger y en las dos frases-resumen — muere: la pieza es la bolsa de render; ADom aplica solo el prewrite. - morfo.md: partProps re-descrito (bolsa completa), la cadena causal de commits, y las filas de la tabla de ejecutores (data/aria/role → bolsa). - coincident: los DOS docstrings que ensenaban «in-flight» (types.ts y morfo.md) se alinean con la equivalencia FIRMADA de sema.md §slider («emit-then-handler, like pre; declared indivisible») — el hallazgo del informe queda refutado como defecto de runtime y reducido a esto. - glossary: el kind fantasma `internal` (la clase exacta que docs-check:422 mata y su regex no ve en tablas markdown) → `public|private|virtual` real. - El gate entra en la doctrina: check:gate/gate en el loop de verificacion (testing-and-tooling y getting-started), eidos:lint como script npm en su fila, y la tabla de Commands de morfo.md. - Mi propia sobreafirmacion de ayer, calificada: el test SSR es un CANARIO de dos representantes sobre mecanismo compartido, no un censo — el censo por provider queda encolado (P1). morfo:check en getting-started declara su alcance real (data-*; role/aria sin validador DOM). - Supervivientes de C2c: el assert-vacio de cropper (la clase «guard sobre VACIO pasa») retirado con acta; sticky/README deja de citar syncAttrs. - gradient-builder/README: fila data-kind del Track + acotacion mesh v1. - Handoff: DOS fe de erratas — «nunca los vio nadie» era falso (vistos y reducidos 12→3 en cb554ae2e; lo que faltaba era la puerta) y la ley del escritor imperativo gana su excepcion abierta (textarea autosize, con su cierre correcto encolado). Verificacion: docs:check 0/817 · cropper 19/19 · morfo schema+compile 133/133. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
1 month ago
| `npm run gate` | The pre-push chain (validators + suite) — `docs/testing-and-tooling.md` §The gate. |
| `npx vitest run src/uix/morfo` | Run morfo unit tests (schema invariants). |
| `npm run smoke` | Playwright smoke over concrete `web/routes` pages (requires dev server). |
| `npm run morfo:check` | Validate routed `/uix/components/{kebab}` demos vs morfo; unrouted morfos skip. |
| `npm run morfo:vocabulary` | Flag data-state enums that diverge from canonical vocabularies. |
---
## Common pitfalls
**Using `: Morfo =` instead of `as const satisfies Morfo`.** The annotated form widens all literals to `string`, so `createAttrs(morfo)` degrades to `Record<string, string>` — no autocomplete, typos slip past the compiler:
```ts
// ❌ Wrong — works at runtime, but loses literal types.
export const dialogMorfo: Morfo = { ... };
const attrs = createAttrs(dialogMorfo);
attrs.trigerr; // compiles as `string`, runtime undefined
// ✅ Right — literal-preserved shape.
export const dialogMorfo = { ... } as const satisfies Morfo;
const attrs = createAttrs(dialogMorfo);
attrs.trigerr; // compile error — no such part
attrs.trigger; // typed as 'data-dialog-trigger'
```
Every morfo in the codebase use the `as const satisfies Morfo` form. This is mandatory, not stylistic.
**Adding a `defaultElement` to only ONE of its two lists.** The `MorfoElement` vocabulary is declared **twice**: the TypeScript union in [`types.ts`](../../src/uix/morfo/types.ts) and the `literal(...)` list of the sium validator in [`schema.ts`](../../src/uix/morfo/schema.ts). Extending only the union **compiles clean** and then throws at runtime:
```
morfo::invariant: [morfo] Part at path {kebab} failed shape validation:
[sium] validation failed with 1 issue(s)
```
`npm run check` does not catch it — the validation is runtime. `npm run morfo:check` (or simply mounting the component) does. Touch both lists in the same edit. Incident 2026-07-29: `'text'` was added for Barcode's human-readable interpretation and the demo threw on mount with a green typecheck.
**Duplicate kebab in the tree.** `item` in one part and `item` in another = error. Rename one.
**`stateRef` without declaring `states[]`.** If a part emits `aria-expanded` via `v.stateRef('open')`, the part **must** declare `states: ['open', ...]`. Otherwise the validator throws.
**`partRef` to a non-existent kebab.** Common after renaming a part. The validator catches this — but the dev-time warning is silent if you skip `validateMorfo`.
**Using a component-relative `translationRef` for shared text.** `v.translationRef('close', 'Close')` compiles to `components.{component}.close`, which duplicates the same close label across many components. Use `v.commonRef('buttons.close', 'Close')` for shared actions.
docs(morfo,chronos): la doctrina alcanza a lo que se construyo hoy `morfo.md` gana la trampa que se llevo cinco etiquetas: **`texts` es una DECLARACION, nunca una tabla de consulta**. `normalizeTranslationRef` toma la clave VERBATIM y no traduce un alias a una ruta, asi que `prevMonth` frente al `prev-month` del catalogo resuelve a una ruta que no existe y **envia el fallback ingles sin error ni aviso**. Igual con `commonRef('action.undo')` cuando `common.action` no existe. Y el aviso que faltaba: el guard que deberia cazarlo, `npm run translations:check`, **estaba cascando** antes de comprobar nada — arreglarlo y censar los 166 morfos antes de volver a fiarse. README de soma/chronos: `SearchButton` faltaba en la tabla de partes, y ahora documenta los dos permisos de manipulacion directa (`canMove` / `canResize` — separados a proposito aunque hoy coincidan, porque el handler de teclado preguntaba el equivocado) y el estado `searchOpen`. README de eidos/chronos: seccion **Composicion** nueva (la ranura `children`, que las partes publicas las decide `kind` en el morfo, que son ergonomicas y no ganchos vacios, y que los overlays los pone la RAIZ). La coletilla «sin API compound publica» queda marcada como ANULADA en su gap. Busqueda y undo/redo pasan de hueco a hecho; la recurrencia anota que la manipulacion directa se bloquea de forma visible y que falta PINTARLO. `CONTINUE.md` de chronos se marca como NO vigente y apunta al handoff del eje. El handoff gana una seccion §0 «donde estas» con los seis commits y el orden de lo siguiente, y §4 recoge las dos decisiones de autor medidas y listas. docs:check 0/621 · rtl:check 0/177. Los .md tocados que prettier marca ya estaban asi en HEAD salvo el README de eidos, cuyo contenido SI pasa (el aviso es solo CRLF del working tree, que git normaliza). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2 months ago
**Declaring `v.translationRef('content.label')` without a matching catalog entry.** The relative form normalizes to `#?components.{kebab}.content.label` at compile time. That path must resolve in `src/uix/langs/components/{kebab}.ts`. Either add the leaf there (and reference it from `morfo.texts`) or use an absolute ref via `v.commonRef`, `v.langRef`, or a raw `#?...` idlangref.
> ⚠️ **`texts` is a DECLARATION, never a lookup table — and this failure is silent.**
> `normalizeTranslationRef` takes the key **verbatim**; it does not consult `morfo.texts`
> to translate an alias into a path. So `texts: { prevMonth: '#?components.x.prev-month|…' }`
> plus `v.translationRef('prevMonth')` resolves `#?components.x.prevMonth`, misses, and ships
> the English **fallback** — no error, no warning, just the wrong language.
> The same applies to `v.commonRef('action.undo')` when `common.action` does not exist
> (the groups are `buttons`, `calendar`, `month-grid`, `year-grid`, `date`, `time`, `field`).
> **Keep the `texts` keys identical to the catalog keys** so the mismatch is visible at a glance.
feat(morfo,soma): el attr de nombrado es un DEFAULT del contrato, no una imposicion A-85 estaba confirmado desde el ledger de blocks: la demo de site-header escribia `aria-label` en el trigger del cajon y el canon lo pisaba. El censo dio la magnitud real — 132 declaraciones en 59 morfos — y la disposicion escrita (replicar el `prop-truthy` de button en las 132) resulto ser la direccion inversa a la buena: obligaba a 264 entradas de boilerplate y hacia que el texto del consumidor diese una vuelta entera por el grafo reactivo para aterrizar donde ya estaba. Las 132 declaraciones eran CORRECTAS. El defecto era la precedencia. Asi que el contrato gana dos clases, por VOCABULARIO y no por declaracion: contract (role, data-*, aria-expanded, aria-controls, roledescription...) el runtime SIEMPRE gana; un consumidor que los pisara haria mentir al componente, y esa garantia es algo que las librerias user-props-last no tienen. naming (`ARIA_NAMING_ATTRS`, hoy `aria-label`) el morfo da un DEFAULT y el attr explicito del consumidor gana — espejo de la propia cadena de nombre accesible de la plataforma. Tres puntos, uno por capa, y ninguno de los 132 morfos tocado: `compileMorfo` clasifica el plan (`consumerWins`) y deja de hoistear esos literales a staticAttrs; el runtime los saca del efecto `syncAttrs` —el `dom.apply` post-render ERA el pisado— y los embarca en el bag de la parte, el mismo vehiculo que `dir`; y `mergeProps` los resuelve consumer-first (`a ?? b`, para que un `aria-label=""` explicito sobreviva). Efecto medido que no se buscaba: los labels salen ahora en el HTML de SSR. Antes los escribia un efecto, que en servidor no corre — el propio ledger lo tenia medido y nadie habia atado los dos cabos. `aria-labelledby` se queda en contract A PROPOSITO: es cableado `partRef`, y la plataforma ya hace que un labelledby del consumidor gane a cualquier label sin que tengamos que arbitrarlo. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2 months ago
> Measured 2026-08-10/11 in chronos: five labels shipping English on an `es` page. Since
> 2026-08-11 `npm run translations:check` guards this class **at the compiled contract**: it
> imports every morfo, compiles it with the real `compileMorfo`, and resolves each normalized
> `translationRef`/`commonRef`/`langRef` against the catalogs — an alias key is a check ERROR,
> not a silent fallback. (The previous script regex-evaluated catalog sources — a `https://`
> inside a string crashed it — and never scanned call sites at all: a `v.translationRef('key')`
> contains no `#?` literal until the compiler normalizes it.) The 166-morfo sweep of 2026-08-11
> fixed 172 findings: six morfos declared `texts` with **no catalog file** (card-group,
> css-field, picker, qr-code, radio-cards, waveform), media-player's catalog had drifted six
> keys behind its morfo, and palabras' editor chrome referenced ~130 keys its catalog lacked —
> including two **leaf↔branch collisions** (`status`, `placeholder`): a catalog node cannot be
> both a label leaf and a group (`isLangRecord` rejects mixed nodes), so the label moves to a
> child (`status.label`, `placeholder.default`) and sibling keys stay flat (`slash.heading-1-d`,
> never `slash.heading-1.d`).
**Missing `severity: 'optional'` on presence flags.** If you declare `{ attr: 'data-disabled' }` without severity, strict mode treats it as required. Add `severity: 'optional'` so morfo-check doesn't flag it missing when the flag is legitimately absent.
**Closing a component with an incomplete morfo.** Incident 2026-05-20:
DateField, DatePicker, RangeCalendar and DateRangePicker exposed a gap between
runtime/demo DOM and declared Morfo. A component is not done if the provider or
demo emits required `data-*` that Morfo does not declare, if the README says
"0 events" while the public UX composes observable events, or if a demo hand
stamps attrs to make a recipe work. For composite components, document the
composed surface: DateField/DateRangeField, Popover, Calendar/RangeCalendar and
the picker wrapper. Ownership may remain in the child component, but the public
picker docs still need the event table, targets and `data-event` trace path.
**Provider emits a data-attr not in the morfo.** Strict mode logs a warning at runtime; morfo-check fails in CI. Either add the attr to the morfo or rename the provider's emission to `data-_*` (private, not declared in morfo).
---
## See also
- [types.ts](../../src/uix/morfo/types.ts) — the TypeScript interfaces (authoritative reference).
- [PERMUTATION_RUNNER.md](../../src/uix/morfo/PERMUTATION_RUNNER.md) — CI tool that cycles components through their state space.
- [component-guide.md](../guides/component-guide.md) — soma component authoring (morfo-specific discipline: translation-namespace grep, DOM-topology audit, smoke validation).
- [sema/README.md](./sema.md) — semantic layer (morfo provides everything Sema needs via `events[].semantic`).
- [`guia-semantica-historica.md`](../decisions/guia-semantica-historica.md) — the original API conventions (historical seed).
docs: la doctrina alcanza a los cambios de la jornada — cinco backlogs y una llave de mas Auditoria de deriva doc<->codigo sobre los 33 commits del 2026-08-13/14: cinco sitios seguian describiendo lo que el codigo dejo de hacer ayer. Se corrigen COMO BITACORA, al final de cada fichero (el cuerpo es el acta de lo que se firmo, no el estado del codigo; reescribirlo falsearia la firma). El formato es el que ya existia en `arts/adom` y `soma/textarea`: `## Backlog`. - `decisions/book-deviations.md` — dos entradas. D.7 nombraba `IntentExpectedFamily`, tipo que M6 (13246a2c2) borro, y describia una derivacion de DOS cubos que S-33 (1a174d5a6) hizo positiva y TRIPLE. Y el caveat SEM-4 de tree-view/tree-grid nombraba `targetOverride` en presente: b8aa333fd lo borro, hoy se ancla por identidad (`partInstance`). - `theming/channels.md` — decia «built-in opt-in channel» de announce. S-19(ii) (23b20fff5) lo encendio por defecto en las DOS raices; ese commit actualizo `sema.md` y se dejo este atras. - `architecture/morfo.md` — el §semaSelector no nombraba los dos matchers que M5 (b53c93e42) anadio: `state` tipado contra `DataPairOf<M>` y el escape `undeclaredState`, que LANZA si el attr resulta estar declarado — es lo que mantiene obligatorio el tipado. - READMEs de virtual-list / virtual-grid — su columna informal «Emitted? NO» precede al campo del contrato. c73cae022 hizo que el morfo diga QUIEN dispara: `emission: 'host'`. La conducta no cambia; cambia su estatus. Ademas, `architecture/sema.md`: `{{ announce: false }}` -> `{ announce: false }`, llave doblada que introduje ayer en 23b20fff5. Verificado: docs-check 0/625 · prettier sin regresion (los 6 ficheros que avisan ya avisaban en HEAD, medido con stash). Los ficheros de blocks (`blocks.md`, `AUDIT-blocks-ledger.md`, `CONTINUE-blocks.md`, `PLAN-blocks.md`) son de la otra sesion y quedan fuera. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2 months ago
## Backlog / Evolution decisions
### 2026-08-13 — `semaSelector`: the `state` matcher speaks the morfo's data contract
Extends §_Typed selector builder — `semaSelector`_ above (M5, `b53c93e42`).
Two slots, deliberately asymmetric:
- **`state?: DataPairOf<M>`** — an `{ attr, value }` pair the part DECLARES in
its `data`. Typed against the morfo, so renaming or dropping the attr breaks
at compile time instead of drifting into a selector that matches nothing.
- **`undeclaredState?: { attr: string; value: string }`** — the escape hatch
for attrs OUTSIDE the contract (a visual wrapper's `data-size`, a
presentation flag like `data-sheet`). It THROWS when the attr turns out to
be declared, which is what keeps the typed slot mandatory: one open slot
with no guard makes the typed one optional in practice.
The pair replaces the free-form state string a cascade rule used to write by
hand — the same drift class M6 closed on names and D.2 closed on emitters.

Powered by TurnKey Linux.