PMTiles archive metadata: the elivagar provenance block
Every archive elivagar writes carries a record of the contract it was built under, as a top-level elivagar member of the PMTiles metadata JSON.
Read it with scripts/dump-pmtiles-metadata.py <archive> --key elivagar. elivagar inspect renders only the fields it knows about (layer list, zoom range) and will not show this block.
Why it exists
A prose rule already said not to compare across dataset variants. reference/technical-implementation-spec.md states it plainly, and names the 2026-07-09 false alarm that "burned a session tail proving the code innocent".
On 2026-07-14 the same thing happened again. A denmark archive blessed from a --variant locations build was compared against a default-variant (raw) build. brokkr regress reported 363,620 structural moves across every layer, which was read as a code regression and investigated as one, at length, in the wrong subsystem. The two builds were both correct.
The rule was five days old and written down. It failed twice because it asks a human to remember an invariant at exactly the moment they are focused on something else, and because nothing in the artifacts could contradict them.
That is what this block changes. The archive states its own contract, so a consumer can refuse an incomparable diff instead of reporting it as a finding. A rule that only exists as prose is a rule that gets broken on a busy afternoon; the same rule enforced against recorded facts is not.
The split, which is the whole design
Groups get opposite treatment, and getting this backwards inverts the gate.
input and config are the comparability contract. They describe the work requested. Two archives whose contract differs describe different work, so a geometry diff between them says nothing about the code. A consumer compares these first and refuses the diff on mismatch.
effective, build and execution are diagnostic and must never be equality-gated. Given identical input and config, effective is a pure function of the code, and build is the code. Regression testing exists to compare revisions, so gating on either would refuse precisely the comparisons the gate is for. Change how the pipeline selects a path and a contract-gated consumer would refuse the diff exactly when it matters most.
These groups are not ranked by importance. effective is the most explanatory thing in the block - it is what tells you why a diff exists once the contract matches - and it is still not part of the gate.
Reproducibility constraint
Nothing in this block may vary between two builds of the same commit on the same input.
No wall-clock, no hostname, no absolute paths, no thread counts, no timings, no measured durations, no environment-order-dependent values. Same-commit builds are byte-identical (AGENTS.md) and the corpus digest and contract are recomputed from archive content; a per-run value here would break both.
Anything that fails this test does not belong in the archive. It belongs in .brokkr/sidecar.db, which exists for exactly that class of data.
Labels versus identities
The governing distinction. A label describes; an identity pins.
input.nameis a label. Never gate on it. Two files named for the same region and built at the same commit can be entirely different contracts, which is how 2026-07-14 happened.input.xxh3_128is the identity. Gate on this.input.featuresare observed, read from the PBF header rather than inferred from a name, and they determine which paths the pipeline takes.build.*.commitis only an identity whendirtyisfalse. A dirty tree means the commit names the nearest ancestor of the code that ran, not the code that ran.
The same lesson applied to dependencies. protohoggr was a path dependency labelled 0.4.0 while a second, content-checksummed 0.4.0 came from the registry via pbfhogg - two crates, one label, no assertion that they matched. Pinning it to the registry version made the lockfile checksum its identity.
Schema
schema is the version. Bump it when the meaning of an existing field changes. Adding a member does not require a bump: readers ignore unknown members, and the contract comparison is defined over named fields, not the whole object.
{
"elivagar": {
"schema": 1,
"input": {
"name": "denmark-20260220-seq4704-locations-prepass.osm.pbf",
"xxh3_128": "58c47f32d3a55b04a56813565efc78ac",
"bytes": 531544047,
"replication_timestamp": 1771622445,
"features": {
"sort_type_then_id": true,
"locations_on_ways": true,
"way_members_v1": true,
"shared_node_pins_v1": true
}
},
"config": {
"profile": "shortbread",
"min_zoom": 0,
"max_zoom": 14,
"tile": {
"format": "mvt",
"compression": "gzip",
"base_compression_level": 6,
"compression_policy": "zoom-v1"
},
"fanout_caps": {},
"polygon_simplify_factor": 1.0,
"ocean": {
"mode": "artifact",
"runtime_simplification": true,
"low_zoom_source": "simplified",
"artifact_key": { }
}
},
"effective": {
"coordinate_source": "inline",
"way_members": "injected_v1",
"shared_node_pins": "injected_v1"
},
"build": {
"elivagar": { "commit": "ec5bd11...", "dirty": true },
"pbfhogg_reader": { "version": "0.5.0" },
"cargo_lock_xxh3_128": "f850fa82608190a904500b37810e3def",
"cargo_features": ["default", "mimalloc-alloc"]
},
"execution": { "resumed_from": null }
}
}Field notes
input.xxh3_128 matches the xxhash brokkr records per dataset variant in brokkr.toml, byte for byte, with no coordination between the two - the same XXH3-128 over the same bytes. They can be compared directly.
config.tile.base_compression_level is a base, not a uniform setting: the encoder clamps low zooms up and caps z13/z14 down. Recording the level alone would misdescribe the output, so compression_policy names and versions the mapping from base to per-zoom level.
config.ocean.mode is the resolved mode, taken from OceanMode rather than inferred from which config paths are set. Those differ: the ocean phase runs only when the full shapefile is present, so a simplified-only config produces no ocean and correctly reports none. It distinguishes artifact from shapefile and is part of the contract: artifact-served tiles are not geometry-identical to extract-computed ones (descent seams depend on each piece's clip extent - see the extract-consumption note in src/pipeline/mod.rs). Comparing an artifact-active archive against an artifact-absent one is a contract mismatch, not a regression.
config.ocean.low_zoom_source is independent of runtime_simplification. Source selection (a separate simplified shapefile serving z0-7) and runtime simplification (--no-ocean-simplify) are different things; a single boolean conflates them.
effective.shared_node_pins is the largest of the three effective paths in blast radius. Pin scope changes which vertices survive simplification, so injected_v1 and block_local produce materially different geometry from the same OSM data.
build.pbfhogg_reader is the pbfhogg linked as our reader, not the pbfhogg that produced the input PBF. Those can differ. The producer is not recorded (see gaps); the input hash is authoritative regardless. It is a pinned registry dependency now, so it records its locked semver version rather than a commit/dirty pair: a registry dependency has no git tree to be dirty against, and cargo_lock_xxh3_128 already pins its exact checksum - the version is the reader-legible half of that pin.
build has no protohoggr entry by design. It is a pinned registry dependency, identified exactly by the content-addressed checksum in cargo_lock_xxh3_128, and cannot drift. pbfhogg is likewise pinned there; it keeps a named entry only to surface its human-readable version. Only path dependencies would need a commit/dirty pair, and there are none.
execution.resumed_from names the phase a --skip-to run began at, or is null for a full run.
Checkpoint provenance
--skip-to reuses sort chunks from a previous run, and nothing in a later invocation reveals what produced them. The v5 checkpoint (JSON) records the input hash, the producer config, and the effective paths, and a resume validates the first two.
Input hash. A mismatch is a hard error: resuming would mix two inputs into one archive and stamp it with provenance describing only the second.
Producer config. The subset of settings that decide what phase12 writes into chunks - zoom range, fanout caps, polygon_simplify_factor. Chunks cannot be reinterpreted under different values, so a mismatch is a hard error naming the field. It deliberately excludes the assemble-side settings (tile format, tile compression, compression level, memory budgets), which are applied after the chunks are read and may legitimately change on a resume; including them would reject safe resumes.
Effective paths. A matching resume inherits them rather than re-deriving, because re-derivation would describe what this invocation would have done, not what produced the chunks on disk.
v4 and earlier are rejected; re-run a full tilegen. v4 also used a whitespace-positional encoding where each added field shifted an index and a miscount was a silent misparse - which is why v5 is JSON.
Where the input hash is taken
Identifying the input means reading all of it, so placement is a performance decision, not a detail. A full run hashes after phase12, when the PBF is warm in page cache from the read that just finished. A resume hashes up front because it must validate before proceeding, and can afford to because phase12 never runs and so never reads the file again.
Hashing before phase12 on a full run would put a cold serial pass in front of the reader and, at planet scale, cost a second whole-file read plus the page cache it evicts.
Freshness, and why absent beats stale
Provenance that lies is worse than provenance that is absent, because it invites a reader to attribute a diff to code that was never linked. Every value is therefore either established or emitted as unknown, and nothing is guessed. Where the input cannot be hashed, the entire block is omitted rather than written partially.
build.rs is where this is hardest. Cargo reruns a build script only when a declared input changes, but relinks the crate whenever a path dependency's sources change - so without care, editing pbfhogg would rebuild elivagar while the script kept reporting the previous commit and dirty state. The rerun-if-changed declarations cover both what git records (HEAD, and the ref it points into) and what git does not (uncommitted edits, via the source trees themselves).
This mechanism is not verified. Confirming it means editing pbfhogg to observe whether the script reruns. Treat a build block as strong evidence, not proof, until it is.
dirty uses brokkr's definition, not git's: *.md and .brokkr/results.db are ignored. A dirty results.db is this repo's expected state and markdown never affects the binary, so counting either would mark nearly every build dirty and make the flag meaningless.
Consumer contract
A consumer comparing two archives:
- Compare
inputandconfig. On mismatch, refuse the geometry comparison and report which field differs. Do not emit a diff. - Report
builddifferences. Never gate on them. - Surface
effectivewhen explaining a diff that survived step 1.
An archive with no elivagar member predates this schema or failed to identify its input. It cannot be gated, and a consumer should say so rather than assuming comparability.
Known gaps
Recorded here so they are not rediscovered as bugs:
- Enforcement landed for the standing gate only.
brokkr pmtiles-corpus check(native brokkr code since the 2026-07-24 corpus redesign, decoding this block through the linked crate) compares the committedcontract.jsonagainst this block before reading content and refuses with the differing field named, including the calibrated stale-artifact mismatch atconfig.ocean.artifact_key.policy_version.brokkr regressstill reads nothing here: an ad-hoc two-archive diff can be run across incomparable archives, so establish comparability withelivagar inspectfirst. input.replication_sequenceis not recorded (the pbfhogg header API for it is unconfirmed).replication_timestampis.config.oceanrecords shapefile hashes only viaartifact_key, so ashapefile-mode archive does not identify its shapefiles.data_boundsis not recorded. It is derived from the input, and it drives ocean selection, so it explains ocean-layer diffs between variants. Note it is derived, so it is diagnostic, not contract.pbf_generator(the pbfhogg revision that produced an injected PBF) is not recorded. It could be stamped into the header by pbfhogg at write time.- No
tileset_digest. Adding provenance makes archives byte-different across commits even when every tile is identical; a digest over ordered tile IDs and uncompressed payloads, excluding metadata, would restore a cheap tiles-identical check. Regress already establishes tile equality per blob, so this is a convenience.