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.'}
-