diff --git a/CHANGELOG.md b/CHANGELOG.md index edd70b84..8c5e2b21 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,15 @@ # Changelog +## Sprint 94 QA/QC evidence drilldown (2026-06-22) + +- Added a selected QA/QC evidence drilldown to the Quality Results panel. +- Surfaced candidate/reference layer names, analysis run/job provenance, status, score and completed/created timestamps for the selected persisted quality check. +- Added false-positive, false-negative and map-evidence handoff cards from persisted metric rows. +- Added parameter/findings JSON panes for persisted QA/QC provenance. +- Added regression coverage for drilldown structure, metric evidence and responsive styles. +- No API contracts, migrations, provider fetching or AI/model behavior changed. + ## Sprint 93 export handoff artifact polish (2026-06-21) - Added a latest handoff artifacts section to the Export Center for project reports, project metadata, dataset GeoJSON, detection GeoJSON and segmentation GeoJSON. diff --git a/backend/tests/test_sprint94_quality_drilldown.py b/backend/tests/test_sprint94_quality_drilldown.py new file mode 100644 index 00000000..15b04bf1 --- /dev/null +++ b/backend/tests/test_sprint94_quality_drilldown.py @@ -0,0 +1,48 @@ +from pathlib import Path + + +ROOT = Path(__file__).resolve().parents[2] + + +def test_quality_results_panel_exposes_selected_check_drilldown() -> None: + panel = (ROOT / "frontend" / "src" / "components" / "quality" / "QualityResultsPanel.tsx").read_text( + encoding="utf-8" + ) + + assert "selectedQualityCheckId" in panel + assert "selectedQualityCheck" in panel + assert "QA/QC evidence drilldown" in panel + assert "Selected check" in panel + assert "Candidate layer" in panel + assert "Reference layer" in panel + assert "Analysis run" in panel + assert "Job" in panel + assert "Completed" in panel + assert "Inspect latest check" in panel + assert "Inspect check" in panel + + +def test_quality_results_panel_surfaces_false_positive_negative_evidence() -> None: + panel = (ROOT / "frontend" / "src" / "components" / "quality" / "QualityResultsPanel.tsx").read_text( + encoding="utf-8" + ) + + assert "False positive evidence" in panel + assert "False negative evidence" in panel + assert "Map evidence handoff" in panel + assert "metricByKey(selectedQualityCheck, 'false_positive_count')" in panel + assert "metricByKey(selectedQualityCheck, 'false_negative_count')" in panel + assert "selectedQualityCheck.parameters_json" in panel + assert "selectedQualityCheck.findings_json" in panel + assert "JSON.stringify" in panel + + +def test_quality_drilldown_styles_are_responsive() -> None: + css = (ROOT / "frontend" / "src" / "styles" / "app.css").read_text(encoding="utf-8") + + assert ".quality-drilldown-surface" in css + assert ".quality-drilldown-grid" in css + assert ".quality-evidence-token-grid" in css + assert ".quality-evidence-map-handoff" in css + assert ".quality-provenance-pre" in css + assert "grid-template-columns: repeat(auto-fit, minmax(12rem, 1fr));" in css diff --git a/docs/CODEX_EXECUTION_LOG.md b/docs/CODEX_EXECUTION_LOG.md index 6a64bb2e..2312e19c 100644 --- a/docs/CODEX_EXECUTION_LOG.md +++ b/docs/CODEX_EXECUTION_LOG.md @@ -3373,3 +3373,32 @@ Limitations: Next recommended pass: - After deploy and live validation, continue with QA/QC drilldown and map evidence layers. + +## Sprint 94 QA/QC evidence drilldown (2026-06-22) + +Changed: +- Added selected-check drilldown state to `frontend/src/components/quality/QualityResultsPanel.tsx`. +- Added a `QA/QC evidence drilldown` surface with selected check, candidate/reference layer, analysis run, job, status, score and timestamp provenance. +- Added false-positive evidence, false-negative evidence and map evidence handoff cards using persisted metric rows and dataset names. +- Added parameter and findings JSON panes for persisted QA/QC provenance. +- Added `Inspect latest check` and per-result `Inspect check` controls. +- Added responsive QA drilldown styles to `frontend/src/styles/app.css`. +- Added `backend/tests/test_sprint94_quality_drilldown.py`. +- Updated `CHANGELOG.md`, `docs/TODO.md` and `frontend/README.md`. + +Tested: +- Red step: `python -m pytest backend\tests\test_sprint94_quality_drilldown.py -q` failed on missing drilldown state/surfaces/styles. +- `python -m pytest backend\tests\test_sprint94_quality_drilldown.py backend\tests\test_sprint70_quality_handoff_polish.py backend\tests\test_sprint71_quality_metric_polish.py backend\tests\test_sprint73_quality_result_filtering.py backend\tests\test_sprint86_quality_workspace_density.py -q` (`15 passed`) +- `cd frontend && npm run typecheck` +- `cd frontend && npm run build` +- `bash scripts/run_readiness_check.sh` (`314 passed`; frontend typecheck/build passed; Alembic head `202606120900`; live smoke syntax passed) + +Open: +- Tower deploy and live browser validation still pending for this pass. + +Limitations: +- QA/QC frontend drilldown only; no API contract, persistence, migration, provider fetching or AI/model behavior changes. +- Map evidence handoff points reviewers to candidate/reference datasets already persisted in the workbench; it does not create a new matched/unmatched geometry export. + +Next recommended pass: +- After deploy and live validation, continue with QA map overlay affordances or raster pipeline hardening. diff --git a/docs/TODO.md b/docs/TODO.md index 69f5926f..b0ca1191 100644 --- a/docs/TODO.md +++ b/docs/TODO.md @@ -365,3 +365,4 @@ This file now starts with the current implementation status. Older preparation/b - [x] Audit populated demo workflow and tighten complete-state Overview guidance copy. - [x] Make Overview workflow rail Map/Export clicks preserve useful dataset context. - [x] Add latest handoff artifact cards to the Export Center for report, metadata and GeoJSON outputs. +- [x] Add QA/QC evidence drilldown for selected checks, false-positive/negative evidence and provenance JSON. diff --git a/frontend/README.md b/frontend/README.md index ed810be8..5b27b856 100644 --- a/frontend/README.md +++ b/frontend/README.md @@ -271,6 +271,7 @@ Detection Lab and Segmentation Lab now share the same AI workspace hierarchy: mo - 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`. - The QA/QC workspace shows candidate/reference handoff cards and resolves persisted quality-check dataset IDs back to dataset names when the datasets are loaded in the current project context. +- The QA/QC workspace includes a selected-check evidence drilldown with candidate/reference provenance, false-positive/negative metric evidence, map handoff context and parameters/findings JSON. ## Raster dependency visibility diff --git a/frontend/src/components/quality/QualityResultsPanel.tsx b/frontend/src/components/quality/QualityResultsPanel.tsx index d327fb4f..daef0595 100644 --- a/frontend/src/components/quality/QualityResultsPanel.tsx +++ b/frontend/src/components/quality/QualityResultsPanel.tsx @@ -42,6 +42,14 @@ function qualityMetricValue(metric: MetricRead | undefined): string { return Number.isInteger(value) ? String(value) : value.toFixed(3) } +function metricByKey(check: QualityCheckRead | null, metricKey: string): MetricRead | undefined { + return check?.metrics.find((metric) => metric.metric_key === metricKey) +} + +function formatQualityTimestamp(value?: string | null): string { + return value || 'n/a' +} + function qualityMatchesSearch(check: QualityCheckRead, query: string, datasetNameById: Map): boolean { const normalizedQuery = query.trim().toLowerCase() if (!normalizedQuery) { @@ -78,6 +86,7 @@ export function QualityResultsPanel({ const [qualityStatusFilter, setQualityStatusFilter] = useState('all') const [qualityTypeFilter, setQualityTypeFilter] = useState('all') const [qualitySearchQuery, setQualitySearchQuery] = useState('') + const [selectedQualityCheckId, setSelectedQualityCheckId] = useState(null) const completedChecks = qualityChecks.filter((check) => check.status === 'ok' || check.status === 'completed').length const latestCheck = qualityChecks[0] ?? null const datasetNameById = useMemo(() => { @@ -89,6 +98,16 @@ export function QualityResultsPanel({ }, [candidateDatasets, referenceDatasets]) const latestCandidateName = latestCheck?.candidate_dataset_id ? datasetNameById.get(latestCheck.candidate_dataset_id) : null const latestReferenceName = latestCheck?.reference_dataset_id ? datasetNameById.get(latestCheck.reference_dataset_id) : null + const selectedQualityCheck = useMemo( + () => qualityChecks.find((check) => check.id === selectedQualityCheckId) ?? latestCheck, + [latestCheck, qualityChecks, selectedQualityCheckId], + ) + const selectedCandidateName = selectedQualityCheck?.candidate_dataset_id + ? datasetNameById.get(selectedQualityCheck.candidate_dataset_id) ?? selectedQualityCheck.candidate_dataset_id + : 'n/a' + const selectedReferenceName = selectedQualityCheck + ? datasetNameById.get(selectedQualityCheck.reference_dataset_id) ?? selectedQualityCheck.reference_dataset_id + : 'n/a' const qualityStatuses = useMemo(() => Array.from(new Set(qualityChecks.map((check) => check.status))).sort(), [qualityChecks]) const qualityTypes = useMemo(() => Array.from(new Set(qualityChecks.map((check) => check.check_type))).sort(), [qualityChecks]) const filteredQualityChecks = useMemo( @@ -154,6 +173,95 @@ export function QualityResultsPanel({ +
+
+
+

