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

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

  • 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

  • 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

  • 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

  • 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.