Add map QA evidence drilldown
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:42:58 +02:00
parent d5ac9a1533
commit de9c11327c
10 changed files with 192 additions and 16 deletions
+7
View File
@@ -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.
@@ -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
+23
View File
@@ -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:
+1
View File
@@ -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.
+1
View File
@@ -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.
+6
View File
@@ -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}
+61 -15
View File
@@ -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({
</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 className="map-selection-qa-evidence" aria-label="Map selection QA result">
<div className="panel-title-row">
<div>
<p className="eyebrow">QA evidence</p>
<h4>Saved selection comparison</h4>
</div>
<button
className="secondary-action"
disabled={!latestMapSelectionQualityCheckId}
type="button"
onClick={onOpenMapSelectionQualityEvidence}
>
Open QA/QC evidence
</button>
</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 className="feature-extract-grid">
<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>Mean IoU</span>
<strong>{mapSelectionQaResult.mean_iou ?? 'n/a'}</strong>
</div>
<div>
<span>Matches</span>
<strong>{mapSelectionQaResult.matches}</strong>
</div>
<div>
<span>False positives</span>
<strong>{mapSelectionQaResult.false_positives}</strong>
</div>
<div>
<span>False negatives</span>
<strong>{mapSelectionQaResult.false_negatives}</strong>
</div>
<div>
<span>Quality check id</span>
<strong>{latestMapSelectionQualityCheckId ?? 'not persisted'}</strong>
</div>
</div>
{mapSelectionQaResult.warnings.length > 0 ? (
<div className="map-selection-qa-warnings" aria-label="Map selection QA warnings">
<span>Map selection QA warnings</span>
<ul>
{mapSelectionQaResult.warnings.map((warning) => (
<li key={warning}>{warning}</li>
))}
</ul>
</div>
) : null}
</div>
) : null}
</div>
+4
View File
@@ -20,6 +20,7 @@ export function useMapSelectionQa({
const [mapSelectionQaRunning, setMapSelectionQaRunning] = useState(false)
const [mapSelectionQaError, setMapSelectionQaError] = useState<string | null>(null)
const [mapSelectionQaResult, setMapQaResult] = useState<QaComparisonResult | null>(null)
const [latestMapSelectionQualityCheckId, setLatestMapSelectionQualityCheckId] = useState<string | null>(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,
}
+47 -1
View File
@@ -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;
+1
View File
@@ -601,6 +601,7 @@ export interface QaComparisonResult {
unsupported_geometry: boolean
unsupported_geometries: string[]
generated_at: string
quality_check_id?: string
}
export interface MetricRead {