You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
svelte-kit-vice/audit/components/cropper.md

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 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)

Powered by TurnKey Linux.