Files
geointel/scripts
Jens 5ac3238c27
GeoIntel release gates / Compile, test, contracts and builds (push) Canceled after 0s
GeoIntel release gates / Python and npm vulnerability policy (push) Canceled after 0s
GeoIntel release gates / GIS image, SBOM and container scan (push) Canceled after 0s
Reject same-year post-mosaic building labels
2026-07-30 03:35:15 +02:00
..
2026-06-16 23:36:32 +02:00
2026-07-25 23:43:08 +02:00
2026-07-25 23:43:08 +02:00
2026-07-28 04:44:16 +02:00
2026-07-25 23:43:08 +02:00
2026-07-25 23:43:08 +02:00
2026-06-16 23:36:32 +02:00
2026-07-25 23:43:08 +02:00

Scripts

Setup-, import-, demo- en maintenance-scripts voor GeoIntel.

WALOUS source provisioning

Run the networked operator only after checking at least 3 GB of archive space plus room for the extracted official GeoTIFFs:

python scripts/provision_walous_sources.py \
  --years 2018 2020 2023 \
  --destination storage/source-cache/walous

The command accepts only the hard-coded official SPW 2018/2020/2023 archives, streams with a 1.25 GB per-archive cap, rejects changed content lengths, extracts only the single GeoTIFF by basename, validates the raster contract and writes checksums plus provisioning-report.json. Existing valid sources are reused; --force performs a new download. This is an operator acquisition, not an application startup task.

SPW Wallonia terrain source provisioning

The official 1 m MNT is a large operator asset, never an implicit startup download. Reserve at least 90 GB temporarily for archive plus extraction and run:

python scripts/provision_spw_terrain_source.py \
  --destination storage/source-cache/spw-terrain

The provisioner accepts only the fixed official SPW artifact, enforces a 30-60 GB archive range and a single safe GeoTIFF member, validates EPSG:3812, one band, native 1 m cells and representative elevation samples, writes source/archive SHA-256 evidence and removes the archive after successful extraction unless --keep-archive is supplied.

Runtime verification

Inspect interrupted runtime state without changing it:

python scripts/runtime_state_report.py

Only after reviewing that report, explicitly reconcile records that can no longer be running:

python scripts/runtime_state_report.py \
  --reconcile \
  --confirm reconcile-interrupted-runtime

The default mode is read-only. Reconciliation only marks currently running jobs and analysis runs as failed with PROCESS_INTERRUPTED; it does not delete jobs, results, datasets or artifacts.

Audit the active backend route surface against docs/API_CONTRACTS.md:

python scripts/audit_api_contracts.py

The audit imports the FastAPI app, compares implemented GET/POST/PATCH/ DELETE routes with active API contract headings and tracks the explicit non-envelope exceptions (/health and export downloads). It fails when a route exists without docs or when docs claim an endpoint that is not implemented.

Verify the browser-facing Docker/LAN runtime:

bash 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 scripts/verify_demo_export_workflow.sh http://192.168.10.150:1202

The demo/export smoke is intentionally mutating and idempotent: it seeds the offline fixture demo if needed, verifies the project area GeoJSON, fixture datasets, vector FeatureCollection content, vector feature summary, persisted QA/QC metrics, creates metadata/report/vector GeoJSON exports, lists exports and downloads the JSON/GeoJSON/HTML artifacts through the frontend proxy. The persisted QA/QC result is compared against fixtures/golden/expected_qa_metrics.json so runtime demo precision, recall, F1, mean IoU and false-positive/negative counts cannot drift silently.

Verify the explicit demo raster workflow:

bash scripts/verify_demo_raster_workflow.sh http://192.168.10.150:1202

The raster smoke is intentionally mutating and idempotent enough for local runtime checks: it seeds the offline demo workflow, validates the demo_context_raster.tif fixture dataset, then exercises raster inspect, preview, stats and one small tile/manifest generation through canonical data envelopes. It does not run AI inference or fetch external imagery.

Verify that the browser-facing workbench can populate the default demo start state through the frontend proxy:

bash scripts/verify_workbench_default_state.sh http://192.168.10.150:1202

This smoke is dependency-light and intentionally idempotent: it seeds the offline demo workflow, then verifies that GeoIntel Demo - Building QA exposes the Demo AOI - Geel buildings map geometry, 3/3 ready demo datasets (candidate vector, reference vector and raster fixture) and a persisted QA/QC result through canonical data.items envelopes. Pair it with a Codex/browser screenshot pass when checking visual layout or overflow.

Verify the backing state for the core workbench interactions:

bash scripts/verify_workbench_interactions.sh http://192.168.10.150:1202

This smoke validates the state behind project switching, AOI/map selection, dataset selection, QA refresh and export refresh through the same frontend proxy used by the browser. The frontend also exposes stable data-testid anchors for Codex/browser click checks on those controls.

Verify the browser click handoff from raster tiling into Detection and Segmentation Lab:

bash scripts/verify_ai_handoff_interactions.sh http://192.168.10.150:1202

The AI handoff smoke seeds the explicit offline demo workflow, generates a small raster tile manifest, opens the workbench in Chromium, clicks the raster inspector Use in Detection Lab and Use in Segmentation Lab actions, and verifies that the selected raster dataset plus manifest path are populated in the AI workspace. Playwright/Chromium must be available in the runner environment; GeoIntel does not add Playwright as a frontend dependency by default. The main readiness gate checks this script's syntax only.

Capture visual regression handoff screenshots for the workbench:

bash scripts/capture_workbench_screenshots.sh http://192.168.10.150:1202
CAPTURE_MOBILE=0 bash scripts/capture_workbench_screenshots.sh http://192.168.10.150:1202 /tmp/geointel-screens

The capture script seeds the explicit offline demo workflow, opens each main workspace, captures viewport desktop screenshots and, by default, viewport mobile screenshots. It writes PNG files plus manifest.json under artifacts/screenshots/... or a caller-provided output directory. Playwright/Chromium must be available in the runner environment; GeoIntel does not add Playwright as a frontend dependency by default. The main readiness gate checks script syntax only.

Verify the deterministic QA/QC golden benchmark:

bash scripts/verify_golden_qa_benchmark.sh
python scripts/run_golden_qa_benchmark.py --json

The benchmark uses only explicit local fixtures under fixtures/golden, executes the existing QA/QC matching logic, verifies the expected precision, recall, F1, mean IoU and false-positive/false-negative counts, and checks that QualityCheck plus Metric rows would be persisted. Scenarios are listed in fixtures/golden/golden_qa_benchmarks.json and currently cover partial match, perfect match, no-overlap and MultiPolygon building comparisons. The main readiness gate runs this benchmark so QA metric drift fails before a release.

Verify a configured local YOLO model without running inference:

python scripts/yolo_preflight.py --model-path /absolute/path/to/model.pt --tile-manifest-path /absolute/path/to/manifest.json --check-model-load --json

Against the Docker runtime:

docker compose exec -T backend python scripts/yolo_preflight.py --model-path /absolute/path/to/model.pt --tile-manifest-path /absolute/path/to/manifest.json --check-model-load --json

The model-load smoke is opt-in, requires real optional AI dependencies, refuses --assume-dependencies, loads only the supplied local file and does not download weights or run prediction.

Verify the full configured-YOLO model asset workflow against a running runtime:

bash scripts/verify_model_asset_detection_workflow.sh http://192.168.10.150:1202

This smoke is intentionally mutating and requires a real AI-enabled runtime with at least one mounted local model asset. It seeds the explicit offline demo workflow, generates a small raster tile manifest, selects the active local model asset from GET /api/v1/detection/model-assets, validates read-only YOLO preflight, runs POST /api/v1/detection/run, and verifies the persisted AnalysisRun, Detection list and Detection GeoJSON endpoints. A zero detection count is allowed because the demo raster is a synthetic runtime fixture; the script validates the operational path and provenance, not production model quality. The main readiness gate checks this script's syntax only.

Verify the full operator-provided raster/reference detection and QA path:

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.168.10.150:1202

The current Tower operator sample is available at:

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 \
bash scripts/verify_real_data_detection_qa_workflow.sh http://192.168.10.150:1202

Reuse the definitive Mol municipality project for a bounded Mol-Centrum analysis zone while keeping the municipality-wide reference layer distinct:

REAL_PROJECT_ID=d74c1f87-29c0-4c67-adfc-560764f2b80e \
REAL_PROJECT_NAME='Mol Municipality Workbench' \
REAL_PROJECT_REGION='Mol, Kempen' \
REAL_AREA_NAME='Mol Centrum - AI analysezone 500m' \
REAL_AREA_BBOX='5.113116,51.189653,5.120284,51.194147' \
REAL_DATASET_NAME_PREFIX='mol_center_ai_500m' \
REAL_RASTER_PATH=/mnt/user/appdata/geointel/storage/operator-data/mol_orthophoto_wms_512.tif \
REAL_REFERENCE_VECTOR_PATH=/mnt/user/appdata/geointel/storage/operator-data/mol_grb_gbg_buildings.geojson \
bash scripts/verify_real_data_detection_qa_workflow.sh http://192.168.10.150:1202

REAL_PROJECT_ID is validated through the canonical project endpoint; it does not create a shadow validation project. When REAL_AREA_BBOX is present, both the raster and matching reference upload persist that new Area id. The optional safe filename prefix prevents the bounded sample from sharing a display name with the complete Mol GRB dataset. The workflow still performs no data fetch or model download: the files and configured local model asset must already exist.

Those files are runtime artifacts generated from Digitaal Vlaanderen's OMWRGBMRVL WMS Ortho layer and GRB OGC API Features GBG building collection for a small Geel AOI. They are intentionally not repository fixtures.

To prepare the documented operator samples reproducibly inside the all-in-one runtime container, run:

docker exec -it geointel python3 /app/scripts/prepare_operator_real_data_samples.py

