fix: harden release checks for grouped routes
GeoIntel CI / docs-smoke (push) Canceled after 0s
GeoIntel CI / contract-smoke (push) Canceled after 0s

This commit is contained in:
Codex
2026-07-14 11:41:16 +02:00
parent 948e50b5e1
commit d7eadbc6ca
8 changed files with 116 additions and 9 deletions
+3
View File
@@ -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. - 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. - 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. - 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) ## Sprint 183 Mol map source clarity and live AI validation (2026-07-14)
+1
View File
@@ -6,6 +6,7 @@ readme = "README.md"
requires-python = ">=3.11" requires-python = ">=3.11"
dependencies = [ dependencies = [
"fastapi>=0.112.0", "fastapi>=0.112.0",
"starlette>=0.46.0,<1.0.0",
"uvicorn[standard]>=0.30.6", "uvicorn[standard]>=0.30.6",
"SQLAlchemy>=2.0.34", "SQLAlchemy>=2.0.34",
"psycopg[binary]>=3.2.1", "psycopg[binary]>=3.2.1",
+7
View File
@@ -1,6 +1,13 @@
from pathlib import Path 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: def test_readiness_gate_treats_deprecation_warnings_as_errors() -> None:
script = Path(__file__).resolve().parents[2] / "scripts" / "run_readiness_check.sh" script = Path(__file__).resolve().parents[2] / "scripts" / "run_readiness_check.sh"
content = script.read_text(encoding="utf-8") content = script.read_text(encoding="utf-8")
@@ -18,6 +18,9 @@ def test_api_contract_audit_checks_openapi_against_docs() -> None:
content = script.read_text(encoding="utf-8") content = script.read_text(encoding="utf-8")
assert "create_app" in content 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 "docs/API_CONTRACTS.md" in content
assert "Missing documented API route" in content assert "Missing documented API route" in content
assert "Documented API route is not implemented" in content assert "Documented API route is not implemented" in content
@@ -8,6 +8,7 @@ from fastapi.testclient import TestClient
from geoalchemy2.shape import from_shape from geoalchemy2.shape import from_shape
from shapely.geometry import Polygon, box from shapely.geometry import Polygon, box
from app.core.errors import AppError
from app.main import app from app.main import app
from app.db.session import get_db from app.db.session import get_db
from app.models import AnalysisRun, Dataset, Detection, Job, Metric, Project, QualityCheck, VectorFeature 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 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)): def _coverage_manifest(tmp_path, dataset_id, bounds=(-1.0, -1.0, 3.0, 3.0)):
manifest_path = tmp_path / "manifest.json" manifest_path = tmp_path / "manifest.json"
manifest_path.write_text( manifest_path.write_text(
+45 -1
View File
@@ -7587,7 +7587,8 @@ Open:
## Local validation ## Local validation
- `bash scripts/run_readiness_check.sh` passed end to end. - `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 - API contract audit passed with `81` implemented routes and the two documented
non-envelope endpoints. non-envelope endpoints.
- Alembic reports one head: `202606120900`. - Alembic reports one head: `202606120900`.
@@ -7599,3 +7600,46 @@ Open:
- Deploy Sprint 184 to Tower, rerun QA for the persisted Mol-center analysis - Deploy Sprint 184 to Tower, rerun QA for the persisted Mol-center analysis
run and verify both persisted coverage evidence and Detection Lab rendering run and verify both persisted coverage evidence and Detection Lab rendering
against live PostGIS before making any model-training decision. 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.
@@ -943,7 +943,7 @@ export function DetectionLab({
</strong> </strong>
<p> <p>
{detectionQaResult.coverage.applied {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.'} : 'This run uses the complete selected reference population.'}
</p> </p>
</div> </div>
+7 -7
View File
@@ -26,15 +26,15 @@ def _load_app_routes() -> set[tuple[str, str]]:
sys.path.insert(0, str(BACKEND)) sys.path.insert(0, str(BACKEND))
from app.main import create_app from app.main import create_app
app = create_app() schema = create_app().openapi()
routes: set[tuple[str, str]] = set() routes: set[tuple[str, str]] = set()
for route in app.routes: for path, path_item in schema.get("paths", {}).items():
path = getattr(route, "path", None) if path in IGNORED_OPENAPI_PATHS or not isinstance(path_item, dict):
methods = getattr(route, "methods", set())
if not path or path in IGNORED_OPENAPI_PATHS:
continue continue
for method in sorted(methods - {"HEAD", "OPTIONS"}): for method in path_item:
routes.add((method, path)) normalized_method = method.upper()
if normalized_method in {"GET", "POST", "PATCH", "DELETE"}:
routes.add((normalized_method, path))
return routes return routes