clone

an offline map explorer where each world, from a game map to a city's open data, ships as one .atlas file

feat/atlas-next-horizon
atlas / docs / generate.md
generate.md 61.2K rendered
1# The generate lane23> **Historical creation path.** The public authoring system is now the4> single-manifest native vNext compiler in [authoring.md](authoring.md). These5> packages remain for fixture migration and regression coverage; their CLI6> commands are not registered.78Generate makes volumes. It takes a capture archive — bytes somebody else9published, fetched once and kept — and writes a `.atlas` file: one volume,10complete, offline, deterministically named.1112The lane has four steps and one rule.1314```15capture ──▶ translate ──▶ [tiles] ──▶ compose ──▶ a volume in a registry16 network per source rasters conventions, packing, stamping17 hand-run read-time derived producer-strict18```1920**The rule: no data source's shape, vocabulary, or name appears outside21`internal/generate/sources/<name>/`.** Everything downstream of a translator22speaks Atlas's own vocabulary and cannot tell which source it read. Only a23merge ledger ever names a source, and merging is not this lane's work.2425Generate is **single-source**. What one source said travels through26untouched; folding two sources' readings of one volume together belongs to the27[enrich lane](enrich.md), and the composed multi-source result is28`generate ⊕ enrich`.2930This document is normative for the interchange document's schema and for the31curation data files. It is the prose twin of `internal/generate/doc` and32`internal/generate/curation`.3334---3536## 1. The interchange document3738`internal/generate/doc` defines the one thing that crosses the middle of the39lane. It is **Atlas's schema, designed backwards from what composition needs**40— worlds, lenses, collections, features, geometry, attributes, provenance —41and no source's shape is privileged in it.4243A document is produced and consumed inside one run. It is not an archive44format and nothing is expected to read one back years later; it exists so that45the two halves of the lane can be reasoned about, tested, and printed46separately. `atlas translate` prints one.4748### 1.1 The shape4950```jsonc51{52 "doc": "atlas-generate-doc",53 "version": 1,54 "volume": { "slug": "tunic", "title": "TUNIC" },55 "source": {56 "name": "mapgenie", // the slug a ledger names it by57 "label": "MapGenie", // the same source, spelled for a person58 "license": "", // what the volume owes, where it is known59 "attribution": "…",60 "idSpace": "native" // "native" | "derived"61 },62 "worlds": [ … ],63 "icons": [ { "key": "fox_shrine", "file": "fox_shrine.svg", "data": "<base64>" } ]64}65```6667A **world**:6869```jsonc70{71 "id": 427,72 "slug": "world",73 "title": "World",74 "center": { "lat": 0.787…, "lng": -0.771… },75 "capture": {76 "kind": "map", // the source's own name for these bytes77 "id": 427, // the capture's id in the source's id space78 "locator": "/api/v1/maps/427/full",79 "contentHash": "ff58c59a…", // SHA-256 of the archived bytes80 "capturedAt": "2026-07-30T03:57:41.529Z"81 },82 "lenses": [ {83 "name": "Default",84 "tileSet": "tunic/world/default-v2",85 "frame": { // the deriver reads this; composition never does86 "minZoom": 9, "maxZoom": 15, "format": "jpg",87 "windows": { "15": { "minX": 16256, "minY": 16256, "maxX": 16383, "maxY": 16383 } }88 }89 } ],90 "collections": [ … ],91 "attrs": { } // the world speaking the conventions92}93```9495A **collection** and its **features**:9697```jsonc98{99 "id": 5984, // zero asks composition to derive one from key100 "key": "regions", // merge identity; absent, the icon key stands in101 "title": "Fox Shrine",102 "group": "Locations", // a legend heading, and only that103 "kind": "point", // point | path | area104 "icon": "fox_shrine", // names artwork in the document's icon set105 "color": "#6984F2",106 "iconColor": "#6984F2",107 "visible": true,108 "attrs": { "atlas.render.as": "pin" },109 "features": [110 {111 "id": 177474,112 "title": "Fox Shrine",113 "subtitle": "",114 "description": "",115 "at": { "lat": 0.804…, "lng": -0.737… }, // points116 "geometry": [ { "type": "Polygon", "coordinates": … } ], // paths and areas117 "center": { "lat": …, "lng": … },118 "member": 0, // the area feature whose ground this stands on119 "parent": 0, // the area feature this area sits inside120 "links": [ { "title": "the well", "feature": 200 } ],121 "attrs": { }122 }123 ]124}125```126127### 1.2 The decisions128129The schema is source-neutral by design130([decision 4](decisions/0004-source-neutral-interchange-document.md) carries131the context), and its load-bearing choices are these:132133- **One ordered `collections` array.** Points, paths and areas are the same134 kind of citizen; a group is a heading string on a collection, never a135 container.136- **Extensions are ordinary fields.** `attrs` speaks the conventions, and a137 collection is declared like anything else.138- **Render policy travels once.** A publisher's own render field is spoken as139 `atlas.render.as` by the source, and the field itself never travels.140- **Coordinates are `{lat, lng}` floats.** A source's tolerance for other141 spellings is the source's business.142- **Absence is `0`, everywhere** — matching the wire's own reading of zero143 (§1.4).144- **Artwork travels in the document**, never as a key implying a file probe at145 composition time.146- **Lens detail splits in two.** Name and tile set are for composition; the147 publisher's claimed frame is carried separately, for the deriver alone,148 because only a source can say which tiles a complete level was supposed to149 hold.150- **Provenance rides the document** — `source`, and a per-world `capture` —151 rather than being recovered from directory names.152- **Links are resolved by the source**, because a link syntax is a153 publisher's.154- **The world window is curation's** to name where it is shared; a pyramid155 names its own.156157Two structural choices frame all of that: the collection/feature model — a158collection has a key, a title, a legend heading, a kind, visibility,159attributes and features; a feature has an identity, a place or a geometry,160nesting, prose, links and attributes — and translation at **read time**, over161an archive of source-native bytes, so an editorial change replays over years162of captures instead of re-crawling them.163164### 1.3 Coordinates165166Positions are latitude and longitude **in the volume's own projection** — the167same world space `format/bundle.Coordinate` carries and the same one a lens's168tile pyramid is cut in. For a game map that space is synthetic: an image was169encoded as a slippy map long ago and the numbers are pixels wearing degrees.170For a planet or a city it is the real thing.171172A source publishes what its ground means; composition does no reprojection.173Composition projects to world pixels in exactly one place — measuring the174ground a world's contents cover — using spherical Mercator against the world's175tile window.176177### 1.4 Identifiers178179Feature and collection identifiers are `int64` in the document and **signed18032-bit on the wire, where zero reads as absence**.181182- A source with native numeric identities passes them through and declares183 `idSpace: "native"`.184- A source without them derives stable identifiers from stable names and185 declares `idSpace: "derived"`. Derivation must be a pure function of a186 name, and a collision must fail rather than rename something.187- A collection may carry a `key` and no `id`; composition derives one, as188 FNV-1a over `"<world id>:collection:<key>"`, masked to the positive range of189 an `int32`, with zero moved to one, refusing a collision with a number the190 source already claimed.191192**One id space per world.** The text payload is keyed by feature id across193every collection and every kind, so two features sharing a number would lose194one of their descriptions. `Document.Validate` refuses it.195196### 1.5 Determinism197198Every ordering in a document comes from the capture. Maps are the only199unordered thing in it and JSON sorts their keys. A source that sorts is200throwing away an editorial order somebody chose.201202---203204## 2. Sources205206A source is one publisher's reader. The interface is three lines:207208```go209type Source interface {210 Describe() doc.Provenance211 Translate(a *archive.Archive, v archive.VolumeRef, log *slog.Logger) (doc.Document, error)212}213```214215`internal/generate/sources/registry.go` is the assembly point and **the one216file outside a source's own directory permitted to name a source**. It may name217only a constructor. If adding a source needs a second line there, the source is218leaking.219220### 2.1 The rules a source keeps221222- **No network.** Capture is a separate, hand-run step. A translator is a pure223 function of bytes already on disk, which `depcheck`'s `netconfine` rule224 enforces: outbound HTTP lives in `internal/generate/crawl` and nowhere else.225- **Refusal over guessing.** A source states its structural preconditions and226 refuses what does not meet them. A volume that is merely *not crawled yet*227 wraps `ErrNotReady` and is skipped; anything else fails the build.228- **Gates are named.** IGN refuses embedded-MapGenie maps, Piggyback refuses229 unverified transforms, ArcGIS refuses an uncurated city230 ([decision 16](decisions/0016-uncurated-captures-are-passed-over.md)). The231 MapGenie reader refuses a capture whose232 kind is not its own, because reading another source's bytes through it would233 produce a document that lies about where it came from.234- **Offline purity starts here.** A publisher's links are resolved or removed235 by its own reader. Nothing downstream should have to know a link syntax.236- **Determinism.** Same archived bytes, same document, on any machine.237238### 2.2 The MapGenie reader239240`internal/generate/sources/mapgenie` is the worked example and the only source241in the tree today.242243- Capture kind `map`: one map's whole API response, archived verbatim.244- Every `group × category` becomes a point collection, in capture order. The245 group survives as a heading string.246- A category's colour resolves over its group's, normalized to `#RRGGBB` upper247 case.248- Coordinates arrive as JSON numbers on some maps and quoted strings on others;249 both are read.250- Regions become an area collection keyed `regions`, titled `Regions`,251 numbered by composition. A region whose geometry all came through empty is252 dropped. A line among the regions is refused: MapGenie's regions are ground,253 and a line would be a path collection somebody meant to declare.254- `display_type` is spoken once as `atlas.render.as` and never travels.255- Markdown links are replaced by their labels; a link carrying256 `locationIds=<n>` that names another pin of the same world survives as a257 cross-reference; every surviving URL is stripped.258- Artwork is read once per key, `.svg` then `.png`, and carried in the259 document.260261### 2.3 The NASA Trek reader262263`internal/generate/sources/nasatrek` reads a planetary volume. A capture marries264two publications that know nothing of each other: a global equirectangular265mosaic from NASA's Trek tile services, and the IAU Gazetteer of Planetary266Nomenclature's feature list for the same body.267268- Capture kind `trek-map`, one per body, carrying every mosaic captured for it.269- The coordinate design is the whole trick. A Trek mosaic is two tiles wide and270 one tall at its own zoom zero, so a Trek level sits one zoom up in the square271 the corpus cuts and the planet fills the top half of the world square. A272 feature's planetary coordinates become a pixel of that image — longitude across273 the full width, latitude down the top half — and the pixel becomes a synthetic274 position. The projection's distortion cancels exactly, because the raster and275 the features ride one mapping.276- The world declares the flattening as conventions (`atlas.geometry.surface`,277 `.projection`, `.equirect.px`, `.equirect.deg`, `.body`), so a reader can run a278 packed position backward and stand on the planet, and every feature carries the279 coordinates the Gazetteer published verbatim as `atlas.geo.lat`/`.lon`.280- One collection per Gazetteer feature type, sorted, all under the heading281 `Nomenclature`. The type descriptor `"Crater, craters"` keeps its singular half282 as the title and lends its slug as the artwork key.283- The Gazetteer has no artwork, so each collection names a library glyph through284 `atlas.icon.std`, chosen from the IAU's own type codes — a *mons* is a mountain,285 a *patera* a volcano, a *palus* literally a marsh — falling back to shape286 language where no glyph says the thing.287- `idSpace: "derived"`: nothing in either publication numbers a mosaic or a288 feature type, so every identity is minted from a stable name.289290### 2.4 The Blue Marble reader291292`internal/generate/sources/nasabluemarble` reads the included Earth volume's293one capture: NASA Earth Observatory's Blue Marble Next Generation base map294married to Natural Earth's 1:110m cultural vectors — pinned publications295rather than live endpoints.296297- Capture kind `blue-marble-map`, one per capture of the pinned files.298- One world (`earth`), one lens (`Blue Marble`), and the lightest cut of299 ground truth: the primary capitals as a point collection per continent,300 each pin a member of the country it stands in, and the country borders as301 one quiet area collection. Continents are Natural Earth's own filing; a302 capital whose microstate the 1:110m borders leave out takes the nearest303 drawn ground's continent, computed from the captured polygons at translate304 time the way the city source computes its watershed join. Everything else305 is later sources' work, folded in by the enrich lane.306- The coordinate design is the shared whole-sphere window NASA Trek also rides307 (`doc.EquirectPx` and friends): a 2:1 global image filling the top half of308 the world square, declared as registered conventions — surface, projection,309 the equirect px/deg pair, the body, and the body's mean radius — so the cell310 systems recognise the ground through the same declarations every spherical311 world makes.312- The capture body records the product's identity — its title, NASA's credit313 line, the source digest and size — and the derivation policy its tiles were314 cut under, so a policy change reads as a new capture rather than the same315 one wearing new bytes.316- The gates: another kind's capture, another source's name, another body, a317 missing digest, a missing size, a capture declaring no pyramid or carrying318 no features, a country with no identity, or a capital off the planet are319 refused by name.320- `idSpace: "derived"`: nothing in the publication numbers a base map.321322The crawler half (`internal/generate/crawl/bluemarble.go`) is the one place323the pinned URLs, digests and capture time are spelled. It fetches each pinned324file exactly once into the staged archive, refuses any digest but the pinned325one, cuts the pyramid's deepest level through the deterministic fixed-point326Catmull-Rom resampler beside it, distills the Natural Earth files into the327capture body — names, codes, continents, label points and rings, coordinates328verbatim — and records the capture under the pinned time, so the archive — and329everything stamped downstream of it — reproduces on any machine. `included/README.md` is the recipe and the provenance, and330`make included-earth` is the whole run.331332### 2.5 The IGN reader333334`internal/generate/sources/ign` reads a community wikimap: a flat image tiled335like a world, markers placed on it in image-relative coordinates, and a flat list336of marker types that names a parent to make two levels of legend.337338- Capture kind `ign-map`.339- A wikimap's image is normalized so its taller dimension spans 1. A marker's340 latitude runs down that span and is negative, its longitude across it, so341 `(lng, -lat)` times the world square's edge is the pixel it is drawn on.342- One collection per marker type, types in slug order, gathered under the heading343 of whichever parent each names, headings in the order their first type appears.344 A type no marker uses is left out — an empty collection would only dim the345 legend — but it stays in the capture, so a marker appearing under it later346 revives the collection without a policy change.347- Artwork is a PNG sprite per type slug, read from the archive and carried.348- **The gate: an embedded MapGenie map is not an IGN map.** Some IGN wikimap349 pages are MapGenie maps in an IGN frame — the page declares a MapGenie game id350 and serves MapGenie's tiles. Capturing one here would archive a second, worse351 copy of data Atlas already reads properly, and a merge would then fold a source352 into itself. The refusal lives in `internal/generate/crawl`, where the page's353 own declaration is still in front of the crawler; by the time a capture exists354 the evidence has been thrown away.355- `idSpace: "derived"`: markers are opaque strings and types are slugs.356357### 2.6 The Piggyback reader358359`internal/generate/sources/piggyback` reads the official guide house's maps,360which carry what a community wikimap does not: prose. Pins arrive with names and361descriptions and both survive into a volume.362363- Capture kind `piggyback-map`.364- Piggyback draws in a game's own coordinates on a Leaflet `CRS.Simple` map: a365 linear transformation squeezes them onto the unit tile at zoom zero. A pin366 passes through it and then through the shared inverse Mercator.367- A declared category becomes a heading and its types become collections under368 it, ordered by declared position then key. A type no pin uses is left out.369- District name pins arrive filed under the reader-state `favorites` category,370 which is nothing to build a legend from. They gather under their own heading371 and render as `text`, which is what they are on Piggyback's own map. Any *other*372 undeclared type fails the build: better a loud build than a pin silently373 dropped.374- Piggyback publishes no bounds, so the crawler's own survey of which tiles375 answered is the only account of where the pyramid is drawn, and a capture with376 no observed level is refused.377- **The gate: an unverified transform is refused.** The transformation is read378 off the page's own scripts, it decides where every pin in the volume stands,379 and a wrong one puts a whole map's contents somewhere plausible and wrong —380 exactly the failure a later merge would try to fit an affine to.381 `verifiedTransforms` is the list of games whose numbers have been checked382 against the published map; a capture whose numbers are not in it fails.383- `idSpace: "derived"`: pins, categories and types are opaque string ids.384385### 2.7 The ArcGIS Hub reader386387`internal/generate/sources/arcgishub` reads a city. It is the only source whose388subject is a real place, and the only one where **a world is a date**: each crawl389day of a city's open-data hub registers its own world, so a volume's world picker390is the city's version history and the difference between two worlds is a391difference in the city.392393- Capture kind `arcgis-map`.394- A city sits off the Web-Mercator diagonal, so its curated bounding box is395 padded five percent per side and squared in Mercator's own terms; that square396 is the world. Features project through the Mercator forward onto it and out397 again through the shared synthetic inverse, and the world declares the mapping398 as `atlas.geometry.mercator.px`/`.deg`, so a reader can invert it and stand on399 the ground. Every pin also keeps the coordinates the city published, verbatim.400- Polygon and line datasets become shape collections — one collection per401 dataset, one feature per curated bucket, holding every row that fell into it.402 Point datasets become pins under their curated heading. Pins are emitted first403 and ground second, which is the legend order.404- **The membership join.** A capture that also carries the national405 subwatersheds can say which subwatershed each of the *city's own* zones lies406 in, computed from the captured polygons at translate time and carried as407 `atlas.hydro.huc12` plus one sentence. The claim is made only when every408 sampled part of a zone agrees on one answer: a zone that straddles two says409 nothing rather than something misleading.410- **The gate: an uncurated city is not read.** The curation table is the whole411 authority — an unverified window would hang every pin on the wrong pixel, and412 unverified field names would title every zone with nothing. Unlike the other413 gates it is not a build failure: an uncurated city wraps `ErrNotReady` and the414 volume is passed over, because an archive may legitimately hold a city this415 table may not name (the operator's own), and a hard refusal would take every416 other volume down with it.417- The one lens is a basemap **rendered offline from the city's own vector data**,418 never fetched, so the pyramid is reproducible from the archived capture alone.419 Its frame declares every level complete, which is what lands the derivation on420 the translated world's own grid rather than on the shared window a city does421 not sit in, and the lens carries the drawing the deriver rasterizes it from —422 see §4.4.423- `idSpace: "derived"`: a hub's object ids are load artifacts that churn between424 refreshes, and nothing above a row is numbered at all.425426### 2.8 Adding a source427428A source is one directory, one constructor, and one line in429`internal/generate/sources/registry.go`. The walkthrough:4304311. **Declare what you read.** Put the archived capture's shape in432 `capture.go`, with only the fields the interchange document needs. A capture433 holds a great deal else, and the way to keep it out of Atlas is to not have a434 field for it. These are the only declarations in the tree permitted to carry435 your publisher's field names.4362. **Describe yourself.** `Describe() doc.Provenance` returns the registry slug a437 ledger names you by, the label a person reads, the licence and attribution the438 volume owes, and your id space. Every document you emit carries it verbatim,439 and the workbench's source card reads it.4403. **State your gates.** Before anything is read out of a capture, say what a441 capture has to be: the right kind, a named map, a declared pyramid, features442 that are actually somewhere. Refuse rather than guess. A volume that is merely443 *not crawled yet* wraps `ErrNotReady` and is skipped; anything else fails the444 build.4454. **Number things.** If your captures carry stable numeric identities, pass them446 through and declare `IDSpaceNative`. If they do not, mint them with447 `doc.NewIDSpace()` from stable names, declare `IDSpaceDerived`, and let a448 collision fail rather than rename something.4495. **Place things.** If your ground is a real planet, publish real coordinates.450 If it is a picture, measure in the world square's pixels and hand them to451 `doc.SyntheticPosition`, which inverts the reader's own projection so a452 feature lands on exactly the pixel you measured.4536. **Resolve your links.** A publisher's link syntax is the publisher's business:454 turn a deep link into a `doc.Link` where it names another feature of the same455 world, and strip every URL that survives. A bundle serves offline.4567. **Say it in the conventions.** A publisher's own render field, its declared457 projection, a feature's true coordinates — speak them once through458 `format/semconv`'s registered keys and let the field itself stop there.4598. **Register.** One line in `registry.go`, naming a constructor. If you need a460 second line there, the source is leaking.4619. **Prove it.** A translator test in your own package comparing what your462 document *means* against a committed capture — the NASA Trek reader is the463 worked example, over `testdata/corpus/translators/nasa-trek.doc.json` and464 its fixture — plus your reader's refusals, exercised the way every source465 package exercises its own.466467What you may not do: reach the network (`depcheck`'s `netconfine` rule forbids468it outside `internal/generate/crawl`), sort anything the capture ordered, read a469clock, or let your vocabulary out of your directory.470471---472473## 3. The capture archive474475`internal/generate/archive` reads it. The archive is a **source-neutral476concept** — content-addressed bytes, grouped by volume and by world, each477carrying the time it was first seen — but the archive on disk is a specific,478historical layout, kept verbatim as input because years of captured history are479data, not code.480481```482<root>/archive.json the volume register483<root>/<vol>/game.json the world register484<root>/<vol>/icons/<key>.svg|.png collection artwork485<root>/<vol>/maps/<world>/snapshots/index.json the capture index486<root>/<vol>/maps/<world>/snapshots/map/<hash>.json the capture bodies487<root>/<vol>/maps/<world>/tiles/index.json per-tile records488<root>/<vol>/maps/<world>/tiles/set-<id>/<z>/<x>/<y>.<ext>489```490491`<vol>` is the register entry's own `directory` field, not a slug this lane492computes — the register is the only thing that says where a volume sits. In the493staged archives it is `games/<title-slug>-<id>`, which is a habit of the layout494and not a rule: a caller that resolves a volume's directory any other way is495reading the disk instead of the register. The `maps/` level is the layout's word496for "the worlds of this volume"; nothing above `archive.Volume` ever sees it.497498What a caller sees is the vocabulary, never the layout: volumes, their worlds,499each world's captures, a capture's body, a volume's artwork. Two habits of the500layout are translated rather than passed on — a register entry naming no source501is MapGenie, and a capture's volume and world are recovered from the path it502sits at rather than from the record.503504**Unchanged bytes record nothing.** A capture is deduplicated by content hash505alone: a re-crawl that fetched the same thing leaves the index byte for byte as506it was, so nothing downstream moves. `capturedAt` is first-seen, never507last-verified. Times compare as strings, which is the order that means508something for RFC 3339.509510**Only the newest capture of a world is read.** Older ones are history kept on511disk.512513*(The layout a new archive would use — and whether one is worth writing — is514still an open question. Nothing new inherits this naming.)*515516### 3.1 Writing it517518`internal/generate/crawl` is the write path, and it is **the only package in519Atlas permitted to reach the network**. `depcheck`'s `netconfine` rule is what520makes that true rather than customary: no other package in the format or521pipeline lanes may import `net/http` at all, and the outbound half of it is522reported anywhere outside this package. Fetching is crawling wherever it523happens — the enrich lane's national hydrography evidence is captured here too,524and travels in the archive, so its join re-runs against the archive rather than525against a live endpoint.526527**Politeness.** One `Fetcher` per run holds a single monotonically advancing528instant under a lock. Every request of the run takes the next slot, so two529requests are never closer together than the interval (150 ms by default,530under seven a second) however many goroutines are waiting. It is not a token531bucket on purpose: a bucket lets a run that idled spend its savings in a burst,532which is the exact shape a rate limiter is watching for. A 429 or a 5xx pushes533*the schedule* forward — the origin's own `Retry-After` where it gives one,534exponential backoff otherwise — so the whole run slows rather than the one535worker that heard the refusal. Four attempts including the first; beyond that,536an origin that is refusing is refusing.537538The user-agent is a browser's string, and that is a decision. Several of these539origins answer 403 to anything else, and a 403 is indistinguishable from "never540published" in a tile pyramid, so an honest header would silently punch holes in541an archive. The politeness that matters is in the behaviour.542543**Absence is a result.** 404 and 403 both mean *not published*: a pyramid is a544rectangle and its corners are usually empty. It is recorded as `absent` so a545re-crawl does not ask again. 202 means the origin is preparing the answer, and546the caller waits. Anything else non-200 is a disagreement rather than a hiccup547and is not retried.548549**Content addressing.** A capture's body is written to a path that is its own550SHA-256, so writing it twice writes the same bytes; the index is appended to551only when that hash has never been seen. `capturedAt` is therefore **first-seen,552never last-verified**, and a re-crawl of unchanged data leaves the working tree553byte for byte as it was. That is the property everything replayable stands on:554the translators replay, the derivation stamps stand still, and a rebuild writes555the file that is already there.556557**Nothing is renamed.** A register entry is found by identity, merged in place,558and keeps its position — so a directory named once is named forever, and a field559this package has never heard of survives a run. Directory names are only ever560computed on first sight.561562**The same-slug policy.** Two publishers describe Night City. Each registers the563volume under its *plain* title, so their builds answer for one library entry and564the newest capture is the one a reader sees; each names its *directory* from a565title carrying its own prefix (`IGN Cyberpunk 2077`), so they do not fight over566one directory. And each source's archive identities carry a bit of their own —567IGN at 2³², Piggyback at 2³³, NASA Trek at 2³⁴, ArcGIS at 2³⁵ — over an FNV-1a568hash of a stable name in the low 31 bits, all held under 2⁵³ so a JSON round569trip through `float64` is exact.570571**A crawl is interruptible.** Every write is idempotent and every fetched thing572is recorded before the next is asked for, so stopping a run is a normal way to573end it. A resumed run skips what is cached and does not re-ask for what was574absent.575576### 3.2 The crawlers577578```579atlas crawl -archive DIR -source NAME TARGET [-n] [-interval D] [-concurrency N] [-max-zoom Z] [-on DATE]580atlas crawl -source list581```582583A crawler is the outward half of a source, and a separate package from its584reader on purpose: the two have opposite properties. A reader is a pure function585of an archive; a crawler is the only thing in Atlas that can fail because586somebody else's server is having a bad afternoon.587588| crawler | target | runnable here |589| --- | --- | --- |590| `ign` | `<objectSlug>/<mapSlug>` | code-complete, not run — the captures are archived |591| `nasa-blue-marble` | `earth` | runnable — one pinned image, digest-verified, fetched once (§2.4) |592593The game sources' captures are archived and their endpoints are somebody else's594editorial work, so their crawlers are kept complete and are not run against live595endpoints. The ArcGIS/USGS crawler is the one that may run, because its data is596public, and it is the one still outstanding. The renderer it would feed landed597first (§4.4), which leaves the city's archive an input rather than something598this lane can take: the proof city's archive is staged at599`crawl/bend-or/fmg-archive`, and the lane reads, draws and600composes from there. What the crawler adds is the ability to take a *new* day —601a second world in the city's version history — rather than the ability to602rebuild the one that is there.603604The IGN crawler carries this lane's most consequential gate. Some IGN wikimap605pages are MapGenie maps in an IGN frame: the page declares a MapGenie game id606and serves MapGenie's tiles. Archiving one would put a second, worse copy of607data Atlas already reads properly into the archive, where a later merge would608fold a source into itself and report a beautiful agreement between a thing and609itself. The evidence is the page's own declaration, and it exists only while the610crawler is looking at the page — which is why the refusal lives there and the611reader says plainly that it cannot check.612613---614615## 4. Tiles616617`internal/generate/tiles` reads derived pyramids and the stamps that say how618they were derived.619620A **lens** is one raster pyramid picturing a world. Deriving one — finding the621deepest complete captured level, folding it down, choosing between622nearest-neighbour and box reduction, recording which tiles exist — happens once,623into a tile set on disk. Composition reads the result.624625```626<root>/index.json the register627<root>/<pyramid>/<z>/<x>/<y>.<ext> the rasters; z, x and y are local628```629630A pyramid's register entry carries its source tile set, its zoom range631(`minZoom`, `maxZoom`, `fullZoom`, `sourceZoom`), the window it was cut from,632a format per level, its content bounds, whether it is smoothed when magnified,633the colour painted behind it, a coverage bitset per partial level, and its634derivation stamp.635636### 4.1 The derivation stamp637638Every pyramid carries a stamp over everything it was derived from: the plan's639shape, the content hash of every source tile that went into it, and a hash of640the deriving tool's own source. Composition folds it into the bundle's stamp641under the part name642643```644tiles/<pyramid>645```646647— one part per pyramid, however many tiles it holds — and **never reads a tile648to do it**. That is what makes an unchanged volume cheap to notice: a rebuild649that would write the same bytes computes the same stamp without decoding a650single raster, and the file it would write is already there under a name651carrying that stamp.652653The mechanism is one-directional on purpose. Composition trusts a pyramid's654stamp and cannot recompute it: the captured tiles it was folded down from are no655longer in front of it. A pyramid with an empty stamp stamps as empty, which is656honest — the bundle records that nothing was claimed about how those tiles came657to be.658659### 4.2 What the deriver promises660661`atlas tiles -archive DIR -output DIR` folds captured frames into pyramids.662Its input is the archive — the tiles a crawler wrote, under the paths a tile663server used — plus each lens's **frame**, which is what the source declares664about the tiles it captured: how deep the pyramid goes, in what encoding, and665which tiles each level was supposed to hold. That last question is one no amount666of looking at an archive can settle, because a level missing its last row looks667exactly like a level that never had one, and it is the only reason a frame rides668the interchange document at all. Composition never reads it.669670- **Frame discovery.** A world is a 32-tile square. Local zoom 0 is the source671 zoom whose window collapses to one tile; local coordinates are source672 coordinates less the window's first tile. No height is assumed: the frame is673 measured, so a publisher cutting the same ground from zoom 6 and one cutting it674 from zoom 13 both derive correctly. A frame whose axes come to rest on675 different tiles is refused — it would draw a map in one place and its features676 in another.677- **The complete-level rule.** The deepest level whose expected tiles are all678 present is `fullZoom` and is copied byte for byte; every shallower level is679 folded down from it, never taken from the captured intermediates. Partial680 levels above it are copied while they stay contiguous, and are what681 `maxZoom` counts.682- **Pixel art versus photographs.** A pyramid that is not interpolated reduces683 nearest-neighbour and normalizes to lossless PNG; a photographic one reduces684 by box filter and keeps its encoding.685- **Coverage bitsets.** A level that is empty or exactly full records nothing;686 otherwise a bounding box in tiles and a row-major bitset over it, least687 significant bit first, base64. An absent entry means the level is complete.688- **The stamp.** SHA-256 over: the tool's source hash; the source and asset689 paths; `fullZoom` and the deepest zoom; the preferred format and the690 interpolation flag; the content bounds where declared; the affine and target691 zoom where the pyramid is a warp; and then, per level in ascending order, the692 level number, its tile count, and each tile's `x`, `y`, content hash and693 format, sorted by `x` then `y`. Every field is NUL-terminated. The listing694 order on disk is not an input.695- **The background tile.** A level's filler is the content hash more than half696 its tiles share, found once on the level that is trusted and omitted from every697 level. Reusing one hash keeps a sparse deep level from having its handful of698 real tiles voted "background" by its own majority. The colour painted behind699 the raster is the mean of that tile's pixels, and only if the tile is flat700 within twelve levels per channel — otherwise nothing is omitted at all, because701 a hole nobody can paint over is worse than a duplicated tile.702- **Incrementality.** A pyramid whose stamp matches the register's previous703 entry, whose asset path is unchanged and whose directory still exists, is704 carried over untouched.705- **The write.** Each pyramid is derived under a temporary name and arrives by706 one rename, so a reader never sees a half-written one; pyramids the archive no707 longer offers are taken out; and the register is written last, so until the end708 it still names the stamps of what is actually on disk. A run interrupted709 anywhere leaves a register whose stamps disagree with a plan, and the next run710 derives exactly those pyramids again.711712### 4.3 What a stamp promises, and what it does not713714A derivation stamp is a **rebuild-cost** promise — *nothing that made this has715moved* — and never a content promise. It covers the deriving code's own source,716so two derivers that write byte-identical tiles stamp differently. That is the717point: changing how a level is reduced has to invalidate every pyramid, and a718stamp that watched only the archive would quietly keep serving the old719derivation.720721The consequence is that stamp identity across two derivers is impossible by722construction. What is proven instead is the tiles and the plan where they are723observable: the stamp arithmetic and the register's promises in724`internal/generate/tiles`, the drawn level held to the capture's own recorded725hashes on every derivation (§4.4), the whole pipeline run twice over one726archive writing the same bytes (`cmd/atlas/pipeline_test.go`), and the corpus727tile inventories, which carry a content and a decoded-pixel digest for every728tile of every corpus pyramid (`testdata/corpus`). `docs/stamps.md` carries729the reasoning.730731### 4.4 The drawn level732733Most pictures in the corpus arrive as tiles from somebody's tile server, and a734pyramid's deepest level is those tiles copied through. A city has no tile735server: a municipal open-data hub publishes geometry, and the offline invariant736forbids reaching for anybody else's raster. So a city's deepest level is737**drawn**, from the geometry the city publishes about itself, and every738shallower level folds down from it exactly as any other pyramid's does. Nothing739downstream of `Derive` can tell a drawn level from a copied one, which is the740design.741742`internal/generate/tiles/basemap` is the renderer, and it draws one tile from743one small vocabulary.744745- **Roles, not datasets.** A shape says what it is to the ground — `parcel`,746 `park`, `water`, `street`, `trail`, `boundary` — and nothing about where it747 came from, so no publisher's vocabulary reaches the renderer (issue #5 §5.1).748 A role the style table does not know draws nothing, rather than being given an749 invented look.750- **The z-order is the drawing's only opinion about layering:** parcel texture751 first, then ground covers, water over parks so a lake punches through one,752 streets over water so a bridge reads as a bridge, trails above streets, and753 the boundary last as the one accent colour. Within a role nothing is ordered:754 a role's shapes accumulate into a single path and the rasterizer saturates, so755 they are unioned rather than painted over one another.756- **Widths are world-true.** The style table's widths are spelled at local zoom757 6 and scale with depth, so a street keeps its width on the ground however deep758 the pyramid goes — which, folded back down, is one width on the screen. A759 shape's emphasis scales its own stroke, so an arterial is a street drawn wider760 rather than a role of its own.761- **Ground is rings; linework is capsules.** Rings clip Sutherland–Hodgman, the762 first ring ground and every ring after it a hole — a positional convention,763 because publishers do not reliably wind holes the way GeoJSON asks. Lines clip764 Liang–Barsky a segment at a time, and each surviving segment joins the path as765 its own closed capsule: the segment widened to the stroke, with a semicircular766 cap at each end. Every capsule winds the same way, so capsules overlapping at a767 vertex union into one continuous line with a round join for free. There is no768 stroker, no join table and no miter limit — it is the cheapest stroker that769 never shows a seam.770- **Eight pixels of bleed.** Both clippers cut against a window bled eight tile771 pixels past the tile's own edge, so every cut lands outside the picture and two772 neighbours drawn independently continue each other exactly. A dashed stroke773 keeps its phase across the seam too: the rhythm advances by the whole of every774 segment whether or not its middle was drawn here, and a segment whose head was775 cut off begins at the phase its lost lead had already carried.776- **Opaque truecolour PNG**, encoded at the slowest and smallest setting. A tile777 has no transparency to carry, so the alpha channel is dropped rather than778 spent on every pixel of every city; the setting is fixed rather than a knob,779 because a knob is a way for two runs of one pipeline to disagree.780781**The curation is the source's, and it is the same curation.** A dataset's role782and optional emphasis travel beside the rest of that layer's judgement in783`generate/sources/arcgishub`, unread by translation, so the drawing and the784legend cannot drift into two tables. What a source states is a `doc.Drawing` on785the lens: shapes in world pixels, projected through the very window the pins are786projected through, so the raster and the features cannot disagree about where787the ground is. It is stated *beside* the world's collections rather than derived788from them, because the two do not line up — every road centreline draws and none789of them is a legend row, only named trails are legend rows and every trail790segment draws, and emphasis varies row by row where a collection's features are791buckets of many rows.792793**The capture is the witness.** The crawl that first drew a city wrote its794rasters into the archive, and a drawn lens's plan still names their content795hashes, so the deriver holds every tile it draws against what the capture796recorded and refuses the derivation by name if the two disagree. That is a797stronger thing to check than a suite could: not "these bytes look right" but798"these bytes are what a different implementation made of the same vectors",799checked on every derivation rather than in a test that could be skipped. It also800means a drawn level's plan — and therefore its stamp — is the ordinary one, with801the level's tiles listed and hashed like any other level's.802803**And the renderer is part of the tool.** A derivation stamp covers the deriving804code because a change to how a level is made has to invalidate that level; a805city's deepest level is now made here, so `basemap`'s sources sit inside the806`ToolStamp` embed beside `plan.go`, `stamp.go`, `derive.go` and `draw.go`.807808### 4.5 Warped variants809810When two sources picture one ground, one raster is usually the finer — a811publisher's own map beside a wiki's rasterized in-game rendering — and a registry812that simply served the finer one would throw the other away. Instead the lesser813raster is **resampled into the finer one's world** and offered as one more814picture of the same ground. Nothing is discarded, both rasters answer to one815grid, and every feature lands on either.816817Three decisions make a warp, and they happen in this order.818819**Names are settled first.** Two sources capturing one volume name the same820ground the same thing, so their pyramids would land in one directory.821`tiles.Settle` gives *every* colliding plan its publisher's own path as a822suffix — all of them, never just the later one — so which pyramid is called what823does not depend on the order an archive listed its captures in. Cyberpunk's two824readings settle as `cyberpunk-2077__night-city__cbp` and825`cyberpunk-2077__night-city__cyberpunk-2077-night-city`. A warped variant is826named after the picture it aligns onto, so it can only be planned once the names827have stopped moving.828829**The pairing is decided from the whole set.** One reading per ground — the830deepest, since a world's other lenses are alternate art already sharing its831window — and, among the readings of one volume, the one that draws its world at832the most pixels is the base every other is brought into. Resampling into a833coarser world would throw away detail that was captured. Only readings from834*different* sources are ever paired: a source that divided its own world into835several grounds did so deliberately. This policy lives in `cmd/atlas/warp.go`.836837**The alignment is an input, not a derivation.** `tiles.PlanWarp` is handed the838fitted affine as six numbers. Fitting one stands on the names two readings share839and belongs to `internal/enrich/align` — the same fit the cross-source merge840folds features by, so a raster and the features drawn on it can never disagree841about where the ground is. Neither lane imports the other; `cmd/atlas` holds842both, and the seam is the one `adapt.go` opens for documents and volumes. Nothing843is duplicated to arrange it: the deriver needs a fitted transformation, not the844ability to fit one, so only the arithmetic of *using* one — apply, invert,845scale — sits beside `tiles.Affine`.846847What the deriver then does:848849- **The target zoom** is the base-frame zoom nearest the donor's real resolution850 after the alignment's own scaling, clamped to the base's deepest complete851 level: deep enough to keep what the donor drew, never so deep as to invent852 detail that was never there.853- **The deepest level is rendered, not copied.** Every pixel of every tile in the854 window the warped content falls in asks the inverse transformation where in the855 donor it came from, and samples the donor's deepest complete level bilinearly,856 clamped inside the tile that holds it. A tile no donor pixel reached is not857 written at all: coverage says nothing about it, and a reader falls back to the858 parent.859- **Everything shallower folds down** from that level exactly as any other860 pyramid folds, as a photograph.861- **The donor's own background** paints wherever the donor never drew, so a862 warped tile is opaque and a picture that simply ends does not end in black.863- **The bounds are the warped content's**, measured through the alignment and864 clipped to the world square — reported even when they come to the whole square,865 because what the alignment reached is a fact about this picture rather than a866 default that can be left out.867- **The plan lists one captured level**, the donor's deepest complete one, which868 is what the warp samples: a stamp naming the donor's other levels would rebuild869 for a change that could not reach these tiles.870- **The stamp carries the alignment** — the base's tile set, the target zoom, the871 base frame, and the six coefficients to nine decimal places — because the same872 donor through a different transformation is a different picture.873874`cmd/atlas/warp_test.go` holds the pairing policy — the finer picture is the875frame, a source is never warped onto itself, readings that do not align stay876apart, and names settle before anything is derived — and the resampling877itself is the deriver's ordinary path, judged the way §4.3 says everything878else is.879880---881882## 5. Composition883884`internal/generate/compose` turns a document into a volume.8858861. **Resolve.** Each world's lenses are found in the tile set, its collections887 numbered, and the ground its contents cover measured. Every lens of a world888 must agree on the window it is cut from; one that does not would be a world889 drawn in two places at once. A world in a window that is not the shared one890 carries a `grid` in its payload.8912. **Order.** Worlds sort as families: a piece of a split sheet carries its892 sheet's position and follows it. Curation decides which world leads and893 whether a volume's worlds read forward or backward.8943. **Artwork.** Every point collection's icon key resolves against the895 document's icon set. Artwork nothing names is left behind.8964. **Conventions.** Composition speaks what it knows on the volume's behalf and897 then holds the whole thing to the registry, **producer-strict**: an898 unregistered `atlas.*` key, a value outside its vocabulary, or a key on the899 wrong entity fails the build here, one step before a bundle.9005. **Pack, stamp, write, install.**901902### 5.1 What composition speaks, and what a source speaks903904| key | written by | when |905| --- | --- | --- |906| `atlas.geometry.kind` | composition | mirrors the collection's kind, unless declared |907| `atlas.icon.kind` | composition | once the artwork has actually been resolved |908| `atlas.icon.outset` | composition | from curation |909| `atlas.collection.key` | composition | from curation, unless declared |910| `atlas.render.as` | the source | it is a reading of a publisher's own field |911| `atlas.geometry.*`, `atlas.geo.*` | the source | only a source knows what its ground is |912| `atlas.icon.std` | the source | naming a glyph is all a source without artwork can do |913914Nothing composition writes overwrites what a source said.915916**Standard icons are named by a source and resolved by composition.** A source917with no artwork of its own — a gazetteer that names two thousand craters and918draws none of them, a city's open data — names a library glyph through919`atlas.icon.std` instead of inventing one. Composition resolves the name against920the vendored library in `internal/generate/icons` and packs the glyph under a921provenance-spelling asset name (`std--maki-mountain.svg`), so a source's own922drawing of a thing can never be shadowed by a generic one and a bundle's icon923listing reads honestly. A declaration the library cannot answer fails the build.924925Resolving a name is not choosing one. Deciding that a collection which named926nothing should carry a standard glyph — and which — is a judgement made after two927sources have been folded together, and it belongs to the enrich lane's928`stdicons` enricher (issue #5 §5.3). The two meet at the attribute.929930### 5.2 Splitting931932A sheet that holds several separate places is **declared in curation, never933detected**: guessing wrong divides a whole map into pieces nobody asked for.934Two modes: `worlds` gives each piece its own entry in the picker, `lenses`935keeps one world and offers the pieces one at a time.936937The splitter reads only the interchange document's own vocabulary, so a second938source describing the same sheet the same way splits the same way. A **piece**939is one top-level area — an area feature nothing is the parent of — together with940its descendants and the point features that name one of them as their member.9419421. **Plan.** Every area walks up its parent chain to its root. A piece's extent943 is the ground its areas outline, projected into world pixels; the points944 assigned to it do not stretch it. A point belonging to no area fails the945 build, because it would vanish when the sheet came apart.9462. **Grow.** Each extent widens by up to 256 world pixels into the empty space947 around it, stopping halfway to whatever it would run into, so a title printed948 beside a piece travels with the piece it names. Two pieces that grew into the949 same diagonal corner are eased apart along whichever axis they overlap least,950 and neither is ever pushed back inside the ground it grew from.9513. **Rehome.** A point whose position lies outside the piece that claims it, and952 inside exactly one other, moves. Where it sits is not in doubt; the claim is953 treated as the mistake it is. An ambiguous point stays where it was claimed.9544. **Order.** Lenses read top to bottom, so the sky comes before the ground955 beneath it. Separate worlds lead with the largest, so a sheet's main map heads956 its insets.9575. **Cut.** Into worlds: the first piece keeps the sheet's identity and every958 other becomes `<sheet title> — <area title>` under `<sheet slug>-<slug of the959 area title>`, carrying `parent`. Into lenses: one lens per piece, named after960 its area, and every feature carries the piece it belongs to as its `shard`. In961 both modes the piece's own outline is dropped — once an area has been used to962 cut a world out of a sheet, drawing it again just traces the edge of what the963 reader is already looking at — and a point collection left empty goes with it.964965The lenses of every piece draw from the same pyramid; only the window onto it966changes. A piece's `bounds` is the grown window and its `surface` is the ground967it actually covers, clipped to that window, because those are not the same968rectangle and anything measuring a world rather than drawing it wants the969ground.970971### 5.3 The semantic volume972973Composition writes one semantic graph: Volume → Worlds → CoordinateSpace,974FeatureSets/Features, RasterPyramids and Presentation, plus volume Assets.975Feature geometry is converted into the declared coordinate space before the976file is compiled. Properties are typed fields; membership and links are977Relationships; source identity and capture time are Provenance records.978979`vnext.Compile` lowers that graph into schema-described typed tables. Feature980identity is a durable string and does not depend on a collection ordinal.981Presentation points at semantic identities, so a new layer/style/legend can be982applied without rewriting feature meaning.983984### 5.4 Stamping and writing985986The stamp is derived by the vNext writer from the canonical bootstrap: release987metadata, canonical schema identity, every typed table block and every opaque988blob identity. Callers cannot provide or retain a stale stamp.989990`createdAt` is the **newest capture time across the volume's worlds**, never991the build clock. The name is `<slug>-<YYYYMMDD>-<stamp12>.atlas`. Building the992same archive anywhere, at any time, yields the same version and the same file993name, which is the whole reason a directory of these files can stand as a994registry — and why a build already present is left untouched.995996The write: the archive is built under a temporary name that does not end in the997bundle extension, so a reader scanning the registry mid-write passes over it;998it is renamed into place, so the name appears whole or not at all; and then it999is reopened from disk and validated, because the file that will serve is the1000copy and it is the copy's promises that matter.10011002Entry order inside the zip is deterministic: the bootstrap first, canonical1003schema, typed `data/*.pack` tables, then opaque blobs. Table blocks, raster1004tiles and content-addressed assets are stored so readers can map or range-read1005them without inflating a document.10061007### 5.5 The policy revision10081009`compose.PolicyRevision` orders builds of the same capture: the data has not1010moved between them, but what the lane makes of a capture has. Among equal1011captures a registry serves the highest revision. It is part of the stamp, so1012bumping it supersedes the builds already in every library. Its changelog is in1013the constant's doc comment.10141015---10161017## 6. Curation10181019`internal/generate/curation/curation.json`, embedded so a build carries its own1020curation and a composed bundle cannot depend on what was on the operator's1021disk. Every key is spelled in Atlas's vocabulary — a volume slug, or1022`"<volume>/<world>"` — never in a source's identifiers.10231024```jsonc1025{1026 "schema": 1,1027 "window": { "sourceZoom": 13, "firstTile": 4064 },1028 "worldOrder": {1029 "preferred": { "<volume>": ["<world>", …] },1030 "newestFirst": { "volumes": ["<volume>", …] }1031 },1032 "iconOutset": {1033 "byVolume": { "<volume>": "light" | "dark" },1034 "byWorld": { "<volume>/<world>": "light" | "dark" }1035 },1036 "shard": {1037 "worlds": { "<volume>/<world>": "worlds" | "lenses" }1038 },1039 "collectionEquivalents": {1040 "<volume>": { "<icon key>": "<shared name>" }1041 }1042}1043```10441045Every section carries its reasoning beside its data, under keys the reader1046ignores (`what`, `why`, `byVolumeWhy`). A value that is not the shape a section1047expects is commentary, not an entry.10481049| table | what it decides |1050| --- | --- |1051| `window` | the source tile window a world is cut from unless it declares one of its own. A property of the corpus, not of the format. |1052| `worldOrder.preferred` | a volume's primary world, ahead of its secondary areas. Unlisted worlds follow in title order. |1053| `worldOrder.newestFirst` | volumes whose worlds are dated captures of one ground: a version history opens on the present. |1054| `iconOutset` | which rim a world's markers wear against its art. The raster decides which outline reads, so it is declared. A world's entry wins over its volume's. |1055| `shard` | the sheets that hold several separate places, and what to do with them. |1056| `collectionEquivalents` | where two sources spell one concept differently, keyed by artwork key. Composition writes the shared name as `atlas.collection.key`, so the payload carries the merge identity and a later merge reads only the attribute. |10571058Every table is keyed in Atlas's vocabulary — including outsets and shard1059declarations, keyed by volume and world slugs rather than by a publisher's1060numeric map ids, so a curation entry is readable without the publisher's1061database in front of you and movable to a second source describing the same1062ground.10631064---10651066## 7. The CLI10671068```1069atlas crawl -archive DIR -source NAME TARGET [-n]1070atlas tiles -archive DIR -output DIR [-force] [volume…]1071atlas compose -archive DIR -tiles INDEX [-bundles DIR] [-n] [volume…]1072atlas translate -archive DIR [-volume SLUG] [-artwork] [-list]1073```10741075`crawl` is the network-touching, hand-run step: §3.2. `tiles` folds captured1076frames into the pyramids composition packs, carrying over every pyramid whose1077captures have not moved.10781079`compose` builds every volume the archive holds a registered source for, or1080only the ones named. `-n` composes and stamps without writing. With no1081`-bundles`, it installs into the application's own library.10821083`translate` prints an interchange document to stdout — the debugging window1084into the first half of the lane, answering the question composition cannot:1085what did the source actually make of these bytes? `-list` names every volume1086the archive registers, as `source · title · directory`; the slug `-volume`1087takes is the one the run logs as `volume=`.10881089Both write their event stream to stderr, so piped stdout stays clean. See1090[logging.md](logging.md).10911092### 7.1 Where the inputs are10931094Neither the capture archives nor the derived tile sets are in git — they are1095large, they are somebody else's bytes, and they are staged in the working copy1096instead. The repository's own copies are gitignored and are what every default1097points at:10981099| input | staged at | override |1100| --- | --- | --- |1101| the games corpus archive | `crawl/fmg-archive` | `ATLAS_ARCHIVE_DIR` |1102| the proof city's archive | `crawl/bend-or/fmg-archive` | `ATLAS_CITY_ARCHIVE_DIR` |1103| the included Earth's archive | `crawl/blue-marble/fmg-archive` | none — `make included-earth` names it |1104| the derived tile sets | `tiles/`, register at `tiles/index.json` | `ATLAS_TILES_INDEX` |11051106So the whole lane, over one volume, is three commands:11071108```sh1109atlas translate -archive crawl/fmg-archive -volume tunic # look at it1110atlas compose -archive crawl/fmg-archive -tiles tiles/index.json \1111 -bundles <a scratch registry> tunic # build it1112```11131114Without `-bundles` the build installs into the application's own library, which1115is a thing to do deliberately rather than while reading a document.11161117`atlas tiles -archive crawl/fmg-archive -output tiles` re-derives the pyramids1118into the staged set, and is only needed when the archive has moved: a pyramid1119whose captures have not moved is carried over untouched (§4.2).11201121**Deriving into a *fresh* output directory does not reproduce the staged set's1122stamps.** A derivation stamp covers the deriving tool's own source (§4.3), so1123re-deriving the same tiles under a different build of the tool stamps1124differently — which changes the bundle's stamp, and therefore its file name.1125The tiles are identical; the accounting of how they were made is not.1126Composing against the staged `tiles/index.json` reproduces the builds already1127in circulation name for name; composing against a freshly derived index is a1128correct build of the same volume under a different name. `docs/stamps.md`1129carries the reasoning.11301131---11321133## 8. What is proven11341135How the lane is judged overall is [testing.md](testing.md); every suite below1136runs under `make test`, hermetically, over stated inputs and the committed1137corpus.11381139| claim | held by |1140| --- | --- |1141| a source reads what its publisher actually serves | `internal/generate/sources/nasatrek`, over the committed capture at `testdata/corpus/translators/`; every other source over stated captures of its own shape, refusals included |1142| the city translates exactly — the window, the buckets, the membership join | `internal/generate/sources/arcgishub` (`TestCityTranslatesExactly`) |1143| the drawn level is deterministic and honest about its edges | `internal/generate/tiles/basemap`: same shapes, same bytes; holes, bleed, dash phase, opaque truecolour |1144| the whole pipeline writes the same bytes twice | `cmd/atlas/pipeline_test.go` (`TestPipelineWritesTheSameBytesTwice`): a synthetic archive — two readings of an invented game and one crawl day of the proof city — run through the shipped stages, twice, byte-identical |1145| a drawn tile is held to its witness | `cmd/atlas/pipeline_test.go` (`TestADrawnTileIsHeldToItsWitness`) — the §4.4 refusal, exercised |1146| the warp pairing policy (§4.5) | `cmd/atlas/warp_test.go` |1147| splitting, ordering, conventions, identifiers | `internal/generate/compose` |1148| what a composed bundle holds, tile for tile | the corpus extractions (`testdata/corpus/bundles/`), read by `format/bundle`'s corpus tests and the render lane's pyramid tests |11491150The staged capture archives and tile sets (§7.1) are maintainer inputs, not1151test inputs: no required test reads them, and `make corpus-smoke` is the1152maintainer's own deep check over a real installed library1153([testing.md](testing.md)).