This writes GeoTIFF/GeoJSON pairs and operator_samples_manifest.json under /app/storage/operator-data inside the container, which maps to storage/operator-data in the Tower appdata checkout. The default corpus contains reference AOIs for Geel, Mol, Turnhout, Herentals, Balen, Retie and Westerlo plus background candidates for Postel-bos, Lommel-heide, Kasterlee-bos, Dessel-heide, Ravels-bos, Meerhout-bos, Geel-Bel, Arendonk-heide and Herenthout-bos. Normal reference AOIs still fail when GRB returns no buildings; background candidates are explicitly marked with sample_role and may write an empty reference FeatureCollection for negative-tile training. Generated manifests also classify background samples as pure_empty_negative when GRB returns zero reference buildings or sparse_building_context when GRB returns one or more contextual buildings. The helper fetches only the explicit documented AOIs, records Digitaal Vlaanderen attribution and reuses existing files by default. Use --force only when the local runtime artifacts should be regenerated. GRB building references are fetched through the provider's OGC API rel=next pagination links, so dense AOIs are not silently limited to the first 1000 features. The default page size is 1000; override it with --reference-page-limit or OPERATOR_GRB_PAGE_LIMIT. The safety cap defaults to 100000 features per sample and can be adjusted with --reference-max-features or OPERATOR_GRB_MAX_FEATURES. Generated reference GeoJSON files record reference_pages_fetched, reference_truncated, reference_page_limit, reference_max_features and every fetched source_urls page for auditability.

Mol has a dedicated operational pack with five positive contexts: center, Achterbos residential, Gompel mixed settlement, Donk canal/industrial and Postel rural village. The four new contexts are validation holdouts and are not silently added to training. Postel-bos remains a separate background control. Prepare the 1 km / 1024 px pack explicitly:

docker exec -it geointel python3 /app/scripts/prepare_operator_real_data_samples.py \
  --output-dir /app/storage/operator-data/mol-operational-1024 \
  --samples mol,mol_achterbos,mol_gompel,mol_donk,mol_postel,postel_bos \
  --width 1024 \
  --height 1024 \
  --half-size-scale 2 \
  --force

Then run the existing persisted positive QA and background-control paths as one operator command:

docker exec -it \
  -e OPERATOR_SAMPLE_MANIFEST_PATH=/app/storage/operator-data/mol-operational-1024/operator_samples_manifest.json \
  -e MOL_VALIDATION_OUTPUT_DIR=/app/storage/operator-evidence/mol-operational-validation/current \
  geointel bash /app/scripts/run_mol_operational_validation.sh http://127.0.0.1

Inside the all-in-one image that persistent storage path is also the automatic default. Evidence therefore survives container replacement. Local repository runs keep using artifacts/mol-operational-validation/<timestamp> unless the output variable is set explicitly.

The runner defaults to the active local model at tile 512, overlap 64, confidence 0.15 and QA IoU 0.25. Every positive run persists Project, Area, Dataset, Job, AnalysisRun, Detection, QualityCheck, Metric and Export records. The background run persists its project, AOI, raster, job, analysis and detections but intentionally does not invent QA metrics for an empty or sparse reference context. Each matrix row now preserves the exact persisted inference coverage counts and the diagnostic-only reference-envelope comparison beside the canonical footprint-IoU metrics.

The runner also writes mol_operational_benchmark_report.json and .md. The default operational gates require four positive holdouts, one background control, coverage provenance for every positive run, at least 90% reference coverage in every zone, mean F1 at least 0.25, per-zone F1 at least 0.10 and zero detections in each pure-empty control. Override the numeric gates only through the documented MOL_MIN_MEAN_F1, MOL_MIN_ZONE_F1, MOL_MIN_REFERENCE_COVERAGE and MOL_MAX_BACKGROUND_DETECTIONS variables. Source bbox queries retain complete edge features and can intentionally over-cover the exact projected raster polygon, so the coverage gate detects a gross source/raster mismatch rather than scoring the model. An accepted report records bounded operational evidence; it does not mutate the active model. A review_required report is still a successful benchmark execution but explicitly blocks a promotion recommendation.

For model-training candidates, prepare a larger operator-only sample manifest so tile overlap can create meaningful context instead of one tile per source raster:

docker exec -it geointel python3 /app/scripts/prepare_operator_real_data_samples.py \
  --output-dir /app/storage/operator-data/operator-samples-1024 \
  --manifest-name operator_samples_manifest.json \
  --width 1024 \
  --height 1024 \
  --half-size-scale 2 \
  --reference-page-limit 1000 \
  --reference-max-features 100000 \
  --force

This keeps the same documented AOI centers but requests larger WMS rasters and a larger GRB reference bbox. Use the generated manifest path for the next YOLO tile export. The default remains 512x512 for quick smoke runs.

The real-data smoke is intentionally mutating and refuses to run without operator-supplied files. Current V1 upload support expects a georeferenced .tif, .tiff or .geotiff raster and a .geojson or .json reference building vector. The script creates a project, uploads the raster as a source dataset, uploads the vector as a reference dataset, validates raster/vector metadata, tiles the raster, selects a mounted local model asset, verifies read-only YOLO preflight, runs configured YOLO detection, runs detection QA against persisted vector_features, and exports the detection run as GeoJSON. It does not seed demo data, enable fixture detections, fetch external data or download 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 by the workflow include source CRS metadata so persisted detection GeoJSON coordinates can be transformed to WGS84. A zero detection count is accepted operationally only when the selected model genuinely returns no usable detections after class filtering; it must be interpreted as model/data quality evidence rather than as a successful building extraction result.

Run a confidence-threshold calibration sweep against the same real-data path:

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.168.10.150:1202

The sweep reuses verify_real_data_detection_qa_workflow.sh once per threshold, so every row is backed by persisted Project, Dataset, AnalysisRun, Detection, QualityCheck, Metric and export records. It writes per-threshold logs plus calibration_summary.json under artifacts/detection-calibration/<timestamp> unless CALIBRATION_OUTPUT_DIR is set. This is a calibration/benchmarking tool only: it does not seed demo data, enable fixture detections, fetch external data or download model weights. Per-threshold summaries include persisted detection count, raw candidate count before GeoIntel duplicate suppression, suppressed duplicate count and the configured duplicate IoU threshold.

Run a broader model/tile/threshold quality matrix when multiple local model assets or tile settings need to be compared:

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.168.10.150:1202

The quality matrix repeats the same real-data upload, tiling, configured-YOLO, QA/QC and export workflow for every model/tile/threshold row. It writes per-run logs plus quality_matrix_summary.json under artifacts/detection-quality-matrix/<timestamp> unless QUALITY_OUTPUT_DIR is set. The summary ranks best_by_score, best_by_recall and best_by_precision so the next model decision is based on persisted QualityCheck/Metric evidence rather than visual guesses. It does not create provider data, use fixtures or download model weights. Coverage-aware rows also record raw/evaluated/excluded/clipped candidate and reference counts, tile coverage provenance and the separately labelled box-to-footprint diagnostic gap.

Run the same matrix across every prepared operator sample:

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.168.10.150:1202

The multi-sample wrapper writes one per-sample quality_matrix_summary.json plus a combined multi_sample_quality_summary.json with best_overall_by_score, best_overall_by_recall, best_overall_by_precision and best_by_sample rankings. It resolves container-style /app/storage/... manifest paths to repo-relative storage/... paths when run from the Tower host checkout. Manifest-backed runs also persist the declared EPSG:4326 AOI and municipality region, and retain municipality/operational-zone metadata in the combined summary.

Export the same operator samples to a local YOLO detection dataset when the public model candidates are not strong enough for the target imagery:

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

The exporter writes dataset.yaml, images/train, labels/train, images/val, labels/val and yolo_dataset_summary.json. It uses only the explicit operator sample manifest and GRB building references where source_name=grb and reference_layer_name=buildings. It does not call GeoIntel APIs, create provider data, run inference or train a model.

Run a small local training smoke only in an AI-enabled runtime with an existing local base model file:

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 \
  geointel bash /app/scripts/train_operator_yolo_detector.sh

The training wrapper is intentionally outside the product UI. It runs Ultralytics from the existing runtime, copies the best trained artifact to TRAIN_MODEL_OUTPUT_PATH and writes training_summary.json. The summary records SHA256 provenance for dataset.yaml, the available YOLO dataset summary, the local base model and the copied trained model. Afterward, treat the resulting .pt file like any other local model asset: verify preflight, run the real-data matrix and compare persisted QA/QC metrics before activating it as a useful default. Inside the all-in-one image the wrapper prefers /opt/geointel/venv/bin/python when that AI runtime exists. Set PYTHON_BIN only when intentionally overriding the interpreter.

When whole-image training does not improve QA/QC, export a tile-level dataset with overlapping raster windows:

docker exec -it geointel python3 /app/scripts/export_operator_yolo_tile_dataset.py \
  --manifest-path /app/storage/operator-data/operator-samples-1024/operator_samples_manifest.json \
  --output-dir /app/storage/operator-data/yolo-building-aoi1024-visible025 \
  --tile-size 512 \
  --stride 256 \
  --negative-keep-ratio 1.0 \
  --min-label-visible-ratio 0.25 \
  --val-samples turnhout,retie,westerlo,arendonk_heide \
  --force

The tile exporter clips GRB building bounding boxes into each tile, writes YOLO labels beside each tile image, keeps a deterministic ratio of empty negative tiles, and records yolo_tile_dataset_summary.json with positive_tile_count, negative_tile_count and skipped negative tile counts. It separately records configured, retained and empty validation sample slugs; this keeps a holdout that lost every tile to quality filtering visible without pretending it contributed evaluation data. --min-label-visible-ratio drops labels where only a small clipped fragment of the original building bbox is visible inside the tile; this reduces noisy tile-edge labels in overlapping-tile datasets. Use 0 for legacy behavior. Use --drop-low-variance-negatives to skip negative tiles whose rendered image has a max-min pixel range at or below --blank-range-threshold. This gate is intended for blank/no-data pure-empty negatives only; positive/labeled tiles are not removed by this filter. The summary records skipped_low_variance_negative_tile_count and skipped tile records with skip_reason=low_visual_variance_negative. For legacy operator manifests that predate explicit background_category, the exporter derives the same categories as the split-background evaluator: background samples with reference_feature_count == 0 become pure_empty_negative, and background samples with one or more reference features become sparse_building_context. It remains operator tooling only: no provider fetch, no API mutation and no automatic model training.

For the current AOI1024 baseline, prefer the stricter clean-label profile before spending another training run:

docker exec -it geointel python3 /app/scripts/export_operator_yolo_tile_dataset.py \
  --manifest-path /app/storage/operator-data/operator-samples-1024/operator_samples_manifest.json \
  --output-dir /app/storage/operator-data/yolo-building-aoi1024-cleanpx12vis035 \
  --tile-size 512 \
  --stride 256 \
  --negative-keep-ratio 1.0 \
  --min-label-px 12 \
  --min-label-visible-ratio 0.35 \
  --drop-low-variance-negatives \
  --blank-range-threshold 3 \
  --val-samples turnhout,retie,westerlo,arendonk_heide \
  --force

