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>
alpha-0.1-background
dev 1 month ago
parent 8ca595a179
commit 3eb819e7d5

@ -61,21 +61,21 @@ The architecture may keep the historical folder names (`morfo`, `soma`,
General rule: one name represents one concept; if a term is a historical
alias, it must be marked as such with a retirement path.
| Concept | Canonical name | Avoid / retire |
| ------------------------------------ | ----------------------------------------- | --------------------------------------------------------- |
| Runtime translation service | `langs` | `lang` as a service |
| Active language | `prefs.language` | `locale` for the translation language |
| Locale / regional formats | `prefs.locale` | `language` for formats |
| Declarative text catalogs | `translations` | `langs` inside `morfo`; global per-component tables |
| UIX preferences | `prefs` | `settings`, `presentation` as new names |
| UIX perceptual events | `events` | `semantic` as a public service |
| In-flight occurrence | `signal` | using it for the whole layer |
| A morfo event's semantic payload | `semantic` | mixing it with the runtime service |
| Declarative TS contract | `morfo` | `contract` as a duplicated TS API |
| Exported CSS/data contract | `contract` | `morfo` for external CSS |
| Runtime CSS bridge | `ActiveEidos` | a mandatory visual runtime for components |
| Independent pure engine | `EngineX` only if it lives outside `ActiveX` | decorative engines |
| Eidos visual root | `DrawerProps`, `DialogProps` | `DrawerProviderProps` in the visual API |
| Concept | Canonical name | Avoid / retire |
| -------------------------------- | -------------------------------------------- | --------------------------------------------------- |
| Runtime translation service | `langs` | `lang` as a service |
| Active language | `prefs.language` | `locale` for the translation language |
| Locale / regional formats | `prefs.locale` | `language` for formats |
| Declarative text catalogs | `translations` | `langs` inside `morfo`; global per-component tables |
| UIX preferences | `prefs` | `settings`, `presentation` as new names |
| UIX perceptual events | `events` | `semantic` as a public service |
| In-flight occurrence | `signal` | using it for the whole layer |
| A morfo event's semantic payload | `semantic` | mixing it with the runtime service |
| Declarative TS contract | `morfo` | `contract` as a duplicated TS API |
| Exported CSS/data contract | `contract` | `morfo` for external CSS |
| Runtime CSS bridge | `ActiveEidos` | a mandatory visual runtime for components |
| Independent pure engine | `EngineX` only if it lives outside `ActiveX` | decorative engines |
| Eidos visual root | `DrawerProps`, `DialogProps` | `DrawerProviderProps` in the visual API |
Decisions already applied:
@ -472,7 +472,7 @@ SomaRuntime transcribes (Soma — reading morfo + sources)
↓
Provider supplies sources/handlers (Soma — TypeScript class)
↓
Effects sync attrs (Soma — $effect + dom.apply)
Render bag re-derives attrs (Soma — partProps; Svelte renders it)
↓
EngineSemantic dispatches signals (Sema — registry + prepare/dispatch)
↓
@ -483,23 +483,24 @@ Eidos reads the DOM and applies CSS (Eidos — selectors + tokens)
Plus, in parallel (not in the chain):
- **ADom** materializes the `dom.apply`/`dom.remove` Soma asks for on the
derived structural attrs (data-state, aria-\*, etc.).
- **ADom** materializes the one imperative attr write left — `trigger`'s
declared prewrite (`data-last-action`, etc.). Derived structural attrs
travel in the render bag since P0 fase C (audit 2026-08-26).
- **Sema's non-visual channels** (sound, haptic, future) receive the same
signal and materialize it in their modality — fire-and-forget.
The pieces with disjoint responsibilities:
| Piece | Responsibility | Doesn't do |
| ------------------- | ----------------------------------------------------- | ---------------------------------------- |
| **Morfo** | Declare the contract | Execute anything |
| **SomaRuntime** | Transcribe morfo into behavior | Decide business logic |
| **Provider** | Supply reactive sources + handlers | Write mutable attrs to the DOM |
| **Effects** | Apply derived attrs via `dom.apply` | Decide which attrs (morfo says that) |
| **EngineSemantic** | Channel registry + prepare/dispatch | Know DOM, audio, vibration |
| **VisualChannel** | Project `data-event*` via projector + awaited hold | Write structural attrs |
| **SignalProjector** | Project `data-event*` via `dom.apply` | Decide when to emit |
| **ADom** | Imperative DOM mutations for structural attrs | Know the upper layers |
| Piece | Responsibility | Doesn't do |
| ------------------- | -------------------------------------------------- | ------------------------------------ |
| **Morfo** | Declare the contract | Execute anything |
| **SomaRuntime** | Transcribe morfo into behavior | Decide business logic |
| **Provider** | Supply reactive sources + handlers | Write mutable attrs to the DOM |
| **Render bag** | Resolve every morfo plan for Svelte to render | Decide which attrs (morfo says that) |
| **EngineSemantic** | Channel registry + prepare/dispatch | Know DOM, audio, vibration |
| **VisualChannel** | Project `data-event*` via projector + awaited hold | Write structural attrs |
| **SignalProjector** | Project `data-event*` via `dom.apply` | Decide when to emit |
| **ADom** | Imperative DOM mutations for structural attrs | Know the upper layers |
`Eidos` stays outside that chain: it reads from the DOM; it does not
participate in the transcription.
@ -559,33 +560,33 @@ perceivable window to choreograph the exit.
Everything that travels between layers travels through DOM attributes:
| Attribute | Who writes | Who reads |
| ------------------------------------------- | --------------------------------------- | ----------------------------- |
| `data-{component}` | partProps (static) | Eidos (root selector) |
| `data-{component}-{part}` | partProps (static) | Eidos (part selector) |
| `data-archetype="trigger"` | partProps (static) | Eidos (transversal selector) |
| `id` | partProps | ARIA refs, tests |
| `role` | dom.apply (effect) | Screen readers, Eidos |
| `aria-*` | dom.apply (effect) | Screen readers, Eidos |
| `data-state="open"` | dom.apply (effect) | Eidos (variant selector) |
| `data-disabled` | dom.apply (effect) | Eidos (state selector) |
| `data-event="emerge-dismiss"` | sema.emit (transient) | Eidos (event selector) |
| `data-event-phase="active"` | sema.emit (transient) | Eidos |
| `data-event-id="sig-N"` | sema.emit (transient) | Future sound/haptic |
| `data-event-family="commit"` | sema.emit (transient) | Eidos (family selector) |
| `data-event-intent="risk"` | sema.emit (transient) | Eidos (signal tinting) |
| `data-event-direction="forward"` | sema.emit (transient, per-emit only) | Eidos (directional signature) |
| `data-color="primary"` | dom.apply (effect) | Eidos (per-token recipe) |
| `data-intent="risk"` | dom.apply (effect, optional per morfo) | Eidos (persistent state) |
| `dir` | prefs projection (page) / provider | Browser bidi, Eidos `:dir()` |
| `data-dir` | provider (resolved, opt-in per recipe) | Eidos (unconditional hook) |
| `data-last-action="cancelled"` | trigger prewrite | Eidos (exit tinting) |
| `data-starting-style` / `data-ending-style` | Presence layer | Eidos (animation hooks) |
**Operational rule**: what `dom.apply` writes, Svelte does not render. Static
identity (id + marker + archetype + ref attachment) ships via `partProps`.
State-derived attrs ship via `dom.apply` from effects. There is no
double-write.
| Attribute | Who writes | Who reads |
| ------------------------------------------- | -------------------------------------- | ----------------------------- |
| `data-{component}` | partProps (static) | Eidos (root selector) |
| `data-{component}-{part}` | partProps (static) | Eidos (part selector) |
| `data-archetype="trigger"` | partProps (static) | Eidos (transversal selector) |
| `id` | partProps | ARIA refs, tests |
| `role` | dom.apply (effect) | Screen readers, Eidos |
| `aria-*` | dom.apply (effect) | Screen readers, Eidos |
| `data-state="open"` | dom.apply (effect) | Eidos (variant selector) |
| `data-disabled` | dom.apply (effect) | Eidos (state selector) |
| `data-event="emerge-dismiss"` | sema.emit (transient) | Eidos (event selector) |
| `data-event-phase="active"` | sema.emit (transient) | Eidos |
| `data-event-id="sig-N"` | sema.emit (transient) | Future sound/haptic |
| `data-event-family="commit"` | sema.emit (transient) | Eidos (family selector) |
| `data-event-intent="risk"` | sema.emit (transient) | Eidos (signal tinting) |
| `data-event-direction="forward"` | sema.emit (transient, per-emit only) | Eidos (directional signature) |
| `data-color="primary"` | dom.apply (effect) | Eidos (per-token recipe) |
| `data-intent="risk"` | dom.apply (effect, optional per morfo) | Eidos (persistent state) |
| `dir` | prefs projection (page) / provider | Browser bidi, Eidos `:dir()` |
| `data-dir` | provider (resolved, opt-in per recipe) | Eidos (unconditional hook) |
| `data-last-action="cancelled"` | trigger prewrite | Eidos (exit tinting) |
| `data-starting-style` / `data-ending-style` | Presence layer | Eidos (animation hooks) |
**Operational rule**: the render bag (`partProps`) is the single attr
pipeline — static identity AND every state-derived plan ship through it,
server-rendered included (P0 fase C, audit 2026-08-26). `dom.apply` writes
only `trigger`'s declared prewrite. There is no double-write.
### Cross-layer vocabularies
@ -724,14 +725,14 @@ A useful lens for deciding where each thing lives:
UIX aims for only the authorship to be human. Transcription is code that
writes code:
| Authorship | Transcription | How |
| --------------------------------------- | ----------------------- | -------------------------------------------- |
| `Props` | `Opts` | hand-written `extends WithRefOpts, StateProps<>, ActiveProps<>` — or `OptsFromProps<P, Managed, StateKey, Preserve>` |
| Each wrapper prop | Active/State boxes | `bindProps<XOpts>({ ... })` — target-typed |
| A part's whole `{ id, ref }` bag | `WithRefOpts` | `partOpts(() => id, () => ref, setRef)` |
| `morfo.events[].commits` | Final DOM after handler | Effects derive |
| `morfo.parts[].data` | Attributes on each tick | Resolver + dom.apply |
| `morfo.events[].name` + canonical verb | `data-event="..."` | sema.emit |
| Authorship | Transcription | How |
| -------------------------------------- | ----------------------- | -------------------------------------------------------------------------------------------------------------------- |
| `Props` | `Opts` | hand-written `extends WithRefOpts, StateProps<>, ActiveProps<>` — or `OptsFromProps<P, Managed, StateKey, Preserve>` |
| Each wrapper prop | Active/State boxes | `bindProps<XOpts>({ ... })` — target-typed |
| A part's whole `{ id, ref }` bag | `WithRefOpts` | `partOpts(() => id, () => ref, setRef)` |
| `morfo.events[].commits` | Final DOM after handler | Effects derive |
| `morfo.parts[].data` | Attributes on each tick | Resolver + dom.apply |
| `morfo.events[].name` + canonical verb | `data-event="..."` | sema.emit |
Two details the table cannot carry, both load-bearing:
@ -857,8 +858,8 @@ The important idea:
## 13. The summary sentence
> **Morfo declares · SomaRuntime transcribes · Provider supplies · Effects
> sync · Semantic emits · Dom applies · Eidos reads.**
> **Morfo declares · SomaRuntime transcribes · Provider supplies · the render
> bag derives · Semantic emits · Dom applies the prewrite · Eidos reads.**
Seven words describing the whole chain. If an architectural decision
contradicts one of those seven, the decision is wrong — or the architecture

@ -97,7 +97,7 @@ responsibilities:
Morfo declares
SomaRuntime transcribes
Provider supplies sources, targets, handlers
Effects sync attrs from state
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)
@ -105,21 +105,22 @@ ADom applies DOM mutations (structural commit)
### What each morfo field maps to at runtime
| Morfo field | Runtime executor | Purpose |
| ----------------------------- | ------------------------------------------------------------------------------------------------------------------------------------- | ----------------- |
| `parts[].data` (with `value`) | Effect of attrs | Reactive `data-*` |
| `parts[].aria` | Effect of attrs — except NAMING attrs (`aria-label`), which ship in the part's render bag so the consumer's attr can win (see Step 4) | Reactive `aria-*` |
| `parts[].role` | Effect of attrs | Stable role |
| `parts[].keyboard` | `runtime.keydown(part, event)` | Key dispatch |
| `events[].prewrite` | `trigger()` step 1 | Transient markers |
| `events[].semantic` | `trigger()` step 2 (emit payload) | Perceptual signal |
| `events[].commits` | **Nobody executes**; smoke validates | Documentation |
| `focus` | Configures FocusScope layer | Layer bootstrap |
| 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` | `runtime.keydown(part, event)` | Key dispatch |
| `events[].prewrite` | `trigger()` step 1 | Transient markers |
| `events[].semantic` | `trigger()` step 2 (emit payload) | Perceptual signal |
| `events[].commits` | **Nobody executes**; smoke validates | Documentation |
| `focus` | Configures FocusScope layer | Layer bootstrap |
`commits` is **descriptive**, not prescriptive. The actual causal chain is
`handler -> state mutation -> effect -> dom.apply`. The `commits` declaration
documents what an external observer will see and is checked by the smoke
suite.
`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
@ -127,7 +128,8 @@ suite.
1. prewrite imperative (data-last-action, etc.)
2. await semantic.emit(event)
3. provider's synchronous handler mutates state
4. effects derive and apply structural attrs (data-state, aria-*)
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.
@ -158,16 +160,19 @@ Each part-provider then renders only the static identity:
```ts
readonly props = $derived.by(() => runtime.partProps('trigger'));
// returns: { id, ref attachment, 'data-{component}-trigger': '' }
// returns: { id, ref attachment, 'data-{component}-trigger': '',
// role, aria-*, data-state, ... } — the FULL resolved contract
```
Everything mutable (`role` derived from prop, `aria-*`, `data-state`, `data-intent`)
is written by the runtime's effects via `dom.apply`. Svelte does not render
those attrs.
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
- `partProps(part)` returns only static identity (id, ref, marker).
- `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 —
@ -808,8 +813,11 @@ Field rules:
relative to the structural state change. Default `'pre'` preserves
the runtime semantics where the signal completes before the commit.
Use `'post'` when the celebration belongs after the new state lands
(commit pulses on completed actions). `'coincident'` is for
in-flight processes (sustain).
(commit pulses on completed actions). `'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.
**Not declarable here: `direction`.** The morfo cannot state the sense of a
traversal, because the same declared event goes backward on one press and
@ -1116,13 +1124,15 @@ runtime that interprets a compiled morfo lives in soma — see
## Commands
| Command | Purpose |
| ------------------------------ | ------------------------------------------------------------------------------- |
| `npm run check` | TypeScript type-check across the repo (catches shape errors in morfos). |
| `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. |
| Command | Purpose |
| ------------------------------ | ---------------------------------------------------------------------------------- |
| `npm run check` | TypeScript type-check across the repo (catches shape errors in morfos). |
| `npm run check:gate` | `check` with policy: `src/` owes zero; `web/` vs the shrinking debt ledger. |
| `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. |
---

