From e8eecb28955513c980a9b78599c0b149892d9772 Mon Sep 17 00:00:00 2001 From: Codex Date: Mon, 13 Jul 2026 17:47:21 +0200 Subject: [PATCH] Make Mol the primary operating context --- CHANGELOG.md | 8 +++ README.md | 2 + .../tests/test_sprint177_mol_primary_focus.py | 54 +++++++++++++++++++ docs/CODEX_EXECUTION_LOG.md | 26 ++++++++- docs/PRODUCT_VISION.md | 6 ++- docs/TODO.md | 1 + frontend/README.md | 2 + frontend/src/App.tsx | 2 +- frontend/src/components/GeoMap.tsx | 5 +- frontend/src/config/primaryFocus.ts | 37 +++++++++++++ frontend/src/hooks/useProjectWorkspace.ts | 35 +++++++++--- scripts/prepare_operator_real_data_samples.py | 12 ++--- scripts/run_detection_quality_matrix.sh | 9 +++- ...n_multi_sample_detection_quality_matrix.sh | 1 + 14 files changed, 179 insertions(+), 21 deletions(-) create mode 100644 backend/tests/test_sprint177_mol_primary_focus.py create mode 100644 frontend/src/config/primaryFocus.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index e3594620..e393a258 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,14 @@ # 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) - Enriched existing QA evidence GeoJSON with persisted detection and segmentation provenance without changing its endpoint or canonical envelope. diff --git a/README.md b/README.md index 63c46a2a..3784f9e3 100644 --- a/README.md +++ b/README.md @@ -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. +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: > data → processing → geospatial output → QA/QC → export diff --git a/backend/tests/test_sprint177_mol_primary_focus.py b/backend/tests/test_sprint177_mol_primary_focus.py new file mode 100644 index 00000000..1efeb59b --- /dev/null +++ b/backend/tests/test_sprint177_mol_primary_focus.py @@ -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 diff --git a/docs/CODEX_EXECUTION_LOG.md b/docs/CODEX_EXECUTION_LOG.md index b6483386..6e8616ba 100644 --- a/docs/CODEX_EXECUTION_LOG.md +++ b/docs/CODEX_EXECUTION_LOG.md @@ -7233,4 +7233,28 @@ Open: ## 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. diff --git a/docs/PRODUCT_VISION.md b/docs/PRODUCT_VISION.md index d1804250..4f205017 100644 --- a/docs/PRODUCT_VISION.md +++ b/docs/PRODUCT_VISION.md @@ -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: -- gebouwdetectie in Geel -- infrastructuurdruk rond Mol +- 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. + ## Richting De definitieve richting is: diff --git a/docs/TODO.md b/docs/TODO.md index 8c326904..add48187 100644 --- a/docs/TODO.md +++ b/docs/TODO.md @@ -18,6 +18,7 @@ This file now starts with the current implementation status. Older preparation/b ## 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] React/TypeScript frontend foundation and MapLibre workbench. - [x] Map layer visibility, opacity and feature property inspection. diff --git a/frontend/README.md b/frontend/README.md index 25ca019b..78a83ed1 100644 --- a/frontend/README.md +++ b/frontend/README.md @@ -2,6 +2,8 @@ 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 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. diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 66b343d4..e32b0aca 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -643,7 +643,7 @@ function App(): JSX.Element {
-

GeoAI Workbench

+

GeoAI Workbench · Mol / Kempen

GeoIntel Kempen