This refreshed cleanpx dataset is the minimum pre-training baseline after the visual contact-sheet pass found six blank-looking arendonk_heide validation negatives in the older export.

The persisted false-negative audit subsequently showed that the cleanpx12 candidate still misses about 79-92% of the comparable reference population and misses every reference building below 25 m2 in the seven-AOI review. Do not train another candidate from the same four positive training AOIs. Refresh the existing AOI1024 sample directory after pulling Sprint 171; existing files are reused and only the four new explicit positive AOIs need to be fetched:

docker exec -it geointel python3 /app/scripts/prepare_operator_real_data_samples.py \
  --output-dir /app/storage/operator-data/operator-samples-1024 \
  --width 1024 \
  --height 1024 \
  --half-size-scale 2

The expansion adds Olen, Lille, Oud-Turnhout and Kasterlee center as training samples. Turnhout, Retie, Westerlo and Arendonk-heide remain explicit validation holdouts in generated manifest provenance. The tile exporter defaults to those four holdouts and rejects a manifest-aware split that leaks one into training. Use the lower min-label-px=4 profile first to measure small-building retention; it remains subject to dataset audit and visual contact-sheet review before any training:

docker exec -it geointel python3 /app/scripts/export_operator_yolo_tile_dataset.py \
  --manifest-path /app/storage/operator-data/operator-samples-1024/operator_samples_manifest.json \
  --output-dir /app/storage/operator-data/yolo-building-aoi1024-expanded-minpx4vis035 \
  --tile-size 512 \
  --stride 256 \
  --negative-keep-ratio 1.0 \
  --min-label-px 4 \
  --min-label-visible-ratio 0.35 \
  --drop-low-variance-negatives \
  --blank-range-threshold 3 \
  --force

Then audit with stricter small-box gates:

docker exec -it geointel python3 /app/scripts/audit_operator_yolo_dataset_quality.py \
  --summary-path /app/storage/operator-data/yolo-building-aoi1024-cleanpx12vis035/yolo_tile_dataset_summary.json \
  --output-dir /app/artifacts/operator-yolo-dataset-audit/aoi1024-cleanpx12vis035 \
  --max-small-box-share 0.25 \
  --min-median-box-area 0.001

Audit the generated tile dataset before spending another long training run:

python scripts/audit_operator_yolo_dataset_quality.py \
  --summary-path /mnt/user/appdata/geointel/storage/operator-data/yolo-building-tile-hardneg160r8/yolo_tile_dataset_summary.json \
  --output-dir /mnt/user/appdata/geointel/artifacts/operator-yolo-dataset-audit/hardneg160r8

The audit reads the tile summary and YOLO label files, then writes operator_yolo_dataset_quality_audit.json and operator_yolo_dataset_quality_audit.md. It reports positive/background sample coverage, train/validation split coverage, repeated hard-negative pressure, minimum visible label ratio, missing or invalid label rows, normalized box-area signals and per-sample label diagnostics such as parsed label count, median box area, small-box share and sample-specific quality warnings. Treat needs_attention as a dataset-design warning, not as a runtime failure: the next action is usually more positive AOIs, better validation coverage or more unique hard negatives rather than simply extending epochs.

Render visual label QA contact sheets before spending CPU on another training run:

docker exec -it geointel python3 /app/scripts/render_operator_yolo_label_qa_contact_sheets.py \
  --summary-path /app/storage/operator-data/yolo-building-aoi1024-cleanpx12vis035/yolo_tile_dataset_summary.json \
  --output-dir /app/artifacts/operator-yolo-label-qa/aoi1024-cleanpx12vis035 \
  --max-tiles 32 \
  --columns 4 \
  --thumb-size 256

The renderer writes operator_yolo_label_qa_summary.json, operator_yolo_label_qa_contact_sheet.md and contact_sheet_001.png. It draws existing YOLO labels on existing tile images only; it does not run inference, train a model, fetch providers or create fake detections. Missing image files, missing label files, invalid YOLO rows and low-variance/blank-looking rendered tiles are reported in the JSON/Markdown artifacts. Selection is balanced by sample_slug before taking additional high-density tiles, so one dense urban AOI cannot hide the other source samples from visual review. The report records selected_sample_count and selected_sample_slugs for coverage evidence.

Current Tower audit status:

  • yolo-building-tile-expanded160: clean baseline; no missing/invalid labels.
  • yolo-building-tile-hardneg160r4 and yolo-building-tile-hardneg160r8: repeat-heavy hard-negative variants; useful evidence, but add more unique background AOIs before training another hard-negative-balanced candidate.
  • Regenerate operator_samples_manifest.json after pulling Sprint 147+ so the expanded unique background AOI set is available for the next tile export.
  • yolo-building-tile-uniquehardneg160: clean expanded-background baseline; 576 tiles, 346 positive, 230 negative, 11,757 labels, 0 invalid labels and 0 repeated background negatives in the first Tower audit.
  • yolo-building-aoi1024-visible025: larger AOI candidate baseline regenerated after paged GRB references; 144 tiles, 117 positive tiles, 27 negative tiles, 29,170 labels, 0 missing label files, 0 invalid labels and min_label_visible_ratio=0.25. Its audit remains needs_attention because the median normalized box area is still small (0.000762939453125) and the small-box share is high (0.34744600617072335). The previously trained geointel-building-yolov8s-aoi1024visible025e50-pt asset is inactive; do not retrain or activate from this dataset without addressing label quality or explicitly accepting the small-box warning.
  • yolo-building-aoi1024-cleanpx12vis035: stricter clean-label AOI1024 export; 144 tiles, 114 positive tiles, 30 negative tiles, 14,632 labels, min_label_px=12, min_label_visible_ratio=0.35, 0 invalid labels, median normalized box area 0.001373291015625 and small-box share 0.0. The trained geointel-building-yolov8s-aoi1024cleanpx12vis035e50-pt asset is available but inactive. The split-aware promotion report rejected all tested thresholds: 0.25 and 0.35 passed the pure-empty background gate but had mean F1 below 0.25; 0.15 also failed the pure-empty false-positive gate.
  • yolo-building-aoi1024-expanded-minpx4vis035: expanded small-building recovery dataset with 20 source AOIs, 171 retained tiles, 45,892 labels, 144 train tiles, 27 validation tiles and 9 low-variance negatives removed. Its configured audit passed with no warnings, median normalized box area 0.000694274766, small-box share 0.3832694151486098, no invalid labels and no missing label files. The balanced visual pass rendered 40 tiles across all 19 source samples that retained at least one tile, with no invalid labels, missing images or low-variance selections. Its promoted model remains the higher-precision legacy 0.15 operator profile.
  • yolo-building-aoi1024-smallbld-minpx3vis035: focused small-building corpus exported from an explicit 23-sample subset. Beerse, Rijkevorsel, Hoogstraten and Vorselaar extend training; Vosselaar and Grobbendonk are validation-only; Turnhout, Retie and Westerlo remain external operation-level holdouts. The Tower export retained 198 tiles and 58,820 labels. Its small-object-aware audit passed with no invalid/missing labels, and the 48-tile balanced visual review contained no missing, invalid or low-variance selections. The trained geointel-building-yolov8s-smallbld-minpx3-img640-ft30-pt candidate passed seven positive-AOI and three pure-empty background gates at tile 512, overlap 64, threshold 0.15 and QA match IoU 0.25. Original promotion evidence measured mean F1 0.5825; the later coverage-aligned rerun measured 0.6069. All pure-empty samples remain at zero detections. The original persisted comparison found 1,571 fewer false negatives than the previous balanced model, with a lower mean precision and therefore a higher operator review load.
  • yolo-building-aoi1024-reviewedexp6-minpx3vis035: leakage-free accuracy expansion of the focused corpus with new training-only AOIs in Arendonk, Dessel, Meerhout, Laakdal, Nijlen and Hulshout. The paged GRB preparation added 9,964 reference features. The export retained 252 tiles (234 positive, 18 negative) and 79,192 labels; its configured audit and 64-tile visual review found no invalid, missing or low-variance selections. The inactive geointel-building-yolov8s-reviewedexp6-minpx3-img640-ft20-pt model improved coverage-aligned seven-zone mean F1 from 0.6069 to 0.6248, but its two detections in empty Postel-bos failed the strict background gate. The active focused model remains unchanged.

Use --samples or OPERATOR_YOLO_SAMPLES to make an experimental corpus membership explicit. Dataset summaries preserve the complete manifest count, selected sample slugs and excluded sample slugs. Split validation still applies after filtering, so a manifest-backed holdout cannot be selected as training by omitting it from --val-samples.

After rebuilding the all-in-one image, the operator scripts are available inside the container at /app/scripts/.... Before rebuilding, use the host checkout or temporarily copy scripts into the running container for one-off data prep.

For hard-negative-balanced experiments, repeat only train-split negative tiles from samples marked sample_role=background_candidate:

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-hardneg160r8 \
  --tile-size 160 \
  --stride 80 \
  --negative-keep-ratio 1.0 \
  --background-negative-repeat 8 \
  --val-samples turnhout,retie,kasterlee_bos \
  --force

The repeat option can also be set with OPERATOR_YOLO_BACKGROUND_NEGATIVE_REPEAT. It does not duplicate validation tiles, positive tiles or normal reference-sample negatives. Repeated background tiles receive deterministic _hnXX filenames and tile metadata records sample_role, repeat_index and is_repeated_background_negative.

Train against the tile dataset by pointing the existing wrapper at the tile output directory:

docker exec \
  -e OPERATOR_YOLO_DATASET_DIR=/app/storage/operator-data/yolo-building-tile-expanded160 \
  -e YOLO_BASE_MODEL_PATH=/app/models/yolov8n.pt \
  -e TRAIN_OUTPUT_DIR=/app/storage/training/operator-yolo \
  -e TRAIN_RUN_NAME=geointel-building-yolov8n-expanded160e50 \
  -e TRAIN_MODEL_OUTPUT_PATH=/app/models/geointel-building-yolov8n-expanded160e50.pt \
  -e TRAIN_EPOCHS=50 \
  -e TRAIN_IMGSZ=256 \
  -e TRAIN_BATCH=8 \
  -e TRAIN_WORKERS=0 \
  -e TRAIN_DEVICE=cpu \
  geointel bash /app/scripts/train_operator_yolo_detector.sh

Benchmark any trained candidate through the same persisted QA/QC matrix before using it operationally:

