Prepare GeoIntel 1.0.0 release candidate
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

This commit is contained in:
Codex
2026-07-18 07:59:38 +02:00
parent 213b57ce2e
commit 79a9f9dbfc
28 changed files with 764 additions and 64 deletions
+4 -2
View File
@@ -2,12 +2,14 @@
## Project identity
GeoIntel Kempen is a GeoAI Workbench, not a generic CRUD app and not a generic dashboard.
GeoIntel is a GeoAI Workbench for Belgium and the Belgian North Sea, not a
generic CRUD app and not a generic dashboard. Mol and the Kempen remain golden
regression areas, not the product boundary.
## Required behavior
- Read `docs/CODEX_BOOTSTRAP_PROMPT.md` first.
- Respect `docs/V1_SCOPE_FREEZE.md`.
- Respect `docs/RC_SCOPE_FREEZE_BELGIUM_NORTH_SEA.md`.
- Use `docs/API_CONTRACTS.md` as source of truth for endpoints.
- Use `docs/DATABASE_IMPLEMENTATION_PLAN.md` as source of truth for persistence.
- Use `docs/DEFINITION_OF_DONE.md` to decide whether work is complete.
+11
View File
@@ -87,6 +87,17 @@
- Classified fixed NGI and RBINS national/maritime editions explicitly. The
live source-freshness report now records three current sources, zero due
sources and zero integrity issues.
- Assigned semantic release version `1.0.0-rc.1` consistently to backend
health, frontend package metadata and the OCI image version label.
- Added a fail-closed release-package builder that requires a clean tagged
revision, exact image revision, evidence inventory, SHA-256 checksums and a
verified detached SSH signature.
- Replaced stale Mol/Kempen product-boundary navigation with the active
Belgium/North Sea scope while retaining Mol and the Kempen as golden
regression areas.
- Added the final release runbook for immutable deployment, fresh install,
backup, isolated restore/upgrade, rollback, browser journeys, SBOM,
vulnerability policy, signed manifest and safe shutdown.
- Added a read-only release-evidence manifest command with Git, migration,
dependency, configuration checksum and optional live endpoint evidence.
- Replaced the obsolete pre-build status with the current implemented
+15 -10
View File
@@ -1,8 +1,12 @@
# GeoIntel Kempen
# GeoIntel Belgium and the Belgian North Sea
GeoIntel Kempen is a GeoAI Workbench for the Belgian Kempen. It is designed as a portfolio-grade project combining GIS, remote sensing, raster/vector processing, computer vision, QA/QC and geospatial exports.
GeoIntel is a map-first GeoAI Workbench for Belgium and the Belgian North Sea.
It combines governed official-source coverage, raster/vector processing,
historical comparison, computer vision, QA/QC and geospatial exports.
The primary operating focus is Mol. New workbench contexts, AOIs and operator samples start there, while the broader Kempen remains fully supported for cross-area validation and regional interoperability. The live operator environment can provision a complete `Mol Municipality Workbench` with the official NIS `13025` municipality boundary and every intersecting GRB GBG building; smaller Mol zones remain analysis and validation contexts instead of the default municipal map.
Mol and the Kempen remain deep regression and model-validation references. The
release scope is all of Belgium plus legally labelled Belgian maritime zones;
source coverage remains explicit per theme and jurisdiction.
GeoIntel is not a generic dashboard or chatbot. The core product is:
@@ -10,17 +14,18 @@ GeoIntel is not a generic dashboard or chatbot. The core product is:
## Current milestone
**M14 — Build Launch Package**
**v1.0.0-rc.1 - Belgium/North Sea release candidate**
The canonical start point is now:
The canonical release controls are:
- `CODEX_START.md`
- `docs/00-start/START_HERE.md`
- `docs/40-build-launch/SPRINT_1_SCOPE_FREEZE.md`
- `docs/40-build-launch/BUILD_SUCCESS_DEFINITION.md`
- `docs/40-build-launch/BUILD_ORDER_GRAPH.md`
- `docs/RC_SCOPE_FREEZE_BELGIUM_NORTH_SEA.md`
- `docs/RC_ROADMAP_BELGIUM_NORTH_SEA.md`
- `docs/RELEASE_RUNBOOK.md`
- `docs/DEFINITION_OF_DONE.md`
Older M0-M13 handoff files are retained as historical preparation artifacts. The M14 build launch docs, M13 optimization docs, M12 final run-readiness docs, M11 governance docs and canonical specs take precedence.
Older milestone and sprint handoff files remain historical evidence. They do
not override the active national/maritime scope freeze or RC roadmap.
## Core V1 vertical slice
+1
View File
@@ -0,0 +1 @@
1.0.0-rc.1
+9
View File
@@ -1934,3 +1934,12 @@ evidence are protected by default. Apply mode requires an exact confirmation,
an explicit candidate ceiling and a recent checksum-verified database plus
SHA-256 storage backup mounted read-only under `/app/backups`. See
`docs/DATA_OPERATIONS_RUNBOOK.md`.
## Release candidate operations
The semantic release version is stored in the repository `VERSION` file and
is exposed by health responses plus the OCI image version label. Fresh
install, checksum-verified backup, isolated restore/upgrade, rollback,
Belgium/North Sea browser journeys, SBOM, vulnerability evidence, SSH-signed
release manifest and final verification commands are defined in
`docs/RELEASE_RUNBOOK.md`.
+4 -1
View File
@@ -11,7 +11,10 @@ class Settings(BaseSettings):
)
app_env: str = Field(default="development", validation_alias="GEOINTEL_ENV")
app_version: str = Field(default="0.1.0")
app_version: str = Field(
default="1.0.0-rc.1",
validation_alias="GEOINTEL_APP_VERSION",
)
build_sha: str | None = Field(default=None, validation_alias="GEOINTEL_BUILD_SHA")
build_time: str | None = Field(default=None, validation_alias="GEOINTEL_BUILD_TIME")
api_prefix: str = Field(default="/api/v1", validation_alias="GEOINTEL_API_PREFIX")
+2 -2
View File
@@ -1,7 +1,7 @@
[project]
name = "geointel-backend"
version = "0.1.0"
description = "GeoIntel Kempen backend"
version = "1.0.0rc1"
description = "GeoIntel Belgium and Belgian North Sea backend"
readme = "README.md"
requires-python = ">=3.11"
dependencies = [
+132
View File
@@ -0,0 +1,132 @@
from __future__ import annotations
import importlib.util
import json
import shutil
import subprocess
from pathlib import Path
import pytest
ROOT = Path(__file__).resolve().parents[2]
SCRIPT = ROOT / "scripts" / "build_release_package.py"
def load_script():
spec = importlib.util.spec_from_file_location("build_release_package", SCRIPT)
assert spec is not None and spec.loader is not None
module = importlib.util.module_from_spec(spec)
spec.loader.exec_module(module)
return module
def test_release_version_is_consistent_across_runtime_packages() -> None:
version = (ROOT / "VERSION").read_text(encoding="utf-8").strip()
config = (ROOT / "backend" / "app" / "core" / "config.py").read_text(
encoding="utf-8"
)
pyproject = (ROOT / "backend" / "pyproject.toml").read_text(encoding="utf-8")
frontend = json.loads(
(ROOT / "frontend" / "package.json").read_text(encoding="utf-8")
)
package_lock = json.loads(
(ROOT / "frontend" / "package-lock.json").read_text(encoding="utf-8")
)
assert version == "1.0.0-rc.1"
assert f'default="{version}"' in config
assert "GEOINTEL_APP_VERSION" in config
assert 'version = "1.0.0rc1"' in pyproject
assert frontend["version"] == version
assert package_lock["version"] == version
assert package_lock["packages"][""]["version"] == version
def test_release_image_carries_semantic_version_identity() -> None:
dockerfile = (ROOT / "deploy" / "unraid" / "Dockerfile.all-in-one").read_text(
encoding="utf-8"
)
deploy = (ROOT / "deploy" / "unraid" / "deploy-release.sh").read_text(
encoding="utf-8"
)
assert "ARG GEOINTEL_APP_VERSION=1.0.0-rc.1" in dockerfile
assert 'org.opencontainers.image.version="${GEOINTEL_APP_VERSION}"' in dockerfile
assert "GEOINTEL_APP_VERSION=\"$(tr -d '[:space:]' < VERSION)\"" in deploy
assert "--build-arg GEOINTEL_APP_VERSION=" in deploy
assert "stored_version" in deploy
@pytest.mark.skipif(shutil.which("ssh-keygen") is None, reason="ssh-keygen unavailable")
def test_release_package_signature_and_checksums_fail_closed(tmp_path: Path) -> None:
module = load_script()
key = tmp_path / "release-key"
result = subprocess.run(
["ssh-keygen", "-q", "-t", "ed25519", "-N", "", "-f", str(key)],
capture_output=True,
text=True,
check=False,
)
assert result.returncode == 0, result.stderr
package = tmp_path / "package"
package.mkdir()
evidence = package / "readiness.txt"
evidence.write_text("passed\n", encoding="utf-8")
identity = "geointel-release"
namespace = "geointel-release"
(package / module.SIGNERS_NAME).write_text(
f"{identity} {module.public_key(key)}\n",
encoding="utf-8",
)
manifest = {
"schema_version": 1,
"release_id": "v1.0.0-rc.1",
"version": "1.0.0-rc.1",
"scope": "Belgium and the Belgian North Sea",
"signature": {"identity": identity, "namespace": namespace},
"evidence": [
{
"path": evidence.name,
"size_bytes": evidence.stat().st_size,
"sha256": module.sha256(evidence),
}
],
}
manifest_path = package / module.MANIFEST_NAME
manifest_path.write_text(json.dumps(manifest) + "\n", encoding="utf-8")
module.run(
(
"ssh-keygen",
"-Y",
"sign",
"-f",
str(key),
"-n",
namespace,
str(manifest_path),
)
)
module.write_checksums(package)
verified = module.verify_package(package)
assert verified["release_id"] == "v1.0.0-rc.1"
evidence.write_text("tampered\n", encoding="utf-8")
with pytest.raises(RuntimeError, match="Checksum mismatch"):
module.verify_package(package)
def test_release_package_cli_requires_tagged_clean_revision() -> None:
source = SCRIPT.read_text(encoding="utf-8")
readiness = (ROOT / "scripts" / "run_readiness_check.sh").read_text(
encoding="utf-8"
)
assert 'run(("git", "status", "--porcelain=v1"))' in source
assert 'run(("git", "rev-list", "-n", "1", release_id))' in source
assert "Image revision must equal the tagged Git commit" in source
assert "ssh-keygen" in source
assert "verify_checksums(package_dir)" in source
assert "py_compile scripts/build_release_package.py" in readiness
@@ -34,6 +34,7 @@ def test_release_evidence_manifest_is_secret_free_and_read_only(tmp_path: Path)
assert manifest["schema_version"] == 1
assert manifest["release_id"] == "test-rc"
assert manifest["version"] == "1.0.0-rc.1"
assert manifest["read_only"] is True
assert manifest["scope"] == "Belgium and the Belgian North Sea"
assert "DATABASE_URL" not in json.dumps(manifest).replace(
@@ -72,6 +73,7 @@ def test_release_evidence_cli_writes_single_head_manifest(tmp_path: Path) -> Non
assert payload["migrations"]["single_head"] is True
assert payload["files"]["docs/RC_SCOPE_FREEZE_BELGIUM_NORTH_SEA.md"]["sha256"]
assert payload["files"]["docs/RC_ROADMAP_BELGIUM_NORTH_SEA.md"]["sha256"]
assert payload["files"]["docs/RELEASE_RUNBOOK.md"]["sha256"]
def test_readiness_gate_compiles_release_evidence_command() -> None:
@@ -49,10 +49,11 @@ def test_operator_workflows_put_mol_first_and_name_future_projects() -> None:
assert 'QUALITY_SAMPLE_SLUG="${sample_slug}"' in multi_matrix
def test_product_docs_record_mol_primary_focus_without_dropping_kempen_scope() -> None:
def test_product_docs_record_national_scope_and_mol_regression_focus() -> None:
readme = (ROOT / "README.md").read_text(encoding="utf-8")
vision = (ROOT / "docs" / "PRODUCT_VISION.md").read_text(encoding="utf-8")
assert "primary operating focus is Mol" in readme
assert "Mol is de primaire operationele focus" in vision
assert "broader Kempen" in readme
assert "Belgium and the Belgian North Sea" in readme
assert "Mol and the Kempen remain deep regression" in readme
assert "Belgie en de Belgische Noordzee" in vision
assert "Mol en de Kempen blijven gouden regressiegebieden" in vision
+4 -1
View File
@@ -164,12 +164,15 @@ RUN chmod +x /usr/local/bin/geointel-all-in-one-start /usr/local/bin/gosu \
ARG GEOINTEL_BUILD_SHA=unknown
ARG GEOINTEL_BUILD_TIME=unknown
ARG GEOINTEL_APP_VERSION=1.0.0-rc.1
ENV GEOINTEL_BUILD_SHA="${GEOINTEL_BUILD_SHA}" \
GEOINTEL_BUILD_TIME="${GEOINTEL_BUILD_TIME}"
GEOINTEL_BUILD_TIME="${GEOINTEL_BUILD_TIME}" \
GEOINTEL_APP_VERSION="${GEOINTEL_APP_VERSION}"
LABEL org.opencontainers.image.title="GeoIntel" \
org.opencontainers.image.description="GeoIntel workbench for Belgium and the Belgian North Sea" \
org.opencontainers.image.version="${GEOINTEL_APP_VERSION}" \
org.opencontainers.image.revision="${GEOINTEL_BUILD_SHA}" \
org.opencontainers.image.created="${GEOINTEL_BUILD_TIME}" \
io.geointel.ai.enabled="${GEOINTEL_INSTALL_AI}"
+5
View File
@@ -247,6 +247,11 @@ bash deploy/unraid/run-dockerman-container.sh
## Release identity, fresh install and rollback
The complete final-release order, including backup/restore, browser journeys,
SBOM, vulnerability policy, SSH-signed manifest and checksums, is in
`docs/RELEASE_RUNBOOK.md`. `VERSION` is the canonical semantic version and is
also written to the image's `org.opencontainers.image.version` label.
Inspect the running immutable revision and retained images:
```bash
+17 -2
View File
@@ -16,6 +16,11 @@ if [ -n "${DEPLOY_GEOINTEL_INSTALL_AI:-}" ]; then
fi
GEOINTEL_INSTALL_AI="${GEOINTEL_INSTALL_AI:-false}"
GEOINTEL_APP_VERSION="$(tr -d '[:space:]' < VERSION)"
if ! [[ "$GEOINTEL_APP_VERSION" =~ ^[0-9]+\.[0-9]+\.[0-9]+(-[0-9A-Za-z.-]+)?$ ]]; then
echo "Invalid semantic version in VERSION: ${GEOINTEL_APP_VERSION}" >&2
exit 2
fi
GEOINTEL_BUILD_SHA="$(git rev-parse HEAD)"
GEOINTEL_BUILD_TIME="$(date -u +%Y-%m-%dT%H:%M:%SZ)"
GEOINTEL_IMAGE_REPOSITORY="${GEOINTEL_IMAGE_REPOSITORY:-geointel-all-in-one}"
@@ -90,7 +95,16 @@ if docker image inspect "$GEOINTEL_RELEASE_IMAGE" >/dev/null 2>&1; then
--format '{{index .Config.Labels "io.geointel.ai.enabled"}}' \
"$GEOINTEL_RELEASE_IMAGE"
)"
if [ "$stored_revision" != "$GEOINTEL_BUILD_SHA" ] || [ "$stored_ai" != "$GEOINTEL_INSTALL_AI" ]; then
stored_version="$(
docker image inspect \
--format '{{index .Config.Labels "org.opencontainers.image.version"}}' \
"$GEOINTEL_RELEASE_IMAGE"
)"
if (
[ "$stored_revision" != "$GEOINTEL_BUILD_SHA" ] ||
[ "$stored_ai" != "$GEOINTEL_INSTALL_AI" ] ||
[ "$stored_version" != "$GEOINTEL_APP_VERSION" ]
); then
echo "Immutable release tag has conflicting metadata: ${GEOINTEL_RELEASE_IMAGE}" >&2
exit 2
fi
@@ -101,6 +115,7 @@ else
--build-arg GEOINTEL_INSTALL_AI="$GEOINTEL_INSTALL_AI" \
--build-arg GEOINTEL_BUILD_SHA="$GEOINTEL_BUILD_SHA" \
--build-arg GEOINTEL_BUILD_TIME="$GEOINTEL_BUILD_TIME" \
--build-arg GEOINTEL_APP_VERSION="$GEOINTEL_APP_VERSION" \
-f deploy/unraid/Dockerfile.all-in-one \
-t "$GEOINTEL_RELEASE_IMAGE" \
-t "${GEOINTEL_IMAGE_REPOSITORY}:latest" \
@@ -128,5 +143,5 @@ fi
echo "Deployed immutable image ${GEOINTEL_RELEASE_IMAGE}."
docker image inspect \
--format 'revision={{index .Config.Labels "org.opencontainers.image.revision"}} created={{index .Config.Labels "org.opencontainers.image.created"}}' \
--format 'version={{index .Config.Labels "org.opencontainers.image.version"}} revision={{index .Config.Labels "org.opencontainers.image.revision"}} created={{index .Config.Labels "org.opencontainers.image.created"}}' \
"$GEOINTEL_RELEASE_IMAGE"
+23 -21
View File
@@ -5,13 +5,18 @@ Older handoff files are historical. If documents conflict, follow the precedence
## Current milestone
**M14 — Build Launch Package**
**v1.0.0-rc.1 - Belgium/North Sea release candidate**
The repository is no longer only a documentation bundle. It is now a specification-controlled engineering repo for building GeoIntel Kempen as a GeoAI Workbench.
The implementation is in final release-candidate acceptance for Belgium and
the Belgian North Sea. Mol and the Kempen remain golden regression areas, not
the product boundary.
## Product one-liner
GeoIntel Kempen is a GeoAI Workbench for the Belgian Kempen that processes raster data, vector data and AI outputs into geospatially correct detections, segmentations, QA/QC metrics and exports.
GeoIntel is a map-first GeoAI Workbench for Belgium and the Belgian North Sea
that processes governed raster data, vector data and AI outputs into
geospatially correct analysis, detections, segmentations, QA/QC metrics and
exports.
## Non-negotiable product identity
@@ -35,21 +40,18 @@ GeoIntel is not primarily:
Read these files in order before coding:
1. `docs/00-start/START_HERE.md`
2. `docs/governance/GEOINTEL_CONSTITUTION.md`
3. `docs/governance/ARCHITECTURE_INVARIANTS.md`
4. `docs/governance/FORBIDDEN_DECISIONS.md`
5. `docs/governance/DECISION_PRECEDENCE.md`
6. `docs/specs/CANONICAL_DOMAIN_MODELS.md`
7. `docs/specs/GIS_STANDARDS.md`
8. `docs/specs/RASTER_STANDARDS.md`
9. `docs/specs/STATE_MACHINES.md`
10. `docs/workflows/GOLDEN_PATHS.md`
11. `docs/build/BUILD_ORDER_DEPENDENCY_GRAPH.md`
12. `docs/build/CODEX_OPERATING_SYSTEM.md`
13. `docs/40-build-launch/SPRINT_1_SCOPE_FREEZE.md`
14. `docs/40-build-launch/BUILD_SUCCESS_DEFINITION.md`
15. `docs/40-build-launch/CODEX_STOP_RULES.md`
16. `prompts/codex/m14/CODEX_FIRST_DAY_MASTER_PROMPT.md`
2. `docs/RC_SCOPE_FREEZE_BELGIUM_NORTH_SEA.md`
3. `docs/RC_ROADMAP_BELGIUM_NORTH_SEA.md`
4. `docs/governance/GEOINTEL_CONSTITUTION.md`
5. `docs/governance/ARCHITECTURE_INVARIANTS.md`
6. `docs/governance/FORBIDDEN_DECISIONS.md`
7. `docs/API_CONTRACTS.md`
8. `docs/DATABASE_IMPLEMENTATION_PLAN.md`
9. `docs/DATA_SPECIFICATION.md`
10. `docs/DATA_SOURCES.md`
11. `docs/STORAGE_ARCHITECTURE.md`
12. `docs/DEFINITION_OF_DONE.md`
13. `docs/RELEASE_RUNBOOK.md`
## Canonical first implementation target
@@ -99,15 +101,15 @@ Codex may not change:
## Conflict resolution
If any older document conflicts with this M14 launch layer, follow this order:
If any older document conflicts with the active RC layer, follow this order:
1. Constitution and architecture invariants.
2. Forbidden decisions.
3. State machines and canonical models.
4. API/database contracts.
5. Build order dependency graph.
6. M14 build-launch docs for first-run scope and stop rules.
7. Older milestone handoff documents.
6. Belgium/North Sea scope freeze and RC roadmap.
7. Older milestone and sprint handoff documents.
## Required pass ending
+3 -3
View File
@@ -55,7 +55,7 @@ Returns process liveness only. It never queries PostgreSQL.
{
"status": "ok",
"service": "geointel-backend",
"version": "0.1.0",
"version": "1.0.0-rc.1",
"build_sha": null,
"build_time": null
}
@@ -75,7 +75,7 @@ degraded. Docker uses `/health/ready`.
{
"status": "ok",
"service": "geointel-backend",
"version": "0.1.0",
"version": "1.0.0-rc.1",
"database": "ok",
"postgis": "ok:3.x",
"migration": "ok:202607160001",
@@ -105,7 +105,7 @@ envelope. PostGIS and configured YOLO state are derived at runtime.
"sam": false,
"grb": "bounded",
"sentinel": "planned",
"version": "0.1.0",
"version": "1.0.0-rc.1",
"build_sha": null,
"providers": []
}
+1 -1
View File
@@ -21,7 +21,7 @@
"redis": "ok",
"storage": "ok"
},
"version": "0.1.0"
"version": "1.0.0-rc.1"
}
```
+5 -2
View File
@@ -1,6 +1,9 @@
# Codex Bootstrap Prompt GeoIntel Kempen
# Codex Bootstrap Prompt - GeoIntel Belgium and the Belgian North Sea
You are building GeoIntel Kempen, a GeoAI Workbench for the Belgian Kempen region. The repository already contains the specification set. Read these documents before editing code:
You are building GeoIntel, a GeoAI Workbench for Belgium and the Belgian North
Sea. Mol and the Kempen are golden regression areas, not the product boundary.
The repository already contains the specification set. Read these documents
before editing code:
1. `docs/SPECIFICATION_FREEZE_M0.md`
2. `docs/V1_SCOPE_FREEZE.md`
+1 -1
View File
@@ -20,7 +20,7 @@ backward-compatible alias.
{
"status": "ok",
"service": "geointel-backend",
"version": "0.1.0",
"version": "1.0.0-rc.1",
"database": "ok",
"postgis": "ok:3.x",
"migration": "ok:202607160001",
+14 -5
View File
@@ -1,6 +1,7 @@
# Product Vision
GeoIntel Kempen bestaat om open en lokale geospatiale data om te zetten in bruikbare, controleerbare GeoAI-resultaten.
GeoIntel bestaat om open en officiele geospatiale data voor Belgie en de
Belgische Noordzee om te zetten in bruikbare, controleerbare GeoAI-resultaten.
## Geen klassieke GIS-viewer
@@ -28,19 +29,27 @@ GeoIntel moet bewijzen dat de ontwikkelaar de volledige keten begrijpt:
8. kwaliteit meten
9. resultaten exporteren
## Regionale identiteit
## Geografische identiteit
De Kempen vormen de afgebakende regio. Dit maakt het project concreet, realistisch en demo-baar. Voorbeelden:
De productscope omvat heel Belgie en de juridisch correct gelabelde Belgische
maritieme zones. De dekking is een federatie van nationale, Vlaamse, Waalse,
Brusselse en maritieme broncontracten; ontbrekende of gedeeltelijke dekking
blijft zichtbaar. Voorbeelden:
- gebouwdetectie en infrastructuuranalyse in Mol
- vergelijkende validatie in Geel
- verstedelijking rond Turnhout
- natuurfragmentatie rond Kasterlee of Retie
Mol is de primaire operationele focus. De standaardkaart, nieuwe AOI-context en operator-volgorde vertrekken daarom vanuit Mol. De bredere Kempen blijft bewust onderdeel van de productscope voor onafhankelijke validatie, overdraagbaarheid en regionale vergelijking.
Mol en de Kempen blijven gouden regressiegebieden voor diepgaande validatie,
modelkwaliteit en historische vergelijkbaarheid. Ze zijn niet langer de
productgrens. Nieuwe kaartselecties kunnen overal binnen Belgie en de
Belgische Noordzee liggen en behouden hun eigen bronautoriteit en beperkingen.
## Richting
De definitieve richting is:
> GeoIntel Kempen is een GeoAI Workbench waarmee je open geodata, luchtfoto's en satellietbeelden verwerkt tot detecties, segmentaties, veranderingen en controleerbare GIS-lagen.
> GeoIntel is een GeoAI Workbench voor Belgie en de Belgische Noordzee waarmee
> je open geodata, luchtfoto's en satellietbeelden verwerkt tot analyse,
> detecties, segmentaties, veranderingen en controleerbare GIS-lagen.
+2
View File
@@ -534,6 +534,8 @@ maritime freshness evidence classifies all three fixed official editions as
## RC-11 - Final release package
**State: in progress.**
### Work
- rerun fresh-install and upgrade proof using the release image;
+13 -3
View File
@@ -1,6 +1,16 @@
# GeoIntel Docs Index
Start here when preparing an implementation pass.
Start with these active release controls:
- `00-start/START_HERE.md`
- `RC_SCOPE_FREEZE_BELGIUM_NORTH_SEA.md`
- `RC_ROADMAP_BELGIUM_NORTH_SEA.md`
- `RELEASE_RUNBOOK.md`
- `DEFINITION_OF_DONE.md`
The milestone and sprint documents below are retained as historical design and
implementation evidence. `TODO.md` is an implementation archive, not the
active release board.
## Product foundation
- PRODUCT_VISION.md
@@ -37,13 +47,13 @@ Start here when preparing an implementation pass.
- UI_PAGE_SPECIFICATIONS.md
- COMPONENT_BREAKDOWN.md
## Execution
## Historical execution
- CODEX_MASTER_PROMPT.md
- CODEX_EXECUTION_PLAN.md
- CODEX_BUILD_PLAN.md
- CODEX_EXECUTION_LOG.md
- ACCEPTANCE_CRITERIA.md
- TODO.md
- TODO.md (historical implementation archive)
- IMPLEMENTATION_BACKLOG.md
- DEVELOPMENT_RULES.md
- AGENTS.md
+182
View File
@@ -0,0 +1,182 @@
# GeoIntel Release Runbook
## Scope
This runbook releases GeoIntel for Belgium and the Belgian North Sea. Mol and
the Kempen remain regression references. A successful release never implies
that every theme is operational in every jurisdiction; the coverage API and
source provenance remain authoritative.
The repository version is stored in `VERSION`. The current release candidate
is `v1.0.0-rc.1`.
## Mandatory preconditions
- clean `main` worktree at the commit being released;
- secure non-default PostGIS password in the Tower `.env`;
- existing local AI model only when the AI image is enabled;
- recent checksum-verified backup with SHA-256 storage inventory;
- Docker, `ssh-keygen`, Python 3.11, Node 20 and Bash available;
- one Alembic head and no unsupported metric represented as successful.
No command in this runbook downloads AI weights or implicitly deletes
application data.
## Repository gate
```bash
python -m compileall backend/app
cd backend && python -m pytest
cd ../frontend && npm run test:unit && npm run typecheck && npm run build
cd ..
bash scripts/run_readiness_check.sh
cd backend && python -m alembic heads
python -m alembic upgrade head --sql
cd ..
bash -n scripts/live_migration_smoke.sh
docker compose config
```
## Immutable deployment
On the Codex workstation:
```powershell
.\scripts\deploy_tower.ps1
```
On Tower:
```bash
cd /mnt/user/appdata/geointel
docker inspect --format \
'{{index .Config.Labels "org.opencontainers.image.version"}} {{index .Config.Labels "org.opencontainers.image.revision"}}' \
geointel
curl -fsS http://127.0.0.1:1202/health/ready
bash scripts/live_migration_smoke.sh
```
## Backup and recovery proof
Create an immutable backup. The SHA-256 inventory can take several minutes on
large storage:
```bash
bash scripts/backup_release_state.sh \
--container geointel \
--output-root /mnt/user/appdata/geointel/backups \
--release-id v1.0.0-rc.1 \
--storage-path /mnt/user/appdata/geointel/storage \
--models-path /mnt/user/appdata/geointel/models \
--inventory-mode sha256
```
Verify and restore only into an automatically generated temporary database:
```bash
bash scripts/verify_release_backup.sh \
--backup-dir /mnt/user/appdata/geointel/backups/v1.0.0-rc.1 \
--container geointel
bash scripts/restore_release_backup_smoke.sh \
--backup-dir /mnt/user/appdata/geointel/backups/v1.0.0-rc.1 \
--container geointel \
--confirm-isolated-restore
bash scripts/verify_release_upgrade_smoke.sh \
--backup-dir /mnt/user/appdata/geointel/backups/v1.0.0-rc.1 \
--container geointel \
--confirm-isolated-upgrade
```
The restore and upgrade scripts refuse the production database name and remove
their generated verification database.
## Fresh install, browser and data operations
```bash
bash scripts/verify_release_fresh_install.sh \
geointel-all-in-one:<release-commit>-ai
bash scripts/run_rc8_release_journeys.sh \
http://127.0.0.1:1202 artifacts/releases/v1.0.0-rc.1/rc8
bash scripts/run_rc9_ux_audit.sh \
http://127.0.0.1:1202 artifacts/releases/v1.0.0-rc.1/rc9
bash scripts/run_rc10_data_operations_audit.sh \
artifacts/releases/v1.0.0-rc.1/rc10
```
The RC10 command is read-only and runs cleanup in dry-run mode only.
## Supply-chain evidence
```bash
bash scripts/audit_python_dependencies.sh
cd frontend && npm audit --audit-level=high
cd ..
bash scripts/generate_container_sbom.sh \
geointel-all-in-one:<release-commit>-ai \
artifacts/releases/v1.0.0-rc.1/geointel-sbom.spdx.json
bash scripts/scan_container_image.sh \
geointel-all-in-one:<release-commit>-ai \
artifacts/releases/v1.0.0-rc.1/container-vulnerabilities.json
```
The complete vulnerability report remains evidence. The executable policy
gate fails on reachable fixed HIGH/CRITICAL findings.
## Rollback proof
The rollback command reuses persistent paths and never downgrades Alembic:
```bash
bash deploy/unraid/rollback-dockerman-container.sh
curl -fsS http://127.0.0.1:1202/health/ready
bash deploy/unraid/deploy-release.sh
curl -fsS http://127.0.0.1:1202/health/ready
```
For a future backward-incompatible migration, restore the verified pre-release
backup instead of running an older image against a newer schema.
## Tag and signed package
Create an SSH-signed Git tag at the accepted clean commit. Use a configured
release key; never add the private key to the repository:
```bash
git -c gpg.format=ssh \
-c user.signingkey=/secure/path/release-key \
tag -s v1.0.0-rc.1 -m "GeoIntel v1.0.0-rc.1"
git push origin v1.0.0-rc.1
```
Place the collected evidence files in an ignored package directory and create
the detached SSH signature plus complete checksum inventory:
```bash
python scripts/build_release_package.py build \
--output-dir artifacts/releases/v1.0.0-rc.1 \
--release-id v1.0.0-rc.1 \
--image-name geointel-all-in-one:<release-commit>-ai \
--image-id sha256:<image-id> \
--image-revision <release-commit> \
--signing-key /secure/path/release-key
python scripts/build_release_package.py verify \
--package-dir artifacts/releases/v1.0.0-rc.1
```
The builder refuses a dirty worktree, a tag not pointing at `HEAD`, a mismatched
image revision, missing evidence, symlinks, checksum drift or an invalid
signature.
## Safe cleanup and shutdown
Stop only the temporary smoke container by its generated name; normal scripts
already clean it automatically. To stop GeoIntel without deleting persistent
data:
```bash
docker stop geointel
```
Do not use broad Docker volume pruning. Application cleanup remains dry-run by
default and requires the exact confirmation, recent backup and delete ceiling
documented in `DATA_OPERATIONS_RUNBOOK.md`.
+2 -2
View File
@@ -1,12 +1,12 @@
{
"name": "geointel-frontend",
"version": "0.1.0",
"version": "1.0.0-rc.1",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "geointel-frontend",
"version": "0.1.0",
"version": "1.0.0-rc.1",
"dependencies": {
"maplibre-gl": "^4.7.1",
"react": "^18.2.0",
+1 -1
View File
@@ -1,7 +1,7 @@
{
"name": "geointel-frontend",
"private": true,
"version": "0.1.0",
"version": "1.0.0-rc.1",
"type": "module",
"scripts": {
"start": "vite",
+290
View File
@@ -0,0 +1,290 @@
#!/usr/bin/env python3
"""Build or verify a checksummed and SSH-signed GeoIntel release package."""
from __future__ import annotations
import argparse
import hashlib
import json
import os
import re
import subprocess
import sys
from datetime import datetime, timezone
from pathlib import Path
from typing import Sequence
ROOT = Path(__file__).resolve().parents[1]
MANIFEST_NAME = "release-manifest.json"
SIGNATURE_NAME = f"{MANIFEST_NAME}.sig"
SIGNERS_NAME = "allowed_signers"
CHECKSUMS_NAME = "CHECKSUMS.sha256"
GENERATED_NAMES = {MANIFEST_NAME, SIGNATURE_NAME, SIGNERS_NAME, CHECKSUMS_NAME}
RELEASE_ID_RE = re.compile(r"^v[0-9]+\.[0-9]+\.[0-9]+(?:-[0-9A-Za-z.-]+)?$")
def run(
command: Sequence[str],
*,
input_bytes: bytes | None = None,
cwd: Path = ROOT,
) -> subprocess.CompletedProcess[bytes]:
result = subprocess.run(
list(command),
cwd=cwd,
input=input_bytes,
capture_output=True,
check=False,
)
if result.returncode != 0:
stderr = result.stderr.decode("utf-8", errors="replace").strip()
raise RuntimeError(f"{' '.join(command)} failed: {stderr}")
return result
def sha256(path: Path) -> str:
digest = hashlib.sha256()
with path.open("rb") as handle:
for chunk in iter(lambda: handle.read(1024 * 1024), b""):
digest.update(chunk)
return digest.hexdigest()
def package_files(package_dir: Path, *, include_generated: bool) -> list[Path]:
result: list[Path] = []
for path in sorted(package_dir.rglob("*"), key=lambda item: item.as_posix()):
if path.is_symlink():
raise RuntimeError(f"Release packages may not contain symlinks: {path}")
if not path.is_file():
continue
relative = path.relative_to(package_dir)
if not include_generated and relative.as_posix() in GENERATED_NAMES:
continue
result.append(path)
return result
def public_key(signing_key: Path) -> str:
public_path = Path(f"{signing_key}.pub")
if public_path.is_file():
value = public_path.read_text(encoding="utf-8").strip()
else:
value = run(("ssh-keygen", "-y", "-f", str(signing_key))).stdout.decode(
"utf-8"
).strip()
if not value.startswith(("ssh-ed25519 ", "ssh-rsa ", "ecdsa-sha2-")):
raise RuntimeError("Unsupported or invalid SSH public signing key")
return " ".join(value.split()[:2])
def write_checksums(package_dir: Path) -> None:
lines = []
for path in package_files(package_dir, include_generated=True):
relative = path.relative_to(package_dir).as_posix()
if relative == CHECKSUMS_NAME:
continue
lines.append(f"{sha256(path)} {relative}")
(package_dir / CHECKSUMS_NAME).write_text(
"\n".join(lines) + "\n",
encoding="utf-8",
newline="\n",
)
def verify_checksums(package_dir: Path) -> None:
checksum_path = package_dir / CHECKSUMS_NAME
if not checksum_path.is_file():
raise RuntimeError(f"Missing {CHECKSUMS_NAME}")
expected_paths: set[str] = set()
for line in checksum_path.read_text(encoding="utf-8").splitlines():
digest, separator, relative = line.partition(" ")
if not separator or not re.fullmatch(r"[0-9a-f]{64}", digest):
raise RuntimeError(f"Invalid checksum line: {line!r}")
candidate = (package_dir / relative).resolve()
try:
candidate.relative_to(package_dir)
except ValueError as exc:
raise RuntimeError(f"Checksum path escapes package: {relative}") from exc
if not candidate.is_file() or candidate.is_symlink():
raise RuntimeError(f"Checksummed release file is unavailable: {relative}")
if sha256(candidate) != digest:
raise RuntimeError(f"Checksum mismatch: {relative}")
expected_paths.add(relative)
actual_paths = {
path.relative_to(package_dir).as_posix()
for path in package_files(package_dir, include_generated=True)
if path.name != CHECKSUMS_NAME
}
if expected_paths != actual_paths:
raise RuntimeError(
"Checksum inventory differs from package contents: "
f"missing={sorted(actual_paths - expected_paths)}, "
f"unexpected={sorted(expected_paths - actual_paths)}"
)
def verify_package(package_dir: Path) -> dict[str, object]:
package_dir = package_dir.expanduser().resolve()
verify_checksums(package_dir)
manifest_path = package_dir / MANIFEST_NAME
manifest = json.loads(manifest_path.read_text(encoding="utf-8"))
identity = str(manifest["signature"]["identity"])
namespace = str(manifest["signature"]["namespace"])
run(
(
"ssh-keygen",
"-Y",
"verify",
"-f",
str(package_dir / SIGNERS_NAME),
"-I",
identity,
"-n",
namespace,
"-s",
str(package_dir / SIGNATURE_NAME),
),
input_bytes=manifest_path.read_bytes(),
)
if manifest.get("scope") != "Belgium and the Belgian North Sea":
raise RuntimeError("Unexpected release scope")
if manifest.get("release_id") != f"v{manifest.get('version')}":
raise RuntimeError("Release id and semantic version differ")
return manifest
def build_package(args: argparse.Namespace) -> dict[str, object]:
package_dir = args.output_dir.expanduser().resolve()
package_dir.mkdir(parents=True, exist_ok=True)
version = (ROOT / "VERSION").read_text(encoding="utf-8").strip()
release_id = args.release_id or f"v{version}"
if not RELEASE_ID_RE.fullmatch(release_id) or release_id != f"v{version}":
raise RuntimeError("Release id must equal v<VERSION> and be valid SemVer")
commit = run(("git", "rev-parse", "HEAD")).stdout.decode().strip()
dirty = run(("git", "status", "--porcelain=v1")).stdout.decode().strip()
if dirty:
raise RuntimeError("Release package requires a clean Git worktree")
tag_commit = run(("git", "rev-list", "-n", "1", release_id)).stdout.decode().strip()
if tag_commit != commit:
raise RuntimeError(f"Tag {release_id} does not point to HEAD")
if args.image_revision != commit:
raise RuntimeError("Image revision must equal the tagged Git commit")
evidence = []
for path in package_files(package_dir, include_generated=False):
evidence.append(
{
"path": path.relative_to(package_dir).as_posix(),
"size_bytes": path.stat().st_size,
"sha256": sha256(path),
}
)
if not evidence:
raise RuntimeError("At least one release evidence file is required")
signing_key = args.signing_key.expanduser().resolve()
if not signing_key.is_file():
raise RuntimeError(f"SSH signing key is unavailable: {signing_key}")
identity = args.identity
namespace = "geointel-release"
(package_dir / SIGNERS_NAME).write_text(
f"{identity} {public_key(signing_key)}\n",
encoding="utf-8",
newline="\n",
)
manifest: dict[str, object] = {
"schema_version": 1,
"release_id": release_id,
"version": version,
"scope": "Belgium and the Belgian North Sea",
"created_at": datetime.now(timezone.utc).isoformat(),
"git": {
"commit": commit,
"tag": release_id,
"clean": True,
},
"image": {
"name": args.image_name,
"id": args.image_id,
"revision": args.image_revision,
},
"signature": {
"algorithm": "SSH",
"identity": identity,
"namespace": namespace,
},
"evidence": evidence,
}
manifest_path = package_dir / MANIFEST_NAME
manifest_path.write_text(
json.dumps(manifest, indent=2, sort_keys=True) + "\n",
encoding="utf-8",
newline="\n",
)
signature_path = package_dir / SIGNATURE_NAME
if signature_path.exists():
signature_path.unlink()
run(
(
"ssh-keygen",
"-Y",
"sign",
"-f",
str(signing_key),
"-n",
namespace,
str(manifest_path),
)
)
if not signature_path.is_file():
raise RuntimeError("ssh-keygen did not create the detached signature")
write_checksums(package_dir)
return verify_package(package_dir)
def parse_args(argv: Sequence[str] | None = None) -> argparse.Namespace:
parser = argparse.ArgumentParser(description=__doc__)
subparsers = parser.add_subparsers(dest="command", required=True)
build = subparsers.add_parser("build")
build.add_argument("--output-dir", type=Path, required=True)
build.add_argument("--release-id")
build.add_argument("--image-name", required=True)
build.add_argument("--image-id", required=True)
build.add_argument("--image-revision", required=True)
build.add_argument(
"--signing-key",
type=Path,
default=os.environ.get("GEOINTEL_RELEASE_SIGNING_KEY"),
required=not bool(os.environ.get("GEOINTEL_RELEASE_SIGNING_KEY")),
)
build.add_argument("--identity", default="geointel-release")
verify = subparsers.add_parser("verify")
verify.add_argument("--package-dir", type=Path, required=True)
return parser.parse_args(argv)
def main(argv: Sequence[str] | None = None) -> int:
args = parse_args(argv)
try:
if args.command == "build":
manifest = build_package(args)
print(
f"Built and verified signed release package "
f"{manifest['release_id']} at {args.output_dir.resolve()}"
)
else:
manifest = verify_package(args.package_dir)
print(
f"Verified signed release package "
f"{manifest['release_id']} at {args.package_dir.resolve()}"
)
except (OSError, KeyError, ValueError, RuntimeError, json.JSONDecodeError) as exc:
print(f"ERROR: {exc}", file=sys.stderr)
return 1
return 0
if __name__ == "__main__":
raise SystemExit(main())
+3
View File
@@ -21,6 +21,7 @@ from typing import Any, Sequence
ROOT = Path(__file__).resolve().parents[1]
BACKEND = ROOT / "backend"
DEFAULT_HASHED_FILES = (
"VERSION",
"AGENTS.md",
"backend/pyproject.toml",
"backend/alembic.ini",
@@ -31,6 +32,7 @@ DEFAULT_HASHED_FILES = (
"deploy/unraid/Dockerfile.all-in-one",
"docs/RC_SCOPE_FREEZE_BELGIUM_NORTH_SEA.md",
"docs/RC_ROADMAP_BELGIUM_NORTH_SEA.md",
"docs/RELEASE_RUNBOOK.md",
)
DEPENDENCIES = (
"alembic",
@@ -227,6 +229,7 @@ def build_manifest(args: argparse.Namespace) -> dict[str, Any]:
return {
"schema_version": 1,
"release_id": args.release_id,
"version": (ROOT / "VERSION").read_text(encoding="utf-8").strip(),
"captured_at": utc_now(),
"read_only": True,
"scope": "Belgium and the Belgian North Sea",
+1
View File
@@ -40,6 +40,7 @@ fi
echo "== GeoIntel run readiness check =="
"$PYTHON_BIN" -m py_compile scripts/capture_release_evidence.py
"$PYTHON_BIN" -m py_compile scripts/build_release_package.py
"$PYTHON_BIN" -m py_compile scripts/verify_python_lock.py
"$PYTHON_BIN" scripts/verify_python_lock.py
"$PYTHON_BIN" -m py_compile scripts/verify_security_exceptions.py
+12 -3
View File
@@ -39,7 +39,16 @@ if 'prompts/codex/m14/CODEX_FIRST_DAY_MASTER_PROMPT.md' not in start:
raise SystemExit('CODEX_START.md does not point to M14 first-day prompt')
readme = (ROOT / 'README.md').read_text(encoding='utf-8')
if 'M14 — Build Launch Package' not in readme:
raise SystemExit('README.md does not identify M14 as current milestone')
active_terms = [
'v1.0.0-rc.1',
'docs/RC_SCOPE_FREEZE_BELGIUM_NORTH_SEA.md',
'docs/RC_ROADMAP_BELGIUM_NORTH_SEA.md',
]
missing_active_terms = [term for term in active_terms if term not in readme]
if missing_active_terms:
raise SystemExit(
'README.md is missing active release controls: '
+ ', '.join(missing_active_terms)
)
print('M14 launch assets OK')
print('Historical M14 assets and active RC controls OK')