7.9 KiB
Audit: collapsible
audit-version: 1 audited-at: 2026-06-26 scope: ['soma', 'sema'] (SCOPE-DRIFT → SYS-1) 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: G:\dev\svelte\vicen\src\uix\soma\components\collapsible\collapsible-provider.svelte.ts sequence-audit: expand: post-sequence ✓ (state set in handler lines 68-70, morfo sequence:post at line 39). collapse: pre-sequence ✓ (state set in handler lines 71-73, morfo sequence:pre at line 48). Both events fire via runtime.trigger() in toggle() which defers handler execution. No lag risk: handler state-settin
Summary
Counts (post-verification): CRITICAL 0 · HIGH 1 · MEDIUM 1 · LOW 0.
Findings
MEDIUM: SYS-1 scope-drift: eidos directory exists but morfo scope omits 'eidos' — collapsible-001
- dimension: Contract(morfo)
- rule: SYS-1 scope-drift: eidos directory exists but morfo scope omits 'eidos'
- location: src/uix/morfo/components/collapsible.ts:22
- evidence: morfo declares scope: ['soma', 'sema'] but src/uix/eidos/components/collapsible/ directory exists with full implementation (collapsible.svelte, collapsible.css, index.ts). Accordion (sibling pattern) correctly declares scope: ['soma', 'sema', 'eidos']
- impact: Scope declaration diverges from actual architecture; contract validators may not catch eidos-layer changes. Documentation of morfo scope is stale.
- proposed-fix: Update collapsibleMorfo scope to ['soma', 'sema', 'eidos'] to match accordion pattern and actual directory structure
- verify: [confirmed] CONFIRMED. morfo declares
scope: ['soma', 'sema']at src/uix/morfo/components/collapsible.ts:22, but src/uix/eidos/components/collapsible/ exists with a full implementation (collapsible.svelte, collapsible.css, collapsible-trigger.svelte, collapsible-content.svelte, types.ts, index.ts, README.md — verified via glob). The sibling disclosure pattern accordion correctly declaresscope: ['soma', 'sema', 'eidos']at src/uix/morfo/components/accordion.ts:7. This is SYS-1 scope-drift, MEDIUM. Severity unchanged. - fix-status: open
HIGH: SEQUENCE-LAG — collapse is sequence:'pre' but sets state in the handler (the Checkbox-244ms-lag mechanism) — collapsible-NEW-001
- dimension: Behavior(soma)
- rule: SEQUENCE / TWO-MOMENTS — a control whose functional state is set INSIDE the runtime.trigger HANDLER must use morfo
sequence: 'post'; with 'pre' the perceptual hold (~240ms) blocks the state change (documented Checkbox 244ms-lag incident). - location: src/uix/morfo/components/collapsible.ts:48 (collapse sequence:'pre') + src/uix/soma/components/collapsible/collapsible-provider.svelte.ts:71-73 (handler sets state)
- evidence: The
collapseevent sets the functional state INSIDE the trigger handler: provider lines 71-73collapse: () => { this.opts.open.current = false; }. The morfo declaressequence: 'pre'at collapsible.ts:48. For 'pre', the runtime serializes the perceptual emit BEFORE the handler: runtime.svelte.ts:749-752else { await runEmit(); if (handler) await handler(); }.runEmitawaitssources.eventEngine.emit(runtime.svelte.ts:696await sources.eventEngine.emit(...)), whose engine awaits the visual channel hold (engine.ts:250await visualChannel.handle(enriched, effective)), which doesawait sleep(holdMs)(visual.ts:79-81). collapsible declares no per-eventholdand the sema pack (sema/components/collapsible.ts) declares none, soemergefalls back to labelbrief(visual.ts:43) = 240ms (durations.ts:27). Result: the content does NOT receivehiddenuntil ~240ms after the click — the exact magnitude of the documented Checkbox 244ms-lag. (Theexpandevent is correctlypostat collapsible.ts:39, so opening has no lag — only closing lags.) This refutes the prompt's sequenceAudit line which markedcollapse: pre-sequence ✓. - impact: User clicks the trigger to collapse an open section; the panel stays fully visible for ~240ms before hiding — a perceptible dead delay on every close, identical in mechanism and magnitude to the Checkbox 244ms-lag that the project already documented and fixed by flipping to
sequence:'post'. The morfo's inline justification (collapsible.ts:10-13, 'collapse remains pre-sequence so the exit signal can play while the content is still visible') is based on a false concurrency assumption: the runtime does NOT overlap emit and handler for 'pre' — it serializes emit-then-handler, so the content is held visible for the FULL hold, then hidden, rather than the exit sound playing during a CSS exit transition. - repro: Open a Collapsible (expand — instant), then click the trigger to collapse. Measure the delay between click and
hidden/data-state='closed'taking effect on the content. Expect ~240ms (the emergebriefhold) rather than near-zero. The expand direction does not exhibit the delay. - proposed-fix: Apply the documented Checkbox/Toggle/Switch doctrine: change
collapsetosequence: 'post'(collapsible.ts:48) so the handler flipsopen=falsefirst and the exit signal then acknowledges the resolved state, matching how accordion's symmetric close event is handled (accordion close is post per accordion.ts pattern). If a visible-during-exit cue is genuinely wanted, drive it from eidos CSS keyed ondata-state='closed'+ a CSS exit transition rather than blocking the functional state behind the sema hold. Alternatively set state at the call-site intoggle()(collapsible-provider.svelte.ts:85-89) beforeruntime.trigger, which would make 'pre' tolerable (the RadioGroup/Tabs pattern). - verify: [verifier-added → LEAD-CONFIRMED HIGH] The analyze agent (and the morfo's own inline comment) marked this CLEAN, assuming
'pre'overlaps emit+handler so the exit plays during the hold. The verifier traced the actual runtime and proved it SERIALIZES (runtime.svelte.ts:749-752: await runEmit(); await handler()), soopen=falseis delayed the full ~240msbriefemerge hold. I initially LEANED toward MEDIUM (reasoning "collapse is emerge.dismiss 'pre', the canonical animate-before-hide pattern like dialog/popover"), but that masking only holds for components that animate ondata-event/Presence DURING the hold (dialog does); collapsible's exit is keyed ondata-state='closed', which doesn't flip until AFTER the hold — so the 240ms is a real dead delay, not a filled exit animation. Restored to HIGH. Caveat: like the checkbox fix, the user-perceptible magnitude should be confirmed by a frame-by-frame browser measurement before/after; the MECHANISM (and the refuted morfo justification) are confirmed in code. - fix-status: open
No-findings dimensions
Behavior(soma) [except the sequence-lag above], DOM-selector, Frontier, TSC, Theming, Tests
Theming facts (E-bis)
- magic z-index: none
- magic literals: collapsible.css:4,19 translateY(-4px) — LOCAL animation amplitude in @keyframes, acceptable
- undeclared parts: none
- roles clean: true · variants clean: true
Tests (F)
- exists: true · env: vitest jsdom
- covers: ID linking via partRef; expand/collapse events + state toggle; disabled guard + disabled attr projection
- untested: onOpenChange callback firing
Style observations (non-blocking)
- CSS uses canonical token vars (--space-, --color-, --font-, --duration-, --ease-, --opacity-, --radius-*)
- Recipe collapsible: in base.ts lines 712-725 with 12 tokens, all reference --*; no raw hex/px/rem/em
- Naming: --collapsible-{slot}.{property} consistent (trigger-color, trigger-padding, content-padding, etc)
- Animations in @keyframes use local px values for amplitude (-4px translateY), not tied to design tokens (acceptable for local animation logic)
- No magic z-index or opacity decimals