OPERATOR_SAMPLE_MANIFEST_PATH=storage/operator-data/operator_samples_manifest.json \
OPERATOR_SAMPLE_SLUGS="geel mol turnhout retie 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" \
MULTI_SAMPLE_OUTPUT_DIR=artifacts/detection-quality-matrix/multi-sample/expanded160e50-live \
bash scripts/run_multi_sample_detection_quality_matrix.sh http://192.168.10.150:1202

The expanded 50-epoch candidate improved dense Geel/Mol/Turnhout/Retie scores, but the sparse Kasterlee-bos run still showed too many false positives. Treat it as the best current experimental dense-AOI candidate, not as a V1 default.

Run a dedicated hard-negative matrix against documented background candidates before changing model defaults:

OPERATOR_SAMPLE_MANIFEST_PATH=storage/operator-data/operator_samples_manifest.json \
OPERATOR_BACKGROUND_SAMPLE_SLUGS="postel_bos lommel_heide kasterlee_bos dessel_heide ravels_bos meerhout_bos geel_bel arendonk_heide herenthout_bos" \
QUALITY_MODEL_ASSET_IDS="geointel-building-yolov8s-aoi1024bg512r3e50-pt" \
QUALITY_TILE_SIZES="512" \
QUALITY_TILE_OVERLAPS="64" \
QUALITY_THRESHOLDS="0.35 0.15" \
BACKGROUND_SPLIT_OUTPUT_DIR=artifacts/detection-hard-negatives/aoi1024bg512r3e50-split \
bash scripts/run_background_corpus_split_matrix.sh http://192.168.10.150:1202

The split runner executes the strict pure_empty_negative matrix and the review-only sparse_building_context matrix as separate runs, then writes background_corpus_split_summary.json and background_corpus_split_summary.md. Use the pure-empty block for the default-promotion false-positive gate; use sparse-context results as review evidence only.

The lower-level hard-negative matrix can still be run directly:

OPERATOR_SAMPLE_MANIFEST_PATH=storage/operator-data/operator_samples_manifest.json \
OPERATOR_BACKGROUND_CATEGORIES="pure_empty_negative" \
OPERATOR_BACKGROUND_SAMPLE_SLUGS="postel_bos lommel_heide kasterlee_bos dessel_heide ravels_bos meerhout_bos geel_bel arendonk_heide herenthout_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" \
HARD_NEGATIVE_OUTPUT_DIR=artifacts/detection-hard-negatives/expanded160e50-live \
bash scripts/run_operator_hard_negative_detection_matrix.sh http://192.168.10.150:1202

The hard-negative matrix uploads only the background raster, generates tiles, runs configured-YOLO detection and counts persisted detections as false_positive_pressure. It does not upload a reference vector and does not run QA/QC, because empty or sparse background AOIs do not have a meaningful precision/recall target. Use OPERATOR_BACKGROUND_CATEGORIES="pure_empty_negative" for the strict default-promotion false-positive gate. Run OPERATOR_BACKGROUND_CATEGORIES="sparse_building_context" separately for contextual review; sparse-context detections should be inspected, not counted as fake precision/recall metrics. In the first live run, geointel-building-yolov8n-expanded160e50-pt was clean on Postel-bos and Lommel-heide at thresholds 0.25 and 0.15, but produced 38 detections on Kasterlee-bos even at 0.25. That blocks it from becoming a V1 default until a hard-negative-balanced candidate improves.

The hard-negative-balanced geointel-building-yolov8n-hardneg160r8e40-pt candidate reduced Kasterlee-bos false-positive pressure to 5/9/25 detections at thresholds 0.25/0.15/0.05 and stayed at 0 detections on Postel-bos and Lommel-heide across all tested thresholds. It also regressed dense-AOI F1 against geointel-building-yolov8n-expanded160e50-pt, so it is useful model quality evidence but not a V1 default.

An external remote-sensing YOLOv8l candidate was also benchmarked as an operator-provided local model asset:

mkdir -p models
curl -L --fail \
  -o models/yolo-remote-sensing-photovoltaic-v8l-detect-1000.pt \
  https://huggingface.co/agademer/yolo-remote-sensing-photovoltaic/resolve/main/yolo-remote-sensing-photovoltaic-v8l-solar-farms-and-cities-v20260331-detect-1000_epochs.pt

GeoIntel exposed the file as yolo-remote-sensing-photovoltaic-v8l-detect-1000-pt with SHA256 242ff4ab889569278f0eb9fcd22eb2c4bf2a52e48d05d89cc7cfa7941165d203, and YOLO preflight loaded it without downloads. On the live dense matrix it missed most buildings and scored far below geointel-building-yolov8n-expanded160e50-pt on Geel, Mol, Turnhout and Retie. On Kasterlee-bos it was clean and precise, but that sparse-AOI behavior is not enough for V1 extraction. Keep it as benchmark evidence only, not as a default model.

Export calibration QA evidence for visual review:

CALIBRATION_SUMMARY_PATH=/mnt/user/appdata/geointel/artifacts/detection-calibration/20260707T002103Z/calibration_summary.json \
bash scripts/export_detection_calibration_evidence.sh http://192.168.10.150:1202

Browser Detection Lab calibration summary exports are supported too:

bash scripts/export_detection_calibration_evidence.sh http://192.168.10.150:1202 ./detection-calibration-summary.json

Run the local browser-summary evidence bundle smoke without touching live application data:

bash scripts/smoke_detection_calibration_evidence_bundle.sh

The smoke creates a temporary Detection Lab-style calibration summary, mocks the canonical persisted QA evidence endpoint responses, runs the real evidence exporter and verifies that calibration_evidence.geojson, calibration_evidence_summary.json and calibration_evidence_review.html are written correctly.

Assemble multiple AOI evidence bundles into one model-review portfolio:

bash scripts/assemble_detection_calibration_evidence_portfolio.sh \
  http://192.168.10.150:1202 \
  ./calibration-evidence-portfolio-manifest.json

Example calibration-evidence-portfolio-manifest.json:

{
  "portfolio_name": "Kempen building model calibration",
  "model_asset_id": "geointel-building-yolov8s-hardneg160r4e50-pt",
  "model_sha256": "optional-model-checksum",
  "notes": "Operator comparison notes.",
  "samples": [
    {
      "sample_slug": "geel",
      "aoi_label": "Geel center",
      "summary_path": "/path/to/detection-calibration-summary.json",
      "operator_notes": "Dense urban validation sample."
    }
  ]
}

The portfolio assembler copies each summary into a deterministic sample folder, runs the existing evidence exporter per AOI and writes calibration_evidence_portfolio.json plus calibration_evidence_portfolio.md. It is evidence packaging only: it does not run inference, create QA checks or mutate application data.

The evidence export reads each persisted quality_check_id, calls the existing QA evidence GeoJSON endpoint, writes calibration_evidence.geojson, calibration_evidence_summary.json and a standalone calibration_evidence_review.html with an SVG overview of matched detections, matched references, false positives and false negatives. Set CALIBRATION_EVIDENCE_MODE=best to export only the best_by_score run.

Build fixed-threshold portfolio inputs when two model runs must be compared at the same confidence threshold across every AOI:

python scripts/build_fixed_threshold_evidence_portfolio_inputs.py \
  --multi-sample-summary artifacts/detection-quality-matrix/multi-sample/<run>/multi_sample_quality_summary.json \
  --threshold 0.35 \
  --model-asset-id geointel-building-yolov8s-aoi1024bg512r3e50-pt \
  --model-sha256 e0980572aac90e7efc514608eb16d7de5bfbf27a4bbec04e7bc1bc8c02f9601f \
  --tile-size 512 \
  --tile-overlap 64 \
  --output-dir artifacts/detection-false-negative-review/active-inputs

The builder selects exactly one persisted QA run per AOI and refuses ambiguous model/tile/threshold matches. Pass its emitted manifest to assemble_detection_calibration_evidence_portfolio.sh with CALIBRATION_EVIDENCE_MODE=all; each filtered summary contains one run.

Compare two or more downloaded evidence portfolios with geodetic WGS84 areas:

python scripts/audit_detection_false_negative_evidence.py \
  --portfolio active=artifacts/detection-false-negative-review/active/calibration_evidence_portfolio.json \
  --portfolio candidate=artifacts/detection-false-negative-review/candidate/calibration_evidence_portfolio.json \
  --output-dir artifacts/detection-false-negative-review/audit

The audit reports false-negative rates and area buckets per AOI/model, plus reference buildings missed by every compared portfolio. It writes the combined persistent_false_negatives.geojson, records geodetic persistent-miss area and adds persistent area buckets so operators can inspect the shared misses on a map instead of relying only on counts. Stable source_feature_id values are preferred; a normalized geometry fingerprint is used only when source IDs are absent. Invalid or missing geometry fails the audit instead of being silently skipped. The tools do not run inference, create QA records, mutate model defaults or download data/models.

Audit the false-positive review load of one persisted evidence portfolio before turning detections into hard-negative training input:

python scripts/audit_detection_false_positive_evidence.py \
  --portfolio artifacts/model-review/small-building-candidate/evidence-portfolio/calibration_evidence_portfolio.json \
  --output-dir artifacts/model-review/small-building-candidate/false-positive-audit

The command validates the portfolio role counts against each persisted evidence GeoJSON, rejects invalid/non-polygon geometry, computes WGS84 geodesic area and size buckets, and reports false-positive pressure per AOI, class and AOI-qualified source tile. It writes detection_false_positive_audit.json, a Markdown handoff and combined false_positives.geojson for map review. Original evidence properties and geometry are preserved. Confidence statistics are only computed when confidence is actually present in persisted evidence; missing coverage is reported explicitly and never inferred from the run threshold.

Render a deterministic, stratified visual review over persisted false-positive evidence. Static portfolios created before detection provenance was added must first be re-exported from the current backend; existing QualityCheck and Detection rows do not need to be rerun:

docker exec \
  -e CALIBRATION_EVIDENCE_MODE=all \
  -e CALIBRATION_PORTFOLIO_OUTPUT_DIR=/app/storage/operator-data/model-review/small-building-candidate/evidence-portfolio-enriched \
  geointel bash /app/scripts/assemble_detection_calibration_evidence_portfolio.sh \
    http://127.0.0.1 \
    /app/storage/operator-data/model-review/small-building-candidate/evidence-inputs/calibration-evidence-portfolio-manifest.json

