From 8089df35049cf900367ec752b511be5daca59b87 Mon Sep 17 00:00:00 2001 From: Codex Date: Wed, 8 Jul 2026 13:31:11 +0200 Subject: [PATCH] Add calibration summary export --- CHANGELOG.md | 7 +++ ...sprint136_calibration_summary_export_ui.py | 23 ++++++++ docs/CODEX_EXECUTION_LOG.md | 25 ++++++++ docs/TODO.md | 1 + .../src/components/detection/DetectionLab.tsx | 57 ++++++++++++++++++- frontend/src/styles/app.css | 8 +++ 6 files changed, 120 insertions(+), 1 deletion(-) create mode 100644 backend/tests/test_sprint136_calibration_summary_export_ui.py diff --git a/CHANGELOG.md b/CHANGELOG.md index 02c0255d..7341aa1c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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. diff --git a/backend/tests/test_sprint136_calibration_summary_export_ui.py b/backend/tests/test_sprint136_calibration_summary_export_ui.py new file mode 100644 index 00000000..bfaae507 --- /dev/null +++ b/backend/tests/test_sprint136_calibration_summary_export_ui.py @@ -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 diff --git a/docs/CODEX_EXECUTION_LOG.md b/docs/CODEX_EXECUTION_LOG.md index 12c64987..18ba9696 100644 --- a/docs/CODEX_EXECUTION_LOG.md +++ b/docs/CODEX_EXECUTION_LOG.md @@ -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: diff --git a/docs/TODO.md b/docs/TODO.md index 76015612..dc001def 100644 --- a/docs/TODO.md +++ b/docs/TODO.md @@ -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. diff --git a/frontend/src/components/detection/DetectionLab.tsx b/frontend/src/components/detection/DetectionLab.tsx index 3608f437..57553e11 100644 --- a/frontend/src/components/detection/DetectionLab.tsx +++ b/frontend/src/components/detection/DetectionLab.tsx @@ -571,7 +571,17 @@ export function DetectionLab({

Calibration run progress

Each row is backed by a persisted detection run and QA check when successful.

- {detectionCalibrationRows.length} thresholds +
+ {detectionCalibrationRows.length} thresholds + +
@@ -940,6 +950,51 @@ function stringFromRecord(record: Record | null | undefined, ke return typeof value === 'string' && value.trim().length > 0 ? value : null } +function buildCalibrationSummaryExport(projectId: string, rows: DetectionCalibrationRunRow[]): Record { + 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' } diff --git a/frontend/src/styles/app.css b/frontend/src/styles/app.css index d439440e..e6c566a5 100644 --- a/frontend/src/styles/app.css +++ b/frontend/src/styles/app.css @@ -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));