Initial GeoIntel V1 foundation
This commit is contained in:
@@ -0,0 +1,46 @@
|
||||
# Scripts
|
||||
|
||||
Setup-, import-, demo- en maintenance-scripts voor GeoIntel.
|
||||
|
||||
## Runtime verification
|
||||
|
||||
Verify the browser-facing Docker/LAN runtime:
|
||||
|
||||
```bash
|
||||
bash scripts/verify_browser_runtime.sh http://192.168.10.150:1202
|
||||
bash scripts/verify_gis_runtime.sh http://192.168.10.150:1202
|
||||
```
|
||||
|
||||
Verify the explicit demo workflow plus export artifact path:
|
||||
|
||||
```bash
|
||||
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
|
||||
results, creates metadata/report/vector GeoJSON exports, lists exports and
|
||||
downloads the JSON/GeoJSON/HTML artifacts through the frontend proxy.
|
||||
|
||||
## Tower deployment
|
||||
|
||||
Push the local branch to Gitea, then rebuild the Unraid/Tower Docker runtime:
|
||||
|
||||
```bash
|
||||
bash scripts/deploy_tower.sh
|
||||
```
|
||||
|
||||
For the first deployment into an existing non-Git appdata folder, bootstrap the
|
||||
checkout explicitly:
|
||||
|
||||
```bash
|
||||
DEPLOY_BOOTSTRAP=1 bash scripts/deploy_tower.sh
|
||||
```
|
||||
|
||||
Useful overrides:
|
||||
|
||||
```bash
|
||||
REMOTE_HOST=root@192.168.10.150
|
||||
REMOTE_PATH=/mnt/user/appdata/geointel
|
||||
REMOTE_REPO=gitea-widefrog:NuklearRabbit/geointel.git
|
||||
FRONTEND_URL=http://192.168.10.150:1202
|
||||
```
|
||||
@@ -0,0 +1,14 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
cd "$(dirname "$0")/../backend"
|
||||
if command -v python3 >/dev/null 2>&1 && python3 -m uvicorn --version >/dev/null 2>&1; then
|
||||
PYTHON_BIN="python3"
|
||||
elif command -v python >/dev/null 2>&1 && python -m uvicorn --version >/dev/null 2>&1; then
|
||||
PYTHON_BIN="python"
|
||||
elif command -v python.exe >/dev/null 2>&1 && python.exe -m uvicorn --version >/dev/null 2>&1; then
|
||||
PYTHON_BIN="python.exe"
|
||||
else
|
||||
echo "No python interpreter with uvicorn available" >&2
|
||||
exit 1
|
||||
fi
|
||||
"${PYTHON_BIN}" -m uvicorn app.main:app --reload
|
||||
@@ -0,0 +1,14 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
cd "$(dirname "$0")/../backend"
|
||||
if command -v python3 >/dev/null 2>&1 && python3 -m pip --version >/dev/null 2>&1; then
|
||||
PYTHON_BIN="python3"
|
||||
elif command -v python >/dev/null 2>&1 && python -m pip --version >/dev/null 2>&1; then
|
||||
PYTHON_BIN="python"
|
||||
elif command -v python.exe >/dev/null 2>&1 && python.exe -m pip --version >/dev/null 2>&1; then
|
||||
PYTHON_BIN="python.exe"
|
||||
else
|
||||
echo "No python interpreter with pip available" >&2
|
||||
exit 1
|
||||
fi
|
||||
"${PYTHON_BIN}" -m pip install -e .[dev]
|
||||
@@ -0,0 +1,27 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
cd "$(dirname "$0")/../backend"
|
||||
if [ ! -d .venv ]; then
|
||||
if command -v python3 >/dev/null 2>&1 && python3 -m pytest --version >/dev/null 2>&1; then
|
||||
PYTHON_BIN="python3"
|
||||
elif command -v python >/dev/null 2>&1 && python -m pytest --version >/dev/null 2>&1; then
|
||||
PYTHON_BIN="python"
|
||||
elif command -v python.exe >/dev/null 2>&1 && python.exe -m pytest --version >/dev/null 2>&1; then
|
||||
PYTHON_BIN="python.exe"
|
||||
else
|
||||
echo "No python interpreter with pytest available" >&2
|
||||
exit 1
|
||||
fi
|
||||
"${PYTHON_BIN}" -m pytest --version >/dev/null 2>&1 || "${PYTHON_BIN}" -m pip install -e .[dev]
|
||||
fi
|
||||
if command -v python3 >/dev/null 2>&1 && python3 -m pytest --version >/dev/null 2>&1; then
|
||||
PYTHON_BIN="python3"
|
||||
elif command -v python >/dev/null 2>&1 && python -m pytest --version >/dev/null 2>&1; then
|
||||
PYTHON_BIN="python"
|
||||
elif command -v python.exe >/dev/null 2>&1 && python.exe -m pytest --version >/dev/null 2>&1; then
|
||||
PYTHON_BIN="python.exe"
|
||||
else
|
||||
echo "No python interpreter with pytest available" >&2
|
||||
exit 1
|
||||
fi
|
||||
"${PYTHON_BIN}" -m pytest
|
||||
@@ -0,0 +1,7 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
required=("README.md" "AGENTS.md" "docs/M5_OPERATIONAL_READINESS.md" "docs/BUILD_GOVERNANCE.md" "docs/V1_SCOPE_FREEZE.md" "backend" "frontend" "contracts/api" "contracts/database" "fixtures/geojson" "storage/originals" "storage/derived" "storage/exports")
|
||||
for path in "${required[@]}"; do
|
||||
if [ ! -e "$path" ]; then echo "Missing required path: $path" >&2; exit 1; fi
|
||||
done
|
||||
echo "Repo structure OK"
|
||||
@@ -0,0 +1,29 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
if [ -n "${PYTHON_BIN:-}" ]; then
|
||||
PYTHON_BIN="${PYTHON_BIN}"
|
||||
elif command -v python3 >/dev/null 2>&1 && python3 -c "import sys" >/dev/null 2>&1; then
|
||||
PYTHON_BIN="python3"
|
||||
elif command -v python.exe >/dev/null 2>&1 && python.exe -c "import sys" >/dev/null 2>&1; then
|
||||
PYTHON_BIN="python.exe"
|
||||
else
|
||||
PYTHON_BIN="python"
|
||||
fi
|
||||
|
||||
echo "== GeoIntel pass end check =="
|
||||
|
||||
"$PYTHON_BIN" scripts/smoke_docs.py
|
||||
"$PYTHON_BIN" scripts/validate_fixtures.py
|
||||
"$PYTHON_BIN" scripts/smoke_contracts.py
|
||||
|
||||
if grep -R \
|
||||
--exclude-dir=node_modules \
|
||||
--exclude-dir=dist \
|
||||
--exclude-dir=__pycache__ \
|
||||
"TODO: implement later\|placeholder only\|fake completed" \
|
||||
-n backend frontend docs 2>/dev/null; then
|
||||
echo "Suspicious placeholder completion text found" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "Pass end check OK"
|
||||
@@ -0,0 +1,30 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
echo "== GeoIntel Codex preflight =="
|
||||
|
||||
required=(
|
||||
"AGENTS.md"
|
||||
"docs/CODEX_MASTER_PROMPT.md"
|
||||
"docs/V1_SCOPE_FREEZE.md"
|
||||
"docs/M6_AUTONOMY_BOUNDARIES.md"
|
||||
"docs/M6_QUALITY_GATES.md"
|
||||
"prompts/codex/PASS_00_REPO_AUDIT.md"
|
||||
".env.example"
|
||||
"docker-compose.yml"
|
||||
)
|
||||
|
||||
for path in "${required[@]}"; do
|
||||
if [[ ! -e "$path" ]]; then
|
||||
echo "Missing required file: $path" >&2
|
||||
exit 1
|
||||
fi
|
||||
echo "OK: $path"
|
||||
done
|
||||
|
||||
if grep -R "<<<<<<<\|>>>>>>>\|=======" -n . --exclude-dir=.git --exclude='*.zip'; then
|
||||
echo "Merge conflict markers found" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "Preflight OK"
|
||||
@@ -0,0 +1,11 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
echo "== Lightweight contract drift grep =="
|
||||
|
||||
if grep -R "TODO: decide\|TBD\|placeholder only\|fake real" -n docs contracts backend frontend prompts 2>/dev/null; then
|
||||
echo "Potential unresolved decision markers found. Review output above."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "No obvious unresolved decision markers found."
|
||||
@@ -0,0 +1,48 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
REMOTE_HOST="${REMOTE_HOST:-root@192.168.10.150}"
|
||||
REMOTE_PATH="${REMOTE_PATH:-/mnt/user/appdata/geointel}"
|
||||
REMOTE_BRANCH="${REMOTE_BRANCH:-main}"
|
||||
REMOTE_REPO="${REMOTE_REPO:-gitea-widefrog:NuklearRabbit/geointel.git}"
|
||||
SSH_KEY="${SSH_KEY:-$HOME/.ssh/widefrog_unraid_deploy}"
|
||||
FRONTEND_URL="${FRONTEND_URL:-http://192.168.10.150:1202}"
|
||||
BOOTSTRAP="${DEPLOY_BOOTSTRAP:-0}"
|
||||
|
||||
ssh_opts=(-o BatchMode=yes -o StrictHostKeyChecking=accept-new)
|
||||
if [[ -f "$SSH_KEY" ]]; then
|
||||
ssh_opts+=(-i "$SSH_KEY")
|
||||
fi
|
||||
|
||||
ssh "${ssh_opts[@]}" "$REMOTE_HOST" \
|
||||
"REMOTE_PATH='$REMOTE_PATH' REMOTE_BRANCH='$REMOTE_BRANCH' REMOTE_REPO='$REMOTE_REPO' FRONTEND_URL='$FRONTEND_URL' DEPLOY_BOOTSTRAP='$BOOTSTRAP' bash -s" <<'REMOTE_SCRIPT'
|
||||
set -euo pipefail
|
||||
|
||||
cd "$REMOTE_PATH"
|
||||
|
||||
if [[ ! -d .git ]]; then
|
||||
if [[ "$DEPLOY_BOOTSTRAP" != "1" ]]; then
|
||||
echo "No git checkout found in $REMOTE_PATH."
|
||||
echo "Re-run with DEPLOY_BOOTSTRAP=1 for the first deployment bootstrap."
|
||||
exit 2
|
||||
fi
|
||||
git init
|
||||
git remote add origin "$REMOTE_REPO"
|
||||
fi
|
||||
|
||||
git fetch origin "$REMOTE_BRANCH"
|
||||
git checkout -B "$REMOTE_BRANCH" "origin/$REMOTE_BRANCH"
|
||||
|
||||
docker compose config >/dev/null
|
||||
docker compose build backend frontend
|
||||
docker compose up -d
|
||||
docker compose ps
|
||||
|
||||
if [[ -x scripts/live_migration_smoke.sh ]]; then
|
||||
bash scripts/live_migration_smoke.sh
|
||||
fi
|
||||
|
||||
if [[ -x scripts/verify_browser_runtime.sh ]]; then
|
||||
bash scripts/verify_browser_runtime.sh "$FRONTEND_URL"
|
||||
fi
|
||||
REMOTE_SCRIPT
|
||||
@@ -0,0 +1,11 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
if ! command -v npm >/dev/null 2>&1; then
|
||||
echo "Node/npm is required but was not found in PATH." >&2
|
||||
echo "Please install Node.js 18+ and ensure npm is available." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
cd "$(dirname "$0")/../frontend"
|
||||
npm run build
|
||||
@@ -0,0 +1,11 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
if ! command -v npm >/dev/null 2>&1; then
|
||||
echo "Node/npm is required but was not found in PATH." >&2
|
||||
echo "Please install Node.js 18+ and ensure npm is available." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
cd "$(dirname "$0")/../frontend"
|
||||
npm run start -- --host 0.0.0.0 --port 5173
|
||||
@@ -0,0 +1,11 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
if ! command -v npm >/dev/null 2>&1; then
|
||||
echo "Node/npm is required but was not found in PATH." >&2
|
||||
echo "Please install Node.js 18+ and ensure npm is available." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
cd "$(dirname "$0")/../frontend"
|
||||
npm install
|
||||
@@ -0,0 +1,11 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
if ! command -v npm >/dev/null 2>&1; then
|
||||
echo "Node/npm is required but was not found in PATH." >&2
|
||||
echo "Please install Node.js 18+ and ensure npm is available." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
cd "$(dirname "$0")/../frontend"
|
||||
npm run typecheck
|
||||
@@ -0,0 +1,15 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
BACKEND_SCRIPTS = ROOT / "backend" / "scripts"
|
||||
sys.path.insert(0, str(BACKEND_SCRIPTS))
|
||||
|
||||
from gis_import_smoke import main # noqa: E402
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
@@ -0,0 +1,82 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||
PYTHON_BIN="${PYTHON_BIN:-}"
|
||||
|
||||
if [ -z "$PYTHON_BIN" ]; then
|
||||
for candidate in python3 python.exe python; do
|
||||
if command -v "$candidate" >/dev/null 2>&1 && "$candidate" -c "import sys" >/dev/null 2>&1; then
|
||||
PYTHON_BIN="$candidate"
|
||||
break
|
||||
fi
|
||||
done
|
||||
fi
|
||||
|
||||
if [ -z "$PYTHON_BIN" ]; then
|
||||
echo "No usable Python interpreter found. Set PYTHON_BIN=/path/to/python and retry." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
cd "$ROOT/backend"
|
||||
|
||||
echo "== GeoIntel live migration smoke =="
|
||||
echo "Using Python: $PYTHON_BIN"
|
||||
echo "Database URL is read from backend settings / DATABASE_URL."
|
||||
|
||||
"$PYTHON_BIN" - <<'PY'
|
||||
from sqlalchemy import text
|
||||
|
||||
from app.db.session import get_engine
|
||||
|
||||
with get_engine().connect() as connection:
|
||||
connection.execute(text("SELECT 1"))
|
||||
print("Database connection: ok")
|
||||
PY
|
||||
|
||||
"$PYTHON_BIN" -m alembic upgrade head
|
||||
|
||||
"$PYTHON_BIN" - <<'PY'
|
||||
from sqlalchemy import text
|
||||
|
||||
from app.db.session import get_engine
|
||||
|
||||
required_objects = [
|
||||
"public.projects",
|
||||
"public.areas",
|
||||
"public.datasets",
|
||||
"public.jobs",
|
||||
"public.vector_features",
|
||||
"public.quality_checks",
|
||||
"public.metrics",
|
||||
"public.analysis_runs",
|
||||
"public.detections",
|
||||
"public.segmentations",
|
||||
"public.ix_vector_features_geometry",
|
||||
"public.ix_detections_geometry",
|
||||
"public.ix_segmentations_geometry",
|
||||
]
|
||||
|
||||
with get_engine().connect() as connection:
|
||||
postgis_version = connection.execute(text("SELECT PostGIS_Version()")).scalar()
|
||||
print(f"PostGIS version: {postgis_version}")
|
||||
|
||||
missing = [
|
||||
object_name
|
||||
for object_name in required_objects
|
||||
if connection.execute(text("SELECT to_regclass(:object_name)"), {"object_name": object_name}).scalar() is None
|
||||
]
|
||||
if missing:
|
||||
raise SystemExit(f"Missing expected migrated schema objects: {', '.join(missing)}")
|
||||
print("Required runtime schema objects: ok")
|
||||
PY
|
||||
|
||||
HEAD_COUNT="$("$PYTHON_BIN" -m alembic heads | grep -c 'head')"
|
||||
if [ "$HEAD_COUNT" -ne 1 ]; then
|
||||
echo "Expected exactly one Alembic head, found $HEAD_COUNT" >&2
|
||||
"$PYTHON_BIN" -m alembic heads >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
"$PYTHON_BIN" -m alembic heads
|
||||
echo "== Live migration smoke passed =="
|
||||
@@ -0,0 +1,27 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
echo "== GeoIntel M7 self-review =="
|
||||
|
||||
missing=0
|
||||
for file in \
|
||||
docs/12-build-control/M7_IMPLEMENTATION_CONTROL_LAYER.md \
|
||||
docs/11-quality/SELF_REVIEW_CHECKLIST.md \
|
||||
docs/11-quality/REGRESSION_TRAPS.md \
|
||||
docs/13-implementation-traps/GEOSPATIAL_CALCULATION_RULES.md \
|
||||
docs/13-implementation-traps/API_RESPONSE_RULES.md \
|
||||
docs/12-build-control/CODEX_DECISION_BOUNDARIES.md \
|
||||
docs/12-build-control/BUILD_SEQUENCE_LOCK.md; do
|
||||
if [[ ! -f "$file" ]]; then
|
||||
echo "Missing required M7 file: $file"
|
||||
missing=1
|
||||
else
|
||||
echo "OK: $file"
|
||||
fi
|
||||
done
|
||||
|
||||
if [[ $missing -ne 0 ]]; then
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "M7 control documents present."
|
||||
@@ -0,0 +1,32 @@
|
||||
#!/usr/bin/env python3
|
||||
from pathlib import Path
|
||||
import sys
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
required = [
|
||||
"CODEX_START.md",
|
||||
"README.md",
|
||||
"docs/00-start/START_HERE.md",
|
||||
"docs/20-run-readiness/RUN_READINESS_FINAL.md",
|
||||
"docs/20-run-readiness/PASS_SEQUENCE_FINAL.md",
|
||||
"docs/20-run-readiness/CODEX_TOMORROW_RUNBOOK.md",
|
||||
"docs/governance/GEOINTEL_CONSTITUTION.md",
|
||||
"docs/governance/ARCHITECTURE_INVARIANTS.md",
|
||||
"docs/governance/FORBIDDEN_DECISIONS.md",
|
||||
"docs/specs/CANONICAL_DOMAIN_MODELS.md",
|
||||
"docs/specs/GIS_STANDARDS.md",
|
||||
"docs/specs/STATE_MACHINES.md",
|
||||
"docs/workflows/GOLDEN_PATHS.md",
|
||||
"prompts/codex/final/DAY_1_MASTER_PROMPT.md",
|
||||
"demo/geel/area_geel_center.geojson",
|
||||
"demo/geel/reference_buildings.geojson",
|
||||
"demo/geel/demo_detections.geojson",
|
||||
]
|
||||
missing = [p for p in required if not (ROOT / p).exists()]
|
||||
if missing:
|
||||
print("Preimplementation audit failed. Missing required files:")
|
||||
for p in missing:
|
||||
print(f"- {p}")
|
||||
sys.exit(1)
|
||||
print("Preimplementation audit passed.")
|
||||
print(f"Checked {len(required)} required files.")
|
||||
@@ -0,0 +1,174 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import sys
|
||||
from pathlib import Path
|
||||
from uuid import uuid4
|
||||
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
BACKEND_ROOT = ROOT / "backend"
|
||||
if str(BACKEND_ROOT) not in sys.path:
|
||||
sys.path.insert(0, str(BACKEND_ROOT))
|
||||
|
||||
from app.models import Area, Dataset, Metric, QualityCheck # noqa: E402
|
||||
from app.services.qa_service import QaService # noqa: E402
|
||||
from app.services.quality_service import QualityService # noqa: E402
|
||||
|
||||
|
||||
class BenchmarkSession:
|
||||
def __init__(self, datasets: list[Dataset]) -> None:
|
||||
self.datasets = {dataset.id: dataset for dataset in datasets}
|
||||
self.added: list[object] = []
|
||||
self.commits = 0
|
||||
self.refreshes: list[object] = []
|
||||
|
||||
def get(self, model, item_id):
|
||||
if model.__name__ == "Dataset":
|
||||
return self.datasets.get(item_id)
|
||||
if model.__name__ == "Area":
|
||||
return None
|
||||
return None
|
||||
|
||||
def add(self, item) -> None:
|
||||
self.added.append(item)
|
||||
|
||||
def commit(self) -> None:
|
||||
self.commits += 1
|
||||
|
||||
def refresh(self, item) -> None:
|
||||
self.refreshes.append(item)
|
||||
|
||||
|
||||
def _load_expected() -> dict:
|
||||
return json.loads((ROOT / "fixtures" / "golden" / "expected_qa_metrics.json").read_text(encoding="utf-8"))
|
||||
|
||||
|
||||
def _dataset(dataset_id, project_id, name: str, path: Path, *, role: str) -> Dataset:
|
||||
return Dataset(
|
||||
id=dataset_id,
|
||||
project_id=project_id,
|
||||
name=name,
|
||||
dataset_type="vector",
|
||||
source="golden_fixture",
|
||||
dataset_role=role,
|
||||
source_name="fixture",
|
||||
reference_layer_name="buildings" if role == "reference" else None,
|
||||
storage_path=str(path),
|
||||
crs="EPSG:4326",
|
||||
metadata_json={"crs_assumed": False},
|
||||
status="ready",
|
||||
)
|
||||
|
||||
|
||||
def _assert_close(label: str, actual: float | int | None, expected: float | int, tolerance: float) -> None:
|
||||
if actual is None:
|
||||
raise AssertionError(f"{label} is None, expected {expected}")
|
||||
if abs(float(actual) - float(expected)) > tolerance:
|
||||
raise AssertionError(f"{label} drifted: actual={actual}, expected={expected}, tolerance={tolerance}")
|
||||
|
||||
|
||||
def run_benchmark() -> dict:
|
||||
expected = _load_expected()
|
||||
project_id = uuid4()
|
||||
candidate_dataset_id = uuid4()
|
||||
reference_dataset_id = uuid4()
|
||||
candidate_path = ROOT / expected["candidate_fixture"]
|
||||
reference_path = ROOT / expected["reference_fixture"]
|
||||
tolerance = float(expected["tolerance"])
|
||||
|
||||
session = BenchmarkSession(
|
||||
[
|
||||
_dataset(candidate_dataset_id, project_id, "golden_predicted_buildings.geojson", candidate_path, role="source"),
|
||||
_dataset(reference_dataset_id, project_id, "golden_reference_buildings.geojson", reference_path, role="reference"),
|
||||
]
|
||||
)
|
||||
|
||||
result = QaService.compare_candidate_with_reference(
|
||||
db=session,
|
||||
project_id=project_id,
|
||||
candidate_dataset_id=candidate_dataset_id,
|
||||
reference_dataset_id=reference_dataset_id,
|
||||
iou_threshold=float(expected["iou_threshold"]),
|
||||
)
|
||||
|
||||
metrics = {
|
||||
"precision": result.precision,
|
||||
"recall": result.recall,
|
||||
"f1": result.f1_score,
|
||||
"mean_iou": result.mean_iou,
|
||||
"false_positive_count": result.false_positives,
|
||||
"false_negative_count": result.false_negatives,
|
||||
}
|
||||
|
||||
_assert_close("candidate_feature_count", result.candidate_feature_count, expected["candidate_feature_count"], 0)
|
||||
_assert_close("reference_feature_count", result.reference_feature_count, expected["reference_feature_count"], 0)
|
||||
_assert_close("matches", result.matches, expected["matches"], 0)
|
||||
_assert_close("false_positive_count", result.false_positives, expected["false_positive_count"], 0)
|
||||
_assert_close("false_negative_count", result.false_negatives, expected["false_negative_count"], 0)
|
||||
_assert_close("precision", result.precision, expected["precision"], tolerance)
|
||||
_assert_close("recall", result.recall, expected["recall"], tolerance)
|
||||
_assert_close("f1", result.f1_score, expected["f1"], tolerance)
|
||||
_assert_close("mean_iou", result.mean_iou, expected["mean_iou"], tolerance)
|
||||
|
||||
quality_check = QualityService.persist_quality_check(
|
||||
db=session,
|
||||
project_id=project_id,
|
||||
reference_dataset_id=reference_dataset_id,
|
||||
check_type="golden_candidate_vs_reference",
|
||||
status=result.status,
|
||||
score=result.f1_score,
|
||||
parameters={"iou_threshold": result.iou_threshold, "benchmark_id": expected["benchmark_id"]},
|
||||
findings={
|
||||
"matches": result.matches,
|
||||
"false_positives": result.false_positives,
|
||||
"false_negatives": result.false_negatives,
|
||||
},
|
||||
candidate_dataset_id=candidate_dataset_id,
|
||||
metrics=metrics,
|
||||
)
|
||||
persisted_metrics = [item for item in session.added if isinstance(item, Metric)]
|
||||
|
||||
return {
|
||||
"status": "passed",
|
||||
"benchmark_id": expected["benchmark_id"],
|
||||
"metrics": metrics,
|
||||
"quality_check_id": str(quality_check.id),
|
||||
"persistence": {
|
||||
"quality_check_count": len([item for item in session.added if isinstance(item, QualityCheck)]),
|
||||
"metric_count": len(persisted_metrics),
|
||||
"metric_keys": [metric.metric_key for metric in persisted_metrics],
|
||||
"commit_count": session.commits,
|
||||
},
|
||||
"fixtures": {
|
||||
"candidate": expected["candidate_fixture"],
|
||||
"reference": expected["reference_fixture"],
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
def main() -> int:
|
||||
parser = argparse.ArgumentParser(description="Run GeoIntel golden QA/QC benchmark.")
|
||||
parser.add_argument("--json", action="store_true", help="Print machine-readable JSON only.")
|
||||
args = parser.parse_args()
|
||||
|
||||
try:
|
||||
payload = run_benchmark()
|
||||
except Exception as exc:
|
||||
if args.json:
|
||||
print(json.dumps({"status": "failed", "error": str(exc)}, indent=2))
|
||||
else:
|
||||
print(f"Golden QA/QC benchmark failed: {exc}", file=sys.stderr)
|
||||
return 1
|
||||
|
||||
if args.json:
|
||||
print(json.dumps(payload, indent=2, sort_keys=True))
|
||||
else:
|
||||
print("GeoIntel golden QA/QC benchmark passed")
|
||||
print(json.dumps(payload, indent=2, sort_keys=True))
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
@@ -0,0 +1,49 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||
cd "$ROOT"
|
||||
if [ -n "${PYTHON_BIN:-}" ]; then
|
||||
PYTHON_BIN="${PYTHON_BIN}"
|
||||
else
|
||||
PYTHON_BIN=""
|
||||
for candidate in python3 python.exe python; do
|
||||
if command -v "${candidate}" >/dev/null 2>&1 && "${candidate}" -c "import sys, pytest" >/dev/null 2>&1; then
|
||||
PYTHON_BIN="${candidate}"
|
||||
break
|
||||
fi
|
||||
done
|
||||
if [ -z "${PYTHON_BIN}" ]; then
|
||||
for candidate in python3 python.exe python; do
|
||||
if command -v "${candidate}" >/dev/null 2>&1 && "${candidate}" -c "import sys" >/dev/null 2>&1; then
|
||||
PYTHON_BIN="${candidate}"
|
||||
break
|
||||
fi
|
||||
done
|
||||
fi
|
||||
fi
|
||||
if [ -z "${PYTHON_BIN}" ]; then
|
||||
echo "No usable python interpreter found" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "== GeoIntel run readiness check =="
|
||||
echo "Using Python: ${PYTHON_BIN}"
|
||||
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/preimplementation_audit.py
|
||||
${PYTHON_BIN} scripts/validate_m13_codex_assets.py
|
||||
${PYTHON_BIN} scripts/validate_m14_launch_assets.py
|
||||
${PYTHON_BIN} -m py_compile scripts/gis_import_smoke.py
|
||||
${PYTHON_BIN} -m py_compile scripts/seed_demo_workflow.py
|
||||
${PYTHON_BIN} -m compileall backend/app
|
||||
(cd backend && ${PYTHON_BIN} -m pytest -W error::DeprecationWarning)
|
||||
(cd backend && ${PYTHON_BIN} -m alembic heads)
|
||||
(cd frontend && npm run typecheck)
|
||||
(cd frontend && npm run build)
|
||||
bash -n scripts/live_migration_smoke.sh
|
||||
bash -n scripts/verify_browser_runtime.sh
|
||||
bash -n scripts/verify_demo_export_workflow.sh
|
||||
bash -n scripts/verify_gis_runtime.sh
|
||||
echo "== Run readiness check passed =="
|
||||
@@ -0,0 +1,38 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
sys.path.insert(0, str(ROOT / "backend"))
|
||||
|
||||
from app.db.session import SessionLocal
|
||||
from app.services.demo_workflow_service import DemoWorkflowService
|
||||
|
||||
|
||||
def main() -> int:
|
||||
parser = argparse.ArgumentParser(description="Seed the explicit GeoIntel offline demo workflow.")
|
||||
parser.add_argument("--json", action="store_true", help="Print machine-readable JSON.")
|
||||
args = parser.parse_args()
|
||||
|
||||
with SessionLocal() as db:
|
||||
result = DemoWorkflowService.seed(db)
|
||||
|
||||
payload = result.model_dump(mode="json")
|
||||
if args.json:
|
||||
print(json.dumps(payload, sort_keys=True))
|
||||
else:
|
||||
print(f"Demo workflow status: {payload['status']}")
|
||||
print(f"Project: {payload['project_id']}")
|
||||
print(f"Area: {payload['area_id']}")
|
||||
print(f"Reference dataset: {payload['reference_dataset_id']}")
|
||||
print(f"Candidate dataset: {payload['candidate_dataset_id']}")
|
||||
print(f"Quality check: {payload['quality_check_id']}")
|
||||
print(payload["message"])
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
@@ -0,0 +1,20 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
if [ ! -d "backend" ]; then echo "backend directory missing" >&2; exit 1; fi
|
||||
if [ -n "${PYTHON_BIN:-}" ]; then
|
||||
PYTHON_BIN="${PYTHON_BIN}"
|
||||
elif command -v python3 >/dev/null 2>&1 && python3 -c "import importlib; import app.main" >/dev/null 2>&1; then
|
||||
PYTHON_BIN="python3"
|
||||
elif command -v python.exe >/dev/null 2>&1 && python.exe -c "import importlib; import app.main" >/dev/null 2>&1; then
|
||||
PYTHON_BIN="python.exe"
|
||||
elif command -v python >/dev/null 2>&1 && python -c "import importlib; import app.main" >/dev/null 2>&1; then
|
||||
PYTHON_BIN="python"
|
||||
else
|
||||
echo "No python interpreter available for backend import smoke" >&2
|
||||
exit 1
|
||||
fi
|
||||
if [ -f "backend/app/main.py" ]; then
|
||||
(cd backend && "$PYTHON_BIN" -c "import importlib; import app.main; importlib.import_module('app.main'); print('Backend import OK')")
|
||||
else
|
||||
echo "backend/app/main.py not implemented yet; smoke skipped for documentation-only milestone"
|
||||
fi
|
||||
@@ -0,0 +1,5 @@
|
||||
from pathlib import Path
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
missing=[d for d in ["contracts/api","contracts/database","contracts/events"] if not (ROOT/d).exists()]
|
||||
if missing: raise SystemExit("Missing contract directories: "+", ".join(missing))
|
||||
print("Contracts smoke OK")
|
||||
@@ -0,0 +1,28 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
echo "GeoIntel Day 1 smoke script"
|
||||
echo "This script is intentionally conservative. It should be updated by Codex once package managers and commands exist."
|
||||
|
||||
if [ -d backend ]; then
|
||||
echo "[backend] directory exists"
|
||||
else
|
||||
echo "[backend] missing" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ -d frontend ]; then
|
||||
echo "[frontend] directory exists"
|
||||
else
|
||||
echo "[frontend] missing" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ -f README.md ]; then
|
||||
echo "[docs] README present"
|
||||
else
|
||||
echo "[docs] README missing" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "Smoke scaffold complete. Codex must extend this with real test/build commands during implementation."
|
||||
@@ -0,0 +1,6 @@
|
||||
from pathlib import Path
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
required_docs = ["docs/M5_OPERATIONAL_READINESS.md","docs/BUILD_GOVERNANCE.md","docs/CI_CD_SPECIFICATION.md","docs/HEALTHCHECK_CONTRACTS.md","docs/TROUBLESHOOTING_RUNBOOK.md","docs/RELEASE_PROCESS.md","docs/ROLLBACK_AND_RECOVERY.md"]
|
||||
missing=[p for p in required_docs if not (ROOT/p).exists()]
|
||||
if missing: raise SystemExit("Missing docs:\n"+"\n".join(missing))
|
||||
print("Documentation smoke OK")
|
||||
@@ -0,0 +1,15 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
echo "GeoIntel M10 smoke placeholder"
|
||||
echo "Run backend tests, frontend build, and docs checks after implementation."
|
||||
|
||||
if [ -d backend ]; then
|
||||
echo "backend directory present"
|
||||
fi
|
||||
if [ -d frontend ]; then
|
||||
echo "frontend directory present"
|
||||
fi
|
||||
if [ -f docs/18-ultra-prep/CODEX_START_HERE.md ]; then
|
||||
echo "M10 docs present"
|
||||
fi
|
||||
@@ -0,0 +1,9 @@
|
||||
from pathlib import Path
|
||||
import json
|
||||
ROOT=Path(__file__).resolve().parents[1]
|
||||
fixture_dir=ROOT/"fixtures"/"geojson"
|
||||
if not fixture_dir.exists(): raise SystemExit("fixtures/geojson missing")
|
||||
for path in fixture_dir.glob("*.geojson"):
|
||||
data=json.loads(path.read_text(encoding="utf-8"))
|
||||
if data.get("type") != "FeatureCollection": raise SystemExit(f"{path} is not a FeatureCollection")
|
||||
print("Fixture validation OK")
|
||||
@@ -0,0 +1,43 @@
|
||||
#!/usr/bin/env python3
|
||||
from pathlib import Path
|
||||
import sys
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
required = [
|
||||
"docs/30-codex-optimization/CODEX_OPTIMIZATION_OVERVIEW.md",
|
||||
"docs/30-codex-optimization/CODEX_RUN_CHECKLIST.md",
|
||||
"docs/30-codex-optimization/PROMPT_DISCIPLINE.md",
|
||||
"docs/30-codex-optimization/TOKEN_BUDGET_POLICY.md",
|
||||
"docs/30-codex-optimization/SECRETS_AND_ENV_POLICY.md",
|
||||
"docs/30-codex-optimization/PARALLEL_AGENT_STRATEGY.md",
|
||||
"docs/30-codex-optimization/CODEX_SKILLS_INDEX.md",
|
||||
"prompts/codex/m13/DAY_1_OPTIMIZED_MASTER_PROMPT.md",
|
||||
"prompts/codex/m13/PASS_COMPLETION_REPORT_PROMPT.md",
|
||||
"prompts/codex/m13/PARALLEL_AGENT_COORDINATION_PROMPT.md",
|
||||
]
|
||||
skills = [
|
||||
"geoai-backend-build",
|
||||
"postgis-migration",
|
||||
"raster-pipeline",
|
||||
"vector-processing",
|
||||
"frontend-maplibre-workbench",
|
||||
"qaqc-review",
|
||||
"codex-pass-review",
|
||||
]
|
||||
missing = []
|
||||
for item in required:
|
||||
p = ROOT / item
|
||||
if not p.exists() or p.stat().st_size == 0:
|
||||
missing.append(item)
|
||||
for skill in skills:
|
||||
p = ROOT / "skills" / skill / "SKILL.md"
|
||||
if not p.exists() or p.stat().st_size == 0:
|
||||
missing.append(str(p.relative_to(ROOT)))
|
||||
|
||||
if missing:
|
||||
print("M13 validation failed. Missing/empty assets:")
|
||||
for item in missing:
|
||||
print(f"- {item}")
|
||||
sys.exit(1)
|
||||
|
||||
print("M13 Codex optimization assets OK")
|
||||
@@ -0,0 +1,45 @@
|
||||
from pathlib import Path
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
required = [
|
||||
'docs/40-build-launch/BUILD_SUCCESS_DEFINITION.md',
|
||||
'docs/40-build-launch/SPRINT_1_SCOPE_FREEZE.md',
|
||||
'docs/40-build-launch/DATA_ACQUISITION_PLAYBOOK.md',
|
||||
'docs/40-build-launch/GOLDEN_DATASET_PACKAGE.md',
|
||||
'docs/40-build-launch/BUILD_ORDER_GRAPH.md',
|
||||
'docs/40-build-launch/MODULE_ACCEPTANCE_CRITERIA.md',
|
||||
'docs/40-build-launch/CODEX_STOP_RULES.md',
|
||||
'docs/40-build-launch/RELEASE_STRATEGY.md',
|
||||
'docs/40-build-launch/RISK_REGISTER.md',
|
||||
'docs/40-build-launch/BACKLOG_PRIORITIES_MOSCOW.md',
|
||||
'docs/40-build-launch/FOLDER_OWNERSHIP.md',
|
||||
'prompts/codex/m14/CODEX_FIRST_DAY_MASTER_PROMPT.md',
|
||||
'checklists/SPRINT_1_OPERATOR_CHECKLIST.md',
|
||||
'release/v0.1-foundation-target.md',
|
||||
]
|
||||
|
||||
missing = [p for p in required if not (ROOT / p).exists()]
|
||||
if missing:
|
||||
raise SystemExit('Missing M14 launch assets:\n' + '\n'.join(missing))
|
||||
|
||||
prompt = (ROOT / 'prompts/codex/m14/CODEX_FIRST_DAY_MASTER_PROMPT.md').read_text(encoding='utf-8')
|
||||
required_terms = [
|
||||
'Sprint 1 only',
|
||||
'Do not build yet',
|
||||
'YOLO live inference',
|
||||
'CODEX_STOP_RULES',
|
||||
'BUILD_SUCCESS_DEFINITION',
|
||||
]
|
||||
missing_terms = [term for term in required_terms if term not in prompt]
|
||||
if missing_terms:
|
||||
raise SystemExit('M14 prompt missing expected control terms: ' + ', '.join(missing_terms))
|
||||
|
||||
start = (ROOT / 'CODEX_START.md').read_text(encoding='utf-8')
|
||||
if 'prompts/codex/m14/CODEX_FIRST_DAY_MASTER_PROMPT.md' not in start:
|
||||
raise SystemExit('CODEX_START.md does not point to M14 first-day prompt')
|
||||
|
||||
readme = (ROOT / 'README.md').read_text(encoding='utf-8')
|
||||
if 'M14 — Build Launch Package' not in readme:
|
||||
raise SystemExit('README.md does not identify M14 as current milestone')
|
||||
|
||||
print('M14 launch assets OK')
|
||||
@@ -0,0 +1,48 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
FRONTEND_URL="${1:-http://localhost:1202}"
|
||||
BACKEND_HEALTH_URL="${2:-}"
|
||||
API_URL="${FRONTEND_URL%/}/api/v1/projects"
|
||||
|
||||
if ! command -v curl >/dev/null 2>&1; then
|
||||
echo "curl is required for browser runtime verification" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "== GeoIntel browser runtime verification =="
|
||||
echo "Frontend: ${FRONTEND_URL}"
|
||||
echo "API through frontend proxy: ${API_URL}"
|
||||
|
||||
frontend_status="$(curl -fsS -o /dev/null -w "%{http_code}" "${FRONTEND_URL}")"
|
||||
if [ "${frontend_status}" != "200" ]; then
|
||||
echo "Frontend returned HTTP ${frontend_status}, expected 200" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
api_response="$(curl -fsS "${API_URL}")"
|
||||
case "${api_response}" in
|
||||
*"<!doctype html"*|*"<html"*)
|
||||
echo "Frontend API proxy returned HTML instead of the backend JSON envelope." >&2
|
||||
echo "Rebuild/restart the frontend container so Vite loads the /api proxy config." >&2
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
|
||||
if ! printf '%s' "${api_response}" | grep -q '"data"'; then
|
||||
echo "API response does not look like the canonical GeoIntel envelope:" >&2
|
||||
printf '%s\n' "${api_response}" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ -n "${BACKEND_HEALTH_URL}" ]; then
|
||||
echo "Backend health: ${BACKEND_HEALTH_URL}"
|
||||
backend_health="$(curl -fsS "${BACKEND_HEALTH_URL}")"
|
||||
if ! printf '%s' "${backend_health}" | grep -q '"status":"ok"'; then
|
||||
echo "Backend health response is not healthy:" >&2
|
||||
printf '%s\n' "${backend_health}" >&2
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
|
||||
echo "Browser runtime verification passed"
|
||||
@@ -0,0 +1,131 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
BASE_URL="${1:-http://localhost:1202}"
|
||||
TMP_DIR="$(mktemp -d)"
|
||||
trap 'rm -rf "${TMP_DIR}"' EXIT
|
||||
|
||||
if ! command -v curl >/dev/null 2>&1; then
|
||||
echo "curl is required for demo/export workflow verification" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ -n "${PYTHON_BIN:-}" ]; then
|
||||
PYTHON_BIN="${PYTHON_BIN}"
|
||||
else
|
||||
PYTHON_BIN=""
|
||||
for candidate in python3 python.exe python; do
|
||||
if command -v "${candidate}" >/dev/null 2>&1 && "${candidate}" -c "import json, sys" >/dev/null 2>&1; then
|
||||
PYTHON_BIN="${candidate}"
|
||||
break
|
||||
fi
|
||||
done
|
||||
fi
|
||||
|
||||
if [ -z "${PYTHON_BIN}" ]; then
|
||||
echo "A Python interpreter is required for JSON parsing" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
json_field() {
|
||||
local file_path="$1"
|
||||
local expression="$2"
|
||||
"${PYTHON_BIN}" - "$file_path" "$expression" <<'PY'
|
||||
import json
|
||||
import sys
|
||||
|
||||
path, expression = sys.argv[1], sys.argv[2]
|
||||
with open(path, "r", encoding="utf-8") as handle:
|
||||
payload = json.load(handle)
|
||||
value = payload
|
||||
for part in expression.split("."):
|
||||
if part:
|
||||
value = value[part]
|
||||
print(value)
|
||||
PY
|
||||
}
|
||||
|
||||
require_json_data() {
|
||||
local file_path="$1"
|
||||
"${PYTHON_BIN}" - "$file_path" <<'PY'
|
||||
import json
|
||||
import sys
|
||||
|
||||
with open(sys.argv[1], "r", encoding="utf-8") as handle:
|
||||
payload = json.load(handle)
|
||||
if "data" not in payload:
|
||||
raise SystemExit("Response is not a canonical GeoIntel data envelope")
|
||||
PY
|
||||
}
|
||||
|
||||
echo "== GeoIntel demo/export workflow verification =="
|
||||
echo "Base URL: ${BASE_URL}"
|
||||
|
||||
curl -fsS "${BASE_URL%/}/health" > "${TMP_DIR}/health.json"
|
||||
if ! grep -q '"status":"ok"' "${TMP_DIR}/health.json"; then
|
||||
echo "Health endpoint did not return ok:" >&2
|
||||
cat "${TMP_DIR}/health.json" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
curl -fsS -X POST "${BASE_URL%/}/api/v1/demo/workflow" > "${TMP_DIR}/demo.json"
|
||||
require_json_data "${TMP_DIR}/demo.json"
|
||||
project_id="$(json_field "${TMP_DIR}/demo.json" "data.project_id")"
|
||||
candidate_dataset_id="$(json_field "${TMP_DIR}/demo.json" "data.candidate_dataset_id")"
|
||||
|
||||
curl -fsS "${BASE_URL%/}/api/v1/projects/${project_id}/quality-checks" > "${TMP_DIR}/quality_checks.json"
|
||||
require_json_data "${TMP_DIR}/quality_checks.json"
|
||||
quality_count="$(json_field "${TMP_DIR}/quality_checks.json" "data.total")"
|
||||
if [ "${quality_count}" -lt 1 ]; then
|
||||
echo "Expected at least one persisted QA/QC result after demo workflow" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
curl -fsS -X POST "${BASE_URL%/}/api/v1/exports/metadata" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d "{\"project_id\":\"${project_id}\"}" > "${TMP_DIR}/metadata_export.json"
|
||||
require_json_data "${TMP_DIR}/metadata_export.json"
|
||||
metadata_export_id="$(json_field "${TMP_DIR}/metadata_export.json" "data.export_id")"
|
||||
|
||||
curl -fsS -X POST "${BASE_URL%/}/api/v1/exports/report" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d "{\"project_id\":\"${project_id}\"}" > "${TMP_DIR}/report_export.json"
|
||||
require_json_data "${TMP_DIR}/report_export.json"
|
||||
report_export_id="$(json_field "${TMP_DIR}/report_export.json" "data.export_id")"
|
||||
|
||||
curl -fsS -X POST "${BASE_URL%/}/api/v1/exports/geojson" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d "{\"export_kind\":\"dataset\",\"dataset_id\":\"${candidate_dataset_id}\"}" > "${TMP_DIR}/dataset_export.json"
|
||||
require_json_data "${TMP_DIR}/dataset_export.json"
|
||||
dataset_export_id="$(json_field "${TMP_DIR}/dataset_export.json" "data.export_id")"
|
||||
|
||||
curl -fsS "${BASE_URL%/}/api/v1/exports/projects/${project_id}/exports" > "${TMP_DIR}/exports.json"
|
||||
require_json_data "${TMP_DIR}/exports.json"
|
||||
export_count="$(json_field "${TMP_DIR}/exports.json" "data.total")"
|
||||
if [ "${export_count}" -lt 3 ]; then
|
||||
echo "Expected at least three exports after workflow smoke, found ${export_count}" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
curl -fsS "${BASE_URL%/}/api/v1/exports/${metadata_export_id}/content" > "${TMP_DIR}/metadata_content.json"
|
||||
require_json_data "${TMP_DIR}/metadata_content.json"
|
||||
if ! grep -q '"quality_checks"' "${TMP_DIR}/metadata_content.json"; then
|
||||
echo "Metadata export content does not include QA/QC summary" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
curl -fsS "${BASE_URL%/}/api/v1/exports/${dataset_export_id}/download" > "${TMP_DIR}/dataset_download.geojson"
|
||||
if ! grep -q '"FeatureCollection"' "${TMP_DIR}/dataset_download.geojson"; then
|
||||
echo "Dataset GeoJSON download is not a FeatureCollection" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
curl -fsS "${BASE_URL%/}/api/v1/exports/${report_export_id}/download" > "${TMP_DIR}/report_download.html"
|
||||
if ! grep -qi '<!doctype html>' "${TMP_DIR}/report_download.html"; then
|
||||
echo "Report download is not an HTML artifact" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "Demo/export workflow verification passed"
|
||||
echo "Project: ${project_id}"
|
||||
echo "Exports created: metadata=${metadata_export_id}, report=${report_export_id}, dataset=${dataset_export_id}"
|
||||
@@ -0,0 +1,40 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
BASE_URL="${1:-${GE_INTEL_BASE_URL:-http://localhost:1202}}"
|
||||
CAPABILITIES_URL="${BASE_URL%/}/api/v1/system/capabilities"
|
||||
|
||||
echo "== GeoIntel GIS runtime verification =="
|
||||
echo "Checking: ${CAPABILITIES_URL}"
|
||||
|
||||
if ! command -v curl >/dev/null 2>&1; then
|
||||
echo "curl is required for GIS runtime verification" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
response="$(curl -fsS "${CAPABILITIES_URL}")"
|
||||
|
||||
if printf '%s' "${response}" | grep -qi '<!doctype html'; then
|
||||
echo "Capabilities endpoint returned HTML; frontend proxy/API routing is not correct" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if ! printf '%s' "${response}" | grep -q '"postgis":true'; then
|
||||
echo "PostGIS capability is not reported as available" >&2
|
||||
printf '%s\n' "${response}" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if ! printf '%s' "${response}" | grep -q '"rasterio":true'; then
|
||||
echo "Rasterio capability is not reported as available in the running backend image" >&2
|
||||
printf '%s\n' "${response}" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if ! printf '%s' "${response}" | grep -q '"geopandas":true'; then
|
||||
echo "GeoPandas capability is not reported as available in the running backend image" >&2
|
||||
printf '%s\n' "${response}" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "GIS runtime capabilities are available."
|
||||
@@ -0,0 +1,52 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
BACKEND_ROOT = ROOT / "backend"
|
||||
if str(BACKEND_ROOT) not in sys.path:
|
||||
sys.path.insert(0, str(BACKEND_ROOT))
|
||||
|
||||
from app.core.config import Settings # noqa: E402
|
||||
from app.services.yolo_preflight_service import YoloPreflightService # noqa: E402
|
||||
|
||||
|
||||
def main() -> int:
|
||||
parser = argparse.ArgumentParser(description="Run local YOLO configuration preflight without loading a model.")
|
||||
parser.add_argument("--model-path", help="Existing local YOLO model path.")
|
||||
parser.add_argument("--tile-manifest-path", help="Existing raster tile manifest path.")
|
||||
parser.add_argument("--enabled", action="store_true", help="Treat YOLO as enabled for this preflight.")
|
||||
parser.add_argument("--max-tiles", type=int, default=100, help="Maximum tile count allowed by preflight.")
|
||||
parser.add_argument(
|
||||
"--assume-dependencies",
|
||||
action="store_true",
|
||||
help="Skip checking installed ultralytics/torch packages; useful for validating local paths on non-AI machines.",
|
||||
)
|
||||
parser.add_argument("--json", action="store_true", help="Print JSON output only.")
|
||||
args = parser.parse_args()
|
||||
|
||||
settings = Settings(
|
||||
yolo_enabled=args.enabled or bool(args.model_path),
|
||||
yolo_model_path=args.model_path,
|
||||
yolo_max_tiles=args.max_tiles,
|
||||
)
|
||||
payload = YoloPreflightService.run(
|
||||
settings=settings,
|
||||
tile_manifest_path=args.tile_manifest_path,
|
||||
assume_dependencies=args.assume_dependencies,
|
||||
)
|
||||
|
||||
if args.json:
|
||||
print(json.dumps(payload, indent=2, sort_keys=True))
|
||||
else:
|
||||
print("GeoIntel YOLO preflight")
|
||||
print(json.dumps(payload, indent=2, sort_keys=True))
|
||||
return 0 if payload["status"] in {"ready", "not_configured", "dependency_unavailable"} else 1
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
Reference in New Issue
Block a user