docker exec geointel /opt/geointel/venv/bin/python \
  /app/scripts/render_detection_false_positive_review_contact_sheets.py \
  --portfolio /app/storage/operator-data/model-review/small-building-candidate/evidence-portfolio-enriched/calibration_evidence_portfolio.json \
  --storage-root /app/storage \
  --output-dir /app/storage/operator-data/model-review/small-building-candidate/false-positive-visual-review \
  --sample-slugs turnhout,herentals,geel \
  --max-features 48 \
  --columns 4 \
  --cards-per-sheet 16 \
  --thumb-size 256

The renderer validates source paths against --storage-root, checks persisted confidence/bbox/tile provenance, samples across AOI, WGS84 area bucket and confidence band, and overlays persisted matched/missed reference polygons. It writes PNG sheets, a JSON/Markdown summary and false_positive_review_decisions.csv with every row set to unreviewed.

After manual inspection, validate the edited CSV:

docker exec geointel /opt/geointel/venv/bin/python \
  /app/scripts/validate_detection_false_positive_review_decisions.py \
  --review-summary /app/storage/operator-data/model-review/small-building-candidate/false-positive-visual-review/detection_false_positive_review_summary.json \
  --decisions-csv /app/storage/operator-data/model-review/small-building-candidate/false-positive-visual-review/false_positive_review_decisions.csv \
  --output-dir /app/storage/operator-data/model-review/small-building-candidate/false-positive-visual-review/validated \
  --require-complete

--require-complete exits with code 2 while any record is still unreviewed. Only explicit confirmed_model_false_positive decisions are written to confirmed_model_false_positives.geojson; the tool never promotes generic QA false-positives into training labels.

Render persisted false negatives against the exact tile manifest recorded by the selected analysis run:

docker exec geointel /opt/geointel/venv/bin/python \
  /app/scripts/render_detection_false_negative_review_contact_sheets.py \
  --portfolio /app/storage/operator-evidence/model-review/portfolio/calibration_evidence_portfolio.json \
  --storage-root /app/storage \
  --output-dir /app/storage/operator-evidence/model-review/false-negative-visual-review \
  --sample-slugs mol_donk,mol_postel \
  --max-features 48 \
  --columns 4 \
  --cards-per-sheet 12 \
  --thumb-size 256

The read-only renderer resolves the one persisted manifest_path from each fixed-threshold sample summary, confines manifests and source tiles to --storage-root, and projects WGS84 missed-reference polygons onto the real source tiles. Red is the missed reference, blue is persisted candidate geometry and green is a matched reference. Selection is deterministic and stratified by AOI and geodetic area bucket. Every CSV decision starts as unreviewed; no positive-training example is inferred.

Validate the completed false-negative decisions symmetrically with the false-positive workflow:

docker exec geointel /opt/geointel/venv/bin/python \
  /app/scripts/validate_detection_false_negative_review_decisions.py \
  --review-summary /app/storage/operator-data/model-review/small-building-candidate/false-negative-visual-review/detection_false_negative_review_summary.json \
  --decisions-csv /app/storage/operator-data/model-review/small-building-candidate/false-negative-visual-review/false_negative_review_decisions.csv \
  --output-dir /app/storage/operator-data/model-review/small-building-candidate/false-negative-visual-review/validated \
  --require-complete

The validator exits with code 2 while any row remains unreviewed. It emits only explicit confirmed_model_false_negative geometries; alignment, reference-gap and uncertain imagery decisions never become positive training labels.

Reference features that do not intersect any persisted inference tile are not silently counted as reviewable model misses. They are reported separately in false_negatives_outside_tile_coverage.geojson with review_exclusion_reason=outside_tile_coverage. Fix the QA evaluation population before using those records in recall or training decisions.

Docker images install only the GIS runtime by default. To build a local/Tower image with PyTorch/Ultralytics available for the configured-YOLO preflight and runtime path, set:

GEOINTEL_INSTALL_AI=true

For Unraid/all-in-one deployments, place model files under GEOINTEL_MODELS_PATH so they appear in the container under /app/models, then set YOLO_ENABLED=true, YOLO_MODELS_DIR=/app/models and YOLO_MODEL_PATH=/app/models/<model>.pt.

Configure the Unraid/Tower env file from an existing local model without downloading weights or running inference:

python scripts/configure_yolo_model.py \
  --models-dir /mnt/user/appdata/geointel/models \
  --env-file /mnt/user/appdata/geointel/.env

If exactly one supported model file (.pt, .onnx or .engine) is present, apply the env update explicitly:

python scripts/configure_yolo_model.py \
  --models-dir /mnt/user/appdata/geointel/models \
  --env-file /mnt/user/appdata/geointel/.env \
  --apply

The configurator refuses to proceed when no model exists or when multiple model files are present without --model-file. It writes only GEOINTEL_INSTALL_AI=true, YOLO_ENABLED=true, YOLO_MODELS_DIR=/app/models and the mounted YOLO_MODEL_PATH.

When a split-background promotion report recommends a specific candidate, use the guarded activation helper instead of choosing a model path manually. The helper validates the exact candidate_key, promotion status and local model asset before writing anything, and it mutates .env only with --apply:

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 updates. The helper never downloads weights, loads the model or runs inference; restart or rebuild the container after applying because YOLO_MODEL_PATH is read from the environment.

Tower-local model evaluation status:

  • geointel-building-yolov8s-hardneg160r4e50.pt is available as an evaluated local runtime artifact after the hard-negative YOLOv8s training pass.
  • The live catalog id is geointel-building-yolov8s-hardneg160r4e50-pt.
  • The model SHA256 is 9bf71ad4742048ac77f07060b677bacd9757b8d310497fcada334d543e320d19.
  • The current safest observed threshold is 0.25, but the model remains an evaluation candidate because one hard-negative forest sample still produced false detections at that threshold.
  • Do not silently activate this model as a default. Apply it only as an explicit operator choice until the model catalog/threshold workflow is hardened.

Build a model promotion decision report from an existing positive-AOI evidence portfolio and one or more hard-negative/background summaries:

python scripts/build_detection_model_promotion_report.py \
  --positive-portfolio /mnt/user/appdata/geointel/artifacts/detection-calibration-portfolio/positive-aoi-expanded-20260708/output/calibration_evidence_portfolio.json \
  --hard-negative-summary /mnt/user/appdata/geointel/artifacts/detection-hard-negatives/hardneg160r8e40-live/hard_negative_matrix_summary.json \
  --output-dir /mnt/user/appdata/geointel/artifacts/detection-model-promotion/expanded-positive-vs-hard-negative-20260708

The report writes detection_model_promotion_report.json and detection_model_promotion_report.md. It groups candidates by model_asset_id, tile size, tile overlap and confidence threshold, then applies explicit gates for positive-AOI sample count, background sample count, mean F1 and maximum background detections per sample. It is evidence/report tooling only: it does not run inference, mutate application data, download models or change the active YOLO configuration.

When the background corpus has been split with run_background_corpus_split_matrix.sh, pass the combined split summary instead of manually wiring both category summaries:

python scripts/build_detection_model_promotion_report.py \
  --positive-portfolio /mnt/user/appdata/geointel/artifacts/detection-quality-matrix/multi-sample/aoi1024bg512r3e50-positive/multi_sample_quality_summary.json \
  --background-split-summary /mnt/user/appdata/geointel/artifacts/detection-hard-negatives/aoi1024bg512r3e50-split/background_corpus_split_summary.json \
  --output-dir /mnt/user/appdata/geointel/artifacts/detection-model-promotion/aoi1024bg512r3e50-split-aware \
  --min-positive-samples 7 \
  --min-background-samples 2 \
  --min-mean-f1 0.25 \
  --max-background-detections-per-sample 0

The promotion report resolves the split summary's pure_empty_negative source summary as the strict default-promotion false-positive gate. The sparse_building_context source remains visible in the JSON/Markdown report as review evidence only and is not counted as a default-promotion gate.

To run both steps after one redeploy, use the workflow wrapper:

PROMOTION_POSITIVE_PORTFOLIO_PATH=/mnt/user/appdata/geointel/artifacts/detection-quality-matrix/multi-sample/aoi1024bg512r3e50-positive/multi_sample_quality_summary.json \
OPERATOR_SAMPLE_MANIFEST_PATH=storage/operator-data/operator_samples_manifest.json \
QUALITY_MODEL_ASSET_IDS="geointel-building-yolov8s-aoi1024bg512r3e50-pt" \
QUALITY_TILE_SIZES="512" \
QUALITY_TILE_OVERLAPS="64" \
QUALITY_THRESHOLDS="0.35 0.15" \
BACKGROUND_SPLIT_OUTPUT_DIR=artifacts/detection-hard-negatives/aoi1024bg512r3e50-split \
PROMOTION_OUTPUT_DIR=artifacts/detection-model-promotion/aoi1024bg512r3e50-split-aware \
bash scripts/run_split_background_promotion_workflow.sh http://192.168.10.150:1202

The wrapper first calls run_background_corpus_split_matrix.sh, then feeds the generated background_corpus_split_summary.json into the split-aware promotion report. It still uses only existing upload, detection and report paths; it does not fetch providers, fetch model weights or activate a default.

Run the same command with --preflight-only first when checking a fresh redeploy. Preflight validates the positive portfolio path, operator manifest presence, required pure_empty_negative and sparse_building_context background categories, Python/curl availability and the frontend API proxy envelope without starting inference:

For older operator manifests that predate explicit background_category, preflight uses the same fallback as the matrix runner: background samples with reference_feature_count == 0 are treated as pure_empty_negative, and background samples with references are treated as sparse_building_context.

PROMOTION_POSITIVE_PORTFOLIO_PATH=/mnt/user/appdata/geointel/artifacts/detection-quality-matrix/multi-sample/aoi1024bg512r3e50-positive/multi_sample_quality_summary.json \
OPERATOR_SAMPLE_MANIFEST_PATH=storage/operator-data/operator_samples_manifest.json \
bash scripts/run_split_background_promotion_workflow.sh --preflight-only http://192.168.10.150:1202

If a legacy positive evidence portfolio records model_asset_id at portfolio level but does not include per-run tile size/overlap, pass explicit tile defaults instead of letting the report guess:

python scripts/build_detection_model_promotion_report.py \
  --positive-portfolio /mnt/user/appdata/geointel/artifacts/detection-calibration-portfolio/uniquehardneg160e50-positive/calibration_evidence_portfolio.json \
  --hard-negative-summary /mnt/user/appdata/geointel/artifacts/detection-hard-negatives/uniquehardneg160e50/hard_negative_matrix_summary.json \
  --output-dir /mnt/user/appdata/geointel/artifacts/detection-model-promotion/uniquehardneg160e50-positive-vs-hard-negative \
  --min-positive-samples 7 \
  --min-background-samples 9 \
  --min-mean-f1 0.25 \
  --max-background-detections-per-sample 0 \
  --default-positive-tile-size 640 \
  --default-positive-tile-overlap 64

