diff --git a/CHANGELOG.md b/CHANGELOG.md index e2548af8..16a33b0a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,13 @@ # Changelog +## Sprint 48 Backend API contract audit (2026-06-17) + +- Added `scripts/audit_api_contracts.py` to compare the active FastAPI route surface with `docs/API_CONTRACTS.md`. +- Added the API contract audit to the readiness gate so undocumented routes and stale documented routes fail release checks. +- Corrected API contract drift for area detail/update, vector stats, dataset content and future analysis/YOLO export placeholders. +- Added regression tests for the contract audit and readiness integration. + ## Sprint 47 Workbench interaction smoke (2026-06-17) - Added stable `data-testid` anchors to the existing project, area, map, dataset, QA/QC and export controls for browser regression checks. diff --git a/backend/tests/test_sprint48_api_contract_audit.py b/backend/tests/test_sprint48_api_contract_audit.py new file mode 100644 index 00000000..850cc083 --- /dev/null +++ b/backend/tests/test_sprint48_api_contract_audit.py @@ -0,0 +1,35 @@ +from __future__ import annotations + +from pathlib import Path + + +ROOT = Path(__file__).resolve().parents[2] + + +def test_readiness_gate_runs_api_contract_audit() -> None: + script = ROOT / "scripts" / "run_readiness_check.sh" + content = script.read_text(encoding="utf-8") + + assert "scripts/audit_api_contracts.py" in content + + +def test_api_contract_audit_checks_openapi_against_docs() -> None: + script = ROOT / "scripts" / "audit_api_contracts.py" + content = script.read_text(encoding="utf-8") + + assert "create_app" 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 + assert "Allowed non-envelope endpoint is not implemented" in content + assert "/api/v1/exports/{export_id}/download" in content + + +def test_api_contract_docs_include_current_implemented_route_surface() -> None: + docs = (ROOT / "docs" / "API_CONTRACTS.md").read_text(encoding="utf-8") + + assert 'GET `/api/v1/projects/{project_id}/areas/{area_id}`' in docs + assert 'PATCH `/api/v1/projects/{project_id}/areas/{area_id}`' in docs + assert 'GET `/api/v1/projects/{project_id}/datasets/{dataset_id}/vector/stats`' in docs + assert 'GET `/api/v1/projects/{project_id}/datasets/{dataset_id}/content`' in docs + assert 'POST `/api/v1/projects/{project_id}/datasets/{dataset_id}/vector/stats`' not in docs diff --git a/docs/API_CONTRACTS.md b/docs/API_CONTRACTS.md index 4639599d..bce7159c 100644 --- a/docs/API_CONTRACTS.md +++ b/docs/API_CONTRACTS.md @@ -161,6 +161,16 @@ Backend responsibilities: - Calculate area in square meters using projected CRS. - Store bbox. +### GET `/api/v1/projects/{project_id}/areas/{area_id}` + +Returns one project area. The payload uses the same `AreaRead` shape as the +area list endpoint and includes persisted GeoJSON geometry for map display. + +### PATCH `/api/v1/projects/{project_id}/areas/{area_id}` + +Updates the area name and/or geometry. Geometry updates follow the same +validation, repair and metric-calculation rules as area creation. + ## Datasets ### POST `/api/v1/projects/{project_id}/datasets/upload` @@ -339,7 +349,7 @@ Apply buffer distance to vector features. Intersect source vector dataset with another vector dataset. -### POST `/api/v1/projects/{project_id}/datasets/{dataset_id}/vector/stats` +### GET `/api/v1/projects/{project_id}/datasets/{dataset_id}/vector/stats` Return vector stats (feature counts and geometry summary). @@ -347,6 +357,12 @@ Return vector stats (feature counts and geometry summary). Return vector bounds and feature count. +### GET `/api/v1/projects/{project_id}/datasets/{dataset_id}/content` + +Returns stored vector dataset content through the canonical API envelope. +Vector content is returned as GeoJSON/JSON payload data. Raster content is not +served through this endpoint. + ## Jobs ### POST `/api/v1/projects/{project_id}/jobs` @@ -670,11 +686,12 @@ Response persists a `quality_check` and `metrics` rows through the existing QA/Q If the reference dataset has no persisted vector features, the endpoint returns `REFERENCE_FEATURES_NOT_FOUND`. It does not calculate fake QA metrics. -### POST `/api/v1/analysis/building-stats` +#### Future analysis route: `/api/v1/analysis/building-stats` -Input: area + vector building layer. +Not implemented in the active API surface. Future input is expected to combine +an area with a vector building layer. -### POST `/api/v1/analysis/object-detection` +#### Future analysis route: `/api/v1/analysis/object-detection` Request: @@ -693,7 +710,7 @@ Request: Response: `AnalysisRunRead`. -### POST `/api/v1/analysis/segmentation` +#### Future analysis route: `/api/v1/analysis/segmentation` Same pattern as object detection, but output includes masks and polygonized geometries. @@ -1027,7 +1044,7 @@ intentionally does not use the JSON envelope because it is a browser/file-downlo path; callers that need canonical API JSON should use `/content` for JSON/GeoJSON artifacts. -### POST `/api/v1/exports/yolo` +#### Future export route: `/api/v1/exports/yolo` Export annotations/detections to YOLO format. diff --git a/docs/CODEX_EXECUTION_LOG.md b/docs/CODEX_EXECUTION_LOG.md index 408b6162..6886904b 100644 --- a/docs/CODEX_EXECUTION_LOG.md +++ b/docs/CODEX_EXECUTION_LOG.md @@ -1,3 +1,38 @@ +## Sprint 48 Backend API contract audit (2026-06-17) + +Changed: +- Added `scripts/audit_api_contracts.py` to import the FastAPI app, enumerate the implemented `GET`/`POST`/`PATCH`/`DELETE` route surface and compare it with active `### METHOD route` headings in `docs/API_CONTRACTS.md`. +- Added the API contract audit to `scripts/run_readiness_check.sh`. +- Corrected contract drift in `docs/API_CONTRACTS.md`: + - documented `GET/PATCH /api/v1/projects/{project_id}/areas/{area_id}`; + - corrected vector stats from `POST` to implemented `GET`; + - documented `GET /api/v1/projects/{project_id}/datasets/{dataset_id}/content`; + - changed non-implemented building-stats, legacy analysis object-detection/segmentation and YOLO export entries from active route headings to future-route notes. +- Added `backend/tests/test_sprint48_api_contract_audit.py`. +- Updated `scripts/README.md`, `docs/TODO.md` and `CHANGELOG.md`. + +Validation: +- RED: `cd backend && python -m pytest tests/test_sprint48_api_contract_audit.py -q` failed before implementation because the audit script, readiness integration and route docs were missing. +- RED: `python scripts/audit_api_contracts.py` reported missing docs for 4 implemented routes and 5 stale documented routes. +- `python scripts/audit_api_contracts.py` passed: 76 implemented routes matched docs and 2 explicit non-envelope endpoints were tracked. +- `cd backend && python -m pytest tests/test_sprint48_api_contract_audit.py tests/test_readiness_gate.py -q` passed: 12 tests. +- `python -m py_compile scripts/audit_api_contracts.py` passed. +- `python -m compileall backend/app` passed. +- `cd backend && python -m pytest -W error::DeprecationWarning` passed: 201 tests. +- `bash scripts/run_readiness_check.sh` passed and included `API contract audit OK`. +- `cd frontend && npm run typecheck` passed. +- `cd frontend && npm run build` passed. +- `cd backend && python -m alembic heads` passed: `202606120900 (head)`. +- `cd backend && python -m alembic upgrade head --sql` passed. +- `bash -n scripts/live_migration_smoke.sh` passed. + +Limitations: +- This pass audits route documentation presence, implemented/stale route drift and explicit non-envelope exceptions. It does not yet exercise every error path response body at runtime. +- No API behavior, migrations, provider fetching, AI behavior or product capabilities changed. + +Next recommended pass: +- Add an error-envelope runtime audit for representative invalid/missing-resource paths across projects, datasets, providers, detection, segmentation, QA and exports. + ## Sprint 47 Workbench interaction smoke (2026-06-17) Changed: diff --git a/docs/TODO.md b/docs/TODO.md index be662bca..15182b69 100644 --- a/docs/TODO.md +++ b/docs/TODO.md @@ -43,6 +43,7 @@ This file now starts with the current implementation status. Older preparation/b - [x] Dry-run-first demo export artifact cleanup tooling. - [x] Browser-facing default workbench state smoke for the offline demo project. - [x] Browser-facing workbench interaction backing-state smoke and stable UI test anchors. +- [x] Backend API contract audit comparing implemented FastAPI routes with `docs/API_CONTRACTS.md`. - [x] Live Docker/PostGIS validation on Tower/Unraid. - [x] Real YOLO compatibility smoke with optional AI extras and local model file. - [x] Detection and segmentation workflow hook extraction beyond Sprint 10. @@ -58,7 +59,7 @@ This file now starts with the current implementation status. Older preparation/b - [x] Demo workflow orchestration hook decomposition. - [x] Final `App.tsx` import/encoding cleanup and size audit. - [x] Optional final bootstrap-effect extraction. -- [ ] Decide next V1 stabilization focus: browser screenshot artifact automation, backend service contract audit, or golden dataset expansion. +- [ ] Decide next V1 stabilization focus: browser screenshot artifact automation, backend error-envelope audit, or golden dataset expansion. ## Sprint 8 status diff --git a/scripts/README.md b/scripts/README.md index d5423d6e..42f29b79 100644 --- a/scripts/README.md +++ b/scripts/README.md @@ -4,6 +4,17 @@ Setup-, import-, demo- en maintenance-scripts voor GeoIntel. ## Runtime verification +Audit the active backend route surface against `docs/API_CONTRACTS.md`: + +```bash +python scripts/audit_api_contracts.py +``` + +The audit imports the FastAPI app, compares implemented `GET`/`POST`/`PATCH`/ +`DELETE` routes with active API contract headings and tracks the explicit +non-envelope exceptions (`/health` and export downloads). It fails when a route +exists without docs or when docs claim an endpoint that is not implemented. + Verify the browser-facing Docker/LAN runtime: ```bash diff --git a/scripts/audit_api_contracts.py b/scripts/audit_api_contracts.py new file mode 100644 index 00000000..6db3efae --- /dev/null +++ b/scripts/audit_api_contracts.py @@ -0,0 +1,73 @@ +from __future__ import annotations + +import re +import sys +from pathlib import Path + + +ROOT = Path(__file__).resolve().parents[1] +BACKEND = ROOT / "backend" +DOCS = ROOT / "docs" / "API_CONTRACTS.md" # docs/API_CONTRACTS.md + +ALLOWED_NON_ENVELOPE_ENDPOINTS = { + ("GET", "/health"), + ("GET", "/api/v1/exports/{export_id}/download"), +} + +IGNORED_OPENAPI_PATHS = { + "/openapi.json", + "/docs", + "/docs/oauth2-redirect", + "/redoc", +} + + +def _load_app_routes() -> set[tuple[str, str]]: + sys.path.insert(0, str(BACKEND)) + from app.main import create_app + + app = create_app() + 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: + continue + for method in sorted(methods - {"HEAD", "OPTIONS"}): + routes.add((method, path)) + return routes + + +def _load_documented_routes() -> set[tuple[str, str]]: + docs = DOCS.read_text(encoding="utf-8") + return set(re.findall(r"###\s+(GET|POST|PATCH|DELETE)\s+`([^`]+)`", docs)) + + +def main() -> None: + implemented_routes = _load_app_routes() + documented_routes = _load_documented_routes() + + missing_docs = sorted(implemented_routes - documented_routes) + stale_docs = sorted(documented_routes - implemented_routes) + missing_non_envelope = sorted(ALLOWED_NON_ENVELOPE_ENDPOINTS - implemented_routes) + + errors: list[str] = [] + for method, path in missing_docs: + errors.append(f"Missing documented API route: {method} {path}") + for method, path in stale_docs: + errors.append(f"Documented API route is not implemented: {method} {path}") + for method, path in missing_non_envelope: + errors.append(f"Allowed non-envelope endpoint is not implemented: {method} {path}") + + if errors: + raise SystemExit("\n".join(errors)) + + print( + "API contract audit OK: " + f"{len(implemented_routes)} implemented routes match docs; " + f"{len(ALLOWED_NON_ENVELOPE_ENDPOINTS)} explicit non-envelope endpoints tracked." + ) + + +if __name__ == "__main__": + main() diff --git a/scripts/run_readiness_check.sh b/scripts/run_readiness_check.sh index 9d62901d..53bd7191 100755 --- a/scripts/run_readiness_check.sh +++ b/scripts/run_readiness_check.sh @@ -32,6 +32,7 @@ bash scripts/check_repo_structure.sh ${PYTHON_BIN} scripts/smoke_docs.py ${PYTHON_BIN} scripts/validate_fixtures.py ${PYTHON_BIN} scripts/smoke_contracts.py +${PYTHON_BIN} scripts/audit_api_contracts.py ${PYTHON_BIN} scripts/preimplementation_audit.py ${PYTHON_BIN} scripts/validate_m13_codex_assets.py ${PYTHON_BIN} scripts/validate_m14_launch_assets.py