diff --git a/CHANGELOG.md b/CHANGELOG.md index ddc5aba4..d3e50565 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,13 @@ # Changelog +## Sprint 110 Map QA evidence drilldown (2026-06-25) + +- Extended the Map workspace QA/QC shortcut with inline evidence after comparing a saved derived selection dataset. +- The result now shows quality-check id, matches, false positives, false negatives, mean IoU and QA warnings beside precision/recall/F1. +- Added an `Open QA/QC evidence` handoff to the existing QA/QC workspace drilldown instead of creating a parallel QA detail system. +- No backend API contracts, migrations, provider fetching, AI behavior or new product domains were introduced. + ## Sprint 109 Map selection QA shortcut (2026-06-25) - Added a Map workspace QA/QC shortcut for saved derived selection datasets. diff --git a/backend/tests/test_sprint110_map_qa_evidence_drilldown.py b/backend/tests/test_sprint110_map_qa_evidence_drilldown.py new file mode 100644 index 00000000..6a2aa774 --- /dev/null +++ b/backend/tests/test_sprint110_map_qa_evidence_drilldown.py @@ -0,0 +1,41 @@ +from pathlib import Path + + +ROOT = Path(__file__).resolve().parents[2] + + +def read_text(relative_path: str) -> str: + return (ROOT / relative_path).read_text(encoding="utf-8") + + +def test_map_qa_result_exposes_quality_check_evidence_contract(): + types = read_text("frontend/src/types.ts") + hook = read_text("frontend/src/hooks/useMapSelectionQa.ts") + workspace = read_text("frontend/src/components/map/MapWorkspace.tsx") + app = read_text("frontend/src/App.tsx") + + assert "quality_check_id?: string" in types + assert "latestMapSelectionQualityCheckId" in hook + assert "parsed.quality_check_id" in hook + assert "setLatestMapSelectionQualityCheckId" in hook + + assert "onOpenMapSelectionQualityEvidence" in app + assert "setActiveWorkspace('analysis')" in app + assert "latestMapSelectionQualityCheckId={latestMapSelectionQualityCheckId}" in app + assert "onOpenMapSelectionQualityEvidence={openMapSelectionQualityEvidence}" in app + + assert "QA evidence" in workspace + assert "Quality check id" in workspace + assert "Mean IoU" in workspace + assert "False positives" in workspace + assert "False negatives" in workspace + assert "Map selection QA warnings" in workspace + assert "Open QA/QC evidence" in workspace + + +def test_map_qa_evidence_keeps_backend_contract_unchanged(): + app = read_text("frontend/src/App.tsx") + api_contracts = read_text("docs/API_CONTRACTS.md") + + assert "/api/v1/qa/detections-vs-reference" in api_contracts + assert "qaApi.runQa" not in app diff --git a/docs/CODEX_EXECUTION_LOG.md b/docs/CODEX_EXECUTION_LOG.md index 0a31a5a3..8073daf1 100644 --- a/docs/CODEX_EXECUTION_LOG.md +++ b/docs/CODEX_EXECUTION_LOG.md @@ -1,3 +1,26 @@ +## Sprint 110 Map QA evidence drilldown (2026-06-25) + +Changed: +- Extended `QaComparisonResult` frontend typing with optional `quality_check_id`, matching the existing backend QA result payload. +- Updated `useMapSelectionQa` to keep the latest persisted map-selection quality-check id after a successful QA/QC comparison. +- Extended the Map workspace QA/QC result state with inline evidence: quality-check id, matches, false positives, false negatives, mean IoU and QA warnings. +- Added `Open QA/QC evidence` handoff from Map workspace to the existing QA/QC workspace drilldown, avoiding a parallel QA detail system. +- Added compact styling for the Map QA evidence and warning surface. +- Updated `frontend/README.md`, `CHANGELOG.md` and `docs/TODO.md`. +- Added regression coverage in `backend/tests/test_sprint110_map_qa_evidence_drilldown.py`. + +Validation: +- RED: `python -m pytest backend\tests\test_sprint110_map_qa_evidence_drilldown.py -q` failed before implementation because the hook/evidence wiring was absent. +- `python -m pytest backend\tests\test_sprint110_map_qa_evidence_drilldown.py -q` passed: 2 tests. + +Limitations: +- The Map evidence handoff opens the existing QA/QC workspace; selecting a specific historical check inside that workspace remains governed by the QA/QC panel's own latest-check behavior. +- False-positive and false-negative geometries are summarized by persisted metrics/findings; dedicated map overlays for unmatched evidence remain future work. +- No backend API contract, migration, provider fetching, AI dependency, real model behavior or new product domain was added. + +Next recommended pass: +- Add optional unmatched-evidence map overlays once the QA persistence model stores explicit matched/unmatched feature ids or geometries. + ## Sprint 109 Map selection QA shortcut (2026-06-25) Changed: diff --git a/docs/TODO.md b/docs/TODO.md index 78a57615..9a50f9c4 100644 --- a/docs/TODO.md +++ b/docs/TODO.md @@ -376,3 +376,4 @@ This file now starts with the current implementation status. Older preparation/b - [x] Persist map area selections as Export Center handoff artifacts. - [x] Persist map area selections as reusable derived vector datasets indexed into `vector_features`. - [x] Add Map workspace QA/QC shortcut for saved derived selection datasets. +- [x] Add Map workspace QA/QC evidence drilldown handoff for saved selection comparisons. diff --git a/frontend/README.md b/frontend/README.md index 888c9869..7bc345d8 100644 --- a/frontend/README.md +++ b/frontend/README.md @@ -281,6 +281,7 @@ AI Lab run controls explicitly explain when no raster dataset is available, inst - After an area extract, `Save area export` persists the selected FeatureCollection as a normal Export Center artifact (`vector_selection_geojson`) so the handoff remains in project export history. - `Save as dataset` persists the same selected FeatureCollection as a derived vector dataset, selects it in the workbench and keeps it queryable through backend `vector_features` for later QA/QC or analysis. - Once a map selection has been saved as a derived dataset, the Map workspace can run QA/QC against a selected reference dataset without switching workspaces. The action reuses the existing QA comparison endpoint and shows precision, recall, F1 and persisted quality-check status inline. +- Map selection QA results also show persisted quality-check id, matches, false positives, false negatives, mean IoU and QA warnings. `Open QA/QC evidence` hands operators to the existing QA/QC evidence drilldown for the full parameters/findings record. - The Data catalog shows a compact selected/reference/candidate/source summary and scan-friendly badges. Persisted `reference` datasets are shown as Reference, non-reference vector/GeoJSON layers are shown as QA Candidates for workbench scanning, and raster/other uploads remain Source. - Dataset cards explain the recommended next action and use compact two-line action buttons for inspect, map, export/QA and metadata refresh. Disabled actions keep a visible reason, such as `Vector/GeoJSON only`. - Raster controls show the latest generated tile manifest path from persisted `raster.tile` jobs and can hand that path directly to Detection Lab or Segmentation Lab with the selected raster dataset. diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 33fc678b..038eb7b3 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -405,6 +405,7 @@ function App(): JSX.Element { mapSelectionQaRunning, mapSelectionQaError, mapSelectionQaResult, + latestMapSelectionQualityCheckId, runMapSelectionQa, setSelectedMapQaReferenceDatasetId, } = useMapSelectionQa({ @@ -413,6 +414,9 @@ function App(): JSX.Element { loadQualityChecks, loadProjectData, }) + const openMapSelectionQualityEvidence = () => { + setActiveWorkspace('analysis') + } const { loadingDemoWorkflow, demoWorkflowMessage, @@ -845,6 +849,7 @@ function App(): JSX.Element { mapSelectionQaRunning={mapSelectionQaRunning} mapSelectionQaError={mapSelectionQaError} mapSelectionQaResult={mapSelectionQaResult} + latestMapSelectionQualityCheckId={latestMapSelectionQualityCheckId} availableMapDatasets={availableMapDatasets} selectedFeature={selectedMapFeature} onSelectMapArea={setSelectedMapAreaId} @@ -861,6 +866,7 @@ function App(): JSX.Element { onDeriveMapSelectionDataset={deriveMapSelectionDataset} onSelectMapQaReferenceDataset={setSelectedMapQaReferenceDatasetId} onRunMapSelectionQa={runMapSelectionQa} + onOpenMapSelectionQualityEvidence={openMapSelectionQualityEvidence} /> ) : null} diff --git a/frontend/src/components/map/MapWorkspace.tsx b/frontend/src/components/map/MapWorkspace.tsx index 18b96249..6156e99b 100644 --- a/frontend/src/components/map/MapWorkspace.tsx +++ b/frontend/src/components/map/MapWorkspace.tsx @@ -202,6 +202,7 @@ interface MapWorkspaceProps { mapSelectionQaRunning: boolean mapSelectionQaError: string | null mapSelectionQaResult: QaComparisonResult | null + latestMapSelectionQualityCheckId: string | null availableMapDatasets: DatasetCreateResponse[] onSelectMapArea: (areaId: string) => void onOpenDatasetInMap: (dataset: DatasetCreateResponse) => void @@ -217,6 +218,7 @@ interface MapWorkspaceProps { onDeriveMapSelectionDataset: (bbox: VectorSelectionBBox) => void onSelectMapQaReferenceDataset: (datasetId: string) => void onRunMapSelectionQa: () => void + onOpenMapSelectionQualityEvidence: () => void } export function MapWorkspace({ @@ -250,6 +252,7 @@ export function MapWorkspace({ mapSelectionQaRunning, mapSelectionQaError, mapSelectionQaResult, + latestMapSelectionQualityCheckId, availableMapDatasets, onSelectMapArea, onOpenDatasetInMap, @@ -265,6 +268,7 @@ export function MapWorkspace({ onDeriveMapSelectionDataset, onSelectMapQaReferenceDataset, onRunMapSelectionQa, + onOpenMapSelectionQualityEvidence, }: MapWorkspaceProps): JSX.Element { const [bboxSelectionMode, setBboxSelectionMode] = useState(false) const [firstSelectionCorner, setFirstSelectionCorner] = useState<[number, number] | null>(null) @@ -685,23 +689,65 @@ export function MapWorkspace({ {mapSelectionQaError ?

