# 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 recipe `cropper:` 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 jsdom` line 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-shot `img.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: if `src` changes and an OLD image's load resolves AFTER the new one, the old onload writes `aspectRatio` (line 54) to a stale ratio. It is NOT an A35/A36 loop — the effect reads `src`, not `aspectRatio`, so no tracked read-back, no freeze. Impact: transient wrong aspect-ratio on rapid src swaps. The provider's `loadImage` (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)