Clean old offline demo export artifacts without touching uploaded source data:

python scripts/cleanup_demo_artifacts.py
python scripts/cleanup_demo_artifacts.py --keep-latest 10 --export-type project_report_html
python scripts/cleanup_demo_artifacts.py --keep-latest 10 --max-delete 100 --apply

Against the Docker runtime, run the backend-container entrypoint:

docker compose exec -T backend python scripts/cleanup_demo_artifacts.py
docker compose exec -T backend python scripts/cleanup_demo_artifacts.py --keep-latest 10 --export-type project_report_html
docker compose exec -T backend python scripts/cleanup_demo_artifacts.py --keep-latest 10 --max-delete 100 --apply

The cleanup script is dry-run by default. It only targets the explicit GeoIntel Demo - Building QA project unless --project-name is provided, keeps the newest exports per matching project, deletes only exports rows/files when --apply is set, and refuses to remove files outside the configured STORAGE_ROOT. --max-delete defaults to 25 and blocks large cleanup runs until the operator raises it after reviewing dry-run output. Repeat --export-type to limit cleanup to specific artifact kinds such as project_report_html or project_metadata_json.

Verify the cleanup path against a running backend without deleting anything:

bash scripts/verify_demo_cleanup_dry_run.sh
CLEANUP_MODE=compose bash scripts/verify_demo_cleanup_dry_run.sh
CLEANUP_MODE=container CLEANUP_CONTAINER=geointel bash scripts/verify_demo_cleanup_dry_run.sh

The smoke runs the cleanup command without --apply, expects dry_run=true, expects deleted_export_count=0, verifies candidate fields are present and prints the matched/type-filtered/selected counts. Use KEEP_LATEST, MAX_DELETE and EXPORT_TYPE environment variables to adjust the dry-run thresholds without changing the script. The main readiness gate checks this script's syntax; run it explicitly against Docker/PostGIS when validating a live deployment.

Complete Mol municipality workspace

Provision the official municipality boundary and every GRB GBG building that intersects it through the existing project, area, dataset and vector-feature persistence paths:

docker exec -it geointel python3 \
  /app/scripts/provision_mol_municipality_workspace.py

The command queries VRBG/Refgem for municipality Mol, verifies NIS code 13025, follows every GRB/GBG pagination link and clips the resulting buildings to the official boundary. It writes source artefacts and a checksum manifest below /app/storage/operator-data/mol-municipality, then creates or reuses Mol Municipality Workbench and imports both layers through the public API. It never writes directly to PostGIS and never runs implicitly at startup.

Completed artefacts and ready datasets are reused on a repeat run. Use --force only when an operator deliberately wants to refetch and replace the local source artefacts. Use --fetch-only to prepare and inspect the manifest without changing application persistence. The internal API default is http://127.0.0.1:8000, avoiding proxy timeouts during the large vector import; override it with --base-url when running outside the all-in-one container.

After the municipality boundary/building workspace exists, provision the official GRB road, water and parcel context layers:

docker exec -it geointel python3 \
  /app/scripts/provision_mol_context_layers.py

The command reads Wegsegment for roads, WTZ/WLAS/WGR for water and ADP for parcels from the Digitaal Vlaanderen OGC API, clips every geometry to the persisted official Mol boundary and uploads each artifact through the normal dataset API. Artifacts and manifests are retained below /app/storage/operator-data/mol-context. Repeat runs reuse both artifacts and datasets; use --force only for an explicit source refresh. Use --layers roads,water or --fetch-only for a bounded operator run.

The context provisioner itself does not add population or forest values. Use the dedicated official-source operators below; zero is never substituted for missing source data.

Official Mol temporal sources

After the municipality workspace exists, provision official Statbel population and both independently modelled land-use series:

docker exec -it geointel python3 /app/scripts/provision_mol_population_history.py
docker exec -it geointel python3 /app/scripts/provision_mol_historical_landuse.py
docker exec -it geointel python3 /app/scripts/provision_official_landuse_timeseries.py

The modern land-use command checks the MercatorNet WCS capabilities, downloads only the Mol bounding subset of each 10 m EPSG:31370 raster, validates the categorical integer grid, clips against the official boundary and polygonizes class 12 (Bos). Raw rasters, vector artifacts and checksum manifests are stored under /app/storage/operator-data/official-landuse/mol. The resulting 2013, 2016, 2019, 2022 and 2025 vectors are uploaded through the canonical API as department-omgeving:land-use:forest:mol.

Prepare and inspect artifacts without changing the database:

docker exec -it geointel python3 \
  /app/scripts/provision_official_landuse_timeseries.py --fetch-only

Use --force only to refetch and rebuild local artifacts. Existing persisted snapshots remain immutable and are reused by year/series. To use the operator for another approved region, pass all scope inputs explicitly, for example --boundary-path, --project-name, --area-name, --municipality-name, --nis-code, --scope-key and --output-dir. GeoIntel does not infer what "Kempen" means administratively.

The 2013-2025 series is methodologically separate from the historical 1778/1873/1969 series. The map-first Evolution view exposes a series selector when both exist; it never calculates one continuous trend across those source families.

Regional historical buildings, water and roads

Provision the three historical editions for the complete approved region:

docker exec geointel python /app/scripts/provision_regional_historical_landuse.py

The public WFS caps broad regional result counts at 10,000. The operator uses the 28 retained official municipality boundaries instead, stores every exact WFS response as a checksummed gzip artifact, clips polygonal geometry per municipality and assembles nine regional GeoJSON snapshots. Completed partitions are reused after checksum validation. --fetch-only prepares and audits artifacts without uploading; --force explicitly refreshes the source partitions. Historical identities are declared unstable and support hectare comparison only, not object lineage.

Belgium and Belgian North Sea foundation

Prepare and validate the complete national/maritime source snapshot without changing application persistence:

docker exec -it geointel python3 /app/scripts/provision_belgium_north_sea_scope.py \
  --fetch-only

Persist the national workbench through the canonical API:

docker exec -it geointel python3 /app/scripts/provision_belgium_north_sea_scope.py

Use --force only for an explicit source refresh. The operator creates or reuses Belgium and North Sea Workbench, eight legal/administrative Areas and six reference Datasets. It never runs on container startup, never disables TLS verification and never writes directly to PostGIS. Output and immutable checksums are retained below /app/storage/operator-data/geographic-scopes/belgium-north-sea.

The command imports the common administrative baseline and marine legal/use zones. It does not make detailed Walloon, Brussels, population, bathymetry or other audited source families operational.

Official Kempen operational scope

GeoIntel defines its regional Kempen workspace as the official Vlaamse Vervoerregio Kempen: 28 explicitly registered municipalities. This is a reproducible policy boundary, not a claim about the wider cultural, landscape or historical Kempen.

Prepare and inspect the current VRBG union and all member boundaries without changing persistence:

docker exec -it geointel python3 /app/scripts/provision_geographic_scope.py \
  --scope kempen-transport-region --fetch-only

Persist the complete scope foundation:

docker exec -it geointel python3 /app/scripts/provision_geographic_scope.py \
  --scope kempen-transport-region

The command creates or reuses Kempen Regional Workbench, the regional Area, 28 municipality Areas and two VRBG source datasets through the canonical API. Artifacts and checksums remain below /app/storage/operator-data/geographic-scopes/kempen-transport-region. Repeat runs are idempotent; --force refreshes today's source snapshot.

This command provisions boundaries only. Regional buildings, population, land use, roads, water and parcels must be added by bounded source operators; missing themes remain unavailable and are never filled with synthetic values.

Regional Kempen GRB buildings

After the scope foundation exists, fetch and persist the complete current GRB GBG building layer for the approved region:

docker exec -it geointel python3 /app/scripts/provision_regional_grb_buildings.py \
  --scope kempen-transport-region

Use --fetch-only to build and inspect artifacts without touching the database. The operator uses one bounded request/pagination sequence per official municipality and writes resumable partitions below /app/storage/operator-data/regional-themes/kempen-transport-region/buildings/<date>. Every source feature is assigned to exactly one partition using its largest intersection with the member boundaries; exact ties use the lowest NIS code. This avoids duplicate building counts at shared borders while retaining the feature clipped only to the complete regional scope.

Once all 28 partitions are complete, the operator streams one combined GeoJSON artifact and invokes DatasetService/VectorFeatureService inside the container. Features are indexed in batches as one normal regional reference dataset, so the existing viewport and rectangle-selection paths do not need a parallel API. The service import is intentionally local-only and refuses a remote backend URL. Repeat runs reuse checksummed artifacts and the persisted dataset; use a new --observed-date for a newer immutable snapshot.

Safety limits can be adjusted explicitly with --max-features-per-member, --max-total-features, --page-limit and --batch-size. Exceeding a limit fails the run instead of producing a truncated dataset. The command never runs during startup or an interactive map query.

The verified Tower snapshot for 2026-07-14 contains 466,078 unique buildings from 879 GRB source pages. Its 28 retained partitions produce one 478,143,249-byte managed artifact and Dataset e236fa03-7fce-4b37-bc7d-8bddd4d50346; an immediate repeat run reused the same checksums and Dataset instead of fetching or inserting duplicates.

Regional Kempen GRB roads, water and parcels

Provision the remaining current GRB context snapshots after the scope foundation is present:

docker exec -it geointel python3 /app/scripts/provision_regional_grb_context.py \
  --scope kempen-transport-region \
  --layers roads water parcels

Each theme has an independent manifest and can be resumed or rerun alone:

docker exec -it geointel python3 /app/scripts/provision_regional_grb_context.py \
  --scope kempen-transport-region --layers roads

Use --fetch-only to prepare checksummed artifacts without database writes. Artifacts are retained below /app/storage/operator-data/regional-themes/kempen-transport-region/<theme>/<date>. The operator follows all OGC API pagination links, fails rather than hiding a safety-limit truncation and persists one normal PostGIS dataset per theme via DatasetService. Repeat runs reuse valid partitions, combined artifacts and datasets; --force refreshes the selected observation date.

The semantic limits are deliberate: Wegsegment is road-map context rather than traffic or routing data; WTZ/WLAS/WGR are heterogeneous water objects rather than a volume metric; ADP is the graphical presumed cadastral parcel location rather than a legal survey. The public GRB provider remains not_configured, and no fetch runs from the browser or during startup.

