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-view.md

70 lines
8.8 KiB

# Audit: tree-view
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: src/uix/soma/components/tree-view/tree-view-provider.svelte.ts
reactivity (A31/A33/A35): A31 hazard identified at lines 351, 353, 569 ($derived per item calling provider.isExpanded/isSelected) and lines 96-98, 145-147 (provider methods doing array .includes). Each branch's derived re-runs on any expansion/selection mutation (O(N) re-runs per mutation * O(K) work per lookup = O(N*K) total). No A33, A35, A6, A30 issues found. CLEAN for structural leaks and ref-write loops.
## Summary
Counts (post-verification): CRITICAL 0 · HIGH 1 · MEDIUM 3 · LOW 0.
## Findings
### HIGH: A31 O(N²): per-item/per-row `$derived` that calls provider method reading global state re- — tree-view-002 <!-- id: tree-view-002 -->
- dimension: B, Behavior, A31
- rule: A31 O(N²): per-item/per-row `$derived` that calls provider method reading global state re-runs for EVERY item on ANY mutation
- location: src/uix/soma/components/tree-view/tree-view-provider.svelte.ts:351,353 (TreeViewBranchProvider) / 569 (TreeViewItemProvider) / 96-98, 145-147 (provider methods)
- evidence: TreeViewBranchProvider.isExpanded (line 351) = `$derived.by(() => this.provider.isExpanded(this.opts.value.current))`. The provider.isExpanded (line 96-98) calls `this.opts.expandedValue.current.includes(value)`. When expandedValue array changes, every branch's derived re-runs (O(N) re-runs) and each calls .includes() on array of size K (O(K) work per call). Total: O(N*K) per mutation. Same pattern for isSelected (lines 353, 569, 145-147).
- impact: Works fine on small trees (test has 3 items). With 100+ branches and frequent expansions, each expansion causes O(N*K) work where N=total items and K=expanded nodes. At 1000 items and 100 expanded, a single expansion triggers 100k array lookups. Manifests as UI lag/hang on medium-to-large trees.
- repro: Render a tree with 100+ branches, many expanded. Toggle expand/collapse on a branch and observe UI lag proportional to item count.
- proposed-fix: Lift expandedValue and selectedValue from arrays to Sets (or use SvelteSet for reactivity). Change `isExpanded(value)` to `return this.opts.expandedSet.current.has(value)` (O(1) instead of O(K)). Update TreeViewOpts interface and consumer code to pass Set state.
- verify: [confirmed] CONFIRMED A31 O(N²). Per-branch derived `isExpanded` (tree-view-provider.svelte.ts:351) = `$derived.by(() => this.provider.isExpanded(this.opts.value.current))`; the provider method (96-98) = `this.opts.expandedValue.current.includes(value)` — reads the GLOBAL expandedValue array. `toggleExpand` (104) assigns a fresh array `this.opts.expandedValue.current = next`, invalidating every branch's derived → all N branch deriveds re-run, each doing O(K) `.includes()` = O(N·K) per expand mutation. Identical pattern for `isSelected` (353 / 569 → method 145-147 `selectedValue.current.includes`). This is the documented A31 incident shape (Listbox rovingTarget, Command). NOT a lifted set — it is a genuine per-item derived calling a global-array-reading method. Fix as proposed: lift to a Set (`.has()` O(1)). Confirmed HIGH. (Note: `tabStopValue` at line 60 is a CORRECT single provider-level derived read O(1) per item via `this.provider.tabStopValue === value` — not part of this hazard.)
- fix-status: open
### MEDIUM: Morfo declares contract for all attributes soma emits — tree-view-001 <!-- id: tree-view-001 -->
- dimension: A, Contract
- rule: Morfo declares contract for all attributes soma emits
- location: src/uix/morfo/components/tree-view.ts:129-141 (BranchControl part) / src/uix/soma/components/tree-view/tree-view-provider.svelte.ts:425
- evidence: BranchControl morfo part (lines 129-141) declares data: [data-state, data-value, data-selected, data-disabled] but soma emits 'data-depth' at line 425: `'data-depth': this.branch.opts.depth.current`. Test at line 260 expects data-depth on control.
- impact: Morfo contract violation—soma is emitting an attribute undeclared in the contract. This defeats contract validation and allows silent mismatches between morfo spec and runtime behavior.
- repro: Inspect TreeViewBranchControlProvider.props at runtime; data-depth will be present but undefined in morfo schema.
- proposed-fix: Add `{ attr: 'data-depth' }` to the BranchControl data array in morfo (tree-view.ts:135-140).
- verify: [downgraded] CONFIRMED as a contract drift, but severity over-rated. morfo BranchControl part (tree-view.ts:135-140) declares data-state, data-value, data-selected, data-disabled — NOT data-depth. Provider emits it at tree-view-provider.svelte.ts:425 `'data-depth': this.branch.opts.depth.current`, and the test asserts it (test:260 `'data-depth': 1`). This is a real 'provider emits aria/data the morfo doesn't declare' violation. However data-depth is a purely presentational indent attribute with NO a11y or behavioral impact, so per the dimension-A rubric this is MEDIUM (undeclared-attr drift), not HIGH. Note the morfo DOES declare data-depth on Branch (line 109) and Item (line 187), so the omission on the other 3 parts looks like an oversight.
- fix-status: open
### MEDIUM: Morfo declares contract for all attributes soma emits — tree-view-003 <!-- id: tree-view-003 -->
- dimension: A, Contract
- rule: Morfo declares contract for all attributes soma emits
- location: src/uix/morfo/components/tree-view.ts:143-154 (BranchContent part) / src/uix/soma/components/tree-view/tree-view-provider.svelte.ts:463
- evidence: BranchContent morfo part (lines 143-154) declares data: [data-state] only, but soma emits 'data-depth' at line 463: `'data-depth': this.branch.opts.depth.current`. Test expects this at line 269.
- impact: Morfo contract violation—soma is emitting an undeclared attribute.
- repro: Inspect TreeViewBranchContentProvider.props; data-depth will be present but undefined in morfo schema.
- proposed-fix: Add `{ attr: 'data-depth' }` to BranchContent data array in morfo (tree-view.ts:152).
- verify: [downgraded] CONFIRMED drift, severity over-rated. morfo BranchContent part (tree-view.ts:152) declares `data: [{ attr: 'data-state', values: ['open','closed'] }]` only. Provider emits data-depth at tree-view-provider.svelte.ts:463 `'data-depth': this.branch.opts.depth.current`; test asserts it (test:269 `'data-depth': 1`). Same class as 001 — undeclared presentational attribute, no a11y/behavior impact → MEDIUM, not HIGH.
- fix-status: open
### MEDIUM: Morfo declares contract for all attributes soma emits — tree-view-004 <!-- id: tree-view-004 -->
- dimension: A, Contract
- rule: Morfo declares contract for all attributes soma emits
- location: src/uix/morfo/components/tree-view.ts:166-174 (BranchIndentGuide part) / src/uix/soma/components/tree-view/tree-view-provider.svelte.ts:534
- evidence: BranchIndentGuide morfo part (lines 166-174) declares data: [] (empty), but soma emits 'data-depth' at line 534: `'data-depth': this.branch.opts.depth.current`.
- impact: Morfo contract violation—soma is emitting an undeclared attribute.
- repro: Inspect TreeViewBranchIndentGuideProvider.props; data-depth will be present but undefined in morfo schema.
- proposed-fix: Change BranchIndentGuide data from [] to [{ attr: 'data-depth' }] in morfo (tree-view.ts:172).
- verify: [downgraded] CONFIRMED drift, severity over-rated. morfo BranchIndentGuide part (tree-view.ts:172) declares `data: []`. Provider emits data-depth at tree-view-provider.svelte.ts:534 `'data-depth': this.branch.opts.depth.current`; test asserts it (test:282 `'data-depth': 1`). The indent guide is precisely the part that NEEDS depth for rendering, so the emission is intentional and the morfo omission is the bug — but it is a presentational-only undeclared attribute, so MEDIUM not HIGH. 001/003/004 are one systemic oversight: data-depth declared only on Branch+Item, emitted (and tested) on BranchControl+BranchContent+BranchIndentGuide too.
- fix-status: open
## No-findings dimensions
C, DOM-selector, D, Frontier, E, TSC, E-bis, Theming
## 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: root props; expansion/collapse; selection single/multiple; navigation; typeahead; keyboard keys; branch control/indicator/content/guide wiring; item and label wiring
- untested: A31 quadratic hazard (would need 100+ items under load); RTL direction edge cases; mixed single/multiple selection scenarios; disabled branch children inheritance; tabindex=0 roving behavior under rapid focus changes

Powered by TurnKey Linux.