From d7eadbc6ca8e289a22a016740484f09f50ea3109 Mon Sep 17 00:00:00 2001 From: Codex Date: Tue, 14 Jul 2026 11:41:16 +0200 Subject: [PATCH] fix: harden release checks for grouped routes --- CHANGELOG.md | 3 ++ backend/pyproject.toml | 1 + backend/tests/test_readiness_gate.py | 7 +++ .../tests/test_sprint48_api_contract_audit.py | 3 ++ ...est_sprint8c_detection_visualization_qa.py | 49 +++++++++++++++++++ docs/CODEX_EXECUTION_LOG.md | 46 ++++++++++++++++- .../src/components/detection/DetectionLab.tsx | 2 +- scripts/audit_api_contracts.py | 14 +++--- 8 files changed, 116 insertions(+), 9 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a842e33b..ccb12c3e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,9 @@ - Kept precision, recall, F1 and mean IoU strictly based on candidate polygons versus persisted reference footprints; added a separately labelled reference-envelope comparison as diagnostic evidence only. - Persisted raw/evaluated/excluded/clipped population counts and diagnostic matching evidence in the existing `quality_checks.findings_json` structure without changing migrations or canonical metric rows. - Surfaced inference coverage and box-to-footprint diagnostics in Detection Lab and hardened the real-data workflow assertions, documentation and regression coverage. +- Live Tower/PostGIS validation on the persisted Mol-center run evaluated 304 of 374 GRB references, excluded 70 outside the inference tile, persisted six canonical Metric rows and exposed 14 possible box-to-footprint artifacts without inflating the strict scores. +- Updated the API contract audit to use the canonical OpenAPI path map after FastAPI 0.139 introduced grouped top-level routers; this keeps all 81 operations audited across clean framework installations. +- Kept Starlette on its supported pre-1.0 compatibility line until GeoIntel deliberately migrates its test client from `httpx` to `httpx2`; this removes the new framework deprecation warning without taking an unrelated test-stack upgrade. ## Sprint 183 Mol map source clarity and live AI validation (2026-07-14) diff --git a/backend/pyproject.toml b/backend/pyproject.toml index 809f3498..5a9a4f63 100644 --- a/backend/pyproject.toml +++ b/backend/pyproject.toml @@ -6,6 +6,7 @@ readme = "README.md" requires-python = ">=3.11" dependencies = [ "fastapi>=0.112.0", + "starlette>=0.46.0,<1.0.0", "uvicorn[standard]>=0.30.6", "SQLAlchemy>=2.0.34", "psycopg[binary]>=3.2.1", diff --git a/backend/tests/test_readiness_gate.py b/backend/tests/test_readiness_gate.py index 397c35ac..223ada25 100644 --- a/backend/tests/test_readiness_gate.py +++ b/backend/tests/test_readiness_gate.py @@ -1,6 +1,13 @@ from pathlib import Path +def test_backend_keeps_starlette_on_the_supported_pre_httpx2_line() -> None: + pyproject = Path(__file__).resolve().parents[1] / "pyproject.toml" + content = pyproject.read_text(encoding="utf-8") + + assert '"starlette>=0.46.0,<1.0.0"' in content + + def test_readiness_gate_treats_deprecation_warnings_as_errors() -> None: script = Path(__file__).resolve().parents[2] / "scripts" / "run_readiness_check.sh" content = script.read_text(encoding="utf-8") diff --git a/backend/tests/test_sprint48_api_contract_audit.py b/backend/tests/test_sprint48_api_contract_audit.py index 850cc083..1398c2a6 100644 --- a/backend/tests/test_sprint48_api_contract_audit.py +++ b/backend/tests/test_sprint48_api_contract_audit.py @@ -18,6 +18,9 @@ def test_api_contract_audit_checks_openapi_against_docs() -> None: content = script.read_text(encoding="utf-8") assert "create_app" in content + assert ".openapi()" in content + assert 'schema.get("paths", {})' in content + assert "for route in app.routes" not in content assert "docs/API_CONTRACTS.md" in content assert "Missing documented API route" in content assert "Documented API route is not implemented" in content diff --git a/backend/tests/test_sprint8c_detection_visualization_qa.py b/backend/tests/test_sprint8c_detection_visualization_qa.py index 78ff5905..6ed1b1ba 100644 --- a/backend/tests/test_sprint8c_detection_visualization_qa.py +++ b/backend/tests/test_sprint8c_detection_visualization_qa.py @@ -8,6 +8,7 @@ from fastapi.testclient import TestClient from geoalchemy2.shape import from_shape from shapely.geometry import Polygon, box +from app.core.errors import AppError from app.main import app from app.db.session import get_db from app.models import AnalysisRun, Dataset, Detection, Job, Metric, Project, QualityCheck, VectorFeature @@ -276,6 +277,54 @@ def test_detection_qa_no_match_case_persists_zero_scores() -> None: assert result["f1_score"] == 0.0 +def test_configured_yolo_qa_requires_persisted_tile_manifest_provenance() -> None: + project_id = uuid4() + dataset_id = uuid4() + reference_dataset_id = uuid4() + analysis_run_id = uuid4() + detection = _detection(project_id, dataset_id, analysis_run_id) + reference_dataset = Dataset( + id=reference_dataset_id, + project_id=project_id, + name="reference.geojson", + dataset_type="vector", + source="manual", + dataset_role="reference", + ) + reference_feature = VectorFeature( + id=uuid4(), + dataset_id=reference_dataset_id, + feature_class="building", + geometry=from_shape(box(4.0, 51.0, 4.1, 51.1), srid=4326), + ) + db = FakeSession( + objects={ + (AnalysisRun, analysis_run_id): AnalysisRun( + id=analysis_run_id, + project_id=project_id, + dataset_id=dataset_id, + analysis_type="detection", + status="success", + model_name="yolo-configured", + parameters_json={"model_id": "yolo-configured"}, + ), + (Dataset, reference_dataset_id): reference_dataset, + }, + query_rows={Detection: [detection], VectorFeature: [reference_feature]}, + ) + + with pytest.raises(AppError) as exc_info: + DetectionService.compare_detections_with_reference( + db=db, + analysis_run_id=analysis_run_id, + reference_dataset_id=reference_dataset_id, + iou_threshold=0.5, + ) + + assert exc_info.value.code == "DETECTION_QA_COVERAGE_UNAVAILABLE" + assert db.added == [] + + def _coverage_manifest(tmp_path, dataset_id, bounds=(-1.0, -1.0, 3.0, 3.0)): manifest_path = tmp_path / "manifest.json" manifest_path.write_text( diff --git a/docs/CODEX_EXECUTION_LOG.md b/docs/CODEX_EXECUTION_LOG.md index f97bcfd0..3bf03975 100644 --- a/docs/CODEX_EXECUTION_LOG.md +++ b/docs/CODEX_EXECUTION_LOG.md @@ -7587,7 +7587,8 @@ Open: ## Local validation - `bash scripts/run_readiness_check.sh` passed end to end. -- Full backend suite passed: `506` tests. +- Full backend suite passed: `508` tests after the final coverage, manifest + provenance and framework-contract regression additions. - API contract audit passed with `81` implemented routes and the two documented non-envelope endpoints. - Alembic reports one head: `202606120900`. @@ -7599,3 +7600,46 @@ Open: - Deploy Sprint 184 to Tower, rerun QA for the persisted Mol-center analysis run and verify both persisted coverage evidence and Detection Lab rendering against live PostGIS before making any model-training decision. + +## Tower and live Mol evidence + +- Pushed commit `948e50b` and rebuilt the all-in-one runtime at + `http://192.168.10.150:1202`. PostGIS `3.6`, required schema/indexes, + database collation, Alembic head `202606120900`, frontend proxy, API proxy + and icon checks passed. +- Reused persisted analysis run `97c204cc-bc75-4139-aab5-25397dd0d87e`, + raster `e9a292e2-57d2-4a85-9df6-32580ba6ea98` and GRB reference dataset + `bfc83882-2acc-470d-baec-ee828b4428f8`; no duplicate inference or fake data + was produced. +- The persisted EPSG:31370 tile manifest contained one inference tile. All 36 + detections were evaluated; one was clipped at the tile boundary. Of 374 + persisted reference footprints, 304 intersected coverage, 70 were excluded + outside coverage and 34 were clipped at the boundary. +- Quality check `a25290ed-14ea-4c84-aff7-20672f568b1c` persisted 19 strict + footprint-IoU matches, 17 false positives and 285 false negatives. Its six + canonical Metric rows contain precision `0.527778`, recall `0.0625`, F1 + `0.111765`, mean IoU `0.599302` and the two error counts. +- The diagnostic-only reference-envelope pass found 33 matches and identifies + 14 possible box-to-footprint matching artifacts. Those values are persisted + only in findings and do not alter canonical Metric rows. +- Internal-browser validation reran the comparison from Detection Lab and + displayed `304 of 374 reference features evaluated`, coverage exclusions, + boundary clipping and the explicitly labelled diagnostic. The deployed page + produced no console warnings or errors for the live Tower URL. +- A later clean framework resolution exposed FastAPI `0.139.0`'s grouped + top-level router representation: `app.routes` contains 17 groups while the + canonical OpenAPI schema still contains all 81 operations. Updated the API + contract audit to read the OpenAPI path map and added a regression guard so + clean framework installations cannot produce a false 81-route drift report. +- Starlette `1.3.1` also deprecates the existing `httpx` TestClient path in + favor of `httpx2`. Kept Starlette on the still-supported + `>=0.46.0,<1.0.0` line until that test-client migration receives a dedicated + compatibility pass; the release gate consequently remains warning-free. + +## Decision + +- Sprint 184 is operationally complete. Coverage bias is now explicit and the + observed 14-match geometry gap confirms that model quality must not be judged + from envelope diagnostics. The next safe model step is an evidence-led + footprint-label/matching review, followed by a fresh bounded Mol multi-zone + benchmark before any activation or retraining decision. diff --git a/frontend/src/components/detection/DetectionLab.tsx b/frontend/src/components/detection/DetectionLab.tsx index 5497e3c0..63a4b62c 100644 --- a/frontend/src/components/detection/DetectionLab.tsx +++ b/frontend/src/components/detection/DetectionLab.tsx @@ -943,7 +943,7 @@ export function DetectionLab({

