clone

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

Clean-room rewrite: a hypermedia app around the .atlas format (issue #5) (#6)

FelineStateMachine committed 3f9d78a
* golden: the boundaries learn to say no for themselves

The dependency rules of issue #5 §3.2 are the kind of thing everyone agrees
with and nobody remembers at the moment it matters. This lands them as static
analysis instead: five analyzers on the go/analysis framework, run by one
driver, each refusing an import edge the architecture already refused in prose.

The rules are the lane import matrix (format is stdlib-only; generate and
enrich never see each other, the app, or the harness; the workbench reads
enrich/maturity and shells out for the rest; nothing imports render), the
clean-room rule that keeps the new layout from linking against the tree it is
supposed to be replacing, hostenv purity so the handler stays one portable
http.Handler, network confinement so only generate/crawl can reach outward,
and semconv discipline so an atlas.* key is spelled where its checker lives.

Two things shape the driver. Every message names the contract and cites the
section, because a violation should teach a boundary rather than merely block
one — that is §9's point, and it is written for agents as much as for review.
And with no pattern given, depcheck scopes itself to the clean-room roots that
exist on disk: the pre-rewrite tree is the oracle, not the subject, so a rule
about a lane nobody has written yet passes by having nothing to say. Today
that means the harness itself is the only package under analysis, and it is
green.

The matrix is a pure function of two package paths, so the whole thing reads
as a table in the test. The escape hatch is a //depcheck:allow pragma that
must name a rule and carry a reason; an unexplained one is itself a finding.

* golden: one command that says what is proven and what is not

The gates of issue #5 §6 land as a single scoreboard before there is anything
to score. Format round-trips, generate ⊕ enrich reproduction, analysis
vectors, the parity tour, HTTP replay and depcheck run in that order — the
order the rewrite builds in — and each one that has no lane to judge yet
reports SKIP naming the milestone it waits on. A run where five suites skip is
green, and reads as a list of what nobody has proven yet, which is the useful
thing to print at this stage.

Only depcheck is real today, so the scoreboard's single PASS is the boundaries
checking themselves. The others carry the command they will run and the file
that will hold it, so the milestone that lands a lane also flips its gate on
by writing that file; -suites=all attempts them anyway, on purpose, for the
moment you want to watch a new gate go red first.

waivers.json arrives empty and is printed on every run. Divergence discipline
is the reason the harness exists at all: goldens are never edited to match the
candidate, so every accepted difference is an entry here with a written reason
and shows up as a standing cost in CI. An entry without a reason fails the run
outright — that is an edited golden wearing a waiver's coat.

The Makefile carries the wiring and nothing else; the existing build recipes
are untouched.

* golden: the same boundaries, written where they cross into TypeScript

depcheck can only speak about Go, and two of the four lanes are not Go. This
is the other half of §9: a flat ESLint config for analysis/ and render/,
carrying the boundaries the issue already states — analysis touches no DOM and
nothing app-shaped, render imports only analysis and itself, fetch lives in
the seam's data layer, and console.* outside the log module is a violation
because the browser console is one end of the single event stream.

The config is written before the lanes exist, which is the point: when M6
starts writing them, the boundary is already there to push back. Every message
names the contract and cites the section, the same shape depcheck uses, so a
refusal teaches rather than merely blocks.

It is deliberately not wired into the existing frontend build — that tree
keeps its own rules — and it needs eslint plus typescript-eslint installed
before `make lint-lanes` does anything. M6 owns three open ends the skeleton
leaves visible: the analysis package's real import specifier (declared in the
render allowlist), type-aware linting once tsconfigs exist, and the seam's
authored line budget as a warning.

* ci: the gates run on every clean-room push, and say what they cost

A new workflow rather than a change to CI: that one builds and ships the
desktop app and needs GTK, WebKitGTK and the frontend toolchain to do it. This
one judges the rewrite, needs nothing but Go, and runs the same `make golden`
a developer runs locally — one entrypoint, no CI-only path that can drift from
what anyone can reproduce. It is green from today: five gates skip because
their lane does not exist, depcheck runs for real, and the harness's own tests
run first because the import matrix is code too.

HARNESS.md is written for the reviewer §6 asks about — the one who should be
able to read golden/ and know what "correct" means without the old code. It
says what each gate checks and which milestone lights it, what each depcheck
rule refuses and why, and how the escape hatches work: the //depcheck:allow
pragma in source and the waiver file for fixtures.

The waiver process gets the space it deserves, because it is the rule the
whole instrument rests on. A golden edited until the new code agrees with it
has stopped being an oracle and become a transcript. So: read the diff first,
assume the candidate is wrong, and if the difference is real, write down why
in the same commit — never step 4. And the note that this ends: once parity
passes, the oracle is archived and the never-edit-to-match rule retires with
it. The guardrails do not; they enforce an architecture the project keeps.

* format: the conventions registry stands on its own

semconv moves under format/ as a package a stranger can import: the
nineteen atlas.* keys, their entities and stability tiers, the value
checkers, and the behavioural helpers a reader actually needs -- render-as
and the equirectangular mapping, apply and invert.

The asymmetry is now stated rather than implied. Validate and Check are
the producer's gate; EntityOf reporting false is the reader's cue to skip
an attribute, and a test says so, so the lenient half cannot quietly
become strict. LabelPolicy joins the helpers because "only areas curate
their labels" was a rule every consumer was re-deriving, and EquirectOf
because reading a world's declared mapping out of its attributes is the
same three lines everywhere.

The prose twin moves to docs/semconv/REGISTRY.md, where the documentation
end-state wants it, and the agreement test now also checks that both
twins claim the same vocabulary version. The generated single source
arrives later; until then this test is the agreement.

* format: the container, read and written from first principles

format/bundle is the .atlas file as a library: manifest, ATLASLOC codec,
stamps, validation, and the registry fold, depending on nothing but the
standard library and the conventions registry beside it.

Three things changed shape in the rewriting, none of them on the wire.

The reader splits in two. NewReader works over any io.ReaderAt, so the
package no longer assumes a filesystem -- a mapped file, a buffer, an
object store's ranged reader all open the same way -- and Open is the
convenience over it. That is what will let hostenv own the I/O without
this package following it there.

The codec grows a borrowing view. OpenPacked returns column accessors
over the caller's bytes and allocates nothing per location; UnpackLocations
is now the whole-payload face of it. The view also walks the title offsets
once at open, so a corrupt payload is refused rather than met with a panic
somewhere downstream. The bytes themselves are unchanged: every packed
payload in the installed library decodes and re-encodes identically.

The registry separates the fold from the filesystem. Newer, Fold,
Shadowed, Changed, and BuildIndex operate on descriptors and touch
nothing; Scan, Install, and WriteIndex are the thin walk above them, and
the watching, the snapshot swapping, and the catalog all belong to the
application rather than to the format. Install reopens and revalidates
what it wrote, because the copy is what will serve.

One thing the old tree did that surprised: the stamp joins a part's name
and hash with a single space, so a part name holding a space would make
the sum ambiguous. Widening the separator would restamp every bundle in
every library, so the constraint is documented and pinned by a test
instead.

* docs: the system gets one voice, and a place it is written down

internal/logging is the slog foundation: the level policy, the shared
attribute vocabulary as constants and attribute constructors, and the
CLI wiring -- text on stderr by default so piped stdout stays clean,
--log-json for a machine, --log-level or ATLAS_LOG to open up debug, with
the flag beating the environment because an explicit instruction beats an
ambient one.

It sits under internal/ on purpose. format/ stays stdlib-pure and
log-free, and flag parsing was never the format's concern.

Two small decisions worth stating. An unreadable level name is refused
rather than quietly treated as info: a run that asked for debug and got
info has been lied to. And Setup is called explicitly from a main, never
from an init, so importing this package gets a caller nothing they did
not ask for.

docs/logging.md is the prose twin -- what each level means, when to reach
for warn rather than error, the nine keys and what each names -- and a
test holds the two to the same vocabulary, the way REGISTRY.md and the
conventions registry hold each other.

* golden: the capture programs that read the reference tree

The rewrite is measured against what this tree does, so what this tree does
has to be written down before it stops running. These are the programs that
write it down: they read the installed .atlas library through
internal/bundle, and the crawl archive through the same translator chain
tools/generate uses, so a fixture is what the application itself sees rather
than what a second reader made of the file.

survey classifies the library and answers which build serves a slug. bundles
extracts a volume: manifest and payloads canonicalized, the packed locations
unpacked, the icons hashed, and a per-pyramid tile inventory that digests
each tile's decoded pixels as well as its bytes -- a re-encode of the same
picture has to compare equal, and only the pixels can say so. translators
captures what each source's translator makes of an archived capture. measure
structures internal/measure's own reading of each fixture. http records the
data plane from the headless build. capture.sh runs them in order against a
registry holding exactly the fixture set, and stops everything it started.

canon is the one JSON shape all of it is written in: keys sorted, numbers
carried as the source wrote them, so two captures of the same content
produce the same bytes.

* golden: bundle fixtures for five of the six shapes a volume takes

One volume per shape the pipeline writes, each verified from its own payload
rather than assumed from its name: tunic is the plain MapGenie game;
cyberpunk-2077 carries a real merge ledger, Piggyback as origin and IGN Wiki
as donor, with matched, added and held pins and an aligned second lens;
fallout-new-vegas is the split sheet, thirteen worlds of which eight are
insets naming their parent; mars is the sphere, the one volume declaring an
equirectangular surface; and zelda-tears-of-the-kingdom is the lens-sharded
one -- three lenses over one ground carrying shards 1783, 1784 and 1785.
The volumes first proposed for that last slot carry no shard on any lens.

Each fixture is the build that would serve, picked by the registry's own
newest-per-slug fold, with its stamp recorded so a later capture that lands
on a different build says so. The manifest and the three world payloads are
committed canonicalized, the packed ATLASLOC payload unpacked to a row per
point in packed order, the icons as a hash list, and every tile as a name, a
content hash and a digest of its decoded pixels. No raster is committed.

The sixth shape -- the city, with its basemap, its municipal layers and the
national hydrography joined by HUC12 -- is captured but withheld: the only
curated city on this machine is registered from the gitignored
cities_local.go, and it is a personal location. Its fixtures go to
golden/fixtures/private, which git ignores, and its name is in no committed
file. What the set loses by that is written down in the README.

* docs: the format written down so a stranger could implement it

docs/format.md is the normative .atlas v3 spec, written from the
artifacts rather than from the code that reads them: the container and
its compression policy, the manifest schema down to which fields are
omitted when zero, the world payload, the ATLASLOC byte table with every
column's offset, stamps and file names and the build ordering, the
validation a producer owes, and the registry directory.

Two things it insists on that were previously only true by accident. The
manifest's encoded bytes are part of the format, because the stamp is
taken over them -- field order, the two omit-when-zero fields, compact
encoding, and the \u escaping of & are all load-bearing, and an encoder
that differs anywhere renames every bundle it writes. And the collections
array's order is load-bearing, because the packed payload's owner column
indexes it; that was a comment in one file and is now a rule.

The exit bar for this milestone is that a third party could write a
reader from this document alone, so it ends with the checklist they would
work through, in the order they would work through it.

* golden: translator fixtures, one archived capture per source

Each source's newest archived capture handed through the same chain of
MaybeTranslate calls tools/generate uses, in the same order, and the
interchange document that comes out committed canonicalized. The captures
chosen are the ones the bundle fixtures were composed from: mapgenie's TUNIC,
ign's and piggyback's Night City, nasa-trek's Mars.

The archived capture is not committed -- it is named by its content hash,
which is enough to prove two runs read the same input. The mapgenie capture
is recorded as a passthrough, because for a MapGenie-shaped snapshot the
translation is the identity and the fixture should say so plainly.

These pin behavior, not shape. The rewrite's interchange document may be
spelled differently and equivalence is judged at the composed bundle; what
these catch is a source's semantics moving quietly -- an id space that stops
being stable, a projection that shifts, a category that stops declaring
itself.

arcgis-hub is missing, and its absence is recorded rather than worked around:
the archived capture is there, but the translator refuses a city it is not
curated for, and the only curated city on this machine is the withheld one.

* golden: the measurement report, with its defects annotated beside it

tools/maturity's report over a registry holding exactly the committed fixture
set, verbatim, and the same numbers structured as internal/measure's own
Build with the figures the reports derive from it.

Nothing in the output is corrected. NOTES.md beside it says what is wrong
with it: DescribedPct divides a count of text entries by a count of point
features and so reads above 100% for any world whose shapes carry prose;
Percent truncates rather than rounds; Depth and DepthTiles conflate every
pyramid in a volume, so a split sheet's deepest level counts tiles from
thirteen worlds' pyramids at once; Groups counts a group once per world; and
the printed merge lines drop the world each account belongs to, which is why
a split sheet prints thirteen indistinguishable MapGenie lines.

The >100% defect is the one to watch: no committed fixture shows it, because
the only volume whose shapes are described is the withheld city. A rewrite
that quietly fixed the arithmetic would pass every committed golden and still
have changed behavior, so the defect is written down here where the harness
cannot see it.

* golden: the data plane as it is served today

Every recorded request and its answer from the headless build serving the
fixture registry: the catalog, and for each volume a world payload, its
packed locations, its deferred prose, a category icon and a tile, then a
range request, eight refusals, the application shell and the two shell assets
the headless host answers for itself. Status, response headers, body length
and body hash on all of them, with small text bodies quoted.

The sampling is derived from what is served rather than from a table kept in
step by hand: the catalog names the volumes, and each volume's first world
payload names its lenses and its icons, so the transcript follows the road a
client follows.

One finding is worth reading before the file. The data plane does not serve
ranges. Tiles are stored uncompressed so that it could, and the rewrite issue
describes them as served by byte range, but the router sets a length and
copies the entry: a Range request is answered 200 with the whole body and no
Accept-Ranges. The transcript records that, so the rewrite either keeps the
behavior or changes it deliberately rather than by accident.

Two values would otherwise move with the machine and are replaced: the Date
header, and the bundles directory the catalog reports. Lengths and hashes are
taken after the replacement, so a capture run from a differently named
working directory produces the same bytes.

* golden: what the fixtures are, and how to capture them again

FIXTURES.json is the fixture set itself: which volume stands for which shape,
how that classification was verified, and each volume's serving stamp, file
hash and counts. README.md is the provenance -- the tree tagged
golden-reference, commit b8d28888 -- the one command that reproduces
everything, and a table of what each fixture pins.

Both say plainly what the set does not cover. The city fixture is withheld
because the only curated city here is a personal location, and with it go the
only path and area collections, the only inline geometry, the only label
policy, the only conventions-resolved standard icons, the only HUC12
membership in a packed payload, and the only demonstration of the >100%
DescribedPct defect. When a bundle exists for one of the public proof cities
the fixture should be captured from it and committed, and that paragraph
deleted.

The rule these files exist to protect is written at the top of the README: a
golden is never edited to match a candidate. A difference is waived with a
reason or it is a failure.

* golden: a filename is not a convention key

The semconv-literal rule matched "atlas.json", which is the manifest's
filename, the moment format/bundle landed and had to spell it. Every
registered key carries at least two segments after the prefix, so the
matcher now requires them, and the filename goes back to being a
filename.

* parity: the tour walks into the rooms it had never entered

The sweep covered the chart and stopped there. Three parts of the
application it never touched: the globe, which is a second camera on the
same data and answers every gesture the chart does; the library's import
and the reconcile that follows it; and the label-policy ladder, where a
reader's word overrides a producer's and is meant to be dropped again the
moment the two agree. A rewrite could have broken any of them without a
baseline noticing.

They are appended after the existing steps rather than woven in, so every
step that had a position keeps it and a diff against an older log still
lines up over the prefix. The globe reads through the seam the pane
already opened for this -- counts of tiles, cells, names and sprites,
never a handle on any of it -- and the flip out and back is the real
check, because it is a round trip through the declared projection and has
to land the chart where it started.

A snapshot now carries what the harness saw for itself beside what the
application volunteered: which pane is up, what the selects offer, the
label toggles as they were actually painted, and the saved arrangement
read out of storage. That half is DOM and localStorage and named seams,
so it survives the internals being replaced -- which is the whole point
of a golden that has to outlive the code that produced it.

A baseline is taken per volume, so the tour takes the volume it is about
rather than whichever sorts first, and the settle waits for two agreeing
polls instead of one: a fly-to that pauses at the top of its arc used to
read as arrived, and the next step then began over a camera still moving.

* golden: the fixture volumes, and what a snapshot promises

Six shapes a volume can take, one volume each, so a baseline that passes
across the set has met every structural case a reader can: a plain
single-source game, a game merged from two sources, a sheet split into
thirteen worlds, the one volume whose lenses carry distinct shards, the
sphere, and a city carrying points, paths and areas at once. Each was
chosen by reading manifests and world payloads out of the zips rather
than by reputation -- the three volumes suggested as lens-sharded all
turned out to carry shard zero, and the sharded one is elsewhere.

The city may not be named here, so its slot stands open in FIXTURES.json
with the reason, and the volume filling it today is described in an
ignored companion file and captured out of the way. The library a
baseline was taken from is part of what the baseline records, so the
public and private passes are two libraries rather than one; the public
proof city takes the slot over when it has a bundle, and that is a full
re-capture rather than an addition.

SCHEMA.md is the contract itself: every key a step emits, what it means,
which invariants the tour asserts on itself, and -- named, with reasons
-- the two fields that do not bind and the four blind spots that remain
after this pass. A reviewer should be able to read it and know what
correct means without the code that produced it.

capture.mjs links exactly the pinned files into a library of its own so
the registry's newest-wins fold has nothing to choose between, and
--twice makes reproducibility a checked property rather than a claim.

* golden: a baseline for every shape a volume takes

Five logs, one per public fixture volume, each taken from a fresh launch
against the pinned library and each verified by taking it twice from two
fresh launches and diffing the pair. They agree on every field but two.

Those two count tiles fetched since the lens was chosen, and they measure
the route a run took rather than where it arrived: a browser that samples
one fly-to a frame differently fetches a tile the other never wanted, and
no amount of settling undoes a request already made. They stay in the
record -- a build that fetches four times as many tiles is worth seeing
-- and are named in SCHEMA.md as the only fields a comparison skips.

Two other instabilities were fixed rather than excused. The overview's
box is now read in whole pixels, and only while the sphere is up: over
the chart it is a stale leftover, because the overview memoizes on the
view's extent rounded to whole world units while writing its box from the
unrounded one, so two runs that finish a fly-to a fraction of a unit
apart keep whichever box their own last frame wrote. That is a real if
small defect in this tree, and it is carried here as an observation. A
golden is never edited to match anything.

* golden: the format gate, in both of its modes

The format-roundtrip suite of issue #5 §6 now has something to run. It reads
the committed bundle fixtures through format/bundle and holds the package to
what the golden-reference tree recorded, in two modes that are deliberately
different in what they can see.

Always-on, with no library present, it stands on the extractions alone, and
that is a real gate rather than a placeholder: every byte it compares was
written by the implementation this one replaces. A canonicalized manifest
parses into the clean-room types and re-encodes to the same canonical bytes;
the identity derived from it names the file the fixture set says the build
carries; each world's unpacked locations pack again to the exact byte count
and digest the extraction recorded, which is the codec checked against another
implementation's bytes without a bundle in hand; the inventories re-emit to
themselves, so the rollup rule used against real archives is the capture's
own; and the extractions reassemble into an archive that Reader.Validate
accepts, which runs the offline scan, the per-kind counts, the geometry rules
and every semconv rule over real captured payloads.

Registry mode opens the real .atlas of each fixture build and compares
everything the container hands back: the manifest byte for byte, because
those bytes are what a producer stamps over; every recorded part hash; every
payload canonicalized to its extraction; the packed payloads; the icon and
tile inventories name for name; tiles stored uncompressed. A directory
holding none of the fixture builds skips, since it is not the library they
came from, and one holding some of them fails on the rest, since a partial
library is a broken oracle rather than a smaller one.

Stamps are not asserted anywhere, and STAMPS.md says why: a tile pyramid's
part hash is the pyramid's derivation stamp, which the tile pipeline computes
and the archive never carries, so a reader cannot recompute the sum. The file
tracks the aspiration per fixture, names the proxies enforced in its place,
and records that M2's tiles lane is what could close it.

* golden: the first gate stops skipping

format-roundtrip is ready, so the harness runs it instead of announcing what
it waits for. M1's exit criterion reads "format goldens green"; until now
nothing turned that sentence into a check that could fail.

HARNESS.md gains the gate's own section — what each mode proves, why a
registry directory deepens a run rather than changing it, and the pointer to
STAMPS.md for the one invariant this gate does not prove. The sample run is
re-transcribed with two gates passing, and the sentence about unready gates
declaring their entrypoint now cites one that is still unready.

* golden: the event stream is a lane of its own

docs/logging.md names internal/logging as the one place level policy and the
attribute vocabulary are written down, and every lane is meant to narrate
itself through it. The lane table did not know the package existed, so it fell
to LaneOutside and the clean-room rule read any import of it as a reach into
the golden-reference tree -- which would have left the one shared clean-room
package unusable by the code written to use it.

It is a lane now: importable by everybody except format/, which depends on the
standard library alone and is refused by its own stricter rule, and permitted
to import nothing of ours in return, because that is what a package everyone
may depend on has to be.

* app: the host contract, and one operating system behind it

The application is going to be one pure http.Handler, which means everything
OS-shaped has to be named before any of it is written. internal/app/hostenv
declares the three things a host owes the handler: a VolumeStore that is the
library of .atlas files, a SessionStore that is bytes under a name, and a
PickFile that puts a picker in front of the reader. The package itself touches
no machine -- no os, no paths, no toolkit -- so a WASM service worker later
implements the same three interfaces over OPFS and mounts the same handler.

hostenv/oshost is the first implementation. Its library is a directory scanned
at construction and on import and never watched: a file dropped in from
outside appears at the next launch, which is the registry model decision 15
settles. The fold that picks which build serves is format/bundle's pure one;
what lives here is the walk, the readers held open so serving an entry is a
seek rather than a fresh open of a hundred-megabyte file, and the whole-
snapshot swap that keeps a request looking at one consistent library. Retired
readers close after a grace period, so a response already streaming out of a
build the rescan replaced finishes undisturbed.

Sessions are one file per record, written through a stage and a rename.
A host with nowhere to write gets hostenv's in-memory store instead, which is
what a dev server pointed at somebody else's library wants: it reads the
library and writes nothing near it. A host with no window refuses PickFile
rather than pretending the reader cancelled -- an import that cannot happen
and an import nobody wanted are different answers.

* privacy: the city in the examples is the public one

Four files carried a personal location as their example volume: the manifest
schema in docs/format.md, the logging vocabulary in docs/logging.md and its
test, and the registry fold's fixtures in format/bundle. The rule that keeps
that city out of the repository is not about where it is curated -- its
curation already lives in a gitignored file -- it is about the name, which had
quietly become the stock example a reader copies.

bend-or takes its place everywhere. It is a city the repository already names
out loud, curated in internal/arcgismap's public table from open data the city
publishes about itself, and it reads exactly the same in an example. Nothing
about the examples changes but the string.

* golden: the city slot, filled from the public proof city

The fixture set called for a city and had none it could commit. The only city
installed on this machine is a personal location curated from a gitignored
file, so its extractions went to golden/fixtures/private and the set went
without a volume that has paths, areas, inline geometry, a label policy, a
standard icon, or a described shape. bend-or ends that. It is a city the
public table already curates -- open data the City of Bend publishes about
itself, plus the USGS hydrography every curated city is enriched with by
bounding box -- and nothing about it needs withholding.

No bundle for it existed anywhere, so one was built rather than found: crawl,
tiles, generate, each unchanged since the golden-reference tag, into a
directory outside the library. The crawl is the only stage that reached the
network -- eleven datasets, seven from the city's hub through the download API
in true degrees and four from hydro.nationalmap.gov by the city's window --
and the basemap was not fetched at all, but rendered from the city's own
vector data, which is why the pyramid is reproducible offline from the
archived capture alone. The capture day is part of the identity: it is the
world's slug, it is in the tile-set path, and it versions the bundle. This
fixture is 2026-08-02, and re-crawling would make another world, not this one.

What the set gains: 104 paths and 44 areas carrying their geometry inline, 88
of those shape features carrying atlas.hydro.huc12 from the subwatershed
membership join, four national layers declaring the quiet label policy, a
standard icon resolved through the conventions, and the DescribedPct defect --
235% here, 153 described entries over 65 pins, because described shapes defer
their prose into the same .text payload the pins do. That defect was written
down in NOTES.md precisely because no committed fixture showed it; it is now
committed, and a rewrite that quietly fixes the arithmetic fails a golden
rather than passing every one of them.

The arcgis-hub translator fixture comes with it. It was skipped before for the
same reason the city was, and is captured here from the city's own archive,
which is a second archive beside the library's -- the capture script grew an
option for it, and lost the default that named the personal city.

Two gaps are recorded rather than papered over. The packed member column is
zero for every city pin: it is the id of a containing area feature, filled
from a translator's region_id, and the ArcGIS translator assigns none -- the
HUC12 claim rides on the shapes, not there. And http/transcript.json predates
this volume and still samples five; re-recording it would move the range
request and the eight refusals onto bend-or, which sorts first, so that trade
is left to whoever owns the HTTP replay gate. FIXTURES.json says so under
http.cityGap.

* golden: the test binary go writes is not a package anybody wrote

Adding the first tests under internal/app turned depcheck red on a file in the
build cache: the main package `go test` synthesizes to run a test binary
imports os, and it carries the path of the package it was written for with a
.test suffix, so hostenv's own test binary read as the application handler
reaching for the operating system.

Generated code is not a boundary anybody crossed. The three rule runners now
pass over a .test package, and the rules go back to judging what somebody
typed.

* app: the handler, both planes of it

internal/app is the application: one http.Handler over the hostenv contract,
serving the data plane the seam reads and the hypermedia the reader navigates.

The data plane is not this rewrite's to redesign. The catalog is composed from
whatever is installed at the moment it is asked for and is never cached;
content sits under a URL carrying the build's twelve-hex stamp, immutable for
a year, gone the moment a newer build takes the slug over -- which is the
client's cue to refetch the catalog. The extension table is the gate on what
may be served at all, and everything else is a plain 404. Byte ranges are
deliberately absent: tiles are stored uncompressed so that they could be
served, and the recorded transcript says the reference implementation sets a
length and copies, so answering 206 would be a better data plane and a red
golden. That is a change to make on purpose, with a waiver, when something
wants it.

The app plane is new. Real URLs replace hash routing, so a world can be
bookmarked and reloaded; / is a doorway that sends a reader back where they
were. One POST per interaction concern, declared in a table that also declares
which regions each concern's answer covers, so a response carries exactly the
partials the interaction touched -- the legend row and the solo chip and the
dock readout, not a page-wide refresh standing in for knowing what moved. The
camera report is the exception that proves it: the seam's one upward whisper,
debounced, answered with 204, because a swap in response to a settling camera
would fight the reader's own hand.

The session record is the server's half of the state-placement rule, versioned
so a record this build cannot read costs a reader their layout once rather
than being half-applied. The events stream carries two names and no more:
catalog, when the library's composition moved, and refresh, the one directive,
sent only to the pages watching a volume whose serving build changed -- their
URLs are gone and patching them is not possible. Nothing watches the library
directory; an import is what triggers a rescan, by decision.

The templates are region stubs. What is load-bearing already, and is what the
next wave must not drift from, is the names: one file per region, one element
id per region, one swap per region, declared in the partial envelope.

* cmd: atlas serve, the host with no window

The headless host is the successor of ATLAS_HEADLESS=1, and it is three lines
of wiring: a library directory, a place for session records, and the handler
over them. That is the portability claim made good -- everything OS-shaped is
here, in a command, and the application below cannot tell which host it is
under.

The address goes to stdout because a script starts the host and reads where it
landed; the narration of the same fact goes to the event stream on stderr, so
a pipe stays clean. Port zero by default, so a running application is never in
the way. No -data means session records live as long as the process, which is
what a dev server pointed at somebody else's library should do: read it and
write nothing near it.

The subcommand table is explicit and one line long. Every other lane's
operations land beside it the same way -- a function returning its entry, in
its own file, listed by hand.

* golden: the cell math, driven from plain node

The vectors have to judge an implementation, and the implementation they
judge until M6 is the one in frontend/src -- which cannot simply be
imported. grid.js holds the plan and its style tokens in the same file as
a DOM controller, so loading it pulls in a document, OpenLayers' map and
globe.gl, and none of that exists under node.

So four imports are swapped for stubs at resolution time, through node's
own loader: dom, detail, navigation, features. Everything else loads from
its real source file, unbundled and unedited, because a recording made
from rewritten source is a weaker recording. Every stub throws if it is
ever called -- a plan that only exists because a stub stayed quiet is not
a plan worth having.

The adapter is eight functions wide, and it is the one file that knows
where the implementation lives. Its applyGround writes a recorded ground
descriptor back into the globals the current systems read it from; in the
clean lane that becomes an argument pass, and this file, its hook and its
stubs retire together.

* golden: the vectors and the plans, as language-neutral JSON

Two families of fixture, both readable by a Go or a TypeScript build with
no knowledge of either implementation. 178 vectors over 9 grounds cover
what the contract promises: opaque ids, the stable child order, containment
that includes its boundaries, whole-or-nothing parsing, rings that close,
rings that stay continuous across the antimeridian, the poles a cell
circles, and the carry across systems at like precision. Every case is one
call from a table of eight.

The numbers are not self-recorded. The geohash values were derived from
the halving rules and the S2 ones are the Go library's own vectors, both
already written down in the frontend suite; capture.mjs carries those
literals as a checklist and aborts if the engine disagrees with one, so
what lands here is a hand-derived golden that happens to have been typed
by a machine. Those cases are marked.

The plans are the composite output, recorded for every grid-touching step
of every parity tour: the step's ground read out of the bundle fixture,
its system and held cell read out of the baseline's own grid state, and
the array that came back, cell for cell. The capture checks each plan
against the cells that baseline recorded on screen, which ties the two
golden families together at the point they describe the same moment.

The tour holds geohash at two depths and never cycles to S2, so the roles
and the system it leaves unexercised -- the leaf at either floor, a deeper
halving, S2 at the root, S2 on a polar face, S2's own leaf -- are captured
beside it on the same Mars ground, where a difference can only be the
systems and not the ground.

* golden: the analysis gate runs, six milestones early

run.mjs re-executes every vector and every plan against the current
systems and compares as JSON text: a vector byte for byte, a plan
positionally, cell for cell, because emission order is the contract and a
set comparison would hide a reordering. A failure names the index -- cell
12 of 32 -- and says so.

The gate did not wait for its lane, because it did not have to. The cell
systems already exist as the oracle, so the vectors judge an
implementation from the day they are captured, and M6 inherits a gate that
has been green the whole way rather than one nobody has ever seen run.
Pointing it at analysis/cellsystems is one import line, marked with a
banner, and the fixtures never learn which implementation answered.

Because the engine it drives today is the current tree's module, the gate
needs s2js and OpenLayers installed; the workflow installs them without
scripts, since nothing here bundles anything. The README says what each
family pins, what a ground carries -- field by field, against the global
each was read from, which is the brief for M6's de-globalized Ground --
and what the positional guarantee means.

* golden: the data plane, replayed

The http-replay gate judges now. Every exchange in the recorded transcript is
asked again of internal/app and held to the recorded answer: the status, the
recorded header set in both directions -- a header the reference did not send
is as much a difference as one it did -- and the body by length and SHA-256,
after the capture's own two normalizations.

It runs at whatever depth the machine allows, which is the honest shape for a
gate whose subject is content out of archives the repository does not carry. A
store built from the committed fixture manifests can answer the catalog and
every refusal, so composition -- field names, field order, volumes by title,
the stamped base URL, bundlesDir -- is byte-exact on any machine, CI included;
what needs a real archive is skipped with the reason printed rather than
counted as a pass. Point ATLAS_REGISTRY_DIR at a library holding the fixture
builds and the same test replays the bodies too: 35 of 38 exchanges identical
to the recording, byte for byte.

The other three are the first two waivers this project has ever written. The
old hash-routed shell at / is replaced by a page per world, by decision; the
built frontend under /static is M6's lane, and a host that mounted no seam
answers 404 -- which the replay asserts, so the waiver is a check of the
deletability principle rather than an unchecked difference. Neither waiver
excuses the exchange from being looked at, which is what golden/http/NOTES.md
is for, along with the one other decision worth writing down: the data plane
still does not serve byte ranges, because the recording says it did not, and
improving that is a change to make on purpose with a waiver in the same
commit.

* docs: the application written down while it is still small

docs/app.md is the route table, the session record, the two envelopes, and the
host contract -- written now, at the point where the shape is decided and the
templates are not, so the next wave fills in prose that already exists rather
than discovering it.

Everything in it that is contract is marked as contract: the region names and
their swaps, the sorted sets in the record, the two event names, the three
distinct answers PickFile owes a caller. Everything that is a stub is under
"next wave", so nobody reads a placeholder as a decision. The one thing the
document argues rather than states is why a GET writes state: arriving at a
URL is a choice however the reader arrived, and the session follows the
address bar rather than the other way round.

* app: the library writes only what it is asked to

An unused WriteIndex on the OS volume store was one method of surface nobody
had asked for. index.json is derived by whoever installs a bundle, from
format/bundle's own function; the store's contract is the four methods hostenv
declares.

* golden: the city fixture is read from where it was built

The parity fixture set kept a slot open for a city and had nothing public to
put in it. bend-or now exists, but it is the one fixture that is not in the
library: it was built for the fixture set by the pipeline rather than
installed by it, into dist/bundles, and the capture only ever looked in the
installed registry.

So a fixture entry may now name builtInto -- the repository-relative
directory the pipeline wrote it to -- and the link farm reads that entry from
there instead. ATLAS_GOLDEN_CITY_DIR overrides it, the same variable and the
same dist/bundles default golden/capture/capture.sh already uses, so a
machine that keeps its builds elsewhere says so once and both captures obey.
A missing bundle used to surface as a stat() stack trace; it now names the
file it wanted and, for the built one, the README section holding the four
commands that make it.

The slot itself is filled: the city entry carries the stamp f0feba1cd00c and
the evidence for calling it a city -- nine shape collections against every
other fixture's zero or one, 104 paths and 44 areas with inline geometry,
three collections declaring atlas.label.policy=quiet -- read out of the zip
rather than taken on the pipeline's word. Its mercator window without a
declared projection is written down too, because it is why the volume offers
no cell grid.

The private pass stays additive. The private city is no longer standing in
for a slot, so it joins the six rather than replacing one of them, and the
pass runs over a seven-volume library: a private baseline is a record of the
same tour over a volume that may not be named here, never diffed against a
committed one, and the machine holding it really does have seven volumes on
its select.

* golden: every baseline re-taken over the six-volume library

library.volumes is part of every snapshot, so a volume joining the fixture set
is a re-capture and not an addition. All six public baselines were taken
again from fresh launches with the city on the select, each twice, and each
pair diffed identical under the two advisory tile counters -- tunic 37 steps,
cyberpunk-2077 49, fallout-new-vegas 39, zelda-tears-of-the-kingdom 67, mars
59, bend-or 66.

The five older baselines moved at exactly three places and nowhere else:
library.volumes on every step, domNodes on every step -- one more option on
the volume select is one more element -- and game-second, which switches to
the first volume that is not the one open and now lands on the city, because
the select sorts by slug and bend-or sorts first. The city's own game-second
goes to cyberpunk-2077. Nothing else in any snapshot changed, which is the
useful reading: the library moved, the application did not.

What the city's baseline adds is the coverage the set was recorded as
missing. and-highlighted, labels-held-highlighted and and-cleared need a
volume with more than one shape-collection feature index, and no game or
planet has one; the city has nine, and the AND across two of them culls 213
features to 148 and gives them back. The label ladder starts with three
collections silent by curation rather than with nothing silent at all, so the
override, the held Z that refuses to revive a silenced collection, and the
flip back that drops the override rather than storing it are all read against
a real policy for the first time.

SCHEMA.md gains the city's column and loses the gap note that was waiting for
it. The gaps that remain are the honest ones: the native import picker, the
catalog that actually changed, the empty library, the pixels nobody checks,
and the globe, which is still reachable on exactly one volume.

* golden: the event stream is a foundation, not the old tree

internal/logging landed with the format core as the one place the system's
structured events are configured, but depcheck had no lane for it, so every
clean-room package that narrated itself was reported as reaching into the
golden-reference tree. The rule was right and its map was incomplete.

It gets a lane of its own rather than an exemption. Every lane may import it,
because one event stream narrates the whole system and a CLI run that says
nothing is not a feature; format/ still may not, because the stdlib-only rule
has already refused it and says so more clearly; and logging may import no lane
back, which is now a checked rule rather than a habit nobody wrote down.

* generate: an interchange document Atlas owns

The document that crossed the middle of the generate lane was MapGenie's API
response, transcribed as Go structs, and every other translator forged itself
into that shape: snake_case field names, a rigid groups-categories-locations
tree, regions standing outside it as a second kind of citizen, and three
fields squatted on somebody else's namespace to smuggle through what the shape
had no room for. A source was privileged by accident of being first.

internal/generate/doc is Atlas's schema instead, designed backwards from what
composition needs: worlds, lenses, collections, features, geometry, attributes,
provenance. Points, paths and areas are the same kind of thing in one ordered
array; a group is the heading string it always was; positions are latitude and
longitude in the volume's own projection; absence is zero, everywhere, matching
what the wire has always read zero as; and a source's publisher-specific
reading -- a legacy render field, a link syntax, a coordinate spelled as a
string -- is spoken once in the registered vocabulary and never travels.

Provenance is carried rather than recovered. A document names its source and
its id space, and every world names the archived bytes it was read from by
content hash and capture time, so a build's version comes from the capture and
two runs that name the same hash provably read the same input.

Icon artwork travels in the document. The old shape carried a key and left
composition to probe an archive directory for a file, which meant composition
had to know where captures live; now a source hands over what it found, and
composition packs what is used.

* generate: the capture archive, read through its own vocabulary

Years of captured history are data, not code, so the archive on disk is kept
exactly as it is and read behind a vocabulary that owes it nothing. A caller
asks for a volume's worlds, a world's captures, a capture's body, a volume's
artwork, and never learns which directory answered or what the register files
called their keys.

Two of the layout's habits are translated here rather than passed on. A
register entry naming no source is MapGenie, because the field was added after
the first crawler and its absence is not a gap. And a capture's volume and
world live in the path it sits at rather than in the record, so identity is
recovered structurally and nothing downstream inherits that coupling.

A volume the register names but nobody has crawled is not ready, not broken:
an archive is filled by hand, one volume at a time, and a caller skips what is
missing rather than failing over it.

* generate: curation moves out of the program and into a file

Which map a game opens on, which rim its markers need against its art, whether
one sheet is really eight places printed side by side, where two sources spell
one concept differently -- none of that can be derived from a capture. It was
six tables scattered through a program, and a judgement about a particular
archive belongs somewhere it can be read and argued with.

curation.json carries them, embedded so a build brings its own curation and a
composed bundle cannot depend on what happened to be on the operator's disk.
Every section states its reasoning beside its data, under keys the reader
ignores, because a table of seven game slugs and the word "dark" is unreadable
without them.

Content is golden-identical to the tables it replaces, but two were re-keyed.
Outsets and shard declarations were keyed by upstream numeric map ids, which
made an entry unreadable without the publisher's database in front of you and
unmovable to a second source describing the same ground; they are keyed by
volume and world slug now, which is Atlas's own vocabulary and survives the
source that happened to supply the ground.

* generate: pyramids, and the stamp that says how they were made

A lens is one raster pyramid picturing a world, and deriving one happens once,
into a tile set on disk. This reads the result: the register that says what
each pyramid is, and the rasters in the order a bundle writes them.

The load-bearing part is the derivation stamp. Every pyramid carries a stamp
over everything it was derived from -- the plan's shape, the content hash of
every source tile, a hash of the deriving tool's own source -- and composition
folds it into a bundle's stamp under one part name per pyramid, without reading
a single tile. That is what makes an unchanged volume cheap to notice: a
rebuild that would write the same bytes computes the same stamp without
decoding a raster, and the file it would write is already there under a name
carrying that stamp.

The relationship is one-directional and stays that way. Composition trusts a
pyramid's stamp and cannot recompute it, because the captured tiles it was
folded down from are no longer in front of it; a pyramid claiming nothing
stamps as nothing, which is the honest record. Derivation itself -- frame
discovery, the complete-level rule, downsampling, coverage bitsets, warps --
is the next wave, and the contract it must fill in is written down in
docs/generate.md rather than left as folklore in a program nobody has read.

* generate: MapGenie becomes an ordinary translator

A source is one publisher's reader, and the interface is two methods: say who
you are, and turn archived bytes into an interchange document. It is spelled in
plain arguments rather than a request struct so a source package can satisfy it
without importing the seam, which keeps registration a one-way edge -- the
registry knows the sources and no source knows the registry.

registry.go is the assembly point and the one file outside a source's own
directory allowed to name a source. It may name a constructor and nothing else;
if adding a source needs a second line there, the source is leaking.

The MapGenie reader is the worked example and now holds everything that used to
be spread across the pipeline on its behalf: the group-category-location tree
folded into collections, colours resolved category over group, coordinates read
whether the capture spelled them as numbers or as quoted strings, regions
gathered into an area collection of their own, the legacy display field spoken
once as a registered attribute, and the deep links written into the
publisher's own prose resolved to cross-references or removed -- because a link
syntax is a publisher's, and Atlas serves offline.

It refuses a capture whose kind is not its own. Reading another source's bytes
through this reader would produce a document that quietly lies about where it
came from, which is worse than a build that stops.

* generate: a document becomes a volume

Composition is the second half of the lane: a source has said what it has to
say, and this makes a .atlas file of it. Worlds are resolved against the tile
set and ordered as families, artwork is attached, the semantic conventions are
spoken on the volume's behalf and the whole thing held to the registry
producer-strict, and the result is packed, stamped, written and installed.

Composition is single-source. Folding two sources' readings of one volume
together moved to the enrich lane, so what one source said travels through
here whole and unmixed -- and every world still opens its own account of where
its ground came from, because provenance is part of a world rather than a side
effect of having been merged.

The division of labour over the conventions is deliberate. A source speaks what
only it can know: how its collections draw, what its ground is, what a feature's
true coordinates are. Composition speaks what only it knows: a collection's
kind mirrored, the artwork's form declared once it has actually been resolved,
the rim a world's markers wear, the shared name two sources meet under. Nothing
composition writes overwrites what a source said, and an unregistered key or a
value outside its vocabulary fails here, one step before a bundle, rather than
riding into somebody's library.

Nothing reads a clock. A build's creation time is the newest capture time in
its document, the stamp is taken over named parts -- payload hashes, pyramid
derivation stamps, icon hashes, and the manifest as it stands before its own
stamp is filled in -- and the file name carries both. Building the same archive
anywhere, at any time, writes the file that is already there, which is the
whole reason a directory of these files can stand as a registry.

Splitting is declared in curation and never detected, and the splitter itself
is the next wave. Until it lands a curated sheet is refused loudly: writing it
whole would publish a volume that silently disagrees with every build before it.

* build: one binary, and a subcommand table anyone can add a line to

cmd/atlas is the one command-line binary the rewrite ships. main.go holds an
explicit table -- name, one line of summary, a function -- and nothing
registers itself, so what the binary can do is visible in one place and no
package does anything before main. Each subcommand owns its own flag set in its
own file, which keeps one lane's flags out of another's help text and keeps two
people adding two subcommands from colliding over one file.

Two subcommands so far. `compose` walks an archive, translates every volume a
registered source answers for, and installs the result. `translate` prints an
interchange document to stdout, which is the debugging window into the first
half of the lane and answers the question composition cannot: what did the
source actually make of these bytes?

Both narrate on stderr through the shared event stream, so piped stdout stays
clean for whatever the subcommand's product is.

* golden: the plain-MapGenie volume, rebuilt from its captures

The harness's generate-enrich gate answers for generate and enrich together
over every bundle fixture, and enrichment does not exist, so the gate stays
skipped and says why. The half that can be proven is proven anyway, as an
ordinary test: the plain-MapGenie fixture composed from archived captures by
the clean-room lane and held against every extraction the reference build was
captured into -- part hashes, the canonicalized manifest, the world payload,
the unpacked locations, the deferred prose, the icon set, the tile inventory,
the archive's entry order.

It reproduces byte for byte. Same stamp, same file name, same eight megabytes,
same SHA-256, on the first run. Canonical-content equality was the requirement
and stamp identity the aspiration; the test reports which of the two it got, so
a future run that drifts to merely-canonical still says so out loud rather than
passing quietly.

The translator fixture is a tripwire rather than a diff. The reference tree's
interchange document was MapGenie's own API response passed through untouched
and the clean room's is Atlas's schema, so the two cannot be compared field by
field; what is compared is what they mean, and every intentional difference of
shape is named in the test rather than left for a reader to infer.

Both read the capture archive and the derived tile set, which are deliberately
not in git, so a checkout without them skips and says which variable would have
pointed at them.

* docs: the generate lane written down

The lane's exit criterion is that a stranger could read this and know what the
interchange document says, what a curation file means, what a source may and
may not do, and how a bundle comes to be named what it is named.

Two sections earn their length. The table of departures from the reference
tree's document is there because the shape it replaces was somebody's schema
that everything else impersonated, and a reader who knows only the new shape
would not otherwise learn what was wrong with the old one. And the tile
deriver's contract is written out in full even though the deriver is the next
wave, because it currently exists only as behaviour in a program the clean room
may read but not carry, and prose is how it survives that.

The archive-layout and source-authoring sections are stubs, marked for the wave
that brings a second source and a new archive to generalize from.

* merge fixup: one subcommand table, one logging lane, no duplicate roots

* golden: the transcript re-recorded with the city aboard

The recorded data plane predated the public city, and the note in FIXTURES.json
left the trade to whoever owned the replay gate: re-recording buys coverage of
the one fixture volume that has paths, areas, inline geometry and a standard
icon, at the cost of re-spelling exchanges already pinned. Worth it. The
sampling is derived from what is served rather than from a table, and bend-or
sorts first by title, so the walk starts there: five new exchanges for the
city's own payloads, and the range request and the eight refusals now spelled
against its world and its .png tiles. 38 exchanges became 43, and the catalog
grew by one volume.

This is a re-capture from the oracle, not an edit toward the candidate. The
reference tree served a six-volume registry -- the five installed builds plus
the city read from where it was built -- and golden/capture/http recorded what
it answered, exactly as capture.sh does it.

One thing moved that is not the city: assets/app.js is a newer build than the
one the first recording caught, because extending the parity tour rebuilt the
frontend. The application code the data plane runs on has not changed since
the tag. FIXTURES.json says so under http.tree, and the /static/app.js
exchange is waived by the replay gate anyway.

* golden: the gates read the city from where it was built

The city fixture is the one volume the library never held: it was built for
the fixture by the reference pipeline into a directory of its own, and
capture.sh reads it from there. Both registry-mode gates were still looking
for it in ATLAS_REGISTRY_DIR, so a run with the library pointed at turned red
on a file that was never going to be in it.

They follow the capture script's rule now, and they follow it the way the
fixture set states it rather than by knowing a slug: a volume whose
FIXTURES.json entry carries a builtFor block is one the library never had, and
its file comes from ATLAS_GOLDEN_CITY_DIR -- dist/bundles by default. A
checkout that has not built the city skips those subtests with the command
that would fix it, instead of failing on a bundle nobody promised was there.

The replay gate replays 40 of 43 exchanges byte for byte in registry mode and
9 -- the catalog and every refusal -- on a machine with no bundles at all,
which is still the whole of catalog composition. golden/http/NOTES.md carries
the two-directory rule, the new counts, and what a fixture set moving under a
recorded transcript costs. The seam-assets waiver says which build of app.js
the recording caught.

* generate: a sheet that holds several places comes apart

Two of the corpus's volumes are published as one tile sheet picturing more
than one place. Fallout: New Vegas rings its Mojave map with eight district
insets in the margins; Tears of the Kingdom stacks the sky, the surface and
the depths of one Hyrule side by side. Composition has been refusing both
loudly since the lane landed, because writing such a sheet whole would publish
a volume that silently disagreed with every build before it.

The splitter reads the interchange document and nothing else. A piece is a
top-level area feature -- one nothing is the parent of -- together with its
descendants and the point features that name one of them. That is the same
structure the reference implementation read out of MapGenie's regions, said in
Atlas's own vocabulary, so a second source describing the same sheet the same
way splits the same way and the rule stays out of the sources.

The parts that look arbitrary are not. A piece grows up to 256 world pixels
into the space around it, because an area outlines the ground it covers and
not the title banner printed above it; it stops halfway to its neighbour,
because two of the Mojave panels sit twenty pixels apart and a greedier crop
shows a slice of the wrong map. Pieces that grew into the same diagonal corner
are eased apart, but never back inside the ground they grew from -- that once
took the bottom border off the Camp McCarran panel. And forty of the Mojave
sheet's pins claim the main map while lying inside the panels around it, so
position decides where a pin lives and the claim is treated as the mistake it
is.

Both volumes now reproduce their fixtures byte for byte: the same stamp, the
same file name, the same SHA-256. The golden gate is table-driven over every
single-source fixture rather than named on tunic alone, so the next source to
land adds a row rather than a file.

* generate: a planet is a volume, and its names come from the Gazetteer

The second source in the clean room reads NASA's Solar System Treks and the
IAU Gazetteer of Planetary Nomenclature at once. Neither publication knows the
other exists: one serves a global equirectangular mosaic of Mars, the other
lists two thousand named places on it. This reader is where a body's names
land on its picture.

The coordinate design is what makes that honest. A Trek mosaic is two tiles
wide and one tall where the corpus's square is one and one, so a Trek level
sits one zoom up in the square and the planet fills its top half. A feature's
planetary coordinates become a pixel of that image and the pixel becomes a
synthetic position, so the raster and the pins ride one mapping and the
projection's distortion cancels exactly. Nothing is flattened away by it: the
world declares the flattening in the registered conventions, so a reader can
run a packed position backward and stand on the planet, and every feature
carries the coordinates the Gazetteer published beside the ones Atlas drew it
at.

Two things moved to make room. The synthetic projection and the derived id
space were a source's private trick in the reference tree and are now the
interchange document's own, because they are the document's coordinate and
identity contracts rather than any publisher's shape -- the next three sources
need both. And standard icons are resolved at composition rather than left for
an enricher: the Gazetteer draws nothing, so it names a library glyph instead,
and packing a named glyph is the same work as packing artwork a source
shipped. Choosing a glyph for a collection that named none is still the
enricher's judgement, and the two meet at the attribute.

Mars reproduces its fixture byte for byte -- 255,455,078 bytes, the same stamp,
the same file name -- and the translator is held against the reference tree's
own reading of the same capture, attribute for attribute across all 29
collections and 2,048 features. docs/generate.md gains the reader and the
"add a sixth source" walkthrough, which there is finally a second source to
generalize from.

* analysis: the cell systems, de-globalized behind one contract

The founding family of lane 3, written from the contract rather than from
the old files. A cell system is now three identity fields, three questions
about a ground, and `on(ground)` returning the other fifteen methods — the
same eighteen the old tree kept, split by what needs a ground and what needs
a ground and an id. That split is the whole of the de-globalization: the old
systems read `state.volume.tileGrid.size`, `state.lens.surface`,
`state.lens.bounds` and `state.world.attrs` out of the running app, and a
`Ground` is exactly those four reads written down. Nothing else moved into
it — the active system, the held cell and the subgrid are session state, and
they arrive as arguments.

Geohash and S2 both, and the port is careful about which parts are
arithmetic the vectors gate compares byte for byte: the halving's mask walk
and axis alternation, S2's tessellation step count, the sequential unwrap,
and the closing point that joins in the frame the walk ended in. s2js stays
as the one third-party dependency, because the spherical numbers are its and
reimplementing them would be reimplementing a golden.

The registry is immutable — `with(system)` answers a new one — because a
mutable module-level list is the same shared client state this milestone
just removed, and would make "which systems exist" depend on import order.
Two outputs are contractual and live here rather than in a renderer: the
plan, whose emission order is frozen, and the style tokens, which both
projections read so they cannot drift apart. `cellRings` joins them: the
pole closure and the antimeridian split are cell rules, and no renderer
should own one.

TypeScript at its strictest, with `lib: ["es2023"]` so the DOM is a type
error before it is a lint error, and node's own type stripping so there is
still no bundler anywhere near the gate.

* analysis: the conformance suite, and a third system to prove it

Issue #5 §5.4 asks for an executable property suite every system must pass,
and says a third system is a registry entry plus a green run. This is both.

The suite asserts properties rather than examples: ids round-trip through
the navigator, parent and children are inverse, child order is stable and is
the ordinal, a cell holds its own centre and its parent holds it too,
descent agrees with containment, rings close on the ground and never jump
the seam, a pole is claimed by at most one child and by that child's parent,
plans are deterministic and in the frozen emission order, and style tokens
are values. It runs over geohash on three grounds, S2 on a sphere, and a
district grid that exists only to be a system nobody designed the contract
for — dash-joined ids, nine-way subdivision, a label that is not the last
character, and a parse with something to refuse.

Three silences are written into the suite rather than asserted falsely, and
each is a real disagreement the systems have: ring winding is not a contract
because the unwrap can reverse it around a pole; the floor is not a contract
because S2 refuses to descend past it and geohash keeps halving; and
boundary inclusivity is checkable only where a system's own geometry lands
exactly on a boundary, which a rectangle's corner does and an S2 vertex does
not. Writing the silence down is the difference between a property suite and
a transcript.

The unit tests beside it document the clean design's intent, and one of them
pins a limit rather than fixing it: S2's unwrap treats the ground as one
whole world, which a Web-Mercator city window is not, even though
`appliesTo` says yes to one. Narrowing that would be a behaviour change no
golden would have caught.

* golden: the vectors gate judges the clean lane

The switch the M0 capture left marked with a banner, thrown. `run.mjs` now
imports `engine/cleanroom.mjs`, which adapts `analysis/cellsystems` to the
same eight functions the old adapter provided, and every fixture in the
directory passed on the first run: 9 grounds, 178 vectors in 8 families, 28
plans over 1,391 cells, byte-exact, plan order positional. Nothing in the
fixtures moved, which is the whole reason the vectors were captured a
milestone before the lane that would be judged by them.

The new adapter is a third the size of the old one, and the missing part is
the point: there is no `applyGround` writing a recorded descriptor back into
module globals, no loader hook swapping four application-shaped imports for
throwing stubs, and no session fields to set before asking for a plan.

`engine/current.mjs` and its two neighbours stay exactly where they are.
They document the oracle these fixtures were recorded from, they still run,
and `ATLAS_ANALYSIS_ENGINE=current` re-points the gate at the old tree for a
side-by-side — which is worth keeping reachable for as long as the old tree
is the reference.

* analysis: the lane's own gate, beside the vectors that judge it

The vectors say the lane reproduces the oracle. They say nothing about
whether the lane is the shape the architecture asked for, and that is a
separate question with its own answer: no DOM, no fetch, nothing app-shaped,
no `any`, no bare `console.*`, and every registered system passing the
property suite. `analysis-lane` runs the repository-root ESLint config, the
type checker at its strictest, and the conformance suite, and joins the
harness as a seventh gate. `make analysis-lane` is the same run on its own.

The ESLint config was written in M0 for lanes that did not exist yet and had
never actually run; it runs now, with its rules unweakened, and it fires —
an import of `ol`, a bare `console.log`, a `document`, a `fetch` and an
`any` each produce the contract-citing message §9 asks for. `lint-lanes`
picks up `render/` the moment that directory exists rather than failing on
its absence.

The clean-room TypeScript lanes install at the repository root as npm
workspaces, so the gate's node dependency moves from the old tree's
`frontend/node_modules` to one `npm ci` that brings the analysis lane its
single dependency. The Golden workflow follows, and moves to node 24, where
type stripping is on by default and the gate can import the lane's
TypeScript with no build step.

* docs: the eighteen-method contract, written down

§8's `analysis.md`, held to the same standard as `format.md`: sufficient on
its own to write a conforming cell system, register it, and pass the
conformance suite without reading the implementation.

The parts that were folklore in the old tree and are now normative: the two
coordinate conventions and the two places they convert; the `Ground` schema
and its three-branch fallback ladder, including why a `-0` shows up in the
goldens; the eighteen methods and the eight rules they keep; the plan's
frozen emission order, spelled out rather than pointed at; and the style
tokens' three role behaviours.

Two sections exist because a specification that only says what is true is
half a specification. §4.2 is what the contract deliberately does not
promise — winding, the floor asymmetry, the whole-world assumption — each
one a place where the two systems disagree and asserting either would be
inventing a rule. §10 is the one place the clean lane knowingly differs from
the oracle: S2 has no token for the root, so six of its methods answered
with numbers rather than meanings there. Nothing reaches them and no golden
covers them, so the lane keeps the written contract instead, and says so
here rather than in a comment nobody will find.

§9 is the system author's checklist — the "add a third system" walkthrough,
with a worked example already in the test tree.

* generate: two readings of one city, and the gates that keep them honest

IGN and Piggyback both publish Night City, and the merged fixture is a fixture
of exactly that: two sources describing one ground, folded together. Folding is
the enrich lane's work, but the readings are this lane's, and both land here.

IGN's wikimaps are a flat image with markers placed on it in image-relative
coordinates and a flat list of marker types that names a parent to make two
levels of legend. Piggyback's are the cartography of a published strategy
guide, and they carry the thing a community wikimap does not: prose. Both are
held against the reference tree's own reading of the same archived capture,
collection for collection and feature for feature, so what is proven is not
that the code runs but that it means the same thing.

Each carries the gate issue #5 §5.2 names for it, and the gates are not
symmetrical because the risks are not. IGN's is a refusal to capture: some of
its wikimap pages are MapGenie maps in an IGN frame, and archiving one would
put a second, worse copy of data Atlas already reads properly into the
archive, where a merge would later fold a source into itself. The evidence for
that -- the page's own declaration of a MapGenie game id -- exists only while
the crawler is looking at the page, so the refusal belongs there and this
reader says so rather than pretending it could check.

Piggyback's gate is a refusal to translate. Its maps are placed by an affine
pair read off the page's own scripts, and that pair is the one thing in a
capture that cannot be checked from the capture: a wrong one puts every pin in
the volume somewhere plausible and wrong, which is precisely the failure a
later merge would try to fit an affine to and might half-succeed at. So the
reader carries the list of games whose numbers have been checked against the
published map, and refuses anything else by name.

* semconv: the names a producer drew as text speak unasked

LabelPolicy answered for areas and said "quiet" to everything else, which
is right for a path and wrong for the one case the reference implementation
had already settled: a point collection whose curation drew it as text.
Floating names are labels a producer pinned on, not a different kind of
thing to draw, so a text collection's names speak on their own and its
legend row wears the toggle that flips them either way.

The golden baselines record this from the other side -- Hyrule's label
ladder lists two point collections among the four that speak -- so a reader
built on the old answer would have disagreed with its own buttons.

The ladder is now stated once, widest step first: a declared policy is the
answer whatever the kind, then an area speaks, a path waits, and a point
collection speaks exactly where its curation granted it text. Every case
the old test asserted still holds; the new ones are the text default and
the declaration overriding it.

* app: the chrome the application ships with, carried and vendored

The stylesheet system comes across as an asset, unedited. It is the
reference implementation's token-first, one-file-per-region cascade --
neutral dark chrome, the identity's palette as accents -- and issue #5 §9
says to carry it rather than rewrite it, so thirty-three files arrive here
exactly as they left frontend/css.

The whole difference between that system and the markup this application
renders lives in one new file beside them. chrome.css says four things: the
region containers wear ids the envelope swaps by, so two carried rules that
reached for an id are re-pointed; a card says it is closed by being empty,
because an innerMorph region cannot set an attribute on the container it
renders the inside of; the seam's custom elements need somewhere to stand;
and import rows are new, because the reference implementation had no import
panel at all. A delta one file long is a delta somebody can read.

htmx 4.0.0-beta6 and its server-sent-events extension are vendored beside
them, with their licence. Linking them from a CDN would be the offline
invariant broken in the one place nobody thinks to look: an atlas opens on
a machine with no network, forever, and that has to include the page.

The files are concatenated in a declared order rather than chained by
@import, because thirty-three imports is thirty-three round trips for forty
kilobytes; the order is the cascade, and a handful of files deliberately
override earlier ones. Reload swaps the source tree for the development
loop, which is how a stylesheet edit becomes a refresh instead of a build.

* app: every region rendered, and the display logic that decided it

The regions were containers with the right ids and nothing inside them.
They are pages now: a topbar that offers the library, a legend that is one
tree over every kind of collection, a dock that lists exactly what the map
draws, a card, a cell navigator, a corner locator, and the inert node the
seam will read. One file per region, mirroring the stylesheet system, and
each renders its own container so a first paint and a swap produce the same
bytes.

None of them decides anything. Everything the reference implementation
worked out in the browser now runs once, in Go, before a template is handed
anything: world.go stands a payload up -- collections in order, points
unpacked from ATLASLOC, shape rings projected onto the world square -- and
legend.go, filter.go and view.go answer the questions the surfaces used to
each answer for themselves.

That last part is the point of the exercise. Three surfaces disagreed in
the old tree: the canvas drew what survived every filter, the footer
counted what the legend allowed, and the panel listed what the search left.
They ask one place now. Highlighting reads AND across collections and OR
within one, which the city fixture is the test bed for: one boundary plus
one waterbody takes the drawn count from 213 to 148, exactly as the
baseline records, because none of the sixty-five annotated places stands in
both. A search narrows points and never the ground under them. Containment
allows a pixel of grace, because a pin dropped on a border was put there to
mean the zone.

The session record grew the fields the tree actually has -- unfolded rows,
highlighted ground, where the panel and the locator are put -- and lost the
solo list, because isolating turned out to be a move on the hide set rather
than a state of its own. The chip is derived from what is hidden, so it is
right however that set was reached, including by switching rows off one at
a time. Three sets have defaults the world supplies rather than the reader,
and a flag records that they have been supplied: without it, "show me
everything" and "nobody has touched this" are the same bytes.

The page also carries the arrangement as an inert JSON island, in the key
names golden/parity/SCHEMA.md records. It is the server's half of the joint
diagnostics issue #5 §6 asks for, and it is what the new golden/island gate
holds to the baselines.

Two smaller things had to move with this. The partial envelope spells its
target and swap as hx-target and hx-swap, because htmx 4 reads an
<hx-partial> through its own attribute vocabulary and an unprefixed target
is silently no target at all; the region names and element ids are
unchanged. And /open joins the routes, because a <select> cannot build a
path out of its own value, and a volume and a world are both in the
address.

* golden: the island, held to what every baseline saved

Each parity baseline records, per step, the arrangement the reference
implementation had saved at that moment. This gate drives the same
arrangements through the new application's own routes and holds the state
island it renders to those recordings, key for key, in both directions: a
key the baseline has and the island lacks is a hole, and a key the island
invents is an invention.

Twenty-seven steps across all six fixture volumes, over the legend's hide
set, its folds and unfolds, isolating, search, the label ladder, the lens,
the grid and the corner locator. The sequences are derived from the
baselines rather than copied from the tour: the tour walks one long path
and every step inherits what came before it, so what is reproduced here is
the state a step recorded, by the shortest route the hypermedia surface
offers to it.

Two of the twelve keys cannot be produced server-side and are not made to
be. center and zoom are the chart's camera, arrived at by fitting a raster
to a window; no amount of session state computes them. The server's only
honest relationship with them is the one the state-placement rule
describes -- the seam reports a settled camera upward and the server hands
it back -- so the gate posts the baseline's own camera and checks the round
trip. What is proved about those two is the echo, not the origin.

Beside it, the display logic read off the rendered regions rather than off
the Go values: the AND-across cull at 213 to 148, the label ladder read off
aria-pressed the way the harness reads it, the tree's own sections, the
three counts agreeing, and a search that leaves the ground standing.

It lives under golden/ because it reads fixture files off the disk, and the
hostenv analyzer forbids internal/app -- tests included -- from importing
os. That is the rule that keeps the handler portable doing its job on the
first person who tried to bend it.

* cmd: one command for the loop a template edit runs in

atlas dev is atlas serve with two additions and no third. Templates and
stylesheets are read from the working copy instead of the binary and
re-parsed the moment a file is written, so a template edit is one refresh
away rather than one rebuild away. Writes are coalesced over sixty
milliseconds, because an editor writes a file three times per save and a
save should be one reparse.

A tree that will not parse is reported and the last set that parsed keeps
serving. An unbalanced action halfway through an edit is a normal thing to
type, and taking the running page down over it would make the loop worse
than the rebuild it replaces.

The watching lives here and nowhere else. The application is one pure
http.Handler that touches no filesystem, and a development loop is exactly
the kind of convenience that erodes a rule like that if it is allowed
inside: what the application exposes is a Reload that takes an fs.FS, and
building one out of a directory is a command's business.

-seam-watch runs the seam's own bundler beside the server. The seam is M6
and does not exist yet, so today the flag says so and carries on rather
than failing. It is the command somebody will want the day the seam lands,
and it should not need writing then.

* docs: the application, written down as far as it now goes

The document said what the routes and the record were and marked
everything else next wave. The regions exist now, so it says what they are:
ten of them, one template file each, the element and swap for every one,
and why the legend is a single region rather than three -- three would be
three ways for the row, the chip and the count to disagree, which is the
defect the reference implementation carried until every surface was made to
ask one place.

New sections for the things that had nowhere to be written: the state
island and its key names, the display logic and where each decision lives,
the development loop, and the arrangement a world opens with -- the three
defaults a payload supplies and the flag that tells them from a reader who
asked to see everything.

Two of the island's keys are recorded as seam-side rather than forced.
center and zoom are the chart's camera and no session state computes them;
the server echoes what the seam last said. Two more things are recorded as
belonging to lanes that have not landed: the grid cull, which needs the
cell systems, and the footer's "in view" half, which needs a camera to ask.
Saying so is cheaper than a reader discovering it.

The pragmatism clause gets a section with three named candidates rather
than a sentence. A clause about re-classifying interactions is worth
nothing without the habit of writing down what it would apply to, so the
search round trip, the feature index and the grid's text field are listed
with what would make each of them a problem. None has been re-classified.

* render: the lane's scaffolding, before the templates wave lands

The workspace entry, the type checker's strictest settings, and the first
three modules of the data layer, committed so the merge that follows is a
merge and not a rebase of unsaved work.

* generate: pyramids derived, and the one thing a stamp cannot promise

The tiles package has been half a package since the lane landed: it read
pyramids and the stamps that said how they were made, and something outside the
clean room made them. This is the other half. `atlas tiles` folds a world's
captured frames into the bounded pyramid composition packs -- the deepest
complete level copied through byte for byte, every shallower level folded down
from it rather than from whatever intermediates a capture happens to hold,
partial levels above it carried while they stay contiguous, the background tile
found once and omitted everywhere, coverage recorded as a bitset per level.

One thing had to move for it. A source's frame -- how deep its pyramid goes, in
what encoding, and which tiles each level was supposed to hold -- now rides the
interchange document, because that last question is one no amount of looking at
an archive can answer: a level missing its final row looks exactly like a level
that never had one. Composition never reads a frame. It is there for the
deriver and says so.

The result is worth being precise about, because it is both better and worse
than the aspiration asked for.

Better: tunic's 741 tiles rebuild from the archive byte for byte, along with
the pyramid's zoom range, window, formats, content bounds, background colour
and coverage bitsets. And all nine pyramids of the four single-source fixtures
are plan-identical -- every field a derivation stamp covers, over every
captured tile of every level, in the stamp's own sort order.

Worse: a stamp also covers the deriving tool's own source hash, deliberately,
so that changing how a level is reduced invalidates every pyramid. Two derivers
that write identical bytes therefore stamp differently, and clean-room stamp
identity for a pyramid is impossible by construction rather than by omission.
Proving the plan under the reference implementation's tool hash is the closest
anything can come, and it is close enough to say the two plans are the same
plan.

STAMPS.md is rewritten around that: four fixtures move from ASPIRATION to HELD
because whole bundles reproduce byte for byte, the city fixture is marked
BLOCKED with the reason its capture is gone, and the tool-hash ceiling is
written down as a property of the format's incrementality mechanism rather than
of this rewrite.

* generate: one way out to the network, and one way into the archive

Until now nothing in the clean room could write an archive, and the boundary
that says only one package may reach outward was a rule with nothing on either
side of it. This is the package the rule is about.

The politeness is the part worth reading. One run holds a single monotonically
advancing instant under a lock, and every request takes the next slot, so two
requests are never closer together than the interval however many goroutines
are waiting. It is deliberately not a token bucket: a bucket lets a run that
idled spend its savings in a burst, which is exactly the shape a rate limiter
is watching for. A 429 or a 5xx pushes the schedule forward rather than the
worker that heard it, because slowing one goroutine only hands its slot to
another, and the schedule is the only thing an origin can actually feel.

Two judgements are recorded rather than left to be rediscovered. A 403 is
treated as "not published", the same as a 404, because tile origins answer 403
for objects they never had and a pyramid's corners are usually empty --
treating it as failure would retry the empty corners of every pyramid four
times. And the user-agent is a browser's string, which is a decision and not an
oversight: several of these origins answer 403 to anything else, and a 403 is
indistinguishable from absence, so an honest header would silently punch holes
in an archive. What makes a crawl polite is the interval, the backoff and the
refusal to re-ask.

The write path is where the archive's most load-bearing habit finally lives in
code that owns it. A capture's body goes to a path that is its own hash, and
the index is appended to only when that hash has never been seen -- so
capturedAt is first-seen, a re-crawl of unchanged data leaves the working tree
untouched, and everything replayable downstream stands on that. A register
entry is found by identity and merged in place, so a directory named once is
named forever and a field this crawler has never heard of survives a run.

The IGN crawler is the worked example, complete and not run: its captures are
archived and its endpoints are somebody else's editorial work. It is here for
its shape, and for the gate this lane's reader promised but cannot keep. Some
IGN wikimap pages are MapGenie maps in an IGN frame, and archiving one would
let a later merge fold a source into itself and report a beautiful agreement
between a thing and itself. The evidence is the page's own declaration and it
exists only while the crawler is looking at the page.

* docs: what the generate lane now proves, and what the city is waiting for

The lane's own document has been claiming one reproduced fixture since it was
written. Four reproduce byte for byte now, and the reasons the other two do not
are different in kind: Cyberpunk needs a merge that belongs to another lane,
and the city needs an archive that no longer exists.

The second is worth writing down properly rather than leaving as a gap. The
city's bundle was built from a live crawl whose captures were not kept, and a
fresh crawl would not reproduce it either -- capturedAt is first-seen, a build's
creation time is capture-derived by the format's own invariant, and the file
name would differ even if every byte of the city's open data were unchanged.
So the volume waits on the crawler, the renderer, and a capture, and it will
reproduce from that archive rather than from this fixture's.

* enrich: a lane where a volume becomes richer, and says so

A volume goes in and a new build of the same volume comes out. That is the
whole contract, and everything here exists to keep it honest: nothing published
is ever mutated, nothing a source said is removed or rewritten, every
contribution is written down, and an enrichment that finds nothing to say leaves
the library exactly where it was.

The interesting decision is what an enricher is allowed to express. It hands
back a contribution -- a flat, ordered list of operations with a canonical
serialized form -- and the vocabulary has no operation that removes a feature,
empties a collection or deletes a world. So "never remove or rewrite what a
source said" is not a rule anybody has to remember; it is a property of what can
be written down. The two operations that touch existing data fill what is empty
and refuse the rest. A refused operation fails the whole contribution, because
half an account is not an account.

The ledger is five words -- matched, added, adopted, held, rejected -- and the
gate reads it back: every offered feature accounted for, every match one-to-one,
every attribute a take claims answering to the conventions registry, and a world
holding exactly as many features of each kind as its own accounts claim.

The queue is data. An enricher running in an order nobody wrote down is
folklore, so curation declares the order and the resolver is strict both ways: a
queued name nothing implements fails, and an implemented enricher nothing queues
fails too.

The lane's own model of a volume is here rather than a pointer into the generate
lane's document, because the two lanes may not import each other. The seam that
joins them is the one binary that holds both, and it lands with the subcommand.

Also here: how an enriched build wins the registry fold. Creation time stays
capture-derived, so an enrichment of one capture ties with the plain build
beside it and the revision has to decide. The revision packs two policy numbers
into one integer -- enrich policy above, generate policy below -- which is
deterministic, needs no look at the library, and keeps the stamp a pure function
of the inputs. Packing makes one axis dominant and this picks enrich, because
within one capture an enriched build holds everything the plain build holds and
more.

* enrich: two readings of one volume, aligned and folded

When two sources capture one game, the newest capture serves and the other
reading rides along unseen in a shadowed bundle. This is the enricher that
absorbs it.

The alignment stands on the places both readings name. Every name that is
unambiguous in both is a calibration point, and enough of them determine the
affine between the two pixel spaces deterministically -- solved per axis by
least squares, trimmed twice to nine tenths to shed the places one side simply
pinned sloppily, and refused outright when the median residual still will not
close. A fit that does not close is a merge that does not happen: the two
readings stay apart, whole, in separate builds, which is exactly where they were
before anybody tried. A bad alignment drawn confidently moves every feature it
touches somewhere plausible and wrong.

Resolution is then a series of refusals to guess. The same name where the
alignment predicts is the same place; the same name far beyond the radius is a
different place bearing it; the stretch between is left undecided and said so. A
name one reading spells inside the other's counts only with the collection
agreeing and only when the shorter side carries two words, so a bare "Apartment"
cannot roam the map for a long-named cousin. A nameless neighbour in a shared
collection is held rather than merged, because proximity alone is not evidence.
A place another donor already claimed cannot be claimed again -- a place is one
place -- and the refusal says what stood in the way.

What is left is added: into the serving collection where the two readings spell
one concept, and otherwise under a collection of its own filed beneath its
source's name, with its artwork carried across under a prefixed key so it can
never displace the volume's own. Attribute by attribute, the serving side keeps
what it has and takes only what it lacks, and every take is ledgered by key.

The held and rejected reasons are the reference implementation's wording, letter
for letter. They are data, not messages about code: they are written into every
merged bundle already published, and a reader comparing two builds' accounts
compares these strings.

* enrich: a membership earned, a glyph resolved, a picture attached

Three enrichers that each say one small thing well.

The membership join is the lane's ethic made concrete. A capture that carries
the national subwatersheds beside a city's own zones can say which subwatershed
each piece of ground lies in -- one sentence for the card, the twelve-digit code
beside it for a machine. It claims only on unanimity: every position a feature
answers for has to land in the same unit, and a trail that crosses a divide or a
zone whose boundary leaves the survey says nothing at all. The anchor alone would
let a citywide polygon claim whichever subwatershed its centroid happened to sit
in, so the boundary is sampled too, at a flat cost however detailed the ring.
The evidence travels in the capture and arrives as data, so the judgement
re-runs against the archive without refetching and re-curating the sentence
never costs a crawl. The survey makes no claims about itself.

Standard icons are the one place in this lane where refusal beats silence. A
declaration the vendored library cannot answer fails the build, because that
promise was made by a translator author in this repository and an unanswerable
one is a typo that should be heard about while it is one table edit old. It runs
after the merge, which is the whole reason it is an enricher: a collection
folded in from another reading resolves its declaration too. The glyph lands
under a name that spells its provenance, so a bundle listing reads honestly and
a source's own artwork can never be shadowed.

Lenses make the ground-and-picture separation a first-class operation. A world
is a ground; a lens is a picture of it; a ground published years ago can be
given another picture without the ground changing and without anything published
being touched. This enricher derives nothing -- rasters are the generate lane's
work and the derivation stamp is written there -- it decides which offered
picture a world does not already have, carries the stamp through, and updates in
place what was re-derived. Repointing a source's lens at another pyramid is
refused: that would be a rewrite of what the source said its picture was.

* measure: what a feature is worth, and a gate that only ever rewards

Bundle maturity is replaced by a per-feature score: unbounded, additive,
monotone. Every feature earns points for each quality it verifiably has -- a
name, prose, prose with something in it, resolved coordinates, a membership its
ground was joined to, a collection that resolves artwork, geometry actually
drawn, another reading that corroborates it -- and the sums roll up through
collections and worlds to the volume.

No denominators and no ceilings, which is the whole point. A share moves when
its denominator moves, so a build that added five hundred features and described
half of them could read as a regression, and the number a gate stands on cannot
behave like that. Percentages survive only as diagnostics. That also retires the
known DescribedPct-above-100% defect by construction; where a share still
prints, it now divides by every feature rather than by the point features alone,
which is the arithmetic that produced 235% for a city whose zones are all
described. The group count is a volume's, not a world's, for the same kind of
reason.

The weights are versioned data rather than constants in a program, so a
re-weighting is a new version of the table and not a mass failure of every build
in the library. The gate compares only within a version, and permits a decrease
exactly as far as the ledger accounts for it in corrections: the gate exists to
reward good data and never to punish somebody removing data that was wrong.

Geometry is log-scaled because ground that is actually drawn is worth more than
ground merely claimed, while one over-noded polygon must not outweigh a city.
Corroboration is read off the merge ledger, because two sources agreeing about a
place is evidence, and evidence is quality.

The five absolute axes carry over beneath the score as its breakdown, and
scoring is a pure function of payload bytes -- an archive is only where Measure
happens to get them -- so a fixture directory can be scored as readily as a file.

* build: the seam where generate meets enrich, as two subcommands

The two pipeline lanes never import each other, so the composed multi-source
result is joined by the one binary that holds both. That join is this commit,
and it is deliberately dull: two mechanical copies, a document into the enrich
lane's model and back, with a test that holds the pair to identity. If the
adaptation had judgement in it, the judgement would be lane logic hiding in a
command -- and the identity test is also the mechanical form of the lane's
no-change-no-build law, because an empty queue cannot produce a different
document.

`atlas enrich` groups every reading in the archive by the volume it is a reading
of, picks the newest capture to serve, folds the rest into it, and composes.
`atlas measure` scores a registry: the score first, the five axes beneath it,
and every ledger the payloads carry -- now saying which world each merge account
belongs to, which the reference report dropped.

Composition gains two options and loses nothing. A world's provenance may be
handed to it already serialized, because the ledger vocabulary belongs to the
enrich lane and composition has no business learning what a matched pair is; it
splices the accounts in verbatim and opens its own origin account only for a
world nobody spoke for. That also leaves every single-source origin account's
bytes exactly where they were. And the build revision may be stated by the
caller, which is how an enriched build outranks the plain build of the same
capture without any mutation, any clock, or any look at the library.

* golden: the merge and the join, held to the fixtures they came from

Two judgements in this lane could be wrong in a way nobody would notice: which
places two readings agree about, and which ground lies in which subwatershed.
Both are now checked against the exact numbers the reference tree recorded, from
committed fixtures, on every run and every machine.

The merge is re-run over the two translator fixtures the merged volume was
composed from -- the Piggyback capture that serves and the IGN capture folded
into it -- and held to that volume's own ledger. It reproduces the alignment
word for word (99 anchors, median 26.0px), the counts (270 matched, 37 added, 7
adopted, 61 held, none rejected), every pair to the same serving feature at the
same distance, every adoption into the same collection, and every hold for the
same recorded reason.

The membership join is re-run over the city volume's own payload -- the survey
having been composed, the evidence travels in the volume as well as in the
capture -- and reproduces exactly the 88 features the reference claimed, with
the same codes and the same sentences. Neither test needs an archive, a tile
set, or a network.

Beside them: the vendored standard glyph against the city fixture's icon hash,
because an asset that is almost the same asset is a different asset; a score for
every bundle fixture, reproducible and equal to the sum of its worlds; and the
monotonicity property on real data -- the city fixture with its claims scores
higher than the same fixture without them, and the gate refuses the reverse.

The whole generate-then-enrich reproduction of the merged bundle cannot run yet:
it needs the sources that read those two captures, which are landing elsewhere.
It is written, prints its activation condition, and skips.

One divergence is declared. An enriched build's revision cannot equal the
reference's -- the reference merged inside composition at the plain revision,
and the lane's contract requires the enrich write to bump past it -- so its
stamp and file name cannot match either. That is a stamp difference by
construction; the canonical content stays checked and unwaived.

* docs: the enrich lane written down

The prose twin of internal/enrich: the five laws, the seam and why it is shaped
the way it is, the revision mechanism that lets an enriched build win a fold
without mutating anything, the enricher interface and the contribution's
canonical form, the ordered queue and why each enricher sits where it does, the
ledger's five words and what the gate checks, each enricher's judgement in the
terms it actually makes it, the point table and the monotonicity gate, the
curation schema, and the evidence ethic stated once as policy for the whole
lane.

It also writes down the alternatives that were rejected for the seam, because
the reason enrich does not simply call composition is a contract rather than an
oversight, and a reader who does not know that will try to fix it.

* golden: read the city fixture's world out of its own manifest

The city volume's world is its capture day, so its slug moves whenever the
volume is re-captured -- as do its stamp, its file name, and, if the survey
beneath it moved, the number of memberships the join can claim. The re-capture
is coming, and a test keyed on any of those would break for a reason that has
nothing to do with the judgement it exists to check.

So the world is read out of the fixture's manifest, and what is asserted is that
the join reproduces exactly the claims the fixture carries, feature by feature,
whatever they turn out to be. The counts recorded at capture -- 88 claims from
12 surveyed units -- are logged as an observation rather than asserted as a
constant: transcribing a golden into a constant beside it only creates a second
copy to disagree with.

* render: the seam, and the three contracts it stands on

The render lane, whole: two custom elements that picture a volume, standing
on the /data plane, the scene description and the analysis API, and on
nothing else. Nothing imports it, it imports no application, and its one
entry point is a script tag a page is complete without.

The shape of it follows the contracts rather than the panes. A data layer
owns every fetch, the URL shapes and the zero-copy ATLASLOC views; a scene
reader turns the inert state node into a described scene and names what moved
between two of them, so a morph swap reconciles rather than rebuilds; a world
model projects a payload once and a standing set answers, once per scene, the
question three surfaces used to answer separately. Only then do the two panes
appear, and both of them read the same answer.

The chart is twelve layers in the recorded z-order, two raster layers because
a pyramid is two different things -- complete below fullZoom, patches above
it, with the coverage bitsets consulted before anything is requested -- and
two overzoom levels of parent upsampling over whichever resampling the lens
declares. The sphere is offered only where a world declares one and the
flattening inverts, builds nothing until it is entered, and hands the chart
back the camera it was given. The grid is the analysis lane's, adapted and
not restated: the plan in its frozen order, the rings as cellRings resolves
them, the tokens as gridCellVisual answers them.

Twenty-eight tests judge what can be judged without a browser. The packed
layout is checked by packing the golden records and matching the recorded
SHA-256 before the reader is allowed to read them; every one of the twelve
golden tile inventories is reproduced tile for tile from bounds, coverage and
formats; the grid over Mars reproduces two recorded parity steps -- ids,
extents, roles, context distances, per-cell counts, and the array order -- and
the projection reproduces the fit, the resolution and the two depths the Mars
baseline opens with.

* render: the lane's own gate, and the boundary rules made real

The ESLint skeleton M0 left open is closed: the analysis package's real
specifier is its workspace name, the pinned dependency surface is named, and
relative imports are allowed at any depth. The patterns are gitignore-shaped,
which is why every allowed scope is un-denied twice -- a path whose parent
directory is excluded cannot be re-included, so "!ol" has to precede "!ol/**"
for a single OpenLayers import to pass. The seam's tests get the standard
library and nothing else.

make lint-lanes no longer guards itself with a wildcard, make render-lane is
the seam's own gate, and make golden runs it as a suite beside the analysis
lane's: the boundary rules, the type checker at its strictest, the seam's
tests against the golden fixtures, and the authored-line budget printed and
warned on past ~3,000 -- 2,761 today, tests counted separately because a lane
is not improved by being under-tested.

make seam builds the bundle, make static assembles the tree an
"atlas serve -static" mount wants, and the stylesheet is deliberately not in
it: the CSS is the application's own asset, served from /assets, so deleting
this lane costs a page one script tag rather than its chrome.

* golden: the city's capture, crawled back into the archive

The bend-or bundle was built during M0 from a live crawl and the archive
that made it was thrown away with the worktree it lived in. What was left
was a fixture nothing could be rebuilt from: STAMPS.md carried the row as
BLOCKED, and the arcgis-hub translator golden was an output whose input no
longer existed.

The city has been crawled again, on the same day it answers to, from the
same eleven datasets and the same three endpoints, into the archive the
pipeline keeps. The repository stages the one-game slice at
crawl/bend-or/fmg-archive and the whole capture set at crawl/fmg-archive,
both gitignored, both the shape tools/generate reads.

The re-crawl reproduced the lost build almost exactly, and the almost is
the interesting part. All 2,320 non-manifest entries hash as they did --
the payloads, the icon, and every one of the 2,316 tiles the basemap
renderer draws offline from the city's own vector data. The city's open
data had not moved between the crawls. atlas.json differs in three fields
and no others: createdAt, the world's updatedAt, and the stamp that follows
them, because capturedAt is first-seen and the manifest's bytes are a
stamped part. So the file is bend-or-20260802-3610a0f10798.atlas now, and
it is the same city.

Every bend-or-dependent golden is re-captured from the reference tree
against that build: the bundle extractions, the translator fixture -- whose
document came back byte for byte, only its capture name moved -- the
measurement, whose DescribedPct defect still reads 235%, and the HTTP
transcript, which spells the city's serving stamp into the range request
and the eight refusals. The six parity baselines were re-captured too, with
--twice --verify, and none of them changed outside the two advisory
tileStats fields: a tour reads a volume's contents, and the contents did
not move.

STAMPS.md reads the row as ASPIRATION rather than BLOCKED, with the three
moved fields written out. Two builds of one city, identical everywhere the
city is and different only where the clock is, is the clearest statement
this repository has that a stamp is a rebuild-cost promise and never a
content promise.

* render: what a smoke run against the goldens moved

Three findings from driving the seam by hand against the real application and
the pinned fixture bundles, each of them a number in a recorded baseline
rather than an opinion.

The camera fits a lens's bounds and not its surface. A lens may declare both
and they are not the same rectangle: bounds is the window the pyramid fills,
surface is the ground inside it, which on a split sheet is smaller because
the window was grown to take in a title drawn beside the map. Tunic declares
a surface and no bounds, and its baseline opens on the whole world square --
which settles it. The surface reading stays where it belongs, in the Ground
the analysis lane divides, so no cell lands on margin.

The detail raster draws only strictly deeper than the complete level. Above
that the base pyramid is whole, and asking the patchy one for the same ground
doubled every request a fresh view made: eight tiles where the baseline
records four, and peakPending is a field that binds.

The sphere drapes nothing until the camera asks for more than its skin
already has, and an in-flight composite is cancelled rather than allowed to
finish into a neighbourhood nobody is over. Both are what the tour asserts --
a globe entered whole shows the base skin and no pyramid tiles, and a
put-away globe keeps none -- and the second one is a real race: a paint walks
its tiles one image at a time.

With those, the initial step of both reachable fixtures agrees with its
baseline on all twenty-seven seam-owned fields, including the fit, the
resolution, the tile counters and the two depths; entering the sphere
reproduces globe-entered's pane; Z raises exactly its budget of names and
releasing drops them; and the camera round trip through the equirect mapping
is exact rather than nearly.

* docs: the seam, written so it can be deleted

docs/render-seam.md, held to the standard issue #5 §8 asks for: a reader who
has never seen render/ should be able to delete it and write it again from
this document and the three contracts it names. So it carries the scene
description field by field, the outward events, the twelve layers and why
each is where it is, the coverage and overzoom rules, the standing set's four
rules and their one bypass, the globe's four budgets and its camera pairing,
and the diagnostics duty as the globals the recorded tours actually call.

Two things it carries that are not in any other document. The projection from
a payload's lat/lng to world pixels is written down here, because it is not
in docs/format.md and every renderer needs it -- along with the world-grid
override that makes the difference between a coordinate on this world and one
850,000 pixels away. And §10 is the honest list of what is not proven yet:
the tour is not re-pointed, the globe's altitude constant is uncalibrated
against the reference pairing, nearest-neighbour resampling is pinned by no
fixture, and the bundle is 2.3 MB of globe.

* generate: the city, read in the clean room's own vocabulary

internal/generate/sources/arcgishub is the fifth source, and the first whose
subject is a real place. A city is a volume and each crawl day is a world:
the picker is the city's version history, and the difference between two
worlds is a difference in the city.

The reading is the reference tree's, said in the interchange document's
terms. The curated bounding box is padded and squared in Mercator's own
terms into the world; features project onto it and out through the shared
synthetic inverse, and the world declares the mapping so nothing is
flattened away. Polygon and line layers become shape collections holding one
feature per curated bucket; point layers become pins under their heading;
pins lead and ground follows, which is the legend order. The membership join
comes with it, and keeps its discipline: a zone claims a subwatershed only
when every sampled part of its ground agrees, so a zone that straddles two
says nothing.

One gate changed shape on the way over. The reference refused an uncurated
city outright. Here it wraps ErrNotReady and the volume is passed over,
because an archive may hold a city this table may not name -- the operator's
own -- and a refusal that loud would take every other volume in the archive
down with it. The refusal is still a refusal; it is just the archive's kind
rather than the build's.

golden/pipeline gains the gate that measures it. The two documents cannot be
diffed -- one is MapGenie-shaped and one is Atlas's own -- so what is
compared is what they mean: the volume, the world's identity and centre, the
declared Mercator window, the lens, the 65 pins with their positions and
their published coordinates, then every declared collection against its
regions, feature by feature, geometry part by geometry part. The fixture's
own numbers are counted rather than merely walked: 104 paths, 44 areas, 88
features carrying the HUC12 the join earned, three national layers labelling
quiet, and no artwork, because the one icon is a standard glyph named for
composition to resolve.

The renderer that draws the city's basemap is still the missing half, and
§4.4 of docs/generate.md now says so precisely: it is the only thing between
the clean room and the city fixture, and the curation it will answer to --
each layer's role and emphasis -- already travels in the table beside the
rest of that layer's judgement.

* workbench: one operation at a time, streamed as it speaks

The workbench runs the pipeline by running the pipeline's own binary, and
what comes back is that run's event stream rather than a summary of it. That
mechanism is a library rather than handler code, because the interesting part
is the four properties it holds, and a property worth naming is worth being
able to test without a page around it.

A foreign Origin is refused. A second operation is refused with the name of
what is running, never queued. A target is admitted only if it is safe as a
command argument and is actually a slug, and a leading dash is refused so a
target can never be read as a flag. A subprocess runs under the request's
context, so a page abandoned mid-operation stops its operation.

Rows are data. `atlas` writes slog records to stderr when asked, so each line
is parsed into a row carrying level, message and the documented attribute
vocabulary, with the documented keys first; a line that is not a record is
carried verbatim, because a tool's own words are worth more than a runner's
opinion of them. What a row looks like belongs to whoever asked for the
operation, which is what keeps HTML out of this package.

Both refusals are decided before the first row goes out. Once a body is
streaming the status line is spent, and every ending after that point is a
row: a program that will not start, one that exits badly, and one that
finishes all end in a result row, and the difference is a boolean.

The tests re-run the test binary as their subprocess, so they need nothing on
PATH and nothing built beforehand: one that speaks a record, one that fails,
one that waits to be stopped. The flush is checked by the clock -- a row that
arrives before the helper's next event arrived because it was flushed.

* workbench: the collection measured, and what moved between two builds

The cartograph successor, as one handler. Where the application serves the
build a reader should have and asks no questions, this answers the questions:
what each build is worth, what moved between two of them, what the collection
owes the people whose work it carries, and what the pipeline should do next.

Every page leads with the score, because the score is the only number
anything gates on. The five absolute axes the reference tree reported are
under it, labelled as diagnostics, and percentages survive there and nowhere
else -- the described share reads against the right denominator now, so the
defect that had a city 235% described is gone by construction. A measurement
page carries the whole of every ledger rather than counts of it: every held
feature with its reason, every adoption, every attribute a collection took,
every correction. A diff is headlined by the score delta and the verdict it
earns, and carries the gate's own comparison -- comparability across point
table versions, and the allowance a ledgered correction buys -- rendered
rather than reimplemented.

Three things the handler cannot reach arrive as data. The source registry
entries come from whoever mounted it, licence and attribution included: a
subcommand emitting them as JSON and a curation file restating them were both
refused, because each puts a second copy of a licence somewhere it can be
wrong. The hypermedia runtime arrives as bytes, so one vendored copy serves
two surfaces with no import edge between them. And the pipeline is reached by
shelling out, never by linking, which is what keeps a lane's work in its
lane while the page that starts it imports only the format and the score.

A workbench with no runtime is a working workbench: every page is plain HTML,
every form an ordinary POST, and an operation still streams -- the browser
renders the rows itself instead of a swap appending them.

The measurement page is anchored to a number that was captured rather than
invented: the golden extraction of TUNIC, packed back into a bundle through
the real writer, scores 1323 under point table v1, which is what the enrich
lane's own suite reports for the same fixture.

* build: the workbench as a subcommand, and the lane it stands beside

One more line in the table, and the wiring that lets the handler stay out of
the lanes it reports on. The command is allowed to know every lane, so it is
where the source registry entries are read out of the generate lane, where
the tile register's file name comes from the lane that owns it, and where the
application's vendored runtime is handed over as bytes.

The binary operations invoke is this process's own executable. A workbench
started from anywhere runs the same build of the pipeline that is serving its
pages, and the reference implementation's search for a repository checkout
has nothing left to look for.

The portability amendment is about the application the three hosts mount, and
the depcheck table now says so out loud: the workbench shells out by
contract, and reading the registry it measures is what it is for.

* docs: the workbench written down, cartograph feature by feature

The pages and what each says, where their facts come from and why the source
entries arrive as data rather than through a subprocess or a second file, the
operations table with the argv each one builds, the runner's contract, and
the six safety properties named as the contract they are.

The last section is the milestone's exit criterion made checkable. Every
user-visible feature of the reference dashboard, read off its code and its
tests, is listed with one of three verdicts: carried, replaced by the score
or by a mechanism this architecture already has, or deliberately dropped with
the reason. Fifty-eight features, forty-three carried, nine replaced, six
dropped -- and the drops say why, which is the only part of a parity claim
worth reading twice.

* tiles: the lesser picture, resampled into the finer one's world

When two sources picture one ground, one raster is the finer, and a registry
that simply served the finer one would throw the other away. The deriver now
builds the warped variant instead: the lesser raster resampled through an
affine into the finer picture's world, offered as one more lens on the same
ground. Nothing is discarded and both rasters answer to one grid.

Three decisions happen in order, and the order is the whole design. Names are
settled against the whole planned set first, because two sources capturing one
volume name the same ground the same thing and every collider -- not just the
later one -- has to take its publisher's path into its name, or which pyramid
is called what depends on the order an archive listed its captures in. The
pairing is then decided from the whole set too: the reading that draws its
world at the most pixels is the frame the others are brought into, and only
readings from different sources are ever paired, since a source that divided
its own world into several grounds did so deliberately. Last, the warp renders:
every pixel of every tile in the window asks the inverse transformation where
in the donor it came from, and the levels above it fold down as any pyramid
folds.

The alignment itself is an input, not a derivation. Fitting it stands on the
names two readings share and belongs to the enrich lane, which is where the
merge that folds the features by the same fit already keeps it. Neither lane
imports the other, so cmd/atlas fits it and hands the deriver six numbers --
the same seam adapt.go opens for documents and volumes. Nothing is duplicated
to arrange that: a resampler needs a fitted transformation, not the ability to
fit one, so only apply, invert and scale sit beside tiles.Affine.

Carrying a pyramid over between runs now looks it up by name rather than by
tile set, because an aligned variant is registered under the picture it hangs
on, and a lookup through the raster it was resampled from would find nothing
and derive every warp again on every run.

* enrich: what a source is called, when curation speaks, and what a donor may take

Four corrections the merged fixture found, each one a place where the clean
room said something the reference had said differently, and where the fixture
is the oracle.

A source's registry name is Atlas's, not the archive's. The IGN reader is
"ign-wiki" -- the name a ledger line and a workbench card agree on -- while the
capture archive on disk, which predates Atlas's naming and is kept verbatim as
input data, still files those captures under "ign". A source says both, and the
lookup asks for both; nothing downstream of a translator learns the archive's
spelling of anything.

Curation speaks over a reading before anything folds it into another. A merge
meets two collections under the name they share, and the name they share is a
curated equivalence; composition declared it, but composition runs after the
merge, so a merge was meeting collections on artwork keys alone. It now runs
where it can be read -- once, over the translated document -- and composition's
own late declaration stands for a document that never passed this way.

Carried artwork travels under two names on two rules. The file the bytes land
in always takes the source's tag when a contribution goes into a ground the
serving reading already draws: a donor's picture may never be written where the
serving volume keeps its own. The key a collection names its artwork by stays
the donor's own, because there is nothing to displace unless the volume already
names different artwork with it -- and only then is the key tagged too. The
previous rule tagged both, which renamed a concept nobody was arguing over.

Finally, the ledger's field order is the wire order, and it is frozen. An
account rides a world payload and a world payload is a stamped part, so where
"added" sits relative to "matched" is part of the format's surface rather than
a struct's declaration order that happens not to matter.

* golden: the merged volume, rebuilt by the command that ships it

generate-enrich is ready. The gate that has been skipped since M0 now judges
every bundle fixture that can be rebuilt at all, and says out loud which one
cannot.

The merged fixture is reproduced by running atlas enrich over the two archived
captures into an empty registry, not by a test arranging the same library calls
in the same order: the whole claim is about the pipeline a person runs, and a
gate standing on its own reassembly of it would be measuring a second
implementation nobody ships. Everything a bundle is comes back identical --
the world payload with its whole merge ledger, the packed locations, the prose,
all 38 icons, both pyramids' 17,507 tiles, the archive's entry order -- and the
one part that differs is the manifest's version object, which is the standing
enriched-build-revision waiver rather than a content difference. The gate
asserts the shape of that difference rather than shrugging at it, so a second,
unrelated divergence could not hide inside the first.

The warped pyramid is held to the reference cache both ways, and its plan proves
more than a native pyramid's can: the stamp covers the alignment itself, six
coefficients to nine decimal places, so reproducing it says the anchors, the
name matching, the trimming, the least-squares fit and the target zoom all came
out identical from the captures alone. Its 1,365 tiles rebuild byte for byte.

bend-or is out of scope and the harness prints it. A ready gate that does not
judge everything its contract names now carries a scope line under its result on
every run, because a gate answering for five of six fixtures is a different gate
from one answering for six, and the difference belongs on the scoreboard rather
than in a paragraph somebody has to go and find.

* docs: what the merged rebuild actually costs, counted

The enriched build's difference from the reference had been described. It is
now measured: 17,549 archive entries in identical order, of which exactly one
differs -- atlas.json, in version.revision and the version.stamp that follows
from it. The two files are the same length to the byte. A waiver is a standing
bill, and a bill reads better with a number on it.

* golden: the city's archive, resolved where it is staged

The city translator gate has been failing since the reader landed, for a
reason that is about staging rather than about the reader: it looked for
bend-or in the corpus's archive, and bend-or is not there.

It cannot be. The corpus archive holds whatever the machine's operator has
crawled, and for a city that is allowed to be a city the public curation
table may not name -- issue #5's privacy rule keeps an operator's own city
out of the repository, so the reader passes it over as uncurated. Staging
the proof city in an archive of its own keeps a checkout able to rebuild it
without a library-sized capture, and keeps the gate from depending on what
else somebody happens to have crawled.

So the gate resolves an archive per volume instead of one for all of them,
with ATLAS_CITY_ARCHIVE_DIR beside ATLAS_ARCHIVE_DIR and the same
skip-with-an-explanation when neither the environment nor the checkout has
one.

* generate: the ground a city stands on, drawn from its own vectors

Every other picture in the corpus arrives as tiles from somebody's tile
server. A city has none: a municipal open-data hub publishes geometry, and
the offline invariant forbids reaching for anyone else's raster. So the
deepest level of a city's pyramid is drawn here, from the geometry the city
publishes about itself, and every shallower level folds down from it exactly
as any other pyramid's does. Nothing downstream of the deriver can tell the
two apart, which is the point.

The drawing is the last clause of the deriver's contract and the last thing
between the clean room and the city fixture. It is a small vocabulary --
seven roles, a fixed z-order, a fill and a stroke each -- and three pieces of
geometry: rings clipped Sutherland-Hodgman, segments clipped Liang-Barsky,
and lines widened into capsules that the rasterizer's saturating accumulation
unions into one continuous stroke. Both clippers cut against a window bled
eight pixels past the tile, so every cut lands outside the picture and two
neighbours drawn independently continue each other exactly.

Three seams are worth naming.

A source states its drawing beside its collections rather than out of them.
The two do not line up: every road centreline draws and none is a legend row,
only named trails are legend rows and every trail segment draws, and emphasis
varies row by row where a collection's features are buckets of many rows. So
doc.Lens carries a Drawing, in the vocabulary of the drawing -- roles, never
dataset names -- and the curation each role comes from is the same entry the
rest of that layer's judgement already lived in.

The archive is the witness. The crawl that first drew this city wrote its
rasters down, and the plan still names their content hashes, so the deriver
holds every tile it draws against what the capture recorded and fails naming
the tile if they disagree. That is a stronger thing to check than a test
could: not "these bytes look right" but "these bytes are what a different
implementation made of the same vectors", checked on every derivation.

And the renderer joins the tool stamp. A derivation stamp covers the deriving
code because a change to how a level is made has to invalidate the level; a
city's deepest level is now made here, so a change to the drawing has to
invalidate that city's pyramid the same way.

* spec: the conventions, said once and cut three ways

The registry had three statements of one vocabulary: the Go registry in
format/semconv, the keys the TypeScript lanes spelled for themselves, and
docs/semconv/REGISTRY.md, held to the first by a test that read the document
with a regular expression. Three statements is two chances to drift, and the
agreement test could only check the two it could see.

spec/registry.yaml is now the one machine-readable source, and spec/gen cuts
three artifacts from it: format/semconv/registry_gen.go, the lanes'
analysis/semconv/keys.ts, and the document itself. The value checkers stay
hand-written where they belong -- a checker is code -- and the spec names one
per key; a spec naming a checker nobody wrote fails codegen rather than
admitting a key that checks nothing.

The agreement test's job moved with it. The question is no longer "do these
two say the same thing" but "is the derivation current", which is the only
thing a generated file can be wrong about, and spec/gen answers it: the
committed artifacts must equal what a run produces, and five runs must produce
identical bytes. That gate runs on every `make golden` as semconv-codegen.

Both TypeScript lanes adopt the generated keys, so the seam no longer spells a
convention key in six files, and analysis/semconv/geometry.ts stops building
one out of a template string.

* docs: the calls this rewrite made, written down as records

Issue #5 §8 asks for short ADRs covering the calls the issue itself makes, and
§10 records those calls as a list without the reasoning that produced them.
This is that reasoning, one record per decision: context, decision,
consequences, and nothing invented -- every record is written from the lane
documents, the harness, and the code that already exists on this branch.

Sixteen records. Eight are the calls §8 names: HTMX at the centre, allons
removed, merge moved to enrich, the source-neutral interchange document, the
unbounded maturity score, the scene-description seam, hostenv portability, and
the stamp-aspiration policy. Eight more are calls execution made that §10 does
not yet record: the adapter that joins generate to enrich without either lane
importing the other, the revision packing that lets an enriched build win the
fold deterministically, the one event stream and the lane it lives in, the
scan-at-launch registry, the two static mounts, the depcheck pragma, the Ground
descriptor, and the difference between a capture that is wrong and a capture
this reader cannot answer for.

The comment policy of §8 is the reason these exist at all: a comment states a
constraint the code cannot show, and "how we got here" belongs here or nowhere.

* golden: the city, reproduced from the archive it was crawled back into

The singleSource table has been four fixtures and an honest note about why
it was not five. It is five.

bend-or composes from its archive and comes back as the file the reference
implementation wrote: bend-or-20260802-3610a0f10798.atlas, 13,642,843 bytes,
the same sha256, the same createdAt, the same stamp. It is the first city to
hold stamp identity and the fifth fixture of five to hold it at all -- and it
holds it in the strongest sense the aspiration asked for, the whole bundle
rather than a proxy for it.

The deriver's tile gate grows a second subject for the same reason. Tunic
proves the copied path: a captured level carried through and reduced. The
city proves the drawn one: 4,096 tiles rasterized from the city's own
vectors, every one held against the hash the capture witnessed, 2,316 of them
surviving the background omission into a pyramid that matches the reference
cache byte for byte. Between them every path a level's pixels can take is
walked by a fixture with an answer already written down.

The plan gate needed no argument at all -- the city's pyramid plans to
624a4e0c under the reference tool's hash, which makes ten of ten.

And one guard of its own: a drawing that disagrees with its witness must be
refused rather than shipped, so the test arranges a disagreement -- one
street re-roled as a boundary -- and asks for the refusal by name.

* ci: the harness on three platforms, and a release for the binary that exists

The golden gates compare bytes for a living, and they had only ever been
compared on one operating system. They now run on ubuntu, macOS and Windows,
and the two things that would have made that meaningless are fixed first.

/.gitattributes puts LF in every working tree and marks the captured fixtures
as data, so no checkout can rewrite a byte between the capture and the
comparison -- a Windows checkout under the default configuration would
otherwise have turned every fixture comparison into a diff of invisible
characters, and the generated semconv artifacts with them. The job proves the
tree arrived unmodified before it runs a gate. The four analysis scripts that
derived their own directory from a file URL's pathname now use fileURLToPath,
which is the same thing everywhere except Windows, where the old form produced
a leading-slash drive path and read nothing.

CI spells the entrypoint `go run ./golden/harness` rather than `make golden`
because the Windows runner has no make. It is the same run, and HARNESS.md now
says so, along with the three gates its table had fallen behind on.

The release recipe is carried forward for what the rewrite actually ships
today: `atlas`, the one CLI. Version from the tag, -trimpath and -s -w, one
archive per platform, a release job gated on the tag with generated notes --
all of it from ci.yml, which keeps releasing the pre-rewrite desktop app on v*
tags until the old tree is archived. This one answers to atlas-v* so the two
never race. It cross-compiles five targets from one runner because the CLI is
pure Go with CGO off; the per-platform runners the old recipe needed were for
the webview and the .app bundle, and they come back in the marked desktop slot
when the Wails host lands.

* docs: the deriver's last clause, written as built

§4.4 was the spec of record for a renderer that did not exist. It is now the
account of one that does, and it says the things a spec could only promise:
what the vocabulary is, why widths are spelled at one zoom and scaled,
why a line becomes capsules rather than going through a stroker, what the
eight pixels of bleed buy and how a dashed stroke keeps its phase across a
seam, and why the curation the renderer answers to is the same curation the
legend answers to rather than a second table beside it.

Two things in it are worth a reader's attention beyond the algorithm. The
capture is a witness: a drawn level's tiles are still listed and hashed in
the plan, so the deriver holds every tile it draws against what the archive
recorded and refuses by name when they disagree -- which makes agreement
between two independent rasterizers a property of every run rather than of a
test. And the renderer is part of the tool stamp, because a city's deepest
level is now made by it and a change to how a level is made has to invalidate
that level.

Warps are alone in §4.5 now, still declared and still refused by name.

§8 carries five fixtures where it carried four and an excuse, §3.2 stops
promising that the crawler and the renderer land together -- the renderer
landed first, and what the crawler adds is a new day rather than the ability
to rebuild this one -- and STAMPS.md reads HELD for the city.

That last row is the strongest in the table and says why: the four game and
planet fixtures hold their stamps while standing on rasters somebody else
derived, and the city holds its stamp while standing on rasters it drew
itself. The ceiling is unchanged and restated where it belongs -- a stamp is
a rebuild-cost promise, and a library built end to end by the clean room
would carry the same tiles under different names.

* docs: the end-state, mapped against what is actually written

Issue #5 §8 lists a documentation end-state and calls it an exit criterion
rather than residue. docs/README.md is the map from that list to the tree: what
each document is, whether it meets the standard it was written to, and where it
does not.

Nine of the eleven entries are complete. Two are not, and saying so is the
point of the file. `golden.md` exists as golden/HARNESS.md, beside the fixtures
it describes rather than under docs/, and it owes the one section §8 asks for
that cannot be written yet -- how to read a parity diff, which waits on the one
gate whose lane does not exist. `app.md` §11 and `render-seam.md` §10 are both
honest lists of what is not built, and each item that lands has to move from a
"not yet" list into a normative section by the hand of the milestone that lands
it.

The audit itself is four questions rather than a checklist, headed by the one
no amount of reading replaces: nobody has run §7's walkthrough test.

One staleness the map turned up: logging.md still spoke of the browser half as
arriving with M6. It arrived. It now describes render/log.ts, including why the
level gate is read exactly once.

* app: the interactions that never left the page

Wiring the parity tour at the rewritten application found the application
answering almost nothing. Six defects, each of which made the one after it
invisible, so they are landed together.

Nothing inherited the volume. `hx-vals:inherited` on the shell reaches only a
control that declares no values of its own; htmx 4 stops at the first
`hx-vals` it finds and merges nothing. Every control on the page that carried
its own values -- which is every control that says which collection, which
section, which feature -- therefore posted without a volume and was answered
400. They ask for the merge by name now, `hx-vals:append`, which is the
spelling that walks up to the shell and joins.

The volume select was the opposite mistake. Values declared on an element
replace the parameters its own name and value contributed, so the inherited
volume -- the one the reader was leaving -- overwrote the one they had just
chosen, and every switch navigated back to where it started. It declares an
empty `hx-vals` now, which is how a control says it inherits nothing, and the
comment beside it says why that is load-bearing rather than noise.

No region swapped. A region's template renders the region's own container, so
every answer carries an element with the target's own id; asking htmx to morph
that into the target's *children* asks it to make an element a descendant of
itself, which it declines silently. The page stayed exactly as it was: a
legend that never recounted, a dock that never lost a row, a footer quoting a
filter nobody was under any more. They are outer morphs now, which is the
reading that matches what the templates actually render.

The state island stood outside the shell. Every navigation selects
`#atlas-shell` out of the answer, so the island was never replaced and a
reader who switched volumes went on being told about the volume they left --
which is the whole of the server's half of the joint diagnostics, wrong on
every step of every tour that switches. It stands inside the shell now.

The grid navigator's own two controls did nothing. `system=cycle` set the
system to the literal string "cycle" and `ascend` was not read at all, so the
back button rewrote the cell as itself. Both are answered now, together with
the `toggle` and `flip` the keyboard needs, and the subdivision defaults to
shown, which is what every baseline records from its first step.

And the keyboard did not exist. G, Escape, Space and ⌘B are declared as
hx-triggers on the shell -- routes, not handlers, because each one moves
discrete state and the page carries no glue. Z is the one shortcut that stays
the seam's, because holding a key to raise names is continuous.

The island gate's ladder helper read as far as the next toggle rather than to
the end of the button, so the last toggle in the legend took its answer from
whatever pressable chrome happened to follow it. The values it asserts are
unchanged; only the window it reads them through moved.

* render: the sphere's distance, solved from the tours that recorded it

The seam's altitude pairing was a field-of-view calculation: resolution times
the viewport height over the declared degree span, scaled by globe.gl's own
full-view distance. It is defensible arithmetic and it reproduces none of the
recorded numbers.

The pairing the reference actually used is a power law anchored at one point.
The whole disc at altitude 2.5 reads as the whole chart at zoom 2, and each
halving of the distance reads as one more zoom:

    altitude(zoom) = clamp(2.5 / 2^(zoom − 2), 0.08, 4)
    zoom(altitude) = clamp(2 + log2(2.5 / max(altitude, 0.04)), 0, maxZoom + 2)

The Mars baseline settles it twice. `globe-left` records a chart zoom of
1.3219, which is 2 + log2(2.5 / 4) exactly -- the camera had been pushed out
to the farthest distance, so the clamp is not hygiene but the thing being
read. And `globe-labels-held` records an altitude of 0.68, which is the entry
altitude halved twice: two halvings for three presses, because a press reads
the camera where it stands and two presses in one tick both read the same
standing distance. So a press is written as a halving of what is there rather
than of what was last asked for, eased over the same 180 ms, and the tour's
three presses land where the baseline says they landed.

The locator's mark came out of the same solve, and it is a dot rather than a
rectangle: a fixed 22-pixel box centred on the point the camera faces, in the
canvas's own pixels rather than its CSS box. That is what makes
`globe-entered`'s "117 53 22 22" fall out -- the world square is 8,192 across
and the locator draws it at 256, so a camera over the middle lands at 117. It
never changes size however close the camera comes, because half a sphere is
out of sight whatever the camera does and a rectangle drawn true to the camera
would be saying something false. Over the chart the shelf puts itself away
when the whole map is on screen, which is the other half of the same locator.

The detail neighbourhood, the label key and the grid's fit key are spoken in
the same pairing now: one level deeper than the zoom the distance reads as,
the camera rounded to whole degrees and two decimals of altitude, and the fit
key a reading of depth in half steps that outlives the grid being closed.

Three other things the tour needed and this lane owed it. The zoom controls
are wired here, because a press moves nothing discrete and what it means
depends on which pane is up. The footer's sentence is completed here -- the
server renders how many features are drawn and only this pane can say how many
of them the window is over. And the two halves of a snapshot are merged one
level deep rather than flat, so the application's account of `sync` and
`zones` is no longer erased by this half's four keys; the world is reported by
its title, which is what the baselines record.

* parity: the golden tour, walked against the new build

The gate of M5 and M6, in four files. `library.mjs` links exactly the builds
FIXTURES.json names into a farm, so the registry's newest-wins fold has
nothing to choose between and candidate and baseline are read from the same
bytes. `tour.js` is the walk. `run.mjs` is one walk from a fresh launch --
its own `atlas serve`, its own session directory, its own browser, all three
torn down after it -- and `compare.mjs` diffs it against the baseline, step
by step, and is the gate the harness runs.

The split inside the tour is the whole of the exercise. Every step name, every
order and every self-check is the reference tour's; a step is a stable
identifier and renaming one breaks the contract, so none is renamed, dropped
or added. What moved is the reading: the panes are custom elements now, the
locator's shelf is a region container, the arrangement is a JSON island rather
than a localStorage key, and the empty-library card is a page of its own, so
an absent card is recorded as a card that is not shown. Each of those is
written down at the read. No value was redefined to match what the new build
says, which is the rule the whole instrument exists to keep.

Two seams the reference opened for the harness are gone and needed no
replacement. `refreshCatalog` was a hook that existed only to be driven; the
rewritten page already re-reads its own URL when the library moves under it,
so the tour raises the event the application listens for and the real
reconcile runs through the real wiring. And the log comes back through the
driver rather than being posted to a sink beside the application: a page
served from one localhost port cannot reach another one, whatever the sink
says about origins.

Three divergences are declared rather than absorbed. The hash route is retired
for real URLs, so the fragment the baseline records is empty -- and what it
stood for is recorded unwaived three times over in the same snapshot. A lens
is addressed by name rather than by ordinal, so the select's option values
differ while everything about which lens is open and what it draws stays
bound. And the count of elements on the page cannot be reproduced without
reproducing the markup, which is the opposite of what a clean room is for.

The gate is not green and its flag says so. What the diff still shows is
written in SCHEMA.md as a list of behaviours to build -- the camera that never
reaches the island, the label ladder offered too widely, the reconcile that
is not yet invisible -- so that turning the flag on is a matter of emptying
that list rather than of deciding the gate is close enough. The two gaps the
reference could not reach are re-read there too: the rewrite can rescan, so
both of them now want one route rather than a different architecture.

* parity: what the first six walks found

The gate ran over all six fixture volumes. Three of the step lists already
agree exactly -- cyberpunk-2077, zelda and bend-or, the three volumes whose
producers curated a label policy; the other three carry extra steps because
the ladder is offered where nobody curated one. What the diff shows is
recorded in SCHEMA.md as a list of behaviours to build, with the per-volume
counts beside it, because a number nobody wrote down is a number nobody will
notice moving.

Four defects the walks found are fixed here.

The live-update pair stood outside the shell, like the island before it. The
address they re-fetch is the address of the volume they were rendered for, so
a pair left outside a swapped shell went on naming the volume the page first
opened on -- and reconciling an unchanged catalog took the reader to another
volume entirely, which is the one thing that step exists to prove cannot
happen.

The globe toggle lost its state to every swap. Which pane is up is the seam's,
and the application renders the button that says so with aria-pressed="false",
because the server cannot know. A lens change re-renders the topbar, so the
sphere stayed on screen with a button saying chart, and every globe step after
the first lens swap read as though the reader had left. The seam re-asserts
the state onto the control after every swap.

The locator forgot the sphere. While the globe is up the chart has not moved,
so a redraw the chart asked for -- a filter, a held key -- overwrote the mark
with the chart's own stale extent. The mark is kept until the sphere is put
away.

And the detail neighbourhood took one row too many: a rectangle landing
exactly on a tile boundary claimed the tile beyond it, which is a hundred
tiles where the budget says eighty-one.

The gate counted a volume twice when its walk was red and its diff nonempty,
which is one failure seen from two sides and read as two failures.

* app: the window, wired straight to the handler

The desktop host of issue #5 §3.4, and the end of the framework. The shell at
the module root resolves two directories, opens an OS host over them with a
native picker attached, and hands the application to wails.Run as its asset
server. Everything the old shell reached a framework for -- an event
subscription and a file dialog -- is either the application's own SSE stream
or eighty lines under hostenv/wailshost, where depcheck says a toolkit import
belongs.

The seam travels inside the binary. `//go:embed static` takes the tree `make
static` fills, so a shipped application is one file; a build that skipped the
seam embeds a README, /static/app.js answers 404, and everything but the
viewport still works, which is the deletability principle checked by the thing
that ships rather than only by the suite.

One thing the plan did not know: a webview scheme task cannot express a
redirect. WebKit hands the 302 to the page as a document, and a first launch
showed the reader the word "Found". The handler is right to redirect -- a
world lives at one URL and / is a doorway -- so the host walks the doorway
itself, in redirects.go, GET only and same-application only, passing streaming
answers through untouched. The handler is not told, and what the HTTP goldens
judge is unchanged.

depcheck now carries the module root in its scope, with the shell named as a
lane: the clean-room rule, network confinement and semconv discipline apply to
it, and hostenv purity is declined by name rather than by the shell happening
to sit outside the analysis.

Verified on macOS: `go build -tags "desktop,production"` links (the
UniformTypeIdentifiers framework is still needed, and still one line), `make
desktop` assembles Atlas.app, and a launch against a scratch HOME, a scratch
data directory and a two-volume library draws the explorer -- topbar, legend,
dock, zoom controls -- with the embedded seam booted and rewriting the
footer's sentence. The empty map canvas that launch shows is the same in
`atlas serve` under Chromium, so it is the render lane's, not the host's.

* chore: the reference tree leaves the branch for its tag

M7's last removal. The implementation this rewrite was measured against is
archived at `golden-reference` (b8d28888), where it is checkout-able, whole
and working; 195 files and 43,000 lines leave the branch, and nothing that
runs here goes with them.

What went: frontend/ (73 files), tools/ (30), cmd/cartograph (19), assets/,
and the eleven pre-rewrite internal packages -- arcgismap, basemap, blend,
bundle, icons, ignmap, measure, mgdoc, pbmap, semconv, trekmap. allons leaves
go.mod with them, and the SQLite and BLAKE3 dependencies it brought.

Two things in golden/ read the old tree and had to be answered rather than
deleted. The capture programs opened bundles through internal/bundle and
translated through four source packages; they are archived with the tree they
read, behind a README that says how to run them from the tag, because a
golden is still never edited to match a candidate and a corrected golden still
comes from a re-capture against the reference. One package moved instead of
leaving: golden/canon, the fixture canonicalizer, which was never a capture
program but the agreement between the capture and the gates about what a byte
comparison compares -- golden/format and golden/pipeline still need their half
of it. The analysis oracle went the same way: engine/current.mjs, its hooks
and stubs, capture.mjs and the ATLAS_ANALYSIS_ENGINE=current side-by-side all
loaded cell math out of frontend/src, and all now run from the tag. The gate
did not move a fixture.

ci.yml is retired. Its `v*` trigger and its whole per-platform recipe move
into release.yml, which now builds the desktop application beside the CLI on
three platforms -- with `make static` first, so the shipped binary carries the
seam -- and keeps `atlas-v*` working. A v* tag today builds the new tree.

The one workflow that could not be repointed is the city snapshot: its first
step is an ArcGIS crawl, and that is the one crawler the rewrite has not
written. Its steps are now the clean room's four commands, its scheduler is
dispatch-only with the cron it is waiting to become written beside it, and
docs/snapshots.md leads with what unparks it. The private-curation hook went
with the mechanism the new source declined to carry: a city that should stay
out of the repository rides on the atlas-ref input instead.

Swept after: the README describes the tree that is here, .gitignore no longer
guards paths that do not exist, eslint has nothing to exclude, and the
provenance documents that name reference-tree paths each say in one line that
those paths resolve on the tag. docs/README.md's close-out audit answers the
comment-policy item and adds the parked scheduler as a question for whoever
closes M7.

Gates: go build, go test, depcheck and the whole harness green -- 9 passed, 1
skipped (parity-compare, which is somebody else's to burn down).

* docs: the redirect a webview cannot carry

ADR 17. The application answers / and /open with a 302, because a world lives
at exactly one URL and a doorway is not a second name for it. A webview scheme
task has no way to express a redirect, so the platform hands the 302 to the
page as a document and the first launch of the desktop host showed the reader
the word "Found".

The record is here rather than only in a comment because the choice among the
three answers is the interesting part: the handler goes on redirecting, the
shell mounts the same handler everything else mounts, and the host walks the
doorway itself. It also names the rule the next one of these should follow --
a limitation of the host's transport is answered in the host -- and says why
the WASM host will not need it.

* parity: the burn-down, from 2,311 differing fields to 81

SCHEMA.md §7's list, worked through against the tour. Tunic went from 2,311
differing fields and twelve failed invariants to 81 and none; every volume's
step list now matches its baseline. What follows is what was actually wrong.

THE APPLICATION

The camera never reached the island. It arrives on its own route, from the
seam, after the view has settled, and nothing else was going to happen
afterwards to carry the record back -- so `session.entry.center` and `.zoom`
were null on every step of every volume. The island is a region now and every
session answer carries it, because every session answer has just written the
record it publishes. The report is still the one concern that moves nothing a
reader can see: an inert script node has no focus to lose and no scroll to
reset.

A shard did not narrow the ground. A shape on another lens's layer is
elsewhere in the world rather than filtered out, exactly as a point is, and
saying so for points only left zelda listing twenty rows of ground that is not
on the map and counting twenty-three shapes where three are drawn.

A held cell narrowed nothing at all. The panel's count is the count of what
the map is drawing, and the map draws what the cell holds, so the application
has to answer the containment question itself; the alternative is a third
upward flow from the seam, which the seam's own contract forbids in as many
words. That answer is `internal/app/cells`, deliberately the smallest piece of
a cell system that will do -- the recursive halving and nothing else, no plan,
no ring, no pole closure, no token. It is a second copy of an arithmetic and
that is a cost rather than a design, so it is gated: `golden/cells` reproduces
all 1,248 cell extents the six baselines recorded, which is the oracle
`analysis-vectors` holds the TypeScript to. A Go twin of the analysis lane
deletes both.

The keyboard listened on the body, where a keystroke with no focused control
is never heard. The sidebar's own shortcut re-rendered a whole HTML document
into the middle of the page: the shell renders a document on a first paint and
its own frame on a swap, and the frame is what a partial carries. Two records
could be written at once and one of them lost. And the island rounded a half
away from zero where the harness reading it rounds a half up -- on the y axis,
where every world coordinate is negative, a whole world unit of difference in
a field the baselines compare exactly.

THE SEAM

The diagnostics were opened over the viewport element that existed at boot, so
every reading after a navigation answered for the volume the reader had left.
The swap hook listened for htmx 2's event names -- htmx 4 separates the words
with colons, so nothing in this lane was ever told a swap had happened. The
camera report was this lane's own fetch and threw the answer away; it is the
page's post now, filled through fields the application renders for it, which
is the arrangement the labels hint and the corner locator already stood in and
leaves this lane's only network calls on the data plane. A resize observer
tells OpenLayers when the pane moves without the window doing anything.
Selecting a feature moves the camera and closing the card gives the view back
unless the reader steered; descending into a cell holds it and ascending goes
back out the same way. A grid counted its own narrowing, so every neighbour it
drew carried a zero. And a style function that throws takes the map down with
it -- there is a moment during a navigation when the world the styles were
built for is not the world the base names any more.

* app: the map nobody could see

The rewritten application has never drawn anything, and every gate in this
repository agreed that it had.

#map and <atlas-viewport> are absolutely positioned siblings filling the same
box. In the reference implementation #map *was* the map -- the renderer drew
into it -- and it carries the backdrop the map sits on. Here the panes are the
custom elements and #map is what is left over: the focusable surface the
keyboard is heard on, and the home of the loading scrim. Rendered after the
panes, its backdrop covered them completely. Mars fetched its tiles, built two
thousand and forty-eight sprites, reported a settled camera and wrote "2,048 of
2,048 features in view" under an empty rectangle.

So the backdrop goes first and the scrim goes last, because a message about
loading has to be readable over the thing that is loading.

The parity tour could not catch it, and that is the more interesting half. It
is a count, a flag and a string all the way down; a count of what is drawn is
not a question about what is on screen, and SCHEMA.md said as much under
"Nothing checks pixels" without anyone reading it as a hole this wide.
`checkCanvas` now asks two questions on the first step of every run: whether
the pane's own canvas is the thing at the middle of the map, and whether it
carries more than one flat colour. It is an invariant the tour checks on
itself rather than a value compared against a baseline, which is the only
shape a pixel check can take here -- there is no golden screenshot and there
should not be.

Found by the desktop-shell agent's smoke test, which looked at the page.

* render: the corner that was never told the camera moved

The locator answers two questions about the camera -- where it is, and whether
it can see the whole map at once -- and it was only ever asked when a filter
moved. So a zoom left the shelf put away across a whole step: the map no
longer fitted its window and nothing had told the corner so, which took
`overview-docked` and `overview-undocked` out of four volumes' walks
altogether. Both questions belong to the camera's own event.

Mars is back to fifty-nine steps, which is its baseline's own count.

* render: the camera built the way the recording built it

The last of the float drift, and it was never floating-point noise.

The chart's view was constructed with a maximum zoom and left to derive its
own resolution ladder. The reference implementation handed one in — the world
square over the tile size, halved per level — and a view given the ladder
converts between a zoom and a resolution by walking it, where a view deriving
one arrives at answers that differ in their last bit. The baselines compare a
camera exactly, so the last bit is the difference between agreeing and not.
Tunic went from eighty-one differing fields to one on that change alone.

Two of the same constructor's options were missing with it, and each was a
defect of its own wearing the float's clothes. `extent` and `showFullExtent`
hold the camera over the ground the lens drew, so widening the window — folding
the sidebar away — slides the view back inside its own picture instead of
panning off the edge of it, which is what `sidebar-collapsed` records and what
this build had been failing. And `smoothResolutionConstraint: false` is what
makes the two ends of the ladder ends rather than springs.

None of the three can be changed after a view is built, so the view is rebuilt
per lens and the camera carried across by hand.

`fitZoom` is decided once, when the world opens: the zoom the reader arrived
at, whether that came from a fit or from the camera they left behind.
Recomputing it later left the corner locator arguing with a camera that had
moved on.

The application wrote its counts without separating thousands, so the panel
said "2048 features" where every recording says "2,048". Every count on the
page goes through one place now.

And the globe toggle's handler resolves the live viewport rather than closing
over the one that wired it: a morph can carry a control across with its
attributes intact — including the mark that says it is already wired — while
the element it was wired to is no longer the element on the page. The tour
re-queries it at every press for the same reason.

* parity: two volumes green, and the five defects between here and six

The final run of the round: `tunic` and `cyberpunk-2077` agree with their
baselines field for field, and all six step lists match -- which is the harder
half, because it means the tour reaches the same places on every shape the
format can take. `parity-compare` stays unready.

What is left is written in SCHEMA.md §7 as five defects rather than as one
thousand five hundred and twenty-four fields, because a field count is a
symptom. The largest is that a row of a shape collection's feature index
highlights where the reference jumped: a left-click there went to the feature,
opened its card and flew the camera, and this build filters the map instead.
Everything downstream of it on zelda and bend-or is that one wrong verb.

No tolerance was declared and no waiver added. The last-bit float drift that
stood on this list turned out not to be floating-point noise: the chart's view
was built without the resolution ladder the reference handed its own. A camera
compared at 1e-9 was never over-compared; it was under-built.

* docs: the walkthrough found the lane's own inputs undocumented

The M7 exit criterion is a newcomer building a bundle from the documents
alone. Walking it turned up four places where generate.md and app.md
describe a tree that has moved on.

The capture archive's layout diagram was missing two levels. A volume sits
under the register entry's own `directory` field -- `games/<slug>-<id>` in
every staged archive -- and its worlds sit under `maps/`. A reader following
the diagram would look for `<root>/<volume>/<world>/snapshots` and find
nothing, which is a poor first hour.

Nothing said where the archives and the tile sets are. They are gitignored
and staged in the working copy, every default in the tree points at them,
and the only place that was written down was a test file's comment. §7.1
now names all three with their environment overrides and spells the two
commands that build one volume -- and says the thing that costs an
afternoon otherwise: deriving tiles into a fresh directory stamps
differently from the staged set, because a derivation stamp covers the
deriving tool's own source, so the bundle that lands is a correct build of
the same volume under a different name. Composing against `tiles/index.json`
is what reproduces a fixture byte for byte.

§8.2 was still waiting for a city that arrived. The archive was re-crawled
into `crawl/bend-or/fmg-archive`, the re-crawl reproduced every non-manifest
entry of the lost build, and the offline renderer it was also waiting on
landed in §4.4 -- which the section cited as §4.5. What is actually
outstanding is the clean-room crawler, and only for taking a *new* day.

app.md carried three notes written before M6: the seam "lands in M6", the
`-seam-watch` flag is a stub, and the grid cull belongs to a lane that has
not landed. The seam landed, the flag runs the bundler, and the cull is
`internal/app/cells` -- which is why the panel's count and the map's
drawing are one number. The flag's *usage string* is still the stub's, and
that is recorded as a code defect rather than papered over here.

enrich.md's proof table had a blank line inside it, which renders the last
row as a second table with a header made of its own data; and its CLI
section now says what `nothing to add` looks like and where its inputs are.

* docs: the seam's brief, read the way a rewriter would read it

§8 of issue #5 holds this document to one standard: someone who has never
seen render/ can delete it and write it again from here. M7's exit is to
run that test rather than assert it. Running it found four places where the
document says something the code does not do, and one honest answer about
where it stops.

The globe's altitude pairing was written down twice and the two disagree.
§7 gave a field-of-view derivation -- viewport height over the equirect
pixel span -- and §10 gave the power law the code actually uses. A
rewriter reads §7 first, implements defensible arithmetic, and reproduces
none of the Mars baseline's numbers, which is exactly what the code's own
comment says happens. §7 now carries the power law and §10 keeps only the
calibration that proves it.

§5 claimed an outward surface that is not the one the seam has. The camera
report is not a request the seam makes: it writes five hidden inputs and
raises `atlas:camera` on the window, and a hidden form in shell.tmpl does
the posting -- which is a stricter reading of "fetch lives in one place"
than the document was giving itself credit for. And the pick's other half,
"the page's one glue listener", does not exist: `atlas:pick` is dispatched
by both panes and consumed by no template. The seam keeps its half of that
contract and the page does not; it is now §10's last entry rather than a
sentence implying it works.

The inbound direction was missing altogether. `atlas:rescan` appears in
§10 as something the reconcile test raises and is defined nowhere; it is
one of five event names the boot module listens for on the body.

Then §10.1, which is the part worth reading. The document is enough to
build a *correct* seam and not enough to build one whose diagnostics equal
a baseline, because a baseline is full of numbers this document does not
carry: every drawn dimension, the four-pixel hit tolerance, what `members`
counts in the priority formula, that the overview's 168 is a level
threshold and not a canvas size, that `fitZoom` is captured once and
`zones.focused` is deliberately not emitted, and the explicit resolution
ladder that is the difference between a camera that equals a baseline's
and one that is close to it. Two of them are contract rather than
cosmetic -- `atlas.icon.outset` and `atlas.render.as` are semconv keys the
seam reads and reads wrongly -- and they are named as the next thing to
write down here.

The line budget sentence was also two hundred lines out of date, and the
seam is now past the guideline rather than under it.

* docs: the close-out audit, answered rather than listed

This file has carried five questions for M7 since the documentation plan
was written, one of which said plainly that nobody had run the walkthrough
test and that no amount of reading replaces it. It has been run: a
newcomer's pass over tunic and cyberpunk-2077, from these documents and
nothing else, through compose, enrich, measure, serve, deleting render/
whole, the registry.yaml codegen loop and the workbench.

The five questions are now five answers. The walkthrough passes, with a
limp: every step worked, and four of them needed a document corrected on
the way past. The blind rewrite is the one that does not pass, and
render-seam.md §10.1 is where that is written down instead of here.

HARNESS.md's placement is confirmed rather than reversed -- a document
about how to read a gate belongs beside the gate -- and the parity-diff
section it owes is blocked on the same skip it always was.

The comment-policy grep was re-run over the whole tree and item 4 no
longer claims more than is true. Almost everything left is provenance in
the places provenance belongs, but one package comment cites the old
tree's own file names as the origin of a contract, which is exactly the
prose §8 sends to decisions/ or nowhere -- and workbench.md already
carries that account properly. It is named, with the grey-area cases
beside it, rather than left inside a sentence saying the grep came back
clean.

And the scheduler stays parked, because generate.md's two accounts of the
crawler now agree that it is the last item on a shipped lane rather than
the first item on an abandoned one.

* app: the four the tour found on this side of the seam

Four of the residue's five defects were the application's, and one of them was
not the defect it had been diagnosed as.

**A row of a feature index goes where it points.** A left click there posted
`/session/highlight`, so the row filtered the map instead of travelling to it,
and the right button had nothing to answer at all. The row is now two
elements -- one element carries one request -- and they answer the two verbs
the reference gave them: the button selects the feature, opens its card and
flies the camera, and the wrapper around it hears the contextmenu the click
bubbles past and highlights. Three things came with it. A jump made from a
list says `focus=1`, because a pick off the canvas is a reader who was already
there. The mark a jump leaves on the index outlives the card it opened, which
is a different fact from what is selected and is now a field of its own,
cleared when the ground is rebuilt rather than when a card is closed. And the
index lists the ground the reader is standing on: a split sheet's other layers
are elsewhere in the world, not filtered out, so they are not in it.

**Titles come in the order a browser puts them in.** The reference sorted with
`localeCompare` and the baselines are a recording of the CLDR root collation:
punctuation before digits before letters, `¡La Fantoma!` at the top of a list
that a byte comparison starts with `188 Trading Post`. `internal/app/collate.go`
is that collation in the two strengths the reference used -- the panel's list
plain, a feature index numeric and case-blind -- over `golang.org/x/text`,
which was already in the module graph and moves one line from the indirect
block. It reproduces the browser's order over all 7,879 titles the fixture set
carries, and the two orders are held to a recorded sample in a test.

**A world opens with the panel away.** The fold lives where a world change is
actually served: the world select navigates, so `handleExplorer` folds it, and
`applyWorld` does too for the route that takes the same move by hand.

**A cell narrows what stands on the ground, not the ground.** This is the one
that was diagnosed wrong. The two halves do not divide different ground and
never did -- every fixture's lens declares a surface and both sides start from
it. What differed was what a held cell *culls*: the server was dropping shapes
whose middle fell outside the cell, and the reference dropped none. Descending
is a question about what is standing in a cell; the ground it stands on goes
on being the ground.

Two more, small: a lens swap re-renders the legend and the dock, because on a
split sheet a lens is a different layer and the ground under the reader changes
with it; and highlighting a piece of ground brings its collection back, since
asking to look at it and keeping it put away cannot both be meant.

`golden/parity/tour.js` re-points one read. `zones.focused` was the reference's
`focusedZoneID`, which a jump set and only a rebuilt ground cleared -- so
reading it off the open card answered null the moment a card closed, which is a
different question from the one the baselines recorded. It is read off the row
the index marks as current instead: the same fact, on the element that now
carries it.

* render: the sphere holds its ground, and turns to what it is asked for

Everything the globe drew of a cell was its outline. The reference drew three
things per cell -- the tint or the dim on the ground, the boundary over it, and
the address on a card in the corner -- and which of the three a cell gets is
the analysis lane's answer, not a renderer's. So the sphere asks
`gridCellVisual` the same question the chart asks, handing it the one thing
only a renderer knows: whether the address would fit inside the cell as this
camera sees it. On the sphere that is worked out from the camera's distance and
the field of view rather than from a resolution, which is the same gate spoken
in the other pane's terms.

Two moves were missing with it. Descending holds a cell, and holding it turns
the planet: a cell chosen on the far side of the world is a cell nobody can
see. And a selection made while the sphere is up turns it too, keeping the
reader's distance, because a ring drawn on the far side selects nothing anyone
can look at. Both are changes rather than states -- opening the grid over the
ground the reader is already on moves nothing, and coming up to the sphere
lands on the chart's camera, which is where they were.

The neighbourhood under the camera is named in tiles now instead of measured in
pixels. It is budgeted in tiles -- a block of (2·reach+1)² around the one the
camera faces -- and deriving that block back out of a rectangle of world pixels
put a hundred tiles under a camera the reference gave eighty-one, because a
rectangle landing a hair past a boundary takes the whole next row. Wrapped in
longitude, because the sphere closes; clipped in latitude, because it does not.

Three smaller ones. A lens swap drops the neighbourhood it drew for the pyramid
it is leaving. Putting the pane away keeps the *name* of that pyramid, which is
a record of what the sphere read and not a live handle. And the key the names
are raised under carries the cell system whether or not a grid is open, because
which system would divide this world is a property of the world.

* render: one press, one flip, and a pane put away has no window

The globe toggle worked by hand and did nothing at the end of a tour, and the
reason was a mark in the one place a mark cannot be kept. The seam wired the
control once and wrote `data-atlas-wired` on it to say so -- but a morph
rewrites an element's attributes from the server's markup, which has never
heard of that attribute, so the mark was rubbed off on every swap while the
listener it recorded stayed attached. The listeners accumulated, one press
flipped the panes once per listener, and whether the sphere came up was decided
by the parity of a count: odd by hand, even after a tour's worth of swaps. The
fact belongs to the lane that owns it, so it is a WeakSet here and no swap can
reach it. Which pane is up is also set before the sphere comes up, because
coming up is the sphere's first camera event and the corner locator will not
follow a globe that is not up yet.

**A pane put away has no window, and every question about a window follows.**
The chart went on standing behind the sphere at full size -- `display: block`
on the custom element is stronger than the browser's own `[hidden]` -- so it
went on answering how much of the map it could see and where the reader had
left off, from a window nobody was looking through. It is put away properly
now, and three answers follow from having no window: nothing is in view, a fit
lands wherever a fit into nothing lands, and no camera is reported. The last is
the important one: a cell fitted into no window would otherwise be saved as the
place to reopen the volume on.

Which events recount had to move with it, because the reference recounted on a
filter, a lens, a camera and a resize, and never on the held key. So `restyle`
no longer writes the footer's sentence -- holding Z restyles -- and the four
places that own the question write it themselves. Flipping panes is not one of
them: nothing about the world changed, and the sentence on screen is still the
last true thing the chart said about it.

Two more. A jump to *ground* fits the shape's extent with room around it rather
than flying at the middle of it, which is what a district reached for from an
index needs, and the destination is read out of OpenLayers before the flight so
that closing the card can tell a landed camera from a steered one. Each jump
holds the view afresh, overwriting whatever the last one left, so reading three
rows in turn steps back through them one view at a time. `selectedPin` is a
pin and only a pin -- the reference put the selection down when a card opened
on ground. Ground is counted in view by whether the window is over it, not by
whether a corner of it falls inside: a district big enough to fill the screen
has every vertex off it. And the corner locator is redrawn after a swap, on the
canvas the swap just replaced.

* parity: the gate goes green, and the two costs it carries

All six volumes agree with their baselines, step for step and field for field,
and the tour's own invariants hold on every step of every walk: the map, the
footer and the dock tell one story, a closed card gives back the view a jump
borrowed, and the sphere and the chart lose the same pins to one filter. The
milestone opened at 2,311 differing fields, stood at 1,524 across four volumes
when this round began, and is at none. `parity-compare` is ready.

Two differences are declared rather than reproduced, and they are the same kind
of thing: a number the reference read off a window that was about to stop
existing.

`fit-across-an-opening` is `fitZoom` at the two steps where a walk opens a
ground it was not standing on. The reference folded the panel beside the map
and fitted the camera on the next animation frame, before the browser had told
OpenLayers that the map had grown -- so a ground reached from somewhere with
the panel out was fitted to the narrower window it was leaving. The fixture set
records both answers for the same volume, which is what makes it a race and not
a rule. A page here arrives with its panel already in the state it will be in.

`chart-camera-under-the-sphere` is mars's *saved* chart camera at five of its
globe steps. The reference wrote its whole arrangement on every interaction,
the chart's camera included, so a filter pressed while the sphere was up saved
a camera that was a fit into a window of no size. Only the pane the reader is
looking through reports one here. The chart's live camera is bound at those
same five steps and agrees.

Both name the steps they cover, which the waiver format now allows and which is
a narrowing rather than a loosening: a field waived at one step of sixty is a
smaller bill than the same field waived across a whole tour, and spelling the
step is what keeps it bound everywhere else. Both are printed by the gate and
again by the harness, with their paths and their steps.

One honest hole comes with the flag. This is the only gate that drives the real
application in a real browser, so it needs a built seam and a Playwright
Chromium; a tree with neither is told so in one line and the gate returns
without judging, which is the same bargain `generate-enrich` makes with a
machine that does not hold the capture archive. The CI job installs no browser
and builds no bundle, so **CI does not walk the tour**. That is written down in
HARNESS.md rather than left to be discovered, along with what closing it would
cost.

HARNESS.md also owes issue #5 §8 a reader's guide, and now carries one: how to
read a parity diff, in the order the diff wants reading -- the step list before
any field, the tour's own problems before any comparison, the earliest step
rather than the loudest, and which half of the snapshot a field belongs to.
SCHEMA.md's residue section is replaced by the numbers, the two waivers, and a
short post-mortem of the five defects this round was handed: four were real,
and the fifth was a different defect wearing the same symptom.

* render: the sphere gives its cards back

A grid rebuild minted new geometries and, for every address it wrote, a canvas
texture of its own -- and then dropped the objects out of their group without
disposing any of it. The group is not the card. One flight to a held cell
rebuilds the grid several times, at a couple of hundred objects a rebuild, so a
tour that visits the sphere twice leaves a few thousand textures on a GPU that
was never told it could have them back. It survives on an idle machine and
wedges the renderer on a busy one, which is exactly the shape of a bug that
passes once and stalls the next run for twenty minutes.

Cleared and released now, on the same two groups the reference disposed: the
cells and the names Z raises.

* chore: the last references to a tree that is now a tag

The comment policy says the old tree's trail stays readable on the
golden-reference tag and nowhere else. The operation runner cited its
ancestors by filename, the dev loop still spoke of a seam that had not
landed, the eslint header remembered a frontend build that is gone, and
the parity schema pointed re-capture at paths the archive removed. All
of them now say what is true today.

The audit's three seam findings and the two standing bargains (no tour
in CI, the seam's line budget) move into docs/README.md as named
follow-ups, where the next session will find them.

* app: the card says it is closed, and the list comes back

The panel counted correctly and showed nothing. The carried pin-detail.css
hands the results list over to the card with a rule that reads the card's
`hidden` attribute, and the card never wore one: the template rendered
`<article id="atlas-detail" class="pin-detail">` in every state and gated only
what went inside it. So the rule fired always, and the list was display:none
under an empty card from the first paint onward.

The patch that was in chrome.css keyed the same question off `:empty` instead,
on the belief that a region rendered inside-out cannot be given an attribute.
That belief is out of date -- `detail` is an outerMorph region, so a swap
replaces the element and everything on it -- and the patch was in any case two
rules that both hide and none that show, which is the same blank pane by a
second route.

The card renders its own `hidden` from the session now, which is what the
reference implementation's script set by hand after every render, said once at
the source instead. The chrome.css section goes, the carried stylesheet works
verbatim, and the file's header says why a difference the markup can spell is
not a difference worth carrying here.

Held by a walk through the four states the rule distinguishes -- a full list
with no card, an empty list with no card, a card open over the list, and the
way back -- reading the attribute off both places one answer renders the card,
because two answers that disagree are the same bug wearing a hat. The parity
tour is untouched and unmoved: `detailOpen` counts the card's children and
`searchResultsVisible` reads the list's own hidden property, and neither of
those is what changed.

* app: somebody is listening for a pick now

Both panes have reported a pick since the seam landed, and nothing on the
page was standing there to take it. The event was dispatched, the identity
was right, and clicking a feature on the canvas moved nothing at all --
selection reached the session only through the dock and the card. The
seam's half of the contract was kept; this is the page's half.

It is the camera's arrangement, sibling to it in the shell and for the same
reasons: the application renders the form and the field it wants filled,
the seam fills it in and raises the event, htmx posts it and applies the
answer. The answer is the selection the legend marks and the dock and the
card draw, which are this page's regions and not the seam's, so a request
the seam made for itself would throw all of it away.

One field, and the two absences are the argument. The volume is inherited
from the shell like every other interaction's on this page. `focus` is not
here at all: a row reached for from a list means take me there and mark it
in the index while I stand on it, and a pick means neither -- the reader
who clicked the ground was already on it. The test holds both ends, the
form being rendered and wired, and the post leaving Focused alone.

* render: a pick leaves the seam the way the camera does

Both panes built a CustomEvent by hand and dispatched it at themselves,
bubbling and composed, for a page listener that was never written. The
report module already knew how to hand something to this application --
fill the fields it rendered, raise the event on the window, let its own
runtime post it -- and a pick is the only other thing that leaves this
lane, so it leaves the same way.

A MISS DOES NOT POST. Clicking open water is not a request to close what
the reader is reading: the card is put away by Escape and by its own
button and by nothing else, and an empty identity posted from here would
clear the selection on every stray click. So the chart returns on no hit
and the sphere returns on nothing near enough, and the report itself
refuses an empty feature whoever asks -- three places, because the rule
belongs to the report and the panes each have their own word for a miss.
The hover is untouched: leaving a feature still clears the cursor, which
is continuous state a pane owns, and a selection is not.

`kind` travels in the event and posts no field. The session stores an
identity and looks the rest up, but a future consumer that wants to tell a
pin from an area should not have to ask.

* render: the ladder a pin's name climbs, and the rim it wears

A pin's label was drawn for anything the seam had promoted, and promotion is
not attention: a search promotes every name it matches so the declutterer
cannot drop one, so searching for a place answered with ten copies of the same
word piled over one town. The reader's own word was not read at all -- a
collection silenced by hand still gave up its names under Z, and one the
reader had asked to speak stayed silent.

pinLabel now climbs the same ladder areaTitle does, with a point collection's
own default at the bottom: the override wins, then the curated
atlas.label.policy, then silence unless the curation asked for text. Over the
policy sit exactly two states, the pin being pointed at and the pin whose card
is open. Promotion keeps everything it decided before -- which layer a label
is drawn in, and whether it may be decluttered -- and decides nothing about
whether there is one.

The marker rim was being handed the outset token, "light" or "dark", where a
colour belonged; OUTSET_COLORS held the two colours and nobody read it.
outsetColor names the lookup and keeps the reference's rule that anything but
"dark" reads light, so an unset or misspelled outset still draws a legible
marker. textSymbol goes with it: a text collection draws an ordinary marker
and always has -- the text-only symbol was dead in the reference too, and
atlas.render.as survives only as the default its policy falls back to.

The seam had no test over its styles. It has a table now: the ladder rung by
rung, Z against a hand-silenced collection, a ten-match search that labels
none of them, and the rim colour of every outset a world can declare.

* render: the skin and the neighbourhood stop cancelling each other

The sphere came up black on first entry, every time, and it was the one
counter in texture.ts that did it. The base skin and the neighbourhood
under the camera composite into one texture through one paint loop, and
that loop answered to a single generation number -- so the cheap pass
that runs on every camera move could cancel the expensive one that runs
once per lens.

The sequence was deterministic. openLens drops the neighbourhood and
then starts the base skin without waiting for it; enter's first
refreshDetail, seen from far enough out to want no detail at all, drops
the neighbourhood a second time while the skin is still suspended on its
first tile. The second drop bumped the shared counter, the base paint
woke up, found itself superseded and returned having drawn nothing --
and because baseKey had been committed before the painting rather than
after it, nothing ever asked for that skin again.

So there are two counters now, one per pass, and each pass checks only
its own: clear cancels both, clearDetail only the neighbourhood. And a
key is committed only when its paint finishes un-superseded, with the
key cleared as the paint begins, because a pass that was abandoned half
way left the texture holding neither the composite it was replacing nor
the one it was drawing. The neighbourhood gets the same treatment: it
had the same latent shape, just no call sequence that reached it.

Nothing about the passes' external shape moves. The tiles map is the
same map the diagnostics are bound to, and what counts as a detail tile
-- which is what detailTiles and detailZoom report to the parity
baselines -- is untouched.

test/texture.test.ts stands the skin up in a stubbed host where images
are handed over one at a time, which is the only way to be standing
between two awaits when the interruption lands. Six tests: the drop that
used to kill the skin, a skin that arrived not being painted twice, a
skin cancelled by a whole-skin clear being asked for again, a newer skin
superseding one in flight, a neighbourhood cancelled mid-block being
asked for again with its bookkeeping intact, and a camera holding still
compositing nothing. Three of them fail against the file as it was.

* render: the lens draws inside its own window, on its own ground

A lens declares two things about how it should be drawn that the chart was
not reading: `background`, the colour the sheet was drawn on, and `bounds`,
the window of the world square its pyramid actually fills. Both were in the
payload and both were being honoured elsewhere -- the corner locator has
always painted the backdrop under its thumbnail -- so on every volume that
declares one, the shelf and the map disagreed about what the same lens looks
like, and the shelf was right.

So `buildRaster` now sets them. The backdrop goes on the base layer alone,
which is what the reference did and what the pair's shape requires: a colour
on the detail layer would paint over the complete pyramid exactly where the
deep capture has a gap, which is the one thing the two layers exist to
prevent. The window goes on both, as `extent`, and it is the same
`lensExtent(lens, grid)` the camera fits -- the lens's bounds, or the whole
square for a lens that declares none.

Clipping is not the same guarantee as not asking. Coverage already refuses
every tile outside the window and `wrapX: false` refuses the repeats, so no
request moves; what moves is what gets *drawn*. An unclipped layer still
paints out past the window, and on a sparse pyramid or one piece of a split
sheet that margin is either transparent -- a hole -- or the ground of the
layer stacked beside it. The detail layer's `maxResolution` gate and both
sources are untouched.

The seam tests grow a raster file to hold this: the backdrop is on the base
and never the detail, a lens that declares none gets none, the Pip-Boy
pyramid over the Mojave (a 4,159 x 4,378 window well inside the 8,192
square) clips both layers to its bounds, Tunic's bounds-less lens clips to
the square, and every lens of every fixture volume gets the extent its own
camera fits. Four of the five fail without this change.

* app: one import, one row, and a cancelled one says nothing

The import region was an append. Every state of every run was a line of its
own -- picking, installing, installed -- and nothing ever took one away short
of a full page render, so a reader who imported three volumes was looking at
nine lines of a log they never asked to keep. A cancelled picker was the worst
of it: the handler wrote "choosing a bundle" before it knew whether anything
had been chosen, and then wrote "nothing was chosen" underneath it, so closing
a dialog you opened yourself left two permanent lines saying so. The
reference implementation had no import panel at all and said nothing here, and
saying two things is further from that than saying one.

The region is an outerMorph now, and the stream is a stream of states of the
same row rather than a stream of rows: the handler renders the whole region,
and flushes it, for every state a run reaches, and each render replaces the
last. The row carries the run's number as its element id, which is what the
morph works against -- within a run it finds the same element and updates it
in place, and a second run brings a different element, so its dismissal starts
over rather than inheriting one that has already played out. The section is
also the same element throughout, which is what lets its aria-live actually
announce the changes.

That container is what makes silence possible. A cancelled picker's last word
is the region rendered with nothing in it, which is where the page started;
the reader closed their own dialog and is told nothing about it. The other two
answers PickFile can give are unchanged and stay distinguishable: a host with
no picker is refused out loud, because a reader who pressed the button is owed
a reason.

A run that ended well marks its row and the stylesheet fades it out four
seconds later -- an animation with a fill mode, no script, nothing for the
handler to remember, and no motion for a reader who asked for less. A refusal
does not fade: it is the only account of what went wrong, and the next import
is what replaces it, which under a morph is a replacement rather than another
line.

The app tests follow the row instead of the bytes: every write of a run is the
region morphed onto itself and holds exactly one row, a cancelled import ends
holding none and the section says it is hidden, a refusal stays and does not
carry the fade, two imports in a row are one row on screen under two different
ids, and a first paint carries the region empty so there is something for the
morph to find.

* app: the import buttons stop describing a swap that never runs

The import response is partials only, so the buttons' own target and
swap were never consulted -- but they still read as a description of the
region's mechanics, and they described the append behaviour the region
just gave up. hx-swap="none" says what actually happens: the partials
land where they say, and the button's own exchange swaps nothing.

* render: the name a pin is given, and the side of the planet it is on

Five things the sphere was doing to every pin at once, all of them in
globe/element.ts and all of them the same mistake in different clothes: the
sphere was drawing what was cheap to draw rather than what was asked for.

A name's card is measured now. It was written onto a fixed 256x64 canvas at
28px and stretched to a fixed sprite scale, so every name longer than the
canvas was clipped, every name shorter was padded, and both came out the same
width on screen -- whatever a name actually was, the sphere said it in the
same nine characters. The card is cut to the text instead: measured in the
font it is drawn in, and the canvas, the texture and the sprite's aspect all
follow that one number. It is anchored by its bottom edge, so it floats above
the marker rather than covering it.

Which names are raised is the camera's answer now, not the legend's. The
budget of 180 is unchanged and is a decision about a sphere; the ordering was
collection priority, which knows nothing about where anybody is standing, so
the cards went to whichever collections happened to be rarest wherever in the
world they stood, and the ground under the camera came up bare. Candidates
are the standing pins within eighty-five degrees of what the camera faces,
nearest first.

The horizon is put back by hand. Cards and chips draw with the depth test off
-- a screen-sized card anchored on the ground loses its lower half to the
planet's own curve at any glancing angle -- and nothing was then stopping the
far side of the world from shining through the near side. A point at the limb
sits where the cosine of its angle from the camera's axis equals the radius
over the distance, and anything past that goes invisible. Only the two groups
that turned their depth test off; the pins keep theirs and the sphere occludes
them itself, which is what keeps the standing count a reading of the filters
rather than of where the camera is pointing.

A pin wears its collection again. Two thousand identical cyan beads say only
"something is here", which is the one thing a reader can already see. Each
collection's mark is drawn once onto an 80x80 canvas -- its picture where
there is one, tinted for a glyph and left alone for a photograph, its initials
where there is not -- rimmed with the world's declared outset and cached as
one shared sprite material per marker and selection state. The chosen pin
grows and takes its white ring by pointing at the other cache entry.

And a rebuild gives back what it minted. The names and the grid mint a canvas
and a texture per card, and a camera settling through a flight rebuilds both
dozens of times; dropping a sprite out of a group frees none of it. One
release does both groups now, and it leaves three's shared sprite quad alone,
which the grid's own loop had been disposing along with the meshes. The pins
stopped minting anything to leak the moment their materials became shared.

The counted quantities are unmoved: pinSprites is the size of the id-to-object
map, one entry per point whatever kind of object stands at it, and
visibleSprites reads that same map, which the horizon does not touch.
labelSprites is the label group's child count, and culling sets visible rather
than removing.

* app: the server divides by every system a session can hold

A grid narrowed the map and not the count. `heldCell` understood geohash and
answered "no cell" for anything else, so a session holding an S2 address had a
seam that narrowed the drawn set and a server that narrowed nothing: the map
drew one cell's worth of pins, the footer and the dock counted the world, and
the checkSync tour invariant -- the count above the panel is the count of what
the map is drawing -- was broken in the one place nobody could see it, because
the tour never cycles to S2.

The other half was the way in. `applyGrid` took any `system` string verbatim,
so one POST could put a value in the record that no world offers; on the seam
a system that does not apply to the ground still resolves out of the registry,
and S2's `requireMapping` throws where `refresh()` has no catch. A live crash
path from one request.

internal/app/cells grows an S2 side. Nothing spherical is written here:
github.com/golang/geo/s2 is the canonical library and the seam's s2js is that
library ported, test vectors and all, so the two lanes hold the same
arithmetic by provenance rather than by two authors agreeing. What is written
is the bridge -- world pixels back onto the sphere through the flattening the
world declares, which is `format/semconv`'s Equirect inverted, with the sign
flipped at the one call where y-negative-down meets y-down -- and the port's
own level bookkeeping, where the root is a place and the six faces are level
one.

`heldCell` is a predicate now rather than a rectangle, because only one of the
two systems has a rectangle: a geohash cell compares four numbers, an S2 cell
is a spherical quadrilateral with edges straight on no projection and the only
honest way to ask it is to put the point back on the sphere. The seam's own
test is the same shape, which is what makes the two sides comparable at all.
Geohash keeps its fast path underneath.

`cells.ApplicableSystems` is the world's own answer -- geohash always, S2 when
the world declares a sphere and a flattening that inverts, the same two
questions in the same order the navigator asks them -- and `applyGrid` spends
it: a named system the world does not divide is refused, logged at warn, and
the record stays exactly where it was. Opening a grid and cycling are moves
rather than destinations and land on geohash, which divides anything, so no
world can refuse them and the wave that gives cycling a second stop inherits
the same validation.

The dock's "filtered" chip reads the held cell rather than the address, for
the same reason: a page that says it is filtered while showing everything is
this disagreement in words instead of numbers.

Held by a table at both ends -- what a request may put in the record, and what
the record then narrows -- on a sphere, on a plane, and on a world that
declares a sphere it cannot flatten.

* golden: the Go S2 side answers to the vectors the TypeScript does

The geohash copy has always been held to cell extents the reference
implementation actually drew, read out of the committed tours. S2 has no such
recording -- the parity tour never cycles to it -- so the second system needed
the other oracle: golden/analysis/vectors, the hand-derived goldens the
analysis lane is judged against, which are the Go library's own test values
(leaf 0x47a1cbd595522b39 and its ancestors) read through a declared
flattening. Language-neutral JSON, so a Go reader and a TypeScript reader see
the same file and the same rules.

Two tests, no new fixtures. The vector walk loads six families and dispatches
every case the server has an answer for: 78 of them, including all twelve S2
containment cases, the ancestor chain from the face down to the telescope's
floor, `locate`, the address arithmetic around it, and the applicable-systems
answer on all nine recorded grounds. The 72 it cannot answer are named in the
log rather than passed over in silence -- plans, rings, pole closures and
style tokens are the seam's and the server has no use for them -- and a floor
under the answered count means a dispatch that quietly stopped recognizing a
call fails instead of passing with nothing to say. Every recorded ground's
surface extent and system list are checked too, which is the same ladder the
filter stands on.

The second test holds the edges a recording never contains, because a tour
walks a map rather than trying to break one: pins exactly on a geohash
boundary and a hair past it, the corner where four quarters meet and all four
hold it, an S2 cell on the Mars fixture's real flattening read out of the
bundle rather than copied into the file, the two ends of a raster that are one
meridian on the body, and a misspelled token -- which must hold nothing,
because the library's zero cell id spans the whole curve and an unguarded
containment on it would quietly stop narrowing anything.

The recorded geohash extents are untouched: this commit adds two files and
edits none.

* render: a feature made of several pieces is drawn as several pieces

The chart was building one `Polygon` per area out of a flat list of every
ring the feature owned, and one `LineString` per path out of the first run of
it. Both throw the same thing away, and both do it silently.

A polygon reads its first ring as the exterior and every ring after it as a
hole, so a district of two separate pieces of ground drew its second piece as
a hole punched out of its first. The city's `RS` zoning is fifty-three
pieces: fifty-two of them were being subtracted from whichever one came
first, and the twenty-five real holes among them were scattered across the
wrong parts on the way. A line string holds one run, so the Deschutes River
Trail -- a hundred and forty-seven runs of it -- drew one and dropped the
other hundred and forty-six on the floor.

The model never lost the structure. `world/model.ts` keeps `lines[i]` as a
part and `holes[i]` as *that* part's own interior rings, which is exactly the
nesting a `MultiPolygon` wants, so the whole of the fix is to stop flattening
it on the way out: `shapeGeometry` builds a `MultiPolygon` from a per-part
ring list and a `MultiLineString` from every run, and the drawn geometry now
says what the payload said.

The scrim had the same flattening and the same reason to stop. Its rings have
to alternate direction with their depth -- the world, then each piece of the
highlighted shape as a hole, then any hole of that piece's own back to solid
-- and depth is a ring's place *within its own part*, which a flat list
cannot spell. Flattened, the exteriors happened to come out right and the
interior rings were not there at all, so a hole inside a highlighted district
was undimmed along with the district. It is per-part now, which is what the
golden reference's `orientRing(ring, index > 0)` always meant.

Nothing else moves, and the parity run says so. The camera never read the
drawn geometry: `shapeExtent` and `centreOf` are computed from the model's
own outer rings, so a jump has always fitted the union of every part and a
title has always anchored at the middle of it -- and even a fit that had read
the flattened polygon would have got the same extent, because every ring was
present in it. The containment test behind the highlight filter lives in
`world/visibility.ts` and was already per-part with its holes attached to the
right part. The defect was confined to what was painted and what could be
clicked, which is why no baseline field could see it.

The seam grows a test file for this. Two synthetic pieces of ground with a
hole in each hold the structure -- two polygons of two rings, not one of four
-- and then the city's own features hold the counts: fifty-three parts ring
for ring, a hundred and forty-seven runs vertex for vertex, and every shape
of every fixture volume against the part structure its payload declares. The
scrim's windings are checked as a relation rather than a convention: an
exterior wound against the world rectangle, an interior wound with it. Six of
the ten fail without this change.

* app: a keyboard that says whose keystroke it is

Four shortcuts stood in the shell and none of them said anything but the name
of a key. Typing "g" into the search field put a grid up mid-word; a space in
the grid input flipped the subgrid instead of doing nothing, which is what a
space does in a hash; and because no filter said the key had been answered,
macOS sounded its rejection tone at every press of every shortcut on the page.
Five more shortcuts the reference has were not here at all.

Every filter now says three things. Whose keystroke it is, through the
editable-target guard the reference spells as isEditableTarget, written once
as "kbd-editable" and pasted into each trigger -- a select counts, because a
select answers the space bar itself, and something that is not an Element at
all is nobody's typing, which is how a key dispatched straight at the window
gets through. That the key is answered, through htmx's own `prevent`, which
runs only when the filter passed: a shortcut suppresses the keystroke it acts
on and nothing else, and reload is left alone because the session is restored
on the way back. And that the key was pressed rather than held, because a key
held down on a route that writes a file is sixty writes a second.

⌘G is deliberately above the guard, as it is in the reference: switching cell
systems is most wanted from inside the token field. Space and Escape are held
to a grid actually being open, and the subgrid toggle no longer answers its
own space bar twice.

The four new ones. ⌘⌥B folds the panel beside the map, by event.code because
Option rewrites event.key on a Mac -- and as two spans rather than one, each
asking the fold button on screen which way it is pointing, because folding the
dock swaps the dock and not the shell, so a direction rendered here would be
right once and stale for the rest of the session. ⌘B refuses altKey so the two
B chords can never both answer. Space reaches the subgrid from inside the grid
field, where the window's guard shuts it out by design. And Escape now closes
the card as well as telescoping the grid out.

ESCAPE MEANS TWO THINGS AT ONCE AND BOTH HAPPEN, which is what the reference
did with one handler and two calls. Here it is two triggers -- one on the map,
one on the window -- so it is two requests, and hx-sync is what keeps that
from being a race: both queue on the shell, so the card closes and then the
grid ascends against a record the first answer has already written.

The test reads the filters back out of the page. String assertions on markup
are usually a smell; here they are the point, because a trigger filter is
executable code the server only ever writes and never runs.

* render: the half of the keyboard that is not a route

Z was the seam's whole keyboard and it was thirteen lines of it. The key was
not swallowed, so the machine complained about every hold; ⌘Z raised the
labels; an autorepeat said sixty times what the first press had already said;
a window that lost focus mid-hold left the names up for good; and the two
window listeners were added again on every reconnect and never removed.
Nothing else was there: no ⌘K, no backquote, no +/- over the map, no way back
out of the grid field, and the webview's own menu still offered Inspect
Element over a map.

keys.ts is the rest of it, and the line it stands on is the same one the
shell's shortcuts stand on from the other side: nothing here writes a session,
and nothing here would survive being posted. Z held. ⌘K into the search field.
The backquote flipping panes where a sphere is offered, from the grid input as
well, since no hash will ever spell one. +/- stepping the chart, swallowed but
inert behind the sphere, where only the buttons change a distance. Escape
handing the keyboard back from the field to the map -- and the stopPropagation
that does it is load-bearing, because it is what lets the first press leave
the field and the second one telescope the grid out.

ONE LISTENER PER EVENT, ON THE WINDOW, IN THE CAPTURE PHASE. One, because a
listener per control is a listener re-added every time a swap carries its
control across, which is the bug that made one press of the globe toggle flip
the panes four times. On the window, because every control this reads belongs
to the application and is replaced by swaps, so all of them are resolved at
the moment a key is pressed. In the capture phase, because the field's Escape
has to be able to stop the window's own ascend from hearing it, and a window
listener that waits for the bubble is too late to stop anything. All four are
registered against one AbortController, so a viewport that leaves the page
takes its keyboard with it.

The viewport keeps the two things only it can do -- the labels flag both panes
read and the sentence in the corner that says so, and one step of zoom on
whichever pane is up, which the buttons beside the map now go through too.

Owed and named in the file: G still has to hand the keyboard to the grid field
on the way in and take it back on the way out, so "gm6" arrives at m6. That
half follows the answer to the G key's post rather than the keystroke, so it
belongs to the swap and not to this ladder.

* render: what the two panes give back when they leave the page

An element's lifetime is not the page's, and neither pane was saying so. A
morph swap replaces the viewport on every volume navigation, and what the two
panes held that the browser does not hold for them survived it.

The sphere had no disconnect at all. Everything a build minted outlived the
element that asked for it: a WebGL context, the animation frame that keeps
asking for the next one forever, globe.gl's controls listener and the corner
locator's callback -- both closures over an element nobody can see, still
drawing a planet out of a context nobody owns. It has one now. The controls
handler is held rather than written inline, because an inline one is
unremovable, and it comes off first; the corner is told nothing further; the
names and the grid are released, because every card in them is a canvas and a
texture this element minted; the skin's texture goes with them; and
globe.gl's own destructor pauses the loop and disposes the renderer, the
controls and the scene.

What is *not* given back is the subtler half. A pin's material is its
collection's, shared with every pin wearing the same mark and cached across
worlds, and its geometry is three's own quad shared with every sprite in the
scene -- so the pins are cleared and not released, and the three groups come
out of the scene *before* the destructor empties it, because globe.gl
deallocates whatever it finds there and what it would find is not its to free.

And the element stays re-connectable, because a morph may put the same one
back. Every memo that guards a rebuild goes back to what a freshly
constructed element carries -- the lens key above all, which would otherwise
have let openLens skip compositing a skin that no longer exists and brought
the sphere up black. The world it was showing is deliberately kept: the
context is what the next entry builds from.

The chart's disconnect put its map's target down and left its ResizeObserver
running. An observer is held by the browser and not by the element, so one
left going outlives the pane it was watching and keeps its callback -- and
the map, the locator and the world context that callback closes over -- alive
for as long as the page is open. It is disconnected now, along with the
settle timer, whose four hundred milliseconds could otherwise report a camera
for a world the reader had already left. Coming back rewires both and hands
the map its element again, and only on a *re*-connect: on the first one there
is no map yet and the observer arrives with the world, from show().

Parity is untouched by construction and the tour is unmoved. Nothing was
added to a draw path: on a page where neither element is ever disconnected
the whole change is one property write in build() and a connectedCallback
that returns before doing anything. The seam the harness reads is reset only
by a teardown, and a teardown means the element is gone -- __atlasGlobe and
the snapshot both resolve the *live* viewport, whose globe is freshly
constructed and reports the same empty seam either way. parity-compare is
green as far as it can judge here: it needs a built static tree and a
Playwright Chromium, and says so rather than judging without them.

The tests drive both panes through their callbacks over stubs -- no WebGL, no
OpenLayers target, no fixtures: one camera report per move and not one per
life the element has had, one live observer and never two, the renderer
disposed once whatever the page does, the collection marks and the shared
sprite quad untouched, and every memo back where a fresh element would have
it.

* app: the telescope answers to the system holding it

Escape stepped out of a cell by taking a character off the end of its
address, which is what a geohash is and is not what an S2 token is: the
level above 47a1cb is 47a1cc, the same length and spelled differently,
and shortening it landed the reader in a cell nobody had asked for. One
level out is the address's own system's business now, and only its own
system's.

The navigator's field is the other half of the same sentence. It used to
post whatever was typed straight into the record, so a session could hold
"M6X!" while the seam would have held "m6x" and the two halves of the page
divided different ground. The server normalizes what the field used to
normalize: the characters a system spells addresses with, lowercased, cut
at the depth its telescope stops at. Geohash travels on every keystroke,
because every prefix of a hash is a hash; an S2 token is whole or it is a
draft, and a draft leaves the reader exactly where they were.

And the second system is reachable at last. The button beside the field
cycles to the next system this world divides by -- which on a game map is
no system at all, so the button is not on the page -- and it carries the
held place across: the cell of the new hierarchy over the old cell's
centre, at the depth whose cells cover closest to the same ground on a log
scale. The addresses cannot survive the crossing, since the two
hierarchies share no boundaries; the ground under the reader can, and the
view is left alone because the carried cell covers roughly what was
already on screen.

The crossing is the analysis lane's `equivalentCell`, held to the recorded
carry vectors in both directions -- the tour never cycles, so those five
vectors are the only oracle there is. Measuring it needed one thing the
server did not have: an S2 cell's rectangle as it is drawn, which is an
edge walk through the world's own flattening rather than an area in
steradians, because a spherical measure and a halved rectangle cannot be
compared at all.

Opening the grid divides the cell again, the way it did before the
rewrite; picking a cell while the grid is closed opens it and does not,
because the reader went somewhere rather than starting over.

* golden: the carried place binds the server too

The five recorded equivalentCell pairs judged the seam alone; now that
cycling moves the held cell through cells.Equivalent, the same fixture
binds both implementations. The transcription in grid_test.go stays as
the fast unit-level statement; this is the one that reads the file.

* app: a second way in, for the other thing a surface can be pointed at

A reader telescopes through the grid three ways and they are one interaction:
typing an address into the navigator's field, clicking a cell the chart drew,
and pressing a point on the sphere. Only the first had a way in. The field is
a control this application renders and posts for itself; the other two are
pixels, and a pixel is the seam's to resolve -- so what it resolved to had
nowhere to land, and a grid could be drawn and never descended from the map
itself.

This is the pick's arrangement exactly, and for the same reason: the
application renders the form and the field it wants filled, the seam fills it
in and raises the event, the runtime posts it and applies the answer. A
request the seam made for itself would throw that answer away, and the answer
is the whole scene a new cell implies -- the navigator's field, the dock's
narrowed list, the island the panes redraw from.

It posts one field, and the two absences are the point. Which system is
dividing the map is already in the record -- the address was resolved *by*
that system -- so a seam posting one alongside would be the seam holding an
opinion about state it does not own, and a stale one the first time ⌘G moved
it. And the volume is inherited from the shell, as everything on this page is.

The route and the field name are the navigator's own, so one address is
normalized and validated once for all three ways in, at the end that owns the
record.

The test reads the form back out of the page and then walks it: the two
attributes that make it hypermedia rather than markup, the one field, the two
that must not be there, and the record the post writes.

* render: a click on a grid is a click on the grid

Both surfaces resolved a pointer into a feature and nothing else, so a reader
with a grid up could see the cells, could see their addresses, and could reach
them only by typing. The reference resolved a cell first and this now does.

THE ORDER IS THE WHOLE OF IT. While the grid is up a click is a request to go
one level in, so the grid's own two layers are asked first, and a cell that
answered returns without the pins standing on it being consulted at all.
Reversed -- pins first, cells after -- the grid would be undescendable
anywhere a marker happened to be, which is everywhere worth descending to.

And only a neighbour or a child is an answer. The cell the reader is already
inside is drawn over its own children, as an outline while the subdivision is
up and as the whole of the grid once the telescope has bottomed out, so a plan
that answered for scope or leaf would hand back the address already held on
every click. A neighbour is a step sideways and a child is a step in; those
are the two moves a click on a grid can mean.

The tolerances differ and say different things. A marker is a point and keeps
its four pixels of slack. A cell is a region the reader is pointing into and
gets one, because slack around a boundary is a boundary that cannot be told
from the cell beyond it.

WHO FITS THE CAMERA, and this is the one decision worth reading twice. The
reference fitted inside selectGridCell because the cell moved synchronously;
here a cell moves by a round trip, and the fit already lives at the arrival --
drawGrid flies to the new extent whenever the held cell changes, at the
reference's own padding, ceiling and duration. So the canvas posts and does
not fit. Every way into a cell -- the field, the back button, Escape, a click,
a press on the sphere -- lands on that one fit, which is the fit the baselines
pinned; a second one fired off at the click would be the same arithmetic said
twice, racing its own confirmation and answering for a cell the server had not
yet agreed to.

What the seam does owe at the click is the half the fit cannot do for itself.
Descending is the reader saying where they want to be, which spends the view a
panel's jump was holding -- and the swap that brings the new cell may bring a
closed card with it, whose closing would hand that view back over the top of
the cell being flown to. The hold is released the moment the cell is seen to
have moved, before the selection is read, which is the reference's own order
of releasing and then closing.

On the sphere it is pure geometry, because the sphere has no drawn surface to
hit-test: the point pressed is flattened back onto the picture and the system
is asked what lies below the held cell there -- descendTarget, which is what
typing one more character into the field means, so the two roads meet at the
same address and post the same form. An empty answer is the floor of the
telescope rather than the root, and the report refuses it. With no grid up a
press is a pick, exactly as it was.

The hover is left alone on purpose: it draws a marker lifting under the
pointer and a cell has no such reading, so it asks the feature question only.

The tests stand a stub of OpenLayers' hit-test contract under both panes --
walk the allowed stack in z-order, stop at the first answer -- and check what
the seam does with it: the order, the roles refused, which layers are asked
with which tolerance, and that a click that resolved to one thing never posts
the other.

* render: the key that opens the grid hands the keyboard to it

The other half of G, owed since the keyboard landed and named in the file so
it would not be forgotten. The reference hands the keyboard on when the grid
opens and takes it back when it closes: the field that takes a hash is focused
and selected on the way in -- so "gm6" arrives at m6, the rest of the word
being typed landing where a hash is typed -- and the map is focused on the way
out, where every shortcut that is not a route is heard.

Both halves are focus, so both are this lane's. Neither could be done from the
keydown, which is why they waited: what they follow is the *answer* to the
key's post, and until the swap has settled the navigator on screen is still
the one from before. So the press arms a listener for one settle and then asks
the page which way it went. Nothing here remembers what the grid was, and
nothing here is swallowed -- the key itself is the application's, and the route
the shell declares against this very filter does the moving.

One settle, and any settle. `once` is what keeps a press from leaving a
listener behind; the listener does not check whose swap it heard, because a
request already in flight can settle first, and that is a keystroke landing a
beat early rather than a keystroke landing somewhere wrong -- the navigator is
asked either way. It is armed against the same signal as the rest of the
keyboard, so a seam that leaves the page leaves no armed press behind.

The guard is the ladder's own: a reader typing a g into the search field or
into the grid's own field arms nothing, because both are answered above, and
⌘G is the system cycle and not this.

The harness now honours `once`, because it is the thing under test.

* render: a system this world refuses is not a system to draw with

Two guards on the seam's edge and one cosmetic on the field, all three about
the same fact: what divides a world is the ground's answer, not the session's.

THE FALLBACK WAS THE BUG. A system named in a record that this ground does not
offer was looked up in the registry and handed back anyway, which is the one
answer that cannot be given: the systems are exact rather than approximate,
and S2 asked about a ground with no invertible flattening throws outright --
appliesTo is the question a caller was supposed to ask first. So the offered
set is now the whole of the answer, and a name outside it draws no grid and
says so once. The server refuses such a value too, and that is not a reason to
trust it here: a record written by an older build, or edited by hand, arrives
at this lane all the same, and the pane that would throw is this one.

So a repaint is fenced as well. Everything in refresh asks a cell system a
question, and a refusal that got past the first guard used to arrive in the
middle of one -- taking the chart, the sphere and the footer with it. It now
costs one repaint and one line on the stream. A reader whose grid is wrong can
still read their world; a reader whose repaint threw has a page that answers
nothing at all.

And the navigator's field shows only what the reader's system could keep of
what they typed. The server stays authoritative -- what an address is, and
whether the text has become one, is decided where the record is written --
this is display assist, so a capital or a stray character does not sit in the
box looking like part of an address. It listens in the capture phase, so the
value is normalized before the route bound to the same field reads it: one
keystroke, one posted value, and the two halves of the page agreeing about
what was typed. It never stops the event, and it rewrites nothing when there
is nothing to rewrite -- which is every keystroke that was already an address,
and which is the path the recorded tours take. The caret is counted rather
than kept: where the surviving half of what stood before it ends is where it
belongs, which holds however many characters were dropped and wherever they
were.

The field is wired through the same WeakSet the other controls use, and
re-wired on every swap, because the navigator is a region and a swap replaces
it whole.

* golden: a pointer, a focus and a photograph

The tour could not have found three of the defects the review did, and the
reason was structural rather than careless. Nothing in it ever dispatched a
pointer, so a click on the canvas that went nowhere read as a step where
nothing happened. Every key it synthesized went to the window and none of them
asked where the focus ended up, so a shortcut that fires while the reader is
typing, and a shortcut that opens a field without putting the cursor in it,
were both invisible. And every observation was a count, a flag or a string, so
a sphere rendered black with the right number of sprites on it agreed with its
baseline field for field. Three step kinds close those.

Picks aim through the pane's own map and click with real pointer events, so
the whole path is the application's: OpenLayers' hit detection, the seam's
singleclick, the pick form the page renders, the route behind it and the swap
that comes back. Four of them, and one is a decision rather than a behaviour --
a click on nothing keeps the card, because a miss is not a deselection. Keys
record where the focus landed, and one of them is dispatched at a text field
rather than at the window, which is the only way to ask whether a shortcut can
hear a reader typing. Pictures are taken by the driver, because a WebGL sphere
has nothing to read back through a 2D context, and compared against a
committed PNG with a threshold that is measured in both directions: two
fresh-launch walks of one volume photograph the same frame exactly, and a
sixty-pixel patch recoloured fails. The reader and the verdict are sixty lines
of node:zlib rather than a dependency.

None of it is on yet, and that is deliberate. The gate asks the baseline
whether to walk the extended half rather than reading a flag, so the six
committed baselines go on being the tour they were captured from and this
changes nothing about a run today; the capture wave flips it by committing
baselines that hold the new steps, and there is no lever beside it afterwards.
capture.mjs grows the one mode here that runs against the rewrite, and it
appends rather than re-takes: the committed snapshots are copied through
untouched, the shared prefix is held to them first, and a red walk is refused.
Several new steps assert behaviour that has not landed -- Command-K, the focus
G leaves behind, the Escape that should leave a field before it leaves a level,
Command-G, a keystroke a text field should keep to itself, a click that should
telescope into a cell -- and SCHEMA.md's awaiting-fix table is what will tell
that wave the difference between new coverage and a regression.

Walked against this tree on three volumes: the picks and the pictures work
today, the awaiting-fix steps fail exactly where the table says they will, and
mars photographs a black planet with every count in its snapshot correct.

* fix: three defects the extended walk found, and none of them counted

The extended capture refused all six volumes and then, once the harness
stopped asking the wrong questions, found three real ones. Every one of
them was invisible to a tour made of counts, which is the argument for
the pointer, the focus reading and the photograph in the first place.

THE PLANET WAS BLACK. globe.gl hands out a sphere with no picture on it
and says so in the only way a Phong material can: it sets `color` to
black, and its own globeImageUrl clears that colour when a texture
finishes loading. This seam never calls globeImageUrl -- the skin is a
live canvas recomposited under the camera, and routing four megapixels
through a data URL on every camera move is not a way to draw a map -- so
nothing ever cleared the tint, and a Phong material multiplies its map by
its colour. The skin was composited correctly, the log said "the sphere
has its skin", the canvas held Mars, every count and sprite was right,
and the planet under them was black. `wearSkin` is the two lines and the
argument, and a unit test states the multiplication rather than trusting
it.

THE KEY THAT OPENS THE GRID FOLLOWED THE WRONG SWAP. G arms a listener
for the answer to its own post, because the focus can only move once the
navigator on screen is the new one. It took the first settle of any kind,
on the argument that a request already in flight only lands the keystroke
a beat early. It does not: the page settles searches, camera reports and
its own session island without the navigator moving at all, and a press
spent on one of those asked a navigator that had not been swapped yet,
read it as shut, and put the keyboard back on the map. So G opened the
grid and left the reader typing into the search box. It was a race, so it
was intermittent. The press now follows the one region its route renders.

THE GRID FIELD REMEMBERED A HASH THE SERVER HAD ALREADY TAKEN AWAY.
`changed` refuses an event carrying the value the last one carried, which
is right for a field nobody else writes to. Every swap re-renders this
one, so ascending out of "m" left an empty field beside a remembered "m",
and a reader typing that hash again was answered with silence -- which is
also why the tour's own second "m" never arrived, and why ⌘G had no cell
to carry. The event underneath is `input`, which a browser raises only
when the value moved, so the modifier was buying nothing. The reference
had no such memory either: one input listener, one address, every time.

* golden: three questions the tour was asking a beat too early, or of the wrong page

The extended steps were written against fixes that were still landing, and
three of them turned out to be asking something the build could not
answer no matter how right it was. None of these is a recorded value
moving: the tour drives the same keys in the same order and the six
baselines' shared prefix is untouched.

THE FOCUS WAS READ BEFORE THE STEP HAPPENED. `record(name, extra)` took
its extra as a value, so `focusRecord()` was evaluated at the call --
before the request the key had just raised was even made. That is fine
for ⌘K and for Escape, which move the focus inside the keydown, and it
can never see a shortcut whose focus arrives with the answer. `key-grid-
open` therefore reported the focus the reader had *before* pressing G, on
every volume, whatever the seam did. A thunk is read after the settle
now; a pick's aim is still a value, because a pick's aim is a
pre-observation by construction.

THE FIELD'S ESCAPE WAS RAISED AT THE WINDOW. The rule under test is a
control answering its own key so that one press leaves the field and the
next leaves the level -- and a control hears the keys that pass through
it, which a key raised at the window never does. The reference binds that
listener to the field; its window ladder asks only whose target a key
was; and `globe-grid-closed` is captured with the grid *shut* by two
window Escapes taken while that same field held the focus. Asking for the
field's behaviour from the window was asking two halves of one baseline
to disagree, and the half that would have had to give was the recorded
one.

⌘G WAS ASKED TO CYCLE ON A WORLD WITH NOWHERE TO CYCLE TO. Which systems
divide a world is the world's answer: geohash divides anything, S2 wants
a sphere with an invertible flattening. A plane offers one system, and
cycling it correctly does nothing -- so five of the six volumes were
failing a step for behaving. The page says which case it is in, on the
button the navigator hides when a cycle leads nowhere, and that is what
is asked now. The step is still recorded everywhere: an unmoved system on
a plane is a fact worth pinning, and a build that starts cycling there
shows up as a changed field.

* golden: the extended half, captured, and the pictures that come with it

The wave of SCHEMA.md §6.1, run against this branch. Six volumes, each from
its own `atlas serve` over its own session directory and its own browser
session, pinned to the link farm FIXTURES.json names -- the five installed
builds and the city out of dist/bundles, verified by sha256 before the walk.

    make static
    node golden/parity/capture.mjs --capture-extended

    tunic               70 steps  (33 appended, 37 carried)  6 pictures
    cyberpunk-2077      82 steps  (33 appended, 49 carried)  6 pictures
    fallout-new-vegas   72 steps  (33 appended, 39 carried)  6 pictures
    zelda-totk         102 steps  (35 appended, 67 carried)  7 pictures
    mars                95 steps  (36 appended, 59 carried)  8 pictures
    bend-or            103 steps  (37 appended, 66 carried)  7 pictures

NOTHING ALREADY CAPTURED WAS RE-TAKEN. The committed snapshots are copied
through byte for byte and the new steps appended after them; the shared
prefix was diffed against them first, under the standing waivers, and a
volume that differed would have been refused rather than recorded. It was:
the first run of this wave refused all six, which is the mode working.

EVERY AWAITING-FIX STEP IS GREEN, and green for a reason rather than by
vacancy. ⌘K reaches the search field and offers what is in it. G opens the
grid and hands the keyboard to the field it opened. The first Escape leaves
the field with the cell still held; the second ascends it. A `g` typed into
the search box is a letter, not a shortcut. A click inside a drawn cell
telescopes into the cell and opens no card. And ⌘G cycles the system where
there is a second one to cycle to -- on mars it lands on S2 holding cell
"24", carried from the geohash "m" the reader was standing in -- and
correctly does nothing on the five planes, which is now recorded rather
than complained about.

THE TWO PICTURES ARE WHY THIS WAVE EXISTS. mars's `screen-globe` was
photographed as a black sphere with its pins on it while this was being
written; it is a lit rust planet now, and the fix is in the commit before
this one. The city's `screen-ground` draws its districts in every part they
have. Both were taken twice from fresh launches and came back byte for byte
identical, which is the same stability the threshold in pixels.mjs was
measured against.

The pictures are committed with the baselines that name them, in one commit,
because a baseline naming a picture that is not there is a broken gate. Both
ignore rules that kept them out are gone, and both files say so where the
rule used to be; .gitattributes marks them binary for the reason every other
fixture is marked that way.

* docs: production files speak in domain terms, and history keeps to its own places

The comment policy of issue #5 §8 says the old tree's prose stays readable on
the golden-reference tag and that no shipped file cites it as documentation.
This is that sweep, run over the files that were still telling the story
rather than stating the rule. Comments and one package description; no code,
no behaviour, and nothing under golden/fixtures or docs/decisions, which are
where history is supposed to live.

WHAT THE RULE IS, said once so the next sweep does not have to work it out:
a production file speaks in domain terms about what it does now. Where a
sentence was doing two jobs -- naming an ancestor *and* stating a contract --
the contract stays and the ancestry goes. So the operation runner keeps its
four safety properties and stops attributing them; the collator keeps both of
its orders and now says what makes them two (a catalogue of collections, and
a list of places a producer numbered) rather than which file used which
comparator; the merge ledger's wording is still frozen letter for letter, and
the reason is now the one that will still be true in five years -- those
strings are data in every bundle already published; the tile stamp still
explains why a test may substitute a tool hash, without naming whose.

WHAT WAS LEFT, on purpose. A comment that names old behaviour because the old
behaviour *is* the specification is not archaeology: "byte-compatible with",
"these are the key names", "the order of these questions is". Deleting the
name there would leave a rule with no authority behind it. The line is
whether a reader needs the name to know what is required of them.

The stale documents went with it, since they were stale in the same
direction:

  - app.md's region table said innerMorph for six of eleven regions and was
    missing the island entirely. Nearly every region is an outer morph, which
    is the reading that matches what the templates render and the only swap
    that can set an attribute on a region's own container -- three states are
    spelled exactly that way. Its §7.1 still described the detail card as
    patched by an `:empty` rule that the card's own `hidden` retired.
  - render-seam.md said atlas:pick had no consumer and that canvas selection
    was inert. It has one: the shell renders a hidden form triggered by the
    event, on the window, non-bubbling, and a miss deliberately raises
    nothing. §10 no longer lists picks and the detail card as unverified,
    because the tour drives a real pointer at a real pixel now.
  - README.md's post-parity list was written when the tour compared only
    counts. Three of its four entries are answered; what is left is stated
    honestly -- CI still does not walk the tour, by decision, and the seam is
    865 lines past its guideline rather than 358. One rendering difference is
    named as genuinely open: the marker raster.

* golden: the third counter that measures the route rather than the destination

The first full run of the gate over the extended half came back five volumes
identical and mars differing in exactly one field, at every step from
`pick-grid-closed` onward: `tileStats.peakPending`, 12 against 10.

It is the high-water mark of tile requests in flight at once, and that makes
it a measure of overlap rather than of anything a walk arrives at. Overlap is
the one thing this file's own rule -- fix the instability, never ignore it --
cannot reach: a settle wait says that the page is quiet now, and says nothing
about how many requests were in the air together a second ago. Three walks of
mars against one build read 12, 16 and 2 at the same step.

It held still for the steps the reference captured because those steps *step*
the camera. The extended half flies it -- two zoom-outs to find an aim, a
zoom-in to find a gap between eight thousand features, a telescope into a cell
-- which is where the requests start overlapping differently from run to run.
So it joins `requested` and `loaded`, which were declared advisory for exactly
this reason and are the two counters beside it in the same object. All three
stay recorded: a candidate that fetches four times as many tiles is still
worth seeing. `tileStats.errors` still binds, because a tile that failed to
load is a fact about the build rather than about the run.

Nothing else in the extended half moved between runs, on any volume.

The harness's status lines go with it: the awaiting-fix table is spent, the
gate walks all three new step kinds on every volume with no lever beside it,
and SCHEMA's reproduction table now counts the whole walk and the pictures.

* golden: the schema stops describing a capture that has not happened yet

Status prose, brought level with the tree. The extended half was written in
the future tense -- steps awaiting a fix, pictures not captured yet, a gate
that would start walking them when a wave ran. The wave has run, so the
document says what is true now and keeps what the exercise found.

  - §2.1 no longer says the baselines hold no pick step. They hold them, so
    every run of the gate walks all three kinds and compares every picture,
    with no lever beside it.
  - §2.1.1 and §2.1.2's "today" columns read passes throughout. Two rows now
    say where their key is raised and why it has to be there: a control only
    hears the keys that pass through it, so the two steps whose subject is a
    control answering its own key are raised at the control.
  - §2.1.4 is kept and marked spent. It is the record of what the wave was
    for, and its last two rows are the argument for the whole exercise -- so
    what actually happened to them is written into it, including the part that
    is uncomfortable: the black planet's `distinct` floor read 519, high
    enough to look healthy, because a black sphere covered in pins is hundreds
    of colours. The floor did exactly what §2.1.3 promises and no more. The
    committed twin and a person looking at the picture are what caught it.
  - §6.1 records that the wave refused all six volumes on its first attempt
    and why that was the mode working: two of the three red steps were the
    tour asking the wrong question and one was a real race in the seam. A mode
    that baselined the first walk would have made all of it golden.
  - §7's pixel gap is narrower and says what is still open: a picture is only
    as good as the frame it was taken in, and a picture taken from the seam
    cannot tell you the marker raster is not the reference's.

* render: the row under the pointer, marked again

Two controls in the legend were on the page, wired to their routes, and
impossible to reach with a mouse. The carried stylesheet reveals the isolate
crosshair (assets/css/layer-header.css) and the label-policy toggle
(assets/css/category-rows.css) only under `.category-row.is-hovered` /
`.layer-header.is-hovered`, and nothing in this tree had ever written that
class -- `grep -r is-hovered` found the two rules and no writer. Both sat at
`opacity: 0` for the whole life of the page. Every test passed, because every
test reads the buttons' attributes and every attribute was right.

The mark is restored as the reference wrote it, and deliberately not as
`:hover`. The CSS comment names `markHoveredRow` for a reason: a list scrolls
under a still cursor -- the wheel, a swap that shortened a section -- and the
browser does not reliably revise `:hover` when the ground moves under a
pointer that did not, so a row kept offering its actions to a pointer that had
left it. Asking where the pointer actually is answers both cases, which is why
the answer is recomputed on scroll as well as on movement.

  - `render/hover.ts` is the tracker, a module beside `keys.ts` rather than
    more of the viewport: it is the same shape of concern -- the application
    renders a control, this lane wires what is continuous about it -- and the
    viewport is already the longest file in the seam.
  - The container is a selector, not an element, for the reason the scene node
    is one: `#atlas-legend` is a morph region and every filtering move can
    replace the list whole. `rescan()` re-resolves it and the viewport calls
    that from the one after-swap hook it already has.
  - A morph can also hand the *same* container back, carrying marks on rows
    the pointer is no longer over. Every pass clears every mark before it
    writes one, and a rescan is a pass, so both kinds of swap have one answer.
  - The WeakSet is per tracker, which is the one place this differs from the
    viewport's module-level `wired`. There a listener is never taken off, so
    the fact has to outlive the element; here the listeners are registered
    against the element's own AbortController and leave with it, and a
    container remembered past its tracker's life would be a legend nobody is
    listening to.

The tests stub the hit test, because `elementFromPoint` is a layout question
and there is no layout in Node. That is the reason the fix was also driven in
a browser: pointer over a row, and the crosshair and the tag are there.

* app: the label toggle wears the mark of what it is asking

The template rendered one glyph for both branches of a control that asks two
different questions. An area draws its names on the ground and the toggle is
whether they speak unasked, which the reference drew as a bar over a stem
(frontend/src/legend.js). A point collection curated as text has no marker to
fall back on -- its names are the whole of how it draws -- and the reference
drew a luggage tag. Rendered with the area's mark, the legend told the reader
the two rows were offering the same thing.

The discriminator is Go's, because the question is: `labelToggle` already
decides *whether* a row wears the control, from the geometry and the curation,
and which mark it wears falls out of the same two cases. `LabelToggle.Glyph`
carries the answer as a word rather than a boolean, so a third kind would be a
third name instead of a nested `if` in a template that is supposed to hold no
policy at all.

Held by a test on hyrule, which is the volume that can say all three words in
one page: one area collection, two point collections curated as text, and 159
rows of plain pins that wear no button and keep the column open anyway -- the
half a missing element gets right by accident and a wrong element gets wrong
in silence.

* render: the mark is the symbol, and the outset is cut to its shape

A marker was a coloured bubble: a filled disc in the collection's colour, an
outset rim around the disc, and a small untinted glyph punched over the top of
it. Every collection therefore said the same thing at a glance -- "a coloured
bubble" -- and the one drawing that told a Shrine from a Landing Site was the
dark shape inside it. The reference draws the symbol itself: the colour is
carried by the glyph, and what keeps it legible over whatever art the world is
drawn on is a halo cut to the glyph's own shape.

  - render/chart/markers.ts is new, and it is the mark's one owner: the 64
    raster (the glyph inset by six, tinted through `source-in` when it is a
    glyph and left alone when it is a picture, stamped 37 times in a disc of
    radius √10 in the world's rim colour and drawn over the top of those
    stamps), the cache that keys it by asset, colour and rim rather than by
    collection, and the colour ladder that lifts or drops a colour which would
    otherwise vanish into the rim it is drawn against.
  - The chart draws that raster as a plain Icon at 31, or 36 for the pin whose
    card is open, and nothing else -- no disc, no second style. A raster is
    composed asynchronously, so a pin whose symbol has not arrived wears its
    collection's initials in the same colour and the same rim, which is also
    what a collection with no artwork wears for good. When one does arrive
    every cached mark is dropped, not that collection's: a raster stands in
    for every collection drawn that way, and the others cached their initials
    while it was loading.
  - The sphere consumes the same raster instead of tinting the raw artwork
    itself. It gained the halo by doing so, and the two panes can no longer
    disagree about what a collection looks like.
  - An icon URL encodes its path segments. An asset with a space in it was a
    404 on both panes, and both now ask the data plane for the URL rather than
    spelling it twice.

* app: the legend wears the mark, and the colour the map draws

A legend row wore two letters. Every row, in every volume: the icon cell
rendered a collection's initials and nothing else, so a sidebar that is meant
to be the key to the map read as a column of monograms beside pins wearing
artwork. The reference put the artwork there -- `--pin-icon` and the
`has-source-icon` class on the icon cell, initials as the text it falls back
to (frontend/src/theme.js, applyCategoryGlyph) -- and the stylesheet that draws
one from the other came over with the rest of the CSS and had been sitting
unused since. The port is server-side, which is where this rewrite decides
things: the row carries the asset's URL or it carries none, and the template
spells whichever arrived.

The second half is what the mark is coloured. `colorFor` hashes a feature's id,
and the legend was asking it about collections; the seam asks the collection
itself first and falls back to the palette by the collection's place in the
payload (render/chart/styles.ts, collectionColor). A curated volume declares a
colour, so the two ladders agreed and nobody noticed. An enriched one does not
-- a city's collections arrive from the national layers with a standard glyph
and no colour of their own -- and there the two fallbacks parted: a green
swatch in the sidebar beside a light-blue pin on the map, for the same
collection. `collectionColor` is the seam's ladder in Go, and the dock's result
rows and the pin card ask it too, because those dots stand for a collection as
much as the row does.

  - internal/app/world.go reads `color` and `iconColor` off a collection and
    keeps its `iconAsset`, none of which the application had been decoding.
  - internal/app/legend.go names the asset the way the seam names it --
    every segment through encodeURIComponent, separators left alone -- so one
    picture is one URL and one download rather than two.
  - The bend-or fixture is the case in the tests: Historic Resources, first in
    the payload, no colour of its own, wearing std--maki-monument.svg.

* render: the grid draped on the curve, and the ground judged where the camera lands

Four defects the reader met, and three of them turn out to be one habit: an
answer worked out at the moment a thing was built rather than at the moment it
is looked at.

THE SPHERE'S GRID WAS DRAWN THROUGH THE PLANET. A cell's ring is four corners,
and at the root of a plan two of them are forty-five degrees apart; joined by a
straight line that is a chord, and a chord across 45 degrees of a hundred-unit
sphere passes seven units inside it. So the fill was a sheet drawn through the
crust and the boundary went with it, and the bigger the cell the deeper it
sank -- which is why the dimmed neighbourhood was the part that "did not
render". It was rendering, underground. Rings are now subdivided by their own
span, every vertex of a fan lifted back to the surface, and the rows of
triangles counted by span as well. The boundaries are Line2: WebGL draws a
native line one pixel wide and ignores the width it was asked for, so the whole
role table -- 2.5 for a leaf, 1.8 for a bare scope, 1.4 for a child, 1.0 for a
neighbour -- came out as one hairline on the sphere while the chart drew it
correctly. Fill, boundary and chip now carry render orders 1, 2 and 3 over
tiles at 0, and the globe reads the ring as the system walked it rather than as
the chart cuts it: an antimeridian split is an answer about a flat sheet.

THE SUBGRID WAS JUDGED AT THE ZOOM THE CAMERA WAS LEAVING. Whether a cell can
carry its address decides whether a child draws at all, and it was answered
once, while the grid was being built, against the resolution of the ground the
reader was departing. Descending flies the camera in afterwards and nothing
asked again -- so a cell held two levels down planned thirty-two children,
counted them, and painted none. The plan cell now rides on the feature and the
style function answers, at the resolution OpenLayers hands it, measuring the
chip as it will actually be drawn. The chip is two cuts again, prefix at 500
and principal at 900, and every cell carries the plan's own z-order.

CYCLING THE SYSTEM IS NOT A DESCENT. The carry gives the held cell to whichever
cell of the next system covers the same ground, so "m" becomes "24" and the
string moves while the reader does not. Both panes read that as a descent and
flew the camera to ground it was already over. A cell moves within a system;
both memos are written whatever happened, so the descent after a cycle is still
read against the system it was made in.

AND THE SPHERE READ TWO LEVELS SHALLOWER THAN THE CHART. The base skin is one
4096-pixel canvas of the whole equirect window, which is level four exactly, so
a level-six tile composited into it was a 256-pixel square drawn 64 wide --
three quarters of the capture thrown away before anything was drawn. The
neighbourhood is draped as its own meshes now, at the pyramid's own radius on an
eight-segment grid, each tile wearing its own texture at its own pitch. The
level and the block are unchanged: the same round-and-one-deeper, the same
budget of 96 shrinking the reach, which is what the recorded 81 tiles of level
5 at globe-zoomed-deep are a reading of.

* render: the icon over the raster, and the sphere that knows when to go

Two things the reader found on the sphere, and both of them are about what a
pane is allowed to keep.

THE PINS WERE BURIED AT THE LIMB. A pin's sprite is screen-sized around a
point half a unit off the skin, so near the rim most of that square stands
over ground nearer the camera than its own anchor: tested against the depth
buffer, the planet swallowed the buried half, and Mars came up wearing a rim
of icons sunk to the waist. The rule is the plain one -- an icon is drawn on
top of the raster -- so the pins turn their depth test off, as the cards and
the chips already had, and the horizon `cull` was already applying to those
now holds the pins too.

WITHOUT MOVING THE COUNT. `visibleSprites` in six recorded tours is a count of
`sprite.visible`, and it means "what the legend left standing" -- 2048 on Mars
at every step, 95 inside a held cell, 1922 with a collection off. So the
horizon is spelled with layers instead: the camera draws layer 0 and nothing
else, and a pin past the rim steps off it without touching a boolean the
filters own. Opacity was closed to us (a material is shared by every pin of a
collection) and so was scale (it is what a selection speaks with). The render
order is stated in one place now rather than five, and the pins take a rung of
their own: over the tiles, the fills and the boundaries, under the cell
addresses and the names, which is the order the reference drew them in.

THE SPHERE OUTLIVED ITS WORLD. Switching the volume to a game map left the
planet turning over it, still wearing the last world's skin, with only the
hidden toggle to say the offer had been withdrawn -- the server's half was
right and nothing on this side acted on it. Which pane is up is seam state and
survives a swap on purpose; a world change is the one swap where surviving is
wrong. So a scene that declares no sphere puts the chart back, sets the toggle
down and retires the globe: the same teardown a disconnect does, plus the
context and the container, because globe.gl's destructor leaves its canvas
where it put it, holding the last frame it drew. No camera is carried back --
a camera is a place on a particular world.

AND ANOTHER SPHERE IS ANOTHER SKIN. The composite was keyed by the data root
and the pyramid's name, and a pyramid is a path inside a world: two worlds of
one volume can spell theirs the same and the second would have worn the
first's ground. The world is in the key now, and the skin is retargeted on the
world's own window of pixels rather than left to lay a new picture into the
last one's rectangle.

The pictures under golden/parity/screens/mars are twins of the defect: the
limb pins in `screen-globe` are buried in the committed frame and are not in
this one, so those two want a re-capture.

* render: the second pass the sphere no longer makes

The detail under the camera is draped as its own meshes now, and the half of
`globe/texture.ts` that used to composite it into the base skin went
unreachable with that change rather than being deleted by it: `detail`,
`clearDetail`, the tile map they kept, and the second key, painting flag and
generation counter that made the two passes independent of each other. The one
call left was `clearDetail` on a skin whose neighbourhood was already empty.

So the file says what it does again. One pass, one counter, one key -- and the
reason there is only one is now written where the ceiling is: 4096 pixels
across the whole equirect window is level four exactly, so a deeper tile
composited here loses three quarters of its capture before it is drawn. That
is the argument for the meshes, and it belongs in the file that would
otherwise look like the obvious place to put them.

The tests go with it. What they were the record of -- a pass that was
cancelled must leave the texture asking for its composite again rather than
claiming to hold it -- is a rule about *any* interruptible pass, and the two
tests that still say it are kept. The two that drove the neighbourhood, and
the one whose whole subject was the two passes not cancelling each other, are
gone with the thing they were about.

* render: the fit a reader asked for last is the one they get

Two Escapes in quick succession left the camera at the parent cell. The reader
ascended twice, the grid ascended twice, and the map stopped one level short --
holding ground they had already left.

OpenLayers walks concurrent animation series backwards every frame
(`View.updateAnimations_`), so the oldest is applied last and wins, and a
later ask is overwritten frame by frame until both finish. Two fits begun
inside one animation's 180 ms are exactly that. Cancelling first makes the fit
mean what a reader means by it: go where I have just asked for, whatever is
still in the air.

This was latent, and it took the cycle guard to show it. While ⌘G was read as
a descent it fired a third fit ahead of these two, and the accident of that
extra animation's timing was what let the last one land -- which is to say the
baselines were pinning the right camera for the wrong reason. mars'
`key-grid-closed` is the step that says so: it is the one volume with a second
cell system to cycle to, and the only step in six walks where the two fits
were close enough to race.

* golden: the pictures pin the mark the reference draws

The full tour, walked fresh per volume against the finished branch. Thirty-two
of the forty committed pictures moved and two volumes moved a field, and every
one of them is the marker or the legend's colour:

  - the mark is the symbol now, not a coloured bubble with a symbol punched
    over it, and it stands 31 pixels rather than 14. That is every
    `screen-chart`, `screen-labels-held`, `screen-raster-deep`,
    `screen-outside-bounds` and the two `screen-ground`s.
  - `screen-dock-open` moved on all six because the dock's result dots ask the
    seam's colour ladder now. On bend-or, whose collections arrive from the
    national layers declaring no colour, Historic Resources went from green to
    the light blue its pins have always been.
  - zelda's `pick-in-grid` resolves a feature where it used to resolve none,
    and bend-or's `pick-a-pin` lands on the neighbouring pin: a mark that grew
    that much has a hit footprint that grew with it. Both echo forward through
    the steps after them, which is the same fact recorded again.

These forty pictures were never recordings of the reference -- it had no
screenshot step -- and SCHEMA.md §7 wrote down what taking them from the seam
would cost: a picture cannot tell you it is not the reference's. The bill has
come in. Nothing the reference did pin moved; the first hundred-odd steps of
every walk are byte-identical.

HELD BACK: mars' `screen-globe` and `screen-globe-labels`. Pin sprites occlude
into the surface at the limb and the fix for it is landing separately, so
those two stay at their old values and go red until it does.

* app: the refresh key throws the record away and comes back

A stale session record cost an hour: five persisted highlights, culled at every
pin, with no row on the page to press and no field a reader could name as the
one at fault. The way out was to know where the file lives. ⌘R is what a reader
already reaches for when a page has gone wrong, it was unclaimed, and the
reference bound it -- above the editable guard, so it answers from inside a
field a reader is typing in -- so it is claimed here for the reset the record
has always needed.

IT IS BLUNT ON PURPOSE. POST /session/reset deletes volume.<slug>.json whole:
what is hidden, folded, unfolded, highlighted and labelled, the search, the
panel, the card, the grid, the index, the corner locator, every world's camera.
Nothing writes defaults. The arrangement a volume opens with is synthesized out
of the world's own curation the moment there is no record to read (arrange, and
docs/app.md §3.1), so forgetting the record *is* the reset, and the state a
reader comes back to is the state a first launch would have given them.

WHAT IS NOT TOUCHED is the last-volume pointer. The reader is not leaving the
volume; they are standing in it with nothing remembered about it, and taking
app.json as well would answer the next / with a library card.

The answer is a full round trip rather than a partial set, which makes it the
one concern in the table with no function beside it and no region list. After a
reset there is no record left to render a region out of, and a reader who
pressed the refresh key is owed the thing refreshing does -- so the route reads
the world out of the record before deleting it, and answers 204 with an
HX-Redirect back to /v/{volume}/{world}. A volume whose record named no world
falls back to its first; one with no world at all falls back to /, which is the
last-volume doorway. It is spelled as the header htmx acts on itself because a
303 would be followed by fetch and swapped into a page that asked for no swap,
which is a reset that appears to do nothing at all.

The store grew the one operation it was missing. SessionStore.Delete is
implemented by the file-backed store, the in-memory one and the golden's own,
and deleting a record nobody holds is success rather than ErrNoSession: the
caller wants the record gone, and a record that was never there is gone. That
is what makes pressing the key twice -- or on a volume nobody has arranged --
a quiet no-op instead of an error about a file the reader was right to want
deleted.

The shortcut's span carries `prevent` like every other, and it is the one place
`prevent` takes something away and hands it straight back: the browser's own
reload is swallowed, and the redirect is the reload, over a record that is no
longer there. No editable guard, exactly as the reference had it. The comment
in shell.tmpl that said ⌘R was deliberately unclaimed, and the test that held
the shortcuts block to it, now say the opposite and check it -- above the
guard, autorepeat guarded, keystroke answered.

Tests: the reset deletes the record and the pointer survives it; the answer
carries HX-Redirect and no partials; the page it comes back to reports no
hidden set, no highlight, a folded panel and a null camera, which is the chart
fitting the world again; a reset with nothing to reset writes nothing and
answers the same; and Delete's two halves -- the file gone, a missing file fine
-- are held at the oshost store and the in-memory one. docs/app.md gains the
route, the concern, the record note, the store's new line, and the keyboard
table the shortcuts never had.

No tour step presses ⌘R and no existing concern's region set moved, so parity
is unchanged: go test ./golden/... is green and the harness reports 11 of 11,
with parity-compare not judged for want of a built seam.

* render: the sphere re-learns its reporter, and mars is photographed standing

Retiring a sphere cuts the camera wire along with everything else, and
the wire was asserted only at connect time -- so the first retire left
every later globe silent and the corner locator centred on nothing. A
globe going up now re-learns where its camera reports.

The two held mars photographs land with it: the limb pins fully drawn
where the old pictures buried them, taken from the same walk that read
every reticle field back at its recorded value.

* app: one zone, asked for exclusively

Highlights accumulate. Every press of a zone adds ground, which is right
for the question "what do these two districts share" and wrong for the one
a reader asks far more often: show me R3 and nothing else. The way to that
was to clear the set by hand, or to reach for the collection's isolate
button -- which answers a different and heavier question, putting every
other collection away when all that was wanted was one zone out of one
index.

So the crosshair the collection rows and the section heads already wear now
sits on a zone row too, one row further in, asking the same thing of one
feature: `only=1` on the highlight concern names a feature and makes the
highlight set exactly that feature. Pressing it again on the zone that is
already alone gives every highlight back, which is the isolate button's own
toggle and means the control that set a filter is the control that lifts
it. The state is derived rather than stored, the way the isolate chip is:
a zone that is alone by any route -- one press of the control, or highlights
added and taken away one at a time -- wears the pressed mark, so the button
always says what pressing it would do. The ride-along is unchanged: an
exclusive highlight still brings its own collection out of hiding, because
asking to look at a piece of ground and keeping it put away cannot both be
meant.

THIS IS NOT PARITY, and is not claimed as any. The reference implementation
gave the only-button to collections and to sections alone, and its zone
highlights only ever accumulated (frontend/src/areas.js,
toggleZoneHighlight). This is a deliberate extension past the reference, on
a surface the recorded tour does not touch -- no baseline moves and none
was made to.

Three things the arrangement turns on, all of them written down where they
live. The control is a *sibling* of the row's own button rather than a
child, which is the whole of how one click cannot be the other: an event
travels to its ancestors and never to its siblings, so it reaches the
wrapper, which listens only for the contextmenu. The mark is spelled
`data-zone-only` and not `data-only-zone` because html/template strips a
`data-` prefix before it infers an attribute's context and reads what is
left beginning with `on` as an event handler -- the other spelling comes
out JS-quoted, and the two older marks in that file wear that quoting
today. And the row had to join the hover tracker's selector: the carried
stylesheet reveals `.only-button` under `.is-hovered`, which the seam
writes from where the pointer actually is, so a row outside the selector
would have shipped a control that was on the page, wired to its route, and
permanently invisible -- exactly the defect that module exists to have
fixed.

Gates: go build, go test ./... , depcheck and the harness are green, parity
included (parity-compare has no built seam in this tree, so it reports "not
judged" as it does on main).

* app: the filtered chip says what narrows, where the goldens let it

The chip's one word is a bound field, so the accounting lives in its
tooltip: hover "filtered" and it reads "2 hidden · 1 highlighted ·
cell 3qd held". A reader once lost every pin to five highlights nobody
could see; the cure for invisible narrowing is narrowing that names
itself.

* render: the sphere is told when its pane changes size

Folding the dock while the planet was up left the globe drawing at the
size it was entered at, until something else happened to poke it.
globe.gl measures once and has to be told, and nothing was telling it:
the dimensions were pushed at entry, out of the pane it was handed, and
never again. The chart has watched its own box since the lifecycle work;
the sphere now watches its own the same way, wired with the planet and
stopped with it, because a ResizeObserver belongs to the browser and one
left running outlives the pane it was watching.

Three things move with the box and only the first is globe.gl's. A Line2
carries its width in pixels, which it can only do by being told the
window it is measured against, so every grid boundary standing was
scaled to the pane as it was when the grid was drawn -- a resize that
did not refresh them mis-scales the whole hierarchy of line weights the
fat lines exist to keep. And the horizon cull is a question about the
camera, which has just been given a new aspect to ask it from.

A pane measured at zero is not a size to draw a planet at: behind the
chart the sphere has no box at all, and a globe told it is zero pixels
wide throws its dimensions away.

* app: the open card says what its point's place is called

The reference card carried a point's cell addresses and the clean room
lost them. They come back the way the footer's "N of M" sentence came
back, because it is the same division: where a point stands is in the
payload and the card writes it out of the session, and what that place
is *called* is the analysis lane's, which no server can reach. So the
card renders one row present, empty and hidden, and the seam fills it
after every swap. There is nothing left to keep in step -- the card is
an outerMorph region, so the swap takes the last answer away with the
node it was written on, and the rescan that follows writes this one.

Geohash is the row itself, at its own fixed depth rather than at
whatever depth the navigator's grid currently stands: the row says where
this point is, and a reader descending the grid did not ask for every
open card to be re-spelled behind them. Every other system this ground
offers adds a row of its own beneath it, out of the one question the
contract has about a point -- which is asked only of the systems that
said they would divide this ground, and fenced anyway, because a system
asked about ground it refused throws rather than shrugging.

The rows are inert text, as the reference's were. A shape has no point
to name and so has no row at all, which is the branch the reference hid
by hand; a card that has just stopped being a point's keeps none of the
rows this lane left on the one before it.

* render: the places a map writes rather than pins

A point collection curated `atlas.render.as: text` now draws its name and
nothing else on the projected map. No symbol, no initials, no marker of any
kind under the word — Zelda's Areas and Provinces are written across the
ground the way a map writes a region, which is what the curation meant by
text in the first place.

What it replaces is two drawings of one place. The reference, by its last
revision, drew a marker for every point collection and kept
`atlas.render.as` only as the default its label policy fell back to, so a
text collection wore its symbol AND its always-on name at once: "A —
Wellspring of Wisdom", the initials of the collection sitting beside the
name of the place. I checked that again before changing anything, and it is
what the reference does — `markerStyles` has no suppression in it, and a
text collection with no artwork at all (Cyberpunk's District and
Sub-District) drew literally two texts, the initials mark and the label.
So this is a deliberate departure, decided by the reader after parity, and
it is written into the module's own comment as one, the way ⌘R was.

The name is drawn as the reference's `.text-symbol` — weight 900 at 11px,
the collection's colour taken through the same legibility ladder the marker
climbs, rimmed with the world's outset. That rule was the key's swatch for a
text collection and has been dead CSS since the key stopped using it; the
form it described is the form the map now has. It stands centred on the
place rather than sixteen pixels below it, because sixteen pixels is the
room a marker takes and there is no marker. Pointing at it or opening its
card grows it a step, 11 to 13, which is the marker's own 13-to-15
convention said at this size, and the feature whose card is open is drawn
over everything, because this text is what a chosen marker would have been.

TWO THINGS PUT THE MARKER BACK, and they are one rule: nothing may be made
to vanish off a map while its box is still ticked and its place still
counted in the footer.

The first is the judgement this needed. A reader who silences a collection
by hand silences its names — and for a collection whose name is its only
form, that would erase the feature outright. So a silenced text collection
draws the ordinary marker instead. The override stops being a choice between
a name and no name and becomes a choice between the two forms a point
collection can take, which is exactly what the reference's own key called
that toggle: "a point collection's override is which shape it takes at all —
markers, or the floating names text categories used to be set apart for."

The second needs no argument: a feature with no title has no text to be
drawn as, and keeps its marker.

Everything else is untouched. A collection with artwork keeps its symbol,
its ladder and its sizes; the sphere keeps drawing markers for everything,
because this is a rule about the projected form; and the counts are the
model's, so the footer and the dock say what they said. The hit test needed
nothing either — it asks every layer rather than the pin layers, so a place
drawn only in the names layer answers a click and opens its card, which the
pick suite now pins down.

Pictures that will move, and are not re-pinned here: zelda's screen-chart,
screen-labels-held, screen-ground, screen-raster-deep and
screen-outside-bounds, and cyberpunk's screen-chart, screen-labels-held,
screen-raster-deep and screen-outside-bounds — every chart frame of the two
volumes that curate text collections. The dock pictures are regions of the
panel and do not move; no other volume has a text collection.

Gates: npm ci, npm run seam-lane (252 seam tests), npm run seam-build,
go run ./golden/depcheck, go test ./golden/... — all green.

* app: the reset keeps the reader's place

Pressing the refresh key forgets how the reader was looking at a world
and nothing about where they stood: the record comes back holding only
the world and the lens, and the next load arranges a fresh session
around that place. A refresh that also teleported would be answering a
question nobody pressed the key to ask.

* render: a camera is checked wherever it crosses between the panes

A reader held geohash 3qd on Mars under the MOLA lens, went into the sphere
and came back, and was left looking at nothing at all -- the chart pane up
over a camera that could not be placed, with no control on the page able to
move it again. That is not a pane that drew badly. It is a pane standing at a
coordinate that is not a coordinate, and every way back from it went through
the same coordinate.

The reference had exactly one guard against this, and the rewrite had lost it:
the sphere's write-back checked that both coordinates were numbers before the
map was told, because -- its own words -- a broken number there once blacked
out both panes with no way back. Put back here, and widened, because
finiteness was never the whole of the class. A finite zoom past what the lens
has levels for is a camera the pyramid cannot serve. A finite centre outside
the square the lens drew is ground that was never drawn. And the chart behind
the sphere is fitted against a window of no size, which lands it at whatever
the deepest level happens to be, over the cell it was told to hold -- right
while nobody is looking through it, and the reader's own view the moment it
comes back up.

So there is one sanitizer now and it stands on every boundary a camera
crosses: entering the sphere, the write-back leaving it, the chart's one door
for every camera it is ever told -- a jump, a lens rebuild, the session's own
record from whatever build wrote it -- and the pane fronting, which is the
only moment the hidden camera can be asked about without moving what the
recorded globe steps pin. It is exact: a healthy camera comes back out of it
bit for bit, and a pane coming up over one is not written to at all. A camera
that cannot be repaired is refused rather than guessed at, and the caller
opens the world's own view instead, which is where a reader arriving for the
first time lands anyway. One press of a zoom control is now enough to get out
of a camera that stands nowhere, where before every press added to a NaN.

The sphere's half is the more dangerous half and is guarded the same way at
every steer it makes. A map given a broken centre can still be told a good one
afterwards; three's camera cannot be told anything at all once it has one, and
every reading taken off it afterwards -- including the camera the sphere hands
back -- carries it. That is the whole mechanism by which one number blanked
two panes.

The Mars tour is identical across all 95 steps.

* app: every legend section opens the same way

Zones opened folded while every other section opened out, which was the
arrangement guessing that ground is long and pins are short -- a
proportion no world promises. All sections now open open, and a reader
folds what they are not using. User-decided; the island-checked baseline
steps move with it, and the full tour re-captures at stabilization.

* test: a gate that refuses to call a skip a pass

The golden workflow's verdict was the exit status, and go test exits
zero when every test in a package skips -- so the pipeline gate said
PASS in CI while judging nothing, and the derive gate went quiet for
weeks when its reference tool left the tree. tools/testgate is the
structural fix: it reads the -json stream and fails the run on any
test-level skip, so a required suite either judges or goes red.

CI is rebuilt around that rule. The new workflow runs go vet and every
Go test in the module -- the 43 hermetic files under format/, internal/
and cmd/ were never gated before -- plus both TypeScript lanes and the
guardrails, on all three platforms, with the race detector where a C
toolchain is free. An e2e job builds every one of its own prerequisites
so there is no 'not judged' outcome left to report. depcheck moves to
tools/, because it is architecture enforcement, not a golden, and it
outlives the harness that used to run it.

* corpus: the tree keeps only what is public

The captured fixtures split along one line: provenance. bend-or is city
open data and mars is NASA Trek, so their extractions move to
testdata/corpus/ and stay -- real bytes, publicly redistributable, and
between them the deepest pipeline output and the sphere. The four
commercial-game captures, the recorded parity tours, the screenshots
and the private link-farm machinery leave the tree entirely; a public
repository is no place for crawled game databases, and the reference
they were captured against is archived on golden-reference either way.
The one captured translator document that survives is NASA Trek's, so
that translator's test can keep parsing what the source actually
serves.

The workbench page test that anchored TUNIC's captured score now states
its own bundle and holds the page to what the measurement lane reports
for those bytes -- what each point is worth is the maturity suite's own
business -- which also retires the last version-skip a required test
could hide behind. The fixture packers go with it.

Tests that read the departed volumes and tours -- the render lane's
grid and projection suites among them -- are rebuilt against the corpus
and stated models in the commits that follow.

* e2e: the application is walked over bytes this repository holds

The parity tour needed the maintainer's installed library and never ran
in CI. Its replacement needs nothing that is not committed or built by
the run: tests/e2e/prep packs the corpus back into real .atlas files
through the format's own writer, Playwright starts atlas serve over
that registry and walks the real pages -- the doorways, the catalog,
the island a page carries, the topbar's selects, a lens change and the
session that remembers it. The specs assert arrangement rather than
pixels, because the corpus commits no rasters and surviving a missing
tile is the render suite's own proof.

The deep check over a real library survives as make corpus-smoke,
deliberately outside CI: it walks an installed bundles directory and
holds every current-format bundle to the reader's invariants -- opens,
validates, unpacks -- and compares no stamps, no hashes and no content.
Older-format bundles are reported as old rather than broken.

tests/corpus is the packer both of them and any future test can share.

* format: the roundtrip judges committed bytes, and a fuzzer joins

The extraction-mode checks move from golden/format into format/bundle
itself, pointed at the corpus: the manifest re-encodes to the committed
extraction byte for byte, the versioned file name derives from it, the
packed locations repack to the recorded hashes and unpack back, and the
whole volume reassembles through the real writer into a bundle that
opens and validates. What needed a real library -- the TestReal suite,
the registry directory tests, ATLAS_REGISTRY_DIR -- is gone from the
required surface; the corpus-smoke command is where that checking now
lives, named as what it is.

Three fuzz targets guard the trust boundary where .atlas bytes come in
from disk. The fuzzer paid rent on its first run: a payload whose
latitude spells a NaN broke the round-trip property -- a flaw in the
comparison, not the codec -- and the trigger is now a permanent seed.

STAMPS.md moves to docs/stamps.md with its measurement record intact.

* island: the page's own account is held to the contract, not the recording

The island suite moves to tests/island -- outside internal/app, where
hostenv lets a test read the corpus off disk -- and every assertion
that used to diff a recorded tour's session object now derives from
island.go's stated contract and the corpus's own arithmetic: exactly
twelve keys, ids that are the payload's own, a footer count computed
from the collections it sums, a camera exercised at the half-up
rounding seams. The four commercial volumes' cases are gone with their
fixtures; the one check that needed Hyrule's shape mix is rebuilt over
a stated three-collection volume.

golden/http retires with its captured transcript. What its synthesized
mode proved and app_test.go did not already: the catalog carries the
build's stamp, grid and world records; a path that climbs toward /etc
stays on-application through every redirect hop and ends 404; and the
doorway at / is a real 302 to a path, which is the only shape the
desktop host's follower accepts. The 34 exchanges that replayed only
against an installed library are retired outright.

* cells: one vector file, two readers, and properties where plans were

The cell-system contract moves to analysis/testdata/cells: the four
hand-derived grounds and all 178 vector rows, none of which ever stood
on a commercial volume, plus a bend-or ground from the corpus -- a
plane with a Mercator window, which is a real refusal case for S2 in
both lanes. The TypeScript lane and a new tests/cells package read the
same files, which is what keeps the two copies of the arithmetic from
drifting until the planned unification deletes one.

The recorded plans -- contract.json included -- are gone, and what
they proved is proven by construction instead: containment nests a
child in its parent, hierarchy inverts both ways, the carry across a
system change keeps the place and the precision, and the plan order is
reconstructed from the documented rule rather than compared against a
tour that can no longer be walked. A halving property closes the loop
the tour extents used to close: 576 cells halve, address back, and
un-halve exactly across all six grounds.

* render: the seam is judged by arithmetic it can show its work for

The recorded tours and plans are gone, so every number the seam tests
lean on is now derived where it stands: the geohash grid's halving
written out cell by cell and pinned to paper extents, pin counts
recounted by an independent sweep of the corpus's own locations, fit
resolutions recomputed from the ruler stub, mars's opening camera
spelled as 8192/909 rather than remembered as a decimal. What the four
commercial volumes used to supply arrives from two stated factories
instead: a split-sheet volume whose three lenses window one shared
pyramid, and a game plane with a holed multipart district, a bounded
lens, and the nearest-neighbour declaration no corpus lens carries --
a hole the old tests named and never exercised.

What is honestly gone with the recordings: the proof that a real
harness run once consumed exactly these bytes, zelda's real captured
split-sheet inventory, and the old breadth of twelve pyramids and
twenty lenses. What replaced it is narrower and better grounded, and
the corpus keeps the depth where it matters -- bend-or alone carries
98 multipart features through the element suite.

* pipeline: it reproduces its own bytes, and every source shows a capture

The reproduction gates that compared against the reference tree's
archives ran on one machine and skipped everywhere else. What they
observably promised is now asserted from stated inputs: a synthetic
archive -- two sources of one game on an alignment grid, a city day
with a drawn basemap whose witness tiles come from the real renderer
-- runs the real shipped stages twice and the outputs are byte
identical, name for name, registry index included; reruns are no-ops;
every bundle validates; the merged ledger accounts for its donors and
the enriched revision outranks the plain one. A flipped witness hash
still refuses the tile derivation.

Every translator now has a hermetic test. NASA Trek keeps parsing a
real capture -- the committed Mars document, 2,048 features landing on
the fixture's own positions, the 2^z-by-2^(z-1) level extents both
mosaics really have. IGN and ArcGIS Hub parse minimal invented
captures that exercise projection, id stability, legend order, the
multipart folds and the membership join, refusals included.

* cells: the predicate table speaks the registry's own names

The ported edge-case table wrote convention keys as string literals,
which the golden lane was exempt from and internal/app is not. The
keys come from format/semconv now, like every other producer's.

* teardown: the tree describes itself in the present tense

What remained of golden/ -- the harness, the waiver file, the canon,
the capture pointer, HARNESS.md -- is deleted. The waivers' substance
is decision 18, the divergences accepted against the archived
reference; the harness's substance is ci.yml and tools/testgate; the
capture programs live where they always will, on the golden-reference
tag, mirrored at archive/golden-reference. depcheck's lane map now
names tests/ where it named golden/, with the same standing: a test
tree may read anything it judges.

Every comment that cited a golden as its authority now states the
contract it was actually pinning and names the suite that holds it --
the island's key names to docs/app.md and tests/island, the data
plane's whole-file rule to its own test, the seam's banner to the
consumers that compare it verbatim. One citation of the archive tag
survives, on the globe's pairing constants, which are calibrated
numbers no arithmetic in this tree can re-derive.

The docs describe the repository as it stands: docs/testing.md is the
testing surface, the per-lane docs name the suites that hold their
claims, and the history is a pointer -- the tag, and the dated records
under docs/decisions.

* test: the last two conditional skips become verdicts

A required suite may not decline to judge, and these two could: the
library-fallback test skipped on a machine with no config directory,
where the refusal itself is half the contract; and the legend-colour
test skipped if the hash ever agreed with the wheel, where agreeing
data is a broken test, not a shrug. Both faces are asserted now, and
a bad draw fails loudly with instructions.

* test: gofmt settles the stated-bundle helpers

* fix: a library can be let go of, and Windows insisted

The first Windows run of the whole suite found what no other platform
enforces: the oshost tests opened a library in a temporary directory
and never released the readers, and Windows refuses to delete a file
somebody is holding. Volumes grows a Close that shuts every reader the
current snapshot holds -- the desktop host never needs it, but a
caller that must delete what it opened has to let go first -- and the
tests clean up through it.

* fix: the wheel answers to the sphere's own clamps

globe.gl's controls arrive willing to dolly the camera to a hair above
the skin -- inside the detail shell and under the pins, where every
face is a back face and the pane renders black -- and out to a hundred
radii, where the planet is a dot. Every deliberate way of moving the
camera already clamps to the pairing's two altitudes; the scroll wheel
was the one door that had never been told. holdControls gives the
controls the same rails, and the globe suite holds the wheel's floor
above everything drawn off the skin and its ceiling short of a dot.

* app: a crumb with one option is a fact, not a choice

The volume and world crumbs are secretly selects, and a select of one
kept offering itself -- chevron, focus ring, a menu with nothing to
choose. With a single option the crumb now renders disabled: still in
the trail, in the trail's own voice, but nothing to press. The lens
crumb already hides below two options. The e2e walk pins both faces:
the two-volume library keeps a real volume select, and the one-world
volumes read but do not edit.

* app: every feature index arrives folded

A city volume opened onto its whole zoning index -- a hundred and
eighteen rows unfolded before the reader asked for one. The sections
still stand open, because the categories are the world's own summary;
what no longer opens itself is a category's member list, which is a
request the reader makes with one press on the row or one on
unfold-all. The unfold-all control keeps its old reach, the ungrouped
shape collections, under a name that now says what it is for.

* app: the mark stands by the field, and a crumb gives the keyboard back

The navigator spelled its system twice -- a full name on the field and
a mark on a button that vanished when there was nothing to cycle to.
Now there is one spelling: the mark, always beside the field it
qualifies, a button when the world offers another system and inert
when it does not. A hash without its system is half an address, so the
mark does not leave with the choice.

And the crumbs learn to let go: a select keeps the keyboard after it
is used, so the next typed letter walked its options -- a g jumping
the reader to a volume that starts with one -- instead of speaking to
the map. A change on a crumb arms a one-shot follow, and the settle
that answers the pick hands focus back to the map. The e2e walk now
asserts the handback.

* render: a zone wears its own colour, not its collection's

The index, the filter and the detail card colour a shape by the
feature hash -- the application's stated contract is that a feature
wears one colour wherever it is drawn -- but the map coloured every
zone by its collection, so a zoning layer's fourteen districts arrived
in one teal and the legend beside them told a different story in five
colours. The seam now spells the server's own hash (featureColor,
Knuth's multiplicative over the id, the same ten-colour wheel the
analysis lane exports) and areas and paths draw with it. The suite
pins the arithmetic to hand-derived indexes and holds a declared
collection colour apart from what its zones wear.

* cells: a game divides its whole square, and a city divides the earth

Two grounds, two truths. A game plane's geohash now halves the world
square rather than whichever surface the open lens declared, so the
grid covers the whole map and an address survives a lens switch --
two lenses of one world used to declare two surfaces, and every hash
moved when the reader changed pictures.

A plane that declares an earth mercator window divides the earth
itself: the addresses are genuine WGS84 geohashes -- 9rcd really is
the city's own cell -- computed lng-first in degrees and drawn back
through the declared window, overhanging the raster's edge honestly
rather than pretending the city is the world. The depth telescope is
fit to the window (base 4, floor 6 for a city this size), the plan
enumerates only cells the window touches, and S2 keeps refusing what
it cannot honestly divide.

Both lanes carry both rules -- the Go twin grows the same mercator
mapping -- and the shared vectors grow thirty hand-derived rows, every
one cross-checked against an independent textbook derivation.

* depcheck: the clean-room rule retires with the tree it guarded

The rule forbade importing the pre-rewrite packages while the two
trees lived side by side. The promotion squash carries none of the old
tree to main, so there is nothing left for the rule to forbid: an
import of a package that does not exist fails the compiler before any
analyzer speaks. Four guardrails remain -- the lane matrix, hostenv
purity, network confinement, semconv discipline -- which are the
architecture's own rules rather than the migration's.
commit
3f9d78a5bdd32456bf402cd552975cd5f4e826a1
parent
b8d2888875133610274227640da74b1c600b4771
author
FelineStateMachine <dami.etoile@gmail.com>
584 files changed, +485638 -38362
M.gitattributes24+
M.github/workflows/ci.yml209+-
M.github/workflows/release.yml240+
M.github/workflows/snapshot.yml52+-
M.github/workflows/snapshots.yml11+-
M.gitignore47+-
MMakefile118+
MREADME.md311+-
Manalysis/cellsystems/contract.ts157+
Manalysis/cellsystems/geohash.ts638+
Manalysis/cellsystems/ground.ts94+
Manalysis/cellsystems/index.ts74+
Manalysis/cellsystems/plan.ts115+
Manalysis/cellsystems/registry.ts113+
Manalysis/cellsystems/ring.ts140+
Manalysis/cellsystems/s2.ts323+
Manalysis/cellsystems/tokens.ts77+
Manalysis/cellsystems/visual.ts115+
Manalysis/index.ts12+
Manalysis/package.json15+
Manalysis/semconv/geometry.ts197+
Manalysis/semconv/keys.ts126+
Manalysis/test/conformance.test.ts107+
Manalysis/test/conformance.ts445+
Manalysis/test/districts.ts190+
Manalysis/test/geohash.test.ts238+
Manalysis/test/ground.test.ts114+
Manalysis/test/grounds.ts116+
Manalysis/test/plan.test.ts110+
Manalysis/test/properties.test.ts370+
Manalysis/test/registry.test.ts72+
Manalysis/test/ring.test.ts91+
Manalysis/test/s2-limits.test.ts60+
Manalysis/test/s2.test.ts122+
Manalysis/test/vectors.test.ts144+
Manalysis/test/visual.test.ts92+
Manalysis/testdata/cells/carry.json94+
Manalysis/testdata/cells/containment.json694+
Manalysis/testdata/cells/continuity.json899+
Manalysis/testdata/cells/geometry.json529+
Manalysis/testdata/cells/grounds.json197+
Manalysis/testdata/cells/hierarchy.json279+
Manalysis/testdata/cells/identity.json835+
Manalysis/testdata/cells/input.json175+
Manalysis/testdata/cells/surface.json193+
Manalysis/tsconfig.json26+
Massets/app.css1-
Massets/app.js5290-
Massets/index.html191-
Mcmd/atlas/adapt.go247+
Mcmd/atlas/adapt_test.go175+
Mcmd/atlas/compose.go144+
Mcmd/atlas/crawl.go102+
Mcmd/atlas/dev.go252+
Mcmd/atlas/enrich.go315+
Mcmd/atlas/main.go116+
Mcmd/atlas/main_test.go78+
Mcmd/atlas/measure.go167+
Mcmd/atlas/pipeline_test.go704+
Mcmd/atlas/serve.go145+
Mcmd/atlas/serve_test.go59+
Mcmd/atlas/tiles.go194+
Mcmd/atlas/translate.go103+
Mcmd/atlas/warp.go182+
Mcmd/atlas/warp_test.go119+
Mcmd/atlas/workbench.go168+
Mcmd/cartograph/diff.go235-
Mcmd/cartograph/diff_test.go135-
Mcmd/cartograph/library.go127-
Mcmd/cartograph/main.go81-
Mcmd/cartograph/measure.go23-
Mcmd/cartograph/measure_test.go226-
Mcmd/cartograph/ops.go93-
Mcmd/cartograph/ops_test.go161-
Mcmd/cartograph/server.go196-
Mcmd/cartograph/server_test.go102-
Mcmd/cartograph/sources.go209-
Mcmd/cartograph/sources_test.go89-
Mcmd/cartograph/ui/app.js31-
Mcmd/cartograph/ui/collection.tmpl35-
Mcmd/cartograph/ui/diff.tmpl58-
Mcmd/cartograph/ui/layout.tmpl23-
Mcmd/cartograph/ui/sources.tmpl44-
Mcmd/cartograph/ui/style.css268-
Mcmd/cartograph/ui/volume.tmpl63-
Mdialog.go39-
Mdocs/README.md32+
Mdocs/analysis.md487+
Mdocs/app.md786+
Mdocs/decisions/0001-htmx-4-at-the-center.md51+
Mdocs/decisions/0002-allons-is-removed.md47+
Mdocs/decisions/0003-merge-moves-to-enrich.md42+
Mdocs/decisions/0004-source-neutral-interchange-document.md46+
Mdocs/decisions/0005-unbounded-feature-maturity.md43+
Mdocs/decisions/0006-the-scene-description-seam.md50+
Mdocs/decisions/0007-hostenv-portability.md50+
Mdocs/decisions/0008-stamp-identity-is-an-aspiration.md55+
Mdocs/decisions/0009-the-enrich-compose-adapter-seam.md55+
Mdocs/decisions/0010-revision-packing.md57+
Mdocs/decisions/0011-one-event-stream.md54+
Mdocs/decisions/0012-scan-at-launch-never-watch.md44+
Mdocs/decisions/0013-assets-and-static-are-two-mounts.md45+
Mdocs/decisions/0014-the-depcheck-pragma.md48+
Mdocs/decisions/0015-the-ground-descriptor.md57+
Mdocs/decisions/0016-uncurated-captures-are-passed-over.md50+
Mdocs/decisions/0017-the-host-follows-redirects.md64+
Mdocs/decisions/0018-divergences-from-the-reference.md77+
Mdocs/decisions/README.md42+
Mdocs/enrich.md600+
Mdocs/format.md704+
Mdocs/generate.md1124+
Mdocs/logging.md112+
Mdocs/render-seam.md645+
M{internal => docs}/semconv/REGISTRY.md78+-
Mdocs/snapshots.md32+-
Mdocs/stamps.md91+
Mdocs/testing.md87+
Mdocs/workbench.md234+
Meslint.config.mjs227+
Mformat/bundle/bundle.go233+
Mformat/bundle/bundle_test.go326+
Mformat/bundle/codec.go251+
Mformat/bundle/codec_test.go226+
Mformat/bundle/corpus_test.go679+
Mformat/bundle/fixture_test.go156+
Mformat/bundle/fuzz_test.go202+
Mformat/bundle/read.go206+
Mformat/bundle/registry.go219+
Mformat/bundle/registry_test.go315+
Mformat/bundle/scan.go186+
Mformat/bundle/stamp.go113+
Mformat/bundle/stamp_test.go188+
Mformat/bundle/validate.go320+
Mformat/bundle/validate_test.go354+
M{internal => format}/bundle/write.go66+-
Mformat/semconv/behavior.go134+
Mformat/semconv/registry.go172+
Mformat/semconv/registry_gen.go178+
Mformat/semconv/semconv.go43+
Mformat/semconv/semconv_test.go266+
Mfrontend/app.css33-
Mfrontend/app.js8-
Mfrontend/build.mjs15-
Mfrontend/mockup/identity.html566-
Mfrontend/package-lock.json1242-
Mfrontend/package.json19-
Mfrontend/parity/compare.mjs82-
Mfrontend/parity/run.mjs135-
Mfrontend/parity/tour.js443-
Mfrontend/src/areas.js320-
Mfrontend/src/boot.js49-
Mfrontend/src/catalog.js96-
Mfrontend/src/cellsystems/geohash.js181-
Mfrontend/src/cellsystems/index.js144-
Mfrontend/src/cellsystems/s2.js243-
Mfrontend/src/collections.js71-
Mfrontend/src/constants.js63-
Mfrontend/src/detail.js357-
Mfrontend/src/diagnostics.js111-
Mfrontend/src/dom.js66-
Mfrontend/src/engine.js276-
Mfrontend/src/events.js352-
Mfrontend/src/features.js179-
Mfrontend/src/globe.js1045-
Mfrontend/src/grid.js377-
Mfrontend/src/legend.js478-
Mfrontend/src/library.js98-
Mfrontend/src/navigation.js430-
Mfrontend/src/overview.js172-
Mfrontend/src/search.js155-
Mfrontend/src/semconv.js158-
Mfrontend/src/session.js64-
Mfrontend/src/state.js72-
Mfrontend/src/styles.js522-
Mfrontend/src/theme.js98-
Mfrontend/src/util.js25-
Mfrontend/src/visibility.js56-
Mfrontend/test/cellsystems.test.mjs219-
Mfrontend/test/collections.test.mjs88-
Mfrontend/test/semconv.test.mjs70-
Mgo.mod21+-
Mgo.sum65+-
Minternal/app/app.go143+
Minternal/app/app_test.go1492+
Minternal/app/assets/assets.go176+
M{frontend => internal/app/assets}/css/base.css0
M{frontend => internal/app/assets}/css/brand.css0
M{frontend => internal/app/assets}/css/category-rows.css0
Minternal/app/assets/css/chrome.css183+
M{frontend => internal/app/assets}/css/dock.css0
M{frontend => internal/app/assets}/css/empty-state.css0
M{frontend => internal/app/assets}/css/fatal-error.css0
M{frontend => internal/app/assets}/css/feature-index.css0
M{frontend => internal/app/assets}/css/globe.css0
M{frontend => internal/app/assets}/css/grid-navigator.css18+-
M{frontend => internal/app/assets}/css/icon-button.css0
M{frontend => internal/app/assets}/css/layer-header.css0
M{frontend => internal/app/assets}/css/layer-switch.css0
M{frontend => internal/app/assets}/css/layers.css0
M{frontend => internal/app/assets}/css/legend-toolbar.css0
M{frontend => internal/app/assets}/css/map-corner.css0
M{frontend => internal/app/assets}/css/map-hint.css0
M{frontend => internal/app/assets}/css/map-loading.css0
M{frontend => internal/app/assets}/css/map-panel.css0
M{frontend => internal/app/assets}/css/map-pickers.css6+-
M{frontend => internal/app/assets}/css/overview.css0
M{frontend => internal/app/assets}/css/pin-detail.css0
M{frontend => internal/app/assets}/css/responsive.css0
M{frontend => internal/app/assets}/css/search.css0
M{frontend => internal/app/assets}/css/shell.css0
M{frontend => internal/app/assets}/css/sidebar-footer.css0
M{frontend => internal/app/assets}/css/sidebar.css0
M{frontend => internal/app/assets}/css/solo-chip.css0
M{frontend => internal/app/assets}/css/text-symbol.css0
M{frontend => internal/app/assets}/css/tokens.css0
M{frontend => internal/app/assets}/css/tooltip.css0
M{frontend => internal/app/assets}/css/topbar-collapse.css0
M{frontend => internal/app/assets}/css/topbar.css0
M{frontend => internal/app/assets}/css/zoom-controls.css0
Minternal/app/assets/htmx.LICENSE13+
Minternal/app/assets/htmx.min.js1+
Minternal/app/assets/hx-sse.min.js1+
Minternal/app/cells/carry.go194+
Minternal/app/cells/cells.go306+
Minternal/app/cells/mercator.go256+
Minternal/app/cells/predicate_test.go410+
Minternal/app/cells/s2.go326+
Minternal/app/chrome.go50+
Minternal/app/collate.go141+
Minternal/app/collate_test.go149+
Minternal/app/data.go163+
Minternal/app/events.go210+
Minternal/app/events_test.go343+
Minternal/app/filter.go309+
Minternal/app/grid.go271+
Minternal/app/grid_test.go602+
Minternal/app/hostenv/hostenv.go177+
Minternal/app/hostenv/hostenv_test.go96+
Minternal/app/hostenv/memory.go77+
Minternal/app/hostenv/oshost/oshost.go78+
Minternal/app/hostenv/oshost/oshost_test.go294+
Minternal/app/hostenv/oshost/sessions.go122+
Minternal/app/hostenv/oshost/volumes.go243+
Minternal/app/hostenv/wailshost/wailshost.go83+
Minternal/app/importing.go133+
Minternal/app/island.go145+
Minternal/app/legend.go710+
Minternal/app/legend_test.go95+
Minternal/app/pages.go199+
Minternal/app/partials.go147+
Minternal/app/session.go1051+
Minternal/app/templates/detail.tmpl46+
Minternal/app/templates/dock.tmpl36+
Minternal/app/templates/empty-state.tmpl37+
Minternal/app/templates/grid-navigator.tmpl58+
Minternal/app/templates/import.tmpl23+
Minternal/app/templates/island.tmpl18+
Minternal/app/templates/legend.tmpl198+
Minternal/app/templates/overview.tmpl23+
Minternal/app/templates/shell.tmpl334+
Minternal/app/templates/templates.go114+
Minternal/app/templates/topbar.tmpl76+
Minternal/app/templates/viewport.tmpl61+
Minternal/app/view.go582+
Minternal/app/world.go660+
Minternal/arcgismap/arcgismap.go1005-
Minternal/arcgismap/arcgismap_test.go600-
Minternal/arcgismap/clip.go203-
Minternal/arcgismap/clip_test.go144-
Minternal/arcgismap/hydro_test.go106-
Minternal/arcgismap/national.go115-
Minternal/basemap/basemap.go283-
Minternal/basemap/basemap_test.go153-
Minternal/basemap/clip.go137-
Minternal/basemap/stroke.go93-
Minternal/basemap/style.go56-
Minternal/blend/blend_test.go134-
Minternal/bundle/bundle.go174-
Minternal/bundle/bundle_test.go388-
Minternal/bundle/bundletest/bundletest.go169-
Minternal/bundle/dir.go24-
Minternal/bundle/locations.go147-
Minternal/bundle/read.go368-
Minternal/bundle/registry.go361-
Minternal/bundle/registry_test.go280-
Minternal/bundle/stamp.go85-
Minternal/{blend/blend.go => enrich/align/align.go}126+-
Minternal/enrich/align/align_test.go201+
Minternal/enrich/contribution.go364+
Minternal/enrich/curation/curation.go137+
Minternal/enrich/curation/curation.json44+
Minternal/enrich/curation/curation_test.go107+
Minternal/enrich/enrich.go268+
Minternal/enrich/enrich_test.go609+
Minternal/enrich/enrichers/enrichers.go26+
Minternal/enrich/enrichers/lenses/lenses.go93+
Minternal/enrich/enrichers/lenses/lenses_test.go138+
Minternal/enrich/enrichers/merge/merge.go716+
Minternal/enrich/enrichers/merge/merge_test.go518+
Minternal/enrich/enrichers/national/national.go324+
Minternal/enrich/enrichers/national/national_test.go319+
Minternal/{icons => enrich/enrichers/stdicons}/standard/maki/LICENSE.txt0
Minternal/{icons => enrich/enrichers/stdicons}/standard/maki/beach.svg0
Minternal/{icons => enrich/enrichers/stdicons}/standard/maki/caution.svg0
Minternal/{icons => enrich/enrichers/stdicons}/standard/maki/circle-stroked.svg0
Minternal/{icons => enrich/enrichers/stdicons}/standard/maki/circle.svg0
Minternal/{icons => enrich/enrichers/stdicons}/standard/maki/fire-station.svg0
Minternal/{icons => enrich/enrichers/stdicons}/standard/maki/hospital.svg0
Minternal/{icons => enrich/enrichers/stdicons}/standard/maki/landuse.svg0
Minternal/{icons => enrich/enrichers/stdicons}/standard/maki/monument.svg0
Minternal/{icons => enrich/enrichers/stdicons}/standard/maki/mountain.svg0
Minternal/{icons => enrich/enrichers/stdicons}/standard/maki/park.svg0
Minternal/{icons => enrich/enrichers/stdicons}/standard/maki/parking.svg0
Minternal/{icons => enrich/enrichers/stdicons}/standard/maki/post.svg0
Minternal/{icons => enrich/enrichers/stdicons}/standard/maki/square-stroked.svg0
Minternal/{icons => enrich/enrichers/stdicons}/standard/maki/square.svg0
Minternal/{icons => enrich/enrichers/stdicons}/standard/maki/triangle-stroked.svg0
Minternal/{icons => enrich/enrichers/stdicons}/standard/maki/triangle.svg0
Minternal/{icons => enrich/enrichers/stdicons}/standard/maki/volcano.svg0
Minternal/{icons => enrich/enrichers/stdicons}/standard/maki/water.svg0
Minternal/{icons => enrich/enrichers/stdicons}/standard/maki/waterfall.svg0
Minternal/{icons => enrich/enrichers/stdicons}/standard/maki/wetland.svg0
Minternal/enrich/enrichers/stdicons/stdicons.go148+
Minternal/enrich/enrichers/stdicons/stdicons_test.go156+
Minternal/enrich/ledger.go308+
Minternal/enrich/maturity/gate.go94+
Minternal/enrich/maturity/maturity.go623+
Minternal/enrich/maturity/maturity_test.go437+
Minternal/enrich/maturity/points.json46+
Minternal/enrich/revision.go82+
Minternal/enrich/volume.go481+
Minternal/generate/archive/archive.go310+
Minternal/generate/archive/archive_test.go188+
Minternal/generate/archive/tiles.go155+
Minternal/generate/compose/compose.go484+
Minternal/generate/compose/compose_test.go371+
Minternal/generate/compose/conventions.go95+
Minternal/generate/compose/payload.go290+
Minternal/generate/compose/split.go577+
Minternal/generate/compose/split_test.go267+
Minternal/generate/compose/surface.go171+
Minternal/generate/compose/write.go291+
Minternal/generate/crawl/crawl.go112+
Minternal/generate/crawl/crawl_test.go184+
Minternal/generate/crawl/crawler.go121+
Minternal/generate/crawl/fetch.go218+
Minternal/generate/crawl/ign.go464+
Minternal/generate/crawl/write.go438+
Minternal/generate/curation/curation.go222+
Minternal/generate/curation/curation.json76+
Minternal/generate/curation/curation_test.go156+
Minternal/generate/doc/doc.go500+
Minternal/generate/doc/doc_test.go166+
Minternal/generate/doc/ids.go53+
Minternal/generate/doc/synthetic.go96+
Minternal/generate/icons/icons.go56+
Minternal/generate/icons/standard/maki/LICENSE.txt116+
Minternal/generate/icons/standard/maki/beach.svg4+
Minternal/generate/icons/standard/maki/caution.svg4+
Minternal/generate/icons/standard/maki/circle-stroked.svg4+
Minternal/generate/icons/standard/maki/circle.svg4+
Minternal/generate/icons/standard/maki/fire-station.svg4+
Minternal/generate/icons/standard/maki/hospital.svg4+
Minternal/generate/icons/standard/maki/landuse.svg4+
Minternal/generate/icons/standard/maki/monument.svg4+
Minternal/generate/icons/standard/maki/mountain.svg4+
Minternal/generate/icons/standard/maki/park.svg4+
Minternal/generate/icons/standard/maki/parking.svg4+
Minternal/generate/icons/standard/maki/post.svg4+
Minternal/generate/icons/standard/maki/square-stroked.svg4+
Minternal/generate/icons/standard/maki/square.svg4+
Minternal/generate/icons/standard/maki/triangle-stroked.svg4+
Minternal/generate/icons/standard/maki/triangle.svg4+
Minternal/generate/icons/standard/maki/volcano.svg4+
Minternal/generate/icons/standard/maki/water.svg4+
Minternal/generate/icons/standard/maki/waterfall.svg4+
Minternal/generate/icons/standard/maki/wetland.svg4+
Minternal/generate/sources/arcgishub/arcgishub.go601+
Minternal/generate/sources/arcgishub/arcgishub_test.go336+
Minternal/generate/sources/arcgishub/capture.go77+
Minternal/generate/sources/arcgishub/cities.go291+
Minternal/generate/sources/arcgishub/drawing.go103+
Minternal/{arcgismap => generate/sources/arcgishub}/hydro.go102+-
Minternal/generate/sources/arcgishub/national.go85+
Minternal/generate/sources/arcgishub/testdata/city.json100+
Minternal/generate/sources/arcgishub/window.go106+
Minternal/generate/sources/ign/capture.go72+
Minternal/generate/sources/ign/ign.go358+
Minternal/generate/sources/ign/ign_test.go288+
Minternal/generate/sources/ign/testdata/wikimap.json29+
Minternal/generate/sources/mapgenie/capture.go117+
Minternal/generate/sources/mapgenie/mapgenie.go432+
Minternal/generate/sources/mapgenie/mapgenie_test.go350+
Minternal/generate/sources/nasatrek/capture.go66+
Minternal/generate/sources/nasatrek/nasatrek.go459+
Minternal/generate/sources/nasatrek/nasatrek_test.go465+
Minternal/generate/sources/piggyback/capture.go118+
Minternal/generate/sources/piggyback/piggyback.go481+
Minternal/generate/sources/piggyback/piggyback_test.go188+
Minternal/generate/sources/registry.go29+
Minternal/generate/sources/sources.go86+
Minternal/generate/sources/sources_test.go56+
Minternal/generate/tiles/basemap/basemap.go279+
Minternal/generate/tiles/basemap/basemap_test.go236+
Minternal/generate/tiles/basemap/clip.go184+
Minternal/generate/tiles/basemap/stroke.go102+
Minternal/generate/tiles/basemap/style.go65+
Minternal/generate/tiles/derive.go477+
Minternal/generate/tiles/draw.go143+
Minternal/generate/tiles/install.go103+
Minternal/generate/tiles/plan.go409+
Minternal/generate/tiles/stamp.go132+
Minternal/generate/tiles/tiles.go256+
Minternal/generate/tiles/tiles_test.go136+
Minternal/generate/tiles/warp.go388+
Minternal/icons/icons.go36-
Minternal/icons/icons_test.go27-
Minternal/ignmap/ignmap.go301-
Minternal/ignmap/ignmap_test.go245-
Minternal/logging/logging.go178+
Minternal/logging/logging_test.go172+
Minternal/measure/measure.go484-
Minternal/mgdoc/mgdoc.go227-
Minternal/mgdoc/mgdoc_test.go59-
Minternal/pbmap/pbmap.go457-
Minternal/pbmap/pbmap_test.go231-
Minternal/semconv/semconv.go428-
Minternal/semconv/semconv_test.go165-
Minternal/trekmap/trekmap.go470-
Minternal/trekmap/trekmap_test.go360-
Minternal/workbench/assets.go58+
Minternal/workbench/assets/tokens.css37+
Minternal/workbench/assets/workbench.css248+
Minternal/workbench/bundles_test.go180+
Minternal/workbench/diff.go300+
Minternal/workbench/library.go214+
Minternal/workbench/operations.go205+
Minternal/workbench/operations_test.go278+
Minternal/workbench/oprunner/oprunner.go395+
Minternal/workbench/oprunner/oprunner_test.go431+
Minternal/workbench/oprunner/serve.go125+
Minternal/workbench/sources.go64+
Minternal/workbench/templates/diff.tmpl88+
Minternal/workbench/templates/layout.tmpl35+
Minternal/workbench/templates/library.tmpl48+
Minternal/workbench/templates/op-row.tmpl10+
Minternal/workbench/templates/operations.tmpl69+
Minternal/workbench/templates/sources.tmpl38+
Minternal/workbench/templates/volume.tmpl169+
Minternal/workbench/workbench.go398+
Minternal/workbench/workbench_test.go356+
Mlink_darwin.go6+
Mmain.go224+-
Mpackage-lock.json3038+
Mpackage.json30+
Mparity_dev.go115-
Mparity_release.go15-
Mredirects.go123+
Mrender/boot.ts78+
Mrender/build.mjs37+
Mrender/chart/element.ts1401+
Mrender/chart/grid.ts140+
Mrender/chart/markers.ts271+
Mrender/chart/overview.ts231+
Mrender/chart/projection.ts103+
Mrender/chart/raster.ts174+
Mrender/chart/styles.ts619+
Mrender/context.ts51+
Mrender/data/atlasloc.ts131+
Mrender/data/payload.ts177+
Mrender/data/plane.ts148+
Mrender/data/pyramid.ts152+
Mrender/data/report.ts114+
Mrender/diagnostics.ts200+
Mrender/globe/element.ts2002+
Mrender/globe/texture.ts231+
Mrender/hover.ts146+
Mrender/index.ts29+
Mrender/keys.ts340+
Mrender/log.ts104+
Mrender/package.json25+
Mrender/scene/observe.ts107+
Mrender/scene/read.ts186+
Mrender/test/atlasloc.test.ts110+
Mrender/test/element.test.ts285+
Mrender/test/fixtures.ts71+
Mrender/test/globe.test.ts684+
Mrender/test/grid.test.ts498+
Mrender/test/hover.test.ts276+
Mrender/test/keys.test.ts411+
Mrender/test/lifecycle.test.ts480+
Mrender/test/markers.test.ts385+
Mrender/test/models.ts182+
Mrender/test/pick.test.ts436+
Mrender/test/pins.test.ts311+
Mrender/test/projection.test.ts113+
Mrender/test/pyramid.test.ts117+
Mrender/test/raster.test.ts175+
Mrender/test/report.test.ts144+
Mrender/test/resilience.test.ts473+
Mrender/test/scene.test.ts124+
Mrender/test/styles.test.ts633+
Mrender/test/texture.test.ts190+
Mrender/test/viewport.test.ts502+
Mrender/test/visibility.test.ts170+
Mrender/tools/lines.mjs84+
Mrender/tsconfig.json24+
Mrender/viewport.ts697+
Mrender/world/model.ts265+
Mrender/world/visibility.ts241+
Mroutes.go110-
Mroutes_test.go629-
Mspec/gen/gen_test.go187+
Mspec/gen/main.go507+
Mspec/registry.yaml434+
Mstatic/README.md29+
Mtest-results/.last-run.json4+
Mtestdata/corpus/README.md32+
Mtestdata/corpus/bundles/bend-or/icons.json8+
Mtestdata/corpus/bundles/bend-or/manifest.json34+
Mtestdata/corpus/bundles/bend-or/tiles/2026-08-02.json2333+
Mtestdata/corpus/bundles/bend-or/volume.json87+
Mtestdata/corpus/bundles/bend-or/worlds/2026-08-02.locations.json75+
Mtestdata/corpus/bundles/bend-or/worlds/2026-08-02.payload.json353185++++++++++++++++++++++++++++++
Mtestdata/corpus/bundles/bend-or/worlds/2026-08-02.text.json721+
Mtestdata/corpus/bundles/mars/icons.json21+
Mtestdata/corpus/bundles/mars/manifest.json35+
Mtestdata/corpus/bundles/mars/tiles/global__mola-elevation.json2748+
Mtestdata/corpus/bundles/mars/tiles/global__viking-mdim-2-1.json2748+
Mtestdata/corpus/bundles/mars/volume.json102+
Mtestdata/corpus/bundles/mars/worlds/global.locations.json2058+
Mtestdata/corpus/bundles/mars/worlds/global.payload.json614+
Mtestdata/corpus/bundles/mars/worlds/global.text.json14338+
Mtestdata/corpus/translators/nasa-trek.doc.json23095++
Mtestdata/corpus/translators/nasa-trek.fixture.json33+
Mtests/cells/conformance_test.go594+
Mtests/corpus/corpus.go127+
Mtests/e2e/app.spec.ts131+
Mtests/e2e/playwright.config.ts36+
Mtests/e2e/prep/main.go51+
Mtests/island/display_test.go618+
Mtests/island/harness_test.go354+
Mtests/island/island_test.go442+
Mtools/corpussmoke/main.go152+
Mtools/crawl/api.go335-
Mtools/crawl/arcgis.go591-
Mtools/crawl/archive.go392-
Mtools/crawl/axis.go135-
Mtools/crawl/ign.go501-
Mtools/crawl/main.go634-
Mtools/crawl/main_test.go303-
Mtools/crawl/mapserver.go193-
Mtools/crawl/mapserver_test.go120-
Mtools/crawl/pb.go580-
Mtools/crawl/trek.go476-
Mtools/crawl/window.go192-
Mtools/depcheck/hostenv.go77+
Mtools/depcheck/imports.go67+
Mtools/depcheck/laneimports.go151+
Mtools/depcheck/main.go123+
Mtools/depcheck/netconfine.go122+
Mtools/depcheck/report.go131+
Mtools/depcheck/rules_test.go210+
Mtools/depcheck/scope.go139+
Mtools/depcheck/semconvlit.go79+
Mtools/generate/bundles.go344-
Mtools/generate/main.go931-
Mtools/generate/main_test.go272-
Mtools/generate/merge.go940-
Mtools/generate/merge_test.go212-
Mtools/generate/normalize.go266-
Mtools/generate/normalize_test.go203-
Mtools/generate/shard.go697-
Mtools/generate/split.go176-
Mtools/generate/split_test.go91-
Mtools/icons/png.mjs200-
Mtools/icons/render-icons.mjs378-
Mtools/maturity/main.go119-
Mtools/testgate/main.go98+
Mtools/tiles/main.go1273-
Mtools/tiles/main_test.go105-
Mtools/tiles/stamp.go146-
Mtools/tiles/stamp_test.go157-
Mtools/tiles/warp.go420-
M .gitattributes +24 -0

Diff not shown: this commit changes too many lines.

M .github/workflows/ci.yml +89 -120

Diff not shown: this commit changes too many lines.

M .github/workflows/release.yml +240 -0

Diff not shown: this commit changes too many lines.

M .github/workflows/snapshot.yml +36 -16

Diff not shown: this commit changes too many lines.

M .github/workflows/snapshots.yml +9 -2

Diff not shown: this commit changes too many lines.

M .gitignore +35 -12

Diff not shown: this commit changes too many lines.

M Makefile +118 -0

Diff not shown: this commit changes too many lines.

M README.md +83 -228

Diff not shown: this commit changes too many lines.

M analysis/cellsystems/contract.ts +157 -0

Diff not shown: this commit changes too many lines.

M analysis/cellsystems/geohash.ts +638 -0

Diff not shown: this commit changes too many lines.

M analysis/cellsystems/ground.ts +94 -0

Diff not shown: this commit changes too many lines.

M analysis/cellsystems/index.ts +74 -0

Diff not shown: this commit changes too many lines.

M analysis/cellsystems/plan.ts +115 -0

Diff not shown: this commit changes too many lines.

M analysis/cellsystems/registry.ts +113 -0

Diff not shown: this commit changes too many lines.

M analysis/cellsystems/ring.ts +140 -0

Diff not shown: this commit changes too many lines.

M analysis/cellsystems/s2.ts +323 -0

Diff not shown: this commit changes too many lines.

M analysis/cellsystems/tokens.ts +77 -0

Diff not shown: this commit changes too many lines.

M analysis/cellsystems/visual.ts +115 -0

Diff not shown: this commit changes too many lines.

M analysis/index.ts +12 -0

Diff not shown: this commit changes too many lines.

M analysis/package.json +15 -0

Diff not shown: this commit changes too many lines.

M analysis/semconv/geometry.ts +197 -0

Diff not shown: this commit changes too many lines.

M analysis/semconv/keys.ts +126 -0

Diff not shown: this commit changes too many lines.

M analysis/test/conformance.test.ts +107 -0

Diff not shown: this commit changes too many lines.

M analysis/test/conformance.ts +445 -0

Diff not shown: this commit changes too many lines.

M analysis/test/districts.ts +190 -0

Diff not shown: this commit changes too many lines.

M analysis/test/geohash.test.ts +238 -0

Diff not shown: this commit changes too many lines.

M analysis/test/ground.test.ts +114 -0

Diff not shown: this commit changes too many lines.

M analysis/test/grounds.ts +116 -0

Diff not shown: this commit changes too many lines.

M analysis/test/plan.test.ts +110 -0

Diff not shown: this commit changes too many lines.

M analysis/test/properties.test.ts +370 -0

Diff not shown: this commit changes too many lines.

M analysis/test/registry.test.ts +72 -0

Diff not shown: this commit changes too many lines.

M analysis/test/ring.test.ts +91 -0

Diff not shown: this commit changes too many lines.

M analysis/test/s2-limits.test.ts +60 -0

Diff not shown: this commit changes too many lines.

M analysis/test/s2.test.ts +122 -0

Diff not shown: this commit changes too many lines.

M analysis/test/vectors.test.ts +144 -0

Diff not shown: this commit changes too many lines.

M analysis/test/visual.test.ts +92 -0

Diff not shown: this commit changes too many lines.

M analysis/testdata/cells/carry.json +94 -0

Diff not shown: this commit changes too many lines.

M analysis/testdata/cells/containment.json +694 -0

Diff not shown: this commit changes too many lines.

M analysis/testdata/cells/continuity.json +899 -0

Diff not shown: this commit changes too many lines.

M analysis/testdata/cells/geometry.json +529 -0

Diff not shown: this commit changes too many lines.

M analysis/testdata/cells/grounds.json +197 -0

Diff not shown: this commit changes too many lines.

M analysis/testdata/cells/hierarchy.json +279 -0

Diff not shown: this commit changes too many lines.

M analysis/testdata/cells/identity.json +835 -0

Diff not shown: this commit changes too many lines.

M analysis/testdata/cells/input.json +175 -0

Diff not shown: this commit changes too many lines.

M analysis/testdata/cells/surface.json +193 -0

Diff not shown: this commit changes too many lines.

M analysis/tsconfig.json +26 -0

Diff not shown: this commit changes too many lines.

M assets/app.css +0 -1

Diff not shown: this commit changes too many lines.

M assets/app.js +0 -5290

Diff not shown: this commit changes too many lines.

M assets/index.html +0 -191

Diff not shown: this commit changes too many lines.

M cmd/atlas/adapt.go +247 -0

Diff not shown: this commit changes too many lines.

M cmd/atlas/adapt_test.go +175 -0

Diff not shown: this commit changes too many lines.

M cmd/atlas/compose.go +144 -0

Diff not shown: this commit changes too many lines.

M cmd/atlas/crawl.go +102 -0

Diff not shown: this commit changes too many lines.

M cmd/atlas/dev.go +252 -0

Diff not shown: this commit changes too many lines.

M cmd/atlas/enrich.go +315 -0

Diff not shown: this commit changes too many lines.

M cmd/atlas/main.go +116 -0

Diff not shown: this commit changes too many lines.

M cmd/atlas/main_test.go +78 -0

Diff not shown: this commit changes too many lines.

M cmd/atlas/measure.go +167 -0

Diff not shown: this commit changes too many lines.

M cmd/atlas/pipeline_test.go +704 -0

Diff not shown: this commit changes too many lines.

M cmd/atlas/serve.go +145 -0

Diff not shown: this commit changes too many lines.

M cmd/atlas/serve_test.go +59 -0

Diff not shown: this commit changes too many lines.

M cmd/atlas/tiles.go +194 -0

Diff not shown: this commit changes too many lines.

M cmd/atlas/translate.go +103 -0

Diff not shown: this commit changes too many lines.

M cmd/atlas/warp.go +182 -0

Diff not shown: this commit changes too many lines.

M cmd/atlas/warp_test.go +119 -0

Diff not shown: this commit changes too many lines.

M cmd/atlas/workbench.go +168 -0

Diff not shown: this commit changes too many lines.

M cmd/cartograph/diff.go +0 -235

Diff not shown: this commit changes too many lines.

M cmd/cartograph/diff_test.go +0 -135

Diff not shown: this commit changes too many lines.

M cmd/cartograph/library.go +0 -127

Diff not shown: this commit changes too many lines.

M cmd/cartograph/main.go +0 -81

Diff not shown: this commit changes too many lines.

M cmd/cartograph/measure.go +0 -23

Diff not shown: this commit changes too many lines.

M cmd/cartograph/measure_test.go +0 -226

Diff not shown: this commit changes too many lines.

M cmd/cartograph/ops.go +0 -93

Diff not shown: this commit changes too many lines.

M cmd/cartograph/ops_test.go +0 -161

Diff not shown: this commit changes too many lines.

M cmd/cartograph/server.go +0 -196

Diff not shown: this commit changes too many lines.

M cmd/cartograph/server_test.go +0 -102

Diff not shown: this commit changes too many lines.

M cmd/cartograph/sources.go +0 -209

Diff not shown: this commit changes too many lines.

M cmd/cartograph/sources_test.go +0 -89

Diff not shown: this commit changes too many lines.

M cmd/cartograph/ui/app.js +0 -31

Diff not shown: this commit changes too many lines.

M cmd/cartograph/ui/collection.tmpl +0 -35

Diff not shown: this commit changes too many lines.

M cmd/cartograph/ui/diff.tmpl +0 -58

Diff not shown: this commit changes too many lines.

M cmd/cartograph/ui/layout.tmpl +0 -23

Diff not shown: this commit changes too many lines.

M cmd/cartograph/ui/sources.tmpl +0 -44

Diff not shown: this commit changes too many lines.

M cmd/cartograph/ui/style.css +0 -268

Diff not shown: this commit changes too many lines.

M cmd/cartograph/ui/volume.tmpl +0 -63

Diff not shown: this commit changes too many lines.

M dialog.go +0 -39

Diff not shown: this commit changes too many lines.

M docs/README.md +32 -0

Diff not shown: this commit changes too many lines.

M docs/analysis.md +487 -0

Diff not shown: this commit changes too many lines.

M docs/app.md +786 -0

Diff not shown: this commit changes too many lines.

M docs/decisions/0001-htmx-4-at-the-center.md +51 -0

Diff not shown: this commit changes too many lines.

M docs/decisions/0002-allons-is-removed.md +47 -0

Diff not shown: this commit changes too many lines.

M docs/decisions/0003-merge-moves-to-enrich.md +42 -0

Diff not shown: this commit changes too many lines.

M docs/decisions/0004-source-neutral-interchange-document.md +46 -0

Diff not shown: this commit changes too many lines.

M docs/decisions/0005-unbounded-feature-maturity.md +43 -0

Diff not shown: this commit changes too many lines.

M docs/decisions/0006-the-scene-description-seam.md +50 -0

Diff not shown: this commit changes too many lines.

M docs/decisions/0007-hostenv-portability.md +50 -0

Diff not shown: this commit changes too many lines.

M docs/decisions/0008-stamp-identity-is-an-aspiration.md +55 -0

Diff not shown: this commit changes too many lines.

M docs/decisions/0009-the-enrich-compose-adapter-seam.md +55 -0

Diff not shown: this commit changes too many lines.

M docs/decisions/0010-revision-packing.md +57 -0

Diff not shown: this commit changes too many lines.

M docs/decisions/0011-one-event-stream.md +54 -0

Diff not shown: this commit changes too many lines.

M docs/decisions/0012-scan-at-launch-never-watch.md +44 -0

Diff not shown: this commit changes too many lines.

M docs/decisions/0013-assets-and-static-are-two-mounts.md +45 -0

Diff not shown: this commit changes too many lines.

M docs/decisions/0014-the-depcheck-pragma.md +48 -0

Diff not shown: this commit changes too many lines.

M docs/decisions/0015-the-ground-descriptor.md +57 -0

Diff not shown: this commit changes too many lines.

M docs/decisions/0016-uncurated-captures-are-passed-over.md +50 -0

Diff not shown: this commit changes too many lines.

M docs/decisions/0017-the-host-follows-redirects.md +64 -0

Diff not shown: this commit changes too many lines.

M docs/decisions/0018-divergences-from-the-reference.md +77 -0

Diff not shown: this commit changes too many lines.

M docs/decisions/README.md +42 -0

Diff not shown: this commit changes too many lines.

M docs/enrich.md +600 -0

Diff not shown: this commit changes too many lines.

M docs/format.md +704 -0

Diff not shown: this commit changes too many lines.

M docs/generate.md +1124 -0

Diff not shown: this commit changes too many lines.

M docs/logging.md +112 -0

Diff not shown: this commit changes too many lines.

M docs/render-seam.md +645 -0

Diff not shown: this commit changes too many lines.

M {internal => docs}/semconv/REGISTRY.md +50 -28

Diff not shown: this commit changes too many lines.

M docs/snapshots.md +25 -7

Diff not shown: this commit changes too many lines.

M docs/stamps.md +91 -0

Diff not shown: this commit changes too many lines.

M docs/testing.md +87 -0

Diff not shown: this commit changes too many lines.

M docs/workbench.md +234 -0

Diff not shown: this commit changes too many lines.

M eslint.config.mjs +227 -0

Diff not shown: this commit changes too many lines.

M format/bundle/bundle.go +233 -0

Diff not shown: this commit changes too many lines.

M format/bundle/bundle_test.go +326 -0

Diff not shown: this commit changes too many lines.

M format/bundle/codec.go +251 -0

Diff not shown: this commit changes too many lines.

M format/bundle/codec_test.go +226 -0

Diff not shown: this commit changes too many lines.

M format/bundle/corpus_test.go +679 -0

Diff not shown: this commit changes too many lines.

M format/bundle/fixture_test.go +156 -0

Diff not shown: this commit changes too many lines.

M format/bundle/fuzz_test.go +202 -0

Diff not shown: this commit changes too many lines.

M format/bundle/read.go +206 -0

Diff not shown: this commit changes too many lines.

M format/bundle/registry.go +219 -0

Diff not shown: this commit changes too many lines.

M format/bundle/registry_test.go +315 -0

Diff not shown: this commit changes too many lines.

M format/bundle/scan.go +186 -0

Diff not shown: this commit changes too many lines.

M format/bundle/stamp.go +113 -0

Diff not shown: this commit changes too many lines.

M format/bundle/stamp_test.go +188 -0

Diff not shown: this commit changes too many lines.

M format/bundle/validate.go +320 -0

Diff not shown: this commit changes too many lines.

M format/bundle/validate_test.go +354 -0

Diff not shown: this commit changes too many lines.

M {internal => format}/bundle/write.go +47 -19

Diff not shown: this commit changes too many lines.

M format/semconv/behavior.go +134 -0

Diff not shown: this commit changes too many lines.

M format/semconv/registry.go +172 -0

Diff not shown: this commit changes too many lines.

M format/semconv/registry_gen.go +178 -0

Diff not shown: this commit changes too many lines.

M format/semconv/semconv.go +43 -0

Diff not shown: this commit changes too many lines.

M format/semconv/semconv_test.go +266 -0

Diff not shown: this commit changes too many lines.

M frontend/app.css +0 -33

Diff not shown: this commit changes too many lines.

M frontend/app.js +0 -8

Diff not shown: this commit changes too many lines.

M frontend/build.mjs +0 -15

Diff not shown: this commit changes too many lines.

M frontend/mockup/identity.html +0 -566

Diff not shown: this commit changes too many lines.

M frontend/package-lock.json +0 -1242

Diff not shown: this commit changes too many lines.

M frontend/package.json +0 -19

Diff not shown: this commit changes too many lines.

M frontend/parity/compare.mjs +0 -82

Diff not shown: this commit changes too many lines.

M frontend/parity/run.mjs +0 -135

Diff not shown: this commit changes too many lines.

M frontend/parity/tour.js +0 -443

Diff not shown: this commit changes too many lines.

M frontend/src/areas.js +0 -320

Diff not shown: this commit changes too many lines.

M frontend/src/boot.js +0 -49

Diff not shown: this commit changes too many lines.

M frontend/src/catalog.js +0 -96

Diff not shown: this commit changes too many lines.

M frontend/src/cellsystems/geohash.js +0 -181

Diff not shown: this commit changes too many lines.

M frontend/src/cellsystems/index.js +0 -144

Diff not shown: this commit changes too many lines.

M frontend/src/cellsystems/s2.js +0 -243

Diff not shown: this commit changes too many lines.

M frontend/src/collections.js +0 -71

Diff not shown: this commit changes too many lines.

M frontend/src/constants.js +0 -63

Diff not shown: this commit changes too many lines.

M frontend/src/detail.js +0 -357

Diff not shown: this commit changes too many lines.

M frontend/src/diagnostics.js +0 -111

Diff not shown: this commit changes too many lines.

M frontend/src/dom.js +0 -66

Diff not shown: this commit changes too many lines.

M frontend/src/engine.js +0 -276

Diff not shown: this commit changes too many lines.

M frontend/src/events.js +0 -352

Diff not shown: this commit changes too many lines.

M frontend/src/features.js +0 -179

Diff not shown: this commit changes too many lines.

M frontend/src/globe.js +0 -1045

Diff not shown: this commit changes too many lines.

M frontend/src/grid.js +0 -377

Diff not shown: this commit changes too many lines.

M frontend/src/legend.js +0 -478

Diff not shown: this commit changes too many lines.

M frontend/src/library.js +0 -98

Diff not shown: this commit changes too many lines.

M frontend/src/navigation.js +0 -430

Diff not shown: this commit changes too many lines.

M frontend/src/overview.js +0 -172

Diff not shown: this commit changes too many lines.

M frontend/src/search.js +0 -155

Diff not shown: this commit changes too many lines.

M frontend/src/semconv.js +0 -158

Diff not shown: this commit changes too many lines.

M frontend/src/session.js +0 -64

Diff not shown: this commit changes too many lines.

M frontend/src/state.js +0 -72

Diff not shown: this commit changes too many lines.

M frontend/src/styles.js +0 -522

Diff not shown: this commit changes too many lines.

M frontend/src/theme.js +0 -98

Diff not shown: this commit changes too many lines.

M frontend/src/util.js +0 -25

Diff not shown: this commit changes too many lines.

M frontend/src/visibility.js +0 -56

Diff not shown: this commit changes too many lines.

M frontend/test/cellsystems.test.mjs +0 -219

Diff not shown: this commit changes too many lines.

M frontend/test/collections.test.mjs +0 -88

Diff not shown: this commit changes too many lines.

M frontend/test/semconv.test.mjs +0 -70

Diff not shown: this commit changes too many lines.

M go.mod +8 -13

Diff not shown: this commit changes too many lines.

M go.sum +15 -50

Diff not shown: this commit changes too many lines.

M internal/app/app.go +143 -0

Diff not shown: this commit changes too many lines.

M internal/app/app_test.go +1492 -0

Diff not shown: this commit changes too many lines.

M internal/app/assets/assets.go +176 -0

Diff not shown: this commit changes too many lines.

M {frontend => internal/app/assets}/css/base.css +0 -0

Diff not shown: this commit changes too many lines.

M {frontend => internal/app/assets}/css/brand.css +0 -0

Diff not shown: this commit changes too many lines.

M {frontend => internal/app/assets}/css/category-rows.css +0 -0

Diff not shown: this commit changes too many lines.

M internal/app/assets/css/chrome.css +183 -0

Diff not shown: this commit changes too many lines.

M {frontend => internal/app/assets}/css/dock.css +0 -0

Diff not shown: this commit changes too many lines.

M {frontend => internal/app/assets}/css/empty-state.css +0 -0

Diff not shown: this commit changes too many lines.

M {frontend => internal/app/assets}/css/fatal-error.css +0 -0

Diff not shown: this commit changes too many lines.

M {frontend => internal/app/assets}/css/feature-index.css +0 -0

Diff not shown: this commit changes too many lines.

M {frontend => internal/app/assets}/css/globe.css +0 -0

Diff not shown: this commit changes too many lines.

M {frontend => internal/app/assets}/css/grid-navigator.css +10 -8

Diff not shown: this commit changes too many lines.

M {frontend => internal/app/assets}/css/icon-button.css +0 -0

Diff not shown: this commit changes too many lines.

M {frontend => internal/app/assets}/css/layer-header.css +0 -0

Diff not shown: this commit changes too many lines.

M {frontend => internal/app/assets}/css/layer-switch.css +0 -0

Diff not shown: this commit changes too many lines.

M {frontend => internal/app/assets}/css/layers.css +0 -0

Diff not shown: this commit changes too many lines.

M {frontend => internal/app/assets}/css/legend-toolbar.css +0 -0

Diff not shown: this commit changes too many lines.

M {frontend => internal/app/assets}/css/map-corner.css +0 -0

Diff not shown: this commit changes too many lines.

M {frontend => internal/app/assets}/css/map-hint.css +0 -0

Diff not shown: this commit changes too many lines.

M {frontend => internal/app/assets}/css/map-loading.css +0 -0

Diff not shown: this commit changes too many lines.

M {frontend => internal/app/assets}/css/map-panel.css +0 -0

Diff not shown: this commit changes too many lines.

M {frontend => internal/app/assets}/css/map-pickers.css +4 -2

Diff not shown: this commit changes too many lines.

M {frontend => internal/app/assets}/css/overview.css +0 -0

Diff not shown: this commit changes too many lines.

M {frontend => internal/app/assets}/css/pin-detail.css +0 -0

Diff not shown: this commit changes too many lines.

M {frontend => internal/app/assets}/css/responsive.css +0 -0

Diff not shown: this commit changes too many lines.

M {frontend => internal/app/assets}/css/search.css +0 -0

Diff not shown: this commit changes too many lines.

M {frontend => internal/app/assets}/css/shell.css +0 -0

Diff not shown: this commit changes too many lines.

M {frontend => internal/app/assets}/css/sidebar-footer.css +0 -0

Diff not shown: this commit changes too many lines.

M {frontend => internal/app/assets}/css/sidebar.css +0 -0

Diff not shown: this commit changes too many lines.

M {frontend => internal/app/assets}/css/solo-chip.css +0 -0

Diff not shown: this commit changes too many lines.

M {frontend => internal/app/assets}/css/text-symbol.css +0 -0

Diff not shown: this commit changes too many lines.

M {frontend => internal/app/assets}/css/tokens.css +0 -0

Diff not shown: this commit changes too many lines.

M {frontend => internal/app/assets}/css/tooltip.css +0 -0

Diff not shown: this commit changes too many lines.

M {frontend => internal/app/assets}/css/topbar-collapse.css +0 -0

Diff not shown: this commit changes too many lines.

M {frontend => internal/app/assets}/css/topbar.css +0 -0

Diff not shown: this commit changes too many lines.

M {frontend => internal/app/assets}/css/zoom-controls.css +0 -0

Diff not shown: this commit changes too many lines.

M internal/app/assets/htmx.LICENSE +13 -0

Diff not shown: this commit changes too many lines.

M internal/app/assets/htmx.min.js +1 -0

Diff not shown: this commit changes too many lines.

M internal/app/assets/hx-sse.min.js +1 -0

Diff not shown: this commit changes too many lines.

M internal/app/cells/carry.go +194 -0

Diff not shown: this commit changes too many lines.

M internal/app/cells/cells.go +306 -0

Diff not shown: this commit changes too many lines.

M internal/app/cells/mercator.go +256 -0

Diff not shown: this commit changes too many lines.

M internal/app/cells/predicate_test.go +410 -0

Diff not shown: this commit changes too many lines.

M internal/app/cells/s2.go +326 -0

Diff not shown: this commit changes too many lines.

M internal/app/chrome.go +50 -0

Diff not shown: this commit changes too many lines.

M internal/app/collate.go +141 -0

Diff not shown: this commit changes too many lines.

M internal/app/collate_test.go +149 -0

Diff not shown: this commit changes too many lines.

M internal/app/data.go +163 -0

Diff not shown: this commit changes too many lines.

M internal/app/events.go +210 -0

Diff not shown: this commit changes too many lines.

M internal/app/events_test.go +343 -0

Diff not shown: this commit changes too many lines.

M internal/app/filter.go +309 -0

Diff not shown: this commit changes too many lines.

M internal/app/grid.go +271 -0

Diff not shown: this commit changes too many lines.

M internal/app/grid_test.go +602 -0

Diff not shown: this commit changes too many lines.

M internal/app/hostenv/hostenv.go +177 -0

Diff not shown: this commit changes too many lines.

M internal/app/hostenv/hostenv_test.go +96 -0

Diff not shown: this commit changes too many lines.

M internal/app/hostenv/memory.go +77 -0

Diff not shown: this commit changes too many lines.

M internal/app/hostenv/oshost/oshost.go +78 -0

Diff not shown: this commit changes too many lines.

M internal/app/hostenv/oshost/oshost_test.go +294 -0

Diff not shown: this commit changes too many lines.

M internal/app/hostenv/oshost/sessions.go +122 -0

Diff not shown: this commit changes too many lines.

M internal/app/hostenv/oshost/volumes.go +243 -0

Diff not shown: this commit changes too many lines.

M internal/app/hostenv/wailshost/wailshost.go +83 -0

Diff not shown: this commit changes too many lines.

M internal/app/importing.go +133 -0

Diff not shown: this commit changes too many lines.

M internal/app/island.go +145 -0

Diff not shown: this commit changes too many lines.

M internal/app/legend.go +710 -0

Diff not shown: this commit changes too many lines.

M internal/app/legend_test.go +95 -0

Diff not shown: this commit changes too many lines.

M internal/app/pages.go +199 -0

Diff not shown: this commit changes too many lines.

M internal/app/partials.go +147 -0

Diff not shown: this commit changes too many lines.

M internal/app/session.go +1051 -0

Diff not shown: this commit changes too many lines.

M internal/app/templates/detail.tmpl +46 -0

Diff not shown: this commit changes too many lines.

M internal/app/templates/dock.tmpl +36 -0

Diff not shown: this commit changes too many lines.

M internal/app/templates/empty-state.tmpl +37 -0

Diff not shown: this commit changes too many lines.

M internal/app/templates/grid-navigator.tmpl +58 -0

Diff not shown: this commit changes too many lines.

M internal/app/templates/import.tmpl +23 -0

Diff not shown: this commit changes too many lines.

M internal/app/templates/island.tmpl +18 -0

Diff not shown: this commit changes too many lines.

M internal/app/templates/legend.tmpl +198 -0

Diff not shown: this commit changes too many lines.

M internal/app/templates/overview.tmpl +23 -0

Diff not shown: this commit changes too many lines.

M internal/app/templates/shell.tmpl +334 -0

Diff not shown: this commit changes too many lines.

M internal/app/templates/templates.go +114 -0

Diff not shown: this commit changes too many lines.

M internal/app/templates/topbar.tmpl +76 -0

Diff not shown: this commit changes too many lines.

M internal/app/templates/viewport.tmpl +61 -0

Diff not shown: this commit changes too many lines.

M internal/app/view.go +582 -0

Diff not shown: this commit changes too many lines.

M internal/app/world.go +660 -0

Diff not shown: this commit changes too many lines.

M internal/arcgismap/arcgismap.go +0 -1005

Diff not shown: this commit changes too many lines.

M internal/arcgismap/arcgismap_test.go +0 -600

Diff not shown: this commit changes too many lines.

M internal/arcgismap/clip.go +0 -203

Diff not shown: this commit changes too many lines.

M internal/arcgismap/clip_test.go +0 -144

Diff not shown: this commit changes too many lines.

M internal/arcgismap/hydro_test.go +0 -106

Diff not shown: this commit changes too many lines.

M internal/arcgismap/national.go +0 -115

Diff not shown: this commit changes too many lines.

M internal/basemap/basemap.go +0 -283

Diff not shown: this commit changes too many lines.

M internal/basemap/basemap_test.go +0 -153

Diff not shown: this commit changes too many lines.

M internal/basemap/clip.go +0 -137

Diff not shown: this commit changes too many lines.

M internal/basemap/stroke.go +0 -93

Diff not shown: this commit changes too many lines.

M internal/basemap/style.go +0 -56

Diff not shown: this commit changes too many lines.

M internal/blend/blend_test.go +0 -134

Diff not shown: this commit changes too many lines.

M internal/bundle/bundle.go +0 -174

Diff not shown: this commit changes too many lines.

M internal/bundle/bundle_test.go +0 -388

Diff not shown: this commit changes too many lines.

M internal/bundle/bundletest/bundletest.go +0 -169

Diff not shown: this commit changes too many lines.

M internal/bundle/dir.go +0 -24

Diff not shown: this commit changes too many lines.

M internal/bundle/locations.go +0 -147

Diff not shown: this commit changes too many lines.

M internal/bundle/read.go +0 -368

Diff not shown: this commit changes too many lines.

M internal/bundle/registry.go +0 -361

Diff not shown: this commit changes too many lines.

M internal/bundle/registry_test.go +0 -280

Diff not shown: this commit changes too many lines.

M internal/bundle/stamp.go +0 -85

Diff not shown: this commit changes too many lines.

M internal/{blend/blend.go => enrich/align/align.go} +73 -53

Diff not shown: this commit changes too many lines.

M internal/enrich/align/align_test.go +201 -0

Diff not shown: this commit changes too many lines.

M internal/enrich/contribution.go +364 -0

Diff not shown: this commit changes too many lines.

M internal/enrich/curation/curation.go +137 -0

Diff not shown: this commit changes too many lines.

M internal/enrich/curation/curation.json +44 -0

Diff not shown: this commit changes too many lines.

M internal/enrich/curation/curation_test.go +107 -0

Diff not shown: this commit changes too many lines.

M internal/enrich/enrich.go +268 -0

Diff not shown: this commit changes too many lines.

M internal/enrich/enrich_test.go +609 -0

Diff not shown: this commit changes too many lines.

M internal/enrich/enrichers/enrichers.go +26 -0

Diff not shown: this commit changes too many lines.

M internal/enrich/enrichers/lenses/lenses.go +93 -0

Diff not shown: this commit changes too many lines.

M internal/enrich/enrichers/lenses/lenses_test.go +138 -0

Diff not shown: this commit changes too many lines.

M internal/enrich/enrichers/merge/merge.go +716 -0

Diff not shown: this commit changes too many lines.

M internal/enrich/enrichers/merge/merge_test.go +518 -0

Diff not shown: this commit changes too many lines.

M internal/enrich/enrichers/national/national.go +324 -0

Diff not shown: this commit changes too many lines.

M internal/enrich/enrichers/national/national_test.go +319 -0

Diff not shown: this commit changes too many lines.

M internal/{icons => enrich/enrichers/stdicons}/standard/maki/LICENSE.txt +0 -0

Diff not shown: this commit changes too many lines.

M internal/{icons => enrich/enrichers/stdicons}/standard/maki/beach.svg +0 -0

Diff not shown: this commit changes too many lines.

M internal/{icons => enrich/enrichers/stdicons}/standard/maki/caution.svg +0 -0

Diff not shown: this commit changes too many lines.

M internal/{icons => enrich/enrichers/stdicons}/standard/maki/circle-stroked.svg +0 -0

Diff not shown: this commit changes too many lines.

M internal/{icons => enrich/enrichers/stdicons}/standard/maki/circle.svg +0 -0

Diff not shown: this commit changes too many lines.

M internal/{icons => enrich/enrichers/stdicons}/standard/maki/fire-station.svg +0 -0

Diff not shown: this commit changes too many lines.

M internal/{icons => enrich/enrichers/stdicons}/standard/maki/hospital.svg +0 -0

Diff not shown: this commit changes too many lines.

M internal/{icons => enrich/enrichers/stdicons}/standard/maki/landuse.svg +0 -0

Diff not shown: this commit changes too many lines.

M internal/{icons => enrich/enrichers/stdicons}/standard/maki/monument.svg +0 -0

Diff not shown: this commit changes too many lines.

M internal/{icons => enrich/enrichers/stdicons}/standard/maki/mountain.svg +0 -0

Diff not shown: this commit changes too many lines.

M internal/{icons => enrich/enrichers/stdicons}/standard/maki/park.svg +0 -0

Diff not shown: this commit changes too many lines.

M internal/{icons => enrich/enrichers/stdicons}/standard/maki/parking.svg +0 -0

Diff not shown: this commit changes too many lines.

M internal/{icons => enrich/enrichers/stdicons}/standard/maki/post.svg +0 -0

Diff not shown: this commit changes too many lines.

M internal/{icons => enrich/enrichers/stdicons}/standard/maki/square-stroked.svg +0 -0

Diff not shown: this commit changes too many lines.

M internal/{icons => enrich/enrichers/stdicons}/standard/maki/square.svg +0 -0

Diff not shown: this commit changes too many lines.

M internal/{icons => enrich/enrichers/stdicons}/standard/maki/triangle-stroked.svg +0 -0

Diff not shown: this commit changes too many lines.

M internal/{icons => enrich/enrichers/stdicons}/standard/maki/triangle.svg +0 -0

Diff not shown: this commit changes too many lines.

M internal/{icons => enrich/enrichers/stdicons}/standard/maki/volcano.svg +0 -0

Diff not shown: this commit changes too many lines.

M internal/{icons => enrich/enrichers/stdicons}/standard/maki/water.svg +0 -0

Diff not shown: this commit changes too many lines.

M internal/{icons => enrich/enrichers/stdicons}/standard/maki/waterfall.svg +0 -0

Diff not shown: this commit changes too many lines.

M internal/{icons => enrich/enrichers/stdicons}/standard/maki/wetland.svg +0 -0

Diff not shown: this commit changes too many lines.

M internal/enrich/enrichers/stdicons/stdicons.go +148 -0

Diff not shown: this commit changes too many lines.

M internal/enrich/enrichers/stdicons/stdicons_test.go +156 -0

Diff not shown: this commit changes too many lines.

M internal/enrich/ledger.go +308 -0

Diff not shown: this commit changes too many lines.

M internal/enrich/maturity/gate.go +94 -0

Diff not shown: this commit changes too many lines.

M internal/enrich/maturity/maturity.go +623 -0

Diff not shown: this commit changes too many lines.

M internal/enrich/maturity/maturity_test.go +437 -0

Diff not shown: this commit changes too many lines.

M internal/enrich/maturity/points.json +46 -0

Diff not shown: this commit changes too many lines.

M internal/enrich/revision.go +82 -0

Diff not shown: this commit changes too many lines.

M internal/enrich/volume.go +481 -0

Diff not shown: this commit changes too many lines.

M internal/generate/archive/archive.go +310 -0

Diff not shown: this commit changes too many lines.

M internal/generate/archive/archive_test.go +188 -0

Diff not shown: this commit changes too many lines.

M internal/generate/archive/tiles.go +155 -0

Diff not shown: this commit changes too many lines.

M internal/generate/compose/compose.go +484 -0

Diff not shown: this commit changes too many lines.

M internal/generate/compose/compose_test.go +371 -0

Diff not shown: this commit changes too many lines.

M internal/generate/compose/conventions.go +95 -0

Diff not shown: this commit changes too many lines.

M internal/generate/compose/payload.go +290 -0

Diff not shown: this commit changes too many lines.

M internal/generate/compose/split.go +577 -0

Diff not shown: this commit changes too many lines.

M internal/generate/compose/split_test.go +267 -0

Diff not shown: this commit changes too many lines.

M internal/generate/compose/surface.go +171 -0

Diff not shown: this commit changes too many lines.

M internal/generate/compose/write.go +291 -0

Diff not shown: this commit changes too many lines.

M internal/generate/crawl/crawl.go +112 -0

Diff not shown: this commit changes too many lines.

M internal/generate/crawl/crawl_test.go +184 -0

Diff not shown: this commit changes too many lines.

M internal/generate/crawl/crawler.go +121 -0

Diff not shown: this commit changes too many lines.

M internal/generate/crawl/fetch.go +218 -0

Diff not shown: this commit changes too many lines.

M internal/generate/crawl/ign.go +464 -0

Diff not shown: this commit changes too many lines.

M internal/generate/crawl/write.go +438 -0

Diff not shown: this commit changes too many lines.

M internal/generate/curation/curation.go +222 -0

Diff not shown: this commit changes too many lines.

M internal/generate/curation/curation.json +76 -0

Diff not shown: this commit changes too many lines.

M internal/generate/curation/curation_test.go +156 -0

Diff not shown: this commit changes too many lines.

M internal/generate/doc/doc.go +500 -0

Diff not shown: this commit changes too many lines.

M internal/generate/doc/doc_test.go +166 -0

Diff not shown: this commit changes too many lines.

M internal/generate/doc/ids.go +53 -0

Diff not shown: this commit changes too many lines.

M internal/generate/doc/synthetic.go +96 -0

Diff not shown: this commit changes too many lines.

M internal/generate/icons/icons.go +56 -0

Diff not shown: this commit changes too many lines.

M internal/generate/icons/standard/maki/LICENSE.txt +116 -0

Diff not shown: this commit changes too many lines.

M internal/generate/icons/standard/maki/beach.svg +4 -0

Diff not shown: this commit changes too many lines.

M internal/generate/icons/standard/maki/caution.svg +4 -0

Diff not shown: this commit changes too many lines.

M internal/generate/icons/standard/maki/circle-stroked.svg +4 -0

Diff not shown: this commit changes too many lines.

M internal/generate/icons/standard/maki/circle.svg +4 -0

Diff not shown: this commit changes too many lines.

M internal/generate/icons/standard/maki/fire-station.svg +4 -0

Diff not shown: this commit changes too many lines.

M internal/generate/icons/standard/maki/hospital.svg +4 -0

Diff not shown: this commit changes too many lines.

M internal/generate/icons/standard/maki/landuse.svg +4 -0

Diff not shown: this commit changes too many lines.

M internal/generate/icons/standard/maki/monument.svg +4 -0

Diff not shown: this commit changes too many lines.

M internal/generate/icons/standard/maki/mountain.svg +4 -0

Diff not shown: this commit changes too many lines.

M internal/generate/icons/standard/maki/park.svg +4 -0

Diff not shown: this commit changes too many lines.

M internal/generate/icons/standard/maki/parking.svg +4 -0

Diff not shown: this commit changes too many lines.

M internal/generate/icons/standard/maki/post.svg +4 -0

Diff not shown: this commit changes too many lines.

M internal/generate/icons/standard/maki/square-stroked.svg +4 -0

Diff not shown: this commit changes too many lines.

M internal/generate/icons/standard/maki/square.svg +4 -0

Diff not shown: this commit changes too many lines.

M internal/generate/icons/standard/maki/triangle-stroked.svg +4 -0

Diff not shown: this commit changes too many lines.

M internal/generate/icons/standard/maki/triangle.svg +4 -0

Diff not shown: this commit changes too many lines.

M internal/generate/icons/standard/maki/volcano.svg +4 -0

Diff not shown: this commit changes too many lines.

M internal/generate/icons/standard/maki/water.svg +4 -0

Diff not shown: this commit changes too many lines.

M internal/generate/icons/standard/maki/waterfall.svg +4 -0

Diff not shown: this commit changes too many lines.

M internal/generate/icons/standard/maki/wetland.svg +4 -0

Diff not shown: this commit changes too many lines.

M internal/generate/sources/arcgishub/arcgishub.go +601 -0

Diff not shown: this commit changes too many lines.

M internal/generate/sources/arcgishub/arcgishub_test.go +336 -0

Diff not shown: this commit changes too many lines.

M internal/generate/sources/arcgishub/capture.go +77 -0

Diff not shown: this commit changes too many lines.

M internal/generate/sources/arcgishub/cities.go +291 -0

Diff not shown: this commit changes too many lines.

M internal/generate/sources/arcgishub/drawing.go +103 -0

Diff not shown: this commit changes too many lines.

M internal/{arcgismap => generate/sources/arcgishub}/hydro.go +49 -53

Diff not shown: this commit changes too many lines.

M internal/generate/sources/arcgishub/national.go +85 -0

Diff not shown: this commit changes too many lines.

M internal/generate/sources/arcgishub/testdata/city.json +100 -0

Diff not shown: this commit changes too many lines.

M internal/generate/sources/arcgishub/window.go +106 -0

Diff not shown: this commit changes too many lines.

M internal/generate/sources/ign/capture.go +72 -0

Diff not shown: this commit changes too many lines.

M internal/generate/sources/ign/ign.go +358 -0

Diff not shown: this commit changes too many lines.

M internal/generate/sources/ign/ign_test.go +288 -0

Diff not shown: this commit changes too many lines.

M internal/generate/sources/ign/testdata/wikimap.json +29 -0

Diff not shown: this commit changes too many lines.

M internal/generate/sources/mapgenie/capture.go +117 -0

Diff not shown: this commit changes too many lines.

M internal/generate/sources/mapgenie/mapgenie.go +432 -0

Diff not shown: this commit changes too many lines.

M internal/generate/sources/mapgenie/mapgenie_test.go +350 -0

Diff not shown: this commit changes too many lines.

M internal/generate/sources/nasatrek/capture.go +66 -0

Diff not shown: this commit changes too many lines.

M internal/generate/sources/nasatrek/nasatrek.go +459 -0

Diff not shown: this commit changes too many lines.

M internal/generate/sources/nasatrek/nasatrek_test.go +465 -0

Diff not shown: this commit changes too many lines.

M internal/generate/sources/piggyback/capture.go +118 -0

Diff not shown: this commit changes too many lines.

M internal/generate/sources/piggyback/piggyback.go +481 -0

Diff not shown: this commit changes too many lines.

M internal/generate/sources/piggyback/piggyback_test.go +188 -0

Diff not shown: this commit changes too many lines.

M internal/generate/sources/registry.go +29 -0

Diff not shown: this commit changes too many lines.

M internal/generate/sources/sources.go +86 -0

Diff not shown: this commit changes too many lines.

M internal/generate/sources/sources_test.go +56 -0

Diff not shown: this commit changes too many lines.

M internal/generate/tiles/basemap/basemap.go +279 -0

Diff not shown: this commit changes too many lines.

M internal/generate/tiles/basemap/basemap_test.go +236 -0

Diff not shown: this commit changes too many lines.

M internal/generate/tiles/basemap/clip.go +184 -0

Diff not shown: this commit changes too many lines.

M internal/generate/tiles/basemap/stroke.go +102 -0

Diff not shown: this commit changes too many lines.

M internal/generate/tiles/basemap/style.go +65 -0

Diff not shown: this commit changes too many lines.

M internal/generate/tiles/derive.go +477 -0

Diff not shown: this commit changes too many lines.

M internal/generate/tiles/draw.go +143 -0

Diff not shown: this commit changes too many lines.

M internal/generate/tiles/install.go +103 -0

Diff not shown: this commit changes too many lines.

M internal/generate/tiles/plan.go +409 -0

Diff not shown: this commit changes too many lines.

M internal/generate/tiles/stamp.go +132 -0

Diff not shown: this commit changes too many lines.

M internal/generate/tiles/tiles.go +256 -0

Diff not shown: this commit changes too many lines.

M internal/generate/tiles/tiles_test.go +136 -0

Diff not shown: this commit changes too many lines.

M internal/generate/tiles/warp.go +388 -0

Diff not shown: this commit changes too many lines.

M internal/icons/icons.go +0 -36

Diff not shown: this commit changes too many lines.

M internal/icons/icons_test.go +0 -27

Diff not shown: this commit changes too many lines.

M internal/ignmap/ignmap.go +0 -301

Diff not shown: this commit changes too many lines.

M internal/ignmap/ignmap_test.go +0 -245

Diff not shown: this commit changes too many lines.

M internal/logging/logging.go +178 -0

Diff not shown: this commit changes too many lines.

M internal/logging/logging_test.go +172 -0

Diff not shown: this commit changes too many lines.

M internal/measure/measure.go +0 -484

Diff not shown: this commit changes too many lines.

M internal/mgdoc/mgdoc.go +0 -227

Diff not shown: this commit changes too many lines.

M internal/mgdoc/mgdoc_test.go +0 -59

Diff not shown: this commit changes too many lines.

M internal/pbmap/pbmap.go +0 -457

Diff not shown: this commit changes too many lines.

M internal/pbmap/pbmap_test.go +0 -231

Diff not shown: this commit changes too many lines.

M internal/semconv/semconv.go +0 -428

Diff not shown: this commit changes too many lines.

M internal/semconv/semconv_test.go +0 -165

Diff not shown: this commit changes too many lines.

M internal/trekmap/trekmap.go +0 -470

Diff not shown: this commit changes too many lines.

M internal/trekmap/trekmap_test.go +0 -360

Diff not shown: this commit changes too many lines.

M internal/workbench/assets.go +58 -0

Diff not shown: this commit changes too many lines.

M internal/workbench/assets/tokens.css +37 -0

Diff not shown: this commit changes too many lines.

M internal/workbench/assets/workbench.css +248 -0

Diff not shown: this commit changes too many lines.

M internal/workbench/bundles_test.go +180 -0

Diff not shown: this commit changes too many lines.

M internal/workbench/diff.go +300 -0

Diff not shown: this commit changes too many lines.

M internal/workbench/library.go +214 -0

Diff not shown: this commit changes too many lines.

M internal/workbench/operations.go +205 -0

Diff not shown: this commit changes too many lines.

M internal/workbench/operations_test.go +278 -0

Diff not shown: this commit changes too many lines.

M internal/workbench/oprunner/oprunner.go +395 -0

Diff not shown: this commit changes too many lines.

M internal/workbench/oprunner/oprunner_test.go +431 -0

Diff not shown: this commit changes too many lines.

M internal/workbench/oprunner/serve.go +125 -0

Diff not shown: this commit changes too many lines.

M internal/workbench/sources.go +64 -0

Diff not shown: this commit changes too many lines.

M internal/workbench/templates/diff.tmpl +88 -0

Diff not shown: this commit changes too many lines.

M internal/workbench/templates/layout.tmpl +35 -0

Diff not shown: this commit changes too many lines.

M internal/workbench/templates/library.tmpl +48 -0

Diff not shown: this commit changes too many lines.

M internal/workbench/templates/op-row.tmpl +10 -0

Diff not shown: this commit changes too many lines.

M internal/workbench/templates/operations.tmpl +69 -0

Diff not shown: this commit changes too many lines.

M internal/workbench/templates/sources.tmpl +38 -0

Diff not shown: this commit changes too many lines.

M internal/workbench/templates/volume.tmpl +169 -0

Diff not shown: this commit changes too many lines.

M internal/workbench/workbench.go +398 -0

Diff not shown: this commit changes too many lines.

M internal/workbench/workbench_test.go +356 -0

Diff not shown: this commit changes too many lines.

M link_darwin.go +6 -0

Diff not shown: this commit changes too many lines.

M main.go +137 -87

Diff not shown: this commit changes too many lines.

M package-lock.json +3038 -0

Diff not shown: this commit changes too many lines.

M package.json +30 -0

Diff not shown: this commit changes too many lines.

M parity_dev.go +0 -115

Diff not shown: this commit changes too many lines.

M parity_release.go +0 -15

Diff not shown: this commit changes too many lines.

M redirects.go +123 -0

Diff not shown: this commit changes too many lines.

M render/boot.ts +78 -0

Diff not shown: this commit changes too many lines.

M render/build.mjs +37 -0

Diff not shown: this commit changes too many lines.

M render/chart/element.ts +1401 -0

Diff not shown: this commit changes too many lines.

M render/chart/grid.ts +140 -0

Diff not shown: this commit changes too many lines.

M render/chart/markers.ts +271 -0

Diff not shown: this commit changes too many lines.

M render/chart/overview.ts +231 -0

Diff not shown: this commit changes too many lines.

M render/chart/projection.ts +103 -0

Diff not shown: this commit changes too many lines.

M render/chart/raster.ts +174 -0

Diff not shown: this commit changes too many lines.

M render/chart/styles.ts +619 -0

Diff not shown: this commit changes too many lines.

M render/context.ts +51 -0

Diff not shown: this commit changes too many lines.

M render/data/atlasloc.ts +131 -0

Diff not shown: this commit changes too many lines.

M render/data/payload.ts +177 -0

Diff not shown: this commit changes too many lines.

M render/data/plane.ts +148 -0

Diff not shown: this commit changes too many lines.

M render/data/pyramid.ts +152 -0

Diff not shown: this commit changes too many lines.

M render/data/report.ts +114 -0

Diff not shown: this commit changes too many lines.

M render/diagnostics.ts +200 -0

Diff not shown: this commit changes too many lines.

M render/globe/element.ts +2002 -0

Diff not shown: this commit changes too many lines.

M render/globe/texture.ts +231 -0

Diff not shown: this commit changes too many lines.

M render/hover.ts +146 -0

Diff not shown: this commit changes too many lines.

M render/index.ts +29 -0

Diff not shown: this commit changes too many lines.

M render/keys.ts +340 -0

Diff not shown: this commit changes too many lines.

M render/log.ts +104 -0

Diff not shown: this commit changes too many lines.

M render/package.json +25 -0

Diff not shown: this commit changes too many lines.

M render/scene/observe.ts +107 -0

Diff not shown: this commit changes too many lines.

M render/scene/read.ts +186 -0

Diff not shown: this commit changes too many lines.

M render/test/atlasloc.test.ts +110 -0

Diff not shown: this commit changes too many lines.

M render/test/element.test.ts +285 -0

Diff not shown: this commit changes too many lines.

M render/test/fixtures.ts +71 -0

Diff not shown: this commit changes too many lines.

M render/test/globe.test.ts +684 -0

Diff not shown: this commit changes too many lines.

M render/test/grid.test.ts +498 -0

Diff not shown: this commit changes too many lines.

M render/test/hover.test.ts +276 -0

Diff not shown: this commit changes too many lines.

M render/test/keys.test.ts +411 -0

Diff not shown: this commit changes too many lines.

M render/test/lifecycle.test.ts +480 -0

Diff not shown: this commit changes too many lines.

M render/test/markers.test.ts +385 -0

Diff not shown: this commit changes too many lines.

M render/test/models.ts +182 -0

Diff not shown: this commit changes too many lines.

M render/test/pick.test.ts +436 -0

Diff not shown: this commit changes too many lines.

M render/test/pins.test.ts +311 -0

Diff not shown: this commit changes too many lines.

M render/test/projection.test.ts +113 -0

Diff not shown: this commit changes too many lines.

M render/test/pyramid.test.ts +117 -0

Diff not shown: this commit changes too many lines.

M render/test/raster.test.ts +175 -0

Diff not shown: this commit changes too many lines.

M render/test/report.test.ts +144 -0

Diff not shown: this commit changes too many lines.

M render/test/resilience.test.ts +473 -0

Diff not shown: this commit changes too many lines.

M render/test/scene.test.ts +124 -0

Diff not shown: this commit changes too many lines.

M render/test/styles.test.ts +633 -0

Diff not shown: this commit changes too many lines.

M render/test/texture.test.ts +190 -0

Diff not shown: this commit changes too many lines.

M render/test/viewport.test.ts +502 -0

Diff not shown: this commit changes too many lines.

M render/test/visibility.test.ts +170 -0

Diff not shown: this commit changes too many lines.

M render/tools/lines.mjs +84 -0

Diff not shown: this commit changes too many lines.

M render/tsconfig.json +24 -0

Diff not shown: this commit changes too many lines.

M render/viewport.ts +697 -0

Diff not shown: this commit changes too many lines.

M render/world/model.ts +265 -0

Diff not shown: this commit changes too many lines.

M render/world/visibility.ts +241 -0

Diff not shown: this commit changes too many lines.

M routes.go +0 -110

Diff not shown: this commit changes too many lines.

M routes_test.go +0 -629

Diff not shown: this commit changes too many lines.

M spec/gen/gen_test.go +187 -0

Diff not shown: this commit changes too many lines.

M spec/gen/main.go +507 -0

Diff not shown: this commit changes too many lines.

M spec/registry.yaml +434 -0

Diff not shown: this commit changes too many lines.

M static/README.md +29 -0

Diff not shown: this commit changes too many lines.

M test-results/.last-run.json +4 -0

Diff not shown: this commit changes too many lines.

M testdata/corpus/README.md +32 -0

Diff not shown: this commit changes too many lines.

M testdata/corpus/bundles/bend-or/icons.json +8 -0

Diff not shown: this commit changes too many lines.

M testdata/corpus/bundles/bend-or/manifest.json +34 -0

Diff not shown: this commit changes too many lines.

M testdata/corpus/bundles/bend-or/tiles/2026-08-02.json +2333 -0

Diff not shown: this commit changes too many lines.

M testdata/corpus/bundles/bend-or/volume.json +87 -0

Diff not shown: this commit changes too many lines.

M testdata/corpus/bundles/bend-or/worlds/2026-08-02.locations.json +75 -0

Diff not shown: this commit changes too many lines.

M testdata/corpus/bundles/bend-or/worlds/2026-08-02.payload.json +353185 -0

Diff not shown: this commit changes too many lines.

M testdata/corpus/bundles/bend-or/worlds/2026-08-02.text.json +721 -0

Diff not shown: this commit changes too many lines.

M testdata/corpus/bundles/mars/icons.json +21 -0

Diff not shown: this commit changes too many lines.

M testdata/corpus/bundles/mars/manifest.json +35 -0

Diff not shown: this commit changes too many lines.

M testdata/corpus/bundles/mars/tiles/global__mola-elevation.json +2748 -0

Diff not shown: this commit changes too many lines.

M testdata/corpus/bundles/mars/tiles/global__viking-mdim-2-1.json +2748 -0

Diff not shown: this commit changes too many lines.

M testdata/corpus/bundles/mars/volume.json +102 -0

Diff not shown: this commit changes too many lines.

M testdata/corpus/bundles/mars/worlds/global.locations.json +2058 -0

Diff not shown: this commit changes too many lines.

M testdata/corpus/bundles/mars/worlds/global.payload.json +614 -0

Diff not shown: this commit changes too many lines.

M testdata/corpus/bundles/mars/worlds/global.text.json +14338 -0

Diff not shown: this commit changes too many lines.

M testdata/corpus/translators/nasa-trek.doc.json +23095 -0

Diff not shown: this commit changes too many lines.

M testdata/corpus/translators/nasa-trek.fixture.json +33 -0

Diff not shown: this commit changes too many lines.

M tests/cells/conformance_test.go +594 -0

Diff not shown: this commit changes too many lines.

M tests/corpus/corpus.go +127 -0

Diff not shown: this commit changes too many lines.

M tests/e2e/app.spec.ts +131 -0

Diff not shown: this commit changes too many lines.

M tests/e2e/playwright.config.ts +36 -0

Diff not shown: this commit changes too many lines.

M tests/e2e/prep/main.go +51 -0

Diff not shown: this commit changes too many lines.

M tests/island/display_test.go +618 -0

Diff not shown: this commit changes too many lines.

M tests/island/harness_test.go +354 -0

Diff not shown: this commit changes too many lines.

M tests/island/island_test.go +442 -0

Diff not shown: this commit changes too many lines.

M tools/corpussmoke/main.go +152 -0

Diff not shown: this commit changes too many lines.

M tools/crawl/api.go +0 -335

Diff not shown: this commit changes too many lines.

M tools/crawl/arcgis.go +0 -591

Diff not shown: this commit changes too many lines.

M tools/crawl/archive.go +0 -392

Diff not shown: this commit changes too many lines.

M tools/crawl/axis.go +0 -135

Diff not shown: this commit changes too many lines.

M tools/crawl/ign.go +0 -501

Diff not shown: this commit changes too many lines.

M tools/crawl/main.go +0 -634

Diff not shown: this commit changes too many lines.

M tools/crawl/main_test.go +0 -303

Diff not shown: this commit changes too many lines.

M tools/crawl/mapserver.go +0 -193

Diff not shown: this commit changes too many lines.

M tools/crawl/mapserver_test.go +0 -120

Diff not shown: this commit changes too many lines.

M tools/crawl/pb.go +0 -580

Diff not shown: this commit changes too many lines.

M tools/crawl/trek.go +0 -476

Diff not shown: this commit changes too many lines.

M tools/crawl/window.go +0 -192

Diff not shown: this commit changes too many lines.

M tools/depcheck/hostenv.go +77 -0

Diff not shown: this commit changes too many lines.

M tools/depcheck/imports.go +67 -0

Diff not shown: this commit changes too many lines.

M tools/depcheck/laneimports.go +151 -0

Diff not shown: this commit changes too many lines.

M tools/depcheck/main.go +123 -0

Diff not shown: this commit changes too many lines.

M tools/depcheck/netconfine.go +122 -0

Diff not shown: this commit changes too many lines.

M tools/depcheck/report.go +131 -0

Diff not shown: this commit changes too many lines.

M tools/depcheck/rules_test.go +210 -0

Diff not shown: this commit changes too many lines.

M tools/depcheck/scope.go +139 -0

Diff not shown: this commit changes too many lines.

M tools/depcheck/semconvlit.go +79 -0

Diff not shown: this commit changes too many lines.

M tools/generate/bundles.go +0 -344

Diff not shown: this commit changes too many lines.

M tools/generate/main.go +0 -931

Diff not shown: this commit changes too many lines.

M tools/generate/main_test.go +0 -272

Diff not shown: this commit changes too many lines.

M tools/generate/merge.go +0 -940

Diff not shown: this commit changes too many lines.

M tools/generate/merge_test.go +0 -212

Diff not shown: this commit changes too many lines.

M tools/generate/normalize.go +0 -266

Diff not shown: this commit changes too many lines.

M tools/generate/normalize_test.go +0 -203

Diff not shown: this commit changes too many lines.

M tools/generate/shard.go +0 -697

Diff not shown: this commit changes too many lines.

M tools/generate/split.go +0 -176

Diff not shown: this commit changes too many lines.

M tools/generate/split_test.go +0 -91

Diff not shown: this commit changes too many lines.

M tools/icons/png.mjs +0 -200

Diff not shown: this commit changes too many lines.

M tools/icons/render-icons.mjs +0 -378

Diff not shown: this commit changes too many lines.

M tools/maturity/main.go +0 -119

Diff not shown: this commit changes too many lines.

M tools/testgate/main.go +98 -0

Diff not shown: this commit changes too many lines.

M tools/tiles/main.go +0 -1273

Diff not shown: this commit changes too many lines.

M tools/tiles/main_test.go +0 -105

Diff not shown: this commit changes too many lines.

M tools/tiles/stamp.go +0 -146

Diff not shown: this commit changes too many lines.

M tools/tiles/stamp_test.go +0 -157

Diff not shown: this commit changes too many lines.

M tools/tiles/warp.go +0 -420

Diff not shown: this commit changes too many lines.