The verified Tower snapshot for 2026-07-14 contains:

  • roads: 84,504 unique Wegsegment objects in Dataset 715b24f1-148a-4d50-9c40-d5468f670ffb;
  • water: 88,332 unique objects (25,648 WTZ, 4,605 WLAS and 58,079 WGR) in Dataset 689bb65d-0b03-4c5c-8f42-b5d2d899d8ca;
  • parcels: 415,288 unique ADP objects in Dataset 3cf5e8da-9ab0-49ef-96e4-f74b7557e0a4.

All three manifests report reference_truncated=false. PostGIS row counts and distinct source IDs match the manifests exactly, all geometries are non-empty and valid in EPSG:4326, and immediate repeat runs reuse the same artifact and Dataset.

Regional official time series

Synchronize official population and modern forest snapshots after the geographic scope workspace exists:

docker exec geointel python /app/scripts/provision_regional_timeseries.py

The explicit operator resolves the checksummed official scope boundary and coordinates Statbel 2021-2025 with Departement Omgeving 2013/2016/2019/2022/2025. It is idempotent and persists only through the canonical dataset upload API. It never runs at application startup. Prepare artifacts without persistence using --fetch-only; bound a run with --skip-population, --skip-landuse, --population-years or --landuse-years. Regional WCS downloads use the 28 official municipality boundaries as resumable request partitions, preserve the native 10 m resolution and merge locally before exact clipping to the regional union.

Waterinfo station histories

Provision real annual station observations for the persisted Mol Area:

docker exec geointel python /app/scripts/provision_waterinfo_station_history.py \
  --project-name "Kempen Regional Workbench" \
  --area-name "Gemeente Mol" \
  --parameters water_level,discharge \
  --from-year 2013 --to-year 2025

Use --fetch-only before first persistence or --force to refresh retained source JSON. The operator is idempotent for existing station/year Datasets, retains source checksums and refuses station sets above --max-stations. A missing discharge series is reported without synthesizing values. Different stations remain separate temporal series and may not be treated as area-wide water level or volume.

Governed BWK/Natura 2000 state 2025 for Mol

Build and validate raw WFS evidence without importing:

docker exec geointel python /app/scripts/provision_mol_bwk_natura2000.py --fetch-only

Import after reviewing the manifest:

docker exec geointel python /app/scripts/provision_mol_bwk_natura2000.py

The operator uses the official BWK:Bwkhab layer, follows complete WFS pagination, retains raw JSON/checksums, clips polygon geometry in EPSG:31370 and writes only through the Dataset upload flow. --force refetches source evidence but never silently replaces a conflicting persisted 2025 artifact.

Definitive agricultural-use parcel history

Prepare a bounded subset of years for source review:

docker exec geointel python /app/scripts/provision_agricultural_parcel_history.py \
  --years 2008,2019,2025 --fetch-only

Import all definitive annual editions for the regional workspace:

docker exec geointel python /app/scripts/provision_agricultural_parcel_history.py

The operator requires the optional GIS runtime already included in the Unraid image. It paginates GeoIntel API collections within the canonical 200-item limit, downloads only the fixed official archive allowlist, enforces a 250 MiB per-archive ceiling, validates one EPSG:31370 polygon GeoPackage and clips exactly to the persisted scope. Repeat runs reuse matching manifests and Datasets. --force refreshes retained evidence but cannot silently replace a conflicting persisted annual checksum. Use --scope mol for an independent municipal series.

Governed future definitive ALZ release

Run the four phases only inside the GeoIntel container. The project id must belong to Kempen Regional Workbench:

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>" \
  --review-note "Schema, gewascodes, scope en jaarverschillen nagekeken"

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>

plan is read-only. stage derives the exact archive from the official catalog campaign/publication date, downloads within the 250 MiB ceiling and runs the existing provisioner with --force --fetch-only. The staged plan binds archive, GeoJSON, schema, CRS, crop-code list, scope counts and previous definitive-edition manifest hash/deltas. ZIP member count and extracted size are bounded before the GeoPackage is read. review imports nothing. apply revalidates the catalog and every byte before using the canonical Dataset upload route.

Only a definitive YYYY-v3 is eligible. Current or older editions, v1/v2 snapshots, changed catalog/source evidence and paths outside the governed roots fail closed. Existing annual Datasets remain immutable and queryable.

Buildings and Addresses Register snapshot

Prepare and audit the current official Mol snapshot without persistence:

docker exec geointel python /app/scripts/provision_buildings_addresses_register.py --fetch-only

Import the audited artifact through the canonical Dataset upload route:

docker exec geointel python /app/scripts/provision_buildings_addresses_register.py

The operator requires a persisted Mol Area and the complete regional GRB buildings Dataset with valid manifest/partition checksums. It reads only the official Gebouw, Gebouweenheid and Adres OGC collections, clips in EPSG:31370 and retains every raw response page under the storage volume. Advertised next links are followed; a full page without next is continued with startIndex until a short final page proves completeness. --force refetches evidence; it cannot overwrite a conflicting snapshot for the same Area/date. Safety limits are configurable with --page-limit, --max-buildings, --max-units and --max-addresses. The 150,000-building default is a bbox acquisition ceiling: exact Mol clipping retains far fewer rows, but the rectangular source request also intersects neighbouring municipalities.

Only aggregate unit/address counts enter the queryable building layer. Review address_relations, grb_reconciliation, checksums and limitations in the manifest before accepting a broader import. Raw address response pages are operator evidence and must not be published.

Mol DHMV terrain rasters

Acquire and validate the official DHMV II DTM and DSM for the exact persisted Mol Area:

docker exec geointel python /app/scripts/provision_mol_dhmv.py

The operator resolves project and Area through the API, derives the bounded EPSG:4326 request rectangle and calls the canonical DHMV endpoints. The backend requests the fixed official WCS coverages in rate-limited tiles of at most 10 km per side, extracts multipart GeoTIFF, creates one georeferenced mosaic, clips to the exact Area, validates EPSG:31370/resolution/nodata/valid cells and stores through DatasetService. It then runs a full-Area terrain metric smoke.

Useful safe overrides:

docker exec geointel python /app/scripts/provision_mol_dhmv.py --products dtm_1m
docker exec geointel python /app/scripts/provision_mol_dhmv.py --resolution-m 5 --force

Do not use DHMV output as water depth or water volume. The command fails when the API no longer reports those metrics as explicitly unsupported.

Regional DHMV terrain rasters

Plan and provision the governed DTM/DSM pair for all 28 persisted Kempen municipality Areas:

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

The complete run plans 56 canonical acquisitions. --members Mol,Geel and --products dtm_1m provide bounded validation subsets; --stop-on-error turns the default complete failure report into fail-fast behavior. Existing checksummed request identities are reused unless --force is explicit. Each successful item is reported immediately so a long operator run remains observable. Every output is still an ordinary Area-linked Dataset and DatasetVersion produced by the existing DHMV service.

The regional operator deliberately retains municipality partitions. It does not assemble a monolithic Kempen height raster, does not claim annual terrain change and rejects any terrain-analysis response that stops listing water depth and water volume as unsupported.

The live completed matrix contains 56/56 ready Dataset/DatasetVersion pairs. The regional Map workspace reads intersecting partitions through the bounded partition-selection endpoint; operator storage remains unchanged.

Mol VMM flood-hazard scenarios

Acquire and validate all twelve official VMM fluvial/pluvial flood-depth scenarios for the exact persisted Mol Area:

docker exec geointel python /app/scripts/provision_mol_flood_hazards.py

The operator resolves the project and Area through canonical APIs, validates the backend registry and acquires every current/2050 T10/T100/T1000 coverage. The backend performs bounded WCS 1.1 retrieval, tiled mosaicking, exact Area clipping and centimetre-to-metre normalization. The operator then runs the full-Area selection smoke and rejects any response that stops declaring bathymetry, permanent water volume and concurrent flood volume unsupported.

Useful safe overrides:

docker exec geointel python /app/scripts/provision_mol_flood_hazards.py --products pluviaal_current_t100
docker exec geointel python /app/scripts/provision_mol_flood_hazards.py --resolution-m 5 --force

Regional VMM flood-hazard scenarios

Provision governed VMM flood-depth scenarios for every persisted municipality Area in an approved scope:

docker exec geointel python /app/scripts/provision_regional_flood_hazards.py \
  --scope kempen-transport-region --dry-run

docker exec geointel python /app/scripts/provision_regional_flood_hazards.py \
  --scope kempen-transport-region

The command requires provision_geographic_scope.py --scope kempen-transport-region to have created the regional project and member Areas. It uses only canonical API calls, validates the backend twelve-product registry and runs a full-Area selection smoke after each acquisition. Existing scenario Datasets are reused unless --force is supplied.

Useful bounded runs while validating source availability:

docker exec geointel python /app/scripts/provision_regional_flood_hazards.py \
  --members Mol --products pluviaal_current_t100

docker exec geointel python /app/scripts/provision_regional_flood_hazards.py \
  --members Mol,Geel --products pluviaal_current_t10,pluviaal_current_t100

A complete Kempen run plans 28 municipalities times 12 scenario rasters. It can take a long time because every VMM WCS tile is bounded, rate-limited and validated. This is expected operator work; the app never fetches these rasters on page load or map click.

The live completed matrix contains 336/336 ready Dataset/DatasetVersion pairs. A repeat run reuses the existing request identities. Regional map selections analyse the persisted files and never trigger the public WCS.

Cross-domain Mol profile

Load the five official policy rasters for the exact Mol municipality Area and immediately verify each persisted selection result:

docker exec geointel python /app/scripts/provision_thematic_rasters.py

Plan the later complete Kempen rollout without source fetches or writes:

docker exec geointel python /app/scripts/provision_thematic_rasters.py \
  --project-name "Kempen Regional Workbench" --all-municipalities --dry-run

Load the official DOV soil map for Mol:

docker exec geointel python /app/scripts/provision_mol_soil_map.py

Load the same historical source for all 28 approved Kempen municipalities as one partition-audited regional snapshot:

docker exec geointel python /app/scripts/provision_regional_soil_map.py

The regional command verifies the checksummed municipality-scope manifest, queries and clips every municipality separately, retains deterministic gzip source pages and imports one complete snapshot through DatasetService. Repeat runs reuse all verified partitions and the existing Dataset. Use --fetch-only for evidence preparation without persistence and --force only for an explicit source refetch.

