You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
svelte-kit-vice/audit/VALIDATOR_GAPS.md

11 KiB

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('<name>') 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 <parent>.<x>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: { <name>: () => { ...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.<name> 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.

Powered by TurnKey Linux.