From 46204edbf4e86cd7b8152cc1d760357f0a71bc4f Mon Sep 17 00:00:00 2001
From: Codex
Date: Wed, 8 Jul 2026 12:04:21 +0200
Subject: [PATCH] Harden raster detection manifest handoff
---
CHANGELOG.md | 9 +++
...23_raster_detection_handoff_operational.py | 46 +++++++++++++++
docs/CODEX_EXECUTION_LOG.md | 24 ++++++++
docs/TODO.md | 1 +
frontend/src/App.tsx | 4 ++
.../datasets/DatasetDetailPanel.tsx | 4 ++
.../components/datasets/RasterControls.tsx | 42 ++++++++++++--
.../src/components/detection/DetectionLab.tsx | 16 ++++++
frontend/src/hooks/useDatasetWorkflow.ts | 46 +++++++++++++--
frontend/src/styles/app.css | 57 +++++++++++++++++++
frontend/src/types.ts | 10 ++++
11 files changed, 250 insertions(+), 9 deletions(-)
create mode 100644 backend/tests/test_sprint123_raster_detection_handoff_operational.py
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 60314cca..6d264fd6 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1301,3 +1301,12 @@ Added:
- Surfaced the current YOLOv8s hard-negative benchmark candidate and recommended starting threshold `0.25` as operator guidance.
- Added regression coverage for the no-auto-select behavior and UI guardrail copy.
- No backend API contracts, migrations, model downloads, provider fetching or model weight mutation behavior changed.
+
+## Sprint 123 Raster detection manifest handoff (2026-07-08)
+
+- Added a structured raster tile manifest handoff from the Data workspace into Detection Lab.
+- Raster controls now surface manifest tile count, tile size, overlap and tile-set provenance before handing the manifest to AI workflows.
+- The Detection Lab handoff now selects the configured YOLO run path, keeps local model assets explicit, applies the current recommended `0.25` starting threshold and refreshes YOLO preflight for the linked manifest.
+- Detection Lab now shows linked tile manifest provenance plus preflight manifest validation, tile count and `will_run_inference` state.
+- Added regression coverage for the handoff contract and preserved the existing no-auto-select model guardrail.
+- No backend API contracts, migrations, model downloads, provider fetching or model weight mutation behavior changed.
diff --git a/backend/tests/test_sprint123_raster_detection_handoff_operational.py b/backend/tests/test_sprint123_raster_detection_handoff_operational.py
new file mode 100644
index 00000000..95833956
--- /dev/null
+++ b/backend/tests/test_sprint123_raster_detection_handoff_operational.py
@@ -0,0 +1,46 @@
+from pathlib import Path
+
+
+ROOT = Path(__file__).resolve().parents[2]
+
+
+def test_raster_workflow_exposes_structured_tile_manifest_handoff() -> None:
+ hook = (ROOT / "frontend" / "src" / "hooks" / "useDatasetWorkflow.ts").read_text(encoding="utf-8")
+ types = (ROOT / "frontend" / "src" / "types.ts").read_text(encoding="utf-8")
+
+ assert "interface RasterTileHandoff" in types
+ assert "latestRasterTileManifest" in hook
+ assert "toRasterTileHandoff(job)" in hook
+ assert "setLatestRasterTileManifest(manifest)" in hook
+ assert "manifest.manifest_path" in hook
+
+
+def test_raster_controls_show_manifest_details_and_ai_handoff_action() -> None:
+ controls = (ROOT / "frontend" / "src" / "components" / "datasets" / "RasterControls.tsx").read_text(
+ encoding="utf-8"
+ )
+
+ assert "latestRasterTileManifest" in controls
+ assert "Tile count" in controls
+ assert "Tile size" in controls
+ assert "Overlap" in controls
+ assert "Use manifest in Detection Lab" in controls
+ assert "Use manifest in Segmentation Lab" in controls
+
+
+def test_detection_handoff_opens_ai_lab_preflights_manifest_and_keeps_asset_explicit() -> None:
+ app = (ROOT / "frontend" / "src" / "App.tsx").read_text(encoding="utf-8")
+ lab = (ROOT / "frontend" / "src" / "components" / "detection" / "DetectionLab.tsx").read_text(
+ encoding="utf-8"
+ )
+
+ assert "setDetectionTileManifestPath(manifestPath)" in app
+ assert "setSelectedDetectionDatasetId(selectedDataset.id)" in app
+ assert "setSelectedDetectionModelId('yolo-configured')" in app
+ assert "setDetectionConfidenceThreshold(0.25)" in app
+ assert "loadYoloPreflight(manifestPath).catch(() => null)" in app
+ assert "setSelectedModelAssetId(" not in app[app.index("const useRasterTileManifestForDetection"):app.index("const {", app.index("const useRasterTileManifestForDetection"))]
+ assert "Linked tile manifest" in lab
+ assert "Tile manifest validation" in lab
+ assert "Tile count" in lab
+ assert "will_run_inference" in lab
diff --git a/docs/CODEX_EXECUTION_LOG.md b/docs/CODEX_EXECUTION_LOG.md
index f17328f9..bf5a3cc9 100644
--- a/docs/CODEX_EXECUTION_LOG.md
+++ b/docs/CODEX_EXECUTION_LOG.md
@@ -5079,6 +5079,30 @@ Open:
Next recommended pass:
- Implement V1 model-catalog hardening: show available local model assets, make active model/threshold explicit, and prevent accidental silent default activation.
+## Sprint 123 Raster detection manifest handoff (2026-07-08)
+
+Changed:
+- Added a typed `RasterTileHandoff` frontend contract so raster tile jobs can expose manifest path, tile count, tile size, overlap, tile-set id and source dataset provenance.
+- Updated the dataset workflow hook to retain the latest structured raster tile manifest alongside the existing manifest path.
+- Updated Raster Controls to show manifest details and explicit Detection/Segmentation Lab handoff actions.
+- Updated the Detection Lab handoff so a raster manifest selects `yolo-configured`, selects the active raster dataset, applies threshold `0.25`, refreshes YOLO preflight and does not auto-select a local model asset.
+- Added linked tile manifest, manifest validation, tile count and `will_run_inference` visibility to Detection Lab.
+- Added regression coverage in `backend/tests/test_sprint123_raster_detection_handoff_operational.py`.
+
+Tested:
+- Red step: `python -m pytest backend\tests\test_sprint123_raster_detection_handoff_operational.py -q` failed while the typed handoff, Raster Controls details and Detection Lab preflight indicators were absent.
+- `python -m pytest backend\tests\test_sprint123_raster_detection_handoff_operational.py -q` (`3 passed`)
+- `python -m pytest backend\tests\test_sprint99_raster_ui_handoff.py backend\tests\test_sprint122_model_asset_activation_guardrails.py backend\tests\test_sprint120_model_asset_detection_workflow_smoke.py backend\tests\test_sprint123_raster_detection_handoff_operational.py -q` (`8 passed`)
+- `python -m compileall backend/app`
+- `cd frontend && npm run typecheck`
+- `cd frontend && npm run build`
+
+Limitations:
+- This pass improves the operator handoff and preflight visibility only. It does not change backend inference contracts, migrations, model files, provider fetching or automatic model promotion.
+
+Next recommended pass:
+- Add threshold calibration comparison UX so an operator can compare candidate thresholds before promoting a local model.
+
## Sprint 117 Safe local YOLO model activation (2026-07-06)
Changed:
diff --git a/docs/TODO.md b/docs/TODO.md
index 2bbc8a47..a8cc2145 100644
--- a/docs/TODO.md
+++ b/docs/TODO.md
@@ -418,6 +418,7 @@ This file now starts with the current implementation status. Older preparation/b
- [x] Train/evaluate a YOLOv8s hard-negative local building-detector candidate on Tower and keep it inactive because hard-negative false positives remain.
- [x] Add an operator-facing local model catalog/activation workflow with SHA256, active model status and explicit threshold guidance.
- [x] Block silent local model asset auto-selection in Detection Lab.
+- [x] Add structured raster tile manifest handoff into Detection Lab with linked preflight visibility.
- [ ] Add full threshold calibration comparison UX so detection runs can compare candidate thresholds before promotion.
- [ ] Add more AOIs after the tile-level baseline so the next local model attempt is not limited to Geel/Mol/Turnhout.
- [ ] Add negative/background AOIs so the next tile dataset is not all positive tiles.
diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx
index 45fde704..052ab980 100644
--- a/frontend/src/App.tsx
+++ b/frontend/src/App.tsx
@@ -83,6 +83,7 @@ function App(): JSX.Element {
jobs,
rasterPreview,
latestRasterTileManifestPath,
+ latestRasterTileManifest,
selectedIntersectTargetId,
selectedClipAreaId,
rasterTileSize,
@@ -279,6 +280,8 @@ function App(): JSX.Element {
setDetectionTileManifestPath(manifestPath)
setSelectedDetectionDatasetId(selectedDataset.id)
setSelectedDetectionModelId('yolo-configured')
+ setDetectionConfidenceThreshold(0.25)
+ loadYoloPreflight(manifestPath).catch(() => null)
setActiveWorkspace('ai')
}
const {
@@ -1088,6 +1091,7 @@ function App(): JSX.Element {
selectedRasterStats,
rasterPreview,
latestRasterTileManifestPath,
+ latestRasterTileManifest,
rasterUnavailableMessage,
selectedClipAreaId,
selectedIntersectTargetId,
diff --git a/frontend/src/components/datasets/DatasetDetailPanel.tsx b/frontend/src/components/datasets/DatasetDetailPanel.tsx
index 6f64a2b8..a4bc3fb1 100644
--- a/frontend/src/components/datasets/DatasetDetailPanel.tsx
+++ b/frontend/src/components/datasets/DatasetDetailPanel.tsx
@@ -5,6 +5,7 @@ import type {
RasterMetadataResponse,
RasterPreviewResponse,
RasterStatsResponse,
+ RasterTileHandoff,
VectorSummary,
} from '../../types'
import { RasterControls } from './RasterControls'
@@ -20,6 +21,7 @@ export interface DatasetDetailPanelProps {
selectedRasterStats: RasterStatsResponse | null
rasterPreview: RasterPreviewResponse | null
latestRasterTileManifestPath: string
+ latestRasterTileManifest: RasterTileHandoff | null
rasterUnavailableMessage: string | null
selectedClipAreaId: string
selectedIntersectTargetId: string
@@ -104,6 +106,7 @@ export function DatasetDetailPanel({
selectedRasterStats,
rasterPreview,
latestRasterTileManifestPath,
+ latestRasterTileManifest,
rasterUnavailableMessage,
selectedClipAreaId,
selectedIntersectTargetId,
@@ -178,6 +181,7 @@ export function DatasetDetailPanel({
selectedRasterStats={selectedRasterStats}
rasterPreview={rasterPreview}
latestRasterTileManifestPath={latestRasterTileManifestPath}
+ latestRasterTileManifest={latestRasterTileManifest}
rasterUnavailableMessage={rasterUnavailableMessage}
selectedClipAreaId={selectedClipAreaId}
rasterTileSize={rasterTileSize}
diff --git a/frontend/src/components/datasets/RasterControls.tsx b/frontend/src/components/datasets/RasterControls.tsx
index 8234ec93..7023e08f 100644
--- a/frontend/src/components/datasets/RasterControls.tsx
+++ b/frontend/src/components/datasets/RasterControls.tsx
@@ -1,4 +1,4 @@
-import type { AreaRead, RasterMetadataResponse, RasterPreviewResponse, RasterStatsResponse } from '../../types'
+import type { AreaRead, RasterMetadataResponse, RasterPreviewResponse, RasterStatsResponse, RasterTileHandoff } from '../../types'
interface RasterControlsProps {
areas: AreaRead[]
@@ -7,6 +7,7 @@ interface RasterControlsProps {
selectedRasterStats: RasterStatsResponse | null
rasterPreview: RasterPreviewResponse | null
latestRasterTileManifestPath: string
+ latestRasterTileManifest: RasterTileHandoff | null
rasterUnavailableMessage: string | null
selectedClipAreaId: string
rasterTileSize: number
@@ -61,6 +62,7 @@ export function RasterControls({
selectedRasterStats,
rasterPreview,
latestRasterTileManifestPath,
+ latestRasterTileManifest,
rasterUnavailableMessage,
selectedClipAreaId,
rasterTileSize,
@@ -167,11 +169,41 @@ export function RasterControls({
{latestRasterTileManifestPath ||
'No tile manifest generated yet. Generate tiles before handing this raster to Detection Lab.'}
-