{detectionQaResult.coverage.applied - ? `${detectionQaResult.coverage.reference_excluded_outside_count} outside coverage, ${detectionQaResult.coverage.reference_clipped_boundary_count} clipped at the boundary, ${detectionQaResult.coverage.tile_count} tiles.` + ? `${detectionQaResult.coverage.reference_excluded_outside_count} outside coverage, ${detectionQaResult.coverage.reference_clipped_boundary_count} clipped at the boundary, ${detectionQaResult.coverage.tile_count} ${detectionQaResult.coverage.tile_count === 1 ? 'tile' : 'tiles'}.` : 'This run uses the complete selected reference population.'}

diff --git a/scripts/audit_api_contracts.py b/scripts/audit_api_contracts.py index 6db3efae..4f7e5234 100644 --- a/scripts/audit_api_contracts.py +++ b/scripts/audit_api_contracts.py @@ -26,15 +26,15 @@ def _load_app_routes() -> set[tuple[str, str]]: sys.path.insert(0, str(BACKEND)) from app.main import create_app - app = create_app() + schema = create_app().openapi() routes: set[tuple[str, str]] = set() - for route in app.routes: - path = getattr(route, "path", None) - methods = getattr(route, "methods", set()) - if not path or path in IGNORED_OPENAPI_PATHS: + for path, path_item in schema.get("paths", {}).items(): + if path in IGNORED_OPENAPI_PATHS or not isinstance(path_item, dict): continue - for method in sorted(methods - {"HEAD", "OPTIONS"}): - routes.add((method, path)) + for method in path_item: + normalized_method = method.upper() + if normalized_method in {"GET", "POST", "PATCH", "DELETE"}: + routes.add((normalized_method, path)) return routes