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.
11 KiB
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 recipefont-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.55opacity), select-010 (0.04emtracking). - which validator: none (THEMING §35-C is doctrine, not enforced).
- gap: bare numeric
z-indexvalues, bare opacity decimals where--opacity-*exists, and bareemletter-spacing where--tracking-*exists are not flagged anywhere. - proposed: a recipe-token lint that flags: (a) a
*-z/z-indextoken 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 bareem(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: truewhoseprops$derivedre-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'spropsobject 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: truein a class +role:/aria-…:/type:in itsprops) 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-activedescendantin its morfo AND callsdom.focus(item)/el.focus()on its option/item elements. - proposed: a lint — if a component's morfo declares
aria-activedescendanton any part, its provider file must not containdom.focus(/.focus()on item-class elements (heuristic:focus(neargetItems/items[). Pair with aperm:checkpermutation that assertsdocument.activeElementstays 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
focusblock is declarative but nothing verifies the provider instantiatesFocusScopewith a matchingtrap. Conversely a virtual-focus component (aria-activedescendant) declaringtrap:trueis contradictory. - proposed: (a) lint — if morfo
focus.trap:true, the provider must referenceFocusScope.use; (b) flag the contradictionaria-activedescendant+focus.trap:truein 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 thetextskey'toggle'). - which validator: morfo:check / translations:check (catches missing runtime keys, but an absolute idlangref resolves, so it stays silent).
- gap:
v.translationRef(key)wherekeyis not a declaredtextsentry (and especially when it's a literal#?…idlangref) bypasses the indirection and leavestextsentries dead — not flagged. - proposed: morfo lint —
v.translationRef(key, …)keyMUST be a key of the morfo'stextsmap; reject akeythat starts with#?(that's an absolute ref, belongs inv.commonRef/an idlangref constant, nottranslationRef). - 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 —: Morfois valid TS, just lossy). - gap: the
: Morfoannotation widens literal types (kebabs,v.literal(...)values, part names) socompileMorfo/createAttrslose exact-key narrowing — silent, no error. - proposed: a trivial lint/grep over
src/uix/morfo/components/*.ts— everyexport const *Morfomust be closed withas const satisfies Morfoand must NOT carry a: Morfoannotation. (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.rootdeliberately 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.tswhose return value is assigned to a field that is never invoked in a teardown ($effect(() => () => field())/ dispose()) → flag. Heuristic grep:$effect.rootin 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-resetevents the provider never callsruntime.trigger(...)for (onlyclose+ field/area events are fired). - which validator: none (
smoke/morfo:checkvalidate the morfo shape, not whether the provider exercises it). - gap: an event declared in
morfo.events[]but never reached by anyruntime.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[].namewith the set of string literals passed toruntime.trigger(...)/.trigger(...)in{kebab}-provider.svelte.ts; warn on any declared event with zero trigger sites. (Polymorphicclosefired viadismissWithis 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.currentinside a$effectinstead of a direct constructor assignment (number-field is the correct reference). - which validator: none (
morfo:check/perm:checkdon't inspect the registration shape; no loop fires today). - gap: A30 mandates direct assignment for id registration; a
$effectthat writes a parent's*Id.currentis 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) —
collapseissequence:'pre'and the runtimeevents.collapsehandler doesthis.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 topost). - which validator: none.
morfo:checkknows thesequencebut 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. Apreevent whoseevents: { <name>: () => { ...opts.X.current = ... } }handler mutates a bound state is a latent lag — undetected. - proposed: a lint over each provider's
runtime(...)eventsmap: ifevents.<name>body assigns to anopts.*.current/ bound state AND the morfo declares that eventsequence:'pre'→ warn "set state at the call-site or usesequence:'post'(lag)". (Skipemerge.dismiss/closeevents that drive a Presence/data-eventexit — those legitimately defer; the heuristic: flag only when no exit-animation path consumes the hold.) Pairs withperm:checkwhich 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.