1# The application23**Status: built.** This document4specifies the hypermedia application: the URL surface, the session record, the5partial and event envelopes, the region and template system, the state island,6and the contract between the handler and whatever host it is mounted in. §117is the standing list of what is named rather than built, so nobody mistakes a8placeholder for a decision.910The implementation is `internal/app`, its host seam is11`internal/app/hostenv`, and the two hosts are the desktop shell at the module12root (`main.go`) and `atlas serve` (`cmd/atlas/serve.go`). Where this document13and the code disagree, take it as a defect in one of them and say so.1415---1617## 1. One handler, three hosts1819The application is **one pure `http.Handler`**. It touches no filesystem,20builds no path, opens no dialog, and reads no environment. Everything21OS-shaped arrives through three small interfaces:2223```go24type Hostenv interface {25 Volumes() VolumeStore26 Sessions() SessionStore27 PickFile(ctx context.Context) (io.ReadCloser, string, error)28}29```3031That is the whole host contract. It exists so the same handler can be mounted32by three different hosts:3334| Host | Status | What it supplies |35|---|---|---|36| **Wails webview** | built | The desktop window; `PickFile` is the native dialog; the library is the application's data directory. `main.go` + `redirects.go` + `hostenv/wailshost`. |37| **`atlas serve`** | built | Plain HTTP, no window. `PickFile` refuses with `ErrNotAvailable`. The dev loop, CI, and the browser suite (`tests/e2e`). |38| **WASM service worker** | not scheduled | Go compiled to `js/wasm`; the stores back onto OPFS. Nothing is built for it; the discipline is what keeps it reachable. |3940Native authoring is a separate, optional capability rather than a fourth41filesystem method on `Hostenv`. The desktop supplies `minting.Minter`; the42headless and future WASM hosts omit it. When it is absent the creator and its43routes are absent together. When it is present, the application still sees44only a compact request, a bounded preview, build events, and the installed45volume identity—never a path, cache, or network client.4647The rule is enforced mechanically: `tools/depcheck`'s `hostenv` analyzer48fails any import of `os`, `os/exec`, `path/filepath`, `syscall`, or a window49toolkit from `internal/app` outside `internal/app/hostenv`. The OS50implementations live in `internal/app/hostenv/oshost`, so a host that is not51an operating system links none of them. The two host entries — `cmd/atlas` and52the desktop shell at the module root — are where the machine is *supposed* to53be reached, and are exempt by sitting outside the lane the rule is written54about; depcheck's scope carries the root package for the rest of its rules.5556### 1.1 `VolumeStore`5758```go59Volumes() []Volume // the serving build of each volume60Rescan() ([]string, error) // → the volumes whose serving build moved61Install(name string, content io.Reader) (Installed, error)62Location() string // a label: where the library lives63```6465The registry model is **scan at launch, rescan on import — never watch** (issue66#5 §2, decision 15). A file dropped into the library from outside appears at67the next launch. The fold that decides which build of a volume serves is68`format/vnext`'s and is pure — newest capture, then policy revision, then69stamp, then locator — so a host only ever has to produce descriptors.7071`Location()` is a label, not a path the handler takes apart: it is what the72catalog reports as `bundlesDir` and what the empty-library page shows.7374### 1.2 `SessionStore`7576```go77Load(name string) ([]byte, error) // hostenv.ErrNoSession when absent78Save(name string, data []byte) error79Delete(name string) error // a record that is not there is already gone80Names() ([]string, error)81```8283Bytes under a name. What the bytes mean is §3 of this document, which is what84lets a file, an OPFS handle, and a test's map all be the same store. Names are85held to `hostenv.ValidName` — lowercase, digits, `-`, `_`, `.`, no separators —86before they reach any store.8788`Delete` is the whole of the blunt reset (§4.3): the arrangement a volume opens89with is synthesized from the world itself, so forgetting the record *is* the90way back to it. Deleting a record the store does not hold is success, not91`ErrNoSession` — the caller wants the record gone, and a record that was never92there is gone.9394### 1.3 `PickFile`9596Returns the chosen file as a stream, the name it was chosen under, and an97error. Three answers are distinct and must stay so: a file, `ErrNoSelection`98(the reader cancelled), and `ErrNotAvailable` (this host has no picker). An99import that cannot happen and an import nobody wanted are different things to100say — and one of the two things to say is nothing. A host with no picker is101refused out loud, because a reader who pressed the button is owed a reason; a102reader who closed their own dialog is told nothing at all, and the row that103was standing by is taken away again (§4.1).104105### 1.4 The desktop shell106107The shell at the module root is host wiring and nothing else (issue #5 §3.4).108It resolves two directories, opens an `oshost` over them with a native picker109attached, and hands the handler to `wails.Run` as the asset server:110111```go112host, _ := oshost.New(oshost.Options{113 BundlesDir: library, SessionsDir: sessions, Pick: window.Pick,114})115wails.Run(&options.App{116 AssetServer: &assetserver.Options{Handler: followRedirects(handler)},117 OnStartup: window.Opened,118})119```120121Five things about it are worth knowing.122123**Where the library is.** `$ATLAS_DATA_DIR`, else `os.UserConfigDir()` +124`dev.felinestatemachine.atlas` — the identifier existing libraries already125live under, so a reader's library is found where it always was. `bundles/` under it126holds the volumes and `sessions/` the session records. `$ATLAS_BUNDLES_DIR`127moves the library alone, which is what a development run points at a freshly128composed `dist/bundles`. The headless host reads the same two variables129(`cmd/atlas/serve.go`).130131The desktop also owns `recipes/` and `cache/` beneath that data directory.132Earth authoring writes exactly one portable `.atlas-project` manifest per133minted volume into `recipes/`, shares captured evidence through `cache/`, and134publishes the validated result into `bundles/`. Those paths never cross into135the handler. Workbench remains an optional advanced manifest/receipt inspector;136it is not required for the first-run creator.137138**The included Earth volume.** The shell embeds the committed bundle at139`included/` (`//go:embed included/*.atlas`) and installs it into the library140before the host is constructed, so the first scan already serves Earth and a141first launch opens onto a world instead of the empty-library card. The install142goes through `format/vnext`'s own path — validated, versioned, staged,143idempotent — so a second launch is a no-op and another Earth build in the144library stays beside it for the fold to order; a built-in that cannot be145installed is a startup error, never a silent absence. After installation the146embedded copy is done with: everything reads the installed file, exactly as if147the reader had imported it148([decision 19](decisions/0019-an-earth-volume-is-included.md);149`included/README.md` carries the provenance and the recipe). The headless host150installs nothing: `atlas serve` uses exactly the registry the operator gives151it.152153**Where the seam is.** `//go:embed static` — the root `static/` directory,154holding the seam's built bundle as `app.js`. `make static` puts the same bytes155in `dist/static`, which is what a `-static` mount is pointed at; the shell156embeds its copy, so the shipped application is one file with no sidecars. A157build that skipped `make static` embeds only that directory's README:158`/static/app.js` answers `404`, `<atlas-viewport>` renders nothing, and159everything else works. The stylesheet system is not there — it is160`internal/app/assets`, embedded by the application itself and served from161`/assets`, so deleting the seam costs a page one script tag, not its chrome.162163**Redirects are followed by the host.** A Wails page is served over a custom164URL scheme, and a scheme task has no way to express a redirect: WebKit hands165the `302` to the page as if it were a document, and the reader is looking at166the two words Go writes in a redirect body. `redirects.go` walks the167application's own doorways instead — GET only, `Location` values naming a path168only, five hops at most, streaming answers passed straight through — so the169handler goes on redirecting and `atlas serve` goes on serving those redirects170to clients that follow them171([decision 17](decisions/0017-the-host-follows-redirects.md)).172173**No Wails runtime JavaScript in the page.** Nothing is bound, no bindings174JSON is generated, and the page hears that the library moved over the175application's own SSE stream (§5). Wails' asset server injects its runtime into176a `200 text/html` answer at a path ending in `/`; this application answers `/`177with a redirect whenever it has a library at all, so the only page that can178ever carry the injection is the empty-library doorway, which uses none of it.179Every explorer page is at `/v/…` and is served exactly as the headless host180serves it.181182The build is a plain `go build -tags "desktop,production"`, not `wails build`:183the Wails CLI's job is scaffolding and driving a Vite frontend, and this tree184serves its own pages — which is also why there is no `wails.json`. `make185desktop` is the macOS recipe, and `.github/workflows/release.yml` carries all186three platforms.187188---189190## 2. Routes191192### 2.1 The data plane193194```195GET /data/catalog.json no-store196GET /data/v/{slug}/{stamp12}/{worlds|tiles|icons}/... private, max-age=31536000, immutable197```198199The seam reads this plane, so its shape is contract rather than the200application's to reshape; the browser suite serves it over the committed201corpus.202203**The catalog** is composed at the moment it is asked for and never cached:204205```json206{"volumes":[{"slug":"…","title":"…","stamp":"<64 hex>","base":"/data/v/<slug>/<stamp12>",207 "tileGrid":{…},"worlds":[…]}],208 "bundlesDir":"…"}209```210211Volumes are listed **by title**. `worlds` is the manifest's world list212verbatim. `base` carries the twelve-hex short stamp, which is the whole cache213story: the URL names exactly one build, so everything under it is immutable,214and the moment a newer build takes the slug over the old URL is a 404 — which215is the client's cue to refetch the catalog.216217**Content** is served out of the archive with a `Content-Length`, a218`Content-Type` from a fixed extension table (`.json .text .bin .jpg .png219.webp .svg`), and nothing else. Everything not admitted by that table, not220under `worlds/`, `tiles/` or `icons/`, or not carrying the serving stamp, is a221plain `404`.222223**Byte ranges are not served.** Tiles are stored uncompressed so that they224could be, but a `Range` request is answered `200` with the whole body and no225`Accept-Ranges`. Changing that is a deliberate act, not a drive-by:226`TestContentDoesNotServeRanges` (`internal/app`) is what keeps it from227happening by accident.228229### 2.2 The app plane230231| Route | Answers |232|---|---|233| `GET /` | 302 to the last volume's world; the library card when nothing is installed. |234| `GET /open?volume=&world=` | 302 to the explorer page those two name. The doorway the topbar's two selects go through. |235| `GET /v/{volume}/{world}` | The whole explorer page, server rendered, in its remembered state. Also records that this is where the reader is. |236| `GET /fragments/detail/{id}?volume=` | One feature's card. |237| `POST /session/{concern}` | The partial set for the regions that concern touches (§4). |238| `POST /session/reset` | Deletes this volume's record and answers `204` with `HX-Redirect` back into the volume (§4.3). The one concern whose answer is a page. |239| `POST /bundles/import` | One progress row, streamed through its states (§4.1); picks, installs, rescans, announces. |240| `POST /mint/preview` | The exact bounded plan for the creator's current WGS84 zone, sources, detail and presentation. Desktop capability only. |241| `POST /mint` | Persists the app-managed manifest, captures/builds/validates/installs, rescans, then redirects into the minted volume. Desktop capability only. |242| `GET /events?volume=` | The SSE stream (§5). |243| `GET /assets/{app.css,htmx.js}` | The application's own chrome, out of the binary. |244| `GET /static/{path...}` | Whatever static tree the host mounted; `404` when it mounted none. |245246Every world lives at a real URL: it can be bookmarked, reloaded, and linked247to. `/` is a doorway, not a second name for the explorer.248249### 2.3 Earth is the authoring surface250251The included Earth volume offers **Create Atlas** only when two facts are true:252the world declares a spherical surface and the host supplied the native253minter. The drawer is a complete server-rendered form. Its title, WGS84 bounds,254detail, public source pack, presentation choices, cost preview,255and submit all work without the render seam. The seam progressively enhances256that same form with two camera facts: **Use current view** and a rectangular257**Draw zone** gesture over the existing map. It does not own a recipe, fetch a258source, or compile anything.259260Minting follows the ordinary authoring path:261262```text263Earth camera / drawn zone264 ↓265app-managed single manifest266 ↓267plan → capture → observe → assemble → native compile → validate → install268 ↓269rescan library → open the new Atlas270```271272The zone itself may be any finite, non-antimeridian Web Mercator rectangle;273it is not fenced to today's source coverage. The default pack is USGS Topo274plus TIGER roads, water, and county context and is labelled as U.S. coverage,275so an out-of-coverage layer may simply be empty. Planning enforces the same276pixel, request, evidence, feature, geometry and raster refusal budgets as the277CLI. A failed build leaves its recipe and cache evidence available for278diagnosis but never installs a partial Atlas.279280A `GET` of an explorer page writes the session's `world` and the last-volume281pointer. That is deliberate: arriving at a URL is a choice whether it was282clicked, typed, or restored by the browser, and the session follows the283address bar rather than the other way round. Arriving on a *different* world284also clears what belonged to the old one — the selection, the highlights, the285search — and re-derives the arrangement (§3.1).286287**`/open` exists because a `<select>` cannot build a path out of its own288value.** The volume and the world are both in the address, so the two crumbs289name what they want and the server redirects to it; after the redirect the290reader is at the real URL, which is the only place the explorer lives. It is291one route rather than client-side glue, which is the same trade the rest of292this document makes.293294**`/assets` is a different mount from `/static`, and the difference is the295deletability principle in the URL space.** `/static` is whatever tree a host296handed over — the seam's built bundle, which `make static` puts where a297`-static` mount wants it ([render-seam.md](render-seam.md) §9) — and answers298`404` when a host handed over nothing. `/assets` is the part of the page that is the299application's own: the stylesheet system and the vendored hypermedia runtime,300compiled into the binary (`internal/app/assets`). A build with no seam serves301a complete, styled, interactive page.302303Both assets are vendored rather than linked. The offline invariant is not only304about bundles: an atlas opens on a machine with no network, forever, and a305page that fetches its runtime from a CDN is a page that does not.306307---308309## 3. The session record310311**The state-placement rule (issue #5 §4.1):** discrete application state lives312on the server and in URLs; continuous interaction state lives in the seam. The313camera is the single, deliberate exception — it reports upward once per settle,314so a volume reopens where the reader left it.315316One record per volume, named `volume.<slug>.json`, plus `app.json` holding the317last volume. `SessionSchema` versions the shape; a record written under a318schema this build does not know is passed over rather than half-read, which319costs a reader their layout once and never corrupts it.320321```json322{323 "schema": 2,324 "volume": "bend-or",325 "stamp": "3610a0f10798",326 "world": "2026-08-02",327 "lens": "Basemap",328 "hidden": ["1496244488"],329 "collapsed": ["zones"],330 "expanded": ["1951802496", "253393030"],331 "highlighted": ["277390785"],332 "arranged": true,333 "labels": {"39191589": "quiet"},334 "search": "mill",335 "dock": {"open": true, "dismissed": false, "section": "counts"},336 "detail": {"open": true},337 "grid": {"system": "geohash", "cell": "9q5c", "subgrid": 2},338 "sidebar": {"collapsed": false},339 "overview": {"docked": false},340 "selected": "277390785",341 "cameras": {"2026-08-02": {"x": 4096, "y": -4096, "zoom": 1.34, "rotation": 0, "at": "2026-08-02T09:12:44Z"}},342 "updatedAt": "2026-08-02T09:12:44Z"343}344```345346Notes that are contract, not style:347348- `hidden`, `collapsed`, `expanded` and `highlighted` are **sorted sets**, so a349 record is stable to diff and two paths to the same state produce the same350 bytes. They are stored as strings — collection and feature ids ride the DOM351 as strings — and the island writes them back as the numbers the payload352 declares (§6).353- `lens` is the lens's **name**, not its index. A name outlives a build's354 ordering; the island's `lens` key is the ordinal (§6).355- `focused` is the ground the reader last *went to* from a list, and it is not356 `selected`: closing the card puts the selection down and leaves the feature357 index still marking where the reader has been. It is set only by a row that358 says `focus=1` — a feature index row, a panel row, a link inside a card —359 because a pick off the canvas is a reader who was already there, and it is360 cleared when the ground itself is rebuilt: a world opened, a split sheet's361 layer swapped, the volume come back to from another one.362- `sidebar` and `overview` are spelled as the thing a reader *does* to them —363 collapsed, docked — so the zero value is the ordinary page.364- `dock.dismissed` is the reader having folded the panel by hand. Until they365 have, the panel comes out on its own the first time it has something to say;366 after that it stays where they put it.367- `stamp` is the serving build the record was last written against. A record368 behind the serving build is still read — slugs outlive builds — and the369 difference is what a stamp-move refresh is about.370- `cameras` is keyed by world slug, because a camera belongs to a ground. The371 server stores it and hands it back. It never reasons about it.372373**Schema 2** is the templates wave. It gained `expanded`, `highlighted`,374`arranged` and `overview`, renamed `sidebar.open` to `sidebar.collapsed`, and375lost `solo`: isolating turned out to be a move on the hide set rather than a376state of its own, and the chip is derived from what is hidden so that it is377right however that set was reached — including by switching rows off one at a378time.379380### 3.1 The arrangement a world opens with381382Three of the sets have non-empty defaults that the *world* supplies, not the383reader:384385| Set | Default | Why |386|---|---|---|387| `hidden` | every collection the payload marks `visible: false` | the producer's own curation |388| `collapsed` | `["zones"]` | shape collections are a navigation aid, not the primary filter surface, so their section folds and pin groups stay above the fold |389| `expanded` | every ungrouped shape collection | their feature indexes are there the moment the section is opened, and in the DOM for anything reaching for a feature without unfolding first |390391`arranged` is what tells a fresh record from an arranged one. Without it an392empty hide set — a reader who asked to see everything — is the same bytes as a393record nobody has touched, and the next request would put the curation back.394395A record can also go away entirely. `POST /session/reset` (§4.3), which ⌘R396presses, deletes it; the next load reads nothing and builds the table above out397of the world again. It is the one operation on the record that is not a move on398a field, and that is the point: a record that has gone wrong is not something a399reader should have to diagnose a field at a time.400401---402403## 4. Regions, templates, and the partial envelope404405### 4.1 The regions406407Twelve regions, one template file each (`internal/app/templates/<region>.tmpl`),408mirroring the one-file-per-region stylesheet system in409`internal/app/assets/css`. A region's template renders that region's own410container, so a first paint and a swap produce the same bytes.411412| Region | Element | Swap | What it is |413|---|---|---|---|414| `shell` | `#atlas-shell` | `outerMorph` | the whole page frame |415| `topbar` | `#atlas-topbar` | `innerMorph` | volume, world, lens, import |416| `legend` | `#atlas-legend` | `outerMorph` | search field, toolbar, tree, footer count |417| `dock` | `#atlas-dock` | `outerMorph` | count, flag, shortlist, and the card inside it |418| `detail` | `#atlas-detail` | `outerMorph` | one feature's card |419| `grid-navigator` | `#atlas-grid-navigator` | `outerMorph` | cell system, held cell, subgrid |420| `overview` | `#atlas-overview` | `outerMorph` | the corner locator's chrome |421| `viewport` | `#atlas-viewport-state` | `outerMorph` | the inert state node the seam reads |422| `island` | `#atlas-session-island` | `outerHTML` | the session record the seam reads, as JSON |423| `empty-state` | `#atlas-shell` | `innerMorph` | the library card |424| `import` | `#atlas-import` | `outerMorph` | the import that is happening, as one row |425| `mint` | `#atlas-mint` / `#atlas-mint-preview` | ordinary form / `outerHTML` | Earth zone, source and presentation authoring plus its bounded preview |426427**Nearly every region is an *outer* morph**, and that is the reading that428matches what the templates render: the element is morphed onto itself, which429keeps the first-paint bytes and the swap bytes identical and is the only swap430that can set an attribute on the region's own container. Three states are431spelled exactly that way — the sidebar's collapsed class on the shell, the432navigator's `hidden`, the detail card's `hidden`. The exceptions each have a433reason: the topbar's container carries nothing a swap moves; the island is a434`<script>` node with nothing inside worth preserving, so it is replaced rather435than morphed, because morphing one leaves its text where it was.436`internal/app/partials.go` is the table this one mirrors.437438**An import is one row, not a log.** The response is a stream — a hundred439megabytes takes long enough that silence reads as failure — but it is a stream440of *states of the same row*: the handler renders the whole region again, and441flushes it, for every state a run reaches, and the morph carries one row from442"choosing a bundle" to "installed" in place. The row's element id carries the443run's number, so a second import is a different element rather than the first444one's leftovers. Three things follow, and they are the point of the445arrangement: a cancelled picker is **silent** (its last render holds no row at446all, and the section renders itself `hidden`), a run that ended well marks its447last row and the stylesheet fades it out on a delay, and a refusal stays until448the next import replaces it — it is the only account of what went wrong, and449nothing else on the page will say it. An append swap, which is what this was,450made every state of every run a line of its own that outlived the run, the451import, and everything short of a full page render.452453**The legend is one region because it is one answer.** Every filtering move454changes the rows, the isolate chip and the footer count together; three455regions would be three ways for them to disagree.456457**The card is nested inside the dock and is still its own region.** That is458where it belongs on screen, and `select` moves the card and the list together,459so a dock render carries the card with it and the two cannot disagree about460what is open.461462`viewport.tmpl` defines two templates. `viewport` is the partial — the state463node alone, because that node is what an interaction moves. `viewport-surface`464is the shell's alone and is never swapped.465466### 4.2 The envelope467468Every `/session/*` POST answers with an `<hx-partial>` set covering **exactly**469the regions that interaction touches — never a page-wide refresh standing in470for knowing what moved.471472```html473<hx-partial hx-target="#atlas-legend" hx-swap="innerMorph">…</hx-partial>474<hx-partial hx-target="#atlas-dock" hx-swap="innerMorph">…</hx-partial>475```476477The target and swap are spelled `hx-target` and `hx-swap`: htmx 4 reads an478`<hx-partial>` through the same attribute vocabulary it reads everything else479through, and an unprefixed `target` is silently no target at all. The region480names and the element ids either side of it are the same as they ever were.481482Morph swaps are what let scroll position, focus, and open `<details>` survive a483re-render. **The seam's surfaces are morph-skipped**: `<atlas-viewport>`, the484map pane and the overview's canvas holder carry `hx-morph-skip-children`, htmx4854's own spelling of "touch my attributes, never my internals". Tearing down a486WebGL context mid-gesture is not a re-render.487488### 4.3 The concern table489490One route per concern, each declaring the regions its answer covers. Reading491the regions column downward is the fastest way to see what the application492thinks is coupled to what.493494| `POST /session/…` | Fields | Regions |495|---|---|---|496| `world` | `world` | topbar, legend, dock, overview, viewport |497| `lens` | `lens` (the lens's name) | topbar, legend, dock, overview, viewport |498| `collections` | `collection`+`visible`, or `section`, or `all`=`show`\|`hide`, or `hidden` (repeated) | legend, dock, viewport |499| `sections` | `section`+`open`, or `all`=`fold`\|`unfold` | legend |500| `expand` | `collection`+`open`, or `all`=`fold`\|`unfold` | legend |501| `labels` | `collection` + `flip`, or `collection`+`policy` (empty clears) | legend, viewport |502| `solo` | `collection` or `section`; neither means show everything | legend, dock, viewport |503| `search` | `q` | legend, dock, viewport |504| `highlight` | `feature` (+ optional `on`, or `only`=`1`), or `all`=`clear` | legend, dock, viewport |505| `dock` | `open`, `byHand`, `section` | dock |506| `select` | `feature` (empty closes), `focus` | legend, dock, detail, viewport |507| `grid` | `system`, `cell`, `subgrid` | grid-navigator, dock, viewport |508| `overview` | `docked` | overview |509| `sidebar` | `open` | shell |510| `view` | `world`, `x`, `y`, `zoom`, `rotation` | — answers `204` |511| `reset` | — | — answers `204` and `HX-Redirect` back into the volume |512513Every request carries `volume`, declared once on the shell as514`hx-vals:inherited`. A volume that is not installed is a `404`; a malformed515slug or a missing required field is a `400`.516517`view` is the camera report: the seam's one debounced upward whisper, answered518with `204 No Content`, because swapping anything in response to a settling519camera would fight the reader's own hand. The other upward flow is the pick:520the seam resolves a canvas hit and submits the *identity* through an ordinary521`POST /session/select`.522523**`reset` is the one concern that answers with a page**, and it earns the524exception by deleting the record every other answer is rendered from. It is the525blunt reset behind ⌘R (§4.5): the volume's `volume.<slug>.json` goes whole —526what is hidden, folded, unfolded, highlighted and labelled, the search, the527panel, the card, the grid, the index, the corner locator, every world's camera528— and the next load reads no record at all, so §3.1's arrangement is529synthesized fresh. Nothing here writes defaults; forgetting *is* the reset.530531The last-volume pointer is deliberately **not** touched: the reader is not532leaving the volume, they are standing in it with nothing remembered, so533`app.json` stays and the answer sends them back to `/v/{volume}/{world}` — the534world read out of the record before it goes, the volume's first world when the535record named none, and `/` when the volume has no world to name. It is spelled536as the `HX-Redirect` header htmx acts on itself, because a `303` would be537followed by `fetch` and swapped into a page that asked for no swap, which is a538reset that appears to do nothing at all.539540**`labels` is spelled as a flip, not a destination.** The policy turns over,541and if the other word is what the producer curated anyway the override has542nothing left to say and is dropped rather than stored. That is what keeps a543ladder turned over and back from leaving overrides behind it — and it is why544the route needs the world, not just the id.545546### 4.4 Template rules547548- **One file per region**, named for the region.549- **No `hx-on`.** Every interaction is an `hx-*` attribute naming a route. The550 only glue on the page is the seam's boot module.551- **Explicit inheritance.** `hx-vals:inherited` and `hx-swap:inherited` are552 declared once, on the shell, and nothing else inherits.553- **Templates render; they do not decide.** Every display decision runs in Go554 first (§7). A template that needs an `if` about what a collection *means* is555 a decision in the wrong place.556- **`hx-*` lives in templates only.** The routes, the region names, the partial557 envelope and the viewport state node are all framework-neutral, so replacing558 HTMX would be an afternoon in one directory rather than an architecture559 event.560561### 4.5 The keyboard562563Every shortcut that moves discrete state is a route rather than a handler: an564`hx-trigger` naming the key and the element it is heard on, declared once in565`shell.tmpl` — the one region a swap never replaces from underneath — and566nothing else. There is no `hx-on` on the page and no listener behind it.567568| Key | Route | Does |569|---|---|---|570| `⌘G` | `grid` | cycles which cell system divides the map — **above the editable guard** |571| `⌘R` | `reset` | throws this volume's remembered state away and reloads it fresh — **above the editable guard** |572| `⌘⌥B` | `dock` | folds the panel beside the map, and unfolds it; two spans, each asking the fold button which way it is pointing |573| `⌘B` | `sidebar` | puts the index away |574| `G` | `grid` | opens the grid and closes it |575| `Space` | `grid` | divides the chosen cell, or stops dividing it; also from inside the address field |576| `Escape` (on `#map`) | `select` | closes the card |577| `Escape` (on the window) | `grid` | telescopes the grid out one level, then out of the grid |578579Every filter says three things, and the page was broken for want of all three:580**whose keystroke it is** (`kbd-editable`, so a reader typing "g" into the581search field does not put a grid up); **that the key is answered** (`prevent`, which htmx runs only when the582filter passed, so a shortcut swallows the keystroke it acts on and nothing583else); and **that the key was pressed rather than held** (`!event.repeat`, so an584autorepeating key is not sixty writes a second down a route that writes a file).585586Two shortcuts stand **above** the guard. ⌘G587because switching cell systems is most wanted from inside the token field; ⌘R588because a reader whose page is misbehaving reaches for reload from wherever the589cursor happens to be. ⌘R is also the one place `prevent` takes something away590and hands it straight back: the browser's own reload is suppressed, and the591route answers with the same page fetched again over a record that is now gone.592593The other half of the keyboard — holding `Z` for labels, `⌘K` to focus the594search, flipping panes, stepping the zoom, swallowing the webview's context595menu — is **not here and must not be**: none of it moves discrete state, and596one of them cannot be a request at all ([render-seam.md](render-seam.md)).597598---599600## 5. The events stream601602`GET /events?volume={slug}` is Server-Sent Events. The connection opens with a603`: atlas` comment, so a client knows it is live rather than merely accepted.604Two event names, and no more:605606```607event: catalog608data: <hx-partial hx-target="#atlas-topbar" hx-swap="innerMorph">…</hx-partial>609610event: refresh611data: <hx-partial hx-target="#atlas-shell" hx-swap="innerMorph" hx-get="/v/tunic/world"></hx-partial>612```613614- **`catalog`** — the library's composition moved. Carries the regions that615 list it. Sent to every connection.616- **`refresh`** — the one directive. The volume this connection is watching now617 serves a different build, every URL under its old stamp is gone, and the page618 has to be fetched whole rather than patched. Sent **only** to connections619 watching that volume.620621The page takes the plainer road of re-fetching its own URL on either event: a622page that re-reads where it is cannot end up half-patched, and both events mean623the library moved under it. The bodies stay in the contract for a client that624wants the cheaper path.625626There is no event for "a file appeared in the library directory", because627nothing watches it. An import is what triggers a rescan, and the rescan is what628produces these events.629630---631632## 6. The state island633634The explorer page carries the session it was rendered from as an inert JSON635script node:636637```html638<script type="application/json" id="atlas-session-island">{…}</script>639```640641It exists for one reason: the application's account of the arrangement has to642be diffable against the seam's, key for key, without either half guessing at643the other. **The key names below are the contract** (`internal/app/island.go`644is their home). Where the arrangement is stored is this application's645business; what the island says about it is not.646647```json648{"last": "bend-or",649 "entry": {"volume": "bend-or", "world": "2026-08-02", "lens": 0,650 "center": [4096, -4096], "zoom": 1.34,651 "hidden": [], "collapsed": ["zones"],652 "expanded": [39191589, 50985093, …],653 "labels": ["39191589=quiet"],654 "overviewDocked": false, "dockFolded": true, "dockDismissed": false}}655```656657Shape notes: ids are emitted as the numbers a payload declares them as but658sorted as the strings they ride the DOM as;659`labels` is the override ledger, one `"<collection>=<policy>"` per entry,660sorted; `lens` is the index; `center` is rounded to whole world units and661`zoom` to three decimals, so two accounts of one camera are diffable without a662normalizer in between.663664`tests/island` is the suite. It drives session POST sequences through the665application's own HTTP surface, over the committed corpus — the legend, solo,666search, label-policy, lens, grid and overview concerns — and holds the island667to its contract key for key, in both directions: a key the contract promises668and the island lacks is a hole, and a key the island invents is an invention.669670### 6.1 What is seam-side, and why671672Two of the twelve keys **cannot be produced server-side**, and are not forced673to be:674675- **`center`** and **`zoom`** are the chart's camera. They are arrived at by676 fitting a raster to a window and depend on the viewport's size, the lens's677 own depth, and the fit the seam computes. No amount of session state produces678 them. The server's only honest relationship with them is the one issue #5679 §4.1 describes: the seam reports a settled camera upward, debounced, at680 `POST /session/view`, and the server stores it and hands it back. Until the681 seam has reported one they are `null`. The island suite posts a camera and682 then checks the round trip, so what is proved about these two is the echo,683 not the origin.684685**The grid cull is not one of them, and was for a while.** A held cell narrows686what stands exactly the way a highlight does, and the count above the panel has687to be the count the map is drawing — so the server has to answer "is this688feature inside the held cell" without asking the seam, which the third upward689flow §5 of [render-seam.md](render-seam.md) forbids would be the only other690way. `internal/app/cells` is the smallest piece of one system's arithmetic that691answers it: the recursive halving and nothing else, gated against the shared692cell vectors (`analysis/testdata/cells/`). The cost — a second copy of arithmetic693`analysis/cellsystems` also holds — is written down in that package's own694comment, and the file is the first thing a Go twin of the analysis lane would695delete.696697One thing still belongs to a lane, and is recorded here rather than papered698over:699700- **The footer's "in view" half.** The footer's full sentence is701 "N of M features in view", and N is the count inside the camera's extent —702 seam-side by construction. The server renders M; refining it to the full703 sentence is the seam's, once there is a camera to ask.704705---706707## 7. Display logic708709All of it runs in Go, once, before a template is handed anything (issue #5710§4.5). It lives in four files beside the handler, and reads the conventions711only through `format/semconv` — an `atlas.*` string literal outside the712registry is a depcheck failure, not a review comment.713714| File | What it decides |715|---|---|716| `world.go` + `vnext_runtime.go` | the semantic model: Presentation applied to FeatureSets, durable string identities, direct coordinate-space geometry, typed properties, relationships, provenance, the parent chain and containment |717| `legend.go` | the tree — sections, rows, counts, the label ladder, the feature index order, isolating, the solo chip, the opening arrangement |718| `filter.go` | what stands: the hide set, the search, the AND-across/OR-within highlight filter, the shard, the counts and the words above them |719| `view.go` | the card, the dock, the grid and overview chrome, the viewport state node |720721Three rules worth stating because they are easy to get subtly wrong:7227231. **Highlighting reads AND across collections, OR within one.** Two districts724 highlighted widens the question; a district and a subwatershed narrows it to725 the ground they share. The corpus city is the test bed: nine shape726 collections, and highlighting one boundary plus one waterbody takes the727 drawn count from 213 to 148, because none of the 65 annotated places stands728 in both. Two answers are exempt from the cull — the feature the reader has729 open, and one they are searching for by name — because both were asked for730 rather than merely drawn.7312. **A search narrows points and never the ground.** Searching for a place has732 never taken the ground out from under it, so a shape answers the search in733 the list and not on the canvas.7343. **Containment is boundary-inclusive with a pixel of grace.** A pin dropped735 on a zone's border was put there to mean the zone, and exact point-in-polygon736 arithmetic would flip it out over the width of the line it stands on.737738**One zone, exclusively, is a fourth way to ask.** Isolating a collection or a739section answers the different and heavier question of putting every other740collection away; `only`=`1` names one feature and makes the highlight set741exactly that feature (`session.go`, `applyHighlight`). It is a move on the742set rather than a state of its own — the same shape as isolating, and derived743the same way, so a zone that is alone *by any route* wears the pressed control744and pressing it gives every highlight back. The ride-along holds: an745exclusive highlight still brings its own collection out of hiding, because746asking to look at a piece of ground and keeping it put away cannot both be747meant.748749The label ladder's one convention rule lives in the registry rather than in a750viewer: `semconv.LabelPolicy` gives a point collection curated as751`atlas.render.as = text` a speaking default, because floating names are labels752a producer pinned on rather than a different kind of thing to draw.753754### 7.1 The stylesheet system755756`internal/app/assets/css` is a token-first, one-file-per-region system,757carried as an asset (issue #5 §9). Everything this application's markup asks758of it beyond the carried files lives in one file, `chrome.css`: the region759containers carry ids of their own, the seam's custom elements need somewhere760to stand, and the import row — including the delayed fade that takes it away761when a run ended well, which is an animation and not a script. The detail card762is an outerMorph region (§4.1) and renders its own `hidden`, so763`pin-detail.css` works verbatim. The visual identity is neutral dark chrome,764palette as accents.765766---767768## 8. The development loop769770```sh771atlas dev # 127.0.0.1:7433, chrome read from the working copy772atlas dev -bundles DIR -root . # against another library773atlas dev -seam-watch # and the seam's bundler beside it774```775776`atlas dev` is `atlas serve` with two additions and no third. Templates and777stylesheets are read from the working copy instead of the binary and re-parsed778the moment a file is written, so a template edit is one refresh away rather779than one rebuild away; writes are coalesced over 60 ms, because editors write a780file three times per save. A tree that will not parse is reported and the last781set that parsed keeps serving — an unbalanced `{{if}}` halfway through an edit782is a normal thing to type, and taking the page down over it would make the loop783worse than a rebuild.784785**The watching lives in `cmd/`, and nowhere else.** `internal/app` is one pure786`http.Handler` that touches no filesystem; what it exposes is787`templates.Reload(fs.FS)` and `assets.Reload(fs.FS)`, and building an `fs.FS`788out of a directory is the command's business. That is the hostenv rule789surviving having a development loop.790791`-seam-watch` runs `npm run watch` in `render/` beside the server, streaming792the bundler's output into the same event stream. A tree with no793`render/package.json` — the deletability principle exercised — is told so at794`info` and the server carries on, because a missing seam is a served page with795one script tag fewer, not a failed run.796797---798799## 9. Logging800801`internal/logging`, per [`logging.md`](logging.md). The application speaks802`op=` values `scan`, `serve`, `install`, `session`, `events`, `catalog`,803`render`, and uses `volume`, `world`, `stamp` and `path` from the shared804vocabulary. `atlas serve` and `atlas dev` print their address to **stdout** — a805script starts the host and reads where it landed — and everything else to the806stream on stderr.807808---809810## 10. The pragmatism clause811812Issue #5 §4.3: HTMX ownership is a means, not a loyalty test. If an interaction813proves laggy or awkward as a round trip, it is re-classified by editing the814issue's interaction inventory — never patched with ad-hoc client code. Three815things are worth watching, recorded here so the edit has evidence behind it816rather than a feeling:8178181. **Search-as-you-type.** Debounced 150 ms on the client attribute and819 answered with three region partials. On the city (213 features) and the820 games (up to 1,400) the round trip is comfortable; on a volume an order of821 magnitude larger the legend partial is the expensive half, because it822 re-renders every row to move one count. If it ever bites, the fix inside823 this architecture is a narrower region — a `legend-count` region — not a824 client-side filter.8252. **The feature index.** A shape row's index is rendered whether or not the826 row is unfolded, which is what827 keeps a feature reachable by name without unfolding first. It is also the828 largest thing in the legend partial.8293. **The grid navigator's text field.** Typing a cell address round-trips per830 keystroke like the search. It is a short string and a cheap region, but it831 is the interaction most likely to feel like typing through a straw, and it832 is the first candidate for the hybrid bucket if it does.833834None of the three has been re-classified. They are listed because the clause is835worth nothing without a habit of writing down what it would apply to.836837---838839## 11. What is still named rather than built840841Named here so a stub is never mistaken for a decision — and so that a wave842which lands moves out of the list rather than sitting in it as folklore.843844- **The WASM service-worker host.** §1's third row. Nothing is built for it;845 the hostenv discipline is what keeps it reachable.846- **The footer's "in view" half.** §6.1.847848The seam's own list of what it has not proven is `docs/render-seam.md` §10, and849it is the one to read for anything below the `<atlas-viewport>` boundary.