Files
ui/_docs/03_implementation/batch_04_report.md
Oleksandr Bezdieniezhnykh 1dd25edee3 [AZ-460] [AZ-462] [AZ-466] [AZ-475] Batch 4 - destructive UX/forms/overlay/save
AZ-466 — Destructive UX policy + ConfirmDialog a11y + no-alert (4pts):
  src/components/ConfirmDialog.test.tsx (8 fast),
  tests/destructive_ux.test.tsx (4 fast, AdminPage class-delete drift),
  e2e/tests/destructive_ux.e2e.ts. New static checks STC-SEC7 (alert
  allowlist) + STC-SEC8 (destructive-surfaces gated/drift) wired through
  scripts/check-banned-deps.mjs reading tests/security/banned-deps.json.

AZ-475 — Numeric form input rejection (2pts):
  tests/form_hygiene.test.tsx (3 fast). Documents two SettingsPage drifts:
  silent zero coercion via parseInt(v)||0 and labels missing htmlFor.

AZ-462 — Overlay membership at in-window edges (2pts):
  tests/overlay_membership.test.tsx (6 fast). Documents getTimeWindowDetections
  strict < drift; AC-1 boundary tests are it.fails(); AC-2 / control PASS.
  Mocks HTMLCanvasElement.getContext to capture strokeRect.

AZ-460 — Annotation save URL + payload contract (2pts):
  tests/annotations_endpoint.test.tsx (6 fast),
  e2e/tests/annotations_endpoint.e2e.ts. AC-1 URL canary PASSes; AC-2
  payload missing 4 fields documented as it.fails(); AC-3 manual-draw
  PASS, AI-suggestion-accept + bulk-edit-save QUARANTINE skip.

Test infrastructure:
  - tests/setup.ts: NoopResizeObserver + NoopEventSource JSDOM polyfills.
  - tests/msw/handlers/annotations.ts: doubly-prefixed paths matching
    production calls (e.g. /api/annotations/annotations).
  - tests/msw/handlers/flights.ts: plural /aircrafts paths.

Verification: bun run test:fast → 80 passed, 13 skipped (14 files).
scripts/run-tests.sh --static-only → 24/24 PASS (was 22; +STC-SEC7/SEC8).
Per-batch self-review verdict: PASS_WITH_WARNINGS. Cumulative review
of batches 04-06 due after batch 6 per implement/SKILL.md Step 14.5.
Report: _docs/03_implementation/batch_04_report.md.

Also includes the previously-untracked
_docs/03_implementation/cumulative_review_batches_01-03_report.md
generated at the start of this session before batch 4 began.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-11 04:15:01 +03:00

22 KiB
Raw Permalink Blame History

Batch Report

Batch: 04 Tasks: AZ-466 (Destructive UX policy + ConfirmDialog + no-alert), AZ-475 (Numeric form hygiene), AZ-462 (Overlay window membership), AZ-460 (Annotation save URL + payload contract) Date: 2026-05-11 Cycle: Phase A baseline, Step 6 — Implement Tests Total complexity: 10 pts (4 + 2 + 2 + 2)

Task Results

Task Status Files Modified Tests AC Coverage Issues
AZ-466_test_destructive_ux Done 2 created (1 ConfirmDialog unit + 1 cross-component); 1 e2e created; 1 modified (tests/security/banned-deps.json adds alert_calls + destructive_surfaces); 1 modified (scripts/check-banned-deps.mjs + scripts/run-tests.sh add STC-SEC7 / STC-SEC8) 8 fast ConfirmDialog.test.tsx (7 pass, 1 skipped); 4 fast tests/destructive_ux.test.tsx (3 pass + 1 skip QUARANTINE incl. 2 it.fails()); 2 e2e e2e/tests/destructive_ux.e2e.ts (both test.fail); 2 new static checks (PASS) 5 / 5 ACs covered 5 documented drifts: ConfirmDialog missing 4 a11y attrs (role="dialog", aria-modal, aria-labelledby, aria-describedby); no focus trap; AdminPage class-delete bypasses ConfirmDialog (file in destructive_surfaces.drift); alert() allowlist seeded with 4 production callsites (Phase B drains it)
AZ-475_test_form_hygiene Done 1 created (tests/form_hygiene.test.tsx) 3 fast (2 pass, including 1 control + 1 it.fails() per AC) 2 / 2 ACs covered 2 documented drifts: <label> lacks htmlFor; parseInt(v) || 0 silently coerces empty/non-numeric to 0 and PUTs
AZ-462_test_overlay_membership Done 1 created (tests/overlay_membership.test.tsx) 6 fast (5 pass, including 2 it.fails() for AC-1 inclusive boundary) 3 / 3 ACs covered 1 documented drift: getTimeWindowDetections uses strict < instead of <=; AC-1 boundary tests are it.fails() until production lifts the operator
AZ-460_test_annotations_endpoint Done 1 created (tests/annotations_endpoint.test.tsx); 1 e2e created (e2e/tests/annotations_endpoint.e2e.ts); 1 modified (tests/msw/handlers/annotations.ts doubly-prefixed paths); 1 modified (tests/msw/handlers/flights.ts plural aircrafts paths) 6 fast (4 pass, 2 skipped QUARANTINE, including 1 it.fails() for AC-2 payload shape); 3 e2e (1 skip-on-no-seed, 2 test.fail for AC-2) 3 / 3 ACs covered 2 documented drifts: save body sends only {mediaId, time, detections} instead of the 6-field wire contract {Source, WaypointId, videoTime, mediaId, detections, status}; AI-suggestion-accept and bulk-edit-save entry points wholly absent in production (it.skip QUARANTINE)

