Make Mol the primary operating context
GeoIntel CI / docs-smoke (push) Canceled after 0s
GeoIntel CI / contract-smoke (push) Canceled after 0s

This commit is contained in:
Codex
2026-07-13 17:47:21 +02:00
parent e2ddf84c74
commit e8eecb2895
14 changed files with 179 additions and 21 deletions
+8
View File
@@ -7,6 +7,14 @@
# Changelog # Changelog
## Sprint 177 Mol-first operating context (2026-07-13)
- Made Mol the explicit primary operating focus while retaining the broader Kempen as the validation and interoperability region.
- Added one centralized frontend focus definition for the empty-map center, new-project region, default AOI and persisted project/dataset recognition.
- Initial workbench selection now prefers real persisted Mol data without overriding an explicit current or newly created project selection.
- Moved Mol to the front of default operator sample preparation and added AOI slugs to future detection quality-matrix project names.
- Kept APIs, migrations, persistence, providers, AI dependencies and model behavior unchanged.
## Sprint 176 Detection false-positive visual review gate (2026-07-13) ## Sprint 176 Detection false-positive visual review gate (2026-07-13)
- Enriched existing QA evidence GeoJSON with persisted detection and segmentation provenance without changing its endpoint or canonical envelope. - Enriched existing QA evidence GeoJSON with persisted detection and segmentation provenance without changing its endpoint or canonical envelope.
+2
View File
@@ -2,6 +2,8 @@
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 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.
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.
GeoIntel is not a generic dashboard or chatbot. The core product is: GeoIntel is not a generic dashboard or chatbot. The core product is:
> data → processing → geospatial output → QA/QC → export > data → processing → geospatial output → QA/QC → export
@@ -0,0 +1,54 @@
from pathlib import Path
ROOT = Path(__file__).resolve().parents[2]
def test_frontend_declares_mol_as_primary_operating_focus() -> None:
focus = (ROOT / "frontend" / "src" / "config" / "primaryFocus.ts").read_text(
encoding="utf-8"
)
project_hook = (ROOT / "frontend" / "src" / "hooks" / "useProjectWorkspace.ts").read_text(
encoding="utf-8"
)
map_source = (ROOT / "frontend" / "src" / "components" / "GeoMap.tsx").read_text(
encoding="utf-8"
)
app = (ROOT / "frontend" / "src" / "App.tsx").read_text(encoding="utf-8")
assert "PRIMARY_FOCUS_LABEL = 'Mol'" in focus
assert "PRIMARY_FOCUS_REGION = 'Mol, Kempen'" in focus
assert "[5.1167, 51.1919]" in focus
assert "isPrimaryFocusProjectData" in focus
assert "isPrimaryFocusProjectData(project, data.datasets)" in project_hook
assert "PRIMARY_FOCUS_AREA_NAME" in project_hook
assert "PRIMARY_FOCUS_AREA_GEOJSON" in project_hook
assert "4.35,51.28" not in project_hook
assert "center: PRIMARY_FOCUS_CENTER" in map_source
assert "GeoAI Workbench · Mol / Kempen" in app
def test_operator_workflows_put_mol_first_and_name_future_projects() -> None:
samples = (ROOT / "scripts" / "prepare_operator_real_data_samples.py").read_text(
encoding="utf-8"
)
matrix = (ROOT / "scripts" / "run_detection_quality_matrix.sh").read_text(
encoding="utf-8"
)
multi_matrix = (
ROOT / "scripts" / "run_multi_sample_detection_quality_matrix.sh"
).read_text(encoding="utf-8")
assert samples.index('"mol": OperatorSample(') < samples.index('"geel": OperatorSample(')
assert 'QUALITY_SAMPLE_SLUG="${QUALITY_SAMPLE_SLUG:-}"' in matrix
assert "sample ${QUALITY_SAMPLE_SLUG}" in matrix
assert 'QUALITY_SAMPLE_SLUG="${sample_slug}"' in multi_matrix
def test_product_docs_record_mol_primary_focus_without_dropping_kempen_scope() -> 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
+25 -1
View File
@@ -7233,4 +7233,28 @@ Open:
## Next recommended pass ## Next recommended pass
- Re-export the seven-AOI evidence portfolio from the deployed backend, render the Turnhout/Herentals/Geel sheets and inspect the real orthophoto evidence. Keep all CSV decisions `unreviewed` until an operator makes an explicit visual classification; do not start another model training run yet. - Re-export the seven-AOI evidence portfolio from the deployed backend, render the Mol/Geel/Turnhout sheets and inspect the real orthophoto evidence. Keep all CSV decisions `unreviewed` until an operator makes an explicit visual classification; do not start another model training run yet.
# Sprint 177 - Mol-first operating context
## Product and workbench focus
- Declared Mol as the primary operational context while preserving the broader Kempen for cross-area validation and regional interoperability.
- Centralized Mol coordinates, default region and a compact 1 km EPSG:4326 MultiPolygon AOI in the frontend focus configuration.
- Initial project discovery now recognizes Mol from project metadata or persisted dataset provenance and inspects those candidates first.
- Explicitly selected and newly created projects still take precedence, so the focus rule does not fight operator intent.
- The empty MapLibre workbench starts over Mol; loaded AOIs and data continue to determine map bounds normally.
## Operator provenance
- Moved Mol to the first position in default real-data sample preparation without dropping any existing Kempen sample.
- Multi-sample quality runs now forward their AOI slug, and future persisted matrix project names retain that origin instead of appearing as anonymous model/tile runs.
- No provider fetch, AI output, QA metric, migration, API contract or model configuration was changed.
## Validation
- Focused Mol-primary regression coverage: `3 passed`.
- `bash scripts/run_readiness_check.sh`: passed with `481` backend tests, API-contract audit, single Alembic head, frontend typecheck and production build.
- Frontend production bundles: app `217.69 kB`, React vendor `140.74 kB`, MapLibre `801.82 kB` before gzip.
- `python -m alembic upgrade head --sql`: complete migration chain rendered through single head `202606120900`.
- Both changed matrix scripts pass `bash -n`; no migration file changed.
+4 -2
View File
@@ -32,11 +32,13 @@ GeoIntel moet bewijzen dat de ontwikkelaar de volledige keten begrijpt:
De Kempen vormen de afgebakende regio. Dit maakt het project concreet, realistisch en demo-baar. Voorbeelden: De Kempen vormen de afgebakende regio. Dit maakt het project concreet, realistisch en demo-baar. Voorbeelden:
- gebouwdetectie in Geel - gebouwdetectie en infrastructuuranalyse in Mol
- infrastructuurdruk rond Mol - vergelijkende validatie in Geel
- verstedelijking rond Turnhout - verstedelijking rond Turnhout
- natuurfragmentatie rond Kasterlee of Retie - 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.
## Richting ## Richting
De definitieve richting is: De definitieve richting is:
+1
View File
@@ -18,6 +18,7 @@ This file now starts with the current implementation status. Older preparation/b
## Current implementation status ## Current implementation status
- [x] Make Mol the primary workbench, AOI and operator-sample context while preserving broader Kempen coverage.
- [x] Backend FastAPI foundation, health endpoint and service structure. - [x] Backend FastAPI foundation, health endpoint and service structure.
- [x] React/TypeScript frontend foundation and MapLibre workbench. - [x] React/TypeScript frontend foundation and MapLibre workbench.
- [x] Map layer visibility, opacity and feature property inspection. - [x] Map layer visibility, opacity and feature property inspection.
+2
View File
@@ -2,6 +2,8 @@
React + TypeScript + MapLibre foundation for project/area/dataset workflow. React + TypeScript + MapLibre foundation for project/area/dataset workflow.
Mol is the primary operating context. On a fresh session the workbench prefers a persisted Mol project or dataset, centers an empty MapLibre view on Mol and pre-fills a compact Mol AOI. Explicit project and dataset selections remain authoritative, and all broader Kempen workflows remain available.
The workbench now uses a task-based shell instead of a single long panel stack. `App.tsx` still owns shared orchestration state, but the UI is organized into Overview, Data, Map, QA/QC, AI Labs, Exports and System workspaces with a persistent top context bar and right-side dataset inspector. The workbench now uses a task-based shell instead of a single long panel stack. `App.tsx` still owns shared orchestration state, but the UI is organized into Overview, Data, Map, QA/QC, AI Labs, Exports and System workspaces with a persistent top context bar and right-side dataset inspector.
The shell has a calmer V1 workbench density pass: the duplicated workspace command bar is hidden, the sidebar is more compact, the top context bar is quieter, readiness tiles are smaller and panels use softer borders/shadows. This keeps the same workflows and API calls while reducing visual noise on desktop and mobile. The shell has a calmer V1 workbench density pass: the duplicated workspace command bar is hidden, the sidebar is more compact, the top context bar is quieter, readiness tiles are smaller and panels use softer borders/shadows. This keeps the same workflows and API calls while reducing visual noise on desktop and mobile.
+1 -1
View File
@@ -643,7 +643,7 @@ function App(): JSX.Element {
</a> </a>
<header className="workbench-topbar"> <header className="workbench-topbar">
<div className="brand-block"> <div className="brand-block">
<p className="eyebrow">GeoAI Workbench</p> <p className="eyebrow">GeoAI Workbench · Mol / Kempen</p>
<h1>GeoIntel Kempen</h1> <h1>GeoIntel Kempen</h1>
</div> </div>
<div className="context-bar" aria-label="Active workbench context"> <div className="context-bar" aria-label="Active workbench context">
+3 -2
View File
@@ -1,6 +1,7 @@
import { useEffect, useRef, useState } from 'react' import { useEffect, useRef, useState } from 'react'
import maplibregl from 'maplibre-gl' import maplibregl from 'maplibre-gl'
import 'maplibre-gl/dist/maplibre-gl.css' import 'maplibre-gl/dist/maplibre-gl.css'
import { PRIMARY_FOCUS_CENTER } from '../config/primaryFocus'
interface GeoMapProps { interface GeoMapProps {
data: GeoJSON.FeatureCollection | null data: GeoJSON.FeatureCollection | null
@@ -162,8 +163,8 @@ function GeoMap({
const map = new maplibregl.Map({ const map = new maplibregl.Map({
container: containerRef.current, container: containerRef.current,
style: defaultMapStyle(), style: defaultMapStyle(),
center: [5.3, 51.3], center: PRIMARY_FOCUS_CENTER,
zoom: 9, zoom: 11,
attributionControl: false, attributionControl: false,
}) })
map.addControl(new maplibregl.NavigationControl(), 'top-right') map.addControl(new maplibregl.NavigationControl(), 'top-right')
+37
View File
@@ -0,0 +1,37 @@
import type { DatasetCreateResponse, ProjectRead } from '../types'
export const PRIMARY_FOCUS_LABEL = 'Mol'
export const PRIMARY_FOCUS_REGION = 'Mol, Kempen'
export const PRIMARY_FOCUS_CENTER: [number, number] = [5.1167, 51.1919]
export const PRIMARY_FOCUS_AREA_NAME = 'Mol primary AOI'
export const PRIMARY_FOCUS_AREA_GEOJSON =
'{"type":"MultiPolygon","coordinates":[[[[5.109476746222376,51.18745107550715],[5.123924622861509,51.18745107550715],[5.123924622861509,51.19634848029239],[5.109476746222376,51.19634848029239],[5.109476746222376,51.18745107550715]]]]}'
const PRIMARY_FOCUS_TOKEN = /(^|[^a-z0-9])mol([^a-z0-9]|$)/i
function containsPrimaryFocus(values: Array<string | null | undefined>): boolean {
return values.some((value) => Boolean(value && PRIMARY_FOCUS_TOKEN.test(value)))
}
export function isPrimaryFocusProject(project: ProjectRead): boolean {
return containsPrimaryFocus([project.name, project.description, project.region])
}
export function isPrimaryFocusProjectData(
project: ProjectRead,
datasets: DatasetCreateResponse[],
): boolean {
if (isPrimaryFocusProject(project)) {
return true
}
return datasets.some((dataset) =>
containsPrimaryFocus([
dataset.name,
dataset.original_filename,
dataset.stored_filename,
dataset.storage_path,
dataset.source_name,
dataset.reference_layer_name,
]),
)
}
+27 -8
View File
@@ -1,4 +1,11 @@
import { FormEvent, useMemo, useState } from 'react' import { FormEvent, useMemo, useState } from 'react'
import {
PRIMARY_FOCUS_AREA_GEOJSON,
PRIMARY_FOCUS_AREA_NAME,
PRIMARY_FOCUS_REGION,
isPrimaryFocusProject,
isPrimaryFocusProjectData,
} from '../config/primaryFocus'
import { areasApi } from '../services/api/areas' import { areasApi } from '../services/api/areas'
import { datasetsApi } from '../services/api/datasets' import { datasetsApi } from '../services/api/datasets'
import { projectsApi } from '../services/api/projects' import { projectsApi } from '../services/api/projects'
@@ -31,12 +38,11 @@ export function useProjectWorkspace() {
const [projectForm, setProjectForm] = useState<ProjectCreate>({ const [projectForm, setProjectForm] = useState<ProjectCreate>({
name: '', name: '',
description: '', description: '',
region: 'Kempen', region: PRIMARY_FOCUS_REGION,
}) })
const [areaForm, setAreaForm] = useState({ const [areaForm, setAreaForm] = useState({
name: 'Demo AOI', name: PRIMARY_FOCUS_AREA_NAME,
geometry: geometry: PRIMARY_FOCUS_AREA_GEOJSON,
'{"type":"MultiPolygon","coordinates":[[[[4.35,51.28],[4.55,51.28],[4.55,51.46],[4.35,51.46],[4.35,51.28]]]]}',
crs: 'EPSG:4326', crs: 'EPSG:4326',
}) })
@@ -60,19 +66,32 @@ export function useProjectWorkspace() {
if (selectedProjectId && items.some((project) => project.id === selectedProjectId)) { if (selectedProjectId && items.some((project) => project.id === selectedProjectId)) {
return selectedProjectId return selectedProjectId
} }
const primaryProjects = items.filter(isPrimaryFocusProject)
const demoProjects = items.filter(isDemoProject) const demoProjects = items.filter(isDemoProject)
const candidates = [...demoProjects, ...items.filter((project) => !isDemoProject(project))].slice(0, 8) const candidates = Array.from(
new Map(
[...primaryProjects, ...demoProjects, ...items].map((project) => [project.id, project]),
).values(),
).slice(0, 12)
const inspectedCandidates: Array<{
project: ProjectRead
data: { areas: AreaRead[]; datasets: DatasetCreateResponse[] }
}> = []
for (const project of candidates) { for (const project of candidates) {
try { try {
const data = await fetchProjectData(project.id) const data = await fetchProjectData(project.id)
if (data.areas.length > 0 && data.datasets.length > 0) { inspectedCandidates.push({ project, data })
if (data.datasets.length > 0 && isPrimaryFocusProjectData(project, data.datasets)) {
return project.id return project.id
} }
} catch { } catch {
// Project list should still render if a candidate's detail endpoints are temporarily unavailable. // Project list should still render if a candidate's detail endpoints are temporarily unavailable.
} }
} }
return demoProjects[0]?.id ?? items[0]?.id ?? null const completeContext = inspectedCandidates.find(
({ data }) => data.areas.length > 0 && data.datasets.length > 0,
)
return completeContext?.project.id ?? primaryProjects[0]?.id ?? demoProjects[0]?.id ?? items[0]?.id ?? null
} }
const loadProjects = async (preferredProjectId?: string | null) => { const loadProjects = async (preferredProjectId?: string | null) => {
@@ -120,7 +139,7 @@ export function useProjectWorkspace() {
const createdProject = await projectsApi.create({ const createdProject = await projectsApi.create({
name: projectForm.name.trim(), name: projectForm.name.trim(),
description: projectForm.description?.trim() || undefined, description: projectForm.description?.trim() || undefined,
region: projectForm.region?.trim() || 'Kempen', region: projectForm.region?.trim() || PRIMARY_FOCUS_REGION,
}) })
setProjectForm((previous) => ({ ...previous, name: '', description: '' })) setProjectForm((previous) => ({ ...previous, name: '', description: '' }))
setSelectedProjectId(createdProject.id) setSelectedProjectId(createdProject.id)
@@ -64,18 +64,18 @@ class OperatorSample:
SAMPLES: dict[str, OperatorSample] = { SAMPLES: dict[str, OperatorSample] = {
"geel": OperatorSample(
slug="geel",
display_name="Geel center",
center_lon=4.991,
center_lat=51.162,
),
"mol": OperatorSample( "mol": OperatorSample(
slug="mol", slug="mol",
display_name="Mol center", display_name="Mol center",
center_lon=5.1167, center_lon=5.1167,
center_lat=51.1919, center_lat=51.1919,
), ),
"geel": OperatorSample(
slug="geel",
display_name="Geel center",
center_lon=4.991,
center_lat=51.162,
),
"turnhout": OperatorSample( "turnhout": OperatorSample(
slug="turnhout", slug="turnhout",
display_name="Turnhout center", display_name="Turnhout center",
+8 -1
View File
@@ -10,6 +10,7 @@ Usage:
QUALITY_TILE_SIZES="512 640" \ QUALITY_TILE_SIZES="512 640" \
QUALITY_TILE_OVERLAPS="64" \ QUALITY_TILE_OVERLAPS="64" \
QUALITY_THRESHOLDS="0.50 0.25 0.15" \ QUALITY_THRESHOLDS="0.50 0.25 0.15" \
QUALITY_SAMPLE_SLUG="mol" \
bash scripts/run_detection_quality_matrix.sh [base_url] bash scripts/run_detection_quality_matrix.sh [base_url]
or: or:
@@ -25,6 +26,7 @@ Optional environment:
QUALITY_TILE_OVERLAPS Space/comma separated raster tile overlaps, default: 64. QUALITY_TILE_OVERLAPS Space/comma separated raster tile overlaps, default: 64.
QUALITY_THRESHOLDS Space/comma separated confidence thresholds, default: 0.50 0.25 0.15. QUALITY_THRESHOLDS Space/comma separated confidence thresholds, default: 0.50 0.25 0.15.
QUALITY_OUTPUT_DIR Output directory, default: artifacts/detection-quality-matrix/<timestamp>. QUALITY_OUTPUT_DIR Output directory, default: artifacts/detection-quality-matrix/<timestamp>.
QUALITY_SAMPLE_SLUG Optional AOI slug included in persisted project names.
REAL_IOU_THRESHOLD QA IoU threshold, default inherited by the underlying workflow. REAL_IOU_THRESHOLD QA IoU threshold, default inherited by the underlying workflow.
This script never downloads models and never uses fixture detections. It repeats This script never downloads models and never uses fixture detections. It repeats
@@ -42,6 +44,7 @@ QUALITY_MODEL_ASSET_IDS="${QUALITY_MODEL_ASSET_IDS:-${REAL_MODEL_ASSET_ID:-__act
QUALITY_TILE_SIZES="${QUALITY_TILE_SIZES:-640}" QUALITY_TILE_SIZES="${QUALITY_TILE_SIZES:-640}"
QUALITY_TILE_OVERLAPS="${QUALITY_TILE_OVERLAPS:-64}" QUALITY_TILE_OVERLAPS="${QUALITY_TILE_OVERLAPS:-64}"
QUALITY_THRESHOLDS="${QUALITY_THRESHOLDS:-0.50 0.25 0.15}" QUALITY_THRESHOLDS="${QUALITY_THRESHOLDS:-0.50 0.25 0.15}"
QUALITY_SAMPLE_SLUG="${QUALITY_SAMPLE_SLUG:-}"
QUALITY_OUTPUT_DIR="${QUALITY_OUTPUT_DIR:-artifacts/detection-quality-matrix/$(date -u +%Y%m%dT%H%M%SZ)}" QUALITY_OUTPUT_DIR="${QUALITY_OUTPUT_DIR:-artifacts/detection-quality-matrix/$(date -u +%Y%m%dT%H%M%SZ)}"
if [ "${BASE_URL}" = "-h" ] || [ "${BASE_URL}" = "--help" ]; then if [ "${BASE_URL}" = "-h" ] || [ "${BASE_URL}" = "--help" ]; then
@@ -174,9 +177,13 @@ while IFS=$'\t' read -r model_request tile_size tile_overlap threshold run_label
if [ "${model_request}" = "__active__" ]; then if [ "${model_request}" = "__active__" ]; then
model_env="" model_env=""
fi fi
project_sample_label=""
if [ -n "${QUALITY_SAMPLE_SLUG}" ]; then
project_sample_label=" sample ${QUALITY_SAMPLE_SLUG}"
fi
echo "-- Matrix run ${run_index}: model=${model_request} tile=${tile_size} overlap=${tile_overlap} threshold=${threshold} --" echo "-- Matrix run ${run_index}: model=${model_request} tile=${tile_size} overlap=${tile_overlap} threshold=${threshold} --"
if ! REAL_PROJECT_NAME="GeoIntel Detection Quality Matrix ${model_request} tile ${tile_size} overlap ${tile_overlap} threshold ${threshold}" \ if ! REAL_PROJECT_NAME="GeoIntel Detection Quality Matrix${project_sample_label} ${model_request} tile ${tile_size} overlap ${tile_overlap} threshold ${threshold}" \
REAL_MODEL_ASSET_ID="${model_env}" \ REAL_MODEL_ASSET_ID="${model_env}" \
REAL_TILE_SIZE="${tile_size}" \ REAL_TILE_SIZE="${tile_size}" \
REAL_TILE_OVERLAP="${tile_overlap}" \ REAL_TILE_OVERLAP="${tile_overlap}" \
@@ -137,6 +137,7 @@ while IFS=$'\t' read -r sample_slug raster_path reference_path reference_feature
echo "-- Sample ${sample_slug}: reference_features=${reference_feature_count} --" echo "-- Sample ${sample_slug}: reference_features=${reference_feature_count} --"
REAL_RASTER_PATH="${raster_path}" \ REAL_RASTER_PATH="${raster_path}" \
REAL_REFERENCE_VECTOR_PATH="${reference_path}" \ REAL_REFERENCE_VECTOR_PATH="${reference_path}" \
QUALITY_SAMPLE_SLUG="${sample_slug}" \
QUALITY_OUTPUT_DIR="${sample_output_dir}" \ QUALITY_OUTPUT_DIR="${sample_output_dir}" \
bash scripts/run_detection_quality_matrix.sh "${BASE_URL}" bash scripts/run_detection_quality_matrix.sh "${BASE_URL}"
done < "${sample_manifest_tsv}" done < "${sample_manifest_tsv}"