8.0 KiB
Audit: grid-list
audit-version: 1
audited-at: 2026-06-26
scope: (SCOPE-DRIFT → SYS-1)
method: adversarially-verified workflow (analyze → refute); HIGH lead-verified by direct read of the cited code. B5 ground-truth: the A31 O(N²) isSelected/isExpanded (.includes from a per-item $derived) is confirmed across listbox/grid-list/tree-view/tree-grid/tag-group (SYS-7); rovingTargetEl is correctly LIFTED everywhere (not A31); virtual-* use SvelteMap (A33-clean).
provider: G:/dev/svelte/vicen/src/uix/soma/components/grid-list/grid-list-provider.svelte.ts
reactivity (A31/A33/A35): GRID-LIST-A31-HAZARD: Per-row isSelected = $derived.by(() => this.provider.isSelected(this.opts.value.current)) at line 493 and per-checkbox isChecked = $derived.by() at line 612 call isSelected(value) method (line 150-152) which does this.opts.value.current.includes(value) — array linear scan O(N) per row/checkbox. For N rows, total O(N²) on any selection mutation. Lifted fix: move to pro
Summary
Counts (post-verification): CRITICAL 0 · HIGH 2 · MEDIUM 1 · LOW 0.
Findings
HIGH: A31: Per-item derived calling provider method that reads global state causes O(N²) re-runs — grid-list-001
- dimension: B - Behavior (A31 reactivity)
- rule: A31: Per-item derived calling provider method that reads global state causes O(N²) re-runs on any mutation
- location: G:/dev/svelte/vicen/src/uix/soma/components/grid-list/grid-list-provider.svelte.ts:493
- evidence: GridListRowProvider line 493:
readonly isSelected = $derived.by(() => this.provider.isSelected(this.opts.value.current))calls line 150-152:isSelected(value: string): boolean { return this.opts.value.current.includes(value); }- performs linear array scan per row, every row re-runs on any selection change - impact: For N rows, each row's derived calls O(N) .includes() method → O(N²) total. Works fine under ~15 items, becomes noticeably slow at 30+ items.
- proposed-fix: Lift a
Set<string>on GridListProvider:readonly selectedSet = $derived(() => new Set(this.opts.value.current)). In isSelected, useselectedSet.has(value)for O(1). Update per-row derived to use the pre-computed set. - verify: [confirmed] Confirmed A31 O(N²). Line 493:
readonly isSelected = $derived.by(() => this.provider.isSelected(this.opts.value.current))per GridListRowProvider calls provider.isSelected (lines 150-152:return this.opts.value.current.includes(value)), a linear array scan reading GLOBAL selection state. Every row's derived depends onthis.opts.value.current; selection replaces the array (line 172this.opts.value.current = next), invalidating ALL row deriveds, each re-running O(N) .includes() → O(N²). The provider DID lift a Set for the roving target (line 144const selected = new Set(this.opts.value.current)inside the singlerovingTargetElderivation) but did NOT lift one for the per-row isSelected path — fix is real and applicable. Matches SYS-7 / documented Listbox-rovingTarget incident. - fix-status: open
HIGH: A31: Per-checkbox derived calling provider method that reads global state — grid-list-002
- dimension: B - Behavior (A31 reactivity)
- rule: A31: Per-checkbox derived calling provider method that reads global state
- location: G:/dev/svelte/vicen/src/uix/soma/components/grid-list/grid-list-provider.svelte.ts:612-614
- evidence: GridListSelectionCheckboxProvider line 612-614:
readonly isChecked = $derived.by(() => { if (!this.rowValue) return false; return this.provider.isSelected(this.rowValue); })- calls the O(N) isSelected method. Also line 636 in props derived repeats the same call. - impact: Per-checkbox derived re-runs and calls isSelected (array .includes), multiplied by number of checkboxes in the list.
- proposed-fix: Same fix as grid-list-001: consume the lifted selectedSet instead of calling isSelected.
- verify: [confirmed] Confirmed A31 on the checkbox part. Lines 612-615:
readonly isChecked = $derived.by(() => { if (!this.rowValue) return false; return this.provider.isSelected(this.rowValue) })calls the same O(N) linear-scan isSelected (line 151). Additionally line 636, inside theprops$derived (632-651):const checked = this.rowValue ? this.provider.isSelected(this.rowValue) : false— a SECOND read of the linear scan per checkbox. Both deriveds depend onvalue.currentvia isSelected and re-run for every checkbox on any selection mutation. Same lifted-Set fix as 001. HIGH per SYS-7. - fix-status: open
MEDIUM: SYS-1: Scope-drift when eidos recipe directory exists but 'eidos' is omitted from morfo.sc — grid-list-003
- dimension: A - Contract (morfo scope)
- rule: SYS-1: Scope-drift when eidos recipe directory exists but 'eidos' is omitted from morfo.scope
- location: G:/dev/svelte/vicen/src/uix/morfo/components/grid-list.ts:7
- evidence: Morfo declares
scope: ['soma', 'sema'](line 7) but full eidos directory exists at G:/dev/svelte/vicen/src/uix/eidos/components/grid-list/ with grid-list.svelte (lines 1-58), grid-list.css (lines 1-236), types.ts, and child components (grid-list-row.svelte, grid-list-cell.svelte, grid-list-selection-checkbox.svelte). Eidos scope is present but not declared. - impact: Inconsistency in declared vs actual scope. Eidos components are real and functional but not flagged in morfo. May confuse consumers about component structure.
- proposed-fix: Either (1) add 'eidos' to morfo.scope:
scope: ['soma', 'sema', 'eidos'], OR (2) remove the eidos directory if GridList is soma-only. Recommend option 1 since eidos wrapper clearly exists and re-exports soma. - verify: [confirmed] Confirmed SYS-1 scope-drift. grid-list.ts:7 declares
scope: ['soma', 'sema']but a full eidos directory exists: grid-list.svelte, grid-list.css, types.ts, index.ts, grid-list-row.svelte, grid-list-cell.svelte, grid-list-selection-checkbox.svelte (verified via glob). The eidos wrapper is real and functional yet 'eidos' is omitted from the morfo scope. This is the documented systemic SYS-1 pattern (siblings command/combobox/date-picker show the same omission). MEDIUM per baseline. No recipe entry in recipes/base.ts (grepgrid-list:/gridListreturned no match), so the eidos surface is CSS-only — adding 'eidos' to scope is the correct alignment. - fix-status: open
No-findings dimensions
C - DOM-selector (CSS.escape used correctly on line 184 for consumer value), D - Frontier (no soma→eidos imports; eidos→soma normal), E - TSC/Theming (--control-height-, --font-size-, --space-, --color- all canonical; no magic hex or z-index), F - Tests (test environment jsdom is acceptable for this DOM-interactive pattern; keyboard nav, selection, typeahead tested), G - Redundancy (no detected duplication in selection/keyboard navigation logic), E-bis - No A33 ($state(new Map/Set)), A30 (id registration is direct field, not $effect), A6 (listeners cleaned in $effect.root), A14 (roving tabindex correctly implements exactly one tabindex=0), A34 DOM-TOPOLOGY (require() pattern not used)
Theming facts (E-bis)
- magic z-index: none
- magic literals: 0.875rem (checkbox size at grid-list.css:196 - specific rem for UI element size, acceptable) | 60vh in min() at grid-list.css:27 - viewport unit for max-height, not a magic z-index
- undeclared parts: none
- roles clean: true · variants clean: true
Tests (F)
- exists: true · env: jsdom (@vitest-environment jsdom at grid-list-provider.svelte.test.ts:1)
- covers: row selection and deselection via click; shift+click range selection; ctrl/meta+click toggle; keyboard navigation (ArrowDown, ArrowUp, Home, End, PageUp, PageDown); row focusing and roving tabindex; typeahead character matching; select-all (Ctrl+A) and clear (Escape); checkbox row association and toggle; cell-level horizontal arrow navigation (ArrowRight/Left); merging Field provider flags (disabled, readonly, required, invalid)
- untested: O(N²) performance regression with 100+ items (no perf test); RTL keyboard navigation specifics (tests hardcoded ltr); Typeahead buffer timer cleanup edge-cases