6.5 KiB
Audit: cropper
audit-version: 1 audited-at: 2026-06-26 scope: (SCOPE-DRIFT → SYS-1) method: adversarially-verified workflow; HIGH lead-verified. B7 ground-truth: Toolbar A35 + Form A36 incidents STAYED FIXED (untrack present); toast/clipboard/drag-drop use uix.timers (no A6 leak); button is clean (its archetype finding was inert — Spinner not runtime-registered); data-size/data-shape are eidos visual attrs (not contract violations). provider: src/uix/soma/components/cropper/cropper-provider.svelte.ts cleanup-audit (A6/A35/A36): Timers: soma.uix.timers.schedule(null, 120, ...) at line 302 — disposed by runtime ✓. Listeners: All pointer events (onpointerdown/move/up in Viewport/Selection/Handle) use setPointerCapture + releasePointerCapture in same handler; no separate cleanup needed. Image load handler in eidos cropper.svel
Summary
Counts (post-verification): CRITICAL 0 · HIGH 0 · MEDIUM 2 · LOW 1.
Findings
MEDIUM: SYS-1: Scope-drift — eidos recipe directory exists but morfo scope omits 'eidos' — cropper-001
- dimension: A
- rule: SYS-1: Scope-drift — eidos recipe directory exists but morfo scope omits 'eidos'
- location: src/uix/morfo/components/cropper.ts:20
- evidence: Morfo declares scope: ['soma', 'sema'] but src/uix/eidos/components/cropper/ exists with CSS and SVG component
- impact: Scope mismatch signals incomplete contract declaration; framework tooling may not validate eidos layer against morfo
- proposed-fix: Update morfo scope to ['soma', 'sema', 'eidos']
- verify: [confirmed] Confirmed SYS-1 scope-drift. morfo cropper.ts:20
scope: ['soma', 'sema']omits 'eidos', yet the eidos layer exists: dir src/uix/eidos/components/cropper/ (cropper.css, cropper.svelte, types.ts) AND a recipecropper:at src/uix/eidos/lib/recipes/base.ts:1152. Matches the established systemic baseline (sibling image-picker.ts:20 and image-adjustments.ts:18 carry the same omission). MEDIUM stands. - fix-status: fixed (
212624e0)
MEDIUM: SYS-3: Missing test coverage for high-risk paths (gesture, async, timers) — cropper-003
- dimension: F
- rule: SYS-3: Missing test coverage for high-risk paths (gesture, async, timers)
- location: src/uix/soma/components/cropper/cropper-provider.svelte.test.ts
- evidence: Tests cover geometry (moveRect, resizeRect) and ZoomPan, but no coverage for: gesture handlers (onpointerdown/move/up in Viewport/Selection/Handle), cropImage() async function, zoom throttle timer, or fixed-size $effect. Test env is jsdom.
- impact: Critical business logic (drag/resize/zoom interactions, canvas extraction, timer cleanup) is untested and may regress without detection
- proposed-fix: Add tests for: (1) pointer gesture sequences (down→move→up), (2) zoom throttle behavior, (3) cropImage() Blob production with shape masking, (4) fixed-size centering effect
- verify: [confirmed] Confirmed SYS-3 / dim F. Test file (cropper-provider.svelte.test.ts,
@vitest-environment jsdomline 1) covers pure geometry thoroughly (moveRect, resizeRect incl. aspect/min/max/edge corners, lines 58-137), ZoomPan (139-181), and provider setCrop/disabled/snippetProps (189-223). UNTESTED high-risk paths: (1) pointer gesture sequences (down→move→up in Viewport/Selection/Handle providers), (2)cropImage()async canvas→Blob extraction incl. round-shape masking (provider lines 237-290), (3) zoom-throttle timer + emitZoom gate (294-305), (4) fixed-size centering $effect (183-205). MEDIUM stands — geometry is well-covered so this is a partial, not total, gap. - fix-status: open
LOW: A6: Event listener / handler not disposed in $effect return — cropper-002
- dimension: B, A6
- rule: A6: Event listener / handler not disposed in $effect return
- location: src/uix/eidos/components/cropper/cropper.svelte:44-58
- evidence: $effect creates new Image element and assigns img.onload handler (line 52) but never cleans up. On src change, new Image created; old handler may fire if response arrives after effect re-run.
- impact: Old onload handlers can fire after component re-runs the effect, causing stale handler invocations and potential memory pressure from accumulated handlers
- proposed-fix: Return a cleanup function from the $effect that clears the handler, or store img in state and reuse it with cleanup:
return () => { img.onload = null; img.src = ''; } - verify: [downgraded] Downgraded from MEDIUM/A6 to LOW. The $effect at cropper.svelte:44-58 creates a local
const img = new ImageCtor()(line 51), sets one-shotimg.onload(line 52),img.src = s(line 57), with no cleanup return. This is NOT an A6 resource leak: the Image is never inserted into the DOM, the handler is one-shot, and the element is GC-eligible once the local goes out of scope — there is no persistent listener/observer/timer to dispose. The genuine (minor) defect is a stale-async-write race: ifsrcchanges and an OLD image's load resolves AFTER the new one, the old onload writesaspectRatio(line 54) to a stale ratio. It is NOT an A35/A36 loop — the effect readssrc, notaspectRatio, so no tracked read-back, no freeze. Impact: transient wrong aspect-ratio on rapid src swaps. The provider'sloadImage(cropper-provider.svelte.ts:96-106) avoids this by awaiting a Promise with no reactive write. LOW. - fix-status: open
No-findings dimensions
C, D, E, E-bis, G
Theming facts (E-bis)
- magic z-index: none
- magic literals: 9999px in box-shadow (line 52 cropper.css) — documented mask pattern
- undeclared parts: none
- roles clean: true · variants clean: true
Tests (F)
- exists: true · env: jsdom
- covers: moveRect geometry; resizeRect with aspect/minSize/maxSize; ZoomPan scale clamping and pan bounds; ZoomPan fraction mapping; setCrop updates state and fires callback; disabled blocks setCrop; snippetProps exposes shape/dragging
- untested: Gesture handlers (onpointerdown/move/up); cropImage() Blob canvas extraction; Zoom throttle timer behavior; Fixed-size $effect re-centering; Shape masking (round crop); Image load aspect-ratio tracking; commit-crop event trigger on canvas complete
Style observations (non-blocking)
- Zoom buttons correctly use --focus-ring-width + --focus-ring-color tokens (canonical two-ring model)
- Recipe tokens all reference theme vars or semantic space/radius/opacity
- Box-shadow 9999px mask is documented pattern (not magic literal violation)
- Cursor values (move, grab, grabbing, resize variants) are semantic browser cursors
- :active pseudo-class on img is native browser feedback for pressed state (acceptable)