Files
geointel/backend/README.md
T
Jens faeb58ef6d
GeoIntel release gates / Compile, test, contracts and builds (push) Successful in 1m49s
GeoIntel release gates / Python and npm vulnerability policy (push) Successful in 21s
GeoIntel release gates / Production AI image, SBOM and container scan (push) Successful in 5m39s
GeoIntel release gates / Deploy exact gated revision to Unraid (push) Failing after 58m43s
Initial public release
2026-08-31 21:56:53 +02:00

2036 lines
88 KiB
Markdown

# GeoIntel Backend (Sprint 3 foundation layer)
FastAPI backend for the GeoIntel Belgium and Belgian North Sea workbench.
Runtime probes:
- `GET /health/live`: process liveness, always independent from PostgreSQL.
- `GET /health/ready`: fail-closed database/PostGIS/migration/storage
readiness used by Docker.
- `GET /health`: compatibility alias for readiness.
- `GET /api/v1/system/capabilities`: runtime-derived PostGIS, GIS dependency,
configured YOLO and provider state.
The all-in-one production runtime enables interrupted job/analysis-run
reconciliation at startup. Local tests and development leave it disabled
unless `GEOINTEL_RECONCILE_INTERRUPTED_RUNS_ON_STARTUP=true`.
The map-first explorer uses the existing persisted vector selection endpoint. Its bounded GeoJSON preview reports `feature_count`, while `total_feature_count` reports the exact PostGIS intersection count before the 1,000-feature response cap. When an active `area_id` is supplied, vector, temporal, derived-dataset and export paths all use `bbox ∩ Area`. A full-work-area bbox resolves to the exact persisted Area geometry; a boundary-crossing rectangle is clipped to the official boundary.
## Scope implemented
- Project CRUD
- Area CRUD with PostGIS geometry
- Vector and raster dataset upload/registration
- Deterministic local storage metadata capture
- PostGIS migration and database foundation
- Job foundation for async-ready GIS operations
## Sprint 2 additions
- Dataset typing and lifecycle support:
- `uploaded`
- `validating`
- `ready`
- `failed`
- Vector metadata extraction:
- feature count
- geometry type summary
- bounds
- approximate area
- CRS and CRS assumption
- Raster metadata endpoint:
- returns raster profile when `rasterio` is available
- returns clear `RASTER_PROCESSING_UNAVAILABLE` error when dependency is missing
- Deterministic storage metadata capture:
- original filename
- stored filename
- MIME/content type
- size bytes
- checksum SHA-256
## Sprint 3 additions
- Lightweight job architecture:
- `jobs` table and migrations
- job create/list/read/status API
- synchronous execution behind job abstraction
- Vector operations foundation:
- inspect
- bbox
- stats
- clip by area
- buffer
- intersect
- invalid geometry rejection with typed errors
- Raster operation foundation:
- inspect
- metadata
- preview readiness
- clip by area (dependency-aware with unavailable fallback)
- tile generation with manifest output
- real preview image generation when dependencies are installed
## Sprint 4 additions
- Raster foundation is now implemented with real extraction and deterministic artifact outputs:
- metadata returns width, height, band count, CRS, bounds, resolution, dtype, nodata, transform
- preview endpoint generates and reuses PNG previews with width/height
- clip operation persists a derived raster dataset with:
- `source_dataset_id`
- `operation`
- `operation_parameters`
- tile operation writes deterministic raster tiles under `tiles/{project_id}/{source_dataset_id}/{tile_set_id}`
- tile manifest includes tile path, pixel window, bounds, transform, and count
- Dependency behavior:
- when `rasterio` is missing, raster processing returns `RASTER_PROCESSING_UNAVAILABLE`
- preview endpoint additionally requires numpy/pillow and returns `RASTER_PROCESSING_UNAVAILABLE` when missing
## Sprint 5 additions
- Raster analytics hardening:
- raster band statistics now include:
- min, max, mean, std
- nodata count and ratio
- valid pixel count
- dtype
- optional histogram bins (default 16 bins)
- raster reproject operation implemented (CRS transform + rasterio reprojection) using dependency-aware raster processing checks.
- reproject failures are explicit (`INVALID_PARAMETERS`, `INVALID_DATASET_CRS`, `RASTER_PROCESSING_UNAVAILABLE`).
- Raster clip and tile hardening:
- clip validates area presence and CRS alignment constraints.
- tile manifest records `tile_set_id`, `tile_size`, `overlap`, `source_dataset_id`, `source_raster_id`, bounds, parameters, count, tile paths, `ai_inference`, and `tile_server`.
- Job result persistence for raster ops:
- raster clip/reproject/tile job payloads persist derived dataset references when outputs are produced.
## Sprint 6 additions
- Added local spectral index operations:
- NDVI endpoint: `POST /raster/indices/ndvi`
- NDWI endpoint: `POST /raster/indices/ndwi`
- NDBI endpoint: `POST /raster/indices/ndbi`
- Spectral index input validation:
- band parameters must be positive integers
- band parameters must exist in source raster band count
- Dependency-aware execution:
- returns `RASTER_PROCESSING_UNAVAILABLE` when rasterio or numpy are unavailable
- Real index output handling:
- local windowed float32 GeoTIFF generation
- `NaN` strategy for invalid pixels / division by zero
- Provenance capture for derived index datasets:
- `source_dataset_id`, `operation`, `band_mapping`, `formula`
- `output_dtype`, `nodata_strategy`, `value_range_note`
- `output_dataset_id`, `created_at`, `path`
## Sprint 7B additions
- Added provider registry skeleton for `grb`, `osm`, `manual` and `fixture`.
- Added provider capability endpoints:
- `GET /api/v1/external/providers`
- `GET /api/v1/external/providers/{provider_name}`
- `GET /api/v1/external/providers/{provider_name}/layers`
- `GET /api/v1/external/providers/{provider_name}/status`
- `POST /api/v1/external/providers/{provider_name}/import`
- GRB and OSM imports return explicit `not_configured` responses; no live WFS or Overpass calls are made.
- Manual and fixture providers describe existing upload/fixture flows only.
- Added live PostGIS migration smoke script for environments with a real database:
```bash
bash scripts/live_migration_smoke.sh
```
## Sprint 8 additions
- Added Detection Lab foundation:
- `detections` ORM model and Alembic migration with PostGIS geometry storage.
- hardened `analysis_runs` for dataset/job/model/result metadata.
- model registry capability service for `yolo-placeholder` and `manual-fixture-detector`.
- detection service boundary for creating jobs, analysis runs and dependency-aware unavailable responses.
- Added detection endpoints:
- `GET /api/v1/detection/models`
- `GET /api/v1/detection/model-assets`
- `POST /api/v1/detection/run`
- `POST /api/v1/detection/run-async` (production browser path)
- `GET /api/v1/detection/runs/{analysis_run_id}`
- `GET /api/v1/detection/runs/{analysis_run_id}/detections`
- YOLO/PyTorch real inference is not enabled in Sprint 8.
- Fixture detector mode is test/demo-only and requires explicit `fixture_mode=true`.
## Sprint 8B additions
- Added optional configured YOLO integration foundation:
- `yolo-configured` model registry capability.
- import-safe adapter for local Ultralytics model files.
- raster tile manifest validation and tile limit enforcement.
- pixel bbox to EPSG:4326 detection polygon conversion.
- persisted detections through the existing detection/job/analysis-run path.
- YOLO dependencies are optional extras and are not required for backend startup.
- GeoIntel does not download YOLO model weights automatically.
## Sprint 8C additions
- Added detection visualization/review API support:
- list detection runs
- list detections by run or dataset with class/confidence filters
- get detection detail
- return persisted detections as GeoJSON FeatureCollections
- Added detection QA against reference vector datasets:
- compares persisted detection geometries against persisted `vector_features`
- persists `quality_checks` and `metrics`
- returns precision, recall, F1, mean IoU and false positive/negative counts
- configured-YOLO runs clip both QA populations to persisted tile-manifest
coverage before matching and fail closed on missing/mismatched coverage
provenance
- persists a diagnostic-only candidate-box versus reference-envelope pass so
box-to-footprint matching artifacts are visible without altering canonical
footprint-IoU metrics
- Segmentation, LiDAR, AI Copilot, Training Studio and Reports remain out of scope.
## Sprint 9 additions
- Added Segmentation Lab foundation:
- `segmentations` ORM model and Alembic migration with PostGIS MultiPolygon geometry storage.
- segmentation model registry capabilities for `segmentation-placeholder`, `fixture-segmenter`, `yolo-seg-configured` and `sam-configured`.
- segmentation service boundary for creating jobs, analysis runs and unavailable model responses.
- explicit fixture segmenter mode for tests/demo fixtures only.
- Added segmentation endpoints:
- `GET /api/v1/segmentation/models`
- `POST /api/v1/segmentation/run`
- `POST /api/v1/segmentation/run-async` (production browser path)
- `GET /api/v1/segmentation/runs`
- `GET /api/v1/segmentation/runs/{analysis_run_id}`
- `GET /api/v1/segmentation/runs/{analysis_run_id}/segmentations`
- `GET /api/v1/segmentation/runs/{analysis_run_id}/geojson`
- `POST /api/v1/segmentation/runs/{analysis_run_id}/qa/reference`
- Real SAM and YOLO-seg inference are not enabled in Sprint 9.
- Mask paths are provenance/debug artifacts; persisted PostGIS geometry is authoritative for QA, map display and GeoJSON.
- Current configured detection and segmentation run through the async analysis
worker (`GEOINTEL_ANALYSIS_WORKER_ENABLED`) and are followed through
`GET /api/v1/projects/{project_id}/jobs/{job_id}`. The Unraid profile sets
`YOLO_REQUIRE_CUDA=true`, so both pipelines fail closed instead of silently
falling back from NVIDIA CUDA to CPU.
## Sprint 17 additions
- Added export foundation backed by the existing `exports` table.
- GeoJSON exports now persist export records and write JSON artifacts for:
- vector datasets
- detection analysis runs
- segmentation analysis runs
- Added project metadata JSON export for project, dataset and QA/QC summary state.
- Added export read/list/content endpoints:
- `POST /api/v1/exports/geojson`
- `POST /api/v1/exports/map-result`
- `POST /api/v1/exports/metadata`
- `GET /api/v1/exports/projects/{project_id}/exports`
- `GET /api/v1/exports/{export_id}`
- `GET /api/v1/exports/{export_id}/content`
- Exported detection and segmentation GeoJSON is generated from persisted first-class geometry rows.
- No new migrations, product lines, live providers or AI dependencies are introduced by this export pass.
- Map-result exports recompute current governed vector/raster selections or
historical comparisons on the backend before persisting the artifact. Client
metrics are never accepted as authoritative export content.
- Old offline demo export artifacts can be inspected with `python scripts/cleanup_demo_artifacts.py`
and removed only with an explicit `--apply`. The script keeps the newest exports
per demo project, refuses to delete files outside `STORAGE_ROOT`, and blocks
apply runs above `--max-delete` until the cap is raised after a dry-run review.
Use repeated `--export-type` values to target only specific artifact kinds.
In Docker, use `docker compose exec -T backend python scripts/cleanup_demo_artifacts.py`.
- Live cleanup validation is available with `bash scripts/verify_demo_cleanup_dry_run.sh`.
It runs the same maintenance path without `--apply` and fails if the summary
reports anything other than a dry-run with zero deleted exports/files.
## Run locally
### Prerequisites
- Python 3.11+
- PostgreSQL with PostGIS
### Install dependencies
```bash
cd backend
python -m pip install -e .[dev]
```
Optional AI dependencies for configured local YOLO inference:
```bash
cd backend
python -m pip install -e .[ai]
```
Docker and Unraid builds keep AI dependencies disabled by default. To build an
image with local PyTorch/Ultralytics support, set:
```bash
GEOINTEL_INSTALL_AI=true
```
The default remains `false` so normal GIS deployments do not install the large AI
runtime. GeoIntel still requires an explicit local model path and never downloads
weights automatically.
AI-enabled Docker images include the native OpenCV runtime libraries required by
Ultralytics. Dependency availability is checked with real `torch` and
`ultralytics` imports, so missing shared libraries are reported as
`dependency_unavailable` instead of being treated as configured.
Docker/Unraid runtimes set `YOLO_CONFIG_DIR` to a writable storage path so
Ultralytics does not attempt to write settings under the root user config
directory.
Configured YOLO requires:
```bash
YOLO_ENABLED=true
YOLO_MODELS_DIR=/absolute/path/to/models
YOLO_MODEL_PATH=/absolute/path/to/local-model.pt
```
Optional local model compatibility smoke:
```bash
python scripts/yolo_preflight.py --model-path /absolute/path/to/local-model.pt --tile-manifest-path /absolute/path/to/manifest.json --check-model-load --json
```
In Docker, run the same smoke through the backend container:
```bash
docker compose exec -T backend python scripts/yolo_preflight.py --model-path /absolute/path/to/local-model.pt --tile-manifest-path /absolute/path/to/manifest.json --check-model-load --json
```
In the all-in-one Unraid runtime, place model files under the configured models
directory, mounted as `/app/models` by default:
```bash
GEOINTEL_MODELS_PATH=/mnt/user/appdata/geointel/models
YOLO_ENABLED=true
YOLO_MODELS_DIR=/app/models
YOLO_MODEL_PATH=/app/models/local-model.pt
```
The root helper can write those values safely after a local model is placed:
```bash
python scripts/configure_yolo_model.py \
--models-dir /mnt/user/appdata/geointel/models \
--env-file /mnt/user/appdata/geointel/.env \
--apply
```
When a promotion report recommends an exact model/tile/threshold candidate,
prefer the guarded activation helper. It validates the report, checks the local
model asset and writes `.env` only when `--apply` is supplied:
```bash
python scripts/activate_promoted_yolo_candidate.py \
--promotion-report /mnt/user/appdata/geointel/artifacts/detection-model-promotion/split-aware/aoi1024bg512r3e50-high-threshold-split-20260710T222934Z/detection_model_promotion_report.json \
--candidate-key 'geointel-building-yolov8s-aoi1024bg512r3e50-pt|512|64|0.35' \
--models-dir /mnt/user/appdata/geointel/models \
--env-file /mnt/user/appdata/geointel/.env \
--json
```
Add `--apply` only after reviewing the emitted env updates. The smoke and
activation helpers load no model by default, run no inference and do not
download weights. Restart or rebuild the runtime after applying because the
active model is read from `YOLO_MODEL_PATH`.
Operator-only local training preparation is available when real public model
candidates are too weak for the target imagery. It is not a browser feature and
does not change API contracts:
```bash
docker exec -it geointel python3 /app/scripts/export_operator_yolo_dataset.py \
--manifest-path /app/storage/operator-data/operator_samples_manifest.json \
--output-dir /app/storage/operator-data/yolo-building-dataset \
--val-samples turnhout \
--force
```
In an AI-enabled runtime with an existing local base model:
```bash
docker exec \
-e OPERATOR_YOLO_DATASET_DIR=/app/storage/operator-data/yolo-building-dataset \
-e YOLO_BASE_MODEL_PATH=/app/models/yolov8n.pt \
-e TRAIN_MODEL_OUTPUT_PATH=/app/models/geointel-building-detector.pt \
-e TRAIN_EPOCHS=8 \
-e TRAIN_IMGSZ=512 \
-e TRAIN_BATCH=2 \
-e TRAIN_WORKERS=0 \
-e TRAIN_DEVICE=cpu \
-e PYTHON_BIN=python3 \
geointel bash /app/scripts/train_operator_yolo_detector.sh
```
The exporter creates a YOLO `dataset.yaml` plus image/label folders from the
explicit operator sample manifest. The training wrapper writes
`training_summary.json` and a local `.pt` artifact, which still must be
validated through model preflight and the real-data QA matrix before use.
For a larger tile-level training set, use overlapping windows instead of one
image per AOI:
```bash
docker exec -it geointel python3 /app/scripts/export_operator_yolo_tile_dataset.py \
--manifest-path /app/storage/operator-data/operator_samples_manifest.json \
--output-dir /app/storage/operator-data/yolo-building-tile-dataset \
--tile-size 192 \
--stride 96 \
--negative-keep-ratio 0.5 \
--val-samples turnhout \
--force
```
Then point `OPERATOR_YOLO_DATASET_DIR` at
`/app/storage/operator-data/yolo-building-tile-dataset` and keep the same
training wrapper. Tile-level output remains operator tooling outside the V1
browser product.
Use `--samples` (or `OPERATOR_YOLO_SAMPLES`) when an experiment needs a
deliberate manifest subset. The generated summary records the source manifest
count plus selected and excluded sample slugs. Unknown samples and any selected
manifest holdout that is omitted from `--val-samples` fail before files are
written.
The backend also exposes a read-only model asset catalog for the mounted model
directory:
```bash
curl http://localhost:1202/api/v1/detection/model-assets
```
The catalog lists local `.pt`, `.onnx` and `.engine` files with size, SHA-256
and active-model status. Detection runs may submit `model_asset_id` with
`model_id="yolo-configured"` to use a cataloged local model for that run. The
backend resolves the ID to a file inside `YOLO_MODELS_DIR`; browser clients do
not send arbitrary model paths.
Configured YOLO inference uses raster tile artifacts from the existing tile
manifest flow. Single-band or otherwise non-RGB tile images are converted to a
temporary RGB prediction image before inference; georeferencing still comes
from the persisted tile manifest transform/bounds metadata.
Optional tuning:
```bash
YOLO_MODEL_ID=yolo-configured
YOLO_MODEL_DISPLAY_NAME="Configured YOLO detector"
YOLO_MODEL_VERSION=local-v1
YOLO_MODELS_DIR=/app/models
YOLO_CONFIG_DIR=/app/storage/ultralytics
YOLO_DEVICE=cpu
YOLO_IMAGE_SIZE=640
YOLO_MAX_TILES=100
YOLO_MAX_DETECTIONS=1000
YOLO_DUPLICATE_IOU_THRESHOLD=0.5
YOLO_BATCH_SIZE=1
```
### YOLO local preflight
Sprint 13 adds a local-only preflight for configured YOLO paths:
```bash
python scripts/yolo_preflight.py --model-path /absolute/path/to/local-model.pt --tile-manifest-path /absolute/path/to/manifest.json
```
Machine-readable output:
```bash
python scripts/yolo_preflight.py --model-path /absolute/path/to/local-model.pt --tile-manifest-path /absolute/path/to/manifest.json --json
```
To validate only local model/manifest paths on a machine without optional AI dependencies:
```bash
python scripts/yolo_preflight.py --model-path /absolute/path/to/local-model.pt --tile-manifest-path /absolute/path/to/manifest.json --assume-dependencies --json
```
The preflight checks configuration, dependency availability, local model file existence, tile manifest validity, tile count and referenced tile paths. JSON output also includes runtime diagnostics for the model directory, `YOLO_CONFIG_DIR`, installed `torch`/`ultralytics` versions and CUDA availability when dependency checks pass. It does not load a YOLO model, run inference or download weights.
`YOLO_MAX_DETECTIONS` is forwarded to Ultralytics as `max_det`. The default is
`1000` because dense building AOIs can exceed the upstream default cap of 300
detections before QA/QC can measure recall honestly.
`YOLO_DUPLICATE_IOU_THRESHOLD` controls GeoIntel-side cross-tile duplicate
suppression after YOLO pixel boxes are converted to EPSG:4326 polygons and
before `Detection` rows are persisted. Candidates are sorted by confidence per
class; lower-confidence same-class candidates with geometry IoU greater than or
equal to the threshold are suppressed. The default is `0.5`; set `0` to disable
this post-processing for debugging.
The same read-only status is available through the API and Detection Lab UI:
```bash
curl http://localhost:1202/api/v1/detection/yolo/preflight
```
To validate the full configured-YOLO runtime path against Docker/Tower after a
model is mounted and selected, run:
```bash
bash scripts/verify_model_asset_detection_workflow.sh http://192.0.2.10:1202
```
The smoke uses the existing demo raster to generate a tile manifest, selects a
cataloged local model asset, verifies read-only preflight, submits the existing
detection run endpoint and checks persisted AnalysisRun, Detection list and
Detection GeoJSON output. It does not download weights or inject detector
fixtures. A zero detection result is still a valid runtime smoke outcome on the
synthetic demo raster.
To validate the configured building model on operator-provided GIS data, mount
or copy a real georeferenced raster and a real reference-building GeoJSON onto
the runtime host, then run:
```bash
REAL_RASTER_PATH=/mnt/user/appdata/geointel/data/orthophoto.tif \
REAL_REFERENCE_VECTOR_PATH=/mnt/user/appdata/geointel/data/reference-buildings.geojson \
bash scripts/verify_real_data_detection_qa_workflow.sh http://192.0.2.10:1202
```
This smoke refuses missing/unsupported inputs, uploads the raster and reference
dataset through the normal dataset service, generates raster tiles, selects a
local model asset, runs configured YOLO detection, compares persisted
detections against persisted `vector_features`, persists QA/QC rows and exports
the detection GeoJSON. It never seeds demo detections, enables fixture mode,
fetches live providers or downloads model weights. Configured-YOLO model class
labels are normalized to lowercase for filtering and persisted detections while
the original model label is retained in detection provenance. Raster tile
manifests generated for AI handoff include source CRS metadata so pixel-space
model outputs can be transformed to WGS84 GeoJSON coordinates. Current V1 upload
support is limited to GeoTIFF-style rasters and GeoJSON/JSON reference vectors.
When `REAL_AREA_BBOX=minx,miny,maxx,maxy` is supplied, the same workflow also
persists an EPSG:4326 project Area before uploading data. `REAL_AREA_NAME` and
`REAL_PROJECT_REGION` retain operator context. The multi-sample runner fills
these values from manifest `wgs84_bbox` and municipality metadata, so generated
projects are immediately usable in the map without an alternate persistence
path or API contract.
To prepare the documented operator sample corpus inside the all-in-one runtime
container, run:
```bash
docker exec -it geointel python3 /app/scripts/prepare_operator_real_data_samples.py
```
The helper writes GeoTIFF orthophotos, GRB GBG building GeoJSON files and
`operator_samples_manifest.json` under `/app/storage/operator-data`. In
addition to the established positive and background AOIs, the registry contains
Beerse, Rijkevorsel, Hoogstraten and Vorselaar as focused small-building
training AOIs. Vosselaar and Grobbendonk are independent validation AOIs and
must not be exported into the training split. Background candidates can persist
empty GRB FeatureCollections for negative-tile training; normal reference AOIs
still fail when GRB returns no buildings. These are runtime artifacts only and
are not committed to Git.
Mol additionally has operational holdouts for Achterbos, Gompel, Donk and
Postel, with Mol center as the historical baseline and Postel-bos as a separate
background control. Prepare and execute that pack with the documented
`prepare_operator_real_data_samples.py` and
`run_mol_operational_validation.sh` commands in `scripts/README.md`. The runner
produces a coverage-aware operational decision report: canonical footprint-IoU
metrics remain authoritative, reference-envelope matches remain diagnostic,
and no report can activate or mutate a model asset.
For municipality-wide navigation, run
`/app/scripts/provision_mol_municipality_workspace.py` inside the all-in-one
container. It verifies the official Mol boundary (NIS `13025`), pages and clips
all GRB GBG buildings, records checksums/provenance under persistent operator
storage and imports both datasets through the existing HTTP service boundary.
The command is explicit and idempotent; it is never executed during backend
startup. See `scripts/README.md` for exact usage and refresh controls.
The current recommended local building model is
`geointel-building-yolov8s-smallbld-minpx3-img640-ft30-pt` with tile size
`512`, overlap `64` and confidence threshold `0.15`. Its SHA256 is
`a9088b8491dfae36694b53e9e9406cb4e3511d334a5712fa34f75078a47759c1`.
The promotion evidence covers seven positive AOIs at QA match IoU `0.25` and
three pure-empty background AOIs. The model improves recall and persisted
false-negative counts, but has lower precision than the previous balanced
model; operators must review and persist QA/QC rather than treating detections
as ground truth.
The latest coverage-aligned rerun of this exact profile measured mean precision
`0.6141`, recall `0.6062` and F1 `0.6069` over Mol Achterbos, Donk, Gompel and
Postel plus Retie, Turnhout and Westerlo. The three pure-empty controls remained
at zero detections. A reviewed six-AOI fine-tuning challenger reached mean F1
`0.6248` but remained inactive because it produced two false detections in the
Postel-bos empty control.
For model-quality calibration, run the confidence sweep wrapper:
```bash
REAL_RASTER_PATH=/mnt/user/appdata/geointel/storage/operator-data/geel_orthophoto_wms_512.tif \
REAL_REFERENCE_VECTOR_PATH=/mnt/user/appdata/geointel/storage/operator-data/geel_grb_gbg_buildings.geojson \
CALIBRATION_THRESHOLDS="0.50 0.35 0.25 0.15" \
bash scripts/run_detection_calibration_sweep.sh http://192.0.2.10:1202
```
The sweep creates one real persisted workflow run per threshold, fetches the
persisted `QualityCheck`/`Metric` rows and writes a `calibration_summary.json`
with persisted detection count, raw candidate count, suppressed duplicate count,
duplicate IoU threshold, score, precision, recall, F1, mean IoU and false
positive/negative counts. It is intended to tune confidence/IoU/model choices,
not to add new inference behavior.
To compare local model assets and tile settings as well as thresholds, run the
quality matrix wrapper:
```bash
REAL_RASTER_PATH=/mnt/user/appdata/geointel/storage/operator-data/geel_orthophoto_wms_512.tif \
REAL_REFERENCE_VECTOR_PATH=/mnt/user/appdata/geointel/storage/operator-data/geel_grb_gbg_buildings.geojson \
QUALITY_MODEL_ASSET_IDS="yolov8n-building-segmentation-pt yolov8n-pt" \
QUALITY_TILE_SIZES="512 640" \
QUALITY_TILE_OVERLAPS="64" \
QUALITY_THRESHOLDS="0.50 0.15" \
bash scripts/run_detection_quality_matrix.sh http://192.0.2.10:1202
```
The matrix creates one real persisted workflow run per combination and writes
`quality_matrix_summary.json` with the selected model asset, tile size, tile
overlap, threshold, detection count, QA score, precision, recall, F1, mean IoU
and false-positive/false-negative counts. It ranks `best_by_score`,
`best_by_recall` and `best_by_precision`. It does not download weights, create
fake detections, fetch live providers or change backend API behavior.
To aggregate the same matrix over every prepared operator sample, run:
```bash
OPERATOR_SAMPLE_MANIFEST_PATH=storage/operator-data/operator_samples_manifest.json \
QUALITY_MODEL_ASSET_IDS="yolov8n-building-segmentation-pt yolov8n-pt" \
QUALITY_TILE_SIZES="512 640" \
QUALITY_TILE_OVERLAPS="64" \
QUALITY_THRESHOLDS="0.50 0.15" \
bash scripts/run_multi_sample_detection_quality_matrix.sh http://192.0.2.10:1202
```
The combined `multi_sample_quality_summary.json` reports per-sample and overall
best configurations. It is an operator benchmarking command, not a backend API
or provider import path.
Before promoting any local model as a default, also run the hard-negative
matrix against the documented background candidates:
```bash
OPERATOR_SAMPLE_MANIFEST_PATH=storage/operator-data/operator_samples_manifest.json \
OPERATOR_BACKGROUND_SAMPLE_SLUGS="postel_bos lommel_heide kasterlee_bos" \
QUALITY_MODEL_ASSET_IDS="geointel-building-yolov8n-expanded160e50-pt geointel-building-yolov8n-tile30-pt yolov8s-building-segmentation-pt" \
QUALITY_TILE_SIZES="640" \
QUALITY_TILE_OVERLAPS="64" \
QUALITY_THRESHOLDS="0.25 0.15 0.05" \
bash scripts/run_operator_hard_negative_detection_matrix.sh http://192.0.2.10:1202
```
This path uploads only background rasters, runs configured-YOLO detection and
counts detections as false-positive pressure. It does not upload reference
vectors or run QA/QC, so it cannot produce fake precision/recall metrics for
empty background AOIs.
To inspect the evidence behind a calibration run, export the persisted QA
evidence bundle:
```bash
CALIBRATION_SUMMARY_PATH=/mnt/user/appdata/geointel/artifacts/detection-calibration/20260707T002103Z/calibration_summary.json \
bash scripts/export_detection_calibration_evidence.sh http://192.0.2.10:1202
```
The bundle writes combined QA evidence GeoJSON plus a standalone HTML/SVG review
artifact that separates matched detections, matched references, false positives
and false negatives by role. It reads existing persisted `QualityCheck` evidence
only and does not rerun inference.
For source-image review of false negatives, run
`scripts/render_detection_false_negative_review_contact_sheets.py` against a
fixed-threshold evidence portfolio. It uses the selected run's persisted tile
manifest, overlays candidate/reference context and explicitly exports reference
features outside tile coverage. The command is read-only and never changes
`QualityCheck`, `Metric`, `Detection` or model state.
### Run backend
```bash
cd backend
python -m uvicorn app.main:app --reload
```
### Run backend tests
```bash
cd backend
python -m pytest
```
For warning-sensitive release checks, the backend is expected to pass with Python deprecation warnings promoted to errors for the timestamp-heavy service paths:
```bash
cd backend
python -m pytest -W error::DeprecationWarning tests/test_geojson_dataset_service.py tests/test_qa_service.py tests/test_sprint7a_persistence_foundation.py tests/test_sprint8c_detection_visualization_qa.py tests/test_sprint9_segmentation_foundation.py tests/test_vector_operations_service.py
```
The repository readiness gate now applies the same warning policy to the full backend suite:
```bash
bash scripts/run_readiness_check.sh
```
That readiness gate also runs the API contract smoke check before backend/frontend compilation and tests.
The RC contract gate loads the generated FastAPI OpenAPI document and requires
every successful JSON operation to expose a concrete Pydantic response schema
inside the canonical `{"data": ...}` envelope. Run it directly with:
```bash
python scripts/audit_api_contracts.py
```
The only tracked non-envelope operations are the three health probes, the four
persisted raster PNG responses and the streamed export download. A newly added
free-form JSON response or undocumented exception fails both the focused RC-7
test and the repository readiness gate.
### Golden QA/QC benchmark
Sprint 12 includes a deterministic QA/QC regression benchmark using explicit fixture data:
```bash
python scripts/run_golden_qa_benchmark.py
```
Machine-readable output:
```bash
python scripts/run_golden_qa_benchmark.py --json
```
Shell wrapper used by release-readiness checks:
```bash
bash scripts/verify_golden_qa_benchmark.sh
```
The benchmark compares `fixtures/golden/predicted_buildings.geojson` against `fixtures/golden/reference_buildings.geojson` and fails on metric drift. Expected baseline:
- precision: `0.5`
- recall: `0.5`
- F1: `0.5`
- mean IoU: `0.8339768339761133`
- false positives: `1`
- false negatives: `1`
The command uses existing QA/QC service logic and verifies `QualityCheck`/`Metric` persistence through an in-memory test session. It does not require live providers, AI models, Docker or PostGIS.
`scripts/run_readiness_check.sh` runs this benchmark automatically, so any
change that alters the golden QA/QC metric baseline must update the fixture and
expected metrics deliberately.
### Demo workflow seed
Sprint 15 adds an explicit offline demo workflow seed. It creates or returns a
demo project, AOI, fixture reference buildings, fixture candidate buildings and
a persisted QA/QC result. It does not fetch live GRB/OSM data and does not run
AI inference.
API:
```bash
curl -X POST http://localhost:1202/api/v1/demo/workflow
```
CLI:
```bash
python scripts/seed_demo_workflow.py --json
```
In Docker Compose on a LAN host:
```bash
curl -X POST http://192.0.2.10:1202/api/v1/demo/workflow
```
### QA/QC result listing
Persisted project quality checks and metric rows can be listed with:
```bash
curl http://localhost:1202/api/v1/projects/{project_id}/quality-checks
```
The frontend QA/QC Results panel uses this endpoint after loading the demo
workflow or running QA.
Detection QA evidence can be reviewed without changing its persisted metrics:
```bash
curl "http://localhost:1202/api/v1/projects/{project_id}/quality-checks/{quality_check_id}/reviews?reviewed=false&limit=50"
curl -X POST "http://localhost:1202/api/v1/projects/{project_id}/quality-checks/{quality_check_id}/reviews" \
-H "Content-Type: application/json" \
-d '{"evidence_role":"false_positive","evidence_feature_id":"DETECTION_UUID","decision":"qa_alignment_mismatch","notes":"Box and footprint represent the same building."}'
```
The list is derived from persisted quality-check evidence and paginates at a
maximum of 200 rows. The upsert verifies project ownership, quality-check type,
role-specific decisions and persisted Detection/VectorFeature ownership.
`detection_reviews` never mutates model output, reference geometry or canonical
Metric rows. Evidence GeoJSON queries only stored evidence ids instead of a
complete regional GRB dataset.
### Export foundation
Persisted exports can be created from the existing workbench state:
```bash
curl -X POST http://localhost:1202/api/v1/exports/metadata \
-H "Content-Type: application/json" \
-d '{"project_id":"PROJECT_UUID"}'
```
Vector dataset GeoJSON export:
```bash
curl -X POST http://localhost:1202/api/v1/exports/geojson \
-H "Content-Type: application/json" \
-d '{"export_kind":"dataset","dataset_id":"DATASET_UUID"}'
```
Detection or segmentation run GeoJSON export:
```bash
curl -X POST http://localhost:1202/api/v1/exports/geojson \
-H "Content-Type: application/json" \
-d '{"export_kind":"detection_run","analysis_run_id":"ANALYSIS_RUN_UUID"}'
```
List and inspect exports:
```bash
curl http://localhost:1202/api/v1/exports/projects/PROJECT_UUID/exports
curl http://localhost:1202/api/v1/exports/EXPORT_UUID/content
```
Download an artifact as a browser/file response:
```bash
curl -OJ http://localhost:1202/api/v1/exports/EXPORT_UUID/download
```
Create a lightweight HTML project report artifact:
```bash
curl -X POST http://localhost:1202/api/v1/exports/report \
-H "Content-Type: application/json" \
-d '{"project_id":"PROJECT_UUID"}'
```
The report contains project, dataset, QA/QC summary and export history state
only. It is not a PDF designer and does not add a separate reporting module.
After rebuilding a Docker/LAN deployment, verify the end-to-end demo and export
flow through the browser-facing frontend proxy:
```bash
bash scripts/verify_demo_export_workflow.sh http://192.0.2.10:1202
```
The script seeds the explicit demo workflow, verifies persisted QA/QC results,
creates metadata/report/vector GeoJSON exports, lists exports and downloads the
JSON/GeoJSON/HTML artifacts.
### Backend import smoke
```bash
cd backend
python -c "from app.main import app; print(app.title)"
```
### Dockerized backend
```bash
docker compose up --build backend db
```
The Docker Compose stack does not require a root `.env` file for the default local runtime. The database service exposes a container-internal Postgres healthcheck, and the backend also runs `docker_start.sh`, which retries an actual SQL `SELECT 1` connection before running `python -m alembic upgrade head` and starting Uvicorn.
PostGIS is not published on the host `5432` port by default. This avoids conflicts with existing Postgres/PostGIS services on NAS or server hosts. The backend connects over Docker networking with `db:5432`.
Backend and frontend Docker build contexts exclude dependency folders, build outputs and Python bytecode caches via `.dockerignore`.
The Docker Compose frontend is published at `http://localhost:1202`.
Compose healthchecks are enabled for all runtime services:
- `db` uses `pg_isready`.
- `backend` checks `http://127.0.0.1:8000/health` inside the container.
- `frontend` checks `http://127.0.0.1/health` through nginx, which also verifies the frontend-to-backend proxy path.
The frontend waits for a healthy backend before starting. Check runtime state:
```bash
docker compose ps
docker compose logs --tail=80 backend
docker compose logs --tail=80 frontend
```
The backend Docker image installs the approved GIS runtime extra (`.[gis]`) so
browser-facing Docker deployments can report raster/vector processing
capabilities accurately:
- `rasterio`
- `numpy`
- `pillow`
- `geopandas`
- `pyogrio`
- GDAL/GEOS/PROJ system libraries
After rebuilding the backend image, verify the LAN/browser runtime from the
repository root:
```bash
bash scripts/verify_gis_runtime.sh http://localhost:1202
```
On a NAS or server host, use the published LAN URL:
```bash
bash scripts/verify_gis_runtime.sh http://192.0.2.10:1202
```
The script calls `/api/v1/system/capabilities` through the frontend proxy and
fails if `postgis`, `rasterio` or `geopandas` are not reported as available.
The backend Docker build also runs:
```bash
python scripts/gis_import_smoke.py
```
Inside the backend Docker build context this resolves to
`backend/scripts/gis_import_smoke.py`. The root `scripts/gis_import_smoke.py`
wrapper calls the same smoke locally. The smoke imports `rasterio`, `geopandas`
and `pyogrio`; if one of those imports fails, the backend image build fails
before deployment.
### Live Docker/PostGIS migration smoke
Sprint 11 validates the real PostGIS runtime path with the existing database service. From the repository root:
```bash
docker compose config
docker compose up -d db
DATABASE_URL=postgresql+psycopg://geointel:geointel@localhost:5432/geointel bash scripts/live_migration_smoke.sh
```
The smoke script:
- opens a backend SQLAlchemy connection and runs `SELECT 1`
- runs `alembic upgrade head`
- checks `PostGIS_Version()` after migrations have created the extension
- verifies one Alembic head
- verifies required migrated tables and GiST indexes exist
Expected local environment:
```bash
DATABASE_URL=postgresql+psycopg://geointel:geointel@localhost:5432/geointel
```
If the database is not reachable, confirm Docker Desktop is running and that port `5432` is not already occupied. To clean up the local database container without deleting the named volume:
```bash
docker compose stop db
```
To remove the local PostGIS volume as well, use only when you explicitly want a fresh database:
```bash
docker compose down -v
```
## Key docs
- `docs/API_CONTRACTS.md`
- `docs/DATABASE_IMPLEMENTATION_PLAN.md`
- `docs/DEFINITION_OF_DONE.md`
- `docs/40-build-launch/SPRINT_1_SCOPE_FREEZE.md`
## Raster dependency note
Raster metadata and raster operations depend on local GDAL/rasterio availability.
To enable raster processing locally:
```bash
python -m pip install rasterio
```
If `rasterio` is unavailable:
- raster metadata responses return `503` with `RASTER_PROCESSING_UNAVAILABLE`
- raster clip/tile endpoints return explicit unavailable responses
## Export report artifact
`POST /api/v1/exports/report` creates the existing lightweight
`project_report_html` artifact. The report is a self-contained HTML handoff
view rendered from persisted project, dataset, QA/QC and export-history state.
It includes readiness scorecards, dataset inventory, QA/QC evidence, artifact
history, known limitations and print-friendly CSS.
This remains a simple HTML export. It does not add a PDF designer, report
builder, live provider fetching or new analysis behavior.
## Vector area selection
`POST /api/v1/projects/{project_id}/datasets/{dataset_id}/vector/select` runs a
read-only EPSG:4326 bbox query against persisted PostGIS `vector_features` and
returns a canonical-envelope GeoJSON FeatureCollection. It is intended for the
Map workspace area-extract flow and does not create derived datasets or export
records by itself.
The same bounded endpoint is the canonical large-layer map delivery path. The
frontend requests at most 1,000 features for the current viewport and surfaces
the response `truncated` flag; the backend does not provide or imply an
unbounded municipality-wide map response.
Selection summaries expose a primary metric plus an additive `metrics` list.
Known persisted themes are aggregated in `EPSG:31370`: building footprints,
forest, water surfaces and parcels return hectares; roads and linear
watercourses return kilometres; population keeps its configured inhabitant
aggregation. Intersecting feature counts remain available as supporting
evidence. Water volume is deliberately unavailable because the current GRB
source has no reliable depth/bathymetry dimension; GeoIntel does not manufacture
volume from 2D polygons.
`POST /api/v1/projects/{project_id}/datasets/{dataset_id}/vector/select/derive`
uses the same persisted `vector_features` selection but writes the result as a
new derived vector dataset. The created dataset uses
`source="operation:selection"`, `source_name="map_selection"` and
`derived_from_dataset_id` for source provenance, stores a GeoJSON artifact and
indexes its features back into `vector_features` for later QA/QC and analysis.
`POST /api/v1/exports/geojson` with `export_kind="vector_selection"` persists
the same bbox-selected FeatureCollection as a normal export record with
`export_type="vector_selection_geojson"`. This creates a handoff artifact only;
it does not create a derived dataset.
## Geographic scope provisioning
The release-candidate national foundation is provisioned explicitly:
```bash
docker exec geointel python /app/scripts/provision_belgium_north_sea_scope.py
```
Use `--fetch-only` to validate official NGI AdminVector, RBINS marine
reporting units and the Belgian Marine Spatial Plan 2026-2034 without changing
application persistence. The normal command creates or reuses
`Belgium and North Sea Workbench`, persists Belgium, all three regions,
territorial sea, EEZ and continental shelf as Areas, and uploads six
checksum-bound reference Datasets through `DatasetService`.
The operator has a fixed URL/layer allowlist, verified TLS, archive and
response-size limits, safe ZIP extraction, complete WFS pagination and
immutable artifact checksums. It does not run at startup and does not write
directly to `vector_features`.
`GET /api/v1/external/coverage/catalog` exposes audited national source
contracts. `POST /api/v1/external/coverage/resolve` intersects a drawn bbox
with persisted legal/administrative Areas and reports a split zone/theme
matrix. `operational` requires a matching `ready` Dataset; integration without
materialized data is only `partial`.
The explicit operator command below provisions the official 28-municipality
Vlaamse vervoerregio Kempen boundary foundation:
```bash
docker exec geointel python /app/scripts/provision_geographic_scope.py \
--scope kempen-transport-region
```
It reads current `VRBG/Refgem` boundaries, validates every registered name and
NIS code, unions the regional geometry and creates one project, one regional
Area, 28 municipality Areas and two source datasets through the public API.
It never writes directly to PostGIS and does not run on startup. The persisted
scope limitation explicitly distinguishes the transport-policy region from a
cultural or landscape definition of Kempen.
Use `--fetch-only` for a source/geometry/checksum audit. The scope pass does
not fetch thematic GRB, population or land-use data; those remain separate,
bounded operator jobs.
Provision the regional GRB building theme after the scope pass:
```bash
docker exec geointel python /app/scripts/provision_regional_grb_buildings.py \
--scope kempen-transport-region
```
The operator retains 28 checksummed municipality partitions but exposes one
normal regional reference dataset. `StorageService` copies the combined
artifact without materializing it as upload bytes; `DatasetService` creates
the Dataset and immutable DatasetVersion; `VectorFeatureService` validates and
flushes partition features in bounded batches. The transaction must index the
exact manifest feature count or it rolls back and removes the managed copy.
No public API contract or provider readiness claim is changed by this
operator-only path.
Provision the regional current road, water and parcel context through the
same persistence boundary:
```bash
docker exec geointel python /app/scripts/provision_regional_grb_context.py \
--scope kempen-transport-region --layers roads water parcels
```
The operator keeps one resumable municipality partition set per theme and
creates one regional reference Dataset per theme. Polygon ownership uses
maximum overlap area; line ownership uses maximum overlap length. It preserves
source geometry dimensions and collection-qualified source IDs, copies the
combined artifact through StorageService and indexes bounded batches through
DatasetService/VectorFeatureService. It does not add API routes, direct SQL or
interactive provider downloads.
## Temporal Mol data and evolution
Dataset uploads accept `temporal_series_key`, `observed_at`, `valid_from`,
`valid_to`, `temporal_granularity` and `source_version`. Every new source or
derived dataset also writes dataset version 1 in the same transaction.
After the Mol municipality workspace is available, import the official source
snapshots explicitly:
```bash
docker exec geointel python /app/scripts/provision_mol_population_history.py
docker exec geointel python /app/scripts/provision_mol_historical_landuse.py
docker exec geointel python /app/scripts/provision_official_landuse_timeseries.py
```
The first command imports Statbel sector population for 2021-2025. The second
imports Digitaal Vlaanderen historical land use for 1778, 1873 and 1969. The
third imports the Departement Omgeving 10 m forest class for 2013, 2016, 2019,
2022 and 2025. All commands are idempotent, use the normal
API/DatasetService flow and retain fetched artifacts in persistent operator
storage. They never run on app startup.
Every newly fetched or `--force` rebuilt Statbel population edition now passes
`statbel_population_preflight.py` before a derived GeoJSON can reach the
upload API. The operator retains both official ZIPs, writes an atomic
preflight manifest and verifies the source and derived SHA-256 values again at
upload time. A passed preflight does not replace an existing Dataset.
The preflight can also be run without downloads or database mutation against
already staged official artifacts:
```bash
docker exec geointel python /app/scripts/statbel_population_preflight.py \
--year 2025 \
--layout new \
--scope kempen-transport-region \
--population-archive /tmp/OPENDATA_SECTOREN_2025_NEW.zip \
--population-url https://statbel.fgov.be/sites/default/files/files/opendata/bevolking/sectoren/OPENDATA_SECTOREN_2025_NEW.zip \
--geometry-archive /tmp/sh_statbel_statistical_sectors_31370_20250101.geojson.zip \
--geometry-url https://statbel.fgov.be/sites/default/files/files/opendata/Statistische%20sectoren/sh_statbel_statistical_sectors_31370_20250101.geojson.zip \
--baseline-snapshot /app/storage/operator-data/regional-timeseries/kempen-transport-region/population/kempen_transport_region_statbel_population_2024.geojson \
--output /app/storage/operator-evidence/statbel-population/2025-kempen.preflight.json
```
The command exits non-zero and emits a stable `error_code` when source
identity, archive safety, schema, CRS, geometry, join, total reconciliation,
scope coverage or the default 5% annualized population-change review limit
fails. `ZZZZ` rows are reconciled as official unlocated population but remain
excluded from spatial metrics. The 2025 REDEGEO contract deliberately compares
explicit municipality fields; it does not assume that `CD_SECTOR` still starts
with the current `CD_REFNIS` after municipal mergers.
Future official editions use the separate four-phase release coordinator. The
project id must belong to `Kempen Regional Workbench`:
```bash
docker exec geointel python /app/scripts/manage_statbel_population_release.py plan \
--project-id <KEMPEN_PROJECT_ID> \
--api-url http://127.0.0.1:8000/api/v1 \
--refresh-catalog
docker exec geointel python /app/scripts/manage_statbel_population_release.py stage \
--project-id <KEMPEN_PROJECT_ID> \
--api-url http://127.0.0.1:8000/api/v1 \
--confirm-edition <YEAR_FROM_PLAN> \
--confirm-layout <LAYOUT_FROM_PLAN>
docker exec geointel python /app/scripts/manage_statbel_population_release.py review \
--project-id <KEMPEN_PROJECT_ID> \
--api-url http://127.0.0.1:8000/api/v1 \
--confirm-edition <YEAR_FROM_PLAN> \
--confirm-layout <LAYOUT_FROM_PLAN> \
--confirm-plan-sha256 <SHA256_FROM_STAGE> \
--approve --reviewer "<OPERATOR_NAME>" \
--review-note "Schema, totalen, ZZZZ en geometrieherstel nagekeken"
docker exec geointel python /app/scripts/manage_statbel_population_release.py apply \
--project-id <KEMPEN_PROJECT_ID> \
--api-url http://127.0.0.1:8000/api/v1 \
--confirm-edition <YEAR_FROM_PLAN> \
--confirm-layout <LAYOUT_FROM_PLAN> \
--confirm-plan-sha256 <SHA256_FROM_STAGE> \
--confirm-review-sha256 <SHA256_FROM_REVIEW>
```
`plan` is read-only and creates no file. `stage` always uses bounded fresh
downloads and `--fetch-only`; `review` imports nothing; `apply` revalidates
the current catalog, plan, review, source archives, preflight manifest and
derived snapshot before using the existing upload API. An already-current
release cannot be staged. A repeated successful apply resolves the existing
Dataset through complete paginated lookup rather than creating a duplicate.
Historical land-use work can be bounded explicitly:
```bash
docker exec geointel python /app/scripts/provision_mol_historical_landuse.py --years 1778,1969 --themes forest,water
```
`GET /api/v1/projects/{project_id}/temporal/series` discovers the series and
`POST /api/v1/projects/{project_id}/temporal/compare` compares two snapshots
inside one EPSG:4326 bbox. Partial statistical sectors are estimates; old map
editions without stable identities do not produce invented object changes.
Modern raster-derived forest polygons have the same identity limitation. Their
area is measured in EPSG:31370 and is exact within the 10 m source
representation, not a cadastral forest survey.
The same source-governed operators can synchronize the approved regional
scope in one explicit pass:
```bash
docker exec geointel python /app/scripts/provision_regional_timeseries.py
```
This resolves the retained official boundary and imports five Statbel
population snapshots plus five modern forest, water, built-function and
transport-infrastructure snapshots, followed by the 1778/1873/1969 historical
building, water and road snapshots, into
`Kempen Regional Workbench`. Mol and regional series keys remain separate and
existing immutable datasets are reused. Complete statistical sectors use exact
published totals; a rectangle cutting a sector remains an area-weighted
estimate. Forest area is measured within the official 10 m representation.
Use `--fetch-only` to validate source artifacts without database mutation.
The regional forest path partitions WCS requests by official municipality to
stay within upstream response limits, then builds one retained 10 m mosaic and
one normal regional vector Dataset. A failed source request leaves completed
partition artifacts reusable and never lowers source resolution silently.
Historical WFS retrieval is likewise partitioned by all 28 municipality
boundaries because broad WFS counts stop at 10,000. Exact source responses are
retained as checksummed gzip artifacts before clipping and regional assembly.
Run that stage independently when needed:
```bash
docker exec geointel python /app/scripts/provision_regional_historical_landuse.py
```
Use `--fetch-only` for source/artifact validation without persistence. The
historical building class represents mapped built land-use surfaces, not
individual building footprints; water remains surface area, not depth or
volume; historical roads are mapped road surfaces, not present-day centerline
length.
Official operator datasets record that their geometries were clipped to the
persisted Area. When that exact Area is selected, vector totals and aggregate
metrics use the already clipped geometries directly rather than intersecting
every row with the same detailed boundary again. This optimization is allowed
only for matching Dataset/Area ids with explicit clipping metadata or a known
clipping operator; drawn rectangles and ordinary uploads keep the normal exact
PostGIS intersection path.
## Local Ollama GIS assistant
The optional assistant is a read-only backend integration. It lists locally
installed Ollama models, calculates the active Area/bbox metrics from persisted
PostGIS features and sends only that compact JSON context to Ollama. It never
downloads models, sends geometries or treats model prose as source data.
Configuration:
```text
OLLAMA_ENABLED=true
OLLAMA_BASE_URL=http://host.docker.internal:11434
OLLAMA_DEFAULT_MODEL=qwen3.5:9b
OLLAMA_TIMEOUT_SECONDS=120
OLLAMA_MAX_OUTPUT_TOKENS=1200
OLLAMA_CONTEXT_TOKENS=16384
```
The Unraid deployment adds `host.docker.internal:host-gateway` automatically.
Verify the connection with `GET /api/v1/assistant/status`, inspect installed
models with `GET /api/v1/assistant/models` and ask a grounded question through
`POST /api/v1/projects/{project_id}/assistant/query`. A requested model must be
present in Ollama `/api/tags`. Missing water depth/bathymetry remains explicit;
the assistant cannot turn 2D water geometry into volume. GeoIntel rejects an
answer when Ollama reports `done_reason=length`, so a visibly truncated sentence
is never presented as a complete result. The 1,200-token default leaves enough
room for a compact cross-domain profile while the system prompt requires every
explicitly requested theme and excludes unrelated themes.
## Agricultural-use parcel history
Prepare all definitive 2008-2025 regional editions without database writes:
```bash
docker exec geointel python /app/scripts/provision_agricultural_parcel_history.py --fetch-only
```
Import the checked artifacts through the canonical Dataset upload route:
```bash
docker exec geointel python /app/scripts/provision_agricultural_parcel_history.py
```
Use `--scope mol`, `--years 2008,2019,2025` or `--force` only as explicit
operator choices. The default scope is the persisted 28-municipality Kempen
transport region. Every annual source ZIP and crop code list remains under the
storage volume. PostGIS computes exact hectares for drawn rectangles and
persisted Areas; parcel identities are deliberately unavailable for lineage.
Future definitive editions use the separate four-phase release coordinator:
```bash
docker exec geointel python /app/scripts/manage_alz_agriculture_release.py plan \
--project-id <KEMPEN_PROJECT_ID> --refresh-catalog
docker exec geointel python /app/scripts/manage_alz_agriculture_release.py stage \
--project-id <KEMPEN_PROJECT_ID> \
--confirm-edition <YYYY-v3_FROM_PLAN>
docker exec geointel python /app/scripts/manage_alz_agriculture_release.py review \
--project-id <KEMPEN_PROJECT_ID> \
--confirm-edition <YYYY-v3_FROM_PLAN> \
--confirm-plan-sha256 <SHA256_FROM_STAGE> \
--approve --reviewer "<OPERATOR_NAME>"
docker exec geointel python /app/scripts/manage_alz_agriculture_release.py apply \
--project-id <KEMPEN_PROJECT_ID> \
--confirm-edition <YYYY-v3_FROM_PLAN> \
--confirm-plan-sha256 <SHA256_FROM_STAGE> \
--confirm-review-sha256 <SHA256_FROM_REVIEW>
```
The manager accepts only one catalog-confirmed definitive v3 release. `plan`
writes nothing; `stage` downloads and normalizes without PostGIS mutation;
`review` binds a named approval; `apply` revalidates catalog, hashes, schema,
crop codes, scope accounting and previous-edition deltas before delegating to
DatasetService. Provisional v1/v2 snapshots never enter the historical series.
## Buildings and Addresses Register snapshot
After the Mol Area and regional GRB buildings have been provisioned, prepare
the official register evidence with:
```bash
docker exec geointel python /app/scripts/provision_buildings_addresses_register.py --fetch-only
```
Review the generated manifest and then persist through DatasetService:
```bash
docker exec geointel python /app/scripts/provision_buildings_addresses_register.py
```
The resulting `building_registry` Dataset uses ordinary EPSG:4326
`vector_features`; no register-specific table or direct operator database write
exists. Exact PostGIS selection exposes footprint hectares, lifecycle counts,
aggregate unit/address counts and GRB reconciliation counts. Raw address pages
are checksummed storage evidence only. Address labels and house/box numbers are
not copied into queryable properties.
## Helpful repository scripts
- `bash scripts/backend_install.sh`
- `bash scripts/backend_test.sh`
- `bash scripts/backend_dev.sh`
- `bash scripts/smoke_backend_import.sh`
## Bounded official orthophoto acquisition
`GET /api/v1/projects/{project_id}/datasets/orthophoto/products` lists the
governed product allowlist. `POST .../datasets/orthophoto/acquire` accepts an
explicit EPSG:4326 map rectangle plus `product_key` and stores the official
regional WMS response as a canonical EPSG:31370 raster Dataset. Digitaal
Vlaanderen, SPW (`wallonia_latest`) and Paradigm UrbIS (`brussels_latest`) are
allowlisted. The two regional products are bound to persisted Wallonia and
Brussels-Capital Region Areas. The
default safety envelope is 128-1,024 m per side, 1 m/pixel, 32 MiB and a
24-hour exact-request cache. It runs synchronously behind the existing Job
abstraction and never during startup.
Available products cover the most recent winter image, annual winter mosaics
for 2012-2025, three older winter periods, RGB 1979-1990 and panchromatic 1971.
Historical products persist validity metadata and are deliberately excluded
from configured-YOLO/current-GRB QA. `GET .../datasets/{dataset_id}/raster/image`
is the constrained binary PNG endpoint used by the MapLibre image overlay.
Settings: `ORTHOPHOTO_ENABLED`, `ORTHOPHOTO_WMS_URL`,
`SPW_ORTHOPHOTO_WMS_URL`, `BRUSSELS_ORTHOPHOTO_WMS_URL`,
`ORTHOPHOTO_WMS_LAYER`, `ORTHOPHOTO_RESOLUTION_M`,
`ORTHOPHOTO_MIN_SIDE_M`, `ORTHOPHOTO_MAX_SIDE_M`,
`ORTHOPHOTO_TIMEOUT_SECONDS`, `ORTHOPHOTO_MAX_RESPONSE_MB` and
`ORTHOPHOTO_CACHE_TTL_HOURS`. Keep the official HTTPS URL and 1 m profile
unless a separately verified deployment/model profile requires a change.
An explicit bounded request may provide `resolution_m` down to the governed
product's native resolution. This is intended for reviewed training corpora;
the service rejects source oversampling and records rolling-latest observation
time as unknown per pixel rather than equating it with download time.
Before a future `most_recent` source release is allowed into a governed pixel
stage, run the metadata-only preflight for the exact intended rectangle:
```bash
docker exec geointel python /app/scripts/orthophoto_release_preflight.py \
--project-id 82a85913-c522-45d7-84a1-02b393d89e55 \
--api-url http://127.0.0.1:8000/api/v1 \
--bbox 5.110 51.180 5.117 51.185 \
--refresh-catalog
```
The command reads canonical API envelopes, exact official WMS capabilities,
WCS `DescribeCoverage` and at most 64 queryable flight-day points. It never
requests raster pixels or mutates application/storage state. `current`,
remote-older and mixed/incorrect flight years remain non-stageable. The
report's point grid is flight-date evidence; complete selected-area coverage
comes from containment inside the official 15 cm WCS raster domain.
Official release promotion is a separate four-action operator workflow. Run it
inside the all-in-one container so stage/apply can use only the loopback API:
```bash
# Read-only decision; copy the reported edition and current local marker.
docker exec geointel python /app/scripts/manage_orthophoto_release.py plan \
--project-id 82a85913-c522-45d7-84a1-02b393d89e55 \
--bbox 5.110 51.180 5.117 51.185 --refresh-catalog
# First official baseline only: both values must match the fresh preflight.
docker exec geointel python /app/scripts/manage_orthophoto_release.py stage \
--project-id 82a85913-c522-45d7-84a1-02b393d89e55 \
--bbox 5.110 51.180 5.117 51.185 \
--confirm-edition 2025.04 \
--establish-official-baseline \
--confirm-local-version most_recent_at_2026-07-15
# Inspect review-preview.png, then use the exact plan SHA printed by stage.
docker exec geointel python /app/scripts/manage_orthophoto_release.py review \
--project-id 82a85913-c522-45d7-84a1-02b393d89e55 \
--bbox 5.110 51.180 5.117 51.185 \
--confirm-edition 2025.04 --confirm-plan-sha256 <plan-sha256> \
--approve --reviewer "<operator name>" --review-note "<bounded review>"
# Apply only the exact approved bytes and hashes.
docker exec geointel python /app/scripts/manage_orthophoto_release.py apply \
--project-id 82a85913-c522-45d7-84a1-02b393d89e55 \
--bbox 5.110 51.180 5.117 51.185 \
--confirm-edition 2025.04 --confirm-plan-sha256 <plan-sha256> \
--confirm-review-sha256 <review-sha256>
```
For a later comparable `YYYY.NN` update, omit the two first-baseline flags.
Stage performs one bounded pixel request but no database mutation. Apply is
idempotent for the exact plan/raster checksum, creates a new immutable raster
Dataset and DatasetVersion with the official edition, and retains every older
snapshot. No command is scheduled or invoked by startup or browser actions.
## Governed bounded GRB acquisition
`GET /api/v1/projects/{project_id}/datasets/grb/products` exposes four fixed
official vector products: building footprints, road segments, water
surfaces/lines and administrative parcels. `POST .../datasets/grb/acquire`
accepts an EPSG:4326 rectangle, optional project Area, one product key and an
explicit refresh flag.
The service queries only the allowlisted GRB OGC API collection paths, follows
complete same-host pagination and clips every geometry to `bbox ∩ Area`.
It explicitly requests OGC CRS84 GeoJSON for bbox and output; the native
EPSG:31370 storage CRS remains provenance rather than being guessed from raw
coordinates.
Requests fail closed above 20 km per side, 200 pages, 150,000 retained
features, 20 MiB per page or 256 MiB total. No partial Dataset is persisted
when a limit is exceeded. Official ids, request URLs, page checksums and the
final artifact checksum are retained as provenance.
Persistence uses the existing synchronous Job plus
`DatasetService.import_vector_bytes`, so Dataset, DatasetVersion and
VectorFeature rows remain one canonical flow. The browser never contacts the
provider directly. Exact request identities are reused for 24 hours. Buildings
return footprint area in hectares, roads return line length in kilometres,
water returns surface area plus supporting water-line length, and parcels
return mapped area. GRB cannot provide water volume, legal parcel boundaries
or traffic information.
Settings: `GRB_ENABLED`, `GRB_OGC_API_URL`, `GRB_MIN_SIDE_M`,
`GRB_MAX_SIDE_M`, `GRB_PAGE_SIZE`, `GRB_MAX_PAGES`, `GRB_MAX_FEATURES`,
`GRB_TIMEOUT_SECONDS`, `GRB_MAX_RESPONSE_MB`,
`GRB_MAX_TOTAL_RESPONSE_MB` and `GRB_CACHE_TTL_HOURS`.
## Governed DHMV terrain acquisition
`GET /api/v1/projects/{project_id}/datasets/dhmv/products` exposes the fixed
official DTM/DSM registry. `POST .../datasets/dhmv/acquire` requests only
`DHMVII_DTM_1m` or `DHMVII_DSM_1m` from the production Digitaal Vlaanderen WCS.
The default 5 m analysis copy keeps complete-Mol processing bounded while
retaining native 1 m resolution, EPSG:31370, TAW, `-9999` nodata and the
2013-2015 acquisition period in provenance.
Municipality-sized requests are split into sequential WCS tiles of at most
10 km per side. The client sends the explicit media accept header required by
the production service, waits between requests, retries transient provider
statuses once and mosaics only tiles that validate against EPSG:31370, one
band and the requested resolution. Every tile URL and aggregate transfer
checksum remains in provenance.
Run the complete Mol operator after the regional workspace and Mol Area exist:
```bash
docker exec geointel python /app/scripts/provision_mol_dhmv.py
```
The operator acquires DTM and DSM, clips each raster to the exact persisted
Area, validates checksums and calls the terrain selection endpoint as a smoke.
Use `--products dtm_1m`, `--resolution-m 5` or `--force` when explicitly
needed. `POST .../raster/terrain/select` returns height in m TAW, relief in
metres and slope in degrees. `GET .../raster/terrain/image` returns the
constrained MapLibre PNG. Water depth, volume and drainage remain unavailable.
Provision the same governed DTM/DSM pair for every persisted municipality in
the approved Kempen scope:
```bash
docker exec geointel python /app/scripts/provision_regional_dhmv.py \
--scope kempen-transport-region --dry-run
docker exec geointel python /app/scripts/provision_regional_dhmv.py \
--scope kempen-transport-region
```
This plans 56 municipality/product acquisitions. It supports bounded
`--members` and `--products` subsets, backend cache reuse, per-item progress
and a complete failure summary. Persistence remains inside the canonical
DHMV acquisition service and Dataset/DatasetVersion/Job flow; the operator
does not fetch WCS bytes or write raster metadata directly.
The complete live matrix contains 56 ready Datasets and 56 DatasetVersions
across 28 Areas. On the complete Kempen Area the Map workspace presents those
partitions as one logical DTM/DSM layer. `POST .../datasets/raster/terrain/select`
opens only partitions intersecting the drawn rectangle and computes exact
global cell statistics. It does not create a hidden regional mosaic.
Settings: `DHMV_ENABLED`, `DHMV_WCS_URL`, `DHMV_RESOLUTION_M`,
`DHMV_MIN_SIDE_M`, `DHMV_MAX_SIDE_M`, `DHMV_MAX_PIXELS`,
`DHMV_TIMEOUT_SECONDS` and `DHMV_MAX_RESPONSE_MB`.
## Governed VMM flood-hazard depth scenarios
`GET /api/v1/projects/{project_id}/datasets/flood-hazard/products` exposes the
twelve allowlisted VMM OGRK coverages. `POST .../flood-hazard/acquire` performs
bounded WCS 1.1 requests, exact Area clipping, checksum validation and ordinary
Dataset/DatasetVersion/Job persistence. The source's positive centimetre
values are normalized to metres; null/zero cells are transparent nodata.
Run all scenarios for Mol after the regional workspace and Mol Area exist:
```bash
docker exec geointel python /app/scripts/provision_mol_flood_hazards.py
```
Provision the same official VMM scenario set for every persisted municipality
Area in the approved Kempen regional workspace:
```bash
docker exec geointel python /app/scripts/provision_regional_flood_hazards.py \
--scope kempen-transport-region
```
Inspect the planned municipality/scenario matrix without writing data:
```bash
docker exec geointel python /app/scripts/provision_regional_flood_hazards.py \
--scope kempen-transport-region --dry-run
```
Useful bounded runs:
```bash
docker exec geointel python /app/scripts/provision_regional_flood_hazards.py \
--members Mol,Geel --products pluviaal_current_t100
docker exec geointel python /app/scripts/provision_regional_flood_hazards.py \
--members 13025 --products pluviaal_current_t10,pluviaal_current_t100
```
The regional operator uses the canonical API only. It requires the geographic
scope Areas to exist first, persists one ordinary raster Dataset per
municipality/scenario and reuses existing Datasets unless `--force` is supplied.
The full Kempen scope with all products means 28 municipalities times 12
scenario rasters. This is intentionally explicit operator work, not startup
work and not a browser-side provider fetch.
The complete live matrix contains 336 ready Datasets and 336 DatasetVersions.
The regional Map workspace deduplicates them into twelve scenario choices,
renders all municipality image partitions for the selected scenario and uses
`POST .../datasets/raster/flood-hazard/select` for exact bounded cross-boundary
analysis. The same 12-million-cell guard prevents unsafe full-region reads.
Use `--products pluviaal_current_t100`, `--resolution-m 5` or `--force` for an
explicit subset/refresh. `POST .../raster/flood-hazard/select` returns mapped
inundated hectares, selection share and local modeled maximum-depth statistics.
The `modelled_max_depth_area_integral_m3` metric is an area integral of local
maxima and must not be called actual, permanent or concurrent water volume.
`GET .../raster/flood-hazard/image` serves the constrained transparent PNG.
Settings: `FLOOD_HAZARD_ENABLED`, `FLOOD_HAZARD_WCS_URL`,
`FLOOD_HAZARD_RESOLUTION_M`, `FLOOD_HAZARD_MIN_SIDE_M`,
`FLOOD_HAZARD_MAX_SIDE_M`, `FLOOD_HAZARD_MAX_PIXELS`,
`FLOOD_HAZARD_TIMEOUT_SECONDS` and `FLOOD_HAZARD_MAX_RESPONSE_MB`.
## Cross-domain thematic rasters and DOV soil
The governed thematic registry exposes five fixed MercatorNet products through
`GET .../datasets/thematic-raster/products`. Acquisition uses
`POST .../datasets/thematic-raster/acquire`; selection and PNG rendering use
`POST .../raster/thematic/select` and `GET .../raster/thematic/image`.
Provision every product for the exact persisted Mol Area:
```bash
docker exec geointel python /app/scripts/provision_thematic_rasters.py
```
Inspect the complete 28-municipality matrix without writes, then run it after
the Mol source/runtime gate passes:
```bash
docker exec geointel python /app/scripts/provision_thematic_rasters.py \
--project-name "Kempen Regional Workbench" --all-municipalities --dry-run
```
Settings: `THEMATIC_RASTER_ENABLED`, `THEMATIC_RASTER_WCS_URL`,
`THEMATIC_RASTER_MIN_SIDE_M`, `THEMATIC_RASTER_MAX_SIDE_M`,
`THEMATIC_RASTER_MAX_PIXELS`, `THEMATIC_RASTER_TIMEOUT_SECONDS` and
`THEMATIC_RASTER_MAX_RESPONSE_MB`.
The default thematic ceiling is 60 km and 30 million cells so the exact
Kempen work area fits. External WCS transfers remain split into fixed 10 km
tiles, product identifiers remain server-allowlisted and other raster
pipelines retain their smaller independent limits.
The Flanders browser workflow uses these same endpoints on demand. It never
performs a startup import or direct browser WCS request: an explicit
municipality or drawn rectangle starts five bounded acquisitions, followed by
the existing persisted-raster analyses. Exact request hashes reuse ready
Datasets. A full-Flanders raster request remains blocked by the same 60 km and
30 million cell limits. Dataset metadata labels only Areas named
`Gemeente ...` as `coverage_scope=municipality`; regional Area clipping is
stored as `bounded_selection`.
## Walloon WALOUS land cover and flood hazard
The Wallonia map flow uses bounded PICC vector products, the queryable legal
SPW flood-hazard polygon layer and provisioned official WALOUS land-cover
rasters. Provision the 2018, 2020 and 2023 source editions once in the persistent
storage mount:
```bash
docker exec geointel python /app/scripts/provision_walous_sources.py \
--years 2018 2020 2023 \
--destination /app/storage/source-cache/walous
```
The provisioner verifies advertised archive sizes, safe ZIP structure,
EPSG:3812, one band, 1 m cells, the official non-contiguous class codes
`1,2,3,4,5,6,7,8,9,80,90` and SHA-256 checksums. It does not run at
application startup. `GET .../datasets/walous/products` therefore reports
`source_not_provisioned` for each edition whose source file is absent.
For a bounded Walloon selection the browser persists the latest edition and
all other configured comparable editions. `POST .../raster/walous/select`
returns cell-area hectares; the temporal API compares the same semantic metric
keys for 2018, 2020 and 2023. The 2018 stacked classes use the official visible-
class crosswalk and retain the earlier-method limitation. WALOUS is land cover,
not legal land use, ownership,
tree count, timber volume or water volume.
The class semantics follow the official raster codes, not display-list
positions: 1 artificial ground, 2 above-ground construction, 3 railway, 4 bare
soil, 5 surface water, 6 rotating herbaceous cover, 7 continuous herbaceous
cover, 8/9 trees above 3 m and 80/90 woody cover up to 3 m. Observation ranges
are retained from the SPW metadata rather than replaced by arbitrary year-end
dates.
Settings: `WALOUS_ENABLED`, `WALOUS_SOURCE_DIR`,
`WALOUS_ANALYSIS_RESOLUTION_M`, `WALOUS_MAX_SIDE_M` and
`WALOUS_MAX_PIXELS`. The SPW flood polygon adapter uses
`SPW_FLOOD_HAZARD_ENABLED` and `SPW_FLOOD_HAZARD_MAPSERVER_URL`.
The official Walloon 2021-2022 1 m MNT is an explicit operator asset. Provision
it once with `scripts/provision_spw_terrain_source.py`; the runtime then reads
only bounded windows and persists 5 m analysis derivatives. The full 0.5 m
artifact remains intentionally excluded because it adds no V1 metric and is
about 213 GB. Settings: `SPW_TERRAIN_ENABLED`, `SPW_TERRAIN_SOURCE_DIR`,
`SPW_TERRAIN_ANALYSIS_RESOLUTION_M`, `SPW_TERRAIN_MAX_SIDE_M` and
`SPW_TERRAIN_MAX_PIXELS`.
Provision the official DOV soil polygons for Mol through the existing vector
upload path:
```bash
docker exec geointel python /app/scripts/provision_mol_soil_map.py
```
Provision all approved Kempen municipalities and the complete work area after
the official geographic-scope artifacts exist:
```bash
docker exec geointel python /app/scripts/provision_regional_soil_map.py
```
The regional operator retains one checksummed WFS evidence chain per
municipality, gives boundary-split source features a NIS suffix and assembles
one Dataset linked to the complete Kempen Area. EPSG:31370 clipping followed by
EPSG:4326 persistence can create submeter coordinate-rounding slivers at the
stored boundary, so regional and municipal selection metrics deliberately run
the exact PostGIS intersection instead of using a preclipped fast path.
Use `--fetch-only` to retain and validate source evidence without importing.
The operator never writes directly to PostGIS. Soil drainage and related map
classes represent the 1949-1971 survey and are not current observations.
## Waterinfo station histories
Run the explicit operator after the regional workspace and Mol Area exist:
```bash
docker exec geointel python /app/scripts/provision_waterinfo_station_history.py \
--project-name "Kempen Regional Workbench" \
--area-name "Gemeente Mol" \
--from-year 2013 --to-year 2025
```
The command retains raw KiWIS JSON/checksums and imports only real annual
observations through the canonical dataset upload API. Every station has its
own temporal-series key. Water levels and discharges remain Point measurements;
they are never averaged across stations or presented as municipal water volume.
Use `--fetch-only` to prepare and audit artifacts without persistence.
## BWK/Natura 2000 state 2025
Run the governed Mol operator after the regional workspace and Mol Area exist:
```bash
docker exec geointel python /app/scripts/provision_mol_bwk_natura2000.py
```
The command fetches the official INBO WFS, retains raw checksummed pages,
clips in EPSG:31370 and imports through DatasetService. `--fetch-only` builds
evidence without persistence. A conflicting checksum for an already persisted
state-2025 Mol Dataset fails closed instead of creating a silent replacement.
PostGIS selection summaries keep BWK value classes separate and label
PHAB-derived habitat hectares as estimates.
For the complete approved Kempen transport region, run the partitioned
operator after `provision_geographic_scope.py --scope kempen-transport-region`:
```bash
docker exec geointel python /app/scripts/provision_regional_bwk_natura2000.py
```
Use `--fetch-only` to build and validate all 28 municipality partitions without
database persistence. A normal rerun validates and reuses the immutable source
evidence and existing Dataset. `--force` explicitly refetches the WFS but still
fails closed if a different state-2025 checksum is already persisted. The
regional output uses the same selection-summary API as Mol; no new endpoint or
direct PostGIS write is introduced.
## Source freshness and version audit
`GET /api/v1/projects/{project_id}/datasets/source-freshness` derives a
read-only source status from persisted Dataset, DatasetVersion and storage
evidence. It distinguishes rolling snapshots and annual publications from
fixed editions, scenarios, historical archives and local artifacts. Fixed
source editions are never called stale solely because they are old.
The endpoint checks missing versions, checksum disagreement, missing local
files and stored-size disagreement. It performs no provider request and no
database write. The packaged operator command is suitable for an explicit
Unraid cron entry:
```bash
docker exec geointel python /app/scripts/audit_source_freshness.py \
--project-id 82a85913-c522-45d7-84a1-02b393d89e55 \
--api-url http://127.0.0.1/api/v1 \
--fail-on integrity \
--output /app/storage/operator-evidence/source-freshness/latest.json
```
Use `--fail-on due` to make a planned review date fail automation, or
`--fail-on never` for reporting only. The command never starts a refresh.
An operator can explicitly add the official GRB, orthophoto, Statbel and ALZ
edition check:
```bash
docker exec geointel python /app/scripts/audit_source_freshness.py \
--project-id 82a85913-c522-45d7-84a1-02b393d89e55 \
--api-url http://127.0.0.1/api/v1 \
--probe-catalogs \
--output /app/storage/operator-evidence/source-freshness/with-catalogs.json
```
Use `--refresh-catalogs` to bypass the 15-minute in-memory cache and
`--fail-on-catalog` only when temporary official-provider unavailability must
fail an operator job. This path reads bounded WFS/WMS capabilities and their
fixed ISO 19139 metadata records. It confirms GRB `GBG`, `WBN`, `WGO`, `ADP`
and orthophoto `Ortho`, `Vliegdagcontour`. It parses the exact official Statbel
DCAT Turtle catalog to identify the latest population-by-statistical-sector
year, landing page, license and allowed distribution identities. It never
follows those ZIP/XLSX links. It also reads the exact official ALZ
publication page and validates only allowlisted archive-link identities. It
never requests feature, raster or ALZ ZIP content. ALZ v1/v2 campaign snapshots
remain provisional; only a v3 publication is compared with a local definitive
historical edition.
Runtime controls are `SOURCE_CATALOG_PROBE_ENABLED`,
`SOURCE_CATALOG_GRB_WFS_URL`, `SOURCE_CATALOG_STATBEL_DCAT_URL`,
`SOURCE_CATALOG_STATBEL_MAX_RESPONSE_MB`, `SOURCE_CATALOG_ALZ_RELEASE_URL`,
`SOURCE_CATALOG_PROBE_TIMEOUT_SECONDS`, `SOURCE_CATALOG_PROBE_MAX_RESPONSE_MB` and
`SOURCE_CATALOG_PROBE_CACHE_TTL_SECONDS`. Metadata links remain restricted to
the official HTTPS CSW path on `metadata.vlaanderen.be` even when an operator
overrides the capabilities endpoint. The ALZ release URL is fail-closed to the
exact HTTPS host/path and cannot be redirected to another page or download
host.
The Statbel catalog has a separate 5 MiB default response bound. Population
year, sector-geometry year and REDEGEO layout remain separate concepts: the
2025 population release uses the new layout, while the concurrently published
old layout is transition evidence only. The existence of 2026 sector geometry
does not imply a 2026 population-by-sector release.
## Governed regional GRB refresh
`GET /api/v1/projects/{project_id}/datasets/grb-refresh-plan` combines the
explicit official GRB edition probe with the four existing regional snapshot
series. It is read-only: it reports local feature/storage impact and whether
buildings, roads, water and parcels are current, updateable or require review.
Regional refreshes use a two-phase operator flow inside the all-in-one
container. First stage and validate every source partition without touching
PostGIS:
```bash
docker exec geointel python /app/scripts/manage_grb_refresh.py stage \
--project-id 82a85913-c522-45d7-84a1-02b393d89e55 \
--api-url http://127.0.0.1:8000/api/v1 \
--confirm-edition 2026-07-15 \
--layers buildings roads water parcels
```
The JSON result gives `plan_path`, exact feature deltas, artifact sizes and
`plan_sha256`. Review that evidence, then apply those exact staged bytes:
```bash
docker exec geointel python /app/scripts/manage_grb_refresh.py apply \
--project-id 82a85913-c522-45d7-84a1-02b393d89e55 \
--api-url http://127.0.0.1:8000/api/v1 \
--confirm-edition 2026-07-15 \
--confirm-plan-sha256 SHA256_FROM_STAGE
```
Both commands fail when project/scope, official edition, manifests, partition
count or any checksum differs. Interrupted staging is safely resumable because
the existing municipal manifests are reused. Apply imports through
DatasetService/VectorFeatureService, creates new temporal Datasets and retains
all previous snapshots. Do not add `--force` to this coordinator; a source
refetch remains a separate deliberate recovery action in the lower-level
operators.
## Safe project lifecycle cleanup
Operational validation, calibration and benchmark runs can create technical
projects. The default project API now returns active workspaces only, while
archived workspaces remain queryable with `GET /api/v1/projects?status=archived`.
Use the packaged cleanup command to archive only the strict technical-name
allowlist:
```bash
# Dry-run: inspect the number of matches without changing the database.
python scripts/archive_technical_projects.py
# Apply the exact allowlisted plan.
python scripts/archive_technical_projects.py --apply
```
Inside the all-in-one Unraid container:
```bash
docker exec geointel python /app/scripts/archive_technical_projects.py
docker exec geointel python /app/scripts/archive_technical_projects.py --apply
```
The command never deletes projects or related datasets, jobs, analyses,
quality checks and exports. It always preserves `Kempen Regional Workbench`
and `Mol Municipality Workbench`, defaults to dry-run and can print every
matched name with `--show-names`.
## Governed VHA bathymetry profiles
`POST /api/v1/projects/{project_id}/datasets/bathymetry/profiles/acquire`
performs a bounded official VHA ArcGIS query, exact persisted-Area clipping,
watercourse-name normalization and ordinary Dataset/VectorFeature persistence.
`GET /api/v1/projects/{project_id}/datasets/bathymetry/sources` reports VHA as
operational, MDK as probe-only and the pinned SPW raster operator as
operational.
Runtime controls are `BATHYMETRY_PROFILES_ENABLED`,
`BATHYMETRY_PROFILES_LAYER_URL`, `BATHYMETRY_WATERCOURSE_LAYER_URL`,
`BATHYMETRY_PROFILES_PAGE_SIZE`, `BATHYMETRY_PROFILES_MAX_FEATURES`,
`BATHYMETRY_PROFILES_TIMEOUT_SECONDS` and
`BATHYMETRY_PROFILES_MAX_RESPONSE_MB`. The feature limit intentionally forces
large Flemish scopes into exact Area partitions.
The Dataset exposes profile count and nullable structured depth/width metrics.
It does not claim a continuous bed model, current depth or volume. Use
`scripts/provision_mol_bathymetry_profiles.py` for the canonical Mol operator
flow.
Provision the complete current Flemish land scope and then run the resumable
VHA municipality coordinator:
```bash
docker exec geointel python /app/scripts/provision_flanders_geographic_scope.py
docker exec geointel python /app/scripts/provision_flanders_bathymetry_profiles.py
```
The first command discovers all current VRBG RefGem municipalities, validates
a 270..300 safety range and persists their exact Areas. The second writes its
manifest after every partition. Repeating it reuses completed source
identities; `--force` deliberately refreshes them. A partial `--members` or
`--max-partitions` run never marks regional coverage complete.
Regional map analysis uses
`POST /api/v1/projects/{project_id}/datasets/bathymetry/profiles/partitions/select`.
It selects the latest complete manifest, prefilters overlapping municipality
partitions and performs one PostGIS query over their persisted
`vector_features`. Municipality Areas use only their exact partition. The
response and server-side map export retain every contributing Dataset id and
never substitute a single municipality Dataset for all of Flanders.
Inspect the MDK North Sea WCS without downloading coverage:
```bash
docker exec geointel python /app/scripts/probe_mdk_bathymetry.py
```
Exit code `0` means verified capabilities; `2` means a truthful blocked
readiness state such as TLS or endpoint failure. Runtime controls are
`MDK_BATHYMETRY_PROBE_ENABLED`, `MDK_BATHYMETRY_WCS_URL`,
`MDK_BATHYMETRY_PROBE_TIMEOUT_SECONDS` and
`MDK_BATHYMETRY_PROBE_MAX_RESPONSE_MB`. TLS verification cannot be disabled.
### SPW waterbed raster
The official 2023-05-23 SPW bathymetry ZIP is integrated only through the
bounded operator. Stage the immutable ZIP under persistent storage and run:
```bash
docker exec geointel python /app/scripts/import_spw_bathymetry.py \
--base-url http://127.0.0.1:8000 \
--project-name "Belgium and North Sea Workbench" \
--area "RC Golden - Wallonia urban-rural" \
--bbox 4.85,50.45,4.87,50.47 \
--raw-zip /app/storage/operator-evidence/spw-bathymetry/2023-05-23/raw/BATHY_50CM_ALTITUDE_DNG_GEOTIFF_3812.zip \
--output-dir /app/storage/operator-evidence/spw-bathymetry/2023-05-23/derived
```
The script validates the pinned official checksum, safe archive members,
EPSG:3812, one Float32 band, approximately 0.5 m cells and nodata `-9999`.
It then creates a bounded COG and uploads it through `/datasets/upload`.
`POST .../raster/bathymetry/select` returns waterbed elevation in mDNG,
surveyed surface and coverage. Current depth, volume and datum conversion stay
unavailable without a compatible water-surface source. Selection analysis is
bounded by `BATHYMETRY_RASTER_MAX_PIXELS` (30 million by default).
## Governed regional official-vector acquisition
The thematic raster registry includes forest and agricultural land-use masks
derived from Landgebruik Vlaanderen 2025 classes 12 and 13/14. They use the
existing thematic acquisition and selection routes.
Eight fixed products are exposed through
`/datasets/official-vector/products` and
`/datasets/official-vector/acquire`: INBO BWK/Natura 2000 2025 and DOV soil
for Flanders; PICC buildings, roads, hydrographic axes and surfaces for
Wallonia; and UrbIS buildings and cadastral parcels for Brussels. All require
an EPSG:4326 rectangle, clip in a provider-appropriate metric CRS and persist
through `DatasetService.import_vector_bytes`. SPW/PICC and UrbIS additionally
require a persisted exact regional coverage Area and never write directly to
`vector_features`.
Runtime controls are `OFFICIAL_VECTOR_ENABLED`, `BWK_WFS_URL`,
`DOV_SOIL_WFS_URL`, `SPW_PICC_ENABLED`, `SPW_PICC_MAPSERVER_URL`,
`URBIS_ENABLED`, `URBIS_WFS_URL`, `OFFICIAL_VECTOR_MIN_SIDE_M`,
`OFFICIAL_VECTOR_MAX_SIDE_M`, `OFFICIAL_VECTOR_PAGE_SIZE`,
`OFFICIAL_VECTOR_MAX_PAGES`, `OFFICIAL_VECTOR_MAX_FEATURES`,
`OFFICIAL_VECTOR_TIMEOUT_SECONDS`, `OFFICIAL_VECTOR_MAX_RESPONSE_MB`,
`OFFICIAL_VECTOR_MAX_TOTAL_RESPONSE_MB` and
`OFFICIAL_VECTOR_CACHE_TTL_HOURS`.
## Locked CI dependencies
The release image installs the hashed Linux/Python 3.11 base/GIS graph from
`requirements-runtime.lock`; CI adds test tools through
`requirements-ci.lock`. Both deliberately exclude the optional `ai` extra.
Regenerate and validate them from the repository root with:
```bash
bash scripts/generate_python_lock.sh
python scripts/verify_python_lock.py
```
The complete gate and vulnerability/SBOM policy are documented in
`docs/CI_SUPPLY_CHAIN.md`.
## Release golden areas
The RC browser suite uses seven deterministic, bounded regression areas across
Belgium and the Belgian North Sea. Preview the required changes without
mutating the runtime:
```bash
python scripts/provision_release_golden_areas.py \
--base-url http://127.0.0.1:8000 \
--output artifacts/rc8-golden-areas.json
```
Create only missing Areas through the canonical project/area APIs:
```bash
python scripts/provision_release_golden_areas.py \
--base-url http://127.0.0.1:8000 \
--output artifacts/rc8-golden-areas.json \
--apply
```
The operator copies the governed Mol and Kempen geometries into the national
workbench with their source project/Area identifiers and provisions bounded
Wallonia, Brussels, language-boundary, coast and offshore multi-zone Areas.
Every geometry receives a deterministic SHA-256 fingerprint in the evidence
file. It never imports provider data or writes directly to database tables.
Explicit demo seeding also reactivates its own archived technical project.
This keeps the opt-in fixture workflow selectable without changing the normal
active-project lifecycle.
## Data operations and retention
The runtime packages `audit_data_operations.py`,
`cleanup_storage_artifacts.py` and the shared release-backup guard. The audit
is read-only and combines disk pressure, storage lifecycle, persisted path
integrity, failed-work counts and national/regional/maritime source-family
inventory. Cleanup is limited to old unreferenced derived/cache/export files.
Unknown paths, official source material, uploads, models and release/operator
evidence are protected by default. Apply mode requires an exact confirmation,
an explicit candidate ceiling and a recent checksum-verified database plus
SHA-256 storage backup mounted read-only under `/app/backups`. See
`docs/DATA_OPERATIONS_RUNBOOK.md`.
## Release candidate operations
The semantic release version is stored in the repository `VERSION` file and
is exposed by health responses plus the OCI image version label. Fresh
install, checksum-verified backup, isolated restore/upgrade, rollback,
Belgium/North Sea browser journeys, SBOM, vulnerability evidence, SSH-signed
release manifest and final verification commands are defined in
`docs/RELEASE_RUNBOOK.md`.