# 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` on GridListProvider: `readonly selectedSet = $derived(() => new Set(this.opts.value.current))`. In isSelected, use `selectedSet.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 on `this.opts.value.current`; selection replaces the array (line 172 `this.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 144 `const selected = new Set(this.opts.value.current)` inside the single `rovingTargetEl` derivation) 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: fixed (92f988e7) ### 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 the `props` $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 on `value.current` via isSelected and re-run for every checkbox on any selection mutation. Same lifted-Set fix as 001. HIGH per SYS-7. - fix-status: fixed (92f988e7) ### 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 (grep `grid-list:`/`gridList` returned no match), so the eidos surface is CSS-only — adding 'eidos' to scope is the correct alignment. - fix-status: fixed (212624e0) ## 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