diff --git a/CHANGELOG.md b/CHANGELOG.md index d899cab3..ddc5aba4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,14 @@ # Changelog +## Sprint 109 Map selection QA shortcut (2026-06-25) + +- Added a Map workspace QA/QC shortcut for saved derived selection datasets. +- The shortcut reuses the existing QA comparison workflow and persists `QualityCheck`/`Metric` rows through the existing backend route. +- Added reference dataset selection, loading/error state and compact precision/recall/F1/status feedback beside the saved map selection. +- Kept QA orchestration in a dedicated frontend hook so `App.tsx` remains an orchestrator and API calls stay out of the shell component. +- No backend API contracts, migrations, provider fetching, AI behavior or new product domains were introduced. + ## Sprint 108 Map selection derived datasets (2026-06-25) - Added `POST /api/v1/projects/{project_id}/datasets/{dataset_id}/vector/select/derive` to persist a map bbox selection as a reusable derived vector dataset. diff --git a/backend/tests/test_sprint109_map_selection_qa_shortcut.py b/backend/tests/test_sprint109_map_selection_qa_shortcut.py new file mode 100644 index 00000000..12635e0d --- /dev/null +++ b/backend/tests/test_sprint109_map_selection_qa_shortcut.py @@ -0,0 +1,30 @@ +from pathlib import Path + +ROOT = Path(__file__).resolve().parents[2] + + +def test_map_selection_qa_shortcut_uses_existing_qa_workflow_contract() -> None: + hook_path = ROOT / "frontend" / "src" / "hooks" / "useMapSelectionQa.ts" + app = (ROOT / "frontend" / "src" / "App.tsx").read_text(encoding="utf-8") + map_workspace = (ROOT / "frontend" / "src" / "components" / "map" / "MapWorkspace.tsx").read_text(encoding="utf-8") + + assert hook_path.exists() + hook = hook_path.read_text(encoding="utf-8") + assert "qaApi.runQa" in hook + assert "loadQualityChecks" in hook + assert "candidate_dataset_id: latestSelectionDataset.id" in hook + assert "reference_dataset_id: selectedMapQaReferenceDatasetId" in hook + assert "setMapQaResult" in hook + assert "useMapSelectionQa" in app + assert "mapQaReferenceDatasets={referenceDatasets}" in app + assert "onRunMapSelectionQa={runMapSelectionQa}" in app + assert "Run QA on saved dataset" in map_workspace + assert "mapSelectionQaError" in map_workspace + assert "mapSelectionQaResult" in map_workspace + + +def test_app_keeps_qa_api_calls_out_of_orchestration() -> None: + app = (ROOT / "frontend" / "src" / "App.tsx").read_text(encoding="utf-8") + + assert "qaApi" not in app + assert "from './services/api'" not in app diff --git a/docs/CODEX_EXECUTION_LOG.md b/docs/CODEX_EXECUTION_LOG.md index 64238ac7..76487f23 100644 --- a/docs/CODEX_EXECUTION_LOG.md +++ b/docs/CODEX_EXECUTION_LOG.md @@ -1,3 +1,28 @@ +## Sprint 109 Map selection QA shortcut (2026-06-25) + +Changed: +- Added `useMapSelectionQa` to keep Map workspace QA/QC orchestration out of `App.tsx`. +- Added a Map workspace QA/QC shortcut after `Save as dataset`, allowing the latest derived map selection dataset to be compared against a selected reference dataset. +- The shortcut reuses the existing `qaApi.runQa` flow and refreshes persisted quality checks/project data after completion. +- Added inline precision, recall, F1 and quality-check status feedback in the Map workspace. +- Added compact styling for the Map selection QA surface. +- Updated `frontend/README.md`, `CHANGELOG.md` and `docs/TODO.md`. +- Added regression coverage in `backend/tests/test_sprint109_map_selection_qa_shortcut.py`. + +Validation: +- RED: `python -m pytest backend\tests\test_sprint109_map_selection_qa_shortcut.py -q` failed before implementation because the hook and wiring were absent. +- `python -m pytest backend\tests\test_sprint109_map_selection_qa_shortcut.py -q` passed: 2 tests. +- `cd frontend && npm run typecheck` passed. +- `cd backend && python -m pytest -q` passed: 347 tests. + +Limitations: +- The Map shortcut currently uses the existing QA comparison defaults with IoU threshold `0.5`. +- QA results are summarized inline; detailed false-positive/false-negative evidence remains in the QA/QC workspace. +- No backend API contract, migration, provider fetching, AI dependency, real model behavior or new product domain was added. + +Next recommended pass: +- Add a QA result drilldown/handoff from the Map workspace so operators can inspect persisted false-positive and false-negative evidence directly after running selection QA/QC. + ## Sprint 108 Map selection derived datasets (2026-06-25) Changed: diff --git a/docs/TODO.md b/docs/TODO.md index 3f4efb0d..78a57615 100644 --- a/docs/TODO.md +++ b/docs/TODO.md @@ -375,3 +375,4 @@ This file now starts with the current implementation status. Older preparation/b - [x] Add persisted vector area selection from the Map workspace with bbox extract and GeoJSON download. - [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. diff --git a/frontend/README.md b/frontend/README.md index d65970d0..888c9869 100644 --- a/frontend/README.md +++ b/frontend/README.md @@ -280,6 +280,7 @@ AI Lab run controls explicitly explain when no raster dataset is available, inst - The Map workspace can extract persisted vector features by area: open a ready vector dataset, use `Start map bbox` and click two map corners or enter EPSG:4326 bbox values, then run `Run area extract` to query backend `vector_features`. Results are highlighted on the map and can be downloaded as GeoJSON. - 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. - 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 01eeaf30..33fc678b 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -20,6 +20,7 @@ import { useDetectionWorkflow } from './hooks/useDetectionWorkflow' import { useDatasetWorkflow } from './hooks/useDatasetWorkflow' import { useExportWorkflow } from './hooks/useExportWorkflow' import { useMapSelectionDataset } from './hooks/useMapSelectionDataset' +import { useMapSelectionQa } from './hooks/useMapSelectionQa' import { useMapWorkspaceState } from './hooks/useMapWorkspaceState' import { useMapSelectionExtract } from './hooks/useMapSelectionExtract' import { useProviderCapabilities } from './hooks/useProviderCapabilities' @@ -399,6 +400,19 @@ function App(): JSX.Element { loadDatasetDetails, setMapLayerVisible, }) + const { + selectedMapQaReferenceDatasetId, + mapSelectionQaRunning, + mapSelectionQaError, + mapSelectionQaResult, + runMapSelectionQa, + setSelectedMapQaReferenceDatasetId, + } = useMapSelectionQa({ + selectedProjectId, + latestSelectionDataset, + loadQualityChecks, + loadProjectData, + }) const { loadingDemoWorkflow, demoWorkflowMessage, @@ -826,6 +840,11 @@ function App(): JSX.Element { selectionDatasetSaving={selectionDatasetSaving} selectionDatasetError={selectionDatasetError} latestSelectionDatasetName={latestSelectionDataset?.name ?? null} + mapQaReferenceDatasets={referenceDatasets} + selectedMapQaReferenceDatasetId={selectedMapQaReferenceDatasetId} + mapSelectionQaRunning={mapSelectionQaRunning} + mapSelectionQaError={mapSelectionQaError} + mapSelectionQaResult={mapSelectionQaResult} availableMapDatasets={availableMapDatasets} selectedFeature={selectedMapFeature} onSelectMapArea={setSelectedMapAreaId} @@ -840,6 +859,8 @@ function App(): JSX.Element { onClearMapSelectionExtract={resetMapSelectionExtract} onExportMapSelection={exportMapSelectionGeoJson} onDeriveMapSelectionDataset={deriveMapSelectionDataset} + onSelectMapQaReferenceDataset={setSelectedMapQaReferenceDatasetId} + onRunMapSelectionQa={runMapSelectionQa} /> ) : null} diff --git a/frontend/src/components/map/MapWorkspace.tsx b/frontend/src/components/map/MapWorkspace.tsx index 9384a36d..18b96249 100644 --- a/frontend/src/components/map/MapWorkspace.tsx +++ b/frontend/src/components/map/MapWorkspace.tsx @@ -1,6 +1,6 @@ import { useEffect, useState } from 'react' import GeoMap from '../GeoMap' -import type { AreaRead, DatasetCreateResponse, VectorSelectionBBox, VectorSelectionResponse } from '../../types' +import type { AreaRead, DatasetCreateResponse, QaComparisonResult, VectorSelectionBBox, VectorSelectionResponse } from '../../types' const DEFAULT_SELECTED_FEATURE_FILENAME = 'selected-feature.geojson' const DEFAULT_AREA_SELECTION_FILENAME = 'area-selection.geojson' @@ -197,6 +197,11 @@ interface MapWorkspaceProps { selectionDatasetSaving: boolean selectionDatasetError: string | null latestSelectionDatasetName: string | null + mapQaReferenceDatasets: DatasetCreateResponse[] + selectedMapQaReferenceDatasetId: string + mapSelectionQaRunning: boolean + mapSelectionQaError: string | null + mapSelectionQaResult: QaComparisonResult | null availableMapDatasets: DatasetCreateResponse[] onSelectMapArea: (areaId: string) => void onOpenDatasetInMap: (dataset: DatasetCreateResponse) => void @@ -210,6 +215,8 @@ interface MapWorkspaceProps { onClearMapSelectionExtract: () => void onExportMapSelection: (bbox: VectorSelectionBBox) => void onDeriveMapSelectionDataset: (bbox: VectorSelectionBBox) => void + onSelectMapQaReferenceDataset: (datasetId: string) => void + onRunMapSelectionQa: () => void } export function MapWorkspace({ @@ -238,6 +245,11 @@ export function MapWorkspace({ selectionDatasetSaving, selectionDatasetError, latestSelectionDatasetName, + mapQaReferenceDatasets, + selectedMapQaReferenceDatasetId, + mapSelectionQaRunning, + mapSelectionQaError, + mapSelectionQaResult, availableMapDatasets, onSelectMapArea, onOpenDatasetInMap, @@ -251,6 +263,8 @@ export function MapWorkspace({ onClearMapSelectionExtract, onExportMapSelection, onDeriveMapSelectionDataset, + onSelectMapQaReferenceDataset, + onRunMapSelectionQa, }: MapWorkspaceProps): JSX.Element { const [bboxSelectionMode, setBboxSelectionMode] = useState(false) const [firstSelectionCorner, setFirstSelectionCorner] = useState<[number, number] | null>(null) @@ -644,6 +658,54 @@ export function MapWorkspace({ {latestSelectionDatasetName ? (
Saved derived dataset: {latestSelectionDatasetName}
) : null} + {latestSelectionDatasetName ? ( +{mapSelectionQaError}
: null} + {mapSelectionQaResult ? ( +