Add calibration summary export
GeoIntel CI / docs-smoke (push) Has been cancelled
GeoIntel CI / contract-smoke (push) Has been cancelled

This commit is contained in:
Codex
2026-07-08 13:31:11 +02:00
parent 682a113d64
commit 8089df3504
6 changed files with 120 additions and 1 deletions
+7
View File
@@ -7,6 +7,13 @@
# Changelog
## Sprint 136 Guided calibration summary export (2026-07-08)
- Added a Detection Lab `Download calibration summary` action for guided calibration rows.
- The exported JSON includes thresholds, persisted analysis run IDs, job IDs, quality check IDs, metrics and evidence GeoJSON URLs for successful rows.
- Reused browser-side JSON download behavior only; no backend endpoint, API contract, migration, model, provider or inference behavior changed.
- Added regression coverage for the summary export wiring.
## Sprint 135 Calibration evidence handoff (2026-07-08)
- Added a guided-calibration table action that opens the persisted QA/QC evidence map for successful threshold rows.
@@ -0,0 +1,23 @@
from pathlib import Path
ROOT = Path(__file__).resolve().parents[2]
def test_guided_calibration_runner_exports_review_summary() -> None:
lab = ROOT / "frontend" / "src" / "components" / "detection" / "DetectionLab.tsx"
todo = ROOT / "docs" / "TODO.md"
lab_source = lab.read_text(encoding="utf-8")
todo_source = todo.read_text(encoding="utf-8")
assert "downloadCalibrationSummary" in lab_source
assert "buildCalibrationSummaryExport" in lab_source
assert "downloadJsonFile('detection-calibration-summary.json'" in lab_source
assert "evidence_geojson_url" in lab_source
assert "/api/v1/projects/${projectId}/quality-checks/${row.quality_check_id}/evidence/geojson" in lab_source
assert "Download calibration summary" in lab_source
assert "disabled={detectionCalibrationRows.length === 0 || !selectedProjectId}" in lab_source
assert "calibration_thresholds" in lab_source
assert "quality_check_ids" in lab_source
assert "[x] Add guided calibration summary export from the Detection Lab" in todo_source
+25
View File
@@ -1,3 +1,28 @@
## Sprint 136 Guided calibration summary export (2026-07-08)
Changed:
- Added a `Download calibration summary` action to the guided Detection Lab calibration progress table.
- The client-side JSON export includes calibration thresholds, persisted `analysis_run_id`, `job_id`, `quality_check_id`, metric values and QA evidence GeoJSON URLs.
- Added compact action-row styling and regression coverage in `backend/tests/test_sprint136_calibration_summary_export_ui.py`.
- Updated `CHANGELOG.md` and `docs/TODO.md`.
Tested:
- Red step: `python -m pytest backend\tests\test_sprint136_calibration_summary_export_ui.py -q` failed while the summary export helpers and button were absent.
- `python -m pytest backend\tests\test_sprint136_calibration_summary_export_ui.py backend\tests\test_sprint135_calibration_evidence_handoff.py backend\tests\test_sprint134_guided_detection_calibration_runner.py -q` (`3 passed`)
- `python -m compileall backend/app`
- `cd frontend && npm run typecheck`
- `cd frontend && npm run build`
- `bash scripts/run_readiness_check.sh` (`415 passed`; frontend typecheck/build passed; Alembic head `202606120900`; live smoke syntax passed)
Open:
- Live Tower deploy validation still needs to run for this pass.
Limitations:
- This is a browser-side summary export only. It does not create server-side export records, rerun inference, create new QA metrics, promote thresholds, mutate model configuration, download models, add provider fetching or change API/database contracts.
Next recommended pass:
- Add a small import/consume path for downloaded calibration summaries in the existing operator evidence bundle script, so browser-exported summary JSON can be used directly from an operator workstation.
## Sprint 135 Calibration evidence handoff (2026-07-08)
Changed:
+1
View File
@@ -422,5 +422,6 @@ This file now starts with the current implementation status. Older preparation/b
- [x] Add full threshold calibration comparison UX so detection runs can compare candidate thresholds before promotion.
- [x] Add guided in-app detection calibration runner for explicit threshold sweeps.
- [x] Link guided calibration rows to the QA evidence map.
- [x] Add guided calibration summary export from the Detection Lab.
- [ ] Add more AOIs after the tile-level baseline so the next local model attempt is not limited to Geel/Mol/Turnhout.
- [ ] Add negative/background AOIs so the next tile dataset is not all positive tiles.
@@ -571,7 +571,17 @@ export function DetectionLab({
<h3>Calibration run progress</h3>
<p className="muted">Each row is backed by a persisted detection run and QA check when successful.</p>
</div>
<div className="panel-action-row">
<span className="count-pill">{detectionCalibrationRows.length} thresholds</span>
<button
className="secondary-action"
type="button"
onClick={() => downloadCalibrationSummary(selectedProjectId, detectionCalibrationRows)}
disabled={detectionCalibrationRows.length === 0 || !selectedProjectId}
>
Download calibration summary
</button>
</div>
</div>
<div className="table-scroll">
<table>
@@ -940,6 +950,51 @@ function stringFromRecord(record: Record<string, unknown> | null | undefined, ke
return typeof value === 'string' && value.trim().length > 0 ? value : null
}
function buildCalibrationSummaryExport(projectId: string, rows: DetectionCalibrationRunRow[]): Record<string, unknown> {
const successfulRows = rows.filter((row) => row.status === 'success')
return {
export_type: 'detection_calibration_summary',
project_id: projectId,
created_at: new Date().toISOString(),
calibration_thresholds: rows.map((row) => row.threshold),
quality_check_ids: successfulRows.map((row) => row.quality_check_id).filter(Boolean),
rows: rows.map((row) => ({
threshold: row.threshold,
status: row.status,
analysis_run_id: row.analysis_run_id ?? null,
job_id: row.job_id ?? null,
quality_check_id: row.quality_check_id ?? null,
evidence_geojson_url: row.quality_check_id
? `/api/v1/projects/${projectId}/quality-checks/${row.quality_check_id}/evidence/geojson`
: null,
detection_count: row.detection_count ?? null,
precision: row.precision ?? null,
recall: row.recall ?? null,
f1_score: row.f1_score ?? null,
false_positives: row.false_positives ?? null,
false_negatives: row.false_negatives ?? null,
message: row.message ?? null,
})),
}
}
function downloadCalibrationSummary(projectId: string | null, rows: DetectionCalibrationRunRow[]): void {
if (!projectId || rows.length === 0) {
return
}
downloadJsonFile('detection-calibration-summary.json', buildCalibrationSummaryExport(projectId, rows))
}
function downloadJsonFile(filename: string, payload: unknown): void {
const blob = new Blob([JSON.stringify(payload, null, 2)], { type: 'application/json' })
const url = URL.createObjectURL(blob)
const link = document.createElement('a')
link.href = url
link.download = filename
link.click()
URL.revokeObjectURL(url)
}
function formatNullableNumber(value: number | null, digits: number): string {
return typeof value === 'number' && Number.isFinite(value) ? value.toFixed(digits) : 'n/a'
}
+8
View File
@@ -3315,6 +3315,14 @@ button.entity-card {
white-space: nowrap;
}
.panel-action-row {
display: flex;
flex-wrap: wrap;
align-items: center;
justify-content: flex-end;
gap: 0.5rem;
}
.calibration-summary-grid {
display: grid;
grid-template-columns: repeat(3, minmax(0, 1fr));