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

40 lines
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 <!-- id: 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: fixed (92f988e7)
## 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.