AC Test Coverage: All covered (13 / 13 ACs across the four tasks)

AZ-466 — Destructive UX policy (5 ACs, 14 scenarios)

Scenario Where Profile Status
AC-1 / FT-P-04 (ConfirmDialog role="dialog" + aria-modal) src/components/ConfirmDialog.test.tsx fast it.fails() — both attrs missing
AC-1 / FT-P-05 (ConfirmDialog labeled by title via aria-labelledby + described by message) same fast it.fails() — neither attr today
AC-1 / FT-P-06 (Escape key closes dialog) same fast PASS — production already calls onClose on Escape
AC-1 / focus-trap (Tab cycles within dialog) same fast it.skip QUARANTINE — no trap implemented
AC-1 / control: dialog renders (positive sanity) same fast PASS
AC-1 / control: confirm/cancel callbacks fire same fast PASS
AC-1 / control: hidden when closed same fast PASS
AC-2 / FT-P-26 (Delete → Confirm → DELETE fires) tests/destructive_ux.test.tsx + e2e/tests/destructive_ux.e2e.ts fast + e2e it.fails() (fast) + test.fail (e2e) — AdminPage bypasses ConfirmDialog
AC-2 / FT-N-07 (Delete → Cancel → no DELETE) same fast + e2e it.fails() + test.fail
AC-2 / control: production today deletes immediately tests/destructive_ux.test.tsx fast PASS — pins drift
AC-3 / no alert() outside allowlist scripts/run-tests.sh::STC-SEC7check-banned-deps.mjs --kind=alert_calls static PASS (allowlist enforced; new alerts FAIL)
AC-4 / FT-P-27 (every destructive surface gated or in drift list) STC-SEC8--kind=destructive_surfaces static PASS (3 files: 2 gated, 1 drift)
AC-4 / runtime mirror (one example via class-delete) tests/destructive_ux.test.tsx fast covered by AC-2 above
AC-5 / NFT-SEC-07 (no alert() in src/) STC-SEC7 (allowlist) static PASS — static check is the gating signal

AC summary:

  • AC-1 ConfirmDialog a11y → 4 it.fails() + 1 it.skip + 4 controls; FT-P-06 (Escape) PASS.
  • AC-2 Delete-confirm-cancel happy path → it.fails() + control + e2e companion (test.fail).
  • AC-3 / AC-5 No alert() → STC-SEC7 with 4-entry allowlist (Phase B drains).
  • AC-4 Destructive surfaces enumeration → STC-SEC8 file-level heuristic (3 files: MediaList.tsx and FlightsPage.tsx gated; AdminPage.tsx in drift).

AZ-475 — Numeric form input rejection (2 ACs, 3 scenarios)

Scenario Where Profile Status
AC-1 / FT-N-11 (clear → validation error + no PUT) tests/form_hygiene.test.tsx fast it.fails() — silent zero today
AC-1 / control: production silently coerces empty input to 0 and PUTs same fast PASS — pins drift
AC-2 / FT-N-12 (non-numeric → validation error + no PUT) same fast it.fails() — same coercion path