--fetch-only builds the soil artifact and manifest without API import; --force is the only way to bypass an existing ready soil Dataset. Both operators use canonical APIs and persistent operator-evidence storage. They do not run on application startup.

Read-only source freshness audit

Inspect the persisted publication, version and storage evidence for one project without contacting an external provider:

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

Add --output /app/storage/operator-evidence/source-freshness/latest.json for a persistent JSON evidence copy. --fail-on integrity exits non-zero only for missing versions/checksum/file/size evidence; due also gates planned source reviews and attention additionally gates unclassified sources. This command uses one canonical GET, changes no application data and performs no source download. It can therefore be scheduled explicitly through Unraid cron without turning GeoIntel into a real-time monitoring system.

Add --probe-catalogs to explicitly read the allowlisted official GRB and orthophoto capabilities plus linked ISO metadata. --refresh-catalogs bypasses the short server cache and implies the probe. --fail-on-catalog makes a degraded/unavailable official metadata service fail the command. With a probe, JSON output contains source_freshness and catalog_probes; without it, the original local report shape is unchanged. No flag downloads provider features or imagery and no flag writes a Dataset.

Governed GRB refresh

Use the refresh coordinator only inside the GeoIntel container. The default plan action is read-only and prints the canonical decision from the API:

docker exec geointel python /app/scripts/manage_grb_refresh.py plan \
  --project-id 82a85913-c522-45d7-84a1-02b393d89e55 \
  --api-url http://127.0.0.1:8000/api/v1 \
  --refresh-catalog

A refresh is deliberately split into two operator approvals:

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

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

stage delegates to the existing buildings/context operators in fetch-only mode, validates every municipality partition and writes /app/storage/operator-evidence/grb-refresh/<scope>/<edition>/staged-plan.json. The plan contains exact feature-count deltas and artifact sizes. apply revalidates the entire plan and refuses any mismatched byte before delegating to the DatasetService-based persistence path. Existing snapshots are retained. The coordinator accepts no provider URL, collection name, arbitrary process or automatic schedule.

Governed Statbel population release

Use the population release coordinator only inside the GeoIntel container. It derives the exact population and matching sector-geometry URLs from the strict official catalog contract; operators cannot inject another provider URL.

docker exec geointel python /app/scripts/manage_statbel_population_release.py plan \
  --project-id <KEMPEN_PROJECT_ID> --refresh-catalog

docker exec geointel python /app/scripts/manage_statbel_population_release.py stage \
  --project-id <KEMPEN_PROJECT_ID> \
  --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> \
  --confirm-edition <YEAR_FROM_PLAN> --confirm-layout <LAYOUT_FROM_PLAN> \
  --confirm-plan-sha256 <SHA256_FROM_STAGE> \
  --approve --reviewer "<OPERATOR_NAME>"

docker exec geointel python /app/scripts/manage_statbel_population_release.py apply \
  --project-id <KEMPEN_PROJECT_ID> \
  --confirm-edition <YEAR_FROM_PLAN> --confirm-layout <LAYOUT_FROM_PLAN> \
  --confirm-plan-sha256 <SHA256_FROM_STAGE> \
  --confirm-review-sha256 <SHA256_FROM_REVIEW>

plan writes nothing. stage downloads within the population/geometry size bounds, invokes the existing provisioner with --force --fetch-only and emits staged-plan.json. review requires a named approval and emits review-evidence.json. apply refuses catalog drift or any changed source, manifest, snapshot, plan or review byte, then delegates to the canonical DatasetService upload. applied-evidence.json records the resulting Dataset without deleting any historical snapshot. Current, older, unavailable or ambiguous releases are fail-closed.

Tower deployment

Push the local branch to Gitea, then rebuild the Unraid/Tower Docker runtime:

bash scripts/deploy_tower.sh

From the Codex Windows workspace, use the PowerShell wrapper:

.\scripts\deploy_tower.ps1

For the first deployment into an existing non-Git appdata folder, bootstrap the checkout explicitly:

DEPLOY_BOOTSTRAP=1 bash scripts/deploy_tower.sh
.\scripts\deploy_tower.ps1 -Bootstrap

Useful overrides:

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

Regional BWK/Natura 2000 state 2025

Prepare all official municipality partitions and inspect the combined manifest without persisting a Dataset:

docker exec geointel python /app/scripts/provision_regional_bwk_natura2000.py --fetch-only

Import the checksum-bound regional snapshot through DatasetService:

docker exec geointel python /app/scripts/provision_regional_bwk_natura2000.py

The command requires the canonical geographic-scope manifest and 28-member boundary artifact. Defaults cap each municipality at 30,000 source features and the assembled snapshot at 300,000 features. It never truncates silently, never writes vector_features directly and never turns the single 2025 state into a fabricated historical series.

Mol VHA bathymetry profiles

Provision and verify the official profile points for the exact persisted Mol Area:

docker exec geointel python /app/scripts/provision_mol_bathymetry_profiles.py

Use --force only for a deliberate fresh provider snapshot. The command finds Kempen Regional Workbench and Gemeente Mol, calls the canonical bathymetry acquisition endpoint and then verifies that vector selection count and semantic metrics match the persisted Job result. It never writes directly to PostGIS.

For another approved workspace or Area:

python scripts/provision_mol_bathymetry_profiles.py \
  --base-url http://127.0.0.1:8000 \
  --project-name "Project name" \
  --area-name "Area name fragment"

The output is a point dataset with historical evidence. It is not a continuous water-bottom raster and cannot calculate current water volume.

Flanders VHA bathymetry partitions

Create the complete official Flemish land scope from the current VRBG municipality collection:

docker exec geointel python /app/scripts/provision_flanders_geographic_scope.py

Acquire VHA profile partitions with atomic resume evidence:

docker exec geointel python /app/scripts/provision_flanders_bathymetry_profiles.py

Use --members Mol Geel or --max-partitions 5 only for a partial operational check. Partial runs do not activate regional coverage. The complete run finalizes only when every official municipality has either one ready Dataset or an explicit zero-profile source result.

Probe the official-metadata MDK WCS safely:

docker exec geointel python /app/scripts/probe_mdk_bathymetry.py

This performs only GetCapabilities, keeps strict TLS verification enabled and returns exit code 2 for an honest non-ready source.

Bounded SPW bathymetry raster

Download or stage only the official SPW ZIP documented in docs/DATA_SOURCES.md. The operator pins its SHA-256, validates its archive and raster contract, and imports a bounded COG through the public GeoIntel API:

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

Use --dry-run to validate identity and scope without cropping or persistence. Use --download only when the requested raw path does not yet exist; download remains strict-TLS and fixed to the official source URL. Selections above --max-pixels fail before raster materialization.

Release backup and restore proof

The release-candidate safety path is host-operated against the running all-in-one container:

bash scripts/backup_release_state.sh \
  --container geointel \
  --output-root /mnt/user/appdata/geointel/backups \
  --storage-path /mnt/user/appdata/geointel/storage \
  --models-path /mnt/user/appdata/geointel/models \
  --inventory-mode sha256

The backup is written atomically and contains a PostgreSQL custom-format dump, archive listing, Alembic/PostGIS metadata, critical table counts, optional storage/model inventories and SHA-256 checksums. An empty or known-default database password leaves the release gate failed. For an emergency backup before rotating that password, add --allow-insecure-password; the manifest still records the insecure state.

Verify without changing any database:

bash scripts/verify_release_backup.sh \
  --backup-dir /mnt/user/appdata/geointel/backups/<release-id>

Prove restoration only in a generated temporary database:

bash scripts/restore_release_backup_smoke.sh \
  --backup-dir /mnt/user/appdata/geointel/backups/<release-id> \
  --confirm-isolated-restore

The restore smoke rejects the production database name, compares PostGIS, Alembic and retained table counts, and removes its temporary database.

After a verified backup/restore, rotate a default production password without printing or committing the generated secret:

bash scripts/rotate_postgres_password.sh \
  --container geointel \
  --env-file /mnt/user/appdata/geointel/.env \
  --restart-all-in-one

The command atomically updates the operator-owned .env, changes the matching PostgreSQL role and recreates the container. A failed role change restores the previous environment file. The generated secret is never printed.

RC-10 data operations and retention

Run the read-only storage, provenance, disk-pressure and source-family audit:

docker exec geointel python /app/scripts/audit_data_operations.py \
  --minimum-age-days 7 \
  --output /app/storage/release-evidence/rc-current/data-operations.json

Preview old unreferenced derived/cache/export candidates without deletion:

docker exec geointel python /app/scripts/cleanup_storage_artifacts.py \
  --minimum-age-days 7 \
  --max-delete 25

Apply requires a reviewed candidate count, the exact DELETE_STORAGE_ARTIFACTS token and a backup no older than 24 hours with a checksum-verified database dump and SHA-256 storage inventory. The host backup root is mounted read-only at /app/backups. See docs/DATA_OPERATIONS_RUNBOOK.md. No cleanup is scheduled by GeoIntel.

RC-8 Belgium/North Sea release journeys

Preview the seven release areas without mutating GeoIntel:

python scripts/provision_release_golden_areas.py \
  --base-url http://192.168.10.150:1202 \
  --output artifacts/rc8-golden-areas.json

Provision only missing Areas and execute the complete live browser/API release journey:

bash scripts/run_rc8_release_journeys.sh \
  http://192.168.10.150:1202 \
  artifacts/rc8-release-journeys \
  artifacts/rc8-golden-areas.json

The wrapper runs the same operator with --apply, then starts the Playwright runner. Mol and Kempen retain their official source Area identifiers while bounded copies are made selectable in the national workbench. It validates all land, cross-region, coast and maritime golden areas; governed metrics and provenance; compatible history; no-data, partial, unsupported and provider-failure states; persistent map export; real local Ollama context; and the explicit configured-YOLO fixture journey.

The command is intentionally live and creates missing Area records, an export, an assistant conversation and explicit technical demo AI records. It never fetches fake production provider data. Evidence is written below the requested ignored artifacts/ directory as screenshots plus manifest.json.

RC-9 UX and performance audit

Run the non-mutating frontend audit against a deployed runtime:

bash scripts/run_rc9_ux_audit.sh \
  http://192.168.10.150:1202 \
  artifacts/rc9-ux-audit

The audit checks the map-first flow at mobile, desktop and ultrawide viewports; rejects page-level horizontal overflow and visible controls without an accessible name; verifies keyboard tabs and skip-link focus; simulates a slow project bootstrap; and proves coverage timing is visible. It writes only ignored screenshots and manifest.json.