{mapSelectionQaError}

: null} {mapSelectionQaResult ? ( -
-
- Precision - {mapSelectionQaResult.precision ?? 'n/a'} +
+
+
+

QA evidence

+

Saved selection comparison

+
+
-
- Recall - {mapSelectionQaResult.recall ?? 'n/a'} -
-
- F1 - {mapSelectionQaResult.f1_score ?? 'n/a'} -
-
- Quality check - {mapSelectionQaResult.status} +
+
+ Precision + {mapSelectionQaResult.precision ?? 'n/a'} +
+
+ Recall + {mapSelectionQaResult.recall ?? 'n/a'} +
+
+ F1 + {mapSelectionQaResult.f1_score ?? 'n/a'} +
+
+ Mean IoU + {mapSelectionQaResult.mean_iou ?? 'n/a'} +
+
+ Matches + {mapSelectionQaResult.matches} +
+
+ False positives + {mapSelectionQaResult.false_positives} +
+
+ False negatives + {mapSelectionQaResult.false_negatives} +
+
+ Quality check id + {latestMapSelectionQualityCheckId ?? 'not persisted'} +
+ {mapSelectionQaResult.warnings.length > 0 ? ( +
+ Map selection QA warnings +
    + {mapSelectionQaResult.warnings.map((warning) => ( +
  • {warning}
  • + ))} +
+
+ ) : null}
) : null}
diff --git a/frontend/src/hooks/useMapSelectionQa.ts b/frontend/src/hooks/useMapSelectionQa.ts index a8662123..3535b168 100644 --- a/frontend/src/hooks/useMapSelectionQa.ts +++ b/frontend/src/hooks/useMapSelectionQa.ts @@ -20,6 +20,7 @@ export function useMapSelectionQa({ const [mapSelectionQaRunning, setMapSelectionQaRunning] = useState(false) const [mapSelectionQaError, setMapSelectionQaError] = useState(null) const [mapSelectionQaResult, setMapQaResult] = useState(null) + const [latestMapSelectionQualityCheckId, setLatestMapSelectionQualityCheckId] = useState(null) const runMapSelectionQa = async () => { if (!selectedProjectId) { @@ -42,6 +43,7 @@ export function useMapSelectionQa({ setMapSelectionQaRunning(true) setMapSelectionQaError(null) setMapQaResult(null) + setLatestMapSelectionQualityCheckId(null) try { const request: QaComparisonRequest = { candidate_dataset_id: latestSelectionDataset.id, @@ -65,6 +67,7 @@ export function useMapSelectionQa({ return } setMapQaResult(parsed) + setLatestMapSelectionQualityCheckId(parsed.quality_check_id ?? null) await loadQualityChecks(selectedProjectId) await loadProjectData(selectedProjectId) } catch (error) { @@ -79,6 +82,7 @@ export function useMapSelectionQa({ mapSelectionQaRunning, mapSelectionQaError, mapSelectionQaResult, + latestMapSelectionQualityCheckId, runMapSelectionQa, setSelectedMapQaReferenceDatasetId, } diff --git a/frontend/src/styles/app.css b/frontend/src/styles/app.css index e691499b..057d0189 100644 --- a/frontend/src/styles/app.css +++ b/frontend/src/styles/app.css @@ -2690,10 +2690,56 @@ button.entity-card { } .map-selection-qa-surface .error, -.map-selection-qa-surface .feature-extract-grid { +.map-selection-qa-surface .map-selection-qa-evidence { grid-column: 1 / -1; } +.map-selection-qa-evidence { + display: grid; + gap: 0.62rem; + min-width: 0; + border: 1px solid rgba(15, 118, 110, 0.2); + border-radius: 8px; + padding: 0.68rem; + background: #ffffff; +} + +.map-selection-qa-evidence .panel-title-row { + margin-bottom: 0; +} + +.map-selection-qa-evidence .secondary-action { + width: fit-content; + max-width: 100%; + margin-top: 0; +} + +.map-selection-qa-warnings { + display: grid; + gap: 0.36rem; + min-width: 0; + border-top: 1px solid var(--line); + padding-top: 0.58rem; +} + +.map-selection-qa-warnings span { + color: var(--muted); + font-size: 0.72rem; + font-weight: 850; + letter-spacing: 0.05em; + text-transform: uppercase; +} + +.map-selection-qa-warnings ul { + display: grid; + gap: 0.28rem; + margin: 0; + padding-left: 1.1rem; + color: var(--text); + font-size: 0.82rem; + line-height: 1.35; +} + @media (max-width: 720px) { .map-selection-qa-surface { grid-template-columns: 1fr; diff --git a/frontend/src/types.ts b/frontend/src/types.ts index 7549589a..ffaa7f5e 100644 --- a/frontend/src/types.ts +++ b/frontend/src/types.ts @@ -601,6 +601,7 @@ export interface QaComparisonResult { unsupported_geometry: boolean unsupported_geometries: string[] generated_at: string + quality_check_id?: string } export interface MetricRead {