@ -270,7 +270,7 @@ responsibilities. None invades the next.
Morfo declares
SomaRuntime transcribes
Provider supplies sources, targets and handlers
Effects sync derived attrs
Render bag re-derives attrs from state (Svelte renders them)
EngineSemantic orchestrates prepare + dispatch to perceptual channels
VisualChannel prepares data-event* via SignalProjector + holds
SignalProjector projects data-event* via uix.dom
@ -311,7 +311,8 @@ Sema and ADom are sibling layers: the engine no longer depends on ADom.
1. imperative prewrite (transient markers like data-last-action)
2. await events.emit(event)
3. the provider's synchronous handler mutates state
4. effects derive and apply structural attrs (data-state, aria-*)
4. the render bag re-derives structural attrs (data-state, aria-*) and
Svelte renders them
```
The handler mutates state. The effects see the change and rewrite the DOM.
@ -478,8 +479,8 @@ virtual prop on the provider — do not extend the contract.
## 6. The difference in one sentence
> Morfo declares, SomaRuntime transcribes, Provider supplies, Effects sync,
> Sema emits, ADom applies, Eidos reads.
> Morfo declares, SomaRuntime transcribes, Provider supplies, the render bag
> derives, Sema emits, ADom applies the prewrite, Eidos reads.
Seven pieces, seven responsibilities, none invades the next.

@ -209,7 +209,7 @@ six pieces with disjoint responsibilities:
Morfo declares
SomaRuntime transcribes (lives in soma/)
Provider supplies sources, targets and handlers
Effects sync derived attrs
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 (the structural commit)
@ -328,11 +328,13 @@ emitEvent(event) {
1. imperative prewrite (transient markers like data-last-action)
2. await events.emit(event)
3. the provider's synchronous handler mutates state
4. effects derive and apply structural attrs (data-state, aria-*)
4. the render bag re-derives structural attrs (data-state, aria-*) and
Svelte renders them
```
The runtime's effects listen to the reactive sources and re-apply attrs every
time state changes. ADom is the only writer of mutable attrs.
The bag reads rune-backed sources, so every spread re-derives when state
changes — server-rendered included. ADom's one attr write left is the
prewrite (step 1).
### Operational rules

@ -20,11 +20,11 @@ npm run dev # vite dev — routes resolve from web/routes/
Open the dev server. There are three route trees:
| Route | What it is |
| --- | --- |
| **`/uix`** | The UIX component system. `/uix/components/{name}` is an interactive testbed per component — try `/uix/components/toggle` and `/uix/components/dialog`. Each demo has **Live · API · Morfo · Sema · Recipe · A11y** tabs. |
| **`/active`** | The runtime artifacts (`arts`): `/active/docs/{name}` per artifact (auth, cache, http, format, …), plus `/active/get-started/*` and `/active/security`. |
| **`/temas`** | Themes (`/temas/grafito`) and motion (`/temas/animations`). |
| Route | What it is |
| ------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **`/uix`** | The UIX component system. `/uix/components/{name}` is an interactive testbed per component — try `/uix/components/toggle` and `/uix/components/dialog`. Each demo has **Live · API · Morfo · Sema · Recipe · A11y** tabs. |
| **`/active`** | The runtime artifacts (`arts`): `/active/docs/{name}` per artifact (auth, cache, http, format, …), plus `/active/get-started/*` and `/active/security`. |
| **`/temas`** | Themes (`/temas/grafito`) and motion (`/temas/animations`). |
## 2. The mental model (5 minutes)
@ -69,16 +69,19 @@ sema stamps the event, eidos paints — no layer reaches into another's job.
## 4. The verification loop
```bash
npm run check # types — svelte-check, expect 0 errors
npm run check:gate # types WITH policy — src/ owes zero; web/ vs the ledger
npm run check # raw svelte-check (web/ carries frozen ledger errors)
npm run test # vitest suite (two projects: browser client + node server)
npx vitest run <file> # one file
npm run gate # what the pre-push hook runs (docs/testing-and-tooling §The gate)
npm run lint # prettier --check (npm run format to fix)
```
For a component specifically:
```bash
npm run morfo:check # the real DOM vs the morfo contract
npm run morfo:check # the real DOM vs the morfo contract (data-* attrs;
# role/aria-* have no DOM validator yet — informe P1)
npm run smoke # runtime/hydration errors (needs `npm run dev` running)
npm run component:audit # acceptance matrix (see guides/completion-checklist.md)
npm run perm:check # re-validate morfo across state transitions

@ -17,80 +17,80 @@ New here? Start at [`docs/README.md`](./README.md).
## The layers
| Term | Meaning |
| --- | --- |
| **morfo** | The declarative contract (a component's "DNA"): its public DOM surface — parts, `data-*`/ARIA, keyboard, events — declared once in a typed object. Every other layer reads it. → [`architecture/morfo`](./architecture/morfo.md) |
| **soma** | The headless behavior layer: keyboard, focus, ARIA wiring, state machines, composition. No visuals. → [`architecture/soma`](./architecture/soma.md) |
| **sema** | The perceptual engine: turns a declared event into sound / haptic (runtime) and a `data-event-*` projection (for eidos), via a cascade. → [`architecture/sema`](./architecture/sema.md) |
| **eidos** | The visual layer: CSS recipes, tokens, themes, sizes, variants — reacts to the DOM attrs morfo promises. → [`architecture/eidos`](./architecture/eidos.md) |
| **arts** | Runtime artifacts: the `Engine*` / `Active*` services (auth, cache, http, format, langs, dom, motion, …). → [`arts/README`](../src/arts/README.md) |
| **libs** | Pure, zero-dependency helpers (`$libs/days`, `$libs/dom`, `$reactive`, …). |
| **svrs** | Server-authoritative engines (`$svrs/auth`, `$svrs/perm`, `$svrs/cache`). |
| **active-uix** | The composition root that wires the layers — `createActiveUix` (standalone) or `attachActiveUix` (attach to an app). → [`architecture/active-uix`](./architecture/active-uix.md) |
| **ActiveDom / `$adom`** | The single reactive DOM service: the only sanctioned surface for managed DOM writes, listeners, queries, focus and scroll. |
| **pack** | An encapsulated opt-in collection above the layers (decorative leaves: no morfo, outside the acceptance matrix, one-way dependency). Admission rule + P contract → [`architecture/packs`](./architecture/packs.md) |
| **scene (`EngineScene` / `$scene`)** | The ambient-scene runtime art: mounts a WebGL/canvas-2D **effect** on a host with the citizenship done once (frame loop, off-view pause, DPR cap, mandatory reduced-motion, context loss/restore, budget, teardown). Effects are shared resources (`$scene/effects`) — the same one a pack mounts decoratively, `Aura` will mount semantically. → [`arts/scene/README`](../src/arts/scene/README.md) |
| **Ambient** | The first pack (`$packs/ambient`) — animated backgrounds. `<Ambient effect="…">` mounts a registered scene effect; colors accept theme tokens (P-4). → [`src/packs/ambient/README`](../src/packs/ambient/README.md) |
| **Aura** | The canonical **agent-presence** component: it materializes the `delegate` + `sustain` families by consuming `$scene/effects` semantically (intent → speed/amplitude/hue). The promotion path it walked is in [`architecture/packs`](./architecture/packs.md). → [`eidos/components/aura/README`](../src/uix/eidos/components/aura/README.md) |
| **Background** | The canonical **host of a surface's background layers** — image, video, pattern, gradient, scrim, or a scene the app mounts itself. A CHILD, never a wrapper: it renders inside the surface it dresses and pins itself behind that surface's content, so any element can host it and layout never moves. The parent is ADOPTED by a foundation rule rather than configured. The animated effect stays in the pack tier; the host is canon because it owns a contract (`aria-hidden` layers, the WCAG 2.2.2 pause control, a token surface). → [`eidos/components/background/README`](../src/uix/eidos/components/background/README.md) |
| **text effects** | A canon family of eidos components that treat REAL text content (`TextGradient`, `TextCircular`, `TextBlur`, `TextFocus`, `TextScramble`) plus the service `CountUp` — the animated siblings of the typographic primitives. Content stays the accessibility surface; the animation is presentation. Each self-documents (README + demo). |
| Term | Meaning |
| ------------------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **morfo** | The declarative contract (a component's "DNA"): its public DOM surface — parts, `data-*`/ARIA, keyboard, events — declared once in a typed object. Every other layer reads it. → [`architecture/morfo`](./architecture/morfo.md) |
| **soma** | The headless behavior layer: keyboard, focus, ARIA wiring, state machines, composition. No visuals. → [`architecture/soma`](./architecture/soma.md) |
| **sema** | The perceptual engine: turns a declared event into sound / haptic (runtime) and a `data-event-*` projection (for eidos), via a cascade. → [`architecture/sema`](./architecture/sema.md) |
| **eidos** | The visual layer: CSS recipes, tokens, themes, sizes, variants — reacts to the DOM attrs morfo promises. → [`architecture/eidos`](./architecture/eidos.md) |
| **arts** | Runtime artifacts: the `Engine*` / `Active*` services (auth, cache, http, format, langs, dom, motion, …). → [`arts/README`](../src/arts/README.md) |
| **libs** | Pure, zero-dependency helpers (`$libs/days`, `$libs/dom`, `$reactive`, …). |
| **svrs** | Server-authoritative engines (`$svrs/auth`, `$svrs/perm`, `$svrs/cache`). |
| **active-uix** | The composition root that wires the layers — `createActiveUix` (standalone) or `attachActiveUix` (attach to an app). → [`architecture/active-uix`](./architecture/active-uix.md) |
| **ActiveDom / `$adom`** | The single reactive DOM service: the only sanctioned surface for managed DOM writes, listeners, queries, focus and scroll. |
| **pack** | An encapsulated opt-in collection above the layers (decorative leaves: no morfo, outside the acceptance matrix, one-way dependency). Admission rule + P contract → [`architecture/packs`](./architecture/packs.md) |
| **scene (`EngineScene` / `$scene`)** | The ambient-scene runtime art: mounts a WebGL/canvas-2D **effect** on a host with the citizenship done once (frame loop, off-view pause, DPR cap, mandatory reduced-motion, context loss/restore, budget, teardown). Effects are shared resources (`$scene/effects`) — the same one a pack mounts decoratively, `Aura` will mount semantically. → [`arts/scene/README`](../src/arts/scene/README.md) |
| **Ambient** | The first pack (`$packs/ambient`) — animated backgrounds. `<Ambient effect="…">` mounts a registered scene effect; colors accept theme tokens (P-4). → [`src/packs/ambient/README`](../src/packs/ambient/README.md) |
| **Aura** | The canonical **agent-presence** component: it materializes the `delegate` + `sustain` families by consuming `$scene/effects` semantically (intent → speed/amplitude/hue). The promotion path it walked is in [`architecture/packs`](./architecture/packs.md). → [`eidos/components/aura/README`](../src/uix/eidos/components/aura/README.md) |
| **Background** | The canonical **host of a surface's background layers** — image, video, pattern, gradient, scrim, or a scene the app mounts itself. A CHILD, never a wrapper: it renders inside the surface it dresses and pins itself behind that surface's content, so any element can host it and layout never moves. The parent is ADOPTED by a foundation rule rather than configured. The animated effect stays in the pack tier; the host is canon because it owns a contract (`aria-hidden` layers, the WCAG 2.2.2 pause control, a token surface). → [`eidos/components/background/README`](../src/uix/eidos/components/background/README.md) |
| **text effects** | A canon family of eidos components that treat REAL text content (`TextGradient`, `TextCircular`, `TextBlur`, `TextFocus`, `TextScramble`) plus the service `CountUp` — the animated siblings of the typographic primitives. Content stays the accessibility surface; the animation is presentation. Each self-documents (README + demo). |
## Morfo vocabulary
| Term | Meaning |
| --- | --- |
| **part** | A named sub-element of a component (`provider`, `trigger`, `content`, …). |
| **archetype** | Cross-component classification of a part (`trigger`, `item`, `option`, …) — used for transversal eidos selectors and sema verbs. |
| **kind** | A part's visibility: `public` \| `internal` \| `private`. |
| **data-attr contract** | The stable markers a part emits: `data-{component}` (provider) and `data-{component}-{part}`. Never `data-soma-*`. The eidos/sema frontier. |
| **value sources (`v.*`)** | Typed origins for an ARIA/data value in morfo: `v.literal`, `v.stateRef`, `v.partRef`, `v.propRef`, `v.translationRef`. |
| **scope** | Which layers implement the component: `['soma']`, `['soma', 'eidos']`, … |
| **2-of-3 rule** | A morfo field is justified only if at least 2 of soma / sema / eidos consume it. |
| **compileMorfo** | Turns a morfo into a `CompiledMorfo` (resolved attr/keyboard/action plans + the closed set of CSS selectors), cached by morfo identity. |
| **expression** | How a morfo materializes its perceptual signature: `'pack'` \| `'family-default'` \| `'delegated'` \| `'none'`. |
| Term | Meaning |
| ------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------- |
| **part** | A named sub-element of a component (`provider`, `trigger`, `content`, …). |
| **archetype** | Cross-component classification of a part (`trigger`, `item`, `option`, …) — used for transversal eidos selectors and sema verbs. |
| **kind** | A part's visibility (`MorfoPartKind`): `public` \| `private` \| `virtual`. |
| **data-attr contract** | The stable markers a part emits: `data-{component}` (provider) and `data-{component}-{part}`. Never `data-soma-*`. The eidos/sema frontier. |
| **value sources (`v.*`)** | Typed origins for an ARIA/data value in morfo: `v.literal`, `v.stateRef`, `v.partRef`, `v.propRef`, `v.translationRef`. |
| **scope** | Which layers implement the component: `['soma']`, `['soma', 'eidos']`, … |
| **2-of-3 rule** | A morfo field is justified only if at least 2 of soma / sema / eidos consume it. |
| **compileMorfo** | Turns a morfo into a `CompiledMorfo` (resolved attr/keyboard/action plans + the closed set of CSS selectors), cached by morfo identity. |
| **expression** | How a morfo materializes its perceptual signature: `'pack'` \| `'family-default'` \| `'delegated'` \| `'none'`. |
## Soma vocabulary
| Term | Meaning |
| --- | --- |
| **provider** | The concrete state class for a component or part. The root registers context; sub-parts read it. Exported as `Xxx.Provider`. |
| **SomaRuntime** | The morfo interpreter in soma. `runtime.part()` registers a part; `runtime.trigger(event)` sequences prewrite → emit → handler → effect-driven attrs. |
| **layer (soma)** | A shared behavior class consumed by providers: `Presence`, `FocusScope`, `Dismissal`, `ScrollLock`, `Gesture`, `SafePolygon`. → [`SOMA_ARCHITECTURE`](./architecture/soma-architecture.md) §6 |
| **Presence** | Animation-aware mount/unmount (waits for exit animations before removing). |
| **Active\<T\> / State\<T\>** | Reactive containers (readonly / mutable, exposing `.current`) that let runes be passed by reference between classes. |
| **context convention** | The `X.create()` / `X.get()` / `X.require()` static methods every context-using class follows. |
| **roving vs virtual focus** | Two keyboard strategies: real DOM focus with one `tabindex=0` (roving) vs focus stays on the input and items are `data-highlighted` via `aria-activedescendant` (virtual). |
| **polymorphic close** | One `emerge-close` event with `allowedFamilies`; the provider chooses the family at dismiss time (used by Dialog/Drawer/Popover). |
| **prewrite / commit** | DOM written imperatively *before* the semantic emit (`prewrite`, e.g. `data-last-action`) vs the structural state written *after* (`commit`). |
| **activeDir** | The direction resolver a wrapper runs for a component; returns `Active<Direction \| undefined>`, where `undefined` means nobody asserted a direction. → [`canon/direction-contract`](./canon/direction-contract.md) |
| **resolvedDir** | A provider's concrete direction — `activeDir`'s value with the fallback applied, once, for the component's own maths. |
| Term | Meaning |
| ---------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **provider** | The concrete state class for a component or part. The root registers context; sub-parts read it. Exported as `Xxx.Provider`. |
| **SomaRuntime** | The morfo interpreter in soma. `runtime.part()` registers a part; `runtime.trigger(event)` sequences prewrite → emit → handler → effect-driven attrs. |
| **layer (soma)** | A shared behavior class consumed by providers: `Presence`, `FocusScope`, `Dismissal`, `ScrollLock`, `Gesture`, `SafePolygon`. → [`SOMA_ARCHITECTURE`](./architecture/soma-architecture.md) §6 |
| **Presence** | Animation-aware mount/unmount (waits for exit animations before removing). |
| **Active\<T\> / State\<T\>** | Reactive containers (readonly / mutable, exposing `.current`) that let runes be passed by reference between classes. |
| **context convention** | The `X.create()` / `X.get()` / `X.require()` static methods every context-using class follows. |
| **roving vs virtual focus** | Two keyboard strategies: real DOM focus with one `tabindex=0` (roving) vs focus stays on the input and items are `data-highlighted` via `aria-activedescendant` (virtual). |
| **polymorphic close** | One `emerge-close` event with `allowedFamilies`; the provider chooses the family at dismiss time (used by Dialog/Drawer/Popover). |
| **prewrite / commit** | DOM written imperatively _before_ the semantic emit (`prewrite`, e.g. `data-last-action`) vs the structural state written _after_ (`commit`). |
| **activeDir** | The direction resolver a wrapper runs for a component; returns `Active<Direction \| undefined>`, where `undefined` means nobody asserted a direction. → [`canon/direction-contract`](./canon/direction-contract.md) |
| **resolvedDir** | A provider's concrete direction — `activeDir`'s value with the fallback applied, once, for the component's own maths. |
## Sema vocabulary
The values live in [`CANON.md`](./CANON.md); these are the term shapes.
| Term | Meaning |
| --- | --- |
| **family** | One of the **8** perceptual event families (contact · commit · signal · handle · emerge · shift · sustain · delegate). → CANON |
| **intent** | The evaluative load of an occurrence (neutral · affirm · fulfill · risk · threat · loss) — only on valenced families. → CANON |
| **verb** | The specific act within a family (`tap`, `select`, `close`, …). → CANON |
| **direction** | The sense of a traversal (`forward` \| `backward`, `SemaDirection`), stamped as `data-event-direction`. Two values, because the event NAME already separates `shift-enter-mode` from `shift-exit-mode`; what a name cannot carry is which way THIS occurrence went. Decided per emit and optional, like intent. A SENSE, not an axis — eidos maps it onto the inline axis so `:dir(rtl)` flips it. → CANON |
| **channel** | An expression modality. Sema runs two at runtime (sound, haptic) and projects `visual`; eidos owns the rest (motion/presence/depth/shape/color). → CANON |
| **hold** | The minimum perceptible duration a signal stays projected (`data-event-*` stamped during it). |
| **cascade** | The layered resolution of a perceptual signature (1 family base → 2 intent deltas → 3 per-event → 4 globals → 5a packs / 5b app rules). → [`architecture/sema`](./architecture/sema.md) |
| **persistence** | A signal's lifecycle, distinct from hold: `transient` \| `untilAction` \| `untilFix` \| `stateBound`. |
| Term | Meaning |
| --------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **family** | One of the **8** perceptual event families (contact · commit · signal · handle · emerge · shift · sustain · delegate). → CANON |
| **intent** | The evaluative load of an occurrence (neutral · affirm · fulfill · risk · threat · loss) — only on valenced families. → CANON |
| **verb** | The specific act within a family (`tap`, `select`, `close`, …). → CANON |
| **direction** | The sense of a traversal (`forward` \| `backward`, `SemaDirection`), stamped as `data-event-direction`. Two values, because the event NAME already separates `shift-enter-mode` from `shift-exit-mode`; what a name cannot carry is which way THIS occurrence went. Decided per emit and optional, like intent. A SENSE, not an axis — eidos maps it onto the inline axis so `:dir(rtl)` flips it. → CANON |
| **channel** | An expression modality. Sema runs two at runtime (sound, haptic) and projects `visual`; eidos owns the rest (motion/presence/depth/shape/color). → CANON |
| **hold** | The minimum perceptible duration a signal stays projected (`data-event-*` stamped during it). |
| **cascade** | The layered resolution of a perceptual signature (1 family base → 2 intent deltas → 3 per-event → 4 globals → 5a packs / 5b app rules). → [`architecture/sema`](./architecture/sema.md) |
| **persistence** | A signal's lifecycle, distinct from hold: `transient` \| `untilAction` \| `untilFix` \| `stateBound`. |
## Eidos vocabulary
| Term | Meaning |
| --- | --- |
| **recipe** | A component's token + CSS definition (in `EidosConfig.recipes` / `{name}.css`). |
| **token** | A CSS custom property. Public: `--{component}-*`; private recipe-internal: `--_{component}-*`. Never `--eidos-*` / `--soma-*`. |
| **TSC (Token Scope Contract)** | Where each token is allowed to be emitted (`:root` / `[data-{c}]` / by color / by event) + transitivity validation. → [`canon/tsc`](./canon/tsc.md) |
| **variant** | A fixed visual archetype (`solid`, `outline`, `ghost`, …). Canon of eidos — a theme cannot invent or redefine one. |
| **role** | One of the **9** canonical color roles (`primary`, `secondary`, `tertiary`, `neutral`, `affirm`, `fulfill`, `risk`, `threat`, `loss`). |
| **scaling / density** | Orthogonal structural axes: global zoom (90–110) vs spacing (compact/comfortable/spacious). |
| **theme** | A retint of the perceptually-fixed: it changes *which hex* is `affirm`, never *what* `outline` means. → [`THEMING`](./theming/reference.md) |
| **`data-dir`** | A component's own direction attribute, carrying the resolved value — opt-in, for a recipe that needs a hook which always matches; native `dir` carries the raw value and is absent when nobody asserted one. |
| **RTL-1** | The eidos lint rule (`npm run rtl:check`) that flags a logical inline anchor paired with a physical inline translate in the same CSS block. |
| **`rtl-physical:`** | The sanctioned comment that exempts a block from RTL-1 when its geometry genuinely is physical: `/* rtl-physical: <reason> */`. |
| Term | Meaning |
| ------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| **recipe** | A component's token + CSS definition (in `EidosConfig.recipes` / `{name}.css`). |
| **token** | A CSS custom property. Public: `--{component}-*`; private recipe-internal: `--_{component}-*`. Never `--eidos-*` / `--soma-*`. |
| **TSC (Token Scope Contract)** | Where each token is allowed to be emitted (`:root` / `[data-{c}]` / by color / by event) + transitivity validation. → [`canon/tsc`](./canon/tsc.md) |
| **variant** | A fixed visual archetype (`solid`, `outline`, `ghost`, …). Canon of eidos — a theme cannot invent or redefine one. |
| **role** | One of the **9** canonical color roles (`primary`, `secondary`, `tertiary`, `neutral`, `affirm`, `fulfill`, `risk`, `threat`, `loss`). |
| **scaling / density** | Orthogonal structural axes: global zoom (90–110) vs spacing (compact/comfortable/spacious). |
| **theme** | A retint of the perceptually-fixed: it changes _which hex_ is `affirm`, never _what_ `outline` means. → [`THEMING`](./theming/reference.md) |
| **`data-dir`** | A component's own direction attribute, carrying the resolved value — opt-in, for a recipe that needs a hook which always matches; native `dir` carries the raw value and is absent when nobody asserted one. |
| **RTL-1** | The eidos lint rule (`npm run rtl:check`) that flags a logical inline anchor paired with a physical inline translate in the same CSS block. |
| **`rtl-physical:`** | The sanctioned comment that exempts a block from RTL-1 when its geometry genuinely is physical: `/* rtl-physical: <reason> */`. |

@ -85,8 +85,13 @@ el hallazgo original demostrándose) destapó y se adjudicó así:
## Los 8 guards rojos de `contracts.test.ts` (preexistentes, PRIMERA tarea de mañana)
Nunca los vio nadie (mi patrón de extracción no capturaba el fichero raíz;
sin CI nadie corría la suite). Sujetos verificados AJENOS a los diffs de P0:
FE DE ERRATAS (lectura del corpus 2026-08-26): «nunca los vio nadie» era
FALSO — vistos y contabilizados en 2026-06-27 (gradient-builder/CONTINUE.md:299,
«~12 PRE-EXISTING failures from other sessions' WIP») y reducidos de 12 a 3
por `cb554ae2e`; lo que no existía era una PUERTA que los detuviera, y
volvieron a crecer sin que nadie corriera la suite — que es exactamente el
hallazgo que este handoff vindica. (Mi patrón de extracción tampoco capturaba
el fichero raíz.) Sujetos verificados AJENOS a los diffs de P0:
1. Barrel de soma: falta `waveform -> Waveform` en el export público.
2. Filenames de provider filtrándose a un barrel público (1).
@ -174,7 +179,13 @@ estos 8 + política para los 4 flaky de carga (¿timeouts subidos? ¿retry?).
- ⚠ **La bolsa entrega valores CRUDOS** (número 20, no '20'); Svelte
stringifica al render. Asertos de bolsa ≠ asertos de DOM.
- ⚠ **El único escritor imperativo sancionado es el prewrite** de `trigger`
(commit declarado por el morfo). Todo lo demás: bolsa.
(commit declarado por el morfo). Todo lo demás: bolsa. FE DE ERRATAS
(corpus 2026-08-26): la ley tiene UNA excepción abierta en el árbol —
textarea-provider:164/191 escribe `style: height` imperativamente para el
autosize (geometría medida, con bug documentado en su README:96-103: pisa
el overflow). El cierre correcto es (b): altura por custom property que
eidos lee (GESTURES.md:404 regla 2), que arregla el bug de paso — encolado,
no ejecutado.
- ⚠ `npm run dev` lleva `--force`: primer morfo:check tras arrancar servidor
paga compilación fría → timeouts falsos (accordion/background/link).
Pasarlo DOS veces o precalentar. `morfo-check`/`perm`/`smoke` aceptan la URL

@ -15,9 +15,15 @@ _for_.
## The verification loop (the short version)
```bash
npm run check # types — svelte-kit sync && svelte-check (expect 0 errors)
npm run test # the vitest suite (one run)
npm run lint # prettier --check · npm run format to fix
npm run check:gate # types WITH the policy: src/ owes ZERO; web/ measured
# against the shrinking ledger scripts/check-debt.ts
npm run check # raw svelte-check (src/ expect 0; web/ carries the
# frozen ledger errors, which may only shrink)
npm run test # the vitest suite (one run)
npm run gate # everything the pre-push hook runs (see §The gate)
npm run lint # prettier --check · npm run format to fix
# (NOT in `gate` yet — pre-existing repo-wide format debt;
# the one-shot is `npm run format` on a quiet tree)
```
For a component you also run the contract validators (below). A change is not
@ -61,7 +67,7 @@ These catch classes of bug that `check` (types) and an HTTP 200 (SSR) miss.
| `npm run layer:check` | a **shared visual layer losing a cascade fight** — for every element carrying a layer hook (`data-viewport-placement`), asserts the computed `position`, that the stacking token resolved, and that no override slot declared inline computes to nothing (the signature of a custom-property CYCLE). Needs `npm run dev`. Its consumer list is DERIVED from who imports the layer, so a component joins the day it migrates. **Deliberate hole, stated in the script**: geometry. `getComputedStyle` reports the USED value, so an `inset: auto` reads back as pixels (measured: `-1976.7px`) — there is no property-level way to tell a dead `calc()` from an intended value. |
| `npm run translations:check` | missing / malformed translation keys. |
| `npm run docs:check` | **doc-corpus drift** — copied vocabulary counts vs the source consts, phantom fields (the legacy morfo text field; rejected API shapes), dependency claims vs `package.json`, the variant-vocab mirror in `component-audit.ts`, checklist↔audit rule-ID sync, and relative links (warn severity). Guards the "link the canon, never copy it" law of [`docs/authoring.md`](./authoring.md). |
| `scripts/eidos-lint.ts` · `eidos-lint-all.ts` | classifies every `[data-*]` selector in eidos CSS as **morfo-backed / eidos-only / invalid** — drift between the morfo contract and the CSS. (Opt-in safety net; the architectural defense is the typed `semaSelector` builder — see the "Eidos drift defense" rule in CLAUDE.md.) |
| `npm run eidos:lint` | classifies every `[data-*]` selector in eidos CSS as **morfo-backed / eidos-only / invalid** — drift between the morfo contract and the CSS. A `gate` member since P0 fase B (audit 2026-08-26); the architectural defense is still the typed `semaSelector` builder — see the "Eidos drift defense" rule in CLAUDE.md. |
| `src/uix/eidos/shared-layer-contract.test.ts` (vitest) | the **text half** a browser cannot cover for a shared layer: every zone has a rule, `stretch` stays on the axes it was scoped to, no axis is read without its override slot, no geometry rule keys on the component identity, and each consumer imports the layer / stamps the hook / mints no parallel token / re-asserts `position` when the primitive it composes declares one. It exists for ONE thing the computed check is blind to: `getComputedStyle` of a safe-area slot returns `"0px"` on desktop, so a `:dir(rtl)` remap with one half flipped reads identical to a correct one on every CI machine and only surfaces on a notched phone, sideways, in RTL. |
| `src/uix/contracts.test.ts` (vitest, via `npm run test`) | **catalogue invariants** that keep declarations / recipes coherent as the framework grows. Each fails on drift naming the offender, and excludes the active-dev-track set so it stays green for the maintained catalogue: **VG-8** every morfo is `as const satisfies Morfo`, never `: Morfo` (a `: Morfo` annotation widens the literal so the schema can't check it); **SYS-1 scope-drift** a component shipping an `eidos/components/{c}/` recipe declares `'eidos'` in `scope`; **A31** no per-item membership predicate (`isSelected` / `isItemPressed` / …) doing `.current.includes` (O(N²) — lift a `Set`, use `.has()`); **A30** `inputId` registered with the parent Field in the constructor, not wrapped in a `$effect`; **THEME-SYS-1** overlay z-index references the named `--z-index-overlay-*` scale, never a raw integer. |
@ -106,10 +112,12 @@ and headless tests working:
`npm run smoke` exists to catch.
- **The morfo contract server-renders** — since the render bag became the
single attr pipeline (P0 fase C, audit 2026-08-26), a part's `role` /
`aria-*` / `data-state` / literals ship in the server HTML, not only after
hydration. `src/uix/soma/ssr-contract.test.ts` (server project, node — no
window) pins it with a real-composition harness; it was born red against the
old client-only effect and is the canary for the whole pipeline.
`aria-*` / `data-state` / literals resolve at render time, server included.
The mechanism is SHARED (`partPropsForRegistration`) and pinned by
`src/uix/soma/ssr-contract.test.ts` (server project, node — no window) with
two representatives, Toggle and RadioGroup — a CANARY, not a per-provider
census; the per-component SSR snapshot census is still owed (informe P1).
It was born red against the old client-only effect.
## The gate

@ -332,11 +332,14 @@ export interface MorfoSemanticIntent {
/**
* Timing of the perceptual signal relative to the structural state change.
*
* Per src/docs/GUIA_IMPLEMENTACION_SEMAUIX.md §5.2:
* - `pre` — the signal precedes the state change (e.g. dismiss anim
* runs before the element actually unmounts).
* - `coincident` — signal runs while the process is in flight
* (e.g. sustain.progress).
* - `coincident` — mechanically IDENTICAL to `pre` (emit-then-handler);
* the distinct name declares that signal and mutation are INDIVISIBLE
* (the user IS the value changing — a drag, sustain.progress). Signed
* in docs/architecture/sema.md §slider: «coincident is
* emit-then-handler, like pre; the two are just declared indivisible».
* It is a semantic marker, not a third runtime timing.
* - `post` — signal follows the resolved state (e.g. commit.save +
* affirm: do not celebrate before the result exists).
*

@ -107,26 +107,63 @@ describe('resizeRect', () => {
});
it('keeps the rect inside [0,1]', () => {
const r = resizeRect({ x: 0.8, y: 0.8, width: 0.15, height: 0.15 }, 'se', 0.5, 0.5, undefined, 100, 100, 0.05);
const r = resizeRect(
{ x: 0.8, y: 0.8, width: 0.15, height: 0.15 },
'se',
0.5,
0.5,
undefined,
100,
100,
0.05
);
expect(r.x + r.width).toBeLessThanOrEqual(1.0000001);
expect(r.y + r.height).toBeLessThanOrEqual(1.0000001);
});
it('respects maxSize', () => {
const r = resizeRect({ x: 0.1, y: 0.1, width: 0.4, height: 0.4 }, 'se', 1, 1, undefined, 100, 100, 0.05, 0.5);
const r = resizeRect(
{ x: 0.1, y: 0.1, width: 0.4, height: 0.4 },
'se',
1,
1,
undefined,
100,
100,
0.05,
0.5
);
expect(r.width).toBeLessThanOrEqual(0.5 + 1e-6);
expect(r.height).toBeLessThanOrEqual(0.5 + 1e-6);
});
it('edge handle "e" resizes width only', () => {
const r = resizeRect({ x: 0.1, y: 0.1, width: 0.4, height: 0.4 }, 'e', 0.2, 0.1, undefined, 100, 100, 0.05);
const r = resizeRect(
{ x: 0.1, y: 0.1, width: 0.4, height: 0.4 },
'e',
0.2,
0.1,
undefined,
100,
100,
0.05
);
expect(approx(r.width, 0.6)).toBe(true);
expect(approx(r.height, 0.4)).toBe(true);
expect(approx(r.y, 0.1)).toBe(true);
});
it('edge handle "s" resizes height only', () => {
const r = resizeRect({ x: 0.1, y: 0.1, width: 0.4, height: 0.4 }, 's', 0.2, 0.1, undefined, 100, 100, 0.05);
const r = resizeRect(
{ x: 0.1, y: 0.1, width: 0.4, height: 0.4 },
's',
0.2,
0.1,
undefined,
100,
100,
0.05
);
expect(approx(r.width, 0.4)).toBe(true);
expect(approx(r.height, 0.5)).toBe(true);
expect(approx(r.x, 0.1)).toBe(true);
@ -134,7 +171,16 @@ describe('resizeRect', () => {
it('edge handle "n" with aspect derives width from height', () => {
// square viewport + aspect 1 → height change pulls width to match.
const r = resizeRect({ x: 0.2, y: 0.2, width: 0.4, height: 0.4 }, 'n', 0, -0.1, 1, 100, 100, 0.05);
const r = resizeRect(
{ x: 0.2, y: 0.2, width: 0.4, height: 0.4 },
'n',
0,
-0.1,
1,
100,
100,
0.05
);
expect(approx(r.height, 0.5)).toBe(true);
expect(approx(r.width, 0.5)).toBe(true);
});
@ -239,13 +285,13 @@ describe('CropperProvider', () => {
installSomaHarness();
const opts = cropperOpts();
const { result: p, cleanup } = withEffectRoot(() => CropperProvider.create(opts));
// Run the syncAttrs effect, so the DOM half below is not vacuous.
flushSync();
// Naming default (consumerWins, A-85): it rides the bag so the consumer's
// own `aria-label` — which stays in restProps — wins by merge policy.
// (The old DOM half asserted the retired syncAttrs effect skipped naming
// attrs; post-C2c nothing writes any attr imperatively, so that assert
// inspected a vacuum — corpus audit 2026-08-26.)
expect(p.props).toMatchObject({ 'aria-label': 'Crop image' });
expect(opts.ref.current!.hasAttribute('aria-label')).toBe(false);
cleanup();
});

@ -9,7 +9,10 @@ the surfaces.
The differentiator nobody ships: every color stop is a real `role="slider"` with
`aria-valuetext` and arrow / Home / End / Delete keys — gradient editing that
works from the keyboard and a screen reader. v1 edits linear / radial / conic
(mesh editing is the fast-follow).
(mesh editing is the fast-follow). A bound `mesh` value is OUT of v1 scope:
the shared `Gradient` model admits it, but the editor renders no stops for it
and `data-kind` would carry a value outside the declared enum — until the
enum widens with mesh support, don't bind mesh values to this component.
## Anatomy
@ -29,11 +32,11 @@ ColorPicker) read / write it without a soma part of their own.
## Parts
| Part | Element | Description |
| ---------- | -------- | --------------------------------------------------------------------- |
| `Provider` | `<div>` | State machine. Owns `value`, `selectedIndex`, `kind`, `angle`, drag. |
| `Track` | `<div>` | Stop rail / drag surface. Click empty rail to add a stop at that x. |
| `Stop` | `<div>` | One color stop — `role="slider"` thumb. Keyboard + pointer drag. |
| Part | Element | Description |
| ---------- | ------- | -------------------------------------------------------------------- |
| `Provider` | `<div>` | State machine. Owns `value`, `selectedIndex`, `kind`, `angle`, drag. |
| `Track` | `<div>` | Stop rail / drag surface. Click empty rail to add a stop at that x. |
| `Stop` | `<div>` | One color stop — `role="slider"` thumb. Keyboard + pointer drag. |
## Provider API (via `GradientBuilderProvider.require()`)
@ -45,48 +48,49 @@ ColorPicker) read / write it without a soma part of their own.
## ARIA
| Part | Attribute | Value |
| ----- | ----------------- | ---------------------------------------------- |
| Track | `role` | `group` |
| Track | `aria-label` | "Gradient stops" (translatable) |
| Stop | `role` | `slider` |
| Stop | `aria-valuemin` | `0` |
| Stop | `aria-valuemax` | `100` |
| Stop | `aria-valuenow` | Stop position as a percentage |
| Stop | `aria-valuetext` | "Stop 2 of 4, 40%" |
| Stop | `aria-label` | "Color stop 2" |
| Part | Attribute | Value |
| ----- | ---------------- | ------------------------------- |
| Track | `role` | `group` |
| Track | `aria-label` | "Gradient stops" (translatable) |
| Stop | `role` | `slider` |
| Stop | `aria-valuemin` | `0` |
| Stop | `aria-valuemax` | `100` |
| Stop | `aria-valuenow` | Stop position as a percentage |
| Stop | `aria-valuetext` | "Stop 2 of 4, 40%" |
| Stop | `aria-label` | "Color stop 2" |
## Data Attributes
| Part | Attribute | Values |
| -------- | -------------------------- | ---------------------------- |
| Provider | `data-gradient-builder` | Always present |
| Provider | `data-kind` | `linear` \| `radial` \| `conic` |
| Provider | `data-disabled` | Present when disabled |
| Track | `data-gradient-builder-track` | Always present |
| Stop | `data-gradient-builder-stop` | Always present |
| Stop | `data-selected` | Present on the active stop |
| Stop | `data-dragging` | Present while dragging |
| Part | Attribute | Values |
| -------- | ----------------------------- | ------------------------------------------------------------------------------------------- |
| Provider | `data-gradient-builder` | Always present |
| Provider | `data-kind` | `linear` \| `radial` \| `conic` |
| Provider | `data-disabled` | Present when disabled |
| Track | `data-gradient-builder-track` | Always present |
| Track | `data-kind` | `linear` \| `radial` \| `conic` (soma-owned value — the morfo declares it without a source) |
| Stop | `data-gradient-builder-stop` | Always present |
| Stop | `data-selected` | Present on the active stop |
| Stop | `data-dragging` | Present while dragging |
## Keyboard (Stop)
| Key | Action |
| ------------------------- | ------------------------------- |
| `ArrowLeft` / `ArrowDown` | Move stop −1% (Shift: −10%) |
| `ArrowRight` / `ArrowUp` | Move stop +1% (Shift: +10%) |
| `Home` | Move stop to 0% |
| `End` | Move stop to 100% |
| `Delete` / `Backspace` | Remove the stop (min 2 remain) |
| `Enter` / `Space` | Select the stop |
| Key | Action |
| ------------------------- | ------------------------------ |
| `ArrowLeft` / `ArrowDown` | Move stop −1% (Shift: −10%) |
| `ArrowRight` / `ArrowUp` | Move stop +1% (Shift: +10%) |
| `Home` | Move stop to 0% |
| `End` | Move stop to 100% |
| `Delete` / `Backspace` | Remove the stop (min 2 remain) |
| `Enter` / `Space` | Select the stop |
## Sema events
| Event | Family | Verb | Target | When |
| -------------- | -------- | ------- | ---------- | ------------------------------------------------- |
| `handle-pick` | `handle` | `pick` | `track` | A stop is grabbed (pointerdown) — pickup cue. |
| `handle-drag` | `handle` | `drag` | `track` | While a stop is dragged — haptic stream. |
| Event | Family | Verb | Target | When |
| -------------- | -------- | ------- | ---------- | ----------------------------------------------------------------------------- |
| `handle-pick` | `handle` | `pick` | `track` | A stop is grabbed (pointerdown) — pickup cue. |
| `handle-drag` | `handle` | `drag` | `track` | While a stop is dragged — haptic stream. |
| `commit-set` | `commit` | `set` | `provider` | Pointer release / keyboard nudge end / add / remove / recolor / kind / angle. |
| `commit-reset` | `commit` | `reset` | `provider` | `reset(g)` replaces the whole gradient. |
| `commit-reset` | `commit` | `reset` | `provider` | `reset(g)` replaces the whole gradient. |
The pack lives at `src/uix/sema/components/gradient-builder.ts` (handle pickup /
drag on the track, soft release on commit). Mirrors the `slider` pack.

@ -55,7 +55,7 @@ in v1.)
`sticky-provider.svelte.test.ts` (jsdom) asserts: the offset-derived
`rootMargin` per edge, the null-coerced root, the `data-stuck` flip on the
observer callback, `data-edge`/`aria-hidden` via `syncAttrs`, the marker +
observer callback, `data-edge`/`aria-hidden` via the render bag, the marker +
offset on the props bag, and observer teardown. It uses the shipped
`installSomaHarness` (identity translator) like every other provider unit
test — see the build notes on the doctrine tension.

Loading…
Cancel
Save

Powered by TurnKey Linux.