7.4 KiB
Audit: splitter
audit-version: 1
audited-at: 2026-06-26
scope: soma, sema, eidos
method: adversarially-verified workflow; HIGH lead-verified. B6 ground-truth: checkbox/toggle/switch commit-toggle = sequence post (lag-fixed); radio-group/tabs/accordion/stepper set state at call-site (pre OK); toggle-group + checkbox-group carry the A31 .includes pattern (SYS-7); slider has no gesture-layer A6 leak.
provider: src/uix/soma/components/splitter/splitter-provider.svelte.ts
sequence-audit: N/A - splitter is not a state-binding control (Checkbox/Toggle/Switch/RadioGroup pattern). It is a direct-manipulation drag component with commit-set: 'post' morfo sequence. State changes (panel sizes) are set BEFORE pointerup closes the drag; notifyResizeEnd() routes through runtime.trigger('comm
Summary
Counts (post-verification): CRITICAL 0 · HIGH 0 · MEDIUM 1 · LOW 1.
Findings
MEDIUM: Frontier: eidos CSS must reference morfo event names exactly; mismatch breaks sema pulse v — splitter-001
- dimension: Frontier, Data-selector
- rule: Frontier: eidos CSS must reference morfo event names exactly; mismatch breaks sema pulse visual feedback
- location: src/uix/eidos/components/splitter/splitter.css:115; src/uix/morfo/components/splitter.ts:51,15
- evidence: CSS selector
[data-event='commit-resize']but morfo defines eventname: 'commit-set'. Morfo comment at line 15 confirms eidos recipe should referencedata-event='commit-set'— CSS uses wrong event name. - impact: Sema event pulse visual feedback (handle background flash on drag commit) never fires because eidos events layer stamps
data-event='commit-set'but CSS selector waits forcommit-resize. User gets no visual confirmation of resize completion. - repro: Drag a splitter resize trigger; release. Observe no background pulse on the handle.
- proposed-fix: Change line 115 of splitter.css from
[data-splitter-resize-trigger][data-event='commit-resize']to[data-splitter-resize-trigger][data-event='commit-set'] - verify: [confirmed] CONFIRMED that the pulse never fires, but the cited rule/fix is partially wrong and severity is overstated. splitter.css:115 selects
[data-splitter-resize-trigger][data-event='commit-resize'][data-event-phase='active']; the morfo (splitter.ts:51) names the eventcommit-setand the provider triggersruntime.trigger('commit-set')(provider lines 205, 97). The sema engine stampsdata-event='commit-set', so thecommit-resizeselector never matches — the comment at css:110-111 and css:5 are stale relics of the old event name. HOWEVER the proposed fix (renamecommit-resize→commit-set) is INSUFFICIENT: the morfo (splitter.ts:55) and sema cascade (sema/components/splitter.ts:49onProvider({ eventName: 'commit-set' })) targetv.partRef('provider'), so the stamp lands on the provider element[data-splitter], NOT on[data-splitter-resize-trigger]. The selector targets both the wrong event name AND the wrong element. To actually pulse the handle on commit the selector would need to key off the provider stamp (e.g.[data-splitter][data-event='commit-set'] [data-splitter-resize-trigger]) or the commit event's target would need to move. Severity downgraded HIGH→MEDIUM: this is a purely cosmetic/ornamental sema-pulse that never fired (no functional or a11y impact); the handle still gets its[data-state='dragging']/[data-dragging]active background via css:77-80 during the actual drag, so the user is not left without resize feedback. - fix-status: open
LOW: Theming: opacity literals forbidden; must use canonical --opacity-* token or fallback to - — splitter-003
- dimension: Theming, Tokens
- rule: Theming: opacity literals forbidden; must use canonical --opacity-* token or fallback to --opacity-disabled.
- location: src/uix/eidos/components/splitter/splitter.css:35
- evidence: Line 35:
opacity: var(--splitter-disabled-opacity, 0.6);. Recipe base (recipes/base.ts) defines splitter['disabled-opacity'] = 'var(--opacity-disabled)', which is the canonical fallback. CSS uses magic literal 0.6 instead. - impact: Opacity value diverges from canonical token scale. If --opacity-disabled is updated system-wide, splitter disabled state won't reflect the change because it has a hardcoded literal fallback.
- proposed-fix: Change line 35 to
opacity: var(--splitter-disabled-opacity, var(--opacity-disabled));so the fallback chains to the canonical token instead of a magic literal. - verify: [downgraded] CONFIRMED as token drift but DOWNGRADED MEDIUM→LOW. splitter.css:35
opacity: var(--splitter-disabled-opacity, 0.6)uses a magic opacity decimal0.6as the inline fallback, violating E-bis (opacity decimal →--opacity-*). Verified recipes/base.ts:4231 definessplitter['disabled-opacity'] = 'var(--opacity-disabled)', so under normal operation the recipe value wins and the0.6literal NEVER surfaces — it only appears if the recipe layer is entirely absent. Other consumers chain the canonical token (var(--opacity-disabled)) as their fallback, so this is a real inconsistency, but it is the lowest-impact token-drift category: cosmetic, no behavioral divergence in practice. Proposed fix is correct (chain tovar(--opacity-disabled)). - fix-status: open
No-findings dimensions
Contract: morfo satisfies Morfo and uses as const, Contract: parts registered via runtime.part exist in morfo, Contract: data-{c}/-{part} naming, Behavior: setPointerCapture is immediate (pure handle, no child buttons), Behavior: A12 RTL keyboard via getDirectionalKeys, Behavior: A14 roving not applicable (single handle, not roving group), Behavior: A15 gesture cleanup on unmount (dragSignalFrame, pendingDragPercent cancelled), Behavior: A30 panel registration is direct (sync), not effect-wrapped for props, Behavior: A31 no O(N²) patterns (find/getPanelState are on local panels array, not global), Behavior: A33 no $state(Map/Set), Behavior: A6 requestFrame/cancelFrame properly disposed in effect cleanup, DOM: no querySelector interpolating consumer values, Frontier: soma does not import eidos, Frontier: syncAttrs true with derived props (no double-write), Theming: only standard 9 roles + space tokens referenced, Theming: no raw hex where role alias canonical, Theming: spacing uses --space-* tokens, Theming: no local z-index or canonical --z-index-* used (no z needed for splitter), Test: provider test exists, jsdom env, covers gesture drag, keyboard, collapse
Theming facts (E-bis)
- magic z-index: none
- magic literals: 0.6 (splitter-001-adjacent)
- undeclared parts: none
- roles clean: true · variants clean: true
Tests (F)
- exists: true · env: jsdom
- covers: panel registration and sizing; resize clamping and min/max constraints; collapse/expand with adjacent space redistribution; pointer drag (down/move/up); keyboard arrows (RTL-aware via getDirectionalKeys); keyboard Home/End (minimize/maximize); aria-valuenow/min/max projection; disabled state blocks resize
- untested: gesture on lostpointercapture recovery; double-click toggle collapse; dragSignalFrame cleanup during unmount with active drag
Style observations (non-blocking)
- Excellent drag-signal throttling with per-frame payload queue (72ms window)
- Clean pointer-capture semantics for pure-handle resize (not using Gesture class)
- Robust collapse/expand logic respects min/max constraints on both panels
- Well-documented keyboard shortcut implementation (Home/End/ArrowKeys)