AC summary:

  • AC-1 Empty input rejection → it.fails() + control proving defaultCameraWidth: 0 PUTs today.
  • AC-2 Non-numeric rejection → it.fails() (the <input type="number"> path swallows non-numeric chars; the helper sets the value via dispatchEvent to force the React state).

AZ-462 — Overlay membership at in-window edges (3 ACs, 6 scenarios)

Scenario Where Profile Status
AC-1 / FT-P-14 (annotation EXACTLY on lower bound IS rendered) tests/overlay_membership.test.tsx fast it.fails() — strict < excludes boundary
AC-1 / FT-P-15 (annotation EXACTLY on upper bound IS rendered) same fast it.fails() — same drift
AC-1 / control: strict < excludes the boundary today same fast PASS — pins drift
AC-2 / FT-N-01 (annotation BEFORE lower bound NOT rendered) same fast PASS
AC-2 / FT-N-02 (annotation AFTER upper bound NOT rendered) same fast PASS
AC-2 / positive control: annotation INSIDE the window IS rendered same fast PASS — proves test apparatus would observe a render

AC summary:

  • AC-1 Inclusive boundary → 2 it.fails() + control proving exclusion today.
  • AC-2 Strict exclusion outside the window → 2 PASS + positive control (apparatus sanity).
  • AC-3 Canvas-output assertion (not React state) → satisfied by mocking HTMLCanvasElement.prototype.getContext to capture every strokeRect call.

AZ-460 — Annotation save URL + payload contract (3 ACs, 6 scenarios)

Scenario Where Profile Status
AC-1 / FT-P-07 (URL canary: /api/annotations/annotations) tests/annotations_endpoint.test.tsx + e2e/tests/annotations_endpoint.e2e.ts fast + e2e PASS (fast) — production already POSTs the doubly-prefixed URL; e2e gated by suite stack
AC-2 / FT-P-08 (required-fields: Source, WaypointId, videoTime, mediaId, detections, status) same fast + e2e it.fails() + test.fail — production sends only {mediaId, time, detections}
AC-2 / control: production sends partial body ({mediaId, detections}) tests/annotations_endpoint.test.tsx fast PASS — pins drift
AC-3 / manual-draw / select-existing entry point same + e2e fast + e2e PASS — exercises the only wired entry point
AC-3 / AI-suggestion-accept entry point same fast it.skip QUARANTINE — no production path today
AC-3 / bulk-edit-save entry point same fast it.skip QUARANTINE — no production path today

AC summary:

  • AC-1 URL canary → PASS for the only wired save path; e2e companion gated.
  • AC-2 Required fields → it.fails() for the missing 4 fields; control pins the partial-body drift.
  • AC-3 Multi-entry-point coverage → 1 PASS for manual-draw + 2 it.skip QUARANTINE for unimplemented paths (test shape documented in skip comments).

Code Review Verdict: PASS_WITH_WARNINGS

