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.
143 lines
11 KiB
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.
|