From d97a677a7e3e536931441e4bca4e1fbe74c3fc25 Mon Sep 17 00:00:00 2001 From: Klaas Schoute Date: Mon, 14 Sep 2026 13:54:32 +0200 Subject: [PATCH 1/3] docs: specify R2 producer handoff and runtime --- docs/development/data-foundation-stack.md | 10 +++++----- docs/development/data-import-contract.md | 8 ++++++++ 2 files changed, 13 insertions(+), 5 deletions(-) diff --git a/docs/development/data-foundation-stack.md b/docs/development/data-foundation-stack.md index ef19bbb1..5127ab35 100644 --- a/docs/development/data-foundation-stack.md +++ b/docs/development/data-foundation-stack.md @@ -1,6 +1,6 @@ # Techstack voor de eerste importketen -Status: eenvoudige uitvoeringskeuzes bij [#1214](https://github.com/NIPKaart/core/issues/1214). Zie de [leveringsafspraak](data-import-contract.md) en [werkpakketten](data-foundation-delivery.md). Er is nog geen live producer/core-keten of bucket ingericht. +Status: eenvoudige uitvoeringskeuzes bij [#1214](https://github.com/NIPKaart/core/issues/1214). Zie de [leveringsafspraak](data-import-contract.md) en [werkpakketten](data-foundation-delivery.md). De live Amsterdam-export en de handmatig beoordeelde core-import zijn op 14 september 2026 geaccepteerd; disabled-parking #783 en core #1222 zijn gemerged. De private bucket en automatische uitvoering zijn nog niet ingericht. | Onderdeel | Keuze voor de eerste stap | | --- | --- | @@ -10,13 +10,13 @@ Status: eenvoudige uitvoeringskeuzes bij [#1214](https://github.com/NIPKaart/cor | Dependencybeheer | uv en `uv.lock`; installatie met `uv sync --locked`, daarna `uv run ...`. Universele packages houden hun eigen tooling. | | Mapping | Kleine Python-dataclass en expliciete mapping voor één bron. | | Bestand | Eén begrensd UTF-8 JSON-document, geschreven via tijdelijk bestand en atomische vervanging. | -| Validatie | Expliciete typen en inhoudscontroles aan beide kanten. Opis uit #1222 is beschikbaar als een klein gedeeld schema nuttig blijkt; geen schemarelease-infrastructuur vereist. | +| Validatie | Expliciete typen en inhoudscontroles aan beide kanten. De Opis/schema-proef is in #1222 verwijderd; één intakeservice verwerkt het pilotformaat. | | Tests | Bestaande Python unittest/pre-commit-checks en core Pest. Kleine mappingvoorbeelden; geen gemeentelijke netwerken in gewone CI. | | Eerste overdracht | Lokaal bestand naar dezelfde core-intakeservice die later bucketbestanden verwerkt. | -| Automatische overdracht | Private bucket; R2 is kandidaat. Provider, regio, kosten, volledigheidsmechanisme en scoped rechten in #1217. | -| Uitvoering later | Eén geplande eindige producentuitvoering en core scheduler/queue. Hosting en ophaalritme pas kiezen bij #775/#1217. | +| Automatische overdracht | Private Cloudflare R2-bucket. Regio, kosten, toegangsrechten en stagingvalidatie in #1217; er zijn nog geen resources ingericht. | +| Uitvoering later | Eén permanent draaiende Docker-container per producerrepo op een eigen Linux-host of cloud-VM. Een interne scheduler start eindige opdrachten achter elkaar; geen container per gemeente. Core gebruikt zijn eigen scheduler/queue. Uitwerking in #775/#1217. | -De uv-migratie en SQL-runtimeverwijdering zijn gemerged in disabled-parking #779 en #780. #781 bevat één klein Hamburg-voorbeeld; dat is nog geen live aansluiting. De tijdelijke export wordt in #774 vervangen door de afgesproken pilotroute. Core bevat geen Python-code of bronclients. +De uv-migratie en SQL-runtimeverwijdering zijn gemerged in disabled-parking #779 en #780. De tijdelijke Hamburg-export uit #781 is door de live Amsterdam-route in #783 vervangen. Core #1222 levert de bijbehorende beoordeling en publicatie. Core bevat geen Python-code of bronclients. Gebruik bestaande standaardbibliotheken en dependencies waar die voldoen. Geen nieuwe broker, workerframework, lokale SQLite-planningsdatabase, verplichte sequences, JSONL of apart manifest voor de eerste bron. Voeg een component pas toe wanneer de werkende keten die aantoonbaar nodig heeft. diff --git a/docs/development/data-import-contract.md b/docs/development/data-import-contract.md index 7ae405bf..b04f88af 100644 --- a/docs/development/data-import-contract.md +++ b/docs/development/data-import-contract.md @@ -91,3 +91,11 @@ Bronclaims staan ongewijzigd in `source_record`. `geometry_derivation` bewaart b `municipal_imports.before_values` bewaart voor gewijzigde records de vorige databasewaarden en voor nieuw aangemaakte records `null`. De ontvangen levering en beoordeling blijven bewaard. Dit ondersteunt een onderbouwde herstelbeslissing; er is geen automatische terugzetknop die latere handmatige correcties kan overschrijven. Bij een databasefout blijft de levering te beoordelen en kan dezelfde publicatie opnieuw worden geprobeerd. De reviewpagina vergelijkt altijd met de huidige gegevens, ook na publicatie; historische verschillen zijn niet hetzelfde als deze actuele vergelijking. Een migratierollback verwijdert de importaudit en bronkoppeling. Maak eerst een databaseback-up en beoordeel herstel op gegevensniveau; een code-rollback is geen gegevensherstel. Onbekende capaciteit blijft nullable, ook na rollback. Productieactivering, legacyoverdracht, bucketcredentials en planning vallen buiten deze stap. + +## Automatische R2-overdracht (#775 / #1216) + +De producer uploadt hetzelfde bestand met één `PutObject` naar `municipal/nl-amsterdam-parkeervakken-e6a/.json` in een private Cloudflare R2-bucket. `If-None-Match: *` voorkomt overschrijven door deze uploader. `Content-MD5` controleert transportintegriteit; metadata `sha256` bevat de SHA-256 van de exacte bytes. Bij een bestaande objectnaam vergelijkt de producer de volledige remote inhoud met het bewaarde pending bestand. Alleen identieke bytes tellen als een geslaagde herhaling. Geen multipart-upload of los manifest. Zie de [ondersteunde R2-operaties](https://developers.cloudflare.com/r2/api/s3/api/). + +De nog te implementeren consumer in core #1216 ontdekt complete objecten binnen de toegestane prefix, begrenst downloads op 32 MiB en controleert de SHA-256, bestandsinhoud en overeenkomst tussen objectnaam, dataset en delivery-ID vóór dezelfde intake. Metadata is geen zelfstandige vertrouwensbron. Het bestaande intakebeleid voor conflicten, herhaling, volgorde en beoordeling blijft gelden; ontvangst publiceert niets automatisch. + +Eén permanent draaiende producercontainer op een eigen Linux-host voert eindige opdrachten achter elkaar uit. De standaardwachttijd na succes is 24 uur; na fouten maximaal vijf minuten. Een volume bewaart `pending.json` totdat de upload is bevestigd en bewaart daarna `last.json`. Dezelfde bytes worden na herstart opnieuw aangeboden voordat nieuwe gegevens worden opgehaald. De definitieve regio, bewaartermijn langer dan de toegestane core-uitval, budget en daadwerkelijk geteste toegangsrechten blijven onderdeel van #1217. De implementatie en lokale tests vervangen geen herstelproef tegen echte staging-R2 en core. From c2fc33433f4ba34ad40164bf599d4c3a3202b923 Mon Sep 17 00:00:00 2001 From: Klaas Schoute Date: Mon, 14 Sep 2026 15:17:06 +0200 Subject: [PATCH 2/3] docs: write import contract and technology stack in English --- docs/development/data-foundation-stack.md | 36 +++--- docs/development/data-import-contract.md | 134 +++++++++++----------- 2 files changed, 85 insertions(+), 85 deletions(-) diff --git a/docs/development/data-foundation-stack.md b/docs/development/data-foundation-stack.md index 5127ab35..469e5166 100644 --- a/docs/development/data-foundation-stack.md +++ b/docs/development/data-foundation-stack.md @@ -1,25 +1,25 @@ -# Techstack voor de eerste importketen +# Technology stack for the first import pipeline -Status: eenvoudige uitvoeringskeuzes bij [#1214](https://github.com/NIPKaart/core/issues/1214). Zie de [leveringsafspraak](data-import-contract.md) en [werkpakketten](data-foundation-delivery.md). De live Amsterdam-export en de handmatig beoordeelde core-import zijn op 14 september 2026 geaccepteerd; disabled-parking #783 en core #1222 zijn gemerged. De private bucket en automatische uitvoering zijn nog niet ingericht. +Status: implementation choices for [#1214](https://github.com/NIPKaart/core/issues/1214). See the [delivery contract](data-import-contract.md) and [work packages](data-foundation-delivery.md). The live Amsterdam export and manually reviewed core import were accepted on 14 September 2026; disabled-parking #783 and core #1222 are merged. The private bucket and automatic execution have not been provisioned. -| Onderdeel | Keuze voor de eerste stap | +| Component | Initial choice | | --- | --- | -| Core | Bestaande Laravel 13-applicatie, PHP 8.4 baseline en PostgreSQL/PostGIS. | -| Beheer | Bestaande React/Inertia-interface voor vergelijken en beoordelen; geen afzonderlijke importapp. | -| Producer | `disabled-parking`, bestaande Python >=3.11-omgeving en universele bronpackage. Bronpackageversies worden bij de noodzakelijke fix gecontroleerd. | -| Dependencybeheer | uv en `uv.lock`; installatie met `uv sync --locked`, daarna `uv run ...`. Universele packages houden hun eigen tooling. | -| Mapping | Kleine Python-dataclass en expliciete mapping voor één bron. | -| Bestand | Eén begrensd UTF-8 JSON-document, geschreven via tijdelijk bestand en atomische vervanging. | -| Validatie | Expliciete typen en inhoudscontroles aan beide kanten. De Opis/schema-proef is in #1222 verwijderd; één intakeservice verwerkt het pilotformaat. | -| Tests | Bestaande Python unittest/pre-commit-checks en core Pest. Kleine mappingvoorbeelden; geen gemeentelijke netwerken in gewone CI. | -| Eerste overdracht | Lokaal bestand naar dezelfde core-intakeservice die later bucketbestanden verwerkt. | -| Automatische overdracht | Private Cloudflare R2-bucket. Regio, kosten, toegangsrechten en stagingvalidatie in #1217; er zijn nog geen resources ingericht. | -| Uitvoering later | Eén permanent draaiende Docker-container per producerrepo op een eigen Linux-host of cloud-VM. Een interne scheduler start eindige opdrachten achter elkaar; geen container per gemeente. Core gebruikt zijn eigen scheduler/queue. Uitwerking in #775/#1217. | +| Core | Existing Laravel 13 application, PHP 8.4 baseline and PostgreSQL/PostGIS. | +| Administration | Existing React/Inertia interface for comparison and review; no separate import application. | +| Producer | `disabled-parking`, its existing Python >=3.12 environment and reusable source package. Check source package versions when a required fix is made. | +| Dependency management | uv and `uv.lock`; install with `uv sync --locked`, then use `uv run ...`. Reusable packages retain their own tooling. | +| Mapping | Small Python dataclass and explicit mapping for one source. | +| File | One bounded UTF-8 JSON document, written through a temporary file and atomic replacement. | +| Validation | Explicit type and content checks on both sides. The Opis/schema prototype was removed in #1222; one intake service handles the pilot format. | +| Tests | Existing Python unittest/pre-commit checks and core Pest. Small mapping examples; no municipal network calls in regular CI. | +| Initial handoff | Local file passed to the same core intake service that will later process bucket objects. | +| Automatic handoff | Private Cloudflare R2 bucket. Region, costs, access rights and staging validation belong to #1217; no resources have been provisioned. | +| Scheduled execution | One persistent Docker container per producer repository on an owner-managed Linux host or cloud VM. An internal scheduler runs finite commands serially; no container per municipality. Core uses its own scheduler/queue. Implementation in #775/#1217. | -De uv-migratie en SQL-runtimeverwijdering zijn gemerged in disabled-parking #779 en #780. De tijdelijke Hamburg-export uit #781 is door de live Amsterdam-route in #783 vervangen. Core #1222 levert de bijbehorende beoordeling en publicatie. Core bevat geen Python-code of bronclients. +The uv migration and SQL runtime removal were merged in disabled-parking #779 and #780. The temporary Hamburg export from #781 was replaced by the live Amsterdam route in #783. Core #1222 provides the corresponding review and publication flow. Core contains no Python code or source clients. -Gebruik bestaande standaardbibliotheken en dependencies waar die voldoen. Geen nieuwe broker, workerframework, lokale SQLite-planningsdatabase, verplichte sequences, JSONL of apart manifest voor de eerste bron. Voeg een component pas toe wanneer de werkende keten die aantoonbaar nodig heeft. +Use standard libraries and existing dependencies where they suffice. The first source needs no new broker, worker framework, local SQLite scheduling database, mandatory sequences, JSONL or separate manifest. Add a component only when the working pipeline demonstrates a need for it. -De producent krijgt uitsluitend de noodzakelijke rechten op zijn eigen private bucketlocatie. Core krijgt aparte leesrechten en beslist over publicatie. Geen databasecredentials of coretoken in de producer. Het overdrachtsmechanisme moet complete, unieke leveringen garanderen; daarvoor is niet vooraf één bepaalde S3-versioningimplementatie voorgeschreven. +The producer should receive only the necessary permissions for its private bucket destination. Core receives separate read access and decides publication. The producer holds no database credentials or core token. The handoff must guarantee complete, unique deliveries without prescribing a particular S3 versioning implementation upfront. Actual credential boundaries must be verified in #1217. -Eerst handmatig de hele keten bewijzen, daarna dezelfde bestanden automatisch overdragen. Bij groei meten we looptijd, bronlimieten, bestandsomvang en beheerwerk voordat we extra processen of gedeelde frameworks toevoegen. Offstreet volgt met eigen inhoudelijke afspraken; algemene garagebezetting bewijst geen beschikbare gehandicaptenparkeerplaats. +First prove the entire pipeline manually, then automate delivery of the same files. As usage grows, measure runtime, source limits, file size and maintenance effort before adding processes or shared frameworks. Offstreet follows with its own data semantics; general garage occupancy does not establish the availability of an accessible parking space. diff --git a/docs/development/data-import-contract.md b/docs/development/data-import-contract.md index b04f88af..601178c2 100644 --- a/docs/development/data-import-contract.md +++ b/docs/development/data-import-contract.md @@ -1,101 +1,101 @@ -# Voorlopige gegevenslevering +# Provisional data delivery contract -Status: werkafspraak voor [#1214](https://github.com/NIPKaart/core/issues/1214). Eén JSON-bestand voor de eerste handmatige import; het formaat wordt pas vastgezet nadat de producer en core samen zijn beproefd. De schema's, validator, Opis-dependency en fixturecorpus uit [PR #1222](https://github.com/NIPKaart/core/pull/1222) zijn vervangen door één daadwerkelijke intake in #1215. Tien ongeldige Amsterdamse polygonen krijgen in core een expliciet te beoordelen geometrie-afleiding. Het formaat is nog geen geaccepteerde productieaansluiting. +Status: working agreement for [#1214](https://github.com/NIPKaart/core/issues/1214). One JSON file supports the initial manual import; the format is finalized only after the producer and core have been tested together. The schemas, validator, Opis dependency and fixture corpus from [PR #1222](https://github.com/NIPKaart/core/pull/1222) were replaced by a single working intake in #1215. Ten invalid Amsterdam polygons receive a geometry derivation in core that requires explicit review. The format is not yet an accepted production integration. -## Van bron naar gebruiker +## From source to user -`gemeentelijke API → universele Python-package → disabled-parking → JSON-bestand → core: valideren, vergelijken en beoordelen → gemeentelijke parkeergegevens → publieke discovery` +`municipal API → reusable Python package → disabled-parking → JSON file → core: validate, compare and review → municipal parking data → public discovery` -De eerste overdracht gebeurt lokaal. Daarna uploadt de producent hetzelfde formaat naar een private bucket en ontdekt core de complete bestanden. De bucket hoort bij de automatische architectuur. R2 is een kandidaat; provider en overdrachtsmechanisme worden in #1217 gekozen. Er komt geen directe producerverbinding met de core-API of database. +The first handoff uses a local file. The producer then uploads the same format to a private bucket, where core discovers complete files. The bucket is part of the automated architecture. Cloudflare R2 has been selected; provisioning and operational acceptance belong to #1217. The producer never connects directly to the core API or database. -| Onderdeel | Verantwoordelijkheid | +| Component | Responsibility | | --- | --- | -| Universele package | Bronprotocol, volledige ophaling van een selectie, bron-ID's, oorspronkelijke waarden en volledigheidsinformatie. Zelfstandig bruikbaar zonder NIPKaart. | -| disabled-parking | Datasetselectie, vertaling, leveringsmetadata en bestand schrijven. Python en bronspecifieke tests staan hier of upstream. | -| core | Toegelaten dataset, bestandsvalidatie, verschillen, beoordeling, publicatie en behoud van correcties. Geen Python-omgeving. | -| offstreet-parking | Later een eigen producent voor voorzieningen; pas bij werkelijk gedeelde behoeften uitvoeringscode delen. | +| Reusable package | Source protocol, complete retrieval of a selection, source IDs, original values and completeness evidence. Independently usable without NIPKaart. | +| disabled-parking | Dataset selection, mapping, delivery metadata and file output. Python and source-specific tests belong here or upstream. | +| core | Allowed datasets, file validation, differences, review, publication and preservation of corrections. No Python environment. | +| offstreet-parking | A separate producer for facilities later; share execution code only when actual common needs emerge. | -## Eén bestand +## One file -UTF-8 JSON met één object en een `records`-array. Geen apart manifest, JSONL, schemarelease of opslagprovider-ID voor de lokale pilot. De concrete bronrij en mapping staan in de [pilotbeschrijving](data-import-pilot.md). +UTF-8 JSON containing one object and a `records` array. No separate manifest, JSONL, schema release or storage-provider ID for the local pilot. The concrete source record and mapping are documented in the [pilot description](data-import-pilot.md). -| Veld | Betekenis | +| Field | Meaning | | --- | --- | -| `format` | `nipkaart-municipal-pilot-1`; één voorlopige revisie voor producer en consumer. Vervangt `municipal-records-draft`, geen compatibiliteitslaag. | -| `dataset` | Vaste, door core toegelaten datasetcode. Bronhouder, licentie en geografische mapping horen bij deze aansluiting. | -| `delivery_id` | UUID, één keer gemaakt per geslaagde ophaling. Een retry van hetzelfde bestand behoudt ID en bytes. | -| `retrieved_at` | UTC-tijdstip waarop de ophaling begon, RFC 3339 met `Z`; sorteert leveringen. Geen waarnemingsdatum. Eén actieve ophaling per dataset. | -| `selection` | Vaste code voor de afgesproken collectie/filter, bijvoorbeeld `all`. Een scopewijziging vereist eerst een nieuwe beoordeelde aansluiting of selectie. | -| `complete` | Moet `true` zijn voor intake. Producent verklaart dit alleen op basis van bronbewijs. | -| `source_count` | Aantal dat de bron voor deze selectie meldt; gelijk aan aantal ontvangen unieke records. Een totaal alleen bewijst geen volledigheid wanneer nog een volgende pagina bestaat. | -| `records` | Alle records van de selectie, zonder stilzwijgend afgekeurde of overgeslagen bronrijen. | +| `format` | `nipkaart-municipal-pilot-1`; one provisional revision shared by producer and consumer. Replaces `municipal-records-draft` without a compatibility layer. | +| `dataset` | Fixed dataset code allowed by core. The source owner, license and geographical mapping belong to this integration. | +| `delivery_id` | UUID created once per successful retrieval. Retrying the same file preserves its ID and bytes. | +| `retrieved_at` | UTC retrieval start, RFC 3339 with `Z`; orders deliveries. Not an observation date. One active retrieval per dataset. | +| `selection` | Fixed code for the agreed collection/filter, such as `all`. A scope change first requires a newly reviewed integration or selection. | +| `complete` | Must be `true` for intake. The producer declares this only on the basis of source evidence. | +| `source_count` | Count reported by the source for this selection; equal to the number of unique received records. A total alone does not prove completeness when another page remains. | +| `records` | Every record in the selection, without silently rejected or skipped source rows. | -De producent controleert bronpagina's, aantallen en unieke ID's voordat het bestand wordt geschreven. Opvangen van parsefouten en doorgaan met de overige records is geen complete levering. Als een bron geen totaal aanbiedt, wordt eerst een andere aantoonbare volledigheidscontrole afgesproken; verzin geen `source_count` uit alleen de ontvangen lijst. +The producer checks source pages, counts and unique IDs before writing the file. Catching parsing failures and continuing with the remaining records does not produce a complete delivery. If a source provides no total, agree another demonstrable completeness check first; do not invent `source_count` from the received list alone. -Core accepteert alleen het bekende formaat en de toegelaten dataset/selectie; valideert typen en inhoud en weigert dubbele JSON-sleutels en bron-ID's. Geen remote schemaresolutie of door het bestand aangeleverde download-URL uitvoeren. Begin met de bestaande grenzen van 10.000 records en 32 MiB; de Amsterdamse bronproef van ongeveer 1,33 MB valt daar ruim binnen. Valideer vóór databasepublicatie. +Core accepts only the known format and allowed dataset/selection, validates types and content, and rejects duplicate JSON keys and source IDs. Do not resolve remote schemas or follow download URLs supplied by the file. Start with the existing limits of 10,000 records and 32 MiB; the Amsterdam source sample of approximately 1.33 MB fits comfortably. Validate before database publication. -## Een record +## A record -| Veld | Regel | +| Field | Rule | | --- | --- | -| `external_id` | Niet-lege oorspronkelijke ID als string. Identiteit is `(dataset, external_id)`; behoud volledige ID en voorloopnullen. Geen coördinatenhash of interne core-ID. | -| `geometry` | Oorspronkelijke GeoJSON `Polygon` in WGS84 voor Amsterdam, met `[longitude, latitude]`. Behoud ringen; begrens omvang en valideer bereik en geometrie. Geen verzonnen bronpunt. | -| `number` | Niet-negatief geheel aantal of `null`; voor Amsterdam een bronschatting, geen geverifieerde telling. Nul en onbekend blijven verschillend; maak van een bronaggregaat geen verzonnen losse bays. | -| `street` | Bronadres of `null`; een nabijheidsadres is geen exact parkeeradres. | -| `access_category` | Voor deze pilot uitsluitend `general`; een andere waarde blokkeert de levering. `general` betekent niet persoonsgebonden gehandicaptenparkeren, geen beschikbaarheid of parkeren zonder vergunning. | -| `source_attributes` | Voor Amsterdam: `regimes`, `orientation` en `version_date`. Alle regimes met hun tijden/dagen/datums/opmerkingen behouden; geen generiek regelsysteem of uitspraak “nu beschikbaar”. | -| `source_updated_at` | Voor deze Amsterdam-pilot verplicht aanwezig en uitsluitend `null`: de betekenis als wijziging van het bronrecord is niet aangetoond. Ook een lege string of array wordt geweigerd. Ondersteuning voor een echte bronwijzigingsdatum vereist eerst een beoordeelde bronmapping en bijbehorende contract- en validatorwijziging; portaalverwerking is geen veldcontrole. | +| `external_id` | Non-empty original ID as a string. Identity is `(dataset, external_id)`; preserve full IDs and leading zeros. No coordinate hash or internal core ID. | +| `geometry` | Original GeoJSON `Polygon` in WGS84 for Amsterdam, using `[longitude, latitude]`. Preserve rings, bound size and validate coordinate ranges and geometry. Do not invent a source point. | +| `number` | Non-negative integer or `null`; an estimate from the Amsterdam source, not a verified count. Zero and unknown remain distinct; do not turn a source aggregate into invented individual bays. | +| `street` | Source address or `null`; a nearby address is not an exact parking address. | +| `access_category` | Only `general` for this pilot; any other value blocks the delivery. This means accessible parking without a personal reservation, not availability or permit-free parking. | +| `source_attributes` | For Amsterdam: `regimes`, `orientation` and `version_date`. Preserve every regime with its times/days/dates/comments; no generic rules engine or claim of current availability. | +| `source_updated_at` | Required and strictly `null` for this Amsterdam pilot: its meaning as a source-record modification timestamp has not been established. Empty strings and arrays are also rejected. Supporting a genuine source modification timestamp first requires a reviewed source mapping and corresponding contract and validator changes; portal processing is not a field inspection. | -Core bewaart de oorspronkelijke brongeometrie bij de bronclaim en gebruikt PostGIS `ST_PointOnSurface` voor de marker op de publieke kaart. De publieke kaart toont alleen die marker, geen parkeervlak. Een ongeldige polygoon krijgt waar mogelijk een afzonderlijke afleiding met `ST_MakeValid(geometry, 'method=linework')`; alleen een geldige, niet-lege Polygon of MultiPolygon is bruikbaar. Geometrieën die uiteenvallen in lijnen/punten of gemengde collecties worden geweigerd, zonder delen weg te filteren. Het kaartpunt wordt uit het bruikbare vlak berekend. Dit punt ligt binnen het bruikbare parkeervlak en is geen ingang of individueel vak; het hoeft niet het geometrische middelpunt te zijn. Core schrijft de bestaande latitude/longitude-kolommen; PostgreSQL blijft de bestaande `location` afleiden. Zo zijn geen extra Python-geometriepackage of twee concurrerende afleidingen nodig. Zie [PostGIS PointOnSurface](https://postgis.net/docs/ST_PointOnSurface.html), [MakeValid](https://postgis.net/docs/ST_MakeValid.html) en de [bestaande opslagafspraak](postgresql.md#spatial-representation). +Core preserves the original source geometry with the source claim and uses PostGIS `ST_PointOnSurface` for the public map marker. The public map displays only that marker, not the parking polygon. Where possible, an invalid polygon receives a separate derivation through `ST_MakeValid(geometry, 'method=linework')`; only a valid, non-empty Polygon or MultiPolygon is usable. Geometries that collapse into lines/points or mixed collections are rejected without filtering out components. The map point is calculated from the usable area. It lies within that area and represents neither an entrance nor an individual bay; it need not be the geometric centroid. Core writes the existing latitude/longitude columns; PostgreSQL continues deriving the existing `location`. No additional Python geometry package or competing derivation is needed. See [PostGIS PointOnSurface](https://postgis.net/docs/ST_PointOnSurface.html), [MakeValid](https://postgis.net/docs/ST_MakeValid.html) and the [existing storage agreement](postgresql.md#spatial-representation). -Land en administratieve relaties worden door core uit de toegelaten datasetconfiguratie gekoppeld. Geen Nederlandse verplichte codes voor Europese bronnen en geen interne foreign keys in het bestand. Deze pilot ondersteunt alleen de aangetroffen Polygon-geometrie. Puntbronnen of andere geometrieën krijgen pas ondersteuning wanneer ze worden aangesloten. Nuttige broninformatie wordt daarbij nooit stilzwijgend weggegooid. +Core associates country and administrative relationships using the allowed dataset configuration. Do not require Dutch codes for European sources or include internal foreign keys in the file. This pilot supports only the observed Polygon geometry. Point sources and other geometries receive support when they are integrated. Useful source information must never be silently discarded. -## Herhaling en wijzigingen +## Repeated deliveries and changes -| Geval | Gedrag | +| Case | Behavior | | --- | --- | -| Eerste complete levering | Valideren, verschillen en kaartsteekproef tonen; publicatie na beoordeling. | -| Zelfde dataset en delivery-ID, dezelfde bytes | Bestaande importstatus teruggeven; geen tweede verwerking of publicatie. Core bewaart de SHA-256 van de ontvangen bytes. | -| Zelfde delivery-ID, andere bytes | Conflict afwijzen; nooit een bestaande levering vervangen. | -| Nieuwe levering, ongewijzigde records | Geen dubbele plekken of inhoudsrevisies; wel nieuwe ontvangst vastleggen. | -| Oudere `retrieved_at` dan de laatst geaccepteerde levering | Geen actuele gegevens overschrijven. Gelijke tijd met verschillende delivery-ID's is een conflict, geen willekeurige winnaar. | -| Een veld wijzigt bij dezelfde bron-ID | Nieuwe bronwaarde tonen voor beoordeling; identiteit, favorieten en detailverwijzingen behouden. | -| Een record ontbreekt in een complete selectie | Markeren als mogelijk verdwenen en beoordelen; geen automatische verwijdering. Terugkeer gebruikt dezelfde identiteit. | -| Lege/incomplete/ongeldige levering of mislukte fetch | Geen publicatie; bestaande gegevens en laatste geldige export blijven behouden. | -| Bronwaarde botst met geaccepteerde correctie | Bronwaarde in de ontvangen levering bewaren; correctie behouden en publicatie van de levering blokkeren bij een botsing. Ook in de eerste importimplementatie. | -| Onbekende toegang of gewijzigde scope | Geen automatische algemene publicatie of vergelijking van ontbrekende records; eerst beoordelen. | +| First complete delivery | Validate, show differences and a map sample; publish after review. | +| Same dataset and delivery ID, identical bytes | Return the existing import status; no second processing or publication. Core stores the SHA-256 of received bytes. | +| Same delivery ID, different bytes | Reject the conflict; never replace an existing delivery. | +| New delivery, unchanged records | No duplicate places or content revisions; record the new receipt. | +| `retrieved_at` older than the last accepted delivery | Do not overwrite current data. Equal timestamps with different delivery IDs are a conflict, not an arbitrary winner. | +| A field changes for the same source ID | Present the new source value for review; preserve identity, favorites and detail references. | +| A record is missing from a complete selection | Flag it as potentially removed and review it; no automatic deletion. A returning record keeps its identity. | +| Empty/incomplete/invalid delivery or failed fetch | No publication; preserve existing data and the last valid export. | +| Source value conflicts with an accepted correction | Store the source value in the received delivery, preserve the correction and block publication on conflict, including in the first import implementation. | +| Unknown access or changed scope | No automatic general publication or missing-record comparison; review first. | -Core controleert volgorde en actuele correcties opnieuw bij publicatie, ook als twee beoordeelde imports tegelijk klaarstaan. `retrieved_at` is een eenvoudige volgorderegel voor één producent met correcte UTC-klok; het bewijst geen transactiesnapshot bij de bron. Toekomstige of onlogische tijdstippen vragen beoordeling. Geen gedistribueerde teller bouwen voor de pilot. +Core rechecks ordering and current corrections at publication, including when two reviewed imports are ready concurrently. `retrieved_at` provides a simple ordering rule for one producer with an accurate UTC clock; it does not establish a transactional source snapshot. Future or implausible timestamps require review. Do not build a distributed counter for the pilot. -## Concrete overdracht +## Delivery milestones -1. **#1214:** bronkeuze, toegestane voorbeeldrij, betekenis en deze voorlopige afspraak. -2. **disabled-parking #774:** eventuele generieke bronpackagefix eerst, daarna één live commando dat dit bestand atomair schrijft. Vervang draftformaat, null-volledigheidsmetadata en achterhaalde voorbeeldroute; behoud slechts nuttige kleine tests. -3. **core #1215:** één intakepad met beoordeling en veilige eerste, gewijzigde, herhaalde, oudere, ontbrekende en conflicterende levering. Vervang/verwijder de oude #1222-schema's en validatie in dezelfde implementatie. -4. **#1217, disabled-parking #775 en core #1216:** provider kiezen, dezelfde levering automatisch uploaden en ontdekken. De gekozen opslaggrens moet voorkomen dat core een gedeeltelijk bestand verwerkt. +1. **#1214:** source selection, accepted example record, semantics and this provisional agreement. +2. **disabled-parking #774:** any generic source-package fix first, followed by one live command that writes this file atomically. Replace the draft format, null completeness metadata and obsolete example route; retain only useful small tests. +3. **core #1215:** one intake path with review and safe handling of initial, changed, repeated, older, missing and conflicting deliveries. Replace/remove the old #1222 schemas and validation in the same implementation. +4. **#1217, disabled-parking #775 and core #1216:** provision the selected provider, automatically upload and discover the same delivery. The storage boundary must prevent core from processing partial files. -Gewone CI werkt offline met kleine voorbeelden van packageobjecten. Eén afzonderlijke begrensde live proef bewijst bronophaling; een daadwerkelijke beoordeelde core-import bewijst de volgende stap. Geen van beide wordt door alleen een fixturetest vervangen. +Regular CI runs offline with small package-object examples. A separate bounded live probe establishes source retrieval; an actual reviewed core import establishes the next step. Fixture tests alone replace neither. -## Lokale uitvoering en herstel +## Local execution and recovery -1. Voer de migraties uit op de bedoelde ontwikkelomgeving en zorg dat de bestaande geografische relaties voor Amsterdam aanwezig zijn. Er worden geen legacygegevens automatisch gekoppeld of vervangen. -2. Registreer de aansluiting met `php artisan nipkaart:register-amsterdam `. Het commando controleert Amsterdam, land `NL` en provincie `NL-NH`; publicatie staat standaard uit. De ingestelde bbox `[4.65, 52.2, 5.15, 52.5]` is een ruime operationele begrenzing, geen officiële gemeentegrens. -3. Open als beheerder **Gemeentelijke imports**. Controleer de bronvoorwaarden en leg de onderbouwing vast vóór de eerste te publiceren levering. Een configuratiewijziging maakt oudere beoordelingen ongeldig; haal daarna een nieuwe levering op. -4. Bied het ongewijzigde producerbestand aan. Configureer PHP `upload_max_filesize` op minstens `32M` en `post_max_size` en de webserver-bodylimiet hoger dan 32 MiB voor multipart-overhead. Kleinere serverlimieten gelden vóór de applicatiecontrole. -5. Controleer aantallen, oorspronkelijke bronvelden, alle regelingen en een kaartsteekproef. De lijst toont maximaal 50 records per pagina; het parkeervlak en afgeleide punt zijn per record te openen. Goedkeuren en afwijzen vereisen een reden. Records met een afleiding staan bovenaan. Alleen het beheerscherm toont origineel en afleiding samen voor beoordeling, met onderscheid tussen lijnen en kleuren. Bij publicatie van een levering met afleidingen moet de beheerder expliciet bevestigen dat alle afleidingen zijn beoordeeld; afwijzen vereist die bevestiging niet. Ook de service dwingt dit af. +1. Run migrations in the intended development environment and ensure Amsterdam's existing geographical relationships are present. No legacy data is automatically associated or replaced. +2. Register the integration using `php artisan nipkaart:register-amsterdam `. The command checks Amsterdam, country `NL` and province `NL-NH`; publication is disabled by default. The configured bbox `[4.65, 52.2, 5.15, 52.5]` is a broad operational boundary, not an official municipal boundary. +3. As an administrator, open **Municipal imports** (shown as **Gemeentelijke imports** in the Dutch interface). Check source terms and record the supporting evidence before the first delivery to be published. Configuration changes invalidate earlier reviews; retrieve a new delivery afterwards. +4. Submit the unchanged producer file. Set PHP `upload_max_filesize` to at least `32M`, and `post_max_size` and the web server body limit above 32 MiB to allow multipart overhead. Smaller server limits apply before application validation. +5. Check counts, original source fields, every restriction and a map sample. The list displays up to 50 records per page; each record's parking area and derived point can be expanded. Approval and rejection require a reason. Records with a derivation appear first. Only the administration screen displays the original and derived geometry together for review, distinguished by lines and colors. Publishing a delivery with derivations requires explicit confirmation that every derivation has been reviewed; rejection does not require this confirmation. The service enforces this too. -`MunicipalImportService::intake()` is het gedeelde toegangspunt voor de upload en de latere bucketconsumer. Autorisatie geldt ook in de service. Eén datasetrij wordt vergrendeld tijdens publicatie; actuele bronrijen en importstatus worden opnieuw gecontroleerd. Een verouderde reviewtoken vereist opnieuw beoordelen. De levering, mutaties en laatste gepubliceerde ophaaltijd worden samen gecommit of teruggedraaid. Opnieuw aanbieden van dezelfde bytes geeft de bestaande status terug, ook na een configuratiewijziging. +`MunicipalImportService::intake()` is the shared entry point for uploads and the future bucket consumer. Authorization also applies inside the service. Publication locks one dataset row and rechecks current source records and import status. An outdated review token requires another review. The delivery, mutations and latest published retrieval timestamp are committed or rolled back together. Resubmitting identical bytes returns the existing status, including after a configuration change. -Bronclaims staan ongewijzigd in `source_record`. `geometry_derivation` bewaart bij een reparatie afzonderlijk de voorgestelde vorm, foutreden, methode en PostGIS/GEOS-versie; de ontvangen levering bewaart dezelfde afleiding. De normale review bewaart beoordelaar, tijdstip en reden. Er worden geen losse parkeerplaatsen uit MultiPolygon-delen gemaakt. Een nieuwe levering met dezelfde afleiding verandert geen inhoud; een later door de bron hersteld vlak verwijdert de actuele afleiding en behoudt de vorige in de importaudit. Bron en afleiding vallen onder de reviewtoken, zodat een gewijzigde afleiding opnieuw beoordeeld moet worden. `last_imported_values` bewaart de laatst afgeleide waarden. De huidige velden zijn de effectieve waarden: een afwijking ten opzichte van de vorige import geldt conservatief als handmatige correctie. Niet-conflicterende correcties en zichtbaarheid blijven behouden; een gelijktijdige afwijkende bronwijziging blokkeert publicatie. `last_checked_at` registreert de geslaagde controle, `source_updated_at` blijft onbekend. Bestaande niet-aangesloten gemeentelijke records blijven ongemoeid; aansluiting of reconciliatie daarvan is afzonderlijk werk. +Source claims remain unchanged in `source_record`. For repairs, `geometry_derivation` separately stores the proposed shape, error reason, method and PostGIS/GEOS version; the received delivery retains the same derivation. Normal review records the reviewer, timestamp and reason. MultiPolygon components are not converted into individual parking spaces. A new delivery with the same derivation does not change content; a later source-repaired polygon removes the current derivation while preserving the previous one in the import audit. Source and derivation are covered by the review token, so a changed derivation requires review again. `last_imported_values` stores the last derived values. Current fields are the effective values: a difference from the previous import is conservatively treated as a manual correction. Non-conflicting corrections and visibility are preserved; a concurrent conflicting source change blocks publication. `last_checked_at` records the successful check; `source_updated_at` remains unknown. Existing municipal records that are not connected to this integration remain untouched; linking or reconciling them is separate work. -`municipal_imports.before_values` bewaart voor gewijzigde records de vorige databasewaarden en voor nieuw aangemaakte records `null`. De ontvangen levering en beoordeling blijven bewaard. Dit ondersteunt een onderbouwde herstelbeslissing; er is geen automatische terugzetknop die latere handmatige correcties kan overschrijven. Bij een databasefout blijft de levering te beoordelen en kan dezelfde publicatie opnieuw worden geprobeerd. De reviewpagina vergelijkt altijd met de huidige gegevens, ook na publicatie; historische verschillen zijn niet hetzelfde als deze actuele vergelijking. +`municipal_imports.before_values` stores previous database values for changed records and `null` for newly created records. The received delivery and review are retained. This supports an informed recovery decision; there is no automatic rollback button that could overwrite later manual corrections. A database failure leaves the delivery available for review and the same publication can be retried. The review page always compares against current data, including after publication; historical differences are distinct from this current comparison. -Een migratierollback verwijdert de importaudit en bronkoppeling. Maak eerst een databaseback-up en beoordeel herstel op gegevensniveau; een code-rollback is geen gegevensherstel. Onbekende capaciteit blijft nullable, ook na rollback. Productieactivering, legacyoverdracht, bucketcredentials en planning vallen buiten deze stap. +Rolling back the migration removes the import audit and source association. Back up the database first and assess recovery at the data level; reverting code does not restore data. Unknown capacity remains nullable, including after rollback. Production activation, legacy transfer, bucket credentials and scheduling are outside this step. -## Automatische R2-overdracht (#775 / #1216) +## Automatic R2 handoff (#775 / #1216) -De producer uploadt hetzelfde bestand met één `PutObject` naar `municipal/nl-amsterdam-parkeervakken-e6a/.json` in een private Cloudflare R2-bucket. `If-None-Match: *` voorkomt overschrijven door deze uploader. `Content-MD5` controleert transportintegriteit; metadata `sha256` bevat de SHA-256 van de exacte bytes. Bij een bestaande objectnaam vergelijkt de producer de volledige remote inhoud met het bewaarde pending bestand. Alleen identieke bytes tellen als een geslaagde herhaling. Geen multipart-upload of los manifest. Zie de [ondersteunde R2-operaties](https://developers.cloudflare.com/r2/api/s3/api/). +The producer uploads the same file in one `PutObject` to `municipal/nl-amsterdam-parkeervakken-e6a/.json` in a private Cloudflare R2 bucket. `If-None-Match: *` prevents this uploader from overwriting objects. `Content-MD5` checks transport integrity; `sha256` metadata contains the SHA-256 of the exact bytes. When an object name already exists, the producer compares its full remote content with the retained pending file. Only identical bytes count as a successful retry. No multipart upload or separate manifest. See the [supported R2 operations](https://developers.cloudflare.com/r2/api/s3/api/). -De nog te implementeren consumer in core #1216 ontdekt complete objecten binnen de toegestane prefix, begrenst downloads op 32 MiB en controleert de SHA-256, bestandsinhoud en overeenkomst tussen objectnaam, dataset en delivery-ID vóór dezelfde intake. Metadata is geen zelfstandige vertrouwensbron. Het bestaande intakebeleid voor conflicten, herhaling, volgorde en beoordeling blijft gelden; ontvangst publiceert niets automatisch. +The consumer still to be implemented in core #1216 will discover complete objects within the allowed prefix, limit downloads to 32 MiB, and check SHA-256, file contents and agreement between object name, dataset and delivery ID before calling the same intake. Metadata is not an independent source of trust. Existing intake rules for conflicts, retries, ordering and review remain in force; receiving a file never publishes it automatically. -Eén permanent draaiende producercontainer op een eigen Linux-host voert eindige opdrachten achter elkaar uit. De standaardwachttijd na succes is 24 uur; na fouten maximaal vijf minuten. Een volume bewaart `pending.json` totdat de upload is bevestigd en bewaart daarna `last.json`. Dezelfde bytes worden na herstart opnieuw aangeboden voordat nieuwe gegevens worden opgehaald. De definitieve regio, bewaartermijn langer dan de toegestane core-uitval, budget en daadwerkelijk geteste toegangsrechten blijven onderdeel van #1217. De implementatie en lokale tests vervangen geen herstelproef tegen echte staging-R2 en core. +One persistent producer container on an owner-managed Linux host runs finite commands serially. The default wait after success is 24 hours; after failures, at most five minutes. A volume retains `pending.json` until upload is acknowledged, then keeps `last.json`. After a restart, the same bytes are retried before fetching new data. The final region, retention exceeding the allowed core outage, budget and verified access rights remain part of #1217. Implementation and local tests do not replace recovery rehearsal against actual staging R2 and core. From 3669703340ac7c99f0cef8f7911178d7f761db5c Mon Sep 17 00:00:00 2001 From: Klaas Schoute Date: Mon, 14 Sep 2026 15:28:03 +0200 Subject: [PATCH 3/3] docs: link pilot PRs and clarify Content-MD5 encoding --- docs/development/data-foundation-stack.md | 6 +++--- docs/development/data-import-contract.md | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/development/data-foundation-stack.md b/docs/development/data-foundation-stack.md index 469e5166..bb53afdd 100644 --- a/docs/development/data-foundation-stack.md +++ b/docs/development/data-foundation-stack.md @@ -1,6 +1,6 @@ # Technology stack for the first import pipeline -Status: implementation choices for [#1214](https://github.com/NIPKaart/core/issues/1214). See the [delivery contract](data-import-contract.md) and [work packages](data-foundation-delivery.md). The live Amsterdam export and manually reviewed core import were accepted on 14 September 2026; disabled-parking #783 and core #1222 are merged. The private bucket and automatic execution have not been provisioned. +Status: implementation choices for [#1214](https://github.com/NIPKaart/core/issues/1214). See the [delivery contract](data-import-contract.md) and [work packages](data-foundation-delivery.md). The live Amsterdam export and manually reviewed core import were accepted on 14 September 2026; [disabled-parking #783](https://github.com/NIPKaart/disabled-parking/pull/783) and [core #1222](https://github.com/NIPKaart/core/pull/1222) are merged. The private bucket and automatic execution have not been provisioned. | Component | Initial choice | | --- | --- | @@ -10,13 +10,13 @@ Status: implementation choices for [#1214](https://github.com/NIPKaart/core/issu | Dependency management | uv and `uv.lock`; install with `uv sync --locked`, then use `uv run ...`. Reusable packages retain their own tooling. | | Mapping | Small Python dataclass and explicit mapping for one source. | | File | One bounded UTF-8 JSON document, written through a temporary file and atomic replacement. | -| Validation | Explicit type and content checks on both sides. The Opis/schema prototype was removed in #1222; one intake service handles the pilot format. | +| Validation | Explicit type and content checks on both sides. The Opis/schema prototype was removed in [core #1222](https://github.com/NIPKaart/core/pull/1222); one intake service handles the pilot format. | | Tests | Existing Python unittest/pre-commit checks and core Pest. Small mapping examples; no municipal network calls in regular CI. | | Initial handoff | Local file passed to the same core intake service that will later process bucket objects. | | Automatic handoff | Private Cloudflare R2 bucket. Region, costs, access rights and staging validation belong to #1217; no resources have been provisioned. | | Scheduled execution | One persistent Docker container per producer repository on an owner-managed Linux host or cloud VM. An internal scheduler runs finite commands serially; no container per municipality. Core uses its own scheduler/queue. Implementation in #775/#1217. | -The uv migration and SQL runtime removal were merged in disabled-parking #779 and #780. The temporary Hamburg export from #781 was replaced by the live Amsterdam route in #783. Core #1222 provides the corresponding review and publication flow. Core contains no Python code or source clients. +The uv migration and SQL runtime removal were merged in [disabled-parking #779](https://github.com/NIPKaart/disabled-parking/pull/779) and [#780](https://github.com/NIPKaart/disabled-parking/pull/780). The temporary Hamburg export from [#781](https://github.com/NIPKaart/disabled-parking/pull/781) was replaced by the live Amsterdam route in [#783](https://github.com/NIPKaart/disabled-parking/pull/783). [Core #1222](https://github.com/NIPKaart/core/pull/1222) provides the corresponding review and publication flow. Core contains no Python code or source clients. Use standard libraries and existing dependencies where they suffice. The first source needs no new broker, worker framework, local SQLite scheduling database, mandatory sequences, JSONL or separate manifest. Add a component only when the working pipeline demonstrates a need for it. diff --git a/docs/development/data-import-contract.md b/docs/development/data-import-contract.md index 601178c2..f4090323 100644 --- a/docs/development/data-import-contract.md +++ b/docs/development/data-import-contract.md @@ -94,7 +94,7 @@ Rolling back the migration removes the import audit and source association. Back ## Automatic R2 handoff (#775 / #1216) -The producer uploads the same file in one `PutObject` to `municipal/nl-amsterdam-parkeervakken-e6a/.json` in a private Cloudflare R2 bucket. `If-None-Match: *` prevents this uploader from overwriting objects. `Content-MD5` checks transport integrity; `sha256` metadata contains the SHA-256 of the exact bytes. When an object name already exists, the producer compares its full remote content with the retained pending file. Only identical bytes count as a successful retry. No multipart upload or separate manifest. See the [supported R2 operations](https://developers.cloudflare.com/r2/api/s3/api/). +The producer uploads the same file in one `PutObject` to `municipal/nl-amsterdam-parkeervakken-e6a/.json` in a private Cloudflare R2 bucket. `If-None-Match: *` prevents this uploader from overwriting objects. `Content-MD5` contains the base64-encoded binary MD5 digest of the file bytes (not the hexadecimal digest) and checks transport integrity; `sha256` metadata contains the SHA-256 of the exact bytes. When an object name already exists, the producer compares its full remote content with the retained pending file. Only identical bytes count as a successful retry. No multipart upload or separate manifest. See the [supported R2 operations](https://developers.cloudflare.com/r2/api/s3/api/). The consumer still to be implemented in core #1216 will discover complete objects within the allowed prefix, limit downloads to 32 MiB, and check SHA-256, file contents and agreement between object name, dataset and delivery ID before calling the same intake. Metadata is not an independent source of trust. Existing intake rules for conflicts, retries, ordering and review remain in force; receiving a file never publishes it automatically.