QA/QC evidence drilldown

+

Inspect the persisted check, metric evidence and dataset handoff context before exporting or sharing results.

+
+ +
+ {selectedQualityCheck ? ( + <> +
+
+ Selected check + {selectedQualityCheck.check_type} +

{selectedQualityCheck.id}

+
+
+ Candidate layer + {selectedCandidateName} +

{selectedQualityCheck.candidate_dataset_id ?? 'candidate not stored'}

+
+
+ Reference layer + {selectedReferenceName} +

{selectedQualityCheck.reference_dataset_id}

+
+
+ Analysis run + {selectedQualityCheck.analysis_run_id ?? 'n/a'} +

Job: {selectedQualityCheck.job_id ?? 'n/a'}

+
+
+ Status + {selectedQualityCheck.status} +

Score: {selectedQualityCheck.score ?? 'n/a'}

+
+
+ Completed + {formatQualityTimestamp(selectedQualityCheck.completed_at)} +

Created: {formatQualityTimestamp(selectedQualityCheck.created_at)}

+
+
+
+
+ False positive evidence + {qualityMetricValue(metricByKey(selectedQualityCheck, 'false_positive_count'))} +

Candidate geometries not matched to the reference layer.

+
+
+ False negative evidence + {qualityMetricValue(metricByKey(selectedQualityCheck, 'false_negative_count'))} +

Reference geometries not matched by the candidate layer.

+
+
+ Map evidence handoff + {selectedCandidateName} / {selectedReferenceName} +

Use the candidate and reference datasets as map layers for spatial review.

+
+
+
+
+ Parameters +
+                    {JSON.stringify(selectedQualityCheck.parameters_json ?? {}, null, 2)}
+                  
+
+
+ Findings +
+                    {JSON.stringify(selectedQualityCheck.findings_json ?? {}, null, 2)}
+                  
+
+
+ + ) : ( +
+ No QA/QC check selected. +

Run or load a persisted QA/QC check to inspect metric and map evidence.

+
+ )} +
+
+
+ +
Score diff --git a/frontend/src/styles/app.css b/frontend/src/styles/app.css index b8312d38..662ef0dc 100644 --- a/frontend/src/styles/app.css +++ b/frontend/src/styles/app.css @@ -1483,6 +1483,7 @@ button.entity-card { .quality-summary-surface, .quality-evidence-surface, +.quality-drilldown-surface, .quality-control-surface, .quality-history-surface { min-width: 0; @@ -1500,6 +1501,21 @@ button.entity-card { background: #f8fbf9; } +.quality-drilldown-surface { + display: grid; + gap: 0.72rem; + background: linear-gradient(180deg, #ffffff, #f7fbf8); +} + +.quality-drilldown-surface .panel-title-row { + align-items: start; +} + +.quality-drilldown-surface .panel-title-row button { + width: auto; + min-height: 2.35rem; +} + .quality-control-surface, .quality-history-surface { display: grid; @@ -1600,6 +1616,14 @@ button.entity-card { margin: 0; } +.quality-drilldown-grid, +.quality-evidence-token-grid, +.quality-provenance-grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(12rem, 1fr)); + gap: 0.58rem; +} + .quality-summary-grid > div { border: 1px solid var(--line); border-radius: 8px; @@ -1615,8 +1639,31 @@ button.entity-card { background: #ffffff; } +.quality-drilldown-grid > div, +.quality-evidence-token-grid > div, +.quality-provenance-grid > div { + min-width: 0; + border: 1px solid var(--line); + border-radius: 8px; + padding: 0.68rem; + background: #ffffff; +} + +.quality-evidence-token-grid > div { + border-color: rgba(15, 118, 110, 0.22); + background: linear-gradient(180deg, #f8fcfa, #ffffff); +} + +.quality-evidence-map-handoff, +.quality-evidence-token-grid > div:nth-child(3) { + background: var(--accent-soft); +} + .quality-summary-grid span, .quality-handoff-grid span, +.quality-drilldown-grid span, +.quality-evidence-token-grid span, +.quality-provenance-grid span, .quality-score-row span, .latest-export-card span, .metric-list span { @@ -1643,6 +1690,52 @@ button.entity-card { line-height: 1.2; } +.quality-drilldown-grid strong, +.quality-evidence-token-grid strong { + display: block; + margin-top: 0.22rem; + overflow: hidden; + color: var(--ink); + font-size: 0.9rem; + line-height: 1.25; + text-overflow: ellipsis; + white-space: nowrap; +} + +.quality-drilldown-grid p, +.quality-evidence-token-grid p { + margin: 0.24rem 0 0; + color: var(--muted); + font-size: 0.8rem; + line-height: 1.35; + overflow-wrap: anywhere; +} + +.quality-provenance-pre { + max-height: 11rem; + margin: 0.38rem 0 0; + overflow: auto; + border: 1px solid var(--line); + border-radius: 7px; + padding: 0.58rem; + background: #0f172a; + color: #e2e8f0; + font-size: 0.75rem; + line-height: 1.35; + white-space: pre-wrap; + overflow-wrap: anywhere; +} + +.quality-check-actions { + display: flex; + justify-content: flex-end; +} + +.quality-check-actions button { + width: auto; + min-height: 2.15rem; +} + .quality-dataset-name, .quality-check-dataset-link { overflow-wrap: anywhere;