diff --git a/frontend/src/components/GeoMap.tsx b/frontend/src/components/GeoMap.tsx index bb3f5bf0..a85e2d80 100644 --- a/frontend/src/components/GeoMap.tsx +++ b/frontend/src/components/GeoMap.tsx @@ -1,6 +1,7 @@ import { useEffect, useRef, useState } from 'react' import maplibregl from 'maplibre-gl' import 'maplibre-gl/dist/maplibre-gl.css' +import { PRIMARY_FOCUS_CENTER } from '../config/primaryFocus' interface GeoMapProps { data: GeoJSON.FeatureCollection | null @@ -162,8 +163,8 @@ function GeoMap({ const map = new maplibregl.Map({ container: containerRef.current, style: defaultMapStyle(), - center: [5.3, 51.3], - zoom: 9, + center: PRIMARY_FOCUS_CENTER, + zoom: 11, attributionControl: false, }) map.addControl(new maplibregl.NavigationControl(), 'top-right') diff --git a/frontend/src/config/primaryFocus.ts b/frontend/src/config/primaryFocus.ts new file mode 100644 index 00000000..9713c505 --- /dev/null +++ b/frontend/src/config/primaryFocus.ts @@ -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): 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, + ]), + ) +} diff --git a/frontend/src/hooks/useProjectWorkspace.ts b/frontend/src/hooks/useProjectWorkspace.ts index ac3f7172..2ff34bd8 100644 --- a/frontend/src/hooks/useProjectWorkspace.ts +++ b/frontend/src/hooks/useProjectWorkspace.ts @@ -1,4 +1,11 @@ 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 { datasetsApi } from '../services/api/datasets' import { projectsApi } from '../services/api/projects' @@ -31,12 +38,11 @@ export function useProjectWorkspace() { const [projectForm, setProjectForm] = useState({ name: '', description: '', - region: 'Kempen', + region: PRIMARY_FOCUS_REGION, }) const [areaForm, setAreaForm] = useState({ - name: 'Demo AOI', - geometry: - '{"type":"MultiPolygon","coordinates":[[[[4.35,51.28],[4.55,51.28],[4.55,51.46],[4.35,51.46],[4.35,51.28]]]]}', + name: PRIMARY_FOCUS_AREA_NAME, + geometry: PRIMARY_FOCUS_AREA_GEOJSON, crs: 'EPSG:4326', }) @@ -60,19 +66,32 @@ export function useProjectWorkspace() { if (selectedProjectId && items.some((project) => project.id === selectedProjectId)) { return selectedProjectId } + const primaryProjects = items.filter(isPrimaryFocusProject) 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) { try { 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 } } catch { // 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) => { @@ -120,7 +139,7 @@ export function useProjectWorkspace() { const createdProject = await projectsApi.create({ name: projectForm.name.trim(), description: projectForm.description?.trim() || undefined, - region: projectForm.region?.trim() || 'Kempen', + region: projectForm.region?.trim() || PRIMARY_FOCUS_REGION, }) setProjectForm((previous) => ({ ...previous, name: '', description: '' })) setSelectedProjectId(createdProject.id) diff --git a/scripts/prepare_operator_real_data_samples.py b/scripts/prepare_operator_real_data_samples.py index 41cb951f..0ee10d57 100644 --- a/scripts/prepare_operator_real_data_samples.py +++ b/scripts/prepare_operator_real_data_samples.py @@ -64,18 +64,18 @@ class OperatorSample: SAMPLES: dict[str, OperatorSample] = { - "geel": OperatorSample( - slug="geel", - display_name="Geel center", - center_lon=4.991, - center_lat=51.162, - ), "mol": OperatorSample( slug="mol", display_name="Mol center", center_lon=5.1167, center_lat=51.1919, ), + "geel": OperatorSample( + slug="geel", + display_name="Geel center", + center_lon=4.991, + center_lat=51.162, + ), "turnhout": OperatorSample( slug="turnhout", display_name="Turnhout center", diff --git a/scripts/run_detection_quality_matrix.sh b/scripts/run_detection_quality_matrix.sh index 8b643373..227f9638 100644 --- a/scripts/run_detection_quality_matrix.sh +++ b/scripts/run_detection_quality_matrix.sh @@ -10,6 +10,7 @@ Usage: QUALITY_TILE_SIZES="512 640" \ QUALITY_TILE_OVERLAPS="64" \ QUALITY_THRESHOLDS="0.50 0.25 0.15" \ + QUALITY_SAMPLE_SLUG="mol" \ bash scripts/run_detection_quality_matrix.sh [base_url] or: @@ -25,6 +26,7 @@ Optional environment: 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_OUTPUT_DIR Output directory, default: artifacts/detection-quality-matrix/. + QUALITY_SAMPLE_SLUG Optional AOI slug included in persisted project names. REAL_IOU_THRESHOLD QA IoU threshold, default inherited by the underlying workflow. 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_OVERLAPS="${QUALITY_TILE_OVERLAPS:-64}" 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)}" 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 model_env="" 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} --" - 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_TILE_SIZE="${tile_size}" \ REAL_TILE_OVERLAP="${tile_overlap}" \ diff --git a/scripts/run_multi_sample_detection_quality_matrix.sh b/scripts/run_multi_sample_detection_quality_matrix.sh index 138b4150..d095e597 100644 --- a/scripts/run_multi_sample_detection_quality_matrix.sh +++ b/scripts/run_multi_sample_detection_quality_matrix.sh @@ -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} --" REAL_RASTER_PATH="${raster_path}" \ REAL_REFERENCE_VECTOR_PATH="${reference_path}" \ + QUALITY_SAMPLE_SLUG="${sample_slug}" \ QUALITY_OUTPUT_DIR="${sample_output_dir}" \ bash scripts/run_detection_quality_matrix.sh "${BASE_URL}" done < "${sample_manifest_tsv}"