fix: harden release checks for grouped routes
This commit is contained in:
@@ -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)
|
||||
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -943,7 +943,7 @@ export function DetectionLab({
|
||||
</strong>
|
||||
<p>
|
||||
{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.'}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user