Skip to content

Adversarial review: fix scoped-filter replay, browse async seams, edge detection#22

Merged
jacobson30-bot merged 3 commits into
mainfrom
adversarial-review-fixes
Jun 11, 2026
Merged

Adversarial review: fix scoped-filter replay, browse async seams, edge detection#22
jacobson30-bot merged 3 commits into
mainfrom
adversarial-review-fixes

Conversation

@jacobson30-bot

Copy link
Copy Markdown
Contributor

Summary

Fixes for 10 confirmed bugs found in an adversarial seam review of the recent work (async browse loading, quick selections / scoped processing, edge detection, metadata caching), plus three new adversarial test suites that pin every finding and the guarantees that already held. Suite: 2273 passed, 0 xfailed.

Processing scopes / replay (state.py, gui_adapter.py, viewer mixins)

  • Scope-filter replay ordering (high severity): committed ROI/region/mask filters always replayed before geometric_ops, so a filter committed after a flip/rotate landed at the mirrored location. Commit paths now stamp after_geometric_ops and the adapter interleaves scope steps at their commit position. Legacy sidecars (no key) replay byte-identically.
  • ROI-scoped image arithmetic freezes geometry + position at commit (parity with scoped filters); legacy live-ROI entries unchanged.
  • operand_resolver is forwarded into nested roi/mask recursion (scoped arithmetic no longer falls back to disk I/O past an injected resolver).
  • Malformed frozen snapshots now block export instead of being silently dropped from the replay; apply-time skips warn.
  • rotate_arbitrary warning no longer claims ROI steps were "skipped" (they never were); it fires only for the genuinely frame-ambiguous live-roi-after-rotation case.

Edge detection (edge_detection.py)

  • Flat non-zero images no longer become an all-True edge mask (float-residue floor instead of strict > 0.0; a 1e-12 step is still detected).
  • ROI-restricted Canny computes percentile thresholds within the ROI (was ~2x over-detection from whole-image quantiles).
  • gradient_magnitude is now the true physical slope (skimage kernels respond 2.0 per unit slope; halved before pixel-size scaling).

Browse async / cache (workers.py, thumbnail_grid.py, indexing.py)

  • Every worker now emits a placeholder for every render it owes — render exceptions no longer leave cards/channel panels silently stuck.
  • Timer-sliced card build survives mid-build appearance changes (own generation token; was: tail cards of large folders never built, background timer polled forever).
  • Folder-index failure restores current_dir to the folder actually displayed (was: refresh retargeted the failed path, Back history corrupted).
  • Transient metadata read failures are no longer cached as permanently broken (key is (path, mtime, size), so they never recovered until mtime changed).
  • STM vs qPlus display chain pinned end-to-end: a constant-Δf setpoint can never surface as a pA current.

Test plan

  • 3 new suites: test_processing_scope_seams.py, test_edge_detection_adversarial.py, test_browse_worker_seams.py (registered in conftest GUI gating), plus test_browse_metadata_display.py and additions to test_browse_cache.py.
  • Full suite green locally: 2273 passed, 3 skipped.

🤖 Generated with Claude Code

jacobson30-bot and others added 3 commits June 11, 2026 16:59
…olver injectable

Adversarial review findings in the GUI-state -> ProcessingState -> replay
path, each pinned by tests in the new tests/test_processing_scope_seams.py:

- Scope replay ordering (the high-severity finding): committed
  roi_filter_ops/mask_filter_ops always replayed before geometric_ops, so a
  quick-selection or ROI-scoped filter committed after a flip/rotate landed
  at the mirrored location (frozen geometry captured in display coordinates,
  replayed in the raw frame). Commit paths now stamp after_geometric_ops
  (the geometric-op count at commit time) and processing_state_from_gui
  interleaves scope steps at that position. Legacy entries without the key
  keep the historical pre-geometric position so old sidecars replay
  byte-identically.
- ROI-scoped image arithmetic now freezes geometry + position at commit
  (parity with scoped filters); legacy roi_id-only entries keep live
  resolution after geometric_ops.
- operand_resolver is forwarded into the nested roi/mask recursion, so
  scoped image arithmetic honours injected in-memory resolvers instead of
  falling back to disk I/O.
- Malformed frozen_geometry/frozen_mask snapshots now block export:
  _roi_from_frozen_geometry validates the kind and probes rasterisation
  (ROI.to_mask silently rasterises unknown kinds to all-False), and
  missing_roi_references reports unreconstructible snapshots; apply-time
  skips warn instead of being silent.
- rotate_arbitrary's warning no longer claims ROI steps "have been skipped"
  (they never were); it now fires only for live-resolving roi steps after
  the rotation, the genuinely frame-ambiguous case.

Co-Authored-By: Claude Fable 5 <[email protected]>
…calibration

Adversarial stress findings, pinned by tests/test_edge_detection_adversarial.py:

- A constant non-zero image produced an ALL-TRUE edge mask: Sobel leaves an
  O(eps*|data|) float-cancellation residue (~2e-16 for a flat 3.7) that
  defeated the strict "magnitude > 0.0" guard, making the percentile cut
  ~2e-16. The guard is now a relative floor scaled by data magnitude and the
  pixel-size division, so a genuine 1e-12-amplitude step is still detected
  while flat images of any value stay empty.
- ROI-restricted Canny over-detected ~2x (432 vs 218 edge pixels on
  identical data): skimage computes quantile thresholds over the whole
  gradient-magnitude array, so near-zero gradient outside a small ROI
  diluted the cut. Percentile thresholds are now computed over the
  restricted region (mirroring skimage's smoothed ndi.sobel magnitude) and
  passed to skimage as absolute values.
- gradient_magnitude was 2x the true physical slope: skimage's normalised
  Sobel/Scharr kernels respond 2.0 on a unit-slope ramp (two-pixel
  difference span, no /2). Responses are halved before the pixel-size
  division so the docstring's promise of real dz/dx, dz/dy holds.
  Orientation and all relative/normalised outputs are unchanged.

Co-Authored-By: Claude Fable 5 <[email protected]>
…bility

Adversarial review of the async browse path, pinned by the new
tests/test_browse_worker_seams.py and test_browse_metadata_display.py:

- Worker emit guarantees: render_scan_image was unguarded in
  ThumbnailLoader, FolderThumbnailLoader, and ChannelPreviewLoader's
  per-plane loop (and ChannelLoader) — a render exception escaped work()
  with zero or partial emits, leaving cards/panels stuck on their loading
  placeholder forever (QThreadPool swallows the exception). Failures now
  emit null-QImage placeholders / None slots, one per owed render.
- Orphaned card-build queue: appearance changes (colormap/channel/
  align-rows) replace _load_token to drop in-flight renders, which also
  cancelled the timer-sliced card build — the tail cards of a large folder
  were never constructed and the background thumbnail timer polled forever.
  The build now has its own _card_build_token, reset only when the grid
  re-renders; _rerender_scan_thumbnails also keeps not-yet-built entries
  pending so they thumbnail with the new appearance once built.
- Navigation-failure consistency: a failed folder index left _current_dir
  on the failed path while the old folder's entries stayed displayed —
  refresh() retargeted the failed folder and the next navigation pushed it
  onto Back history. current_dir is now restored to the rendered folder,
  the failed push is dropped, and the breadcrumb resynced. Re-renders also
  announce their selection reset via selection_changed(0).
- Cache durability: _process_file cached load_error items keyed by
  (path, mtime, size), pinning transient read failures (network hiccup,
  file locked by the acquisition software) as permanently broken until the
  file's mtime changed. Only healthy items and the unrecognised-file None
  are cached now; failures retry on the next visit.
- STM vs qPlus display chain verified end-to-end (item -> SxmFile unit
  conversions -> card/info-panel strings): a constant-delta-f setpoint can
  never surface as a pA current, negative delta-f survives, and error items
  degrade to explicit "?" placeholders.

Co-Authored-By: Claude Fable 5 <[email protected]>
@jacobson30-bot jacobson30-bot merged commit aca6723 into main Jun 11, 2026
3 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant