Roadmap

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:#fff

Milestone 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:#fff

Phase 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 / partitionForPosition map 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 requestAnimationFrame loops outside R3F's. useTimedFrame gives one Render entry per useFrame consumer and FrameMetrics measures the same loop; the other callers in apps/game/src run main-thread work no instrument sees. The three boot-time ones — render/firstLight.ts, render/createRenderer.ts and render/warmup.ts — come first, because boot is the one window the performance panel structurally cannot look at and the Boot track already exists.
  • ir.timing.drain() is the page's thread only. A performance.mark lands in the timeline of the scope that wrote it, so the worker pool's Tasks entries never reach drain() or ir.profile; a page-side profile carries only the pool's queue and run. A Chrome trace carries every thread, so pnpm timing --threads over a --trace recording is the reading that includes the worker side. Closing the gap for drain means collecting each worker's entries over postMessage on every drain, which is worth doing only if a full-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.

  • ScrollArea is not used, and should not be. Radix's viewport wraps its content in a display: table box that grows past 100% to fit the widest line — and every readout in the dock is truncate inside a 27 rem column, so the ellipsis would simply stop happening. index.css already paints the native gutter in this system's colors for exactly this surface. The row in the old plan was wrong.
  • OverlayPage keeps its hand-rolled dialog. Radix's Dialog with modal={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 the AnimatePresence keying — 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.

  1. releaseFocus is 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 inside hud/Action.tsx, hud/SwitchRow.tsx and hud/TransportButton.tsx rather 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".
  2. PerfPanel still carries 'use no memo', and so do the pieces split out of it. Moving a panel onto useEngine selectors is what would make that opt-out unnecessary; moving it onto shadcn components alone does not.
  3. The accent is still never a fill behind text. Button's default variant is a solid bg-primary plate, which index.css rules out; the primary tone is outline plus the sky-500/15 wash, 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
No CI configuration ✅ .github/workflows/check.yml runs pnpm check and the capability self-test on every pull request
No formatter ✅ 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 cold descent on every CI run ✅ 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.


  • CONTEXT.md — the build log and the bugs not to reintroduce
  • Architecture — where each seam lives
  • ADRs — the decisions these build on