# UIX Audit — Fix Execution Plan (ready to run) plan-version: 1 prepared: 2026-06-26 source: this audit (124/124 components, 0 CRITICAL · 18 HIGH · 129 MEDIUM · 60 LOW) branch: active-uix status: NOT STARTED — read-only audit done; this is the authorized fix track. ## How to use this plan Execute **phase by phase, top to bottom** (ordered by leverage). After EACH phase: 1. `npm run check` → 0 new errors. 2. Run the phase's vitest scope (named per phase). 3. For HIGH UI/behavior fixes, a browser check (the audit was read-only; some HIGHs — e.g. collapsible sequence-lag — need a frame-by-frame measurement to confirm the fix, like the original Checkbox fix). 4. Update the finding's `fix-status:` in `audit/components/{kebab}.md` (`open` → `fixed`), then re-run `MODE=reeval` later to confirm. **Hard exclusions (every phase):** never touch `soma/components/words/**`, `eidos/components/palabras/**`, or `chronos` — active dev tracks, excluded from the audit. When committing, stage explicit paths. **Severity legend:** 🔴 HIGH · 🟠 MEDIUM · ⚪ LOW. Each item links its finding id. --- ## Phase 1 — SYS-7: the A31 O(N²) selection cluster ⭐ HIGHEST LEVERAGE **6 HIGH + 3 MEDIUM, one mechanical pattern, ~2 lines per component.** This is the single best ROI in the audit. Pattern: a per-item `$derived` calls `provider.isSelected(v)`/`isExpanded(v)`/`isItemPressed(v)`/`isItemChecked(v)` which does `array.includes(v)` (O(N)). With N items re-deriving on every mutation → O(N²) ("hangs at 30+"). **Fix (apply to each provider):** - Add a provider-level `readonly selectedSet = $derived(new SvelteSet(this.opts.value.current))` (import `SvelteSet` from `svelte/reactivity`). Trees also add `expandedSet`. - Rewrite the provider method to `return this.selectedSet.has(value)` (O(1)). The per-item `$derived` is unchanged. - The providers already prove the shape — `rovingTargetEl` is lifted exactly this way (with a comment citing "avoids an O(N²) cascade"). Mirror it. | 🔴/🟠 | Component | provider method | id | | --- | --- | --- | --- | | 🔴 | listbox | `isSelected` @ listbox-provider:157-158 (item @ :413) | listbox-001 | | 🔴 | grid-list | `isSelected` :493 + per-checkbox `isChecked` :151 | grid-list-001/002 | | 🔴 | tree-view | `isSelected`/`isExpanded` :96-98/:145-147 (items :351/:353/:569) | tree-view-002 | | 🔴 | tree-grid | `isSelected`/`isExpanded` :148/:189 (rows :578-579) | tree-grid-001 | | 🔴 | tag-group | `isSelected` :115-117 (item :321 + link :410) | tag-group-001 | | 🟠 | toggle-group | `isItemPressed` :72 (item :146) | toggle-group-001 | | 🟠 | checkbox | `CheckboxGroupProvider.isItemChecked` :313-315 | checkbox-001 | | 🟠 | select | `isSelected` :158 (single-select, low-risk but harmless) | select-007 | | 🟠 | combobox | `isSelected` :182 (single-select, low-risk) | combobox-004 | **Optional extraction (EX-3):** a 3-line shared `liftedSet(() => string[])` helper, then all 9 consume it. Decide inline-vs-helper at execution (≥2 consumers justify the helper). **Verify:** `npx vitest run src/uix/soma/components/{listbox,grid-list,tree-view,tree-grid,tag-group,toggle-group,checkbox,select,combobox}` + a 50-item browser smoke on listbox/tree. --- ## Phase 2 — the isolated HIGH findings (12) Each is well-grounded and self-contained. Order within the phase is by risk. 1. 🔴 **dialog-001** — Content double-writes `role`/`aria-roledescription` (a11y race for `alertdialog`). `dialog-provider:391-397` registers Content `syncAttrs:true`, then `:460-463` sets `role:variant` + `aria-roledescription:undefined`. **Fix:** make `role` morfo-expressible (bind a `propRef('variant')` / stateRef so the morfo owns the variant role) OR drop `syncAttrs` for Content and supply role/aria via `renderProps()` + a single override. Don't mix syncAttrs + manual same-attr writes. Verify alertdialog role is stable across ticks. 2. 🔴 **dialog-002** — dead/conflicting `[data-dialog-trigger]` CSS envelope after the trigger migrated to a composed `