Add map selection QA shortcut
GeoIntel CI / docs-smoke (push) Has been cancelled
GeoIntel CI / contract-smoke (push) Has been cancelled

This commit is contained in:
Codex
2026-06-25 02:35:21 +02:00
parent f712272b72
commit e189a2881d
9 changed files with 265 additions and 1 deletions
+8
View File
@@ -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.
@@ -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
+25
View File
@@ -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:
+1
View File
@@ -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.
+1
View File
@@ -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.
+21
View File
@@ -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}
+63 -1
View File
@@ -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 ? (
<p className="muted">Saved derived dataset: {latestSelectionDatasetName}</p>
) : null}
{latestSelectionDatasetName ? (
<div className="map-selection-qa-surface" aria-label="Map selection QA shortcut">
<label>
Reference dataset
<select
value={selectedMapQaReferenceDatasetId}
onChange={(event) => onSelectMapQaReferenceDataset(event.target.value)}
disabled={mapQaReferenceDatasets.length === 0 || mapSelectionQaRunning}
>
<option value="">Select reference</option>
{mapQaReferenceDatasets.map((dataset) => (
<option key={dataset.id} value={dataset.id}>
{dataset.name}
</option>
))}
</select>
</label>
<button
className="primary-action"
disabled={!selectedMapQaReferenceDatasetId || mapSelectionQaRunning}
type="button"
onClick={onRunMapSelectionQa}
>
{mapSelectionQaRunning ? 'Running QA...' : 'Run QA on saved dataset'}
</button>
{mapSelectionQaError ? <p className="error">{mapSelectionQaError}</p> : null}
{mapSelectionQaResult ? (
<div className="feature-extract-grid" aria-label="Map selection QA result">
<div>
<span>Precision</span>
<strong>{mapSelectionQaResult.precision ?? 'n/a'}</strong>
</div>
<div>
<span>Recall</span>
<strong>{mapSelectionQaResult.recall ?? 'n/a'}</strong>
</div>
<div>
<span>F1</span>
<strong>{mapSelectionQaResult.f1_score ?? 'n/a'}</strong>
</div>
<div>
<span>Quality check</span>
<strong>{mapSelectionQaResult.status}</strong>
</div>
</div>
) : null}
</div>
) : null}
{areaSelectionPreviewFeatures.length > 0 ? (
<div className="table-scroll feature-property-table" aria-label="Area selection feature table">
<table>
+85
View File
@@ -0,0 +1,85 @@
import { useState } from 'react'
import type { DatasetCreateResponse, JobRead, QaComparisonRequest, QaComparisonResult, QualityCheckRead } from '../types'
import { formatError } from '../lib/formatError'
import { qaApi } from '../services/api'
interface UseMapSelectionQaOptions {
selectedProjectId: string | null
latestSelectionDataset: DatasetCreateResponse | null
loadQualityChecks: (projectId?: string | null) => Promise<QualityCheckRead[] | void>
loadProjectData: (projectId: string) => Promise<unknown>
}
export function useMapSelectionQa({
selectedProjectId,
latestSelectionDataset,
loadQualityChecks,
loadProjectData,
}: UseMapSelectionQaOptions) {
const [selectedMapQaReferenceDatasetId, setSelectedMapQaReferenceDatasetId] = useState('')
const [mapSelectionQaRunning, setMapSelectionQaRunning] = useState(false)
const [mapSelectionQaError, setMapSelectionQaError] = useState<string | null>(null)
const [mapSelectionQaResult, setMapQaResult] = useState<QaComparisonResult | null>(null)
const runMapSelectionQa = async () => {
if (!selectedProjectId) {
setMapSelectionQaError('Select a project before running QA/QC.')
return
}
if (!latestSelectionDataset) {
setMapSelectionQaError('Save the map selection as a dataset before running QA/QC.')
return
}
if (!selectedMapQaReferenceDatasetId) {
setMapSelectionQaError('Select a reference dataset for QA/QC.')
return
}
if (latestSelectionDataset.id === selectedMapQaReferenceDatasetId) {
setMapSelectionQaError('Candidate and reference datasets must be different.')
return
}
setMapSelectionQaRunning(true)
setMapSelectionQaError(null)
setMapQaResult(null)
try {
const request: QaComparisonRequest = {
candidate_dataset_id: latestSelectionDataset.id,
reference_dataset_id: selectedMapQaReferenceDatasetId,
iou_threshold: 0.5,
area_id: latestSelectionDataset.area_id ?? null,
}
const job: JobRead = await qaApi.runQa(request)
if (job.status === 'failed') {
setMapSelectionQaError(job.error_message || 'Map selection QA/QC failed')
return
}
const payload = job.result_json
if (!payload || typeof payload !== 'object') {
setMapSelectionQaError('Map selection QA/QC result was not available')
return
}
const parsed = payload as unknown as QaComparisonResult
if (!parsed || typeof parsed.status !== 'string') {
setMapSelectionQaError('Map selection QA/QC result format was unexpected')
return
}
setMapQaResult(parsed)
await loadQualityChecks(selectedProjectId)
await loadProjectData(selectedProjectId)
} catch (error) {
setMapSelectionQaError(formatError(error, 'Map selection QA/QC failed'))
} finally {
setMapSelectionQaRunning(false)
}
}
return {
selectedMapQaReferenceDatasetId,
mapSelectionQaRunning,
mapSelectionQaError,
mapSelectionQaResult,
runMapSelectionQa,
setSelectedMapQaReferenceDatasetId,
}
}
+31
View File
@@ -2669,6 +2669,37 @@ button.entity-card {
padding-top: 0.68rem;
}
.map-selection-qa-surface {
display: grid;
grid-template-columns: minmax(11rem, 1fr) auto;
gap: 0.58rem;
align-items: end;
min-width: 0;
border-top: 1px solid var(--line);
padding-top: 0.68rem;
}
.map-selection-qa-surface label {
min-width: 0;
}
.map-selection-qa-surface .primary-action {
width: fit-content;
max-width: 100%;
margin-top: 0;
}
.map-selection-qa-surface .error,
.map-selection-qa-surface .feature-extract-grid {
grid-column: 1 / -1;
}
@media (max-width: 720px) {
.map-selection-qa-surface {
grid-template-columns: 1fr;
}
}
.feature-extract-surface {
display: grid;
gap: 0.7rem;