Add 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-22 02:45:42 +02:00
parent 978c55c979
commit f3ad9c3f4f
7 changed files with 294 additions and 0 deletions
+9
View File
@@ -7,6 +7,15 @@
# Changelog # 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) ## 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. - Added a latest handoff artifacts section to the Export Center for project reports, project metadata, dataset GeoJSON, detection GeoJSON and segmentation GeoJSON.
@@ -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
+29
View File
@@ -3373,3 +3373,32 @@ Limitations:
Next recommended pass: Next recommended pass:
- After deploy and live validation, continue with QA/QC drilldown and map evidence layers. - 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.
+1
View File
@@ -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] Audit populated demo workflow and tighten complete-state Overview guidance copy.
- [x] Make Overview workflow rail Map/Export clicks preserve useful dataset context. - [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 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.
+1
View File
@@ -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. - 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`. - 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 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 ## Raster dependency visibility
@@ -42,6 +42,14 @@ function qualityMetricValue(metric: MetricRead | undefined): string {
return Number.isInteger(value) ? String(value) : value.toFixed(3) 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<string, string>): boolean { function qualityMatchesSearch(check: QualityCheckRead, query: string, datasetNameById: Map<string, string>): boolean {
const normalizedQuery = query.trim().toLowerCase() const normalizedQuery = query.trim().toLowerCase()
if (!normalizedQuery) { if (!normalizedQuery) {
@@ -78,6 +86,7 @@ export function QualityResultsPanel({
const [qualityStatusFilter, setQualityStatusFilter] = useState('all') const [qualityStatusFilter, setQualityStatusFilter] = useState('all')
const [qualityTypeFilter, setQualityTypeFilter] = useState('all') const [qualityTypeFilter, setQualityTypeFilter] = useState('all')
const [qualitySearchQuery, setQualitySearchQuery] = useState('') const [qualitySearchQuery, setQualitySearchQuery] = useState('')
const [selectedQualityCheckId, setSelectedQualityCheckId] = useState<string | null>(null)
const completedChecks = qualityChecks.filter((check) => check.status === 'ok' || check.status === 'completed').length const completedChecks = qualityChecks.filter((check) => check.status === 'ok' || check.status === 'completed').length
const latestCheck = qualityChecks[0] ?? null const latestCheck = qualityChecks[0] ?? null
const datasetNameById = useMemo(() => { const datasetNameById = useMemo(() => {
@@ -89,6 +98,16 @@ export function QualityResultsPanel({
}, [candidateDatasets, referenceDatasets]) }, [candidateDatasets, referenceDatasets])
const latestCandidateName = latestCheck?.candidate_dataset_id ? datasetNameById.get(latestCheck.candidate_dataset_id) : null 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 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 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 qualityTypes = useMemo(() => Array.from(new Set(qualityChecks.map((check) => check.check_type))).sort(), [qualityChecks])
const filteredQualityChecks = useMemo( const filteredQualityChecks = useMemo(
@@ -154,6 +173,95 @@ export function QualityResultsPanel({
</div> </div>
</div> </div>
<div className="quality-drilldown-surface" aria-label="QA/QC evidence drilldown">
<div className="panel-title-row">
<div>
<h3>QA/QC evidence drilldown</h3>
<p className="muted">Inspect the persisted check, metric evidence and dataset handoff context before exporting or sharing results.</p>
</div>
<button
type="button"
className="secondary-action"
onClick={() => setSelectedQualityCheckId(latestCheck?.id ?? null)}
disabled={!latestCheck}
>
Inspect latest check
</button>
</div>
{selectedQualityCheck ? (
<>
<div className="quality-drilldown-grid">
<div>
<span>Selected check</span>
<strong>{selectedQualityCheck.check_type}</strong>
<p>{selectedQualityCheck.id}</p>
</div>
<div>
<span>Candidate layer</span>
<strong>{selectedCandidateName}</strong>
<p>{selectedQualityCheck.candidate_dataset_id ?? 'candidate not stored'}</p>
</div>
<div>
<span>Reference layer</span>
<strong>{selectedReferenceName}</strong>
<p>{selectedQualityCheck.reference_dataset_id}</p>
</div>
<div>
<span>Analysis run</span>
<strong>{selectedQualityCheck.analysis_run_id ?? 'n/a'}</strong>
<p>Job: {selectedQualityCheck.job_id ?? 'n/a'}</p>
</div>
<div>
<span>Status</span>
<strong>{selectedQualityCheck.status}</strong>
<p>Score: {selectedQualityCheck.score ?? 'n/a'}</p>
</div>
<div>
<span>Completed</span>
<strong>{formatQualityTimestamp(selectedQualityCheck.completed_at)}</strong>
<p>Created: {formatQualityTimestamp(selectedQualityCheck.created_at)}</p>
</div>
</div>
<div className="quality-evidence-token-grid">
<div>
<span>False positive evidence</span>
<strong>{qualityMetricValue(metricByKey(selectedQualityCheck, 'false_positive_count'))}</strong>
<p>Candidate geometries not matched to the reference layer.</p>
</div>
<div>
<span>False negative evidence</span>
<strong>{qualityMetricValue(metricByKey(selectedQualityCheck, 'false_negative_count'))}</strong>
<p>Reference geometries not matched by the candidate layer.</p>
</div>
<div className="quality-evidence-map-handoff">
<span>Map evidence handoff</span>
<strong>{selectedCandidateName} / {selectedReferenceName}</strong>
<p>Use the candidate and reference datasets as map layers for spatial review.</p>
</div>
</div>
<div className="quality-provenance-grid">
<div>
<span>Parameters</span>
<pre className="quality-provenance-pre">
{JSON.stringify(selectedQualityCheck.parameters_json ?? {}, null, 2)}
</pre>
</div>
<div>
<span>Findings</span>
<pre className="quality-provenance-pre">
{JSON.stringify(selectedQualityCheck.findings_json ?? {}, null, 2)}
</pre>
</div>
</div>
</>
) : (
<div className="result-state result-state-empty">
<strong>No QA/QC check selected.</strong>
<p>Run or load a persisted QA/QC check to inspect metric and map evidence.</p>
</div>
)}
</div>
<div className="quality-control-surface" aria-label="QA/QC refresh and filters"> <div className="quality-control-surface" aria-label="QA/QC refresh and filters">
<div className="button-row"> <div className="button-row">
<button <button
@@ -273,6 +381,11 @@ export function QualityResultsPanel({
{check.status} {check.status}
</span> </span>
</div> </div>
<div className="quality-check-actions">
<button type="button" className="secondary-action" onClick={() => setSelectedQualityCheckId(check.id)}>
Inspect check
</button>
</div>
<div className="quality-score-row"> <div className="quality-score-row">
<div> <div>
<span>Score</span> <span>Score</span>
+93
View File
@@ -1483,6 +1483,7 @@ button.entity-card {
.quality-summary-surface, .quality-summary-surface,
.quality-evidence-surface, .quality-evidence-surface,
.quality-drilldown-surface,
.quality-control-surface, .quality-control-surface,
.quality-history-surface { .quality-history-surface {
min-width: 0; min-width: 0;
@@ -1500,6 +1501,21 @@ button.entity-card {
background: #f8fbf9; 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-control-surface,
.quality-history-surface { .quality-history-surface {
display: grid; display: grid;
@@ -1600,6 +1616,14 @@ button.entity-card {
margin: 0; 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 { .quality-summary-grid > div {
border: 1px solid var(--line); border: 1px solid var(--line);
border-radius: 8px; border-radius: 8px;
@@ -1615,8 +1639,31 @@ button.entity-card {
background: #ffffff; 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-summary-grid span,
.quality-handoff-grid span, .quality-handoff-grid span,
.quality-drilldown-grid span,
.quality-evidence-token-grid span,
.quality-provenance-grid span,
.quality-score-row span, .quality-score-row span,
.latest-export-card span, .latest-export-card span,
.metric-list span { .metric-list span {
@@ -1643,6 +1690,52 @@ button.entity-card {
line-height: 1.2; 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-dataset-name,
.quality-check-dataset-link { .quality-check-dataset-link {
overflow-wrap: anywhere; overflow-wrap: anywhere;