docs(reconciliation): soma deps truth + checklist enforcement column + sema cascade renumber + archetype pointers

Phase 1d of PLAN-docs-reconciliation.
- soma/README s3 + SOMA_ARCHITECTURE s2/s6/s12 + popover/link-preview
  READMEs: positioning is the in-house engine (layers/floating +
  $ethereal); @floating-ui is devDep-only. Layers table gains Stacking /
  AxialDrag / ZoomPan / ImageProvider / ListSelection (popper/ is an empty
  leftover dir — not documented). clsx documented as it really is: NOT
  declared but still imported by props/props.ts via a svelte transitive
  (phantom dep — flagged for a decision, chip spawned).
- COMPONENT_COMPLETION_CHECKLIST: new Enforcement column on every rule
  table (audit | tool:X | manual) so declared-but-unchecked rules are
  explicit; morfo rule IDs renamed M-* -> A-* to grep-match what
  component-audit.ts emits; stale rows fixed against the corrected script
  (D-1.2 v2 9-tab, D-3.1 single eidosSnippet, E-2.2 wrapper import);
  implemented-but-undeclared rules added (R-2.7 literal typography,
  A-3.4b verb/family pairing); A-3.3 family count decopied; sI now
  requires an honest Enforcement value for new rules.
- Sema cascade numbering unified to 1 family - 2 intent - 3 morfo -
  4 runtime - 5a packs - 5b app across sema/README, engine.ts,
  resolver.ts, resolver.test.ts and soma/runtime.svelte.ts (three
  divergent schemes coexisted; CLAUDE.md already uses the chosen one).
- '24 archetypes' copied lists replaced by pointers to
  ARCHETYPE_VOCABULARY in active_architecture s6 + morfo/README.
- TriggerOptions.semantic JSDoc: phantom defaultSemantic removed
  (implemented shape is additive allowedFamilies, D.11); 'warns via
  logger' claim corrected — the override is silently ignored on
  non-polymorphic events (no warn exists in code).
npm run check: 61 errors = pre-existing baseline, 0 new.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
menubar-v4-safe
dev 3 months ago
parent d3c25db6e8
commit eeedd0f8ea

@ -16,7 +16,7 @@
## How to read this
Each rule has a **severity** and an **applicability**:
Each rule has a **severity**, an **applicability**, and an **enforcement**:
- **Severity**:
- `error` — blocks the component from being considered done.
@ -26,6 +26,14 @@ Each rule has a **severity** and an **applicability**:
- `all` — every public component.
- `interactive` — components with user actions (most). Identified by `morfo.events.length > 0` OR `morfo.parts.*.keyboard.length > 0`.
- `passive` — purely structural / display components (icon, avatar, breadcrumb, meter, progress). Allowed `0 events` only after README justifies it.
- **Enforcement** — who verifies the rule. Declaring a rule here does NOT
imply the audit script checks it; this column makes the gap explicit:
- `audit` — implemented in `scripts/component-audit.ts` (the report prints
the same rule ID).
- `tool:{name}` — enforced by another script/test (e.g. `tool:morfo:check`,
`tool:smoke`, `tool:check` for tsc/svelte-check).
- `manual` — human review; no mechanical check exists yet. Candidates for
promotion to `audit` are welcome (see §I).
---
@ -35,47 +43,48 @@ The morfo is DNA. If it's incomplete, every downstream layer is incomplete.
### A1 · Basics
| ID | Rule | Severity | Applicability |
| ----- | ---------------------------------------------------------------------------------------------------------------------------------------------------------- | -------- | ------------- |
| M-1.1 | Exports a single `{Name}Morfo` const satisfying `Morfo` | error | all |
| M-1.2 | Has `name`, `kebab`, `scope: ['soma', ...]` declared | error | all |
| M-1.3 | Has `texts.label` as a valid idlangref (`#?components.{kebab}.label\|Fallback`), catalog entry in `langs/components/{kebab}.ts` | error | all |
| M-1.4 | If interactive: `apg` URL declared pointing at the relevant W3C ARIA pattern | warn | interactive |
| ID | Rule | Severity | Applicability | Enforcement |
| ----- | ---------------------------------------------------------------------------------------------------------------------------------------------------------- | -------- | ------------- | ----------- |
| A-1.1 | Exports a single `{Name}Morfo` const satisfying `Morfo` | error | all | audit |
| A-1.2 | Has `name`, `kebab`, `scope: ['soma', ...]` declared | error | all | audit |
| A-1.3 | Has `texts.label` as a valid idlangref (`#?components.{kebab}.label\|Fallback`), catalog entry in `langs/components/{kebab}.ts` | error | all | audit |
| A-1.4 | If interactive: `apg` URL declared pointing at the relevant W3C ARIA pattern | warn | interactive | audit |
### A2 · Parts
| ID | Rule | Severity | Applicability |
| ----- | ---------------------------------------------------------------------------------------------------------------------------------------------------------- | -------- | ------------- |
| M-2.1 | Has at least one `Provider` part with `archetype: 'provider'` | error | all |
| M-2.2 | Every part declares `kebab`, `archetype`, `kind: 'public' \| 'internal'`, `defaultElement`, `role` | error | all |
| M-2.3 | Every public part has at least one `data-*` attr OR explicit justification in component README (`A2.3 exception: ...`) | warn | all |
| M-2.4 | Parts with non-trivial state declare `states: [...]` array | warn | interactive |
| M-2.5 | Archetype ∈ `ARCHETYPE_VOCABULARY` (`src/uix/morfo/types.ts`). No invented archetypes. | error | all |
| M-2.6 | Every part with focusable behavior has a `tabindex` or `role` that the browser focuses (no silent unfocusable interactive parts) | warn | interactive |
| ID | Rule | Severity | Applicability | Enforcement |
| ----- | ---------------------------------------------------------------------------------------------------------------------------------------------------------- | -------- | ------------- | ----------- |
| A-2.1 | Has at least one `Provider` part with `archetype: 'provider'` | error | all | audit |
| A-2.2 | Every part declares `kebab`, `archetype`, `kind: 'public' \| 'internal'`, `defaultElement`, `role` | error | all | manual |
| A-2.3 | Every public part has at least one `data-*` attr OR explicit justification in component README (`A2.3 exception: ...`) | warn | all | manual |
| A-2.4 | Parts with non-trivial state declare `states: [...]` array | warn | interactive | manual |
| A-2.5 | Archetype ∈ `ARCHETYPE_VOCABULARY` (`src/uix/morfo/types.ts`). No invented archetypes. | error | all | audit |
| A-2.6 | Every part with focusable behavior has a `tabindex` or `role` that the browser focuses (no silent unfocusable interactive parts) | warn | interactive | manual |
### A3 · Events — the part where current components leak
| ID | Rule | Severity | Applicability |
| ----- | ---------------------------------------------------------------------------------------------------------------------------------------------------------- | -------- | ------------- |
| M-3.1 | If interactive: `events.length >= 1` | error | interactive |
| M-3.2 | Every event has `name`, `semantic.family`, `semantic.target` (partRef) | error | all |
| M-3.3 | `semantic.family` ∈ `SEMA_FAMILIES` (7 valid) | error | all |
| M-3.4 | `semantic.verb` ∈ `SEMA_VERBS[family]` | error | all |
| M-3.5 | `semantic.sequence` ∈ `'pre' \| 'coincident' \| 'post'` declared explicitly | warn | interactive |
| M-3.6 | Event name follows `{verb}-{variant}` or `{family}-{verb}` pattern | warn | interactive |
| M-3.7 | **Event/keyboard coverage**: every distinct keyboard action that mutates state has a corresponding semantic event. Pure focus moves don't need an event. | error | interactive |
| M-3.8 | If component supports value reset/clear: `commit.reset` or `commit.discard` event declared | warn | interactive |
| M-3.9 | If component supports navigation steps (calendar, stepper, pagination): `shift.navigate` event declared with target part | warn | interactive |
| M-3.10 | Intent is declared (`'neutral' \| 'affirm' \| 'fulfill' \| 'risk' \| 'threat' \| 'loss'`) when family is valenced (commit/signal). `target.partRef` always set | warn | interactive |
| ID | Rule | Severity | Applicability | Enforcement |
| ----- | ---------------------------------------------------------------------------------------------------------------------------------------------------------- | -------- | ------------- | ----------- |
| A-3.1 | If interactive: `events.length >= 1` | error | interactive | audit |
| A-3.2 | Every event has `name`, `semantic.family`, `semantic.target` (partRef) | error | all | tool:check |
| A-3.3 | `semantic.family` ∈ `SEMA_FAMILIES` (source: `src/uix/sema/types.ts` — do not copy the list) | error | all | audit |
| A-3.4 | `semantic.verb` ∈ `SEMA_VERBS[family]` | error | all | audit |
| A-3.4b | Per-event `family.verb` pairing is canonical (no verb borrowed from another family) | warn | all | audit |
| A-3.5 | `semantic.sequence` ∈ `'pre' \| 'coincident' \| 'post'` declared explicitly | warn | interactive | audit |
| A-3.6 | Event name follows `{verb}-{variant}` or `{family}-{verb}` pattern | warn | interactive | audit |
| A-3.7 | **Event/keyboard coverage**: every distinct keyboard action that mutates state has a corresponding semantic event. Pure focus moves don't need an event. | error | interactive | audit |
| A-3.8 | If component supports value reset/clear: `commit.reset` or `commit.discard` event declared | warn | interactive | manual |
| A-3.9 | If component supports navigation steps (calendar, stepper, pagination): `shift.navigate` event declared with target part | warn | interactive | manual |
| A-3.10 | Intent is declared when family requires it per `SEMA_FAMILY_POLICY` (`src/uix/sema/types.ts`). `target.partRef` always set | warn | interactive | audit |
### A4 · ARIA + keyboard
| ID | Rule | Severity | Applicability |
| ----- | ---------------------------------------------------------------------------------------------------------------------------------------------------------- | -------- | ------------- |
| M-4.1 | Provider part has `aria-label` or `aria-labelledby` declared with `severity: 'recommended'` | warn | interactive |
| M-4.2 | If component has invalid/disabled/readonly/required state: matching `aria-invalid`/`aria-disabled`/`aria-readonly`/`aria-required` declared conditionally | error | interactive |
| M-4.3 | If APG pattern requires specific keys (e.g., Grid: Arrow×4 + Home/End/PageUp/PageDown), all are declared in part keyboard | warn | interactive |
| M-4.4 | No reinvented keys (`Spacebar` is `' '`; `Esc` is `'Escape'`; etc.) — must match KeyboardEvent.key values | error | interactive |
| ID | Rule | Severity | Applicability | Enforcement |
| ----- | ---------------------------------------------------------------------------------------------------------------------------------------------------------- | -------- | ------------- | ----------- |
| A-4.1 | Provider part has `aria-label` or `aria-labelledby` declared with `severity: 'recommended'` | warn | interactive | manual |
| A-4.2 | If component has invalid/disabled/readonly/required state: matching `aria-invalid`/`aria-disabled`/`aria-readonly`/`aria-required` declared conditionally | error | interactive | manual |
| A-4.3 | If APG pattern requires specific keys (e.g., Grid: Arrow×4 + Home/End/PageUp/PageDown), all are declared in part keyboard | warn | interactive | manual |
| A-4.4 | No reinvented keys (`Spacebar` is `' '`; `Esc` is `'Escape'`; etc.) — must match KeyboardEvent.key values | error | interactive | audit |
---
@ -83,34 +92,34 @@ The morfo is DNA. If it's incomplete, every downstream layer is incomplete.
### B1 · API shape (Option C disciplined)
| ID | Rule | Severity | Applicability |
| ----- | ---------------------------------------------------------------------------------------------------------------------------------------------------------- | -------- | ------------- |
| E-1.1 | Has `{name}.svelte` (root visual) + per-part files `{name}-{part}.svelte` | error | all |
| E-1.2 | `index.ts` does **explicit per-property assignment** (`X.Part = Part`), not `Object.assign(X, { ... })` — see DEMO_AUTHORING §12.3 | error | all |
| E-1.3 | `index.ts` exports `{Name}` named + `default {Name}` | error | all |
| E-1.4 | No exports of `Provider`, `Base`, `Root`, `Parts`, or `Soma{Name}Provider` | error | all |
| E-1.5 | Imports Soma as `import * as {Name} from '$soma/components/{kebab}'` — namespace, not destructured | warn | all |
| E-1.6 | Types: `{Name}Props`, `{Name}Size`, `{Name}Variant` (no `EidosX*` prefixes) | error | all |
| E-1.7 | For single-part components, the default IS the component (Toggle, Switch, Icon) — no fake compound API | error | all |
| ID | Rule | Severity | Applicability | Enforcement |
| ----- | ---------------------------------------------------------------------------------------------------------------------------------------------------------- | -------- | ------------- | ----------- |
| E-1.1 | Has `{name}.svelte` (root visual) + per-part files `{name}-{part}.svelte` | error | all | audit |
| E-1.2 | `index.ts` does **explicit per-property assignment** (`X.Part = Part`), not `Object.assign(X, { ... })` | error | all | audit |
| E-1.3 | `index.ts` exports `{Name}` named + `default {Name}` | error | all | audit |
| E-1.4 | No exports of `Provider`, `Base`, `Root`, `Parts`, or `Soma{Name}Provider` | error | all | audit |
| E-1.5 | Imports Soma as `import * as {Name} from '$soma/components/{kebab}'` — namespace, not destructured | warn | all | manual |
| E-1.6 | Types: `{Name}Props`, `{Name}Size`, `{Name}Variant` (no `EidosX*` prefixes) | error | all | manual |
| E-1.7 | For single-part components, the default IS the component (Toggle, Switch, Icon) — no fake compound API | error | all | manual |
### B2 · Files + structure
| ID | Rule | Severity | Applicability |
| ----- | ---------------------------------------------------------------------------------------------------------------------------------------------------------- | -------- | ------------- |
| E-2.1 | `types.ts` exports the public Props + size/variant/color unions | error | all |
| E-2.2 | `{name}.css` exists and is imported from `src/uix/eidos/index.css` | error | all |
| E-2.3 | `README.md` exists with baseline (Air or "no baseline"), external comparison table, decisions, gaps | error | all |
| E-2.4 | Every part declared in morfo (`kind: 'public'`) has either a wrapper file or an explicit README note explaining why it's not exposed | warn | all |
| ID | Rule | Severity | Applicability | Enforcement |
| ----- | ---------------------------------------------------------------------------------------------------------------------------------------------------------- | -------- | ------------- | ----------- |
| E-2.1 | `types.ts` exports the public Props + size/variant/color unions | error | all | audit |
| E-2.2 | `{name}.css` exists and is wired: imported by the component's own wrapper (current, code-split pattern) OR from `eidos/index.css` (layout primitives + shared visuals like spin-field) | error | all | audit |
| E-2.3 | `README.md` exists with baseline (Air or "no baseline"), external comparison table, decisions, gaps | error | all | audit |
| E-2.4 | Every part declared in morfo (`kind: 'public'`) has either a wrapper file or an explicit README note explaining why it's not exposed | warn | all | manual |
### B3 · Wrapper internals
| ID | Rule | Severity | Applicability |
| ----- | ---------------------------------------------------------------------------------------------------------------------------------------------------------- | -------- | ------------- |
| E-3.1 | `{name}.svelte` renders Soma's `{Name}.Provider` (or equivalent) — does NOT mount Soma `Trigger`/`Content` directly | error | all |
| E-3.2 | No `$state` re-declaration of bindable props from Soma (use `$bindable` proxy) | warn | all |
| E-3.3 | Snippets receive `children` prop and don't shadow it with `{#snippet children}` in same scope — see DEMO_AUTHORING §12.4 | error | all |
| E-3.4 | No data-*/CSS leaking from other layers: no `data-soma-*`, no `--soma-*`/`--air-*` CSS variables | error | all |
| E-3.5 | All visual props (`size`, `variant`, `color`, `radius`) map to `data-{prop}="value"` on the root for CSS to read | warn | all |
| ID | Rule | Severity | Applicability | Enforcement |
| ----- | ---------------------------------------------------------------------------------------------------------------------------------------------------------- | -------- | ------------- | ----------- |
| E-3.1 | `{name}.svelte` renders Soma's `{Name}.Provider` (or equivalent) — does NOT mount Soma `Trigger`/`Content` directly | error | all | manual |
| E-3.2 | No `$state` re-declaration of bindable props from Soma (use `$bindable` proxy) | warn | all | manual |
| E-3.3 | Snippets receive `children` prop and don't shadow it with `{#snippet children}` in same scope | error | all | manual |
| E-3.4 | No data-*/CSS leaking from other layers: no `data-soma-*`, no `--soma-*`/`--air-*` CSS variables | error | all | manual |
| E-3.5 | All visual props (`size`, `variant`, `color`, `radius`) map to `data-{prop}="value"` on the root for CSS to read | warn | all | manual |
---
@ -118,34 +127,35 @@ The morfo is DNA. If it's incomplete, every downstream layer is incomplete.
### C1 · State coverage
| ID | Rule | Severity | Applicability |
| ----- | ---------------------------------------------------------------------------------------------------------------------------------------------------------- | -------- | ------------- |
| R-1.1 | Has root selector `[data-{component}]` defining base layout/spacing | error | all |
| R-1.2 | If morfo declares `data-disabled` on any part: `[data-disabled]` styled | error | interactive |
| R-1.3 | If morfo declares `data-readonly`: `[data-readonly]` styled | warn | interactive |
| R-1.4 | If morfo declares `data-invalid`: `[data-invalid]` styled (using `--color-risk-element` or similar) | warn | interactive |
| R-1.5 | All focusable parts have `:focus-visible` styled | error | interactive |
| R-1.6 | Hover state defined for trigger-like archetypes (`trigger`, `item`, `option`, `close`, `action`) | warn | interactive |
| R-1.7 | Disabled state has `cursor: not-allowed` OR documented exception in README | warn | interactive |
| ID | Rule | Severity | Applicability | Enforcement |
| ----- | ---------------------------------------------------------------------------------------------------------------------------------------------------------- | -------- | ------------- | ----------- |
| R-1.1 | Has root selector `[data-{component}]` defining base layout/spacing | error | all | audit |
| R-1.2 | If morfo declares `data-disabled` on any part: `[data-disabled]` styled | error | interactive | audit |
| R-1.3 | If morfo declares `data-readonly`: `[data-readonly]` styled | warn | interactive | audit |
| R-1.4 | If morfo declares `data-invalid`: `[data-invalid]` styled (using `--color-risk-element` or similar) | warn | interactive | audit |
| R-1.5 | All focusable parts have `:focus-visible` styled | error | interactive | audit |
| R-1.6 | Hover state defined for trigger-like archetypes (`trigger`, `item`, `option`, `close`, `action`) | warn | interactive | manual |
| R-1.7 | Disabled state has `cursor: not-allowed` OR documented exception in README | warn | interactive | manual |
### C2 · Token discipline
| ID | Rule | Severity | Applicability |
| ----- | ---------------------------------------------------------------------------------------------------------------------------------------------------------- | -------- | ------------- |
| R-2.1 | No raw colors (hex/rgb/named). All colors come from `var(--color-*)` or `var(--{component}-*)`. Applies to `recipes/base.ts`, `archetypes.css`, `events.css` and every component `*.css` — no exceptions. | error | all |
| R-2.2 | No raw font-size in px/rem. Use `var(--font-size-*)` from Eidos recipe | warn | all |
| R-2.3 | No magic numbers in spacing — use `var(--space-*)` or `var(--{component}-*)` | warn | all |
| R-2.4 | Component-scoped tokens come from `EidosConfig.recipes` (`base.css`) — verify via `ActiveEidos.listRecipes()` | warn | all |
| R-2.5 | No `--eidos-*` or `--soma-*` variable invented in component recipe | error | all |
| R-2.6 | Every `var(--color-X)` referenced in recipes or component CSS is declared in `generated/base.css`. The theme contract (`SurfaceColorRoles`, `ContentColorRoles`, `BorderColorRoles`, `FocusColorRoles` + intent role maps) is the closed set; new tokens go through `themes/base.ts` + regen. | error | all |
| ID | Rule | Severity | Applicability | Enforcement |
| ----- | ---------------------------------------------------------------------------------------------------------------------------------------------------------- | -------- | ------------- | ----------- |
| R-2.1 | No raw colors (hex/rgb/named). All colors come from `var(--color-*)` or `var(--{component}-*)`. Applies to `recipes/base.ts`, `archetypes.css`, `events.css` and every component `*.css` — no exceptions. | error | all | audit |
| R-2.2 | No raw font-size in px/rem. Use `var(--font-size-*)` from Eidos recipe | warn | all | audit (via R-2.7) |
| R-2.3 | No magic numbers in spacing — use `var(--space-*)` or `var(--{component}-*)` | warn | all | manual |
| R-2.4 | Component-scoped tokens come from `EidosConfig.recipes` (`base.css`) — verify via `ActiveEidos.listRecipes()` | warn | all | manual |
| R-2.5 | No `--eidos-*` or `--soma-*` variable invented in component recipe | error | all | audit |
| R-2.6 | Every `var(--color-X)` referenced in recipes or component CSS is declared in `generated/base.css`. The theme contract (`SurfaceColorRoles`, `ContentColorRoles`, `BorderColorRoles`, `FocusColorRoles` + intent role maps) is the closed set; new tokens go through `themes/base.ts` + regen. | error | all | audit |
| R-2.7 | No literal typography in recipes (`font-size`, `line-height`, `letter-spacing`, `font-weight` raw values) — consume the foundation's type anchor via tokens. Escape valves: CSS keywords, numeric identities (`0`, `1`), or a same-line `/* literal: <reason> */` | warn | all | audit |
### C3 · Color resolution
| ID | Rule | Severity | Applicability |
| ----- | ---------------------------------------------------------------------------------------------------------------------------------------------------------- | -------- | ------------- |
| R-3.1 | Recipe uses `[data-color='X']` selectors only for the subset declared in component's README | warn | colored |
| R-3.2 | No legacy color names (`success`, `warning`, `danger`, `info`) | error | all |
| R-3.3 | Intent ↔ color resolution implemented: when component receives `intent != 'neutral'`, `data-color` reflects the intent | warn | colored |
| ID | Rule | Severity | Applicability | Enforcement |
| ----- | ---------------------------------------------------------------------------------------------------------------------------------------------------------- | -------- | ------------- | ----------- |
| R-3.1 | Recipe uses `[data-color='X']` selectors only for the subset declared in component's README | warn | colored | manual |
| R-3.2 | No legacy color names (`success`, `warning`, `danger`, `info`) | error | all | audit |
| R-3.3 | Intent ↔ color resolution implemented: when component receives `intent != 'neutral'`, `data-color` reflects the intent | warn | colored | manual |
### C4 · Recipe Contract — transversal systems
@ -159,14 +169,14 @@ The morfo is DNA. If it's incomplete, every downstream layer is incomplete.
> WIP tracks `words` / `palabras` / `chronos` are excluded. Escape valves: a same-line
> `/* literal: <reason> */` (values) or `/* functional: <reason> */` (keyframes).
| ID | Rule | Severity | Applicability |
| ----- | ---------------------------------------------------------------------------------------------------------------------------------------------------------- | -------- | ------------- |
| R-4.1 | No literal `box-shadow` — elevation goes through `var(--shadow-*)` / `var(--depth-{plane}-*)` (or the inset-ring pattern, which carries `var()`) | error | all |
| R-4.2 | No literal fractional `opacity` outside `@keyframes` — disabled/muted states consume `var(--opacity-*)` | error | all |
| R-4.3 | `:hover` backgrounds are the state-layer (`var(--state-*)`) or a palette token — no raw values, no hand-rolled `color-mix(… currentColor …)` | error | interactive |
| R-4.4 | No physical-axis token keys (`padding-x/-y`, `margin-x/-y`) in `lib/recipes/base.ts` — logical axes (`padding-inline/-block`) are the canon | error | all |
| R-4.5 | Local `@keyframes` require a `/* functional: … */` annotation — perceptual signatures live in `EidosConfig.motion`, not in component CSS | error | all |
| R-4.6 | No direct `var(--scale-*)` / `var(--primitive-*)` in component CSS — consume `var(--color-{role}-{slot})` or recipe tokens | error | all |
| ID | Rule | Severity | Applicability | Enforcement |
| ----- | ---------------------------------------------------------------------------------------------------------------------------------------------------------- | -------- | ------------- | ----------- |
| R-4.1 | No literal `box-shadow` — elevation goes through `var(--shadow-*)` / `var(--depth-{plane}-*)` (or the inset-ring pattern, which carries `var()`) | error | all | audit |
| R-4.2 | No literal fractional `opacity` outside `@keyframes` — disabled/muted states consume `var(--opacity-*)` | error | all | audit |
| R-4.3 | `:hover` backgrounds are the state-layer (`var(--state-*)`) or a palette token — no raw values, no hand-rolled `color-mix(… currentColor …)` | error | interactive | audit |
| R-4.4 | No physical-axis token keys (`padding-x/-y`, `margin-x/-y`) in `lib/recipes/base.ts` — logical axes (`padding-inline/-block`) are the canon | error | all | audit |
| R-4.5 | Local `@keyframes` require a `/* functional: … */` annotation — perceptual signatures live in `EidosConfig.motion`, not in component CSS | error | all | audit |
| R-4.6 | No direct `var(--scale-*)` / `var(--primitive-*)` in component CSS — consume `var(--color-{role}-{slot})` or recipe tokens | error | all | audit |
---
@ -174,89 +184,89 @@ The morfo is DNA. If it's incomplete, every downstream layer is incomplete.
### D1 · Template compliance
| ID | Rule | Severity | Applicability |
| ----- | ---------------------------------------------------------------------------------------------------------------------------------------------------------- | -------- | ------------- |
| D-1.1 | Outer element is `<div data-uix-canvas-inner>` | error | all |
| D-1.2 | Tab union is exactly `'live' \| 'api' \| 'morfo' \| 'sema' \| 'recipe' \| 'a11y'` | error | all |
| D-1.3 | Imports `compileMorfo` + the component's morfo | error | all |
| D-1.4 | Imports `getActiveUix` if Sema tab has interactive Play buttons | warn | interactive |
| D-1.5 | Has MutationObserver on `data-event` attribute, populating `trace` state | error | interactive |
| D-1.6 | Has `<div data-uix-stage>` between header and tablist (live always rendered) | error | all |
| D-1.7 | Has `<div data-uix-stage-trace>` with at least the trace strip | error | interactive |
| ID | Rule | Severity | Applicability | Enforcement |
| ----- | ---------------------------------------------------------------------------------------------------------------------------------------------------------- | -------- | ------------- | ----------- |
| D-1.1 | Outer element is `<div data-uix-canvas-inner>` | error | all | audit |
| D-1.2 | Tab union matches the canonical v2 9-tab template (`'live' \| 'system' \| 'motion' \| 'sema' \| 'services' \| 'api' \| 'morfo' \| 'recipe' \| 'a11y'`); the v1 6-tab union is accepted only pending migration | error | all | audit |
| D-1.3 | Imports `compileMorfo` + the component's morfo | error | all | audit |
| D-1.4 | Imports `getActiveUix` if Sema tab has interactive Play buttons | warn | interactive | manual |
| D-1.5 | Has MutationObserver on `data-event` attribute, populating `trace` state | error | interactive | audit |
| D-1.6 | Has `<div data-uix-stage>` between header and tablist (live always rendered) | error | all | audit |
| D-1.7 | Has `<div data-uix-stage-trace>` with at least the trace strip | error | interactive | audit |
### D2 · Header
| ID | Rule | Severity | Applicability |
| ----- | ---------------------------------------------------------------------------------------------------------------------------------------------------------- | -------- | ------------- |
| D-2.1 | Header has `data-uix-eyebrow` (Category · Name) | error | all |
| D-2.2 | Header has `<h1 data-uix-page-title>` and `<p data-uix-page-lede>` (single paragraph summary) | error | all |
| D-2.3 | Header has `data-uix-page-meta` with at minimum `parts` and `events` pills | error | all |
| ID | Rule | Severity | Applicability | Enforcement |
| ----- | ---------------------------------------------------------------------------------------------------------------------------------------------------------- | -------- | ------------- | ----------- |
| D-2.1 | Header has `data-uix-eyebrow` (Category · Name) | error | all | audit |
| D-2.2 | Header has `<h1 data-uix-page-title>` and `<p data-uix-page-lede>` (single paragraph summary) | error | all | audit |
| D-2.3 | Header has `data-uix-page-meta` with at minimum `parts` and `events` pills | error | all | audit |
### D3 · Snippet parity (DEMO_AUTHORING §8.1)
### D3 · Snippet parity (DEMO_AUTHORING §12)
| ID | Rule | Severity | Applicability |
| ----- | ---------------------------------------------------------------------------------------------------------------------------------------------------------- | -------- | ------------- |
| D-3.1 | Both `somaSnippet` and `eidosSnippet` derived and rendered | error | all |
| D-3.2 | Snippet code reflects current control values (not static placeholders) | warn | all |
| D-3.3 | If live preview uses a schema/options/state, snippet declares the same | warn | all |
| ID | Rule | Severity | Applicability | Enforcement |
| ----- | ---------------------------------------------------------------------------------------------------------------------------------------------------------- | -------- | ------------- | ----------- |
| D-3.1 | `eidosSnippet` derived and rendered (the v2 template's single snippet; v1 demos may additionally carry `somaSnippet`) | error | all | audit |
| D-3.2 | Snippet code reflects current control values (not static placeholders) | warn | all | manual |
| D-3.3 | If live preview uses a schema/options/state, snippet declares the same | warn | all | manual |
### D4 · Sema tab
| ID | Rule | Severity | Applicability |
| ----- | ---------------------------------------------------------------------------------------------------------------------------------------------------------- | -------- | ------------- |
| D-4.1 | Sema tab always rendered (even for 0-event components, with explicit empty state) | error | all |
| D-4.2 | Sema tab has events table: `name / family / verb / sequence / intent / play` | error | interactive |
| D-4.3 | Play buttons emit via `uix.events.emit(...)` onto a real DOM target inside the stage | error | interactive |
| ID | Rule | Severity | Applicability | Enforcement |
| ----- | ---------------------------------------------------------------------------------------------------------------------------------------------------------- | -------- | ------------- | ----------- |
| D-4.1 | Sema tab always rendered (even for 0-event components, with explicit empty state) | error | all | manual |
| D-4.2 | Sema tab has events table: `name / family / verb / sequence / intent / play` | error | interactive | manual |
| D-4.3 | Play buttons emit via `uix.events.emit(...)` onto a real DOM target inside the stage | error | interactive | audit |
### D5 · Morfo tab (declarative contract surface)
| ID | Rule | Severity | Applicability |
| ----- | ---------------------------------------------------------------------------------------------------------------------------------------------------------- | -------- | ------------- |
| D-5.1 | Has header table: name / kebab / scope / apg / parts / events | error | all |
| D-5.2 | Has parts overview table | error | all |
| D-5.3 | For each part with data/aria/keyboard: per-part subsection rendered | warn | all |
| D-5.4 | Events declaration table rendered | error | interactive |
| ID | Rule | Severity | Applicability | Enforcement |
| ----- | ---------------------------------------------------------------------------------------------------------------------------------------------------------- | -------- | ------------- | ----------- |
| D-5.1 | Has header table: name / kebab / scope / apg / parts / events | error | all | manual |
| D-5.2 | Has parts overview table | error | all | manual |
| D-5.3 | For each part with data/aria/keyboard: per-part subsection rendered | warn | all | manual |
| D-5.4 | Events declaration table rendered | error | interactive | manual |
### D6 · A11y tab + Recipe tab
| ID | Rule | Severity | Applicability |
| ----- | ---------------------------------------------------------------------------------------------------------------------------------------------------------- | -------- | ------------- |
| D-6.1 | A11y tab has keyboard table (from morfo) + ARIA contract table | warn | interactive |
| D-6.2 | Recipe tab lists selectors with their layer source (`morfo` / `eidos`) | warn | all |
| ID | Rule | Severity | Applicability | Enforcement |
| ----- | ---------------------------------------------------------------------------------------------------------------------------------------------------------- | -------- | ------------- | ----------- |
| D-6.1 | A11y tab has keyboard table (from morfo) + ARIA contract table | warn | interactive | manual |
| D-6.2 | Recipe tab lists selectors with their layer source (`morfo` / `eidos`) | warn | all | manual |
### D7 · Visible controls discipline (DEMO_AUTHORING §12.6)
| ID | Rule | Severity | Applicability |
| ----- | ---------------------------------------------------------------------------------------------------------------------------------------------------------- | -------- | ------------- |
| D-7.1 | Every control on the Live tab produces a visible change on the stage | warn | all |
| D-7.2 | No demo-only `data-*` attrs hand-stamped to fake morfo selectors | error | all |
| D-7.3 | Soma layer badge `[soma]` and Eidos layer badge `[eidos]` used to group control subsections | warn | all |
| D-7.4 | **Chip parity**: every chip-group control (`size`, `variant`, `color`) enumerates the **full** union of the component's type — no truncated arrays. Theme defines 3 variants → all 3 are selectable. See DEMO_AUTHORING §7.x. | error | all |
| D-7.5 | **Size coverage**: the chip array for `size` matches the component's category in DEMO_AUTHORING §7.x (form controls / text inputs / progress-meter / field-form expose `xs..xl`; nav controls expose `xs..lg`; passive panels keep `sm..lg`). | warn | all |
| ID | Rule | Severity | Applicability | Enforcement |
| ----- | ---------------------------------------------------------------------------------------------------------------------------------------------------------- | -------- | ------------- | ----------- |
| D-7.1 | Every control on the Live tab produces a visible change on the stage | warn | all | manual |
| D-7.2 | No demo-only `data-*` attrs hand-stamped to fake morfo selectors | error | all | manual |
| D-7.3 | Soma layer badge `[soma]` and Eidos layer badge `[eidos]` used to group control subsections | warn | all | manual |
| D-7.4 | **Chip parity**: every chip-group control (`size`, `variant`, `color`) enumerates the **full** union of the component's type — no truncated arrays. Theme defines 3 variants → all 3 are selectable. See DEMO_AUTHORING §6. | error | all | audit |
| D-7.5 | **Size coverage**: the chip array for `size` matches the component's declared union in recipe + types, 1:1 (form controls / text inputs / progress-meter / field-form expose `xs..xl`; nav controls expose `xs..lg`; passive panels keep `sm..lg`). | warn | all | manual |
---
## E. Cross-layer integrity
| ID | Rule | Severity | Applicability |
| ----- | ---------------------------------------------------------------------------------------------------------------------------------------------------------- | -------- | ------------- |
| X-1.1 | `npm run morfo:check` PASS for this component | error | all |
| X-1.2 | `npm run perm:check` PASS for this component if it's instrumented | warn | interactive |
| X-1.3 | `eidos-lint-all.ts` invalid count = 0 for this component | error | all |
| X-1.4 | `npm run check` does not produce errors in this component's files | error | all |
| X-1.5 | Component's smoke route loads without console errors (`SMOKE_SCOPE=/uix/components/{kebab} npm run smoke`) | error | all |
| ID | Rule | Severity | Applicability | Enforcement |
| ----- | ---------------------------------------------------------------------------------------------------------------------------------------------------------- | -------- | ------------- | ----------- |
| X-1.1 | `npm run morfo:check` PASS for this component | error | all | tool:morfo:check |
| X-1.2 | `npm run perm:check` PASS for this component if it's instrumented | warn | interactive | tool:perm:check |
| X-1.3 | `eidos-lint-all.ts` invalid count = 0 for this component | error | all | tool:eidos-lint |
| X-1.4 | `npm run check` does not produce errors in this component's files | error | all | tool:check |
| X-1.5 | Component's smoke route loads without console errors (`SMOKE_SCOPE=/uix/components/{kebab} npm run smoke`) | error | all | tool:smoke |
---
## F. Documentation completeness (component README)
| ID | Rule | Severity | Applicability |
| ----- | ---------------------------------------------------------------------------------------------------------------------------------------------------------- | -------- | ------------- |
| F-1.1 | README has section "## Baseline" with Air comparison or explicit "no Air baseline" | error | all |
| F-1.2 | README has section "## Comparativa" with at least 3 external references (Ark UI, Bits UI, Radix/shadcn, React Aria, MUI, or similar) | error | all |
| F-1.3 | README has section "## Decisiones" with explicit choices made vs alternatives | warn | all |
| F-1.4 | README has section "## Gaps" listing what's deferred / not implemented, each with disposition (implementar / diferir / descartar) | error | all |
| F-1.5 | If component is `passive` (0 events): README explicitly justifies why (`## Passive justification`) | error | passive |
| ID | Rule | Severity | Applicability | Enforcement |
| ----- | ---------------------------------------------------------------------------------------------------------------------------------------------------------- | -------- | ------------- | ----------- |
| F-1.1 | README has section "## Baseline" with Air comparison or explicit "no Air baseline" | error | all | audit |
| F-1.2 | README has section "## Comparativa" with at least 3 external references (Ark UI, Bits UI, Radix/shadcn, React Aria, MUI, or similar) | error | all | audit |
| F-1.3 | README has section "## Decisiones" with explicit choices made vs alternatives | warn | all | audit |
| F-1.4 | README has section "## Gaps" listing what's deferred / not implemented, each with disposition (implementar / diferir / descartar) | error | all | audit |
| F-1.5 | If component is `passive` (0 events): README explicitly justifies why (`## Passive justification`) | error | passive | audit |
---
@ -264,19 +274,19 @@ The morfo is DNA. If it's incomplete, every downstream layer is incomplete.
### G1 · Subset declaration
| ID | Rule | Severity | Applicability |
| ----- | ---------------------------------------------------------------------------------------------------------------------------------------------------------- | -------- | ------------- |
| G-1.1 | README declares `## Subset` listing which `color` values + which `intent` values the component accepts (per GUIA_IMPLEMENTACION_SEMAUIX §3) | warn | colored |
| G-1.2 | Types restrict `intent`/`color` props to the declared subset via union types — not free-form string | warn | colored |
| G-1.3 | Recipe CSS only matches `[data-color='X']` for X in the declared subset | warn | colored |
| ID | Rule | Severity | Applicability | Enforcement |
| ----- | ---------------------------------------------------------------------------------------------------------------------------------------------------------- | -------- | ------------- | ----------- |
| G-1.1 | README declares `## Subset` listing which `color` values + which `intent` values the component accepts (per GUIA_IMPLEMENTACION_SEMAUIX §3) | warn | colored | manual |
| G-1.2 | Types restrict `intent`/`color` props to the declared subset via union types — not free-form string | warn | colored | manual |
| G-1.3 | Recipe CSS only matches `[data-color='X']` for X in the declared subset | warn | colored | manual |
### G2 · Sequence canon
| ID | Rule | Severity | Applicability |
| ----- | ---------------------------------------------------------------------------------------------------------------------------------------------------------- | -------- | ------------- |
| G-2.1 | Events that animate exit before structural commit use `sequence: 'pre'` (dismiss, close-cancel, etc.) | warn | interactive |
| G-2.2 | Events that confirm a result use `sequence: 'post'` (commit-save, submit, etc.) | warn | interactive |
| G-2.3 | Continuous progress events use `sequence: 'coincident'` (sustain.progress) | warn | passive |
| ID | Rule | Severity | Applicability | Enforcement |
| ----- | ---------------------------------------------------------------------------------------------------------------------------------------------------------- | -------- | ------------- | ----------- |
| G-2.1 | Events that animate exit before structural commit use `sequence: 'pre'` (dismiss, close-cancel, etc.) | warn | interactive | manual |
| G-2.2 | Events that confirm a result use `sequence: 'post'` (commit-save, submit, etc.) | warn | interactive | manual |
| G-2.3 | Continuous progress events use `sequence: 'coincident'` (sustain.progress) | warn | passive | manual |
---
@ -296,9 +306,18 @@ A component is **BROKEN** when >5 errors OR any X-1.x script fails.
## I. How to add a new rule
1. Append to the appropriate section table.
2. Update `scripts/component-audit.ts` with a check function that returns `CheckResult`.
3. Add the rule to the doctrinal source if it crosses a layer (`active_architecture.md`, `GUIA_IMPLEMENTACION_SEMAUIX.md`, or `DEMO_AUTHORING_GUIDE.md`).
4. Document the rationale at the top of the new rule's check function.
Rules should be **doctrinally grounded** (cite the source doc) and **machine-checkable** (avoid pure aesthetic criteria — those go in the per-component README review).
1. Append to the appropriate section table **with its Enforcement value**.
2. If `Enforcement: audit`: implement it in `scripts/component-audit.ts` with a
check function that returns `CheckResult`, using the SAME rule ID the table
declares (the report and this doc must grep-match).
3. If `Enforcement: manual` / `tool:X`: no script change, but the value must be
honest — declaring a rule here does not make it checked.
4. Add the rule to the doctrinal source if it crosses a layer
(`active_architecture.md`, `docs/CANON.md`, or `DEMO_AUTHORING_GUIDE.md`).
5. Document the rationale at the top of the new rule's check function.
Rules should be **doctrinally grounded** (cite the source doc) and, when
`audit`, **machine-checkable** (avoid pure aesthetic criteria — those go in the
per-component README review). Keep this table and the script in sync: every
`audit` rule ID exists in the script, and every rule ID the script emits exists
here (`npm run docs:check` verifies both directions).

@ -608,19 +608,12 @@ hay double-write.
Dos vocabulary estables anclan la articulación:
**Archetypes** (`src/uix/morfo/types.ts:ARCHETYPE_VOCABULARY`):
```
provider · trigger · content · overlay · viewport
item · option · indicator · thumb · track
label · title · description · close · action
header · image · fallback · arrow · separator
group · input · segment · preview
```
24 categorías de parte que aparecen en múltiples componentes. Una `Trigger`
de Dialog, Popover, DropdownMenu y Tooltip son la misma categoría — Eidos
las puede estilar transversalmente con `[data-archetype=trigger]`.
**Archetypes** — categorías de parte que aparecen en múltiples componentes.
El inventario canónico es la const `ARCHETYPE_VOCABULARY`
(`src/uix/morfo/types.ts`) — no se copia aquí: la lista copiada divergió
(quedó en 24 cuando el código ya tenía 26). Una `Trigger` de Dialog,
Popover, DropdownMenu y Tooltip son la misma categoría — Eidos las puede
estilar transversalmente con `[data-archetype=trigger]`.
**Verbs** (`src/uix/sema/verbs.ts:SEMA_VERBS`), agrupados por familia:

@ -178,19 +178,14 @@ 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 (24 archetypes)
```
provider · trigger · content · overlay · viewport
item · option · indicator · thumb · track
label · title · description · close · action
header · image · fallback · arrow · separator
group · input · segment · preview
```
Defined in [`types.ts:ARCHETYPE_VOCABULARY`](./types.ts) and validated by
the sium schema. Optional field — omit when a part is genuinely unique to
its component (`Slider.Range`, `PinInput.Segment` internals).
### Vocabulary
The canonical inventory is the `ARCHETYPE_VOCABULARY` const in
[`types.ts`](./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
26). 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

@ -141,7 +141,7 @@ y dispatcha `(signal, effective)` a cada canal. Cada canal lee su slice
ignora el signature si no lo usa. Solo el canal visual bloquea al caller con
el hold perceptivo; los demás son fire-and-forget.
### Resolver y sema-map — cascada de 5 capas
### Resolver y sema-map — la cascada de resolución
El engine, en cada `emit`:
@ -149,23 +149,26 @@ El engine, en cada `emit`:
semánticos `data-event`, `data-event-family`, `data-event-intent`,
`data-event-phase`, `data-event-id` al `signal.target`. Estos son los
tokens que la cascade y la CSS de eidos leen.
2. Llama a `resolveSignature(signal, opts)` que aplica la cascada de 5
capas (cada una sobreescribe la anterior):
2. Llama a `resolveSignature(signal, opts)` que aplica la cascada
(numeración canónica **1 · 2 · 3 · 4 · 5a · 5b** — la misma en
`engine.ts`, `resolver.ts` y CLAUDE.md; cada capa sobreescribe la
anterior):
```
1. FAMILY base — SEMA_MAP.families[signal.family].base
sound / haptic + activeChannels + hold
2. INTENT deltas — SEMA_MAP.intents[signal.intent] cuando exista;
numbers ADD por defecto — son modificadores)
3. MORFO overrides — signal.overrides + signal.channels
(numbers REPLACE por defecto — son set values)
4. RUNTIME overrides — engineOpts.overrides.runtime (path-based globals;
baked into el map en el constructor; numbers REPLACE)
5. CASCADE rules — engineOpts.components (per-component packs prepended)
+ engineOpts.overrides.cascade (app-level appended;
gana en empate de specificity por declaration order).
Selectors CSS-like contra signal.target con los
data-event-* ya stampados; numbers REPLACE.
1. FAMILY base — SEMA_MAP.families[signal.family].base
sound / haptic + activeChannels + hold
2. INTENT deltas — SEMA_MAP.intents[signal.intent] cuando exista;
numbers ADD por defecto — son modificadores)
3. MORFO overrides — signal.overrides + signal.channels
(numbers REPLACE por defecto — son set values)
4. RUNTIME overrides — engineOpts.overrides.runtime (path-based globals;
baked into el map en el constructor; numbers REPLACE)
5a. PACK cascade — engineOpts.components (per-component packs)
5b. APP cascade — engineOpts.overrides.cascade (appended tras los
packs; gana en empate de specificity por
declaration order). Selectors CSS-like contra
signal.target con los data-event-* ya stampados;
numbers REPLACE.
```
3. Despacha a cada canal con la signature resuelta.
@ -176,7 +179,7 @@ El engine, en cada `emit`:
- **Capa 2 (intent.deltas)**: `pitch: -200` significa "restar 200 al
pitch base". Modificadores compositivos.
- **Capas 3, 4, 5 (overrides)**: `pitch: 720` significa "set pitch a
- **Capas 3, 4, 5a/5b (overrides)**: `pitch: 720` significa "set pitch a
720". Como CSS — `gain: 0.4` no añade, asigna.
- Para sumar explícitamente desde una capa de override: `{ op: 'add',
value: 100 }`.
@ -444,17 +447,17 @@ const semantic = new EngineSemantic({
sound: true,
haptic: true,
// Capa 6a — packs de componentes (defaults shipped con cada componente)
// Capa 5a — packs de componentes (defaults shipped con cada componente)
components: [dialogSema /* , toastSema, drawerSema, … */],
overrides: {
// Capa 5 — edits puntuales del SEMA_MAP, válidos a TODA la app
// Capa 4 — edits puntuales del SEMA_MAP, válidos a TODA la app
runtime: {
'families.commit.base.sound.pitch': 850,
'intents.threat.deltas.haptic.intensity': 0.4
},
// Capa 6b — rules CSS-like de la app, matched contra signal.target
// Capa 5b — rules CSS-like de la app, matched contra signal.target
// AFTER de los packs de componente
cascade: [
{
@ -474,9 +477,9 @@ const semantic = new EngineSemantic({
```
```ts
// Capa 4 — per-event override declarado en la propia morfo del componente.
// Capa 3 — per-event override declarado en la propia morfo del componente.
// Propaga vía SomaRuntime → SemanticSignal → resolver. La app puede
// seguir sobreescribiendo desde la cascade (capa 6).
// seguir sobreescribiendo desde la cascade (capas 5a/5b).
// src/uix/morfo/components/dialog.ts
{

@ -16,8 +16,9 @@
* 3. morfo per-event — signal.overrides + signal.channels
* 4. runtime path overrides — engineOpts.overrides.runtime
* (applied to map at construction)
* 5. cascade rules — engineOpts.components (per-component
* packs prepended) + engineOpts.overrides.cascade
* 5a. pack cascade — engineOpts.components (per-component
* packs, prepended)
* 5b. app cascade — engineOpts.overrides.cascade
* (app-level rules appended; win on tie)
*/
@ -68,7 +69,7 @@ export interface EngineSemanticOptions {
*/
components?: readonly Sema[];
/** Override layers 5 (runtime path) + 6b (app cascade). */
/** Override layers 4 (runtime path) + 5b (app cascade). */
overrides?: EngineSemanticOverrides;
/**
@ -119,10 +120,10 @@ export class EngineSemantic {
constructor(opts: EngineSemanticOptions = {}) {
this.logger = opts.logger;
// Layer 5 — runtime path overrides baked into the map at construction.
// Layer 4 — runtime path overrides baked into the map at construction.
this.map = applyMapOverrides(SEMA_MAP, opts.overrides?.runtime);
// Layer 6 — flatten per-component packs (lower precedence) +
// Layers 5a/5b — flatten per-component packs (lower precedence) +
// app-level cascade (higher precedence on tie).
const packCascade: SemaCascadeRule[] = [];
const preloadUrls = new Set<string>();
@ -213,7 +214,7 @@ export class EngineSemantic {
const id = signal.id ?? `sig-${this.nextSignalId++}`;
const enriched: SemanticSignal = { ...signal, id };
// Explicit silence (capa 4 morfo override): `signal.channels: []`
// Explicit silence (capa 3 morfo override): `signal.channels: []`
// skips EVERYTHING — no projection, no dispatch, no hold. The morfo
// declared this event has no perceptual surface. Other paths to an
// empty `effective.activeChannels` (e.g. signal with no family —

@ -196,7 +196,7 @@ describe('resolveSignature', () => {
expect(next.intents).toBe(SEMA_MAP.intents);
});
// ── Layer 6 — cascade rules ────────────────────────────────────────────
// ── Layers 5a/5b — cascade rules ───────────────────────────────────────
/**
* Minimal mock — resolver only calls `target.matches(selector)` and

@ -11,7 +11,11 @@
* 4. RUNTIME overrides — `options.runtimeMap` already has
* `engineOpts.overrides.runtime` baked in by the
* engine via `applyMapOverrides()` at construction
* 5. CASCADE rules — `options.cascade` — CSS-style rules matched
* 5a. PACK cascade — per-component packs (`engineOpts.components`),
* 5b. APP cascade — app-level rules (`engineOpts.overrides.cascade`),
* appended after the packs so they win on equal
* specificity. Both arrive pre-flattened in
* `options.cascade`: CSS-style rules matched
* against `signal.target` AFTER the engine has
* stamped `data-event-*` on it (see `stamp.ts`).
* Rules are flat: `{ selector, priority?,
@ -158,9 +162,10 @@ export function resolveSignature(
signature = applyOverride(signature, signal.overrides);
}
// Layer 5 — CSS-style cascade. Rules are matched via target.matches()
// — channel prepare hooks have already projected `data-event-*` on the
// target, so selectors that read those tokens match correctly.
// Layers 5a/5b — CSS-style cascade (packs + app rules, pre-flattened by
// the engine). Rules are matched via target.matches() — channel prepare
// hooks have already projected `data-event-*` on the target, so
// selectors that read those tokens match correctly.
if (options.cascade && options.cascade.length > 0 && signal.target) {
const matched = collectCascadeMatches(options.cascade, signal.target);
for (const rule of matched) {
@ -230,7 +235,7 @@ function ruleAsOverride(rule: SemaCascadeRule): SemaSignatureOverride {
return override as SemaSignatureOverride;
}
// ── Override application (layers 4 + 6) ───────────────────────────────────
// ── Override application (layers 3 + 5a/5b) ───────────────────────────────
//
// Overrides use REPLACE-by-default for primitive leaves (numbers, strings,
// booleans). Matches CSS intuition: `sound: { gain: 0.4 }` SETS gain to
@ -356,7 +361,7 @@ function applyLeaf(
return out;
}
// ── Runtime path-based map mutation (layer 5) ─────────────────────────────
// ── Runtime path-based map mutation (layer 4) ─────────────────────────────
/**
* Apply path-based runtime overrides to the SemaMap, producing a new map

@ -73,13 +73,19 @@ soma solo depende de:
- svelte (runes: $state, $derived, $effect)
- runed (Context, watch)
- clsx (class merging)
- @floating-ui (positioning)
- tabbable (focus order)
- `$libs/reactive`, `$libs/days`, `$libs/datagrid`, `$libs/forms`, etc. —
utilidades puras del repo (no façades)
- `$uix/morfo` — el contrato cross-layer (compileMorfo + SomaRuntime)
- `$uix/sema` — vocabulario semántico + EngineSemantic
El posicionamiento flotante es motor propio (`layers/floating` +
`$ethereal`); `@floating-ui` quedó como devDependency (demo + parity
tests), no se importa en la librería. `clsx` NO está declarada en
`package.json` pero `props/props.ts` aún la importa (resuelve como
transitiva de svelte — deuda: declararla o inlinearla). La lista
autoritativa es `package.json > dependencies`.
soma NO depende de eidos. La capa visual lee del DOM y de los tipos
públicos del soma; la dirección del acoplamiento es eidos → soma, no
al revés.

@ -39,7 +39,7 @@ Cada capa tiene responsabilidades estrictas:
- contexto y composicion de partes
- servicios de runtime (`langs`, `format`, `logger`)
- sistema de animaciones (presence, data-starting/ending-style, onComplete)
- posicionamiento flotante (@floating-ui)
- posicionamiento flotante (motor propio: `layers/floating` + `$ethereal`)
### La capa visual aporta
@ -423,11 +423,16 @@ Dos interfaces de opts:
| `TextSelection` | `TextSelection.use(opts)` | Previene selection overflow durante drag. |
| `ScrollLock` | `new ScrollLock(initial?, delay?)` | Body scroll lock con refcount. Soporta delay para animaciones. |
| `ResizeObserver$` | `new ResizeObserver$(getter, cb)` | ResizeObserver con lifecycle Svelte. |
| `Floating*` | `FloatingProvider.create()`, `FloatingContent.create(opts)`, etc. | Posicionamiento relativo a anchor via @floating-ui. |
| `Floating*` | `FloatingProvider.create()`, `FloatingContent.create(opts)`, etc. | Posicionamiento relativo a anchor — motor propio (`layers/floating` + `$ethereal`; `@floating-ui` es devDep de tests de paridad). |
| `Gesture.base` | `Gesture.base(opts)` | Pointer tracking + axis lock + velocity. |
| `Gesture.drag` | `Gesture.drag(opts)` | Base + progress + snap points + dismiss. |
| `Gesture.resize` | `Gesture.resize(opts)` | Base + delta + min/max constraints. |
| `SafePolygon` | `new SafePolygon(opts)` | Hover-gap corridor between trigger↔content. |
| `SafePolygon` | `new SafePolygon(opts)` (`floating/safe-polygon.ts`) | Hover-gap corridor between trigger↔content. |
| `Stacking` | registry module-level (`stacking.svelte.ts`) | Z-order compartido de superficies movibles (FloatPanel): `bringToFront`, `data-topmost`/`data-behind`. |
| `AxialDrag` | `new AxialDrag(opts)` (`manipulation/`) | Drag axial (un eje) con snap points + release state. |
| `ZoomPan` | `new ZoomPan(config)` | Scale + pan de contenido en viewport fijo (Cropper); estado + math puros. |
| `ImageProvider` | `new ImageProvider(opts)` | Estado de carga de imagen (`idle/loading/loaded/error`) con delay. |
| `ListSelection` | funciones puras (`list-selection.ts`) | Máquina de selección single/multi + `allowDeselect` compartida por Select/Combobox. |
`layers/floating/placement.ts` es la fuente unica para `Side`, `Align`,
`Boundary`, `SIDE_OPTIONS` y `ALIGN_OPTIONS`. `floating/types.ts` consume esa
@ -894,9 +899,16 @@ import { Portal, Arrow, VisuallyHidden, Soma } from '$soma/components/internal';
`soma` distingue entre:
- internos: `layers/`, `reactive/`, `dom/`, `provider/` — helpers propios
- externos: `@floating-ui/dom`, `runed`, `tabbable` — dependencias npm
Si una dependencia tiene API inestable o podria cambiar, se accede a traves de una frontera formal (como `layers/floating/` wrappea @floating-ui). Las dependencias estables (runed, svelte) se importan directamente.
- externos: `runed`, `tabbable` — dependencias npm declaradas (`clsx` se
importa en `props/props.ts` sin declarar — resuelve como transitiva de
svelte; deuda pendiente de decisión)
El posicionamiento flotante dejó de ser dependencia externa: es motor
propio (`layers/floating` + `$ethereal`); `@floating-ui` sobrevive solo
como devDependency para los tests de paridad. Si una dependencia tiene
API inestable o podria cambiar, se accede a traves de una frontera formal
(como `layers/floating/` hace con el motor de posicionamiento). Las
dependencias estables (runed, svelte) se importan directamente.
## 13. Estructura del directorio

@ -1,6 +1,6 @@
# LinkPreview
A floating preview card that appears on hover over a link. Supports configurable open/close delays, positioning via @floating-ui, safe pointer transitions, and dismissal.
A floating preview card that appears on hover over a link. Supports configurable open/close delays, positioning via the in-house floating layer (`soma/layers/floating` + `$ethereal`), safe pointer transitions, and dismissal.
## Anatomy
@ -21,7 +21,7 @@ A floating preview card that appears on hover over a link. Supports configurable
| ---------- | ------- | --------------------------------------------------------------- |
| `Provider` | none | Root context. Manages open state, delays, and pointer tracking. |
| `Trigger` | `<a>` | Link element. Opens preview on pointer hover. |
| `Content` | `<div>` | Floating preview card. Positioned via @floating-ui. |
| `Content` | `<div>` | Floating preview card. Positioned via the floating layer. |
| `Arrow` | `<svg>` | Optional arrow pointing toward the trigger. |
## ARIA

@ -1,6 +1,6 @@
# Popover
A floating panel anchored to a trigger element. Supports focus management, dismissal, positioning via `@floating-ui`, and an optional overlay.
A floating panel anchored to a trigger element. Supports focus management, dismissal, positioning via the in-house floating layer (`soma/layers/floating` + `$ethereal`), and an optional overlay.
## Anatomy

@ -285,7 +285,7 @@ export interface TriggerOptions {
* declared in `semantic.overrides`; on key conflict, the per-call value
* wins (per-call > morfo-declared).
*
* Cascade rules (capa 6) STILL win on conflict — a cascade rule that
* Cascade rules (capas 5a/5b) STILL win on conflict — a cascade rule that
* sets `sound.pitch` will override a per-call pitch. To make a per-emit
* value the source of truth, the cascade rule should NOT set the same
* primitive (only set `channels` to activate the channel).
@ -323,13 +323,14 @@ export interface TriggerOptions {
message?: string;
/**
* Polymorphic event concretion (book §5.3). When the morfo event
* declares `allowedFamilies` + `defaultSemantic`, the caller can
* commit to a concrete shape here. Validated at runtime against
* declares `allowedFamilies` (its regular `family`/`verb` act as the
* default shape — additive design, LIBRO_VARIACIONES D.11), the caller
* can commit to a concrete shape here. Validated at runtime against
* `allowedFamilies` — passing a family outside the allowlist raises
* `SomaRuntimePolymorphicError`.
*
* Ignored for non-polymorphic events (those that declare a concrete
* `family`); pass-through warns via logger if present.
* Silently ignored for non-polymorphic events (those without
* `allowedFamilies`).
*/
semantic?: {
family: SemaFamily;
@ -673,7 +674,7 @@ export function createSomaRuntime(morfo: Morfo, sources: SomaRuntimeSources): So
const family = resolvedFamily;
// Intent comes from the resolved morfo declaration (concrete
// `intent` field for non-polymorphic, or the polymorphic
// override / defaultSemantic for polymorphic). Components that
// override / declared default for polymorphic). Components that
// want a consumer prop to flow through declare `intent:
// { fromProp: 'intent', … }` on the event. Transitional families
// (emerge / shift / sustain) MAY declare intent now (canon

Loading…
Cancel
Save

Powered by TurnKey Linux.