Self-review walked inline per .cursor/skills/code-review/SKILL.md phases 17.

  • Phase 1 (Context): 4 task specs re-read; _docs/02_document/module-layout.md Blackbox Tests envelope respected; reuses helpers from AZ-456 (tests/helpers/{render,auth}.ts) and fixtures (seed_users, seed_flights). No new shared helpers introduced — the form-hygiene file inlines a small inputForLabel(...) DOM-traversal helper because SettingsPage's labels lack htmlFor (drift documented in the test header).
  • Phase 2 (Spec compliance): every AC across the four task specs has at least one test (running, it.fails(), or it.skip with QUARANTINE reason). Drift handling uniform with batches 13: it.fails() for documented production drift (attribute/operator/payload-field exists in spec but absent in code); it.skip for behavior wholly absent (AI-suggestion-accept save, bulk-edit save, focus trap inside ConfirmDialog).
  • Phase 3 (Code quality): check-banned-deps.mjs's new checkDestructiveSurfaces is a single function with one responsibility (file-level heuristic comparing gated drift against the live filesystem); tests/security/banned-deps.json alert_calls and destructive_surfaces sections each have an ac: field, a scope: field, an explicit match: description, and inline $*_comment hooks for code review; the test files use Arrange/Act/Assert structure consistently; no bare catch blocks; no error suppression.
  • Phase 4 (Security): no new secrets in test fixtures (reuses AZ-457's test-bearer-default); the AZ-466 changes strengthen security posture (every alert() and every destructive surface is now allowlisted and code-review-visible); the new static checks fail-closed on additions; the check-banned-deps.mjs walks files and runs ripgrep / regex over them — no execution of test inputs.
  • Phase 5 (Performance): fast suite 5.5 s wall-clock for 80 + 13-skipped tests across 14 files (was 4.4 s for 57 + 9 skipped in batch 3 — +1.1 s for 23 new tests, well under the 5 min budget). Static profile ~16 s for 24 checks (was 12 s for 22 in batch 3; +4 s primarily from the two new STC-SEC7 / STC-SEC8 checks reading tests/security/banned-deps.json). The it.fails() tests each consume ~1 s waiting for the assertion to NOT match — same shape as batches 13, acceptable.
  • Phase 6 (Cross-task consistency): the four tasks touch disjoint subsystems (ConfirmDialog + AdminPage destructive UX vs SettingsPage form hygiene vs CanvasEditor overlay vs AnnotationsPage save). Shared surface = tests/helpers/, tests/fixtures/, tests/msw/, tests/security/banned-deps.json — all consumed read-only or strictly extended (new sections, never modifying existing ones). No contract collisions; no duplicate symbols.
  • Phase 7 (Architecture compliance):
    • Test files import only public seams:
      • src/components/ConfirmDialog.test.tsx: ConfirmDialog default export.
      • tests/destructive_ux.test.tsx: AdminPage default export.
      • tests/form_hygiene.test.tsx: SettingsPage default export.
      • tests/overlay_membership.test.tsx: CanvasEditor default export + AnnotationSource/AnnotationStatus/etc. enums (public types).
      • tests/annotations_endpoint.test.tsx: AnnotationsPage default export + FlightProvider (public symbol on FlightContext.tsx) + public enums.
    • No imports of *.internal.* files, no reaching into other components' private files.
    • E2E tests don't import any production modules — Playwright primitives only (consistent with batches 13).
    • No new cyclic module dependencies introduced.
    • Test setup: tests/setup.ts gained two no-op JSDOM polyfills (ResizeObserver and EventSource). These are environment polyfills (not production code workarounds), and per-test installations of richer stubs (e.g. tests/sse_lifecycle.test.tsx's EventSource fake) override + restore — verified by re-running batch 3's SSE suite alongside the new tests with no regressions.

Findings

  1. Low / Maintainability / Drift — AZ-466 AC-1 four ConfirmDialog a11y attributes (role="dialog", aria-modal, aria-labelledby, aria-describedby) are missing today; FT-P-04 / FT-P-05 are it.fails(). The Escape handler exists (FT-P-06 PASSes), but no focus trap (it.skip QUARANTINE). Recommendation: file feat(confirm-dialog): a11y attrs + focus trap in Phase B. Touches one file (src/components/ConfirmDialog.tsx); should also localize the title via t() if the existing copy is hard-coded.

  2. Low / Maintainability / Drift — AZ-466 AC-4 AdminPage.handleDeleteClass calls api.delete without ConfirmDialog. The file is recorded in tests/security/banned-deps.json::destructive_surfaces.drift to keep the static check passing while making the gap visible in code review. Recommendation: feat(admin): gate class-delete via ConfirmDialog — moves src/features/admin/AdminPage.tsx from drift to gated and flips FT-P-26 / FT-N-07 from it.fails() to PASS.

  3. Low / Maintainability / Drift — AZ-466 AC-3 / AC-5 alert() allowlist contains 4 callsites (MediaList.tsx, FlightsPage.tsx, JsonEditorDialog.tsx, flightPlan.tsx). Each is a per-feature blocker dialog or validation message that should migrate to a non-blocking toast or an inline error. Recommendation: 4 small Phase B tasks (one per file), each removing one allowlist entry — measurable progress.

  4. Low / Maintainability / Drift — AZ-475 AC-1 silent-zero coercion AND <label> without htmlFor. Two related drifts in the same file (SettingsPage.tsx). Recommendation: combined Phase B task feat(settings): numeric input validation + label association that lands a useNumericField() hook (or equivalent) and adds id/htmlFor so screen readers and getByLabelText both work.

  5. Low / Maintainability / Drift — AZ-462 AC-1 strict < in getTimeWindowDetections → boundary annotations are dropped. Recommendation: one-character production change (<<=) + flip FT-P-14/15 from it.fails() to PASS. Confirm with the suite annotations service that lowerBound and upperBound are inclusive on the wire.

  6. Low / Architecture / Drift — AZ-460 AC-2 save body shape (4 missing fields). The fields touch the wire contract; the suite annotations service must be checked to see what it expects today. Recommendation: a Phase B task feat(annotations-save): emit Source/WaypointId/videoTime/status that lifts the body shape. May require a coordinated change with the annotations service if the server today happily accepts the partial body.

  7. Low / Architecture / Drift — AZ-460 AC-3 only one save entry point exists. The AI-suggestion-accept and bulk-edit-save paths are documented in it.skip QUARANTINE comments with the test shape they should take when the production paths land. Recommendation: 2 Phase B feature tasks (AI-accept, bulk-edit) — the test side is ready to be activated by removing the .skip.

  8. Low / Architecture / Drift (test infrastructure)tests/msw/handlers/annotations.ts and tests/msw/handlers/flights.ts both gained doubly-prefixed / plural paths (/api/annotations/annotations, /api/flights/aircrafts) to match what production callers actually use. The single-prefix paths are kept for backward compatibility with batch 13 tests. Recommendation: Phase B tracker entry chore(test-infra): drop the single-prefix annotation/flight paths once production has been confirmed to use only the doubly-prefixed/plural shapes everywhere.

  9. Low / Architecture / Drift (test infrastructure)tests/msw/handlers/admin.ts /api/admin/users returns paginate(seedUsers) while AdminPage reads it as a flat User[]. The destructive-UX test override returns [] (flat) to keep AdminPage from crashing. Recommendation: confirm whether the suite admin service emits a flat array or a paginated payload, then align the MSW default with production. Either way, file as chore(admin-handler): align msw with prod /admin/users shape.

  10. Low / Architecture / Interpretation (carried over from batches 13) — Test helpers (tests/helpers/{render,auth,sse-mock}.ts) and the polyfills in tests/setup.ts import / patch production accessors. Reaffirmed per the batch-1 / 2 / 3 rule: "Black-box discipline applies to test bodies, not to test setup helpers / composition-root wrappers / consumer-pattern mirrors". The polyfills are JSDOM environment plumbing (no-op stubs for browser APIs JSDOM doesn't ship), not production-code workarounds.

Auto-Fix Attempts: 0

Stuck Agents: None

Files Changed (10)

Created — src/ (1)

src/components/ConfirmDialog.test.tsx     # AZ-466 fast — 8 tests (1 skipped)

Created — tests/ (3)

tests/destructive_ux.test.tsx              # AZ-466 fast — 4 tests (1 skipped)
tests/form_hygiene.test.tsx                # AZ-475 fast — 3 tests
tests/overlay_membership.test.tsx          # AZ-462 fast — 6 tests
tests/annotations_endpoint.test.tsx        # AZ-460 fast — 6 tests (2 skipped)

Created — e2e/tests/ (2)

e2e/tests/destructive_ux.e2e.ts            # AZ-466 e2e — 2 scenarios (both test.fail)
e2e/tests/annotations_endpoint.e2e.ts      # AZ-460 e2e — 3 scenarios (1 skip-on-no-seed, 1 test.fail)

Modified (5)

tests/setup.ts                             # JSDOM polyfills: NoopResizeObserver, NoopEventSource
tests/security/banned-deps.json            # New sections: alert_calls (4-entry allowlist) + destructive_surfaces (2 gated, 1 drift)
scripts/check-banned-deps.mjs              # New checkDestructiveSurfaces; allowlist support in checkSourceTree; main() routing
scripts/run-tests.sh                       # Add STC-SEC7 (no-alert) + STC-SEC8 (destructive surfaces)
tests/msw/handlers/annotations.ts          # Add doubly-prefixed annotation/settings/classes handlers (production shape)
tests/msw/handlers/flights.ts              # Add plural /api/flights/aircrafts handlers (production shape)
_docs/_autodev_state.md                    # Batch 4 sub_step pointer + notes

(File count = 4 created in tests/ + 1 created in src/ + 2 created in e2e/tests/ + 5 modified + 2 MSW handlers modified = 14 file touches; uniqueness count is 12 — tests/msw/handlers/annotations.ts and tests/msw/handlers/flights.ts are extensions of existing files.)

Verification Run (host)

$ bun run test:fast
 ✓ tests/infrastructure.test.ts (5 tests) 53ms
 ✓ src/api/client.test.ts (9 tests) 61ms
 ✓ tests/sse_lifecycle.test.tsx (9 tests | 1 skipped) 74ms
 ✓ src/auth/AuthContext.test.tsx (4 tests) 249ms
 ✓ src/components/Header.test.tsx (6 tests | 1 skipped) 302ms
 ✓ src/components/ConfirmDialog.test.tsx (8 tests | 1 skipped) 285ms
 ✓ tests/wire_contract.test.ts (11 tests | 2 skipped) 8ms
 ✓ tests/i18n.test.tsx (4 tests | 2 skipped) 4ms
 ✓ tests/annotations_endpoint.test.tsx (6 tests | 2 skipped) 523ms
 ✓ src/auth/ProtectedRoute.test.tsx (12 tests | 3 skipped) 1101ms
 ✓ mission-planner/src/test/jsonImport.test.ts (6 tests) 5ms
 ✓ tests/overlay_membership.test.tsx (6 tests) 2137ms
 ✓ tests/form_hygiene.test.tsx (3 tests) 2351ms
 ✓ tests/destructive_ux.test.tsx (4 tests | 1 skipped) 2342ms

 Test Files  14 passed (14)
      Tests  80 passed | 13 skipped (93)

$ ./scripts/run-tests.sh --static-only
[run-tests] static profile PASSED — 24/24 checks (was 22 in batch 3; +2 from batch 4: STC-SEC7, STC-SEC8)

$ ./scripts/run-tests.sh
[run-tests] static profile : ran (PASS)
[run-tests] fast profile   : ran (PASS)
[run-tests] e2e profile    : skipped (host)
[run-tests] exit code      : 0

E2E profile not exercised in this batch — same Risk 4 as batches 13 (requires docker compose -f e2e/docker-compose.suite-e2e.yml up -d plus parent-suite :test images). The new e2e companion files (e2e/tests/destructive_ux.e2e.ts, e2e/tests/annotations_endpoint.e2e.ts) will run on the suite stack.

Next Batch

Remaining: 14 test-implementation tasks in _docs/02_tasks/todo/:

  • AZ-461 (detection endpoints sync/async/long-video, 2pts)
  • AZ-463 (flight selection persistence + memory soaks, 3pts)
  • AZ-464 (bulk-validate URL + body + UI sync, 2pts)
  • AZ-469 (browser support + responsive variants, 2pts)
  • AZ-470 (panel-width debounced PUT + rehydration, 2pts)
  • AZ-471 (CanvasEditor draw/resize/multi-select/zoom/pan, 5pts)
  • AZ-472 (DetectionClasses load + hotkeys + click + fallback, 3pts)
  • AZ-473 (PhotoMode switch + auto-select + yoloId wire, 2pts) — soft dep on AZ-472
  • AZ-474 (Tile-split + YOLO parser + auto-zoom + indicator, 3pts)
  • AZ-476 (Upload 501 MB → 413 → user-visible error, 2pts)
  • AZ-477 (Settings save 500/network resilience, 3pts)
  • AZ-478 (Network offline + SSE disconnect + tainted-canvas, 3pts)
  • AZ-479 (Bundle ≤2 MB + mission-planner excluded + FCP + soak, 3pts)
  • AZ-480 (Prod image nginx:alpine + 500M + 9 routes + edge RAM, 3pts)

All carry Component: Blackbox Tests and Dependencies: AZ-456 (✓ done). Soft cross-dep: AZ-473 needs AZ-472's DetectionClasses fixtures.

Suggested next batch (4 tasks, ~9 pts, dependency-disjoint at the file level): AZ-461 (detection endpoints, 2pts); AZ-464 (bulk-validate URL/body/sync, 2pts); AZ-470 (panel-width debounced PUT, 2pts); AZ-472 (DetectionClasses load + hotkeys, 3pts). Together they touch the detect/ endpoints, bulk dataset endpoints, useResizablePanel hook, and the DetectionClasses component — disjoint at the file level.

A cumulative cross-batch review (batches 0406) is due after batch 6 per implement/SKILL.md Step 14.5 (every 3 batches). Today's per-batch self-review is recorded above; the cumulative pass will compare batches 0406 against architecture findings F1F9 (the same baseline used by the batches 0103 cumulative review).

Recommendation: continue in a new conversation. Batch 4 added 6 new files + 2 new static checks + 23 new fast tests + 2 new e2e files; the next batch will load distinct task specs (detect endpoints, bulk-validate, resizable-panel, DetectionClasses).