Harden V1 demo workflow smoke
This commit is contained in:
@@ -7,6 +7,13 @@
|
|||||||
|
|
||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
|
## Sprint 21 V1 demo workflow smoke hardening (2026-06-17)
|
||||||
|
|
||||||
|
- Hardened the browser-facing demo/export smoke to verify connected V1 state: project area GeoJSON, fixture datasets, vector FeatureCollection content, vector feature summary, persisted QA/QC metrics and export downloads.
|
||||||
|
- Loading the offline demo workflow in the frontend now opens the candidate vector fixture dataset directly, so the map workbench is populated after the demo action.
|
||||||
|
- Added regression tests for the strengthened demo smoke and frontend demo dataset loading contract.
|
||||||
|
- No migrations, provider downloads, AI inference, new dependencies or API route renames were introduced.
|
||||||
|
|
||||||
## Sprint 20 selected area map overlay (2026-06-17)
|
## Sprint 20 selected area map overlay (2026-06-17)
|
||||||
|
|
||||||
- Added persisted AOI GeoJSON to area API responses without changing the database schema.
|
- Added persisted AOI GeoJSON to area API responses without changing the database schema.
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import json
|
import json
|
||||||
|
import os
|
||||||
from datetime import datetime, timezone
|
from datetime import datetime, timezone
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from uuid import UUID, uuid4
|
from uuid import UUID, uuid4
|
||||||
@@ -30,6 +31,16 @@ class DemoWorkflowService:
|
|||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _fixture_path(filename: str) -> Path:
|
def _fixture_path(filename: str) -> Path:
|
||||||
|
roots: list[Path] = []
|
||||||
|
if os.getenv("GEOINTEL_FIXTURES_ROOT"):
|
||||||
|
roots.append(Path(os.environ["GEOINTEL_FIXTURES_ROOT"]))
|
||||||
|
roots.extend(parent / "fixtures" / "golden" for parent in Path(__file__).resolve().parents)
|
||||||
|
roots.append(Path("/app/fixtures/golden"))
|
||||||
|
|
||||||
|
for root in roots:
|
||||||
|
path = root / filename
|
||||||
|
if path.exists():
|
||||||
|
return path
|
||||||
return DemoWorkflowService._repo_root() / "fixtures" / "golden" / filename
|
return DemoWorkflowService._repo_root() / "fixtures" / "golden" / filename
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
@@ -194,6 +205,8 @@ class DemoWorkflowService:
|
|||||||
@staticmethod
|
@staticmethod
|
||||||
def seed(db: Session) -> DemoWorkflowResponse:
|
def seed(db: Session) -> DemoWorkflowResponse:
|
||||||
existing = DemoWorkflowService._find_existing_project(db)
|
existing = DemoWorkflowService._find_existing_project(db)
|
||||||
|
reference_payload, reference_raw = DemoWorkflowService._load_fixture("reference_buildings.geojson")
|
||||||
|
candidate_payload, candidate_raw = DemoWorkflowService._load_fixture("predicted_buildings.geojson")
|
||||||
if existing:
|
if existing:
|
||||||
area = db.query(Area).filter(Area.project_id == existing.id).order_by(Area.created_at.asc()).first()
|
area = db.query(Area).filter(Area.project_id == existing.id).order_by(Area.created_at.asc()).first()
|
||||||
reference = (
|
reference = (
|
||||||
@@ -229,7 +242,9 @@ class DemoWorkflowService:
|
|||||||
message="Demo workflow already exists.",
|
message="Demo workflow already exists.",
|
||||||
created=False,
|
created=False,
|
||||||
)
|
)
|
||||||
|
project = existing
|
||||||
|
created = True
|
||||||
|
else:
|
||||||
project = Project(
|
project = Project(
|
||||||
id=uuid4(),
|
id=uuid4(),
|
||||||
name=DemoWorkflowService.PROJECT_NAME,
|
name=DemoWorkflowService.PROJECT_NAME,
|
||||||
@@ -240,11 +255,15 @@ class DemoWorkflowService:
|
|||||||
db.add(project)
|
db.add(project)
|
||||||
db.commit()
|
db.commit()
|
||||||
db.refresh(project)
|
db.refresh(project)
|
||||||
|
area = None
|
||||||
|
reference = None
|
||||||
|
candidate = None
|
||||||
|
quality_check = None
|
||||||
|
created = True
|
||||||
|
|
||||||
|
if not area:
|
||||||
area = DemoWorkflowService._create_area(db, project.id)
|
area = DemoWorkflowService._create_area(db, project.id)
|
||||||
reference_payload, reference_raw = DemoWorkflowService._load_fixture("reference_buildings.geojson")
|
if not reference:
|
||||||
candidate_payload, candidate_raw = DemoWorkflowService._load_fixture("predicted_buildings.geojson")
|
|
||||||
|
|
||||||
reference = DemoWorkflowService._create_dataset(
|
reference = DemoWorkflowService._create_dataset(
|
||||||
db=db,
|
db=db,
|
||||||
project_id=project.id,
|
project_id=project.id,
|
||||||
@@ -256,6 +275,7 @@ class DemoWorkflowService:
|
|||||||
source_name="fixture",
|
source_name="fixture",
|
||||||
reference_layer_name="buildings",
|
reference_layer_name="buildings",
|
||||||
)
|
)
|
||||||
|
if not candidate:
|
||||||
candidate = DemoWorkflowService._create_dataset(
|
candidate = DemoWorkflowService._create_dataset(
|
||||||
db=db,
|
db=db,
|
||||||
project_id=project.id,
|
project_id=project.id,
|
||||||
@@ -267,6 +287,7 @@ class DemoWorkflowService:
|
|||||||
source_name="fixture",
|
source_name="fixture",
|
||||||
reference_layer_name=None,
|
reference_layer_name=None,
|
||||||
)
|
)
|
||||||
|
if not quality_check:
|
||||||
quality_check = DemoWorkflowService._persist_qa(
|
quality_check = DemoWorkflowService._persist_qa(
|
||||||
db=db,
|
db=db,
|
||||||
project_id=project.id,
|
project_id=project.id,
|
||||||
@@ -284,5 +305,5 @@ class DemoWorkflowService:
|
|||||||
metric_count=6,
|
metric_count=6,
|
||||||
status="ready",
|
status="ready",
|
||||||
message="Demo workflow seeded from explicit local fixtures.",
|
message="Demo workflow seeded from explicit local fixtures.",
|
||||||
created=True,
|
created=created,
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -97,6 +97,12 @@ def test_compose_waits_for_healthy_database_and_applies_migrations() -> None:
|
|||||||
assert "sh /app/docker_start.sh" in compose
|
assert "sh /app/docker_start.sh" in compose
|
||||||
|
|
||||||
|
|
||||||
|
def test_compose_mounts_demo_fixtures_for_backend_runtime() -> None:
|
||||||
|
compose = (ROOT / "docker-compose.yml").read_text(encoding="utf-8")
|
||||||
|
|
||||||
|
assert "./fixtures:/app/fixtures:ro" in compose
|
||||||
|
|
||||||
|
|
||||||
def test_compose_has_backend_and_frontend_healthchecks() -> None:
|
def test_compose_has_backend_and_frontend_healthchecks() -> None:
|
||||||
compose = (ROOT / "docker-compose.yml").read_text(encoding="utf-8")
|
compose = (ROOT / "docker-compose.yml").read_text(encoding="utf-8")
|
||||||
|
|
||||||
|
|||||||
@@ -27,6 +27,13 @@ def test_demo_export_workflow_script_verifies_export_endpoints() -> None:
|
|||||||
content = script.read_text(encoding="utf-8")
|
content = script.read_text(encoding="utf-8")
|
||||||
|
|
||||||
assert "/api/v1/demo/workflow" in content
|
assert "/api/v1/demo/workflow" in content
|
||||||
|
assert "/areas" in content
|
||||||
|
assert "/datasets" in content
|
||||||
|
assert "/content" in content
|
||||||
|
assert "/vector/summary" in content
|
||||||
|
assert "GeoJSON Polygon/MultiPolygon geometry" in content
|
||||||
|
assert "precision" in content
|
||||||
|
assert "false_negative_count" in content
|
||||||
assert "/api/v1/exports/metadata" in content
|
assert "/api/v1/exports/metadata" in content
|
||||||
assert "/api/v1/exports/report" in content
|
assert "/api/v1/exports/report" in content
|
||||||
assert "/api/v1/exports/geojson" in content
|
assert "/api/v1/exports/geojson" in content
|
||||||
|
|||||||
@@ -55,3 +55,14 @@ def test_demo_workflow_service_uses_explicit_golden_fixtures() -> None:
|
|||||||
assert DemoWorkflowService.PROJECT_NAME == "GeoIntel Demo - Building QA"
|
assert DemoWorkflowService.PROJECT_NAME == "GeoIntel Demo - Building QA"
|
||||||
assert DemoWorkflowService.REFERENCE_FILENAME == "demo_reference_buildings.geojson"
|
assert DemoWorkflowService.REFERENCE_FILENAME == "demo_reference_buildings.geojson"
|
||||||
assert DemoWorkflowService.CANDIDATE_FILENAME == "demo_predicted_buildings.geojson"
|
assert DemoWorkflowService.CANDIDATE_FILENAME == "demo_predicted_buildings.geojson"
|
||||||
|
|
||||||
|
|
||||||
|
def test_demo_workflow_service_supports_container_fixture_mount() -> None:
|
||||||
|
service = (DemoWorkflowService._repo_root() / "backend" / "app" / "services" / "demo_workflow_service.py").read_text(encoding="utf-8")
|
||||||
|
|
||||||
|
assert "GEOINTEL_FIXTURES_ROOT" in service
|
||||||
|
assert 'Path("/app/fixtures/golden")' in service
|
||||||
|
assert "reference_payload, reference_raw = DemoWorkflowService._load_fixture" in service
|
||||||
|
assert "project = existing" in service
|
||||||
|
assert "if not reference:" in service
|
||||||
|
assert "if not candidate:" in service
|
||||||
|
|||||||
@@ -0,0 +1,26 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
|
||||||
|
ROOT = Path(__file__).resolve().parents[2]
|
||||||
|
|
||||||
|
|
||||||
|
def test_demo_workflow_browser_smoke_script_checks_connected_v1_state() -> None:
|
||||||
|
script = (ROOT / "scripts" / "verify_demo_export_workflow.sh").read_text(encoding="utf-8")
|
||||||
|
|
||||||
|
assert "/api/v1/demo/workflow" in script
|
||||||
|
assert "/api/v1/projects/${project_id}/areas" in script
|
||||||
|
assert "/api/v1/projects/${project_id}/datasets" in script
|
||||||
|
assert "/api/v1/projects/${project_id}/datasets/${candidate_dataset_id}/content" in script
|
||||||
|
assert "/api/v1/projects/${project_id}/datasets/${candidate_dataset_id}/vector/summary" in script
|
||||||
|
assert "GeoJSON Polygon/MultiPolygon geometry" in script
|
||||||
|
assert "Candidate vector summary does not report persisted features" in script
|
||||||
|
|
||||||
|
|
||||||
|
def test_frontend_demo_action_loads_candidate_dataset_details_for_map_layer() -> None:
|
||||||
|
app = (ROOT / "frontend" / "src" / "App.tsx").read_text(encoding="utf-8")
|
||||||
|
|
||||||
|
assert "const candidateDataset = projectData?.datasets.find" in app
|
||||||
|
assert "dataset.id === result.candidate_dataset_id" in app
|
||||||
|
assert "await loadDatasetDetails(result.project_id, candidateDataset)" in app
|
||||||
@@ -24,6 +24,7 @@ services:
|
|||||||
- "8000:8000"
|
- "8000:8000"
|
||||||
volumes:
|
volumes:
|
||||||
- ./storage:/app/storage
|
- ./storage:/app/storage
|
||||||
|
- ./fixtures:/app/fixtures:ro
|
||||||
command: sh /app/docker_start.sh
|
command: sh /app/docker_start.sh
|
||||||
depends_on:
|
depends_on:
|
||||||
db:
|
db:
|
||||||
|
|||||||
@@ -1,3 +1,19 @@
|
|||||||
|
## Sprint 21 V1 demo workflow smoke hardening (2026-06-17)
|
||||||
|
|
||||||
|
Changed:
|
||||||
|
- Hardened `scripts/verify_demo_export_workflow.sh` so the explicit offline demo smoke validates area GeoJSON, fixture datasets, vector FeatureCollection content, vector feature summaries, persisted QA/QC metrics and export downloads through the frontend proxy.
|
||||||
|
- Updated the frontend demo workflow action to open the candidate vector fixture dataset after seeding/loading the demo, so the Map Workbench is populated without a manual dataset click.
|
||||||
|
- Added regression tests for the strengthened smoke script and frontend demo loading contract.
|
||||||
|
- Updated scripts/frontend documentation, changelog and TODO status.
|
||||||
|
|
||||||
|
Tested:
|
||||||
|
- Pending validation in this pass: backend compile, backend pytest, readiness, frontend typecheck/build, Alembic checks, demo/export smoke, Tower deploy and browser audit.
|
||||||
|
|
||||||
|
Known limitations:
|
||||||
|
- The demo smoke intentionally seeds fixture demo data when run; it should be used as an explicit verification command, not as an implicit healthcheck.
|
||||||
|
|
||||||
|
Next recommended pass:
|
||||||
|
- Add a compact V1 dashboard/status strip for project, AOI, datasets, QA and exports so operators can see readiness at a glance after opening a project.
|
||||||
## Sprint 20 V1 selected area map overlay (2026-06-17)
|
## Sprint 20 V1 selected area map overlay (2026-06-17)
|
||||||
|
|
||||||
Changed:
|
Changed:
|
||||||
|
|||||||
+3
-3
@@ -9,7 +9,7 @@ This file now starts with the current implementation status. Older preparation/b
|
|||||||
- [x] Enforce Python deprecation warnings as release-readiness failures.
|
- [x] Enforce Python deprecation warnings as release-readiness failures.
|
||||||
- [x] Fix Docker backend package install order and remove mandatory root `.env` dependency.
|
- [x] Fix Docker backend package install order and remove mandatory root `.env` dependency.
|
||||||
- [x] Add Docker build context ignores for backend and frontend.
|
- [x] Add Docker build context ignores for backend and frontend.
|
||||||
- [ ] Run Docker/PostGIS live validation on a machine where Docker is available.
|
- [x] Run Docker/PostGIS live validation on Tower/Unraid.
|
||||||
|
|
||||||
## Current implementation status
|
## Current implementation status
|
||||||
|
|
||||||
@@ -30,8 +30,8 @@ This file now starts with the current implementation status. Older preparation/b
|
|||||||
- [x] Project-scoped QA/QC result listing and frontend QA/QC Results panel.
|
- [x] Project-scoped QA/QC result listing and frontend QA/QC Results panel.
|
||||||
- [x] Persisted export foundation for vector/detection/segmentation GeoJSON and project metadata JSON.
|
- [x] Persisted export foundation for vector/detection/segmentation GeoJSON and project metadata JSON.
|
||||||
- [x] Lightweight HTML project report artifact export.
|
- [x] Lightweight HTML project report artifact export.
|
||||||
- [x] Browser-facing demo/export workflow smoke script.
|
- [x] Browser-facing demo/export workflow smoke script with connected V1 state checks.
|
||||||
- [ ] Live Docker/PostGIS validation in this execution environment.
|
- [x] Live Docker/PostGIS validation on Tower/Unraid.
|
||||||
- [ ] Real YOLO compatibility smoke with optional AI extras and local model file.
|
- [ ] Real YOLO compatibility smoke with optional AI extras and local model file.
|
||||||
- [ ] Further frontend state/module decomposition beyond Sprint 10 extraction.
|
- [ ] Further frontend state/module decomposition beyond Sprint 10 extraction.
|
||||||
|
|
||||||
|
|||||||
@@ -151,6 +151,11 @@ React + TypeScript + MapLibre foundation for project/area/dataset workflow.
|
|||||||
- Added area visibility and opacity controls alongside the existing active vector/result layer controls.
|
- Added area visibility and opacity controls alongside the existing active vector/result layer controls.
|
||||||
- Area list items can select which AOI is shown on the map.
|
- Area list items can select which AOI is shown on the map.
|
||||||
|
|
||||||
|
## Sprint 21 additions
|
||||||
|
|
||||||
|
- Loading the explicit demo workflow now opens the candidate vector fixture dataset directly, so the Map Workbench shows the demo vector layer without an extra manual dataset click.
|
||||||
|
- The demo/export verification script now checks connected V1 state: area GeoJSON, fixture datasets, vector FeatureCollection content, vector feature summary, persisted QA/QC metrics and export downloads through the frontend proxy.
|
||||||
|
|
||||||
## Release hardening updates
|
## Release hardening updates
|
||||||
|
|
||||||
- Production builds split application code, React vendor code and MapLibre vendor code into separate chunks.
|
- Production builds split application code, React vendor code and MapLibre vendor code into separate chunks.
|
||||||
|
|||||||
+10
-2
@@ -339,7 +339,9 @@ function App(): JSX.Element {
|
|||||||
])
|
])
|
||||||
setAreas(areaResponse.items)
|
setAreas(areaResponse.items)
|
||||||
setDatasets(datasetResponse.items)
|
setDatasets(datasetResponse.items)
|
||||||
if (!selectedClipAreaId && areaResponse.items.length > 0) {
|
if (areaResponse.items.length === 0) {
|
||||||
|
setSelectedClipAreaId('')
|
||||||
|
} else if (!selectedClipAreaId || !areaResponse.items.some((area) => area.id === selectedClipAreaId)) {
|
||||||
setSelectedClipAreaId(areaResponse.items[0].id)
|
setSelectedClipAreaId(areaResponse.items[0].id)
|
||||||
}
|
}
|
||||||
if (areaResponse.items.length === 0) {
|
if (areaResponse.items.length === 0) {
|
||||||
@@ -347,8 +349,10 @@ function App(): JSX.Element {
|
|||||||
} else if (!selectedMapAreaId || !areaResponse.items.some((area) => area.id === selectedMapAreaId)) {
|
} else if (!selectedMapAreaId || !areaResponse.items.some((area) => area.id === selectedMapAreaId)) {
|
||||||
setSelectedMapAreaId(areaResponse.items[0].id)
|
setSelectedMapAreaId(areaResponse.items[0].id)
|
||||||
}
|
}
|
||||||
|
return { areas: areaResponse.items, datasets: datasetResponse.items }
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
setErrorMessage(error instanceof Error ? error.message : 'Failed to load project data')
|
setErrorMessage(error instanceof Error ? error.message : 'Failed to load project data')
|
||||||
|
return null
|
||||||
} finally {
|
} finally {
|
||||||
setLoadingAreas(false)
|
setLoadingAreas(false)
|
||||||
setLoadingDatasets(false)
|
setLoadingDatasets(false)
|
||||||
@@ -759,13 +763,17 @@ function App(): JSX.Element {
|
|||||||
setSegmentationReferenceDatasetId(result.reference_dataset_id)
|
setSegmentationReferenceDatasetId(result.reference_dataset_id)
|
||||||
setDemoWorkflowMessage(result.message)
|
setDemoWorkflowMessage(result.message)
|
||||||
await loadProjects()
|
await loadProjects()
|
||||||
await Promise.all([
|
const [projectData] = await Promise.all([
|
||||||
loadProjectData(result.project_id),
|
loadProjectData(result.project_id),
|
||||||
loadDetectionRuns(result.project_id),
|
loadDetectionRuns(result.project_id),
|
||||||
loadSegmentationRuns(result.project_id),
|
loadSegmentationRuns(result.project_id),
|
||||||
loadQualityChecks(result.project_id),
|
loadQualityChecks(result.project_id),
|
||||||
loadExports(result.project_id),
|
loadExports(result.project_id),
|
||||||
])
|
])
|
||||||
|
const candidateDataset = projectData?.datasets.find((dataset) => dataset.id === result.candidate_dataset_id)
|
||||||
|
if (candidateDataset) {
|
||||||
|
await loadDatasetDetails(result.project_id, candidateDataset)
|
||||||
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
setErrorMessage(formatError(error, 'Failed to load demo workflow'))
|
setErrorMessage(formatError(error, 'Failed to load demo workflow'))
|
||||||
} finally {
|
} finally {
|
||||||
|
|||||||
+5
-3
@@ -17,9 +17,11 @@ Verify the explicit demo workflow plus export artifact path:
|
|||||||
bash scripts/verify_demo_export_workflow.sh http://192.168.10.150:1202
|
bash scripts/verify_demo_export_workflow.sh http://192.168.10.150:1202
|
||||||
```
|
```
|
||||||
|
|
||||||
The demo/export smoke seeds the offline fixture demo, verifies persisted QA/QC
|
The demo/export smoke is intentionally mutating and idempotent: it seeds the
|
||||||
results, creates metadata/report/vector GeoJSON exports, lists exports and
|
offline fixture demo if needed, verifies the project area GeoJSON, fixture
|
||||||
downloads the JSON/GeoJSON/HTML artifacts through the frontend proxy.
|
datasets, vector FeatureCollection content, vector feature summary, persisted
|
||||||
|
QA/QC metrics, creates metadata/report/vector GeoJSON exports, lists exports
|
||||||
|
and downloads the JSON/GeoJSON/HTML artifacts through the frontend proxy.
|
||||||
|
|
||||||
## Tower deployment
|
## Tower deployment
|
||||||
|
|
||||||
|
|||||||
@@ -72,6 +72,72 @@ curl -fsS -X POST "${BASE_URL%/}/api/v1/demo/workflow" > "${TMP_DIR}/demo.json"
|
|||||||
require_json_data "${TMP_DIR}/demo.json"
|
require_json_data "${TMP_DIR}/demo.json"
|
||||||
project_id="$(json_field "${TMP_DIR}/demo.json" "data.project_id")"
|
project_id="$(json_field "${TMP_DIR}/demo.json" "data.project_id")"
|
||||||
candidate_dataset_id="$(json_field "${TMP_DIR}/demo.json" "data.candidate_dataset_id")"
|
candidate_dataset_id="$(json_field "${TMP_DIR}/demo.json" "data.candidate_dataset_id")"
|
||||||
|
reference_dataset_id="$(json_field "${TMP_DIR}/demo.json" "data.reference_dataset_id")"
|
||||||
|
area_id="$(json_field "${TMP_DIR}/demo.json" "data.area_id")"
|
||||||
|
quality_check_id="$(json_field "${TMP_DIR}/demo.json" "data.quality_check_id")"
|
||||||
|
|
||||||
|
curl -fsS "${BASE_URL%/}/api/v1/projects/${project_id}" > "${TMP_DIR}/project.json"
|
||||||
|
require_json_data "${TMP_DIR}/project.json"
|
||||||
|
|
||||||
|
curl -fsS "${BASE_URL%/}/api/v1/projects/${project_id}/areas" > "${TMP_DIR}/areas.json"
|
||||||
|
require_json_data "${TMP_DIR}/areas.json"
|
||||||
|
"${PYTHON_BIN}" - "${TMP_DIR}/areas.json" "${area_id}" <<'PY'
|
||||||
|
import json
|
||||||
|
import sys
|
||||||
|
|
||||||
|
path, area_id = sys.argv[1], sys.argv[2]
|
||||||
|
with open(path, "r", encoding="utf-8") as handle:
|
||||||
|
payload = json.load(handle)
|
||||||
|
items = payload["data"]["items"]
|
||||||
|
if not items:
|
||||||
|
raise SystemExit("Demo workflow did not expose any project areas")
|
||||||
|
matches = [item for item in items if item["id"] == area_id]
|
||||||
|
if not matches:
|
||||||
|
raise SystemExit(f"Demo area {area_id} was not returned by the area list")
|
||||||
|
geometry = matches[0].get("geometry")
|
||||||
|
if not geometry or geometry.get("type") not in {"Polygon", "MultiPolygon"}:
|
||||||
|
raise SystemExit("Demo area response does not include GeoJSON Polygon/MultiPolygon geometry")
|
||||||
|
PY
|
||||||
|
|
||||||
|
curl -fsS "${BASE_URL%/}/api/v1/projects/${project_id}/datasets" > "${TMP_DIR}/datasets.json"
|
||||||
|
require_json_data "${TMP_DIR}/datasets.json"
|
||||||
|
"${PYTHON_BIN}" - "${TMP_DIR}/datasets.json" "${candidate_dataset_id}" "${reference_dataset_id}" <<'PY'
|
||||||
|
import json
|
||||||
|
import sys
|
||||||
|
|
||||||
|
path, candidate_id, reference_id = sys.argv[1], sys.argv[2], sys.argv[3]
|
||||||
|
with open(path, "r", encoding="utf-8") as handle:
|
||||||
|
payload = json.load(handle)
|
||||||
|
items = payload["data"]["items"]
|
||||||
|
ids = {item["id"]: item for item in items}
|
||||||
|
if candidate_id not in ids:
|
||||||
|
raise SystemExit("Candidate fixture dataset is missing from project dataset list")
|
||||||
|
if reference_id not in ids:
|
||||||
|
raise SystemExit("Reference fixture dataset is missing from project dataset list")
|
||||||
|
if ids[candidate_id]["dataset_type"] not in {"vector", "geojson"}:
|
||||||
|
raise SystemExit("Candidate fixture dataset is not vector-like")
|
||||||
|
if ids[reference_id].get("dataset_role") != "reference":
|
||||||
|
raise SystemExit("Reference fixture dataset is not marked as reference")
|
||||||
|
PY
|
||||||
|
|
||||||
|
curl -fsS "${BASE_URL%/}/api/v1/projects/${project_id}/datasets/${candidate_dataset_id}/content" > "${TMP_DIR}/candidate_content.json"
|
||||||
|
if ! grep -q '"FeatureCollection"' "${TMP_DIR}/candidate_content.json"; then
|
||||||
|
echo "Candidate dataset content is not a FeatureCollection" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
curl -fsS "${BASE_URL%/}/api/v1/projects/${project_id}/datasets/${candidate_dataset_id}/vector/summary" > "${TMP_DIR}/candidate_summary.json"
|
||||||
|
require_json_data "${TMP_DIR}/candidate_summary.json"
|
||||||
|
"${PYTHON_BIN}" - "${TMP_DIR}/candidate_summary.json" <<'PY'
|
||||||
|
import json
|
||||||
|
import sys
|
||||||
|
|
||||||
|
with open(sys.argv[1], "r", encoding="utf-8") as handle:
|
||||||
|
payload = json.load(handle)
|
||||||
|
feature_count = payload["data"].get("feature_count") or 0
|
||||||
|
if feature_count < 1:
|
||||||
|
raise SystemExit("Candidate vector summary does not report persisted features")
|
||||||
|
PY
|
||||||
|
|
||||||
curl -fsS "${BASE_URL%/}/api/v1/projects/${project_id}/quality-checks" > "${TMP_DIR}/quality_checks.json"
|
curl -fsS "${BASE_URL%/}/api/v1/projects/${project_id}/quality-checks" > "${TMP_DIR}/quality_checks.json"
|
||||||
require_json_data "${TMP_DIR}/quality_checks.json"
|
require_json_data "${TMP_DIR}/quality_checks.json"
|
||||||
@@ -80,6 +146,23 @@ if [ "${quality_count}" -lt 1 ]; then
|
|||||||
echo "Expected at least one persisted QA/QC result after demo workflow" >&2
|
echo "Expected at least one persisted QA/QC result after demo workflow" >&2
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
"${PYTHON_BIN}" - "${TMP_DIR}/quality_checks.json" "${quality_check_id}" <<'PY'
|
||||||
|
import json
|
||||||
|
import sys
|
||||||
|
|
||||||
|
path, quality_check_id = sys.argv[1], sys.argv[2]
|
||||||
|
with open(path, "r", encoding="utf-8") as handle:
|
||||||
|
payload = json.load(handle)
|
||||||
|
items = payload["data"]["items"]
|
||||||
|
matches = [item for item in items if item["id"] == quality_check_id]
|
||||||
|
if not matches:
|
||||||
|
raise SystemExit("Seeded quality_check_id is not listed in project QA/QC results")
|
||||||
|
metric_keys = {metric["metric_key"] for metric in matches[0].get("metrics", [])}
|
||||||
|
required = {"precision", "recall", "f1", "mean_iou", "false_positive_count", "false_negative_count"}
|
||||||
|
missing = required - metric_keys
|
||||||
|
if missing:
|
||||||
|
raise SystemExit(f"QA/QC result is missing metrics: {sorted(missing)}")
|
||||||
|
PY
|
||||||
|
|
||||||
curl -fsS -X POST "${BASE_URL%/}/api/v1/exports/metadata" \
|
curl -fsS -X POST "${BASE_URL%/}/api/v1/exports/metadata" \
|
||||||
-H "Content-Type: application/json" \
|
-H "Content-Type: application/json" \
|
||||||
@@ -128,4 +211,6 @@ fi
|
|||||||
|
|
||||||
echo "Demo/export workflow verification passed"
|
echo "Demo/export workflow verification passed"
|
||||||
echo "Project: ${project_id}"
|
echo "Project: ${project_id}"
|
||||||
|
echo "Area: ${area_id}"
|
||||||
|
echo "Datasets: candidate=${candidate_dataset_id}, reference=${reference_dataset_id}"
|
||||||
echo "Exports created: metadata=${metadata_export_id}, report=${report_export_id}, dataset=${dataset_export_id}"
|
echo "Exports created: metadata=${metadata_export_id}, report=${report_export_id}, dataset=${dataset_export_id}"
|
||||||
|
|||||||
Reference in New Issue
Block a user