1# The generate lane23Generate makes volumes. It takes a capture archive — bytes somebody else4published, fetched once and kept — and writes a `.atlas` file: one volume,5complete, offline, deterministically named.67The lane has four steps and one rule.89```10capture ──▶ translate ──▶ [tiles] ──▶ compose ──▶ a volume in a registry11 network per source rasters conventions, packing, stamping12 hand-run read-time derived producer-strict13```1415**The rule: no data source's shape, vocabulary, or name appears outside16`internal/generate/sources/<name>/`.** Everything downstream of a translator17speaks Atlas's own vocabulary and cannot tell which source it read. Only a18merge ledger ever names a source, and merging is not this lane's work.1920Generate is **single-source**. What one source said travels through21untouched; folding two sources' readings of one volume together belongs to the22[enrich lane](enrich.md), and the composed multi-source result is23`generate ⊕ enrich`.2425This document is normative for the interchange document's schema and for the26curation data files. It is the prose twin of `internal/generate/doc` and27`internal/generate/curation`.2829---3031## 1. The interchange document3233`internal/generate/doc` defines the one thing that crosses the middle of the34lane. It is **Atlas's schema, designed backwards from what composition needs**35— worlds, lenses, collections, features, geometry, attributes, provenance —36and no source's shape is privileged in it.3738A document is produced and consumed inside one run. It is not an archive39format and nothing is expected to read one back years later; it exists so that40the two halves of the lane can be reasoned about, tested, and printed41separately. `atlas translate` prints one.4243### 1.1 The shape4445```jsonc46{47 "doc": "atlas-generate-doc",48 "version": 1,49 "volume": { "slug": "tunic", "title": "TUNIC" },50 "source": {51 "name": "mapgenie", // the slug a ledger names it by52 "label": "MapGenie", // the same source, spelled for a person53 "license": "", // what the volume owes, where it is known54 "attribution": "…",55 "idSpace": "native" // "native" | "derived"56 },57 "worlds": [ … ],58 "icons": [ { "key": "fox_shrine", "file": "fox_shrine.svg", "data": "<base64>" } ]59}60```6162A **world**:6364```jsonc65{66 "id": 427,67 "slug": "world",68 "title": "World",69 "center": { "lat": 0.787…, "lng": -0.771… },70 "capture": {71 "kind": "map", // the source's own name for these bytes72 "id": 427, // the capture's id in the source's id space73 "locator": "/api/v1/maps/427/full",74 "contentHash": "ff58c59a…", // SHA-256 of the archived bytes75 "capturedAt": "2026-07-30T03:57:41.529Z"76 },77 "lenses": [ {78 "name": "Default",79 "tileSet": "tunic/world/default-v2",80 "frame": { // the deriver reads this; composition never does81 "minZoom": 9, "maxZoom": 15, "format": "jpg",82 "windows": { "15": { "minX": 16256, "minY": 16256, "maxX": 16383, "maxY": 16383 } }83 }84 } ],85 "collections": [ … ],86 "attrs": { } // the world speaking the conventions87}88```8990A **collection** and its **features**:9192```jsonc93{94 "id": 5984, // zero asks composition to derive one from key95 "key": "regions", // merge identity; absent, the icon key stands in96 "title": "Fox Shrine",97 "group": "Locations", // a legend heading, and only that98 "kind": "point", // point | path | area99 "icon": "fox_shrine", // names artwork in the document's icon set100 "color": "#6984F2",101 "iconColor": "#6984F2",102 "visible": true,103 "attrs": { "atlas.render.as": "pin" },104 "features": [105 {106 "id": 177474,107 "title": "Fox Shrine",108 "subtitle": "",109 "description": "",110 "at": { "lat": 0.804…, "lng": -0.737… }, // points111 "geometry": [ { "type": "Polygon", "coordinates": … } ], // paths and areas112 "center": { "lat": …, "lng": … },113 "member": 0, // the area feature whose ground this stands on114 "parent": 0, // the area feature this area sits inside115 "links": [ { "title": "the well", "feature": 200 } ],116 "attrs": { }117 }118 ]119}120```121122### 1.2 The decisions123124The schema is source-neutral by design125([decision 4](decisions/0004-source-neutral-interchange-document.md) carries126the context), and its load-bearing choices are these:127128- **One ordered `collections` array.** Points, paths and areas are the same129 kind of citizen; a group is a heading string on a collection, never a130 container.131- **Extensions are ordinary fields.** `attrs` speaks the conventions, and a132 collection is declared like anything else.133- **Render policy travels once.** A publisher's own render field is spoken as134 `atlas.render.as` by the source, and the field itself never travels.135- **Coordinates are `{lat, lng}` floats.** A source's tolerance for other136 spellings is the source's business.137- **Absence is `0`, everywhere** — matching the wire's own reading of zero138 (§1.4).139- **Artwork travels in the document**, never as a key implying a file probe at140 composition time.141- **Lens detail splits in two.** Name and tile set are for composition; the142 publisher's claimed frame is carried separately, for the deriver alone,143 because only a source can say which tiles a complete level was supposed to144 hold.145- **Provenance rides the document** — `source`, and a per-world `capture` —146 rather than being recovered from directory names.147- **Links are resolved by the source**, because a link syntax is a148 publisher's.149- **The world window is curation's** to name where it is shared; a pyramid150 names its own.151152Two structural choices frame all of that: the collection/feature model — a153collection has a key, a title, a legend heading, a kind, visibility,154attributes and features; a feature has an identity, a place or a geometry,155nesting, prose, links and attributes — and translation at **read time**, over156an archive of source-native bytes, so an editorial change replays over years157of captures instead of re-crawling them.158159### 1.3 Coordinates160161Positions are latitude and longitude **in the volume's own projection** — the162same world space `format/bundle.Coordinate` carries and the same one a lens's163tile pyramid is cut in. For a game map that space is synthetic: an image was164encoded as a slippy map long ago and the numbers are pixels wearing degrees.165For a planet or a city it is the real thing.166167A source publishes what its ground means; composition does no reprojection.168Composition projects to world pixels in exactly one place — measuring the169ground a world's contents cover — using spherical Mercator against the world's170tile window.171172### 1.4 Identifiers173174Feature and collection identifiers are `int64` in the document and **signed17532-bit on the wire, where zero reads as absence**.176177- A source with native numeric identities passes them through and declares178 `idSpace: "native"`.179- A source without them derives stable identifiers from stable names and180 declares `idSpace: "derived"`. Derivation must be a pure function of a181 name, and a collision must fail rather than rename something.182- A collection may carry a `key` and no `id`; composition derives one, as183 FNV-1a over `"<world id>:collection:<key>"`, masked to the positive range of184 an `int32`, with zero moved to one, refusing a collision with a number the185 source already claimed.186187**One id space per world.** The text payload is keyed by feature id across188every collection and every kind, so two features sharing a number would lose189one of their descriptions. `Document.Validate` refuses it.190191### 1.5 Determinism192193Every ordering in a document comes from the capture. Maps are the only194unordered thing in it and JSON sorts their keys. A source that sorts is195throwing away an editorial order somebody chose.196197---198199## 2. Sources200201A source is one publisher's reader. The interface is three lines:202203```go204type Source interface {205 Describe() doc.Provenance206 Translate(a *archive.Archive, v archive.VolumeRef, log *slog.Logger) (doc.Document, error)207}208```209210`internal/generate/sources/registry.go` is the assembly point and **the one211file outside a source's own directory permitted to name a source**. It may name212only a constructor. If adding a source needs a second line there, the source is213leaking.214215### 2.1 The rules a source keeps216217- **No network.** Capture is a separate, hand-run step. A translator is a pure218 function of bytes already on disk, which `depcheck`'s `netconfine` rule219 enforces: outbound HTTP lives in `internal/generate/crawl` and nowhere else.220- **Refusal over guessing.** A source states its structural preconditions and221 refuses what does not meet them. A volume that is merely *not crawled yet*222 wraps `ErrNotReady` and is skipped; anything else fails the build.223- **Gates are named.** IGN refuses embedded-MapGenie maps, Piggyback refuses224 unverified transforms, ArcGIS refuses an uncurated city225 ([decision 16](decisions/0016-uncurated-captures-are-passed-over.md)). The226 MapGenie reader refuses a capture whose227 kind is not its own, because reading another source's bytes through it would228 produce a document that lies about where it came from.229- **Offline purity starts here.** A publisher's links are resolved or removed230 by its own reader. Nothing downstream should have to know a link syntax.231- **Determinism.** Same archived bytes, same document, on any machine.232233### 2.2 The MapGenie reader234235`internal/generate/sources/mapgenie` is the worked example and the only source236in the tree today.237238- Capture kind `map`: one map's whole API response, archived verbatim.239- Every `group × category` becomes a point collection, in capture order. The240 group survives as a heading string.241- A category's colour resolves over its group's, normalized to `#RRGGBB` upper242 case.243- Coordinates arrive as JSON numbers on some maps and quoted strings on others;244 both are read.245- Regions become an area collection keyed `regions`, titled `Regions`,246 numbered by composition. A region whose geometry all came through empty is247 dropped. A line among the regions is refused: MapGenie's regions are ground,248 and a line would be a path collection somebody meant to declare.249- `display_type` is spoken once as `atlas.render.as` and never travels.250- Markdown links are replaced by their labels; a link carrying251 `locationIds=<n>` that names another pin of the same world survives as a252 cross-reference; every surviving URL is stripped.253- Artwork is read once per key, `.svg` then `.png`, and carried in the254 document.255256### 2.3 The NASA Trek reader257258`internal/generate/sources/nasatrek` reads a planetary volume. A capture marries259two publications that know nothing of each other: a global equirectangular260mosaic from NASA's Trek tile services, and the IAU Gazetteer of Planetary261Nomenclature's feature list for the same body.262263- Capture kind `trek-map`, one per body, carrying every mosaic captured for it.264- The coordinate design is the whole trick. A Trek mosaic is two tiles wide and265 one tall at its own zoom zero, so a Trek level sits one zoom up in the square266 the corpus cuts and the planet fills the top half of the world square. A267 feature's planetary coordinates become a pixel of that image — longitude across268 the full width, latitude down the top half — and the pixel becomes a synthetic269 position. The projection's distortion cancels exactly, because the raster and270 the features ride one mapping.271- The world declares the flattening as conventions (`atlas.geometry.surface`,272 `.projection`, `.equirect.px`, `.equirect.deg`, `.body`), so a reader can run a273 packed position backward and stand on the planet, and every feature carries the274 coordinates the Gazetteer published verbatim as `atlas.geo.lat`/`.lon`.275- One collection per Gazetteer feature type, sorted, all under the heading276 `Nomenclature`. The type descriptor `"Crater, craters"` keeps its singular half277 as the title and lends its slug as the artwork key.278- The Gazetteer has no artwork, so each collection names a library glyph through279 `atlas.icon.std`, chosen from the IAU's own type codes — a *mons* is a mountain,280 a *patera* a volcano, a *palus* literally a marsh — falling back to shape281 language where no glyph says the thing.282- `idSpace: "derived"`: nothing in either publication numbers a mosaic or a283 feature type, so every identity is minted from a stable name.284285### 2.4 The Blue Marble reader286287`internal/generate/sources/nasabluemarble` reads the included Earth volume's288one capture: NASA Earth Observatory's Blue Marble Next Generation base map289married to Natural Earth's 1:110m cultural vectors — pinned publications290rather than live endpoints.291292- Capture kind `blue-marble-map`, one per capture of the pinned files.293- One world (`earth`), one lens (`Blue Marble`), and the lightest cut of294 ground truth: the primary capitals as a point collection per continent,295 each pin a member of the country it stands in, and the country borders as296 one quiet area collection. Continents are Natural Earth's own filing; a297 capital whose microstate the 1:110m borders leave out takes the nearest298 drawn ground's continent, computed from the captured polygons at translate299 time the way the city source computes its watershed join. Everything else300 is later sources' work, folded in by the enrich lane.301- The coordinate design is the shared whole-sphere window NASA Trek also rides302 (`doc.EquirectPx` and friends): a 2:1 global image filling the top half of303 the world square, declared as registered conventions — surface, projection,304 the equirect px/deg pair, the body, and the body's mean radius — so the cell305 systems recognise the ground through the same declarations every spherical306 world makes.307- The capture body records the product's identity — its title, NASA's credit308 line, the source digest and size — and the derivation policy its tiles were309 cut under, so a policy change reads as a new capture rather than the same310 one wearing new bytes.311- The gates: another kind's capture, another source's name, another body, a312 missing digest, a missing size, a capture declaring no pyramid or carrying313 no features, a country with no identity, or a capital off the planet are314 refused by name.315- `idSpace: "derived"`: nothing in the publication numbers a base map.316317The crawler half (`internal/generate/crawl/bluemarble.go`) is the one place318the pinned URLs, digests and capture time are spelled. It fetches each pinned319file exactly once into the staged archive, refuses any digest but the pinned320one, cuts the pyramid's deepest level through the deterministic fixed-point321Catmull-Rom resampler beside it, distills the Natural Earth files into the322capture body — names, codes, continents, label points and rings, coordinates323verbatim — and records the capture under the pinned time, so the archive — and324everything stamped downstream of it — reproduces on any machine. `included/README.md` is the recipe and the provenance, and325`make included-earth` is the whole run.326327### 2.5 The IGN reader328329`internal/generate/sources/ign` reads a community wikimap: a flat image tiled330like a world, markers placed on it in image-relative coordinates, and a flat list331of marker types that names a parent to make two levels of legend.332333- Capture kind `ign-map`.334- A wikimap's image is normalized so its taller dimension spans 1. A marker's335 latitude runs down that span and is negative, its longitude across it, so336 `(lng, -lat)` times the world square's edge is the pixel it is drawn on.337- One collection per marker type, types in slug order, gathered under the heading338 of whichever parent each names, headings in the order their first type appears.339 A type no marker uses is left out — an empty collection would only dim the340 legend — but it stays in the capture, so a marker appearing under it later341 revives the collection without a policy change.342- Artwork is a PNG sprite per type slug, read from the archive and carried.343- **The gate: an embedded MapGenie map is not an IGN map.** Some IGN wikimap344 pages are MapGenie maps in an IGN frame — the page declares a MapGenie game id345 and serves MapGenie's tiles. Capturing one here would archive a second, worse346 copy of data Atlas already reads properly, and a merge would then fold a source347 into itself. The refusal lives in `internal/generate/crawl`, where the page's348 own declaration is still in front of the crawler; by the time a capture exists349 the evidence has been thrown away.350- `idSpace: "derived"`: markers are opaque strings and types are slugs.351352### 2.6 The Piggyback reader353354`internal/generate/sources/piggyback` reads the official guide house's maps,355which carry what a community wikimap does not: prose. Pins arrive with names and356descriptions and both survive into a volume.357358- Capture kind `piggyback-map`.359- Piggyback draws in a game's own coordinates on a Leaflet `CRS.Simple` map: a360 linear transformation squeezes them onto the unit tile at zoom zero. A pin361 passes through it and then through the shared inverse Mercator.362- A declared category becomes a heading and its types become collections under363 it, ordered by declared position then key. A type no pin uses is left out.364- District name pins arrive filed under the reader-state `favorites` category,365 which is nothing to build a legend from. They gather under their own heading366 and render as `text`, which is what they are on Piggyback's own map. Any *other*367 undeclared type fails the build: better a loud build than a pin silently368 dropped.369- Piggyback publishes no bounds, so the crawler's own survey of which tiles370 answered is the only account of where the pyramid is drawn, and a capture with371 no observed level is refused.372- **The gate: an unverified transform is refused.** The transformation is read373 off the page's own scripts, it decides where every pin in the volume stands,374 and a wrong one puts a whole map's contents somewhere plausible and wrong —375 exactly the failure a later merge would try to fit an affine to.376 `verifiedTransforms` is the list of games whose numbers have been checked377 against the published map; a capture whose numbers are not in it fails.378- `idSpace: "derived"`: pins, categories and types are opaque string ids.379380### 2.7 The ArcGIS Hub reader381382`internal/generate/sources/arcgishub` reads a city. It is the only source whose383subject is a real place, and the only one where **a world is a date**: each crawl384day of a city's open-data hub registers its own world, so a volume's world picker385is the city's version history and the difference between two worlds is a386difference in the city.387388- Capture kind `arcgis-map`.389- A city sits off the Web-Mercator diagonal, so its curated bounding box is390 padded five percent per side and squared in Mercator's own terms; that square391 is the world. Features project through the Mercator forward onto it and out392 again through the shared synthetic inverse, and the world declares the mapping393 as `atlas.geometry.mercator.px`/`.deg`, so a reader can invert it and stand on394 the ground. Every pin also keeps the coordinates the city published, verbatim.395- Polygon and line datasets become shape collections — one collection per396 dataset, one feature per curated bucket, holding every row that fell into it.397 Point datasets become pins under their curated heading. Pins are emitted first398 and ground second, which is the legend order.399- **The membership join.** A capture that also carries the national400 subwatersheds can say which subwatershed each of the *city's own* zones lies401 in, computed from the captured polygons at translate time and carried as402 `atlas.hydro.huc12` plus one sentence. The claim is made only when every403 sampled part of a zone agrees on one answer: a zone that straddles two says404 nothing rather than something misleading.405- **The gate: an uncurated city is not read.** The curation table is the whole406 authority — an unverified window would hang every pin on the wrong pixel, and407 unverified field names would title every zone with nothing. Unlike the other408 gates it is not a build failure: an uncurated city wraps `ErrNotReady` and the409 volume is passed over, because an archive may legitimately hold a city this410 table may not name (the operator's own), and a hard refusal would take every411 other volume down with it.412- The one lens is a basemap **rendered offline from the city's own vector data**,413 never fetched, so the pyramid is reproducible from the archived capture alone.414 Its frame declares every level complete, which is what lands the derivation on415 the translated world's own grid rather than on the shared window a city does416 not sit in, and the lens carries the drawing the deriver rasterizes it from —417 see §4.4.418- `idSpace: "derived"`: a hub's object ids are load artifacts that churn between419 refreshes, and nothing above a row is numbered at all.420421### 2.8 Adding a source422423A source is one directory, one constructor, and one line in424`internal/generate/sources/registry.go`. The walkthrough:4254261. **Declare what you read.** Put the archived capture's shape in427 `capture.go`, with only the fields the interchange document needs. A capture428 holds a great deal else, and the way to keep it out of Atlas is to not have a429 field for it. These are the only declarations in the tree permitted to carry430 your publisher's field names.4312. **Describe yourself.** `Describe() doc.Provenance` returns the registry slug a432 ledger names you by, the label a person reads, the licence and attribution the433 volume owes, and your id space. Every document you emit carries it verbatim,434 and the workbench's source card reads it.4353. **State your gates.** Before anything is read out of a capture, say what a436 capture has to be: the right kind, a named map, a declared pyramid, features437 that are actually somewhere. Refuse rather than guess. A volume that is merely438 *not crawled yet* wraps `ErrNotReady` and is skipped; anything else fails the439 build.4404. **Number things.** If your captures carry stable numeric identities, pass them441 through and declare `IDSpaceNative`. If they do not, mint them with442 `doc.NewIDSpace()` from stable names, declare `IDSpaceDerived`, and let a443 collision fail rather than rename something.4445. **Place things.** If your ground is a real planet, publish real coordinates.445 If it is a picture, measure in the world square's pixels and hand them to446 `doc.SyntheticPosition`, which inverts the reader's own projection so a447 feature lands on exactly the pixel you measured.4486. **Resolve your links.** A publisher's link syntax is the publisher's business:449 turn a deep link into a `doc.Link` where it names another feature of the same450 world, and strip every URL that survives. A bundle serves offline.4517. **Say it in the conventions.** A publisher's own render field, its declared452 projection, a feature's true coordinates — speak them once through453 `format/semconv`'s registered keys and let the field itself stop there.4548. **Register.** One line in `registry.go`, naming a constructor. If you need a455 second line there, the source is leaking.4569. **Prove it.** A translator test in your own package comparing what your457 document *means* against a committed capture — the NASA Trek reader is the458 worked example, over `testdata/corpus/translators/nasa-trek.doc.json` and459 its fixture — plus your reader's refusals, exercised the way every source460 package exercises its own.461462What you may not do: reach the network (`depcheck`'s `netconfine` rule forbids463it outside `internal/generate/crawl`), sort anything the capture ordered, read a464clock, or let your vocabulary out of your directory.465466---467468## 3. The capture archive469470`internal/generate/archive` reads it. The archive is a **source-neutral471concept** — content-addressed bytes, grouped by volume and by world, each472carrying the time it was first seen — but the archive on disk is a specific,473historical layout, kept verbatim as input because years of captured history are474data, not code.475476```477<root>/archive.json the volume register478<root>/<vol>/game.json the world register479<root>/<vol>/icons/<key>.svg|.png collection artwork480<root>/<vol>/maps/<world>/snapshots/index.json the capture index481<root>/<vol>/maps/<world>/snapshots/map/<hash>.json the capture bodies482<root>/<vol>/maps/<world>/tiles/index.json per-tile records483<root>/<vol>/maps/<world>/tiles/set-<id>/<z>/<x>/<y>.<ext>484```485486`<vol>` is the register entry's own `directory` field, not a slug this lane487computes — the register is the only thing that says where a volume sits. In the488staged archives it is `games/<title-slug>-<id>`, which is a habit of the layout489and not a rule: a caller that resolves a volume's directory any other way is490reading the disk instead of the register. The `maps/` level is the layout's word491for "the worlds of this volume"; nothing above `archive.Volume` ever sees it.492493What a caller sees is the vocabulary, never the layout: volumes, their worlds,494each world's captures, a capture's body, a volume's artwork. Two habits of the495layout are translated rather than passed on — a register entry naming no source496is MapGenie, and a capture's volume and world are recovered from the path it497sits at rather than from the record.498499**Unchanged bytes record nothing.** A capture is deduplicated by content hash500alone: a re-crawl that fetched the same thing leaves the index byte for byte as501it was, so nothing downstream moves. `capturedAt` is first-seen, never502last-verified. Times compare as strings, which is the order that means503something for RFC 3339.504505**Only the newest capture of a world is read.** Older ones are history kept on506disk.507508*(The layout a new archive would use — and whether one is worth writing — is509still an open question. Nothing new inherits this naming.)*510511### 3.1 Writing it512513`internal/generate/crawl` is the write path, and it is **the only package in514Atlas permitted to reach the network**. `depcheck`'s `netconfine` rule is what515makes that true rather than customary: no other package in the format or516pipeline lanes may import `net/http` at all, and the outbound half of it is517reported anywhere outside this package. Fetching is crawling wherever it518happens — the enrich lane's national hydrography evidence is captured here too,519and travels in the archive, so its join re-runs against the archive rather than520against a live endpoint.521522**Politeness.** One `Fetcher` per run holds a single monotonically advancing523instant under a lock. Every request of the run takes the next slot, so two524requests are never closer together than the interval (150 ms by default,525under seven a second) however many goroutines are waiting. It is not a token526bucket on purpose: a bucket lets a run that idled spend its savings in a burst,527which is the exact shape a rate limiter is watching for. A 429 or a 5xx pushes528*the schedule* forward — the origin's own `Retry-After` where it gives one,529exponential backoff otherwise — so the whole run slows rather than the one530worker that heard the refusal. Four attempts including the first; beyond that,531an origin that is refusing is refusing.532533The user-agent is a browser's string, and that is a decision. Several of these534origins answer 403 to anything else, and a 403 is indistinguishable from "never535published" in a tile pyramid, so an honest header would silently punch holes in536an archive. The politeness that matters is in the behaviour.537538**Absence is a result.** 404 and 403 both mean *not published*: a pyramid is a539rectangle and its corners are usually empty. It is recorded as `absent` so a540re-crawl does not ask again. 202 means the origin is preparing the answer, and541the caller waits. Anything else non-200 is a disagreement rather than a hiccup542and is not retried.543544**Content addressing.** A capture's body is written to a path that is its own545SHA-256, so writing it twice writes the same bytes; the index is appended to546only when that hash has never been seen. `capturedAt` is therefore **first-seen,547never last-verified**, and a re-crawl of unchanged data leaves the working tree548byte for byte as it was. That is the property everything replayable stands on:549the translators replay, the derivation stamps stand still, and a rebuild writes550the file that is already there.551552**Nothing is renamed.** A register entry is found by identity, merged in place,553and keeps its position — so a directory named once is named forever, and a field554this package has never heard of survives a run. Directory names are only ever555computed on first sight.556557**The same-slug policy.** Two publishers describe Night City. Each registers the558volume under its *plain* title, so their builds answer for one library entry and559the newest capture is the one a reader sees; each names its *directory* from a560title carrying its own prefix (`IGN Cyberpunk 2077`), so they do not fight over561one directory. And each source's archive identities carry a bit of their own —562IGN at 2³², Piggyback at 2³³, NASA Trek at 2³⁴, ArcGIS at 2³⁵ — over an FNV-1a563hash of a stable name in the low 31 bits, all held under 2⁵³ so a JSON round564trip through `float64` is exact.565566**A crawl is interruptible.** Every write is idempotent and every fetched thing567is recorded before the next is asked for, so stopping a run is a normal way to568end it. A resumed run skips what is cached and does not re-ask for what was569absent.570571### 3.2 The crawlers572573```574atlas crawl -archive DIR -source NAME TARGET [-n] [-interval D] [-concurrency N] [-max-zoom Z] [-on DATE]575atlas crawl -source list576```577578A crawler is the outward half of a source, and a separate package from its579reader on purpose: the two have opposite properties. A reader is a pure function580of an archive; a crawler is the only thing in Atlas that can fail because581somebody else's server is having a bad afternoon.582583| crawler | target | runnable here |584| --- | --- | --- |585| `ign` | `<objectSlug>/<mapSlug>` | code-complete, not run — the captures are archived |586| `nasa-blue-marble` | `earth` | runnable — one pinned image, digest-verified, fetched once (§2.4) |587588The game sources' captures are archived and their endpoints are somebody else's589editorial work, so their crawlers are kept complete and are not run against live590endpoints. The ArcGIS/USGS crawler is the one that may run, because its data is591public, and it is the one still outstanding. The renderer it would feed landed592first (§4.4), which leaves the city's archive an input rather than something593this lane can take: the proof city's archive is staged at594`crawl/bend-or/fmg-archive`, and the lane reads, draws and595composes from there. What the crawler adds is the ability to take a *new* day —596a second world in the city's version history — rather than the ability to597rebuild the one that is there.598599The IGN crawler carries this lane's most consequential gate. Some IGN wikimap600pages are MapGenie maps in an IGN frame: the page declares a MapGenie game id601and serves MapGenie's tiles. Archiving one would put a second, worse copy of602data Atlas already reads properly into the archive, where a later merge would603fold a source into itself and report a beautiful agreement between a thing and604itself. The evidence is the page's own declaration, and it exists only while the605crawler is looking at the page — which is why the refusal lives there and the606reader says plainly that it cannot check.607608---609610## 4. Tiles611612`internal/generate/tiles` reads derived pyramids and the stamps that say how613they were derived.614615A **lens** is one raster pyramid picturing a world. Deriving one — finding the616deepest complete captured level, folding it down, choosing between617nearest-neighbour and box reduction, recording which tiles exist — happens once,618into a tile set on disk. Composition reads the result.619620```621<root>/index.json the register622<root>/<pyramid>/<z>/<x>/<y>.<ext> the rasters; z, x and y are local623```624625A pyramid's register entry carries its source tile set, its zoom range626(`minZoom`, `maxZoom`, `fullZoom`, `sourceZoom`), the window it was cut from,627a format per level, its content bounds, whether it is smoothed when magnified,628the colour painted behind it, a coverage bitset per partial level, and its629derivation stamp.630631### 4.1 The derivation stamp632633Every pyramid carries a stamp over everything it was derived from: the plan's634shape, the content hash of every source tile that went into it, and a hash of635the deriving tool's own source. Composition folds it into the bundle's stamp636under the part name637638```639tiles/<pyramid>640```641642— one part per pyramid, however many tiles it holds — and **never reads a tile643to do it**. That is what makes an unchanged volume cheap to notice: a rebuild644that would write the same bytes computes the same stamp without decoding a645single raster, and the file it would write is already there under a name646carrying that stamp.647648The mechanism is one-directional on purpose. Composition trusts a pyramid's649stamp and cannot recompute it: the captured tiles it was folded down from are no650longer in front of it. A pyramid with an empty stamp stamps as empty, which is651honest — the bundle records that nothing was claimed about how those tiles came652to be.653654### 4.2 What the deriver promises655656`atlas tiles -archive DIR -output DIR` folds captured frames into pyramids.657Its input is the archive — the tiles a crawler wrote, under the paths a tile658server used — plus each lens's **frame**, which is what the source declares659about the tiles it captured: how deep the pyramid goes, in what encoding, and660which tiles each level was supposed to hold. That last question is one no amount661of looking at an archive can settle, because a level missing its last row looks662exactly like a level that never had one, and it is the only reason a frame rides663the interchange document at all. Composition never reads it.664665- **Frame discovery.** A world is a 32-tile square. Local zoom 0 is the source666 zoom whose window collapses to one tile; local coordinates are source667 coordinates less the window's first tile. No height is assumed: the frame is668 measured, so a publisher cutting the same ground from zoom 6 and one cutting it669 from zoom 13 both derive correctly. A frame whose axes come to rest on670 different tiles is refused — it would draw a map in one place and its features671 in another.672- **The complete-level rule.** The deepest level whose expected tiles are all673 present is `fullZoom` and is copied byte for byte; every shallower level is674 folded down from it, never taken from the captured intermediates. Partial675 levels above it are copied while they stay contiguous, and are what676 `maxZoom` counts.677- **Pixel art versus photographs.** A pyramid that is not interpolated reduces678 nearest-neighbour and normalizes to lossless PNG; a photographic one reduces679 by box filter and keeps its encoding.680- **Coverage bitsets.** A level that is empty or exactly full records nothing;681 otherwise a bounding box in tiles and a row-major bitset over it, least682 significant bit first, base64. An absent entry means the level is complete.683- **The stamp.** SHA-256 over: the tool's source hash; the source and asset684 paths; `fullZoom` and the deepest zoom; the preferred format and the685 interpolation flag; the content bounds where declared; the affine and target686 zoom where the pyramid is a warp; and then, per level in ascending order, the687 level number, its tile count, and each tile's `x`, `y`, content hash and688 format, sorted by `x` then `y`. Every field is NUL-terminated. The listing689 order on disk is not an input.690- **The background tile.** A level's filler is the content hash more than half691 its tiles share, found once on the level that is trusted and omitted from every692 level. Reusing one hash keeps a sparse deep level from having its handful of693 real tiles voted "background" by its own majority. The colour painted behind694 the raster is the mean of that tile's pixels, and only if the tile is flat695 within twelve levels per channel — otherwise nothing is omitted at all, because696 a hole nobody can paint over is worse than a duplicated tile.697- **Incrementality.** A pyramid whose stamp matches the register's previous698 entry, whose asset path is unchanged and whose directory still exists, is699 carried over untouched.700- **The write.** Each pyramid is derived under a temporary name and arrives by701 one rename, so a reader never sees a half-written one; pyramids the archive no702 longer offers are taken out; and the register is written last, so until the end703 it still names the stamps of what is actually on disk. A run interrupted704 anywhere leaves a register whose stamps disagree with a plan, and the next run705 derives exactly those pyramids again.706707### 4.3 What a stamp promises, and what it does not708709A derivation stamp is a **rebuild-cost** promise — *nothing that made this has710moved* — and never a content promise. It covers the deriving code's own source,711so two derivers that write byte-identical tiles stamp differently. That is the712point: changing how a level is reduced has to invalidate every pyramid, and a713stamp that watched only the archive would quietly keep serving the old714derivation.715716The consequence is that stamp identity across two derivers is impossible by717construction. What is proven instead is the tiles and the plan where they are718observable: the stamp arithmetic and the register's promises in719`internal/generate/tiles`, the drawn level held to the capture's own recorded720hashes on every derivation (§4.4), the whole pipeline run twice over one721archive writing the same bytes (`cmd/atlas/pipeline_test.go`), and the corpus722tile inventories, which carry a content and a decoded-pixel digest for every723tile of every corpus pyramid (`testdata/corpus`). `docs/stamps.md` carries724the reasoning.725726### 4.4 The drawn level727728Most pictures in the corpus arrive as tiles from somebody's tile server, and a729pyramid's deepest level is those tiles copied through. A city has no tile730server: a municipal open-data hub publishes geometry, and the offline invariant731forbids reaching for anybody else's raster. So a city's deepest level is732**drawn**, from the geometry the city publishes about itself, and every733shallower level folds down from it exactly as any other pyramid's does. Nothing734downstream of `Derive` can tell a drawn level from a copied one, which is the735design.736737`internal/generate/tiles/basemap` is the renderer, and it draws one tile from738one small vocabulary.739740- **Roles, not datasets.** A shape says what it is to the ground — `parcel`,741 `park`, `water`, `street`, `trail`, `boundary` — and nothing about where it742 came from, so no publisher's vocabulary reaches the renderer (issue #5 §5.1).743 A role the style table does not know draws nothing, rather than being given an744 invented look.745- **The z-order is the drawing's only opinion about layering:** parcel texture746 first, then ground covers, water over parks so a lake punches through one,747 streets over water so a bridge reads as a bridge, trails above streets, and748 the boundary last as the one accent colour. Within a role nothing is ordered:749 a role's shapes accumulate into a single path and the rasterizer saturates, so750 they are unioned rather than painted over one another.751- **Widths are world-true.** The style table's widths are spelled at local zoom752 6 and scale with depth, so a street keeps its width on the ground however deep753 the pyramid goes — which, folded back down, is one width on the screen. A754 shape's emphasis scales its own stroke, so an arterial is a street drawn wider755 rather than a role of its own.756- **Ground is rings; linework is capsules.** Rings clip Sutherland–Hodgman, the757 first ring ground and every ring after it a hole — a positional convention,758 because publishers do not reliably wind holes the way GeoJSON asks. Lines clip759 Liang–Barsky a segment at a time, and each surviving segment joins the path as760 its own closed capsule: the segment widened to the stroke, with a semicircular761 cap at each end. Every capsule winds the same way, so capsules overlapping at a762 vertex union into one continuous line with a round join for free. There is no763 stroker, no join table and no miter limit — it is the cheapest stroker that764 never shows a seam.765- **Eight pixels of bleed.** Both clippers cut against a window bled eight tile766 pixels past the tile's own edge, so every cut lands outside the picture and two767 neighbours drawn independently continue each other exactly. A dashed stroke768 keeps its phase across the seam too: the rhythm advances by the whole of every769 segment whether or not its middle was drawn here, and a segment whose head was770 cut off begins at the phase its lost lead had already carried.771- **Opaque truecolour PNG**, encoded at the slowest and smallest setting. A tile772 has no transparency to carry, so the alpha channel is dropped rather than773 spent on every pixel of every city; the setting is fixed rather than a knob,774 because a knob is a way for two runs of one pipeline to disagree.775776**The curation is the source's, and it is the same curation.** A dataset's role777and optional emphasis travel beside the rest of that layer's judgement in778`generate/sources/arcgishub`, unread by translation, so the drawing and the779legend cannot drift into two tables. What a source states is a `doc.Drawing` on780the lens: shapes in world pixels, projected through the very window the pins are781projected through, so the raster and the features cannot disagree about where782the ground is. It is stated *beside* the world's collections rather than derived783from them, because the two do not line up — every road centreline draws and none784of them is a legend row, only named trails are legend rows and every trail785segment draws, and emphasis varies row by row where a collection's features are786buckets of many rows.787788**The capture is the witness.** The crawl that first drew a city wrote its789rasters into the archive, and a drawn lens's plan still names their content790hashes, so the deriver holds every tile it draws against what the capture791recorded and refuses the derivation by name if the two disagree. That is a792stronger thing to check than a suite could: not "these bytes look right" but793"these bytes are what a different implementation made of the same vectors",794checked on every derivation rather than in a test that could be skipped. It also795means a drawn level's plan — and therefore its stamp — is the ordinary one, with796the level's tiles listed and hashed like any other level's.797798**And the renderer is part of the tool.** A derivation stamp covers the deriving799code because a change to how a level is made has to invalidate that level; a800city's deepest level is now made here, so `basemap`'s sources sit inside the801`ToolStamp` embed beside `plan.go`, `stamp.go`, `derive.go` and `draw.go`.802803### 4.5 Warped variants804805When two sources picture one ground, one raster is usually the finer — a806publisher's own map beside a wiki's rasterized in-game rendering — and a registry807that simply served the finer one would throw the other away. Instead the lesser808raster is **resampled into the finer one's world** and offered as one more809picture of the same ground. Nothing is discarded, both rasters answer to one810grid, and every feature lands on either.811812Three decisions make a warp, and they happen in this order.813814**Names are settled first.** Two sources capturing one volume name the same815ground the same thing, so their pyramids would land in one directory.816`tiles.Settle` gives *every* colliding plan its publisher's own path as a817suffix — all of them, never just the later one — so which pyramid is called what818does not depend on the order an archive listed its captures in. Cyberpunk's two819readings settle as `cyberpunk-2077__night-city__cbp` and820`cyberpunk-2077__night-city__cyberpunk-2077-night-city`. A warped variant is821named after the picture it aligns onto, so it can only be planned once the names822have stopped moving.823824**The pairing is decided from the whole set.** One reading per ground — the825deepest, since a world's other lenses are alternate art already sharing its826window — and, among the readings of one volume, the one that draws its world at827the most pixels is the base every other is brought into. Resampling into a828coarser world would throw away detail that was captured. Only readings from829*different* sources are ever paired: a source that divided its own world into830several grounds did so deliberately. This policy lives in `cmd/atlas/warp.go`.831832**The alignment is an input, not a derivation.** `tiles.PlanWarp` is handed the833fitted affine as six numbers. Fitting one stands on the names two readings share834and belongs to `internal/enrich/align` — the same fit the cross-source merge835folds features by, so a raster and the features drawn on it can never disagree836about where the ground is. Neither lane imports the other; `cmd/atlas` holds837both, and the seam is the one `adapt.go` opens for documents and volumes. Nothing838is duplicated to arrange it: the deriver needs a fitted transformation, not the839ability to fit one, so only the arithmetic of *using* one — apply, invert,840scale — sits beside `tiles.Affine`.841842What the deriver then does:843844- **The target zoom** is the base-frame zoom nearest the donor's real resolution845 after the alignment's own scaling, clamped to the base's deepest complete846 level: deep enough to keep what the donor drew, never so deep as to invent847 detail that was never there.848- **The deepest level is rendered, not copied.** Every pixel of every tile in the849 window the warped content falls in asks the inverse transformation where in the850 donor it came from, and samples the donor's deepest complete level bilinearly,851 clamped inside the tile that holds it. A tile no donor pixel reached is not852 written at all: coverage says nothing about it, and a reader falls back to the853 parent.854- **Everything shallower folds down** from that level exactly as any other855 pyramid folds, as a photograph.856- **The donor's own background** paints wherever the donor never drew, so a857 warped tile is opaque and a picture that simply ends does not end in black.858- **The bounds are the warped content's**, measured through the alignment and859 clipped to the world square — reported even when they come to the whole square,860 because what the alignment reached is a fact about this picture rather than a861 default that can be left out.862- **The plan lists one captured level**, the donor's deepest complete one, which863 is what the warp samples: a stamp naming the donor's other levels would rebuild864 for a change that could not reach these tiles.865- **The stamp carries the alignment** — the base's tile set, the target zoom, the866 base frame, and the six coefficients to nine decimal places — because the same867 donor through a different transformation is a different picture.868869`cmd/atlas/warp_test.go` holds the pairing policy — the finer picture is the870frame, a source is never warped onto itself, readings that do not align stay871apart, and names settle before anything is derived — and the resampling872itself is the deriver's ordinary path, judged the way §4.3 says everything873else is.874875---876877## 5. Composition878879`internal/generate/compose` turns a document into a volume.8808811. **Resolve.** Each world's lenses are found in the tile set, its collections882 numbered, and the ground its contents cover measured. Every lens of a world883 must agree on the window it is cut from; one that does not would be a world884 drawn in two places at once. A world in a window that is not the shared one885 carries a `grid` in its payload.8862. **Order.** Worlds sort as families: a piece of a split sheet carries its887 sheet's position and follows it. Curation decides which world leads and888 whether a volume's worlds read forward or backward.8893. **Artwork.** Every point collection's icon key resolves against the890 document's icon set. Artwork nothing names is left behind.8914. **Conventions.** Composition speaks what it knows on the volume's behalf and892 then holds the whole thing to the registry, **producer-strict**: an893 unregistered `atlas.*` key, a value outside its vocabulary, or a key on the894 wrong entity fails the build here, one step before a bundle.8955. **Pack, stamp, write, install.**896897### 5.1 What composition speaks, and what a source speaks898899| key | written by | when |900| --- | --- | --- |901| `atlas.geometry.kind` | composition | mirrors the collection's kind, unless declared |902| `atlas.icon.kind` | composition | once the artwork has actually been resolved |903| `atlas.icon.outset` | composition | from curation |904| `atlas.collection.key` | composition | from curation, unless declared |905| `atlas.render.as` | the source | it is a reading of a publisher's own field |906| `atlas.geometry.*`, `atlas.geo.*` | the source | only a source knows what its ground is |907| `atlas.icon.std` | the source | naming a glyph is all a source without artwork can do |908909Nothing composition writes overwrites what a source said.910911**Standard icons are named by a source and resolved by composition.** A source912with no artwork of its own — a gazetteer that names two thousand craters and913draws none of them, a city's open data — names a library glyph through914`atlas.icon.std` instead of inventing one. Composition resolves the name against915the vendored library in `internal/generate/icons` and packs the glyph under a916provenance-spelling asset name (`std--maki-mountain.svg`), so a source's own917drawing of a thing can never be shadowed by a generic one and a bundle's icon918listing reads honestly. A declaration the library cannot answer fails the build.919920Resolving a name is not choosing one. Deciding that a collection which named921nothing should carry a standard glyph — and which — is a judgement made after two922sources have been folded together, and it belongs to the enrich lane's923`stdicons` enricher (issue #5 §5.3). The two meet at the attribute.924925### 5.2 Splitting926927A sheet that holds several separate places is **declared in curation, never928detected**: guessing wrong divides a whole map into pieces nobody asked for.929Two modes: `worlds` gives each piece its own entry in the picker, `lenses`930keeps one world and offers the pieces one at a time.931932The splitter reads only the interchange document's own vocabulary, so a second933source describing the same sheet the same way splits the same way. A **piece**934is one top-level area — an area feature nothing is the parent of — together with935its descendants and the point features that name one of them as their member.9369371. **Plan.** Every area walks up its parent chain to its root. A piece's extent938 is the ground its areas outline, projected into world pixels; the points939 assigned to it do not stretch it. A point belonging to no area fails the940 build, because it would vanish when the sheet came apart.9412. **Grow.** Each extent widens by up to 256 world pixels into the empty space942 around it, stopping halfway to whatever it would run into, so a title printed943 beside a piece travels with the piece it names. Two pieces that grew into the944 same diagonal corner are eased apart along whichever axis they overlap least,945 and neither is ever pushed back inside the ground it grew from.9463. **Rehome.** A point whose position lies outside the piece that claims it, and947 inside exactly one other, moves. Where it sits is not in doubt; the claim is948 treated as the mistake it is. An ambiguous point stays where it was claimed.9494. **Order.** Lenses read top to bottom, so the sky comes before the ground950 beneath it. Separate worlds lead with the largest, so a sheet's main map heads951 its insets.9525. **Cut.** Into worlds: the first piece keeps the sheet's identity and every953 other becomes `<sheet title> — <area title>` under `<sheet slug>-<slug of the954 area title>`, carrying `parent`. Into lenses: one lens per piece, named after955 its area, and every feature carries the piece it belongs to as its `shard`. In956 both modes the piece's own outline is dropped — once an area has been used to957 cut a world out of a sheet, drawing it again just traces the edge of what the958 reader is already looking at — and a point collection left empty goes with it.959960The lenses of every piece draw from the same pyramid; only the window onto it961changes. A piece's `bounds` is the grown window and its `surface` is the ground962it actually covers, clipped to that window, because those are not the same963rectangle and anything measuring a world rather than drawing it wants the964ground.965966### 5.3 The payload967968Three parts, because they are wanted at three different moments.969970- `worlds/<slug>.json` — lenses and the ordered collections array. Point971 collections carry no inline features; shape collections inline theirs whole.972 **Point collections are written first**, so a packed location's owner is the973 ordinal of its collection among the points.974- `worlds/<slug>.bin` — `ATLASLOC` v3, the point features packed.975- `worlds/<slug>.text` — prose, links and per-feature attributes, keyed by976 feature id as a decimal string. A point earns an entry for prose, links or977 attributes; a shape earns one for prose alone and marks `hasText`, keeping978 its attributes inline because a card needs them the moment ground is asked979 about.980981Its field names, its field order, and which fields are omitted when empty all982feed the stamp, so the shape is frozen with the format version.983984Every world opens a **provenance account** — where its ground came from, and985what it held when it got here — merged with anything or not. A single-source986build carries exactly one entry, marked `origin`. The ledger fields the enrich987lane adds extend that entry in place, after `added`, so an origin account's988bytes never move.989990### 5.4 Stamping and writing991992The stamp is taken over **named parts**, sorted, one `"<name> <hash>"` line993each, joined by newlines, SHA-256:994995```996atlas.json hash of the manifest as it stands before its own stamp997 and creation time are filled in — the revision is in998worlds/<slug>.json hash of the payload bytes999worlds/<slug>.bin1000worlds/<slug>.text1001tiles/<pyramid> the pyramid's derivation stamp, not its bytes1002icons/<name> hash of the artwork1003```10041005`createdAt` is the **newest capture time across the volume's worlds**, never1006the build clock. The name is `<slug>-<YYYYMMDD>-<stamp12>.atlas`. Building the1007same archive anywhere, at any time, yields the same version and the same file1008name, which is the whole reason a directory of these files can stand as a1009registry — and why a build already present is left untouched.10101011The write: the archive is built under a temporary name that does not end in the1012bundle extension, so a reader scanning the registry mid-write passes over it;1013it is renamed into place, so the name appears whole or not at all; and then it1014is reopened from disk and validated, because the file that will serve is the1015copy and it is the copy's promises that matter.10161017Entry order inside the zip: the manifest, then each world's three parts in world1018order, then the pyramids in sorted local name order (walked lexically, so level1019`10` precedes level `2`), then the icons in sorted name order. Tiles and packed1020locations are **stored uncompressed** so a reader serves them as byte ranges.10211022### 5.5 The policy revision10231024`compose.PolicyRevision` orders builds of the same capture: the data has not1025moved between them, but what the lane makes of a capture has. Among equal1026captures a registry serves the highest revision. It is part of the stamp, so1027bumping it supersedes the builds already in every library. Its changelog is in1028the constant's doc comment.10291030---10311032## 6. Curation10331034`internal/generate/curation/curation.json`, embedded so a build carries its own1035curation and a composed bundle cannot depend on what was on the operator's1036disk. Every key is spelled in Atlas's vocabulary — a volume slug, or1037`"<volume>/<world>"` — never in a source's identifiers.10381039```jsonc1040{1041 "schema": 1,1042 "window": { "sourceZoom": 13, "firstTile": 4064 },1043 "worldOrder": {1044 "preferred": { "<volume>": ["<world>", …] },1045 "newestFirst": { "volumes": ["<volume>", …] }1046 },1047 "iconOutset": {1048 "byVolume": { "<volume>": "light" | "dark" },1049 "byWorld": { "<volume>/<world>": "light" | "dark" }1050 },1051 "shard": {1052 "worlds": { "<volume>/<world>": "worlds" | "lenses" }1053 },1054 "collectionEquivalents": {1055 "<volume>": { "<icon key>": "<shared name>" }1056 }1057}1058```10591060Every section carries its reasoning beside its data, under keys the reader1061ignores (`what`, `why`, `byVolumeWhy`). A value that is not the shape a section1062expects is commentary, not an entry.10631064| table | what it decides |1065| --- | --- |1066| `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. |1067| `worldOrder.preferred` | a volume's primary world, ahead of its secondary areas. Unlisted worlds follow in title order. |1068| `worldOrder.newestFirst` | volumes whose worlds are dated captures of one ground: a version history opens on the present. |1069| `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. |1070| `shard` | the sheets that hold several separate places, and what to do with them. |1071| `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. |10721073Every table is keyed in Atlas's vocabulary — including outsets and shard1074declarations, keyed by volume and world slugs rather than by a publisher's1075numeric map ids, so a curation entry is readable without the publisher's1076database in front of you and movable to a second source describing the same1077ground.10781079---10801081## 7. The CLI10821083```1084atlas crawl -archive DIR -source NAME TARGET [-n]1085atlas tiles -archive DIR -output DIR [-force] [volume…]1086atlas compose -archive DIR -tiles INDEX [-bundles DIR] [-n] [volume…]1087atlas translate -archive DIR [-volume SLUG] [-artwork] [-list]1088```10891090`crawl` is the network-touching, hand-run step: §3.2. `tiles` folds captured1091frames into the pyramids composition packs, carrying over every pyramid whose1092captures have not moved.10931094`compose` builds every volume the archive holds a registered source for, or1095only the ones named. `-n` composes and stamps without writing. With no1096`-bundles`, it installs into the application's own library.10971098`translate` prints an interchange document to stdout — the debugging window1099into the first half of the lane, answering the question composition cannot:1100what did the source actually make of these bytes? `-list` names every volume1101the archive registers, as `source · title · directory`; the slug `-volume`1102takes is the one the run logs as `volume=`.11031104Both write their event stream to stderr, so piped stdout stays clean. See1105[logging.md](logging.md).11061107### 7.1 Where the inputs are11081109Neither the capture archives nor the derived tile sets are in git — they are1110large, they are somebody else's bytes, and they are staged in the working copy1111instead. The repository's own copies are gitignored and are what every default1112points at:11131114| input | staged at | override |1115| --- | --- | --- |1116| the games corpus archive | `crawl/fmg-archive` | `ATLAS_ARCHIVE_DIR` |1117| the proof city's archive | `crawl/bend-or/fmg-archive` | `ATLAS_CITY_ARCHIVE_DIR` |1118| the included Earth's archive | `crawl/blue-marble/fmg-archive` | none — `make included-earth` names it |1119| the derived tile sets | `tiles/`, register at `tiles/index.json` | `ATLAS_TILES_INDEX` |11201121So the whole lane, over one volume, is three commands:11221123```sh1124atlas translate -archive crawl/fmg-archive -volume tunic # look at it1125atlas compose -archive crawl/fmg-archive -tiles tiles/index.json \1126 -bundles <a scratch registry> tunic # build it1127```11281129Without `-bundles` the build installs into the application's own library, which1130is a thing to do deliberately rather than while reading a document.11311132`atlas tiles -archive crawl/fmg-archive -output tiles` re-derives the pyramids1133into the staged set, and is only needed when the archive has moved: a pyramid1134whose captures have not moved is carried over untouched (§4.2).11351136**Deriving into a *fresh* output directory does not reproduce the staged set's1137stamps.** A derivation stamp covers the deriving tool's own source (§4.3), so1138re-deriving the same tiles under a different build of the tool stamps1139differently — which changes the bundle's stamp, and therefore its file name.1140The tiles are identical; the accounting of how they were made is not.1141Composing against the staged `tiles/index.json` reproduces the builds already1142in circulation name for name; composing against a freshly derived index is a1143correct build of the same volume under a different name. `docs/stamps.md`1144carries the reasoning.11451146---11471148## 8. What is proven11491150How the lane is judged overall is [testing.md](testing.md); every suite below1151runs under `make test`, hermetically, over stated inputs and the committed1152corpus.11531154| claim | held by |1155| --- | --- |1156| 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 |1157| the city translates exactly — the window, the buckets, the membership join | `internal/generate/sources/arcgishub` (`TestCityTranslatesExactly`) |1158| the drawn level is deterministic and honest about its edges | `internal/generate/tiles/basemap`: same shapes, same bytes; holes, bleed, dash phase, opaque truecolour |1159| 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 |1160| a drawn tile is held to its witness | `cmd/atlas/pipeline_test.go` (`TestADrawnTileIsHeldToItsWitness`) — the §4.4 refusal, exercised |1161| the warp pairing policy (§4.5) | `cmd/atlas/warp_test.go` |1162| splitting, ordering, conventions, identifiers | `internal/generate/compose` |1163| 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 |11641165The staged capture archives and tile sets (§7.1) are maintainer inputs, not1166test inputs: no required test reads them, and `make corpus-smoke` is the1167maintainer's own deep check over a real installed library1168([testing.md](testing.md)).