Commit Graph
876 Commits
Author SHA1 Message Date
Jens a0884d64c9 feat(ui): refine governed workbench and dual-screen flows 2026-08-30 06:00:28 +02:00
Jens 80a2d1654d fix(platform): govern geospatial analysis and raster handoffs 2026-08-30 06:00:15 +02:00
Jens 96f90373dc feat(auth): harden Authentik and guest capability boundaries 2026-08-30 05:59:49 +02:00
Jens b93d926b94 chore(repo): retire duplicate nested source mirror 2026-08-30 05:59:32 +02:00
Jens d39816a4ca Merge pull request 'fix(ci): restore release gates and contract baseline (fixes #4)' (#6) from codex/resolve-open-issues into main
Unraid autoredeploy / Deploy geointel (push) Successful in 9m48s
GeoIntel release gates / Compile, test, contracts and builds (push) Failing after 2m24s
GeoIntel release gates / Python and npm vulnerability policy (push) Failing after 25s
GeoIntel release gates / GIS image, SBOM and container scan (push) Failing after 14s
Reviewed-on: #6
2026-08-29 04:07:37 +02:00
NuklearRabbit 38c7b5f85e fix(ci): use one required pull-request gate
Managed validation / full (pull_request) Successful in 11s
2026-08-29 03:35:31 +02:00
NuklearRabbit 029ed9c3f1 fix(ci): stop duplicate feature-branch runs
GeoIntel release gates / Compile, test, contracts and builds (pull_request) Waiting to run
GeoIntel release gates / Python and npm vulnerability policy (pull_request) Waiting to run
GeoIntel release gates / GIS image, SBOM and container scan (pull_request) Waiting to run
Managed validation / full (pull_request) Canceled after 0s
2026-08-29 03:27:53 +02:00
NuklearRabbit edb762219c fix(ci): restore release gates and contract baseline (fixes #4)
Managed validation / full (pull_request) Canceled after 0s
GeoIntel release gates / Compile, test, contracts and builds (pull_request) Canceled after 0s
GeoIntel release gates / Python and npm vulnerability policy (pull_request) Canceled after 0s
GeoIntel release gates / GIS image, SBOM and container scan (pull_request) Canceled after 0s
GeoIntel release gates / Python and npm vulnerability policy (push) Failing after 22s
GeoIntel release gates / Compile, test, contracts and builds (push) Waiting to run
GeoIntel release gates / GIS image, SBOM and container scan (push) Failing after 45s
2026-08-29 02:00:23 +02:00
Jens d39f703abf Merge pull request '[skip ci] Centralize Gitea Actions runner' (#3) from codex/centralize-gitea-runner into main 2026-08-27 23:50:01 +02:00
NuklearRabbit 5b47df2750 chore(actions): use central Unraid runner [skip ci] 2026-08-27 23:42:26 +02:00
Jens ec1fbd5b15 ci: align managed validation contract [skip ci] 2026-08-27 07:40:32 +02:00
Jens 4d916ee04c ci: add managed validation contract [skip ci] 2026-08-27 06:27:41 +02:00
NuklearRabbit fb1b776843 deploy: attach production container group labels
GeoIntel release gates / Compile, test, contracts and builds (push) Failing after 29s
GeoIntel release gates / Python and npm vulnerability policy (push) Failing after 39s
GeoIntel release gates / GIS image, SBOM and container scan (push) Failing after 1m28s
Unraid autoredeploy / Deploy geointel (push) Successful in 7m36s
2026-08-26 04:52:47 +02:00
NuklearRabbit fe51bfe3ae ci: add scoped Unraid autoredeploy
GeoIntel release gates / Compile, test, contracts and builds (push) Failing after 1m17s
GeoIntel release gates / Python and npm vulnerability policy (push) Failing after 30s
GeoIntel release gates / GIS image, SBOM and container scan (push) Failing after 1m29s
2026-08-26 02:49:50 +02:00
Jens 3627a05bfe Recover model provenance from verified backup receipt
GeoIntel release gates / Compile, test, contracts and builds (push) Failing after 29s
GeoIntel release gates / Python and npm vulnerability policy (push) Failing after 53s
GeoIntel release gates / GIS image, SBOM and container scan (push) Failing after 5m38s
2026-08-24 03:22:45 +02:00
Jens 36a3c84699 Fix governed runtime model registration
GeoIntel release gates / Compile, test, contracts and builds (push) Failing after 20s
GeoIntel release gates / Python and npm vulnerability policy (push) Failing after 24s
GeoIntel release gates / GIS image, SBOM and container scan (push) Failing after 2m43s
2026-08-23 23:31:42 +02:00
Jens 73228259a0 Merge runtime provenance script execution fix
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
2026-08-23 23:26:44 +02:00
Jens a37c6d5b17 Place runtime script lint exception correctly
GeoIntel release gates / Compile, test, contracts and builds (push) Failing after 25s
GeoIntel release gates / Python and npm vulnerability policy (push) Failing after 26s
GeoIntel release gates / GIS image, SBOM and container scan (push) Failing after 1m49s
2026-08-23 23:26:37 +02:00
Jens d944fa7276 Keep runtime provenance script lint-clean
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
2026-08-23 23:26:24 +02:00
Jens de8beae183 Make runtime provenance operator script directly executable
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
2026-08-23 23:26:06 +02:00
Jens 2f15da97c3 Merge governed model provenance migration
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
2026-08-23 23:22:12 +02:00
Jens be2e092b33 Recover governed runtime provenance for legacy YOLO models
GeoIntel release gates / Compile, test, contracts and builds (push) Failing after 20s
GeoIntel release gates / Python and npm vulnerability policy (push) Failing after 22s
GeoIntel release gates / GIS image, SBOM and container scan (push) Failing after 2m31s
2026-08-23 23:22:02 +02:00
Jens 300fbba5c9 Merge stable icon compatibility fix
GeoIntel release gates / Compile, test, contracts and builds (push) Failing after 33s
GeoIntel release gates / Python and npm vulnerability policy (push) Failing after 28s
GeoIntel release gates / GIS image, SBOM and container scan (push) Failing after 1m46s
2026-08-23 22:42:30 +02:00
Jens 730d8c8312 Restore stable GeoIntel icon endpoint
GeoIntel release gates / Compile, test, contracts and builds (push) Failing after 24s
GeoIntel release gates / Python and npm vulnerability policy (push) Failing after 31s
GeoIntel release gates / GIS image, SBOM and container scan (push) Failing after 1m50s
2026-08-23 22:42:21 +02:00
Jens 9e8a7a5883 Merge Tower release identity fix
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
2026-08-23 22:39:05 +02:00
Jens 19cd1054b2 Fix Tower release source and image identity
GeoIntel release gates / Compile, test, contracts and builds (push) Failing after 24s
GeoIntel release gates / Python and npm vulnerability policy (push) Failing after 30s
GeoIntel release gates / GIS image, SBOM and container scan (push) Failing after 2m7s
2026-08-23 22:38:42 +02:00
Jens 4268465fc3 Merge visual audit and GPU workflow upgrade
GeoIntel release gates / Compile, test, contracts and builds (push) Failing after 22s
GeoIntel release gates / Python and npm vulnerability policy (push) Failing after 19s
GeoIntel release gates / GIS image, SBOM and container scan (push) Failing after 1m40s
2026-08-23 21:50:31 +02:00
Jens b996986d20 Upgrade async GPU analysis and workbench UX 2026-08-23 21:50:11 +02:00
JensandClaude Opus 5 4040cbca7b Vertaal tien Engelse aria-labels
Deze worden voorgelezen aan gebruikers die met een schermlezer werken. In een
verder Nederlandse werkbank hoorden zij "Export artifact actions" en "Map
selection QA shortcut".

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-23 15:11:46 +02:00
JensandClaude Opus 5 596bbb0e8c Vertaal 56 Engelse foutteksten in de Nederlandse interface
Deze verschenen zodra de backend geen eigen melding meegaf: de terugvalteksten
van formatError en de controles vóór een handeling. Een operator kreeg dan
"Failed to load exports" of "Select a project first" te zien, midden in een
verder Nederlandse werkbank.

Dit is geen meertaligheidsvraagstuk maar inconsistentie binnen de taal die het
product al spreekt. De teksten volgen nu de bestaande toon: benoemen wat er
misging of wat er eerst nodig is, in actieve vorm, zonder excuus.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-23 15:10:07 +02:00
JensandClaude Opus 5 7bf7caf56b Werk de frontend-documentatie bij
De README noemde App.tsx nog de orchestrator; die rol ligt sinds de splitsing
bij WorkbenchApp.tsx. Verder twee secties toegevoegd over wat er veranderd is:
de werkstanden en het leidende designsysteem, en de laadstrategie met de
verzoekdeler en het laden per werkblad.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-23 15:07:55 +02:00
JensandClaude Opus 5 7a5f64e5ca Deel geen antwoorden meer over een identiteitswissel heen
De verzoekdeler uit de vorige commit houdt een antwoord driehonderd
milliseconde vast om de opstartcascade op te vangen. Uitloggen herlaadt de
pagina echter niet, dus die tabel overleeft een sessiewissel: binnen dat venster
zou de volgende gebruiker het antwoord van de vorige kunnen krijgen.

Met de hand is dat nauwelijks te bereiken, maar een verzoekcache die een
identiteitswissel overleeft is hoe dan ook fout. De tabel wordt nu gewist bij
uitloggen, bij aanmelden en zodra de sessie verloopt.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-23 15:04:45 +02:00
JensandClaude Opus 5 70157400d9 Breng de opstartkosten terug en geef de werkstand een schakelaar
De inlogpagina haalde 1,46 MB over de lijn, waaronder de volledige kaartmotor
van 213 kB terwijl er geen kaart staat. De werkbank is nu een aparte brok die
pas na aanmelden geladen wordt: de eerste JS-bundel gaat van 476 naar 30 kB, en
met de beeldwerk uit een eerdere commit komt de inlogpagina op 344 kB.

Bij het opstarten gingen 27 verzoeken de deur uit in drie golven. Alles werd
vooraf opgehaald, ook voor werkbladen die de gebruiker misschien nooit opent,
en twee daarvan werden dubbel opgevraagd door effecten die negentig
milliseconde na elkaar liepen. Gelijktijdige en vlak opeenvolgende identieke
GET-verzoeken delen nu één belofte, en elk werkblad haalt zijn eigen gegevens
pas op wanneer het geopend wordt. Nu zestien verzoeken.

Typen in het themazoekveld kostte 53 ms mediaan met uitschieters tot 127 ms,
omdat die filterwaarde in het viewmodel van de hele werkruimte leefde. Het veld
is een eigen component geworden dat zijn invoer zelf bijhoudt. React.memo om de
kaart is geprobeerd en weer verwijderd: van de negentien props worden er te
veel per render opnieuw gemaakt, dus de vergelijking sloeg nooit over.

Verder: donker blijft de standaard maar er is nu een weg terug naar licht, de
kaartondergrond schakelt mee zonder de datalagen te herbouwen, de onderste
hero-afbeelding wordt pas opgehaald als hij in beeld komt, en de werkbank heeft
een h1 zodat schermlezers een ankerpunt hebben.

De tests op de bootstrap legden het oude gedrag vast en toetsen nu het nieuwe,
van drie naar zeven gevallen.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-23 15:00:57 +02:00
JensandClaude Opus 5 663855f8a4 Geef het tweede scherm zijn opmaak en lettertypen
De analyseconsole opent in een eigen venster en stond daar in Times New Roman
op wit, met standaard browserknoppen, terwijl de werkbank ernaast donker was.
Niemand had er ooit naar gekeken.

Er was code die de opmaak moest meenemen, maar die kloonde de link naar het
nieuwe venster. Dat venster wordt geopend met window.open('') en staat dus op
about:blank; nagemeten leverde link.sheet === null op. De regels worden nu
ingeschreven in plaats van gelinkt, met de verwijzingen absoluut gemaakt tegen
de bron van het blad.

Daarna bleef document.fonts.status eeuwig op 'loading' staan. De bestanden
kwamen wel binnen met status 200, maar in een about:blank-document voltooit het
lettertypeladen niet. Een FontFace hangt niet aan een document, dus de
gezichten die het hoofdvenster al geladen heeft worden nu overgenomen en gaan
de @font-face-regels niet mee. Meetbaar: status 'loaded', en een schermafdruk
die op de lettertypen wacht is klaar in 45 ms in plaats van af te breken na
dertig seconden.

Dit verklaarde ook de vijftien !important in dit blok: die probeerden iets te
overschrijven dat er nooit aankwam. Alle zestien bleken overbodig en zijn in de
vorige commit verwijderd.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-23 15:00:30 +02:00
JensandClaude Opus 5 f567a459b0 Laat een kapot paneel geen kapotte applicatie meer zijn
Er was nergens een error boundary: geen componentDidCatch, geen
getDerivedStateFromError. Eén component die gooit nam de hele werkbank mee,
inclusief de kaart en de navigatie. Nu blijft de schil staan, ziet de gebruiker
welk werkblad het liet afweten, en wist wegnavigeren de fout.

Daarnaast een patroon dat op zeven plekken half was toegepast: een optionele
keten die te vroeg stopt. report?.items?.filter(...) stond geguard, twintig
regels verderop stond report.items.map(...) zonder. Half geguard is de
eigenlijke fout, want het wekt zekerheid zonder die te bieden. Waar de lijst op
meerdere plekken gebruikt wordt staat nu één normalisatie aan de kop.

De brondetails hadden geen enkele afhandeling en werden met .catch(() => null)
overgeslagen; die melden nu wel. De overige elf catch-blokken bleken bovenop
hooks te staan die de fout al zelf opvangen en tonen, wat ik heb nagemeten door
een verzoek te laten mislukken.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-23 15:00:08 +02:00
JensandClaude Opus 5 438d0ec083 Voer de visuele audit door: fundament, donkere werkstand, kaart en schil
Achttien bevindingen uit de audit, met de metingen erbij in de betreffende
CSS-secties.

Fundament. Dertien tokens werden op 87 plaatsen aangeroepen zonder ooit
gedefinieerd te zijn; in de browser gaf getPropertyValue('--gi-text') een lege
string terug, dus die kleurregels vielen terug op overerving. Vorm en hoogte
zijn terug naar de schaal: radius via token van 17% naar 81%, unieke schaduwen
van 115 naar 71, losse kleurwaarden van 928 naar 352.

Typografie. De stylesheets declareerden negen gewichten terwijl er per familie
twee gezichten geladen werden, en font-synthesis-weight: none verbiedt
namaakvet. Alles van 650 tot 850 kwam dus als 600 op het scherm: 138 van de 148
declaraties rendereden identiek. Nu vier stappen die alle vier ook geladen
worden, en Manrope draagt de koppen zoals op de landingspagina.

Donkere werkstand. De werkbank staat tegen luchtbeelden aan; een lichte schil
daarnaast laat de kaart altijd verliezen. De landingspagina blijft licht.

Kaart. De ondergrond wordt ontkleurd en gedempt zodat alleen de eigen data nog
kleur draagt, en de symbologie staat niet langer als losse hexwaarden in
GeoMap. De zes zwevende vlakken zijn er drie: statuskaart, staprail en
resultaatchip bleken al één component te zijn die de opmaak over drie hoeken
van de kaart uiteentrok.

Schil. Eén kopbalk in plaats van drie (215px chroom naar 100px), het raster van
AI-vragen zonder botsingen, kapitalen op één niveau in plaats van 57
declaraties, en het leverancierslogo uit de navigatiebalk.

Van 47 echte !important naar 1, gemeten door telkens twee productiebuilds op
berekende stijlen te vergelijken. Contrast: nul tekstelementen onder AA op zes
werkbladen, in beide werkstanden.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-23 14:59:45 +02:00
JensandClaude Opus 5 4ecec721b1 Haal 8,4 MB dode en oversized beelden uit de build
De inlogpagina haalde 1,46 MB over de lijn voordat iemand kon aanmelden.
Daarvan waren 885 kB drie hero-afbeeldingen; achtergronden in CSS kennen geen
loading="lazy", dus ook de twee onder de vouw laadden meteen.

Daarnaast stond er 8,1 MB aan ongebruikte PNG-originelen in public/portfolio/.
Alles in public/ wordt ongewijzigd naar dist/ gekopieerd, dus die gingen bij
elke build mee terwijl geen regel code ernaar verwees. Ze staan nu in
design-assets/, buiten wat de webserver uitserveert; de README daar legt uit
hoe je een webversie opnieuw maakt.

- de drie hero's opnieuw gecomprimeerd op hun werkelijke weergavegrootte
- de Noordzee-textuur ligt onder een vrijwel dekkende gradiënt: 240 -> 35 kB
- het ITWorx-woordmerk toonde op 118px maar was 512px breed: 40 -> 12 kB
- geointel-icon.png en de dark-case-study-cover werden nergens genoemd

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-23 14:58:43 +02:00
JensandClaude Opus 5 a8d50bba88 Voeg MCP-servers voor beeldgeneratie toe
Higgsfield en Google AI Studio, allebei via npx. De configuratie verwijst naar
omgevingsvariabelen in plaats van naar de sleutels zelf, zodat er niets geheims
in de repository terechtkomt. Zet HF_API_KEY, HF_SECRET en GEMINI_API_KEY in de
omgeving om ze te activeren.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-23 14:58:20 +02:00
JensandClaude Opus 5 5c509ca2a9 publish the accuracy that was measured
GeoIntel release gates / Compile, test, contracts and builds (push) Failing after 29s
GeoIntel release gates / Python and npm vulnerability policy (push) Failing after 22s
GeoIntel release gates / GIS image, SBOM and container scan (push) Failing after 2m57s
The recommended detection profile showed operators precision 0.614, recall
0.606 and F1 0.607. Those three numbers appear nowhere in this repository
except the file that publishes them and a test that pinned them as literal
strings. The only recorded evaluation of that model at that operating point —
tile 512, overlap 64, threshold 0.15, the exact key its promotion report
recommended — reported 0.590, 0.577 and 0.582. The published figures were about
two and a half points more flattering than anything that was measured, on the
profile labelled "aanbevolen", and the test made sure nobody would correct
them. They now carry the measured values.

Worse in kind: the conservative profile reported "gemeten achtergrondfouten 0".
Its nine-sample hard-negative matrix at threshold 0.35 recorded 198 background
detections with 55 in the worst sample. The one number that tells an operator
whether a high-precision model invents buildings on empty terrain said zero
where the evidence said 55.

Those zeros are not simply wrong everywhere, which is why the fix is not just a
number. The other two profiles genuinely produced zero — against a strict
pure-empty gate of three samples, a different and much weaker test than the
nine-sample hard-negative matrix. Printing 0, 0 and 55 side by side invites a
comparison the evidence does not support, so each profile now states its gate,
its background sample count and the evaluation behind its figures, and the
panel shows them.

A test refuses any published figure that does not appear in the evidence
record, with a negative control so it cannot pass by matching nothing. Pinning
the numbers as literal strings is what let an unsourced precision survive; that
assertion is gone.

Also ignoring .codex-artifacts/ — ~300 MB of the rejected SAM2 and
edge-alignment trials plus a deploy bundle. Kept on disk, out of the repository.
No credentials in it; the two token scripts generate from settings.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-23 00:51:53 +02:00
JensandClaude Opus 5 7351993fee stop paging when the provider stops making progress
An ArcGIS layer without supportsPagination accepts resultOffset and ignores it,
answering every page with the first one. The VHA profile reader advanced its
offset by the page length and stopped at the announced count, so for a count
that is a multiple of the page size it collected N copies of page one — and its
completeness check, len(features) == candidate_count, passed. Four announced
records became four stored records, two of them duplicates, filed under an
official provenance. That is the substitution bounded acquisition exists to
prevent, arriving through the front door.

The reader now refuses a record it already collected. It fails rather than
silently dropping the duplicate: a provider that cannot page is a provider
whose count proves nothing, so a smaller-but-clean result would still be
unverifiable. Its watercourse-name loop was worse — a bare `while True` that
ended only when the provider stopped setting exceededTransferLimit, with names
deduplicated by code so a stuck provider produced no visible change while the
requests continued. It now refuses a repeated page body, and both loops have
the page budget the sibling readers already had.

Those siblings turned out to be fine. GRB and official vector already refuse a
repeated page URL, bound the page count, and deduplicate on feature identity —
but none of it had a test, so none of it was known to work. Exercised now,
including the case where distinct URLs defeat the loop check and the budget is
the only backstop. A duplicate across two genuinely different pages is kept
once rather than failing, because a cursor over a changing table produces that
legitimately.

Also: _bash_path fell back to the raw path whenever wslpath failed, except on
timeout, which propagated and reddened the suite when starting WSL took more
than ten seconds under load.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-23 00:13:38 +02:00
JensandClaude Opus 5 7c052a339e keep a precise failure precise, and state one redirect policy
Two problems of the same shape: information about *why* something failed being
replaced by something vaguer.

get_dataset_geojson wrapped the JSON parse, the metadata read, the CRS
resolution and the canonicalisation in one try and reported all of it as
"Stored dataset is not valid JSON" with a 500. An operator whose dataset had an
unusable CRS was sent to inspect a file that parses perfectly well, and the
canonicaliser's own AppError — with its code and its status — never reached
them. Only the parse is now inside that handler; everything after it keeps the
error it raised, and a genuine bug becomes a distinct 500 rather than a
mislabelled client error. A guard finds the same shape elsewhere: catching
Exception around a call into another component and relabelling what it
reported. Wrapping one's own private helper stays legitimate and the guard
says so.

The redirect policy was split without anyone saying so. Two acquisition
services rejected every redirect through a hand-rolled opener, while eight
allowed a same-origin one through the shared guard — and only the latter
checked where the response came from. Both live in the guard now, and the
strict path uses the rejecting handler rather than the guard's after-the-fact
check: objecting to response.url means urllib already opened the connection and
read the body, which for a metadata endpoint is the whole attack. That was a
weakening I introduced in this same commit's first draft.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-22 23:35:59 +02:00
JensandClaude Opus 5 c6837ec1b2 split the map workspace into a view model and two views
MapWorkspace.tsx was 3.157 lines: a props interface, 1.200 lines of derived
state and handlers, and two complete render paths — the map-first explorer and
the advanced workbench behind it. It is now five modules, and the container is
nineteen lines that choose between the two.

The obstacle was the props signature. The explorer reads 97 derived values and
the workbench 40, so passing them individually would have produced a 97-field
interface — worse than the file it replaced. Extracting the derived state into
a hook that returns one object solves it: MapWorkspaceViewModel is
ReturnType<typeof useMapWorkspaceViewModel>, so the shape is derived from what
the hook actually produces and cannot drift from it. Each view then names two
typed objects, and the JSX moved unchanged.

The contract tests found the one place where widening a negative assertion is
wrong. "The map workspace performs no transport" was true of the old file and
false of the whole feature, because the hooks call the API by design. It is now
scoped to the presentational modules, which is what it always meant.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-22 22:38:25 +02:00
JensandClaude Opus 5 c4d873149b page the analysis run listings
Detection and segmentation run listings returned every run a project had ever
produced. Runs accumulate with every analysis while the panel only ever draws
the recent ones, so the response grew without bound for no benefit.

Both take limit and offset now and report total, limit, offset and truncated,
matching the result listings.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-22 22:18:08 +02:00
JensandClaude Opus 5 6572e4ad5f scope frontend contracts to the feature, not to one file
93 test files read a single frontend source and asserted identifiers in it. The
MapWorkspace split showed what that costs: 24 tests went red for a move that
changed no behaviour at all. A contract belongs to the feature — a container,
its hooks, its domain layer — not to whichever file currently holds it.

232 read sites now resolve through read_feature(). The distinction that makes
this safe is direction: a *positive* contract ("this is wired") may widen,
because the identifier must still exist somewhere in the feature; a *negative*
one ("this component performs no transport") is a statement about one file, and
widening it would quietly weaken the check. The 73 single-file reads that
remain are exactly those, and a guard now enforces the rule for new tests.

Verified rather than assumed: of the 732 migrated positive assertions, 644 still
match exactly one module — as specific as before — and the other 86 already
spanned a container and its hook by nature. Two apparent misses are an artefact
of the checking regex reading an escaped newline literally.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-22 22:05:43 +02:00
JensandClaude Opus 5 a2a8775df1 let segmentation honour its post-processing configuration
Segmentation reuses the detection suppressor but passed only the IoU threshold,
so it silently fell back to the hardcoded containment constant while detection
had just been given a configured one. Tuning containment for a promoted model
would have changed detection behaviour and left segmentation on the old value —
the same drift, one commit later.

Masks and boxes overlap differently, so segmentation carries its own setting
rather than borrowing the detector's, and records both thresholds on the run as
detection does.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-22 21:45:33 +02:00
JensandClaude Opus 5 c8d32a4801 serve a promoted model at the post-processing it was gated on
scripts/evaluate_belgium_building_candidate.py freezes its post-processing
before the protected test — NMS IoU and a containment threshold selected during
calibration, defaulting to 1.0. The runtime applied a hardcoded 0.85, so a
promoted candidate was served under suppression its evaluation never measured
and dropped detections the gate had counted. Neither report showed the
difference. That constant was mine, added without noticing the evaluation
pipeline already had a tuned value for the same concept.

Containment is now configuration, recorded on every run beside the duplicate
IoU threshold, so an operator can serve a candidate at the value it was gated
at and afterwards see which value produced a given score.

Two runs that post-processed differently produced different candidate sets from
the same model output, so the comparison endpoint refuses to rank them. Runs
recorded before those values were persisted carry none, and absence is not
treated as a difference.

The remaining gap is deliberate and documented rather than closed: the gate
scores the model on its protected test set, the workbench scores the whole
pipeline including coverage clipping and the tile-edge filter. A promoted
candidate will not reproduce its gate F1 exactly, and pretending otherwise
would be the worse answer.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-22 21:15:05 +02:00
JensandClaude Opus 5 1a1a9af6e7 give segmentation QA the same threshold-independent view as detection
Detection QA reports a precision/recall curve, average precision and a
calibration sweep; segmentation QA reported a single operating point. Both rank
their outputs by confidence, so the same view applies, and the asymmetry meant
the two panels answered different questions about comparable runs — an
inconsistency introduced when detection gained the curve.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-22 20:47:30 +02:00
JensandClaude Opus 5 ff4a15aa74 let an operator's adjudication reach the score
The review vocabulary already separates a model error from a reference gap,
because the product's position is that official footprints are not
automatically perfect ground truth. Those verdicts were only counted. An
operator who inspected forty false positives and established that twelve are
buildings the reference simply lacks still saw a precision counting all forty
against the model — a number they had personally disproved, on the panel where
they disproved it.

Applying the verdicts gives an adjudicated score reported next to the raw one,
so nothing is quietly improved. Not being able to judge is not evidence in the
model's favour, so uncertain and obscured verdicts keep counting, as does a
decision from a later release that this runtime does not recognise.

Because part of the evidence is usually still unreviewed, the honest form is an
interval rather than a single corrected number: pessimistic assumes every
unreviewed finding is a model error, optimistic assumes none is, and the
headline equals the pessimistic reading so a partly reviewed check never
presents as a settled one.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-22 20:43:22 +02:00
JensandClaude Opus 5 f6eced1b94 rank model variants on average precision, and say when they are not comparable
The workbench ranks model variants by a stored F1, each measured at that
variant's own confidence threshold. A conservatively calibrated detector then
looks worse than a liberal one without detecting anything differently: the
number says as much about the threshold as about the model. POST
/detection/runs/compare ranks on average precision instead, which describes the
whole ranking a model produced, and keeps each run's own-threshold F1 visible
next to it so the difference between the two readings is auditable.

Comparability comes before the ranking. Runs over different source rasters,
scored against different references, without a proven inference footprint, or
covering a different evaluated population are not alternatives to one another,
and no metric makes them so. The report names which of those applies and still
returns the numbers — they are simply not a ranking.

Each run is scored through the same QA path the workbench uses, so a comparison
and the persisted quality checks cannot drift apart.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-22 19:47:14 +02:00
JensandClaude Opus 5 2cd2c49389 calibrate a confidence threshold from one inference pass
Threshold calibration ran the model over every tile once per threshold — three
GPU passes to compare 0.50, 0.25 and 0.15 on a hundred-tile raster. The answer
is already in a single run at the lowest value: detections above a higher cut
are a subset of it, and duplicate suppression walks candidates in descending
confidence, so a lower-confidence box can never displace a higher-confidence
one. The kept set above any cut is identical whichever threshold the run used,
which is what makes one pass sufficient rather than merely cheaper.

QA now takes calibration_thresholds and reads each operating point off the same
precision/recall walk it already performs, marking the F1-optimal cut. The lab
runs inference once and fills its table from the sweep.

The contract test asserted the per-threshold loop by name, pinning the waste it
was meant to describe. It now states what calibration owes an operator: a row
per requested threshold, from one run.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-22 19:37:19 +02:00