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/components/tree-grid.md

4.2 KiB

Audit: tree-grid

audit-version: 1 audited-at: 2026-06-26 scope: 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\tree-grid\tree-grid-provider.svelte.ts reactivity (A31/A33/A35): HAZARD A31 FOUND at tree-grid-provider.svelte.ts:578-579. Per-row isSelected and isExpanded $derived call provider.isSelected()/isExpanded() which read opts.value/expanded arrays via .includes(). When arrays change, all N rows re-derive and call O(N) array lookups → O(N²) total. Fix: provider.selectedSet/expandedSet with Set.has().

Summary

Counts (post-verification): CRITICAL 0 · HIGH 1 · MEDIUM 0 · LOW 0.

Findings

HIGH: A31 — per-item/per-row $derived calling provider method that reads global state — tree-grid-001

  • dimension: B, Reactivity
  • rule: A31 — per-item/per-row $derived calling provider method that reads global state
  • location: src/uix/soma/components/tree-grid/tree-grid-provider.svelte.ts:578-579
  • evidence: readonly isSelected = $derived.by(() => this.provider.isSelected(this.opts.value.current)); readonly isExpanded = $derived.by(() => this.provider.isExpanded(this.opts.value.current));
  • impact: O(N²) reactivity: when value[] or expanded[] arrays change, every row's isSelected and isExpanded deriveds re-run, each calling provider.isSelected()/isExpanded() which do .includes() lookups on those arrays. With 50+ rows and 10+ selections, this causes quadratic re-derives and array scans, manifesting as lag on selection/expansion.
  • proposed-fix: Lift a Set on the provider: readonly selectedSet = $derived(new Set(this.opts.value.current)) and readonly expandedSet = $derived(new Set(this.opts.expanded.current)). Each row then does O(1) .has() instead of O(N) .includes(). Per-item derived becomes readonly isSelected = $derived.by(() => this.provider.selectedSet.has(this.opts.value.current)) → O(1) per row.
  • verify: [confirmed] CONFIRMED A31 O(N²). tree-grid-provider.svelte.ts:578-579 per-row readonly isSelected = $derived.by(() => this.provider.isSelected(this.opts.value.current)); and readonly isExpanded = $derived.by(() => this.provider.isExpanded(this.opts.value.current));. The provider methods read GLOBAL arrays via O(N) .includes(): line 188-190 isSelected(value){ return this.opts.value.current.includes(value); } and line 147-149 isExpanded(value){ return this.opts.expanded.current.includes(value); }. When the global value[] or expanded[] array mutates, every row's two deriveds re-run, each scanning the array → quadratic. NOT a lifted Set: the only Set in the file is the single provider-level rovingTargetEl (line 141), unrelated to selection/expansion. Matches the documented Listbox/Command A31 shape exactly. HIGH justified (two O(N) deriveds per row; degrades at 30+ rows on each selection/expansion). Fix as proposed: lift selectedSet/expandedSet $derived(new Set(...)) on the provider and have rows do O(1) .has().
  • fix-status: open

No-findings dimensions

A, C, D, E, F, G

Theming facts (E-bis)

  • magic z-index: none
  • magic literals: none
  • undeclared parts: none
  • roles clean: true · variants clean: true

Tests (F)

  • exists: true · env: jsdom
  • covers: expansion; range-select; visible-rows-filtering; keyboard-navigation; row-click-modes; selectAll; disabled-guards
  • untested: A31 reactivity hazard (> 50 rows with dynamic selections); expandedSet/selectedSet optimization verification

Style observations (non-blocking)

  • CSS uses canonical theme variables (--space-, --radius-, --color-* roles, --font-size-, --leading-, --duration-, --ease-, --opacity-disabled, --z-index base scales). No magic hex or opacity decimals. Indent guides use computed background gradients (lines 275-336) with no-repeat per level; graceful degradation beyond depth 8. Striping targets aria-level=1 only (line 212), limiting scope correctly.

Powered by TurnKey Linux.