7,077 words · 3 diagrams
What is not built yet, with the seam that already exists for it and an honest note on what it would take.
Scope and principles are in vision; what exists is in architecture; what was learned building it is in CONTEXT.md.
Legend — ✅ done · 🟡 partial · ⬜ not started · ⛔ deliberately deferred
Where things stand
flowchart LR
subgraph DONE["✅ foundations proven"]
D1["coordinates · frames · determinism"]
D2["identity · time · persistence"]
D3["workers · streaming · LOD · offline"]
end
subgraph NEXT["🟡 shallow but real"]
N1["terrain · content variety"]
N2["flight model · streaming policy"]
end
subgraph LATER["⬜ not started"]
L1["multiplayer · replay"]
L2["discovery progression · content systems"]
end
DONE --> NEXT --> LATER
style DONE fill:#065f46,stroke:#064e3b,color:#fffMilestone 1 — the vertical architectural proof — is complete: 12/12 capability checks pass in Node and in Chrome, in dev and in a production build. What follows is depth, not foundations.
Status at a glance
| Area | Status | Notes |
|---|---|---|
| Universe coordinates and precision | ✅ | ADR-0001 |
| Reference frames and transitions | ✅ | ADR-0002 |
| Render coordinates, floating origin | ✅ | ADR-0003 |
| Stable identity and addressing | ✅ | ADR-0004 |
| Deterministic generation | ✅ | Core proven; two inputs now — seed and catalog version |
| Real astronomical data | ✅ | 7,123 systems and 702 planets within 150 ly, and the 7,514 naked-eye stars beyond; 129 Solar System bodies; guide |
| Measured body figures | ✅ | 25 shape models from the PDS; generated figures everywhere else — ADR-0013 |
| Simulation clock and determinism | 🟡 | All of it except replay |
| Simulation / rendering separation | ✅ | Proven by apps/headless |
| Worker architecture | ✅ | Pool, contracts, cancellation, instrumentation |
| Offline-first | ✅ | Service worker + IndexedDB + migrations |
| Persistence model | 🟡 | Proven; mutations unbuilt |
| Streaming | 🟡 | Systems and terrain stream; policy is naive |
| Level of detail | 🟡 | Tiers exist; terrain is a restricted morphing quadtree over a measured detail floor, with rock scatter and a per-pixel grain band under it |
| Units and conventions | ✅ | |
| Repository structure and layering | ✅ | Enforced by pnpm graph |
| Protocols and serialization | 🟡 | Worker + save done; net, replay and binary unbuilt |
| Observability | ✅ | All twelve inspectable fields |
| Automation and DX | 🟡 | Commands, docs, CI and the formatter done; no save fixture |
| Testing | 🟡 | Strong; replay and fixtures missing |
| Performance | 🟡 | Instrumented and measured at declared operating points; target-hardware acceptance remains open (performance) |
| Multiplayer | ⛔ | Deferred. Seams only — ADR-0008; the partition key is a live debug field |
| Application shell and modes | ✅ | Five modes, routes as the public surface — ADR-0011 |
| Planetarium | ✅ | Free navigation, Navigator and predicate catalog search, body records, orbit traces, labels, portable photographs, held time and galaxy instruments — design |
| Camera and sensor | ✅ | Three modes over one chain — ADR-0037; optics and export wait in the camera beyond the three modes |
| Cinema player | ✅ | Transport, timecode and a frame-exact link over the cutscene format — design |
| Dockable panels | ✅ | Four zones, property-tested layout algebra — ADR-0012 |
| Mobile | 🟡 | Looking works and is verified; piloting on a touchscreen is not designed |
Content: the rest of the vision
The vision names the eventual inhabitants of the galaxy. Most are not built. The important thing is that none of them need architectural change — they are generators plus representations.
| Thing | Status | Seam |
|---|---|---|
| Galaxy, systems, stars | ✅ | Real out to 150 ly, procedural beyond — catalog guide |
| Planets, moons | ✅ | Confirmed exoplanets and the Solar System are observed; the rest is projected |
| Moons of real planets | 🟡 | Sol's 62 are observed and measured; every exoplanet's moon is a projection, and PackedPlanet still has no moon list to change that |
| Catalog revision diff | ✅ | versionDrift in packages/protocol — one verdict, read by the handshake, the save loader and the health panel |
| Planetary terrain | 🟡 | Whole-disk heightfields, seamless; craters, plates, volcanism and ice from a per-body grammar, shaded from a palette derived from the same facts; the baked sphere carries relief. No authored material sets, and no erosion |
| Ships | 🟡 | Two modeled hulls in data/models/ (a CC-BY Enterprise-D and a CC-BY Rocinante, each scaled to its true length), chosen in Display settings, debug cone as fallback; no subsystems |
| Rings | ✅ | All four giants, with Saturn's shadow on its own and theirs on it; Haumea, Quaoar, Chariklo and Chiron carry theirs; a procedural giant gets a 1-in-6 chance and a strip drawn from its own character |
| Asteroids / belts | 🟡 | 50 real asteroids and comets in Sol, and 6–18 generated per system — but they are b: bodies at system scale, not the o: region population a visible belt would need (see belts as a population) |
| Small-body figures | ✅ | 92 of Sol's 129 bodies are not spheroids; 25 have published shape models and the rest are seeded — ADR-0013 |
| Diffuse galaxy and dust | ✅ | Calibrated stellar populations, luminosity-level resolved stars and absorption share one field; ADR-0038 |
| Star clusters, nebulae | ⬜ | Globular clusters, H II line emission and dust scattering remain separate additions; the existing diffuse galaxy does not implement them — the galaxy beyond the disk |
| Black holes | ⬜ | A body kind; the interesting part is rendering, not simulation |
| Vegetation, flora, fauna | ⬜ | Region-seeded scatter on terrain — the o: address segment exists for this |
| Structures, settlements | 🟡 | Durable body-fixed placements and the Mars landing pad exist, with a declared walkable relief for contact — deck, aprons, ramp, equipment as walls. Settlements and interiors remain unbuilt. ADR-0040 |
| Humanoids | 🟡 | Body-fixed characters walk, sprint, strafe, crouch, and jump; optional pointer lock drives first-person and third-person controls. Trusted session permission gates flight. Suit survival and object interaction remain unbuilt. ADR-0047 |
| Small physical objects | 🟡 | Debug cubes render at the right scale; no interaction |
Gameplay verbs: piloting ✅, in-system travel ✅, approach and orbit ✅, landing ✅. Interstellar travel is 🟡 — possible but takes hours of simulated time, so the designed burn/jump travel loop remains open. The planetarium's reversible Earth-to-disk journey is a presentation tool and does not move a ship. Atmospheric entry is 🟡: drag, a visual entry arc and heat flux are modeled, but there is no thermal damage or structural stress. Surface exploration is 🟡. A character can traverse terrain and a structure's relief, with a choice of first-person or third-person controls. Rock and hull contact, interior movement, survey tools, inventory, and suit hazards remain open. See On foot for current controls and limits.
Terrain
The most visible shallowness, and the milestone in progress — The terrain plan sequences it, and the erosion plan is the phase after the liquid.
flowchart TB
NOW["<b>today</b><br/>a geology with a face,<br/>a sea over its seabed,<br/>valleys and a coast, and<br/>a sphere that wears its ground"]
F["<b>the mesh off the main thread</b><br/>normals and the LOD morph<br/>where the heightfield already is"]
E["<b>erosion</b><br/>a drainage graph per body,<br/>rivers that run downhill,<br/>lakes at their spill"]
NOW -->|"the build is the<br/>queue now"| F
NOW -->|"the valleys do not<br/>know which way is down"| E
style NOW fill:#334155,stroke:#1e293b,color:#fff
style F fill:#0369a1,stroke:#0c4a6e,color:#fff
style E fill:#0369a1,stroke:#0c4a6e,color:#fffPhase 1 landed 27 Aug 2026: per-patch level selection, whole-disk coverage,
cross-face sampling, bordered patches, the CDLOD morph, prefetch and budget.
ADR-0015 is the decision record and
CONTEXT.md has the measurements. What it closed: the horizon
is terrain rather than the datum sphere, cube-face edges are not holes, patch
boundaries have no seam, and the three defects that came from measuring altitude
from the datum are gone — every one of the zoo's twenty-four survey sites now
bottoms out at its own detail floor, where two of Miranda's could not be drawn
at any altitude at all.
Phase 1.5 landed 28 Aug 2026: the camera has a lens, and the refinement predicate reads it instead of assuming 60° over 1080 px. ADR-0017 is the decision record. It matters here because every patch count in the plan is a function of that one number — the flight lens is 848 px/rad against the guess's 935, and the telephoto end of the field-of-view slider saturates the patch cap on most of a descent's steps.
Phase 3 landed 29 Aug 2026: the ground has a material. ADR-0020 is the decision record. The cover — four bytes then, eight since the liquid — carries what a shader cannot derive — impact-fresh material, flood basalt, where the crust sits on the body's compositional ramp, condensed volatiles — and the three a shader can are read per pixel from the mesh it is standing on. Deposits are layered rather than splatted, in the order they are laid down, and the angle of repose does most of the visible work. A body with a published map wears it: the same photograph the sphere in front of it is drawn from, sampled by direction, with the invented channels switched off because the maria are already in the picture. Measured either side of the eight-pixel gate, the streamed ground and the archive's sphere are 3.1% apart on Mars and 1.5% apart on Earth.
Phase 4 landed 30 Aug 2026: the drawn ground goes below the field the ship lands
on. ADR-0021 is the decision record. The crater ladder
continues from the canonical eight-meter floor down to one, presentationally, and
the detail floor follows it — cells of 0.35 to 1.41 m across the zoo against 1.75
to 5.66, which is meter-scale ground under a two-meter stance where there was a
plane. drawnElevation and groundElevation are two functions 1.25 m apart at
worst, and nothing canonical moved. Rocks arrive with it: r:…/o:837 is a
hash, and they are drawn by the terrain's own material, so a boulder is bedrock
on its steep faces and regolith on its top in the palette of the ground it lies
on.
Phase 2 landed 28 Aug 2026: the three noise bands are a surface grammar derived
from each body's own facts, a per-body sketch of plate nuclei, hotspots and a
crater ladder, and six bands over them.
ADR-0019 is the decision record. Mercury comes out
saturated with craters and one lid; Earth with twenty-two plates and orogens
along their margins; Venus the same size as Earth and a stagnant lid, because it
has no ocean; Enceladus with four parallel fractures. maxElevation is a
strength limit rather than a dial, and terrain moved to algorithm version 2.
The liquid landed 2 Sep 2026: a sea read against its ground's temperature — water, hydrocarbon or magma — valleys cut into the landform and a coast remapped round the datum, the heightfield as the seabed with the sea a sheet over it, color families for rock, ice, haze and pigment, every detail octave a fetch of one baked noise texture, four named surface levers, and the sphere of a generated body wearing six faces of its own ground. ADR-0026 is the decision record. Terrain moved to algorithm version 3, and the frame at a retina size over the shore went from 9.5 to 16.3 fps. What it did not do is make a river run downhill: the valleys are the zero-level strip of a noise, and the erosion plan is the answer.
The ladder and the relief landed 3 Sep 2026, and between them they close two
rows of the table below. MAX_CRATER_LEVELS goes from eleven halvings
to fourteen — the change ADR-0021 measured and declined
to spend a version on in its own phase — so a world whose largest basin is
2,170 km carries canonical craters down to 265 m where the ladder stopped at
2.1 km, and Mercury reaches 134 m, Luna 95 m, Callisto 13 m. That is terrain
algorithm version 4 and the ground moves under every save written before it,
at 12 to 18% more per cratered patch. The orbital bake's second pass carries
relief in the same layout and encoding the sphere already reads the archive's
normal maps in (ADR-0026, amended), so a generated
world's disk shades its mountains instead of wearing correct maria over a flat
normal — which is what the eight-pixel gate exists to make invisible and could
not, because the mountains were in the near half of a descent and gone in the
far half.
| Gap | Consequence today | Seam |
|---|---|---|
| Deposits are chosen from the mesh | Two patches at different levels report different slopes for the same ground, so a weight read off the normal steps by ~4% at a level boundary | More channels on the cover, so the deposits read the canonical field instead of the LOD |
| No authored material sets | Detail is gradient noise, which has no period to break and no projection to choose — so hex-tiling and triplanar answer questions nothing is asking | The detail field in render/terrain.ts; both techniques come back with the textures that need them |
| The selection is not frustum-culled | A whole disk is generated, of which the renderer draws about a third | The streamer has the camera; a generous cone would keep a turn from bursting |
| Vertex attributes are float32 | 270 KB a patch — 203 KB of float32 geometry and 68 KB of cover — so a whole-disk selection is 129–291 MB at the flight lens | Int8 normals and Int16 morph deltas are worth about half |
| A rock reads the field, not the mesh | Its foot is 3–9 cm off the triangle under it in the mean and up to 0.70 m at the worst cell on the coarsest body; MESH_SEAT buries it 12 cm |
More channels on the cover, which is the same change the deposits want |
| Scatter has no collision | A rock is presentational; the contact test does not know it exists | On foot, where the canonical floor drops as well |
| The mesh is built on the main thread | 0.25 ms a patch, eight a frame — the queue, now that the heightfield is 10 ms for sixteen | The worker already has the field; the mesh arithmetic has to move to packages/universe first, for the layer rule |
| Patch generation is over its budget | 23.8 to 69.4 ms per bordered 65×65 patch across the zoo, against a documented ≤ 8 ms — and a landing now wants 700 to 1,077 of them | pnpm sim --terrain-baseline is the measurement; the crater neighborhood is most of it, and its radial bound, EJECTA_REACH and the GPU producer are the levers |
| A coarse patch costs more than a fine one | Consecutive samples of a coarse patch land in different noise lattice cells | A whole-disk selection pays it on the shell; per-level merging would amortize it |
Rings
The ring phase landed under ADR-0027; cite the ADR for the decision. Two things it left open.
The record carries one number for a thing that has many. RingSystem is
one annulus and one mean normal optical depth, while the strip draws plateaus,
divisions, shepherded ringlets, hairlines and a dust haze. The object panel
therefore describes less than the picture, and ADR-0014's
standard is met only by putting the profile's peak at the quoted depth. That
is also why the strip cannot be normalized: alpha is a byte in [0, 1], so a
profile whose peaks exceed one clamps rather than scales, and a true
normalization has to divide in the material on a uniform the generator
computes. The change takes the shape of a per-ring profile on the record — an
inner and outer radius, an optical depth, and whether the edge is sharp, a few
times over — which the generator already has when it writes the strip. Until
then there is nothing for a normalized strip to mean, and
planetarium's row on ring divisions describes the
data model rather than the picture.
A floor under the lit face near equinox. A sheet whose star is within a
few degrees of the ring plane is drawn dark, because a flat slab lit at
grazing incidence intercepts almost nothing. Cassini's 2009 equinox pictures
sit at a few percent, not a few thousandths, because a real ring has thickness
and multiple scattering. A μ₀-independent term would buy it and is not
written, because it is a number nobody here has measured and inventing one is
how a photometric model stops being evidence. It wants a measurement of the
multiple-scattering contribution at grazing incidence against a published
Cassini figure first, so the term has a bound to be held to the way the
lit/backlit crossover does in rings.gpu.test.ts.
The galaxy beyond the disk
The journey from Earth orbit to 30 kpc above the plane is built, and one calibrated field feeds the sky from inside, the volume from outside and the resolved star population, all transported through the same dust — ADR-0032 and ADR-0038 own it. What the picture is missing is everything that is not a smooth population.
| Gap | Consequence today | Seam |
|---|---|---|
| H II knots | The arms are ridges of smooth young light. The pink clumps along them are in every photograph of a spiral and are the literature's primary arm tracer — Anderson et al. 2014 catalogs over 8,000 of them | Poisson-disc clumps seeded from the arm coordinate, on the young ridges within 40 pc of the plane, each a small Gaussian emitter at Hα's 656 nm with a shell of OB light. It is also the first thing in the game that emits a narrowband line |
| Globular clusters | The halo is a smooth spheroidal profile with nothing in it a player can name, and Omega Centauri and 47 Tucanae are the two they would | A catalog, not a generator: Harris 2010 lists 157 with positions, distances, half-light radii and magnitudes, and Vasiliev & Baumgardt 2021 give 162 Gaia distances. The ingest packs them like the star catalog; each draws as a sprite whose halo scales with its half-light radius |
| The Clouds and Andromeda | The sky outside the Milky Way is empty. M31 at 770 kpc also lies outside the coordinate system's roughly 249,000 ly span, so it needs a far-field addressing decision before a parameter record helps | The LMC at (280.46°, −32.89°) and 50 kpc, the SMC at (302.79°, −44.30°) and 62 kpc, M31 at (121.17°, −21.57°): each the same field with its own parameter record, drawn from outside at its real direction and size. One PR per neighbor |
| The horizon of knowledge | Catalog completeness is a declared magnitude-and-distance envelope, but the map does not draw it, so observed and projected look alike on screen |
Derive the coverage surface from the completeness record itself; per-class coverage and observed/projected labeling. It is a map-interaction change more than a field one |
| Zodiacal light | The Solar System's own dust does not exist, so the ecliptic has no glow seen from inside it | A separate emitter with its own calibration and sampling plan, integrated at AU scale, reusing the sensor contract rather than the galactic kernel |
Three honest limits sit under all of it. Central morphology is an approximation: passing broad integrated V constraints does not establish a measured bulge/disk decomposition, and no camera response recovers structure the source does not contain. Dust absorbs and does not scatter, which is what separates the modeled lanes from a reference photograph's. And deeper catalog completeness is an ingest and source review, not something to infer from a histogram peak — the envelope is deliberately conservative where the data is.
The camera beyond the three modes
Enhanced, Automatic and Manual are implemented over one sensor chain (ADR-0037, ADR-0029), and the optics they share are a lens, a histogram meter, a glare kernel and a defocus gather. Each row below is a separate piece of optics or workflow with its own evidence, and none of them gates the default image.
| Gap | Consequence today | Seam |
|---|---|---|
| Per-glass iris sampling | One iris shape serves the whole lens, so a particular glass cannot change the shape of the blur it makes | The glass records already exist; the check is blur shape plus the existing half-pixel defocus gate |
| FFT diffraction | Glare is a convolved kernel rather than a diffracted one, so an aperture's blade count never reaches the star | Iris sampling first, then kernel energy and orientation tests against a measured rebuild and convolution cost. The proposed 1 ms rebuild and 1.5 ms a frame at 1080p are unproven budgets, not measurements |
| Spectral attachment and narrowband | Every channel is broadband, so there is no Hα, OIII or SII for a filter to pass | Real line emission from the galaxy model — H II knots is its supplier — with declared units, a channel mapping, and broadband equivalence when the filter is off |
| Photo export and the tether | A photograph is a URL that restores a view; nothing writes a file, and nothing carries one to another device | Stable version 2 camera records; verify dimensions, color encoding, address and time metadata, and held-frame repeatability |
| Display-headroom discovery | Authored peak luminance is a setting because the browser exposes no measurement of the actual panel | A supported measurable signal. The authored peak limits and their fallback stay until one exists |
| Additional response styles | The controls offer Enhanced's authored look and the shared neutral photographic look, plus imported Gentle and Crisp shoulders | Evidence that a further style adds a useful choice without changing mode, source lighting or metering. A curve that decides exposure is the thing ADR-0037 separated out |
The upscaler is a separate performance proposal. Whole-scene temporal reconstruction, reversed-Z conversion and new reactive buffers are not prerequisites for any row here.
The guide
The guide is one voice — ADR-0042 owns the design and ADR-0041 the execution boundary it keeps. The code is complete; what is open is judged by a person or was declined as a core requirement.
| Item | What it needs |
|---|---|
| A person on headphones | Spoken delivery across the voices, the introduction aloud, and pronunciation are a listen, not a test. Measure with the window on screen: the off-screen drive rig has no audio pipeline, so a rig session reads every beat as silent and falls back to the clock's start timeout, which proves the loop but not the pacing |
| A cheaper backend | Whether Sol or Terra paces a routine beat as well as Astra. The browser's usage display decides, not an estimate: extrapolated from the probe, a ten-minute conversation with thirty backend responses costs under $0.30 of backend plus $0.50 of voice at the recorded Astra rates |
| Visual questions | "What is that bright point beside Saturn?" needs a real frame in front of the backend. That is an in-product capture at the sensor boundary — a faithful SDR copy of the displayed frame that advances no sensor temporal history and renders no second simulation frame — because the driver's canvas.toDataURL() returns transparent black once the WebGPU swap-chain image expires. Only an explicit question may capture; a tour, an arrival, silence and timers produce zero captures and zero uploads |
| Composition help | Builds on visual questions: an observe, propose, execute, observe-again loop that only an explicit request such as "frame Saturn and Titan together" starts, bounded in steps, preserving the accepted view on failure and stopping when the visitor takes over. Kept only where an image beats the metadata the tools already return |
Streaming and scale
| Gap | Consequence | Seam |
|---|---|---|
| Interest is a radius scan over cells | Fine at 6 ly; a 100 ly query touches ~1,000 cells | systemsWithin already bounds and refuses oversized queries; a spatial index goes behind the same call |
| Simulation interest = render interest | Distant systems do not simulate at all | updateInterest is the seam; a coarser tier for "simulated but not rendered" is the next step |
Simulation in a worker
The core is provably framework-free — apps/headless runs it in Node with no
DOM. Moving it to a Web Worker is therefore mechanical rather than
architectural: the snapshot is already structured-cloneable and the renderer
already only reads snapshots.
Not done because nothing needs it yet. One integrated entity runs at ~1.25M ticks/s in the browser, and a coasting one is not integrated at all. It becomes interesting when entity counts rise.
Persistent mutations
The model is proven; the data is not built.
{ address, kind: 'discovered' | 'destroyed' | 'placed' | 'terrain', data, tick }The field exists and validates today, so adding the first real mutation is a migration of data rather than a change of model. What each needs:
| Mutation | Needs |
|---|---|
discovered |
A player-state blob; trivial |
destroyed |
A generated entity to be suppressible at generation time — the generator must consult a mutation set |
placed |
Dynamic entities that persist, which already works for the ship |
terrain |
A sparse height delta keyed by region address, applied after elevationAt |
The one to design carefully is destroyed, because it inverts the direction of
dependence: generation currently knows nothing about saved state, and it must
stay a pure function. The likely shape is a filter applied after generation,
not a branch inside it.
Replay and reconciliation
Deterministic stepping ✅ exists; recorded replay does not.
Everything needed is present: the tick is canonical, the state hash compares
universes, and control input is already persisted. What is missing is an input
log — (tick, entityId, controlInput) — plus a driver that replays it.
That would also give: a bug report format that reproduces exactly, a regression test format for flight behavior, and the foundation for client prediction if multiplayer arrives.
Multiplayer
⛔ Deliberately deferred.
Deliberately deferred to a later phase. What exists:
partitionForAddress/partitionForPositionmap to opaque string keys.- Authority follows an entity's frame chain, so a ship in Sol belongs to Sol's partition even though it has no address.
- No vendor SDK anywhere in
packages/*, enforced by the layer check. - ADR-0008 sketches the topology.
What it will need, none of it started: an AuthorityPort interface with a local
implementation, entity replication, client prediction and reconciliation,
interest management, handoff between partitions, conflict resolution for
mutations, and protocol versioning for net messages.
The one piece of design worth restating: because the base universe is deterministic, an authority only has to replicate what a client cannot derive — entity states and persistent mutations. That is the same set a save file contains, which is not a coincidence and is worth preserving.
Performance work
The principle is measure before optimizing. The build has a shared timeline, browser driving and profiling tools, a physical GPU test suite, and recorded cold/warm operating points. These establish costs under named conditions; they do not establish the 60 fps target on every laptop.
| Technique | Status | Current boundary |
|---|---|---|
| Typed arrays and transferables | ✅ | Terrain fields, vertex buffers and worker results |
| Worker pools | ✅ | Galaxy queries, canonical terrain and client atmosphere tasks |
| Instanced rendering | 🟡 | Stars and rock scatter; a visible asteroid-belt population remains unbuilt |
| GPU projection and bounded sky updates | ✅ | Source uploads follow selection changes; observer uniforms handle ordinary camera movement |
| Physical sky reuse | ✅ | Progressive cubes, a bounded regenerable disk cache and temporal history, governed by ADR-0038 |
| WebGPU | ✅ | TSL renderer and compute terrain; WebGL 2 remains a slower fallback |
| WASM and shared mutable buffers | ⬜ | Require evidence that the existing kernels or transferables are insufficient |
ir.profile(ms), pnpm timing and the Perf panel expose engine, terrain,
render, boot and worker work. ADR-0038
records the sky's work budgets and memory allocations; its 2 ms live target is
not a universal bound. ADR-0037 records camera
acceptance, and technical keeps the historical baseline
measurements separate from budgets.
Astro serves readable HTML before the scene loads. GameLoader imports the
runtime separately, and the runtime registers the service worker after startup.
A reader reaching documentation does not need a working renderer to read the
page. ADR-0039 owns that split. Mermaid
is a separate on-demand documentation dependency.
Remaining work includes acceptance on the target laptop, representative input and travel recordings, allocation/GC investigation where traces identify it, and a reliable performance-regression gate on controlled hardware. A fixed wall-time threshold in ordinary CI would measure the runner as much as the code. Physical GPU correctness tests and preset plates already exist; comparing rendered images automatically on stable CI hardware remains separate work.
What the timeline cannot see
ADR-0022 puts every phase of a frame, a boot and a worker job on the browser's performance timeline. Two things are still dark.
- The
requestAnimationFrameloops outside R3F's.useTimedFramegives oneRenderentry peruseFrameconsumer andFrameMetricsmeasures the same loop; the other callers inapps/game/srcrun main-thread work no instrument sees. The three boot-time ones —render/firstLight.ts,render/createRenderer.tsandrender/warmup.ts— come first, because boot is the one window the performance panel structurally cannot look at and theBoottrack already exists. ir.timing.drain()is the page's thread only. Aperformance.marklands in the timeline of the scope that wrote it, so the worker pool'sTasksentries never reachdrain()orir.profile; a page-side profile carries only the pool'squeueandrun. A Chrome trace carries every thread, sopnpm timing --threadsover a--tracerecording is the reading that includes the worker side. Closing the gap fordrainmeans collecting each worker's entries overpostMessageon every drain, which is worth doing only if afull-level report is wanted somewhere a trace cannot be recorded.
Historical measurements retain their conditions: the 27 Aug 2026 entry chunk
was 747.0 KB gzip, the early M5 atmosphere raymarch cost 7.27 ms at 1080p, and
the 150 ly catalog measured 159 KB brotli. They are not current whole-app or
current atmosphere costs. Do not use renderer.info.render.timestamp for that
measurement; the spike records its
double-counting failure and the drained-queue method.
The overlay refactor — landed (22 Aug 2026)
The UI foundations arrived first — lucide-react, shadcn/ui, motion, zustand
and react-router in apps/game — with one real use each, so the wiring was
proven before anything was rewritten onto them. The rewrite is now done.
CONTEXT.md has the build log entry; DESIGN.md
has the token mapping.
What the hand-rolled controls became.
| Was | Is |
|---|---|
hud/widgets.tsx → Action |
hud/Action.tsx, a Button preset — see the note below |
hud/widgets.tsx → Section |
Collapsible, still controlled by usePersistentState |
hud/widgets.tsx → Row |
hud/Row.tsx, unchanged — it is a readout, not a control |
HudDock's hand-rolled tablist |
Tabs / TabsList / TabsTrigger / TabsContent |
CameraPanel's <input type="range"> |
Slider, via the shared hud/LensSlider.tsx |
Both transports' <input type="range"> |
Slider, via the shared hud/FrameScrubber.tsx |
GraphicsPanel's and ViewPanel's two |
one hud/SwitchRow.tsx over Switch |
different Toggles |
|
GraphicsPanel's Cycle |
ToggleGroup — see "one correction" below |
ShellBar's <button role="switch"> |
Toggle |
| The catalog's address field | Input, via hud/AddressForm.tsx |
The w-px bg-slate-800 dividers |
Separator |
| The status chips | Badge |
Icon-only controls' title attribute |
Tooltip — the rest keep title; see below |
One correction to the plan. It listed the anti-aliasing Cycle as having
"no registry form", which was true of a cycle-through control and false of the
problem: the set is off · 2× · 4× and it is closed and small, so it is a radio
group. ToggleGroup is that, it puts all three on screen at once, and it reaches
a specific level in one press instead of up to three.
Two things it deliberately did not do.
ScrollAreais not used, and should not be. Radix's viewport wraps its content in adisplay: tablebox that grows past 100% to fit the widest line — and every readout in the dock istruncateinside a 27 rem column, so the ellipsis would simply stop happening.index.cssalready paints the native gutter in this system's colors for exactly this surface. The row in the old plan was wrong.OverlayPagekeeps its hand-rolled dialog. Radix'sDialogwithmodal={false}is genuinely the shape this wants, and the ~60 lines it would replace are the ones carrying the focus-restore rules, the scrim's drag-release exception and theAnimatePresencekeying — each written against a bug that shipped. That swap is worth doing and it is worth doing on its own, with those cases as its acceptance criteria.
Tooltips were listed as blocked on the portal. They are not. The reasoning
was that shadcn overlays portal to document.body, outside .hud-layer and so
outside dynamic-range-limit: standard, and would wash out against a star. The
clamp exists because the dock and the flight strip are backdrop-filter
surfaces and a backdrop filter samples what is behind it; TooltipContent is
an opaque bg-foreground box with no backdrop filter, so it has nothing to
sample. They are used only where the control is icon-only and the hint is
therefore the label — the dock rail, the panel close buttons, the transports,
the shell bar. Everywhere a control has readable text the title stays, because
there it is doing a different job: recovering a value that truncated. Give a
tooltip a translucent ground and the clamp is load-bearing again, at which point
Radix's Portal takes a container.
The three carry-across risks all held.
releaseFocusis load-bearing — every control blurs itself on a pointer click and not on a keyboard activation, because flight input is a window-level keydown listener. It is now insidehud/Action.tsx,hud/SwitchRow.tsxandhud/TransportButton.tsxrather than at sixty call sites, which is the shape that stops it being forgotten. See CONTEXT.md § "The focus contract, which is subtler than it looks".PerfPanelstill carries'use no memo', and so do the pieces split out of it. Moving a panel ontouseEngineselectors is what would make that opt-out unnecessary; moving it onto shadcn components alone does not.- The accent is still never a fill behind text.
Button'sdefaultvariant is a solidbg-primaryplate, whichindex.cssrules out; the primary tone isoutlineplus thesky-500/15wash, in one place.
Still not in scope, and worth restating because the temptation is real: this refactored the dev dock, which is scaffolding. It does not move it toward the cockpit in ux.md — that starts from where an element physically sits on a canopy, not from this layout.
Small bodies and their figures
Landed 25 Aug 2026 — ADR-0013, and the build log entry for what it cost. What follows is what it left open, in the order the value is.
Photometric normalization
The surface pipeline separates physical reflectance from camera processing. Mapped bodies retain their texture reflectance with a normalized hue tint; mapless bodies use their physical palette. The sphere, ground and orbital bake share this convention. Enhanced applies a bounded visibility gain at the lit material, while Automatic and Manual omit it. ADR-0037 records the current camera contract.
Per-map photometric normalization remains open. The 25 Aug 2026 measurement found mean linear luminance from 0.048 on Callisto to 0.32 on the Moon, without agreement with their published geometric albedos. Removing double-applied albedo does not calibrate those map means.
The remaining work is to record each map's linear mean in the ingest manifest, then evaluate a scale toward the chosen reflectance convention. It changes every mapped world's brightness and needs a deliberate calibration with before/after plates. The historical measurement is in CONTEXT.md.
Shape models the ingest cannot reach
pnpm shapes:build speaks PDS, and two of the most recognizable silhouettes in
the Solar System are archived elsewhere.
| Body | Archive | Why it matters |
|---|---|---|
| 67P/Churyumov–Gerasimenko | ESA Planetary Science Archive | The duck. The single most recognizable small-body outline there is, and it currently renders as a generated figure on its measured half-extents |
| 162173 Ryugu | JAXA DARTS | A spinning top like Bennu, and the other sample-return target |
| 433 Eros, 25143 Itokawa surface maps | NEAR and Hayabusa archived images, not projected mosaics | Both have their figure vendored; neither has a map, and no public archive holds one |
The seam is a second fetcher shape in shapeSources.ts — the readers
(grid, obj, vertex) already cover every format those archives publish.
What radius means
Unresolved on purpose, and the trade-off is the interesting part.
For a body with a shipped model, radius is the measured bounding box, so
nothing ever exceeds it. For a generated body it is the reference ellipsoid's
semi-axis, and the lumps stand above it — about 17% at the median roughness and
up to 55% at the top of the range.
The two cannot be reconciled: a lumpy body with an ellipsoid's volume has a
larger bounding box than that ellipsoid, so a figure can preserve the volume
or match the bounding box and not both. irregularFigure picks volume, because
the mass and the class density depend on it and nothing depends on the bounding
box except which LOD tier gets picked — a tier boundary rather than a fact.
Changing it means carrying both, which is a third set of numbers on every body
for a cosmetic gain.
Gravity is still a point mass
The bodies this added are exactly the ones where that stops being true. Bennu's gravity field is dominated by its shape — the equatorial ridge is centrifugal, material migrates along a slope field that a point mass does not have, and the OSIRIS-REx team published the gravity model precisely because the point-mass answer is wrong at the surface. Nothing in the game is close enough to a small body for long enough for it to matter yet, and the seam is real: the shape field is already a polyhedron and the polyhedral gravity integral is a known closed form.
Belts as a population
Generated systems get 6–18 small bodies each, and Sol has 50 real ones. That is
a sample of a population that in reality runs to millions, and it is deliberate
— they are b: bodies at system scale, addressed and traceable like planets.
A belt you can see is a different object: many thousands of instanced points
from one cell seed, addressed as o: objects within a region, culled and drawn
as a cloud rather than as bodies. The o: address segment exists for it. The
two are complements, not alternatives: the named ones stay addressable
destinations and the population is the scenery between them.
Automation gaps
| Gap | Note |
|---|---|
.github/workflows/check.yml runs pnpm check and the capability self-test on every pull request |
|
prettier, with format:check inside pnpm check, so a badly formatted file fails the gate rather than being noticed in review |
|
| No stored save fixture | Compatibility testing currently synthesizes old saves in-test rather than loading a real one from disk |
| No controlled performance gate | Physical GPU correctness tests exist; timing regression needs stable hardware, see above |
The workflow restores the terrain archive from a cache keyed on its version string and saves it when it grew; the run on main is what leaves a warm archive for every pull request |
|
| No image comparison in CI | pnpm presets:compare differences the lit pictures against their plates or a baseline commit, on a machine with a GPU; the headless picture ledger runs in pnpm test everywhere. A hosted GPU runner is the same open question as the shader suite below |
| No shader tests in CI | pnpm test:gpu needs a physical adapter. Whether a hosted macOS runner gives Dawn a plain MTLDevice is unmeasured — Apple's Virtualization framework enables GPU acceleration but not Metal Performance Shaders, which says nothing about a bare device (runner-images #7085 carries the limitation), and one workflow run settles it. Linux runners are the wrong fallback: Dawn takes SwiftShader there, and a graph that compiles on a software adapter and not on Metal is the class of failure the suite exists to catch |
Known simplifications in the physics
Not roadmap items so much as honest labels on what is modeled:
| Simplification | Reality |
|---|---|
| Multiple-star systems modeled as single stars | The catalog records true component counts for all 375 of them within 150 ly |
| No vendored maps for most small bodies | Titan, Enceladus, Iapetus, Triton, the Uranian moons, Deimos, Eros, Itokawa, Ryugu and everything below Bennu render from measured albedo and tint. Twenty-five of them do have a measured figure, which for a body a few kilometers across is the half that shows |
| Patched conics, no n-body | Lagrange points, resonances and perturbations do not exist |
| Terrain and declared-relief contact only | Character stepping, wall sliding, uphill slope limits, and local-gravity jumps exist. Rocks, hull walls, ceilings, arbitrary meshes, and entity-to-entity contact do not. |
| Surface characters are kinematic | Tangent movement and local-gravity jumps run in the body-fixed frame. No orbital Coriolis or centrifugal integration, moving ship interiors, or suit survival. |
| Circular-ish orbits, coplanar-ish systems | Generated inclinations and eccentricities are small |
| Atmospheres are isothermal exponential | No layers, no weather, no wind |
| Bodies are spheroids or measured figures | Oblateness is carried and drawn, and 92 Solar System bodies have a real figure — but nothing acts on either. No J2 precession, and gravity is a point mass everywhere, so the 500-meter bodies whose shape dominates their gravity field are attracted to as if they were points |
| The collision datum is an ellipsoid | surfaceRadius evaluates the measured ellipsoid for a figured body, not its shape model — packages/universe may not read a file. The residual is the body's own roughness, a median 9% of its mean radius |
What would be next
The terrain quadtree, stitching, GPU heightfields and rock scatter already exist. The remaining paths build on them:
flowchart LR
T["Terrain mesh cost and erosion"] --> C["Content variety and visible belts"]
C --> M["Persistent mutations"]
R["Input recording and replay"] --> N["Multiplayer"]
M --> N
P["Per-map photometric calibration"] --> A["Visual acceptance on target hardware"]Terrain mesh cost and erosion have explicit seams in terrain. Photometric normalization is an independent calibration task. Replay and persistent mutations supply different parts of the multiplayer prerequisite; neither is supplied by the galaxy journey or portable photographs.
Related
CONTEXT.md— the build log and the bugs not to reintroduce- Architecture — where each seam lives
- ADRs — the decisions these build on