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

143 lines
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 <!-- id: VG-001 -->
- 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 <!-- id: VG-002 -->
- 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 <!-- id: VG-003 -->
- 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 <!-- id: VG-004 -->
- 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 <!-- id: VG-005 -->
- 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 <!-- id: VG-006 -->
- 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 <!-- id: VG-007 -->
- 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` <!-- id: VG-008 -->
- 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 <!-- id: VG-009 -->
- 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) <!-- id: VG-010 -->
- 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) <!-- id: VG-011 -->
- 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) <!-- id: VG-012 -->
- 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.