# Validator gaps — rolling updated-at: 2026-06-26 batches-complete: 1 For each behavioral/contract/theming finding that a validator *should* have caught but didn't, the proposed validator extension. Fixing the validator prevents the whole class — highest leverage. ## VG-1 · Coherence guard misses `em`/`%` font-size literals - finding: select-008 (`item-description-font-size: '0.85em'`). - which validator: `recipe-css-contract.test.ts` (THEMING §5 guard: "no recipe `font-size-*` token may be a literal px/rem — must reference `--font-size-*`"). - gap: the guard checks **px/rem** literals only; a bare `em` (or `%`) font-size slips through. - proposed: extend the guard to reject ANY non-`var()` value on a `*font-size*` recipe token (px/rem/em/%/number). - effort: low (regex/value-shape check already exists). ## VG-2 · No check for magic z-index / magic opacity / magic letter-spacing in recipes - findings: SYS-2 (content-z/overlay-z 5/5), dropdown-menu-003 (`0.55` opacity), select-010 (`0.04em` tracking). - which validator: none (THEMING §35-C is doctrine, not enforced). - gap: bare numeric `z-index` values, bare opacity decimals where `--opacity-*` exists, and bare `em` letter-spacing where `--tracking-*` exists are not flagged anywhere. - proposed: a recipe-token lint that flags: (a) a `*-z`/`z-index` token whose value is a bare integer; (b) an `*opacity*` token whose value is a bare decimal (suggest `--opacity-*`); (c) a `*letter-spacing*`/ `*tracking*` token whose value is a bare `em` (suggest `--tracking-*`). Allow an explicit `// coherence-ok:` escape for genuinely physical values (scrim opacity). - effort: medium. ## VG-3 · No check for `syncAttrs:true` + manual re-set of a morfo-declared attr - findings: dialog-001 (role/aria-roledescription — live a11y race), popover-001 (Close type/aria-label). - which validator: none (morfo:check validates the morfo, not the provider's props block). - gap: a part registered `syncAttrs: true` whose `props` `$derived` re-declares a key the morfo already resolves is a double-write (active_architecture §7.7) — undetected. - proposed: a provider lint (AST or grep-assisted) — for each `runtime.part(part, { syncAttrs: true })`, flag any literal key in that part's `props` object that matches a morfo-declared attr name for that part (role / aria-* / type / the data-* WITH a value source). Hard part: associating the props block with the part; a heuristic grep (`syncAttrs: true` in a class + `role:`/`aria-…:`/`type:` in its `props`) catches most. - effort: medium-high; high value (a11y correctness). ## VG-4 · No check that `aria-activedescendant` (virtual focus) excludes real `dom.focus()` on items - finding: combobox-001 (A17 violation — virtual + real focus mixed). - which validator: none. - gap: A17 is doctrine; nothing flags a provider that both declares `aria-activedescendant` in its morfo AND calls `dom.focus(item)` / `el.focus()` on its option/item elements. - proposed: a lint — if a component's morfo declares `aria-activedescendant` on any part, its provider file must not contain `dom.focus(` / `.focus()` on item-class elements (heuristic: `focus(` near `getItems`/ `items[`). Pair with a `perm:check` permutation that asserts `document.activeElement` stays on the input. - effort: medium (lint heuristic) + the perm-runner assertion (higher fidelity). ## VG-5 · No check that morfo `focus.trap` is actually implemented - finding: select-002 (morfo `focus.trap:true` + `initial:'first-focusable'`, provider has no FocusScope). - which validator: none. - gap: the morfo `focus` block is declarative but nothing verifies the provider instantiates `FocusScope` with a matching `trap`. Conversely a virtual-focus component (aria-activedescendant) declaring `trap:true` is contradictory. - proposed: (a) lint — if morfo `focus.trap:true`, the provider must reference `FocusScope.use`; (b) flag the contradiction `aria-activedescendant` + `focus.trap:true` in the same morfo. - effort: low-medium. ## VG-6 · `translationRef` key not validated against the `texts` map - finding: combobox-003 (passes the full idlangref `'#?components.combobox.toggle|Toggle'` as the key instead of the `texts` key `'toggle'`). - which validator: morfo:check / translations:check (catches missing runtime keys, but an absolute idlangref resolves, so it stays silent). - gap: `v.translationRef(key)` where `key` is not a declared `texts` entry (and especially when it's a literal `#?…` idlangref) bypasses the indirection and leaves `texts` entries dead — not flagged. - proposed: morfo lint — `v.translationRef(key, …)` `key` MUST be a key of the morfo's `texts` map; reject a `key` that starts with `#?` (that's an absolute ref, belongs in `v.commonRef`/an idlangref constant, not `translationRef`). - effort: low. ## VG-7 · eidos-lint not enforced for undeclared parts - findings: select-005 (`data-select-indicator`/`item-indicator`), dialog-006 (`data-dialog-header`/`footer`). - which validator: `scripts/eidos-lint.ts` — DOES classify `[data-{c}-{part}]` selectors as morfo-backed / eidos-only / invalid, but it's opt-in (THEMING/TSC framing: "secondary safety net"). - gap: undeclared `data-{c}-{part}` selectors aren't blocked; there's no agreed disposition for legitimate eidos-only decorative/layout parts. - proposed: (a) run eidos-lint in CI with an allow-list file for sanctioned eidos-only parts; (b) adopt SYS-6's convention (declare decorative parts `kind:'internal'` in the morfo) so the lint can be strict. - effort: low (wiring) + the per-component convention decision. ## VG-8 · `morfo:check` does not flag `: Morfo` instead of `as const satisfies Morfo` - finding: alert-dialog-001 (HIGH) — `export const alertDialogMorfo: Morfo = {…}` (the only morfo in 16 audited using the annotation form). - which validator: `morfo:check` / `npm run check` (TS compiles fine — `: Morfo` is valid TS, just lossy). - gap: the `: Morfo` annotation widens literal types (kebabs, `v.literal(...)` values, part names) so `compileMorfo`/`createAttrs` lose exact-key narrowing — silent, no error. - proposed: a trivial lint/grep over `src/uix/morfo/components/*.ts` — every `export const *Morfo` must be closed with `as const satisfies Morfo` and must NOT carry a `: Morfo` annotation. (The single cheapest high-value gap.) - effort: trivial. ## VG-9 · No check for an undisposed `$effect.root` in a provider - finding: navigation-menu-006 (HIGH) — `openedAtEffect = $effect.root(...)` whose returned disposer is stored but never called → a detached reactive root leaks per trigger instance. - which validator: none. - gap: `$effect.root` deliberately escapes the component effect scope and MUST be manually disposed; nothing flags the case where the returned cleanup isn't wired into a teardown. (Legit uses are tests, which DO call the cleanup.) - proposed: a lint — `$effect.root(` in a non-test `*-provider.svelte.ts` whose return value is assigned to a field that is never invoked in a teardown (`$effect(() => () => field())` / dispose()) → flag. Heuristic grep: `$effect.root` in a provider + no matching invocation of the assigned identifier. - effort: medium. ## VG-10 · No check that a declared morfo event is ever fired (inert events) - finding: SYS-INERT (B3) — date/date-range/time/time-range/color-picker each declare `open` + `commit-reset` events the provider never calls `runtime.trigger(...)` for (only `close` + field/area events are fired). - which validator: none (`smoke`/`morfo:check` validate the morfo shape, not whether the provider exercises it). - gap: an event declared in `morfo.events[]` but never reached by any `runtime.trigger('')` in the provider is dead contract — it exists only to pass the schema. Lead-confirmed by grep (trigger-call counts per picker). - proposed: a lint that, per component, intersects `morfo.events[].name` with the set of string literals passed to `runtime.trigger(...)` / `.trigger(...)` in `{kebab}-provider.svelte.ts`; warn on any declared event with zero trigger sites. (Polymorphic `close` fired via `dismissWith` is reached — match the resolved name, or allow an opt-out comment.) - effort: medium. Catches a recurring picker-family contract gap. ## VG-11 · No check for `$effect`-wrapped parent-id registration (A30 doctrine) - finding: SYS-A30-EFFECT (B4) — date-field/time-field/color-field register `field.inputId.current = opts.id.current` inside a `$effect` instead of a direct constructor assignment (number-field is the correct reference). - which validator: none (`morfo:check`/`perm:check` don't inspect the registration shape; no loop fires today). - gap: A30 mandates direct assignment for id registration; a `$effect` that writes a parent's `*Id.current` is the latent-freeze shape even when currently write-only. Nothing flags it. - proposed: a lint — a `$effect(...)` body whose only statement writes `.Id.current = opts.id.current` (a parent-id registration) → warn "use a direct constructor assignment (A30)". Composes with VG-9 ($effect.root). - effort: medium. ## VG-12 · No check for `sequence:'pre'` event whose runtime handler sets the bound state (the lag class) - finding: collapsible-NEW-001 (HIGH) — `collapse` is `sequence:'pre'` and the runtime `events.collapse` handler does `this.opts.open.current = false`; the runtime serializes emit-then-handler, so the state lags the ~240ms hold (the documented Checkbox 244ms-lag class, which was fixed by flipping to `post`). - which validator: none. `morfo:check` knows the `sequence` but not where the provider sets the state. - gap: the doctrine "a control that sets functional state INSIDE the runtime.trigger handler must use `sequence:'post'`" is enforced by humans only. A `pre` event whose `events: { : () => { ...opts.X.current = ... } }` handler mutates a bound state is a latent lag — undetected. - proposed: a lint over each provider's `runtime(...)` `events` map: if `events.` body assigns to an `opts.*.current` / bound state AND the morfo declares that event `sequence:'pre'` → warn "set state at the call-site or use `sequence:'post'` (lag)". (Skip `emerge.dismiss`/`close` events that drive a Presence/`data-event` exit — those legitimately defer; the heuristic: flag only when no exit-animation path consumes the hold.) Pairs with `perm:check` which could measure the click→state-change latency. - effort: medium; high value (catches a perceptible-UX regression class the morfo author can rationalize wrongly). ## Notes - VG-3, VG-4 (B1) and VG-8 (B2) are the highest-value mechanical wins (each catches a real shipped defect class). VG-8 is the single cheapest — a grep would have caught alert-dialog-001. Re-evaluate as later batches recur.