Invariant map

5,942 words

The canonical imperatives live in AGENTS.md. This page maps each one to its technical home and carries the full constraints, exceptions, and reasoning below. Read the relevant rule in full before changing its area; the short imperative is an index, not a substitute for those constraints.

Path-scoped copies in .claude/rules/ carry only the imperative. If a copy disagrees, AGENTS.md wins.

branching.md, writing.md and browser.md are the three exceptions and have no row here. They are unscoped process rules, not invariants about the code, and they mirror working.md § "Starting work", STYLE.md and the drive skill. A rule missing from this table is one of those three, not drift.

Invariant Technical home
No absolute position in a Vec3 Coordinates · ADR-0001
No wall-clock or Math.random in canonical code Determinism · ADR-0005
Never hold an algorithm version because the draw order is intact Determinism · ADR-0027
Generation does not depend on order Determinism · Identity
The three emit APIs live only in the sink ADR-0022 · Observability
Canonical state is not in React Architecture · ADR-0011
Presentation switches go through the stance stack Client
One Worker constructor Workers · Development
One session constructor Client
Terrain is sampled in body-fixed axes Frames
No field value read off a rank ADR-0019 · Determinism
No planetary radius subtracted in a shader ADR-0020 · Rendering
The ground and the sphere behind it shade alike ADR-0020 · Rendering
No dispose() on a mesh holding the shared index ADR-0021 · Rendering
No stored value derived from a captured snapshot ADR-0012 · Client
No varying named after an attribute ADR-0020 · Rendering
No attribute object under two names ADR-0021 · Rendering
A stand-in DataTexture is filtered like its map Testing
Never build two texture nodes over one stand-in object ADR-0021 · Testing
No canonical reader of the drawn ground, or the reverse ADR-0021 · Terrain
No fine lattice coordinate from an absolute float32 direction, no lattice decision in a float ADR-0023 · Rendering
Entity writes go through World's verbs; world.entities is the read half Architecture · ADR-0025
Landedness is a consequence, never asserted Architecture
A coasting entity's epoch does not survive a move it did not make Time · ADR-0025
Saves store references, not regenerable content Persistence · ADR-0007
The star catalog is an argument, not a singleton Galaxy · Catalog
Do not store what the catalog can derive Catalog
A survey is not a search box Catalog
Issue order is not orbital order Identity · ADR-0009
No hosting SDK below the adapter Architecture · Hosting
Do not let the compiler memoize a mutable read Client
The canvas is not inside a route Client · ADR-0011
Mode is a function of the path Client
Overlays resolve location through one helper Client
Render compression is radial about the eye Rendering · ADR-0003
placement.scale is a radius, compression is the ratio Rendering · ADR-0015
A body with a figure is not also flattened ADR-0013
figure: null means round, not unknown ADR-0013
An unmeasured field is a row, in the universe's voice ADR-0014 · Planetarium
Chrome is not sized or positioned against the viewport Client · DESIGN.md
One producer of the camera Client · ADR-0011
One producer of the lens; the field of view is derived ADR-0017 · Art
One window-level key listener; actions by id ADR-0018 · UX § controls
No localStorage outside state/preferences.ts ADR-0018 · Persistence
No key name written into a label ADR-0018 · UX § controls
The aim is an offset on the pose; drag by the pixel angle ADR-0018 · Planetarium
The planetarium does not write canonical state Planetarium
Presentation asks at renderTime, not clock.time ADR-0006 · Planetarium
Mode chrome needs pointer-events-auto Client
Overlay presence is not mode="wait" Client
Do not latch a "run once" effect with a ref Client
Dock moves go through layout.ts Client · ADR-0012
A new document under docs/ goes in a wing Development · ADR-0016
A plan goes in design/, never under docs/ Development · ADR-0016
One component per file Development
Do not hand-roll a registry control Development · DESIGN.md
Cinematic effects are staging, not scripts Cinematics
A scripted camera clears the prop it stages Cinematics
Shots hand a prop over on a shared knot Cinematics
Labels are sentence case in source DESIGN.md · UX
Import three/webgpu, not three Rendering · Development
Compile-ahead goes through one recipe Rendering
Do not edit files pnpm brand writes Development
Site metadata is duplicated on purpose Hosting · Development
Third-party tags load through the analytics module Hosting
A readout neither changes the frame nor stops it ADR-0022 · ADR-0044

When a defect exposes a missing invariant, add the rule to AGENTS.md, a one-liner under .claude/rules/, a row here, and a regression test that can actually fail.

Full rules and reasoning

Rule 1

Never put an absolute position in a Vec3. UniverseVector is the only type that may claim to be one. A Vec3 is a displacement or a frame-local coordinate.

Rule 2

Never use Math.random(), Date.now(), or performance.now() in canonical code. Generation derives from seeds. Simulation depends on the integer tick. Wall clock enters at exactly one call, clock.plan, which World.advance hands the frame's delta; settle takes a count of ticks run, and nothing downstream of either sees a second.

Rule 3

Never call console.timeStamp, performance.mark or performance.measure outside engine/browserTiming.ts, and never name performance. in packages/* at all. Emit through a Timer from packages/shared/src/timing.ts; the sink is the one place that knows a platform API. Two reasons, and the first is the rule above wearing a different hat: canonical code may write to the wall clock and may not read one, and Span.end() returning void is what makes that true by construction rather than by discipline — there is no expression a caller can write that observes a duration, so no canonical value can be a function of wall time. The second is that the level, the drain and the clear are each impossible unless the set of emitters is known in one place, which is the same argument the localStorage rule makes. apps/headless/src/coreHostApis.test.ts greps for it, because performance is a global rather than an import and pnpm graph structurally cannot see it. ADR-0022.

Rule 4

Never make generation depend on order. Derive a seed from the address. Do not draw from a shared stream.

Rule 5

Never hold an algorithm version because the draw order is intact. Order protects a body's neighbors and says nothing about the body: a draw that consumes the values it always did and returns a different number is still a different universe at that address, and world.stateHash() cannot see it. If a generated value moved, spend the version. ADR-0027.

Rule 6

Never put canonical state in a React component, and never put gameplay behavior in a lifecycle callback. Components consume snapshots from apps/game/src/state/engineStore.ts. Subscribe to the narrowest slice you need, and do not add a timer of your own — one sampler owns the rate.

Rule 7

Never write a presentation switch directly. showShip, showOrbits, orbitScope, labels, flareArtifacts, chrome and the observatory's target go through engine.presentation: a mode pushes a stance on mount and releases it on unmount, a panel's override is another push, and release() restores what was underneath rather than a literal. The rule has no carve-out for the fields that look like preferences — orbitScope is read by the frame loop, which is the other half of why it cannot be React state.

Rule 8

Never construct a Worker outside apps/game/src/engine/browserWorker.ts. Tasks are typed and versioned; the pool owns dispatch, cancellation, and instrumentation.

Rule 9

Never assemble a session by hand. openSession in packages/devtools builds the world, the ship, the pool, the store, and the harness, in the one order that works. A host passes adapters in.

Rule 10

Never pass a bare Vec3 to anything that samples terrain. The argument is a BodyFixedDirection. The only producers are bodyFixedDirection, geodeticDirection, and regionDirection.

Rule 11

Never read a field value off something chosen by rank. Nearest, second-nearest, the neighbor across an edge — the value of a ranked distance is continuous and its identity is not, so a property read off the identity is a cliff along the whole locus where the ranking changes. Those loci do not run where the boundary you were guarding runs: which plate is second changes along curves through every plate's interior, which was 3,081 m of wall on Earth and 1,532 m on Proxima Centauri II with the boundary blend already correct, and a cube-sphere ring walk counts one of seven neighbors twice at each of the eight corners. Weight every candidate within a margin by a smooth function of how much farther it is than the nearest, normalize, and sum — plateProperty — or put the field on a lattice with no ranking in it, which is what craters.ts does. The weight has to reach zero before a candidate can leave the set, and the margin has to be no wider than the search that collected them. ADR-0019.

Rule 12

Never write entity state around the world's verbs. world.entities is the store's read half, so there is no update to reach for. A ship that starts moving is spawned moving — spawnShip takes the velocity — and after that it is teleport for a discontinuous move and setControl / setThrottle / setFlightAssist / killRotation for input. Each carries the bookkeeping a write needs: the interpolation history, the landed set, the rails epoch.

Rule 13

Never assert that something is landed. Landedness is a consequence of the contact test, owned by World.#land. teleport has no such flag.

Rule 14

Never let a coasting entity keep its epoch through a move it did not make. An entity with rails set is propagated from that epoch, not from its state, so a state written beside a stale epoch is a ship in two places at once. teleport, reframeEntity, a non-neutral setControl, and a frame change all drop it, and the epoch is in the state hash and the save because two worlds that agree on a state and not on its epoch part in the low bits on the next tick. ADR-0025.

Rule 15

Never persist anything regenerable. A save stores references and mutations. Generated content belongs in a cache, not a save.

Rule 16

Never make the star catalog ambient. Pass it as an argument — resolveSystem, systemsWithin, new World({ catalog }). A singleton would hide the catalog version, which is what invalidates saves when astronomy publishes. Galaxy, rule 1.

Rule 17

Never store what the catalog can derive. The packed file carries measurements. Temperature, luminosity, radius, mass, and color are computed at load.

Rule 18

Never filter a survey to serve a search box. travelTargets is a star sweep with a radius; StarCatalog.search is an index over the whole catalog. They answer different questions and only one of them can run per keystroke.

Rule 19

Never sort a system's planets by orbit and call it order. b:2 is the third body issued, not the third one out. orbitalOrder is for display. ADR-0009.

Rule 20

Never import a hosting vendor's SDK below the adapter layer. Nothing in packages/* may know what a Durable Object is.

Rule 21

Never let React Compiler memoize a component that reads mutable state. Opt out with 'use no memo'. See apps/game/src/hud/PerfPanel.tsx. This is not license to hand-write useMemo.

Rule 22

Never put the <Canvas> inside a route, and never let a mode assume it owns the page. Root keeps the browser runtime and server-rendered page shell outside every route. App owns the persistent canvas; PageShell owns routed chrome. ADR-0039.

Rule 23

Never hold the current mode in React state. It is modeForPath(resolvedLocation(location).pathname) in pages/paths.ts.

Rule 24

Never read the raw pathname when a dialog could be open over a mode. Use resolvedLocation. Links that stay inside a dialog, and controls that close one, go through pages/useOverlay.ts.

Rule 25

Never apply flattening to a body that has a figure. A figure is present exactly when a body is not a spheroid, and its mesh already carries all three measured half-extents. flattening is polarRadius / radius, which the mesh has already spent — applying it as well squashes the body a second time by the same ratio, which on Phobos is 26%. render/bodyUniforms.ts branches once, on whether the body has a figure, and everything downstream of that branch is on one side of it: the mesh's scale, and the cloud deck's and the atmosphere's shells, which are round about a figured body. bodyUniforms.test.ts holds all three. ADR-0013.

Rule 26

Never leave a field out of a record because nothing has measured it. Fact.value is nullable, a null draws as no data with its reason attached, and the panel counts them. An absent row cannot distinguish "this body has no atmosphere" from "nobody has measured this body's atmosphere", and those are opposite claims about the same world. The reason is written in the universe's voice, never in the engine's — "no magnetometer has been flown through it", not "the generator does not produce one". A projected world is real; projected is a claim about the record rather than about the place, and the planetarium is a reading room for a galaxy that is there rather than a debugger with a starfield behind it. dossier.test.ts greps every reason for the vocabulary that would break it. ADR-0014.

Rule 27

Never read a body's figure as "unknown". figure: null means round. Every planet, every large moon, Pluto and Ceres are spheroids and carry none; the ninety-two bodies in Sol that are not, and every generated body below ROUNDING_RADIUS, carry one. A renderer that treated null as "no data available" and fell back to a sphere would be right by accident and wrong the moment the fallback changed.

Rule 28

Never place a compressed body about the render origin. placeAt takes the eye in render space and compresses radially about that, and so does placePathInto, which is the same arithmetic written straight into a vertex buffer for a whole path. The origin is a snapped grid point that lags the camera and jumps; compressing about it gives every far object a parallax error that sawtooths at the rebase cadence, which is small bodies visibly vibrating in their orbits. ADR-0003.

Rule 29

Never scale metric geometry by placement.scale. It is the drawn radius in meters, which is what a unit sphere wants; anything carrying its own geometry wants placement.compression, the dimensionless ratio beside it. Terrain is the case that found this: a patch's vertices are true meters from its anchor, and multiplying them by a radius rather than a ratio put them 10^12 m away. The two fields sit next to each other and only one of them reads like a factor. ADR-0015.

Rule 30

Never size or position chrome against the viewport. No 100vh, no 100vw, no env(safe-area-inset-*) at a call site. .hud-layer spends the four insets as offsets — padding cannot do it, because an absolutely positioned child resolves against the padding edge — so an absolute child is already inside them; a surface that is picture and must reach the display's own edges says so with hud-bleed, and a corner readout inside one of those is laid out in flow so the padding can reach it. The document itself is 100dvh and cannot scroll.

Rule 31

Never add a second producer of the camera. The order is cutscene, then observatory, then the walker, then the ship, written once in engine/frameOwner.ts and resolved once a frame by GameEngine.#step, which publishes the result as engine.owner: the arm, its eye, and the player's ship. No arm of that order may depend on a later one resolving. Only the walker and ship arms need a player, only the ship arm hands buildScene an entity as its eye, and buildScene takes the owner's eye, so a playerless observatory frame is drawn like any other. A consumer switches on engine.owner.arm and names the ship by engine.owner.ship; it never re-derives either from engine.cinematic === null or isCamera, which on foot is the walker.

Rule 32

Never add a second producer of the lens. It follows the camera's own precedence through engine.lens: the cinematic lens wins, an active fixed galaxy instrument supplies its declared lens within the observatory arm, and the flight lens is the fallback. Ordinary observatory framing reads framingLens(), the flight lens alone; a fixed galaxy instrument owns its pose and optics together. Entering or leaving that instrument preserves the flight lens. ADR-0038 records the active galaxy view. The field of view is derived from focal length, gauge and zoom and is never stored beside them; CameraRig writes camera.fov and nothing else does — the aspect it writes beside it is the viewport's, not the lens's — never filmGauge or setFocalLength, because Three's gauge is the sensor's long side divided by the aspect ratio and a lens whose angle moved on a resize would move the terrain selection, the observatory's standoff and every composed shot with it. A consumer that cannot see the lens is a bug, not a case to have a default for: a camera.fov ?? 65 fallback fires exactly when the camera is not a PerspectiveCamera, which is when the picture is least like the one any fixed angle describes. <Canvas camera> sets the initial angle from DEFAULT_LENS and is the one exception, because it is a constructor argument rather than a writer. ADR-0017.

Rule 33

Never add a second window-level key listener. input/keymapStore.ts owns the one keydown in apps/game/src, and there were six: two read event.key and four read event.code, which is why + carried a comment about Shift. A mode registers a handler for an action id (useAction) and declares which context it is (useKeyContext); it never sees a key. Conflicts are checked against LIVE_SETS — every set of contexts that can be live at one moment — because global is live alongside everything, and "conflicts within a context" misses exactly the pair that shipped as a bug: Space was the pause key and the cinema transport, both handlers ran, and clock.paused flipped twice. ADR-0018.

Rule 34

Never call localStorage outside state/preferences.ts. Every key is declared there once — default, guard, revive, migrate, group — and a call site takes the definition rather than a key string, so an unregistered preference is a name that does not resolve. The rule is not tidiness: the export, the import and the live subscription each have to know the whole set, and none of them is possible with the calls spread out. An import reaches mounted hooks through that subscription, because a reload here rebuilds the renderer.

Rule 35

Never write a key name in a label. Not in a title, not in an aria-label, not in a help table. useActionTitle(id, text) and KeySheet read the live chord, which is the only kind of help a rebindable build can have — and the two hand-maintained tables of prose that named keys as string literals are exactly what /settings/controls was printing while it said rebinding was not built.

Rule 36

Never turn the head at a constant radians-per-pixel. Drag sensitivity is pixelAngle(lens, viewport), so the ground under the pointer follows the pointer; a bare constant swung the frame through three of its own field-widths on a 100 px drag at 8× zoom. The aim is a LookOffset on the pose, and it is cleared by whatever replaces the pose — a focus, a frame, a stand, a composed set of angles — and by nothing else, so a viewer who turned to look at Io beside Jupiter is still looking at Io after the wheel. ADR-0018.

Rule 37

Never let the planetarium write canonical state. The observatory resolves an address, asks the world where that is at renderTime, and returns a pose. No teleport, no clock, no entity write, no save. Planetarium.

Rule 38

Never ask where something is at clock.time in order to put it in a frame. Live presentation happens at SimulationClock.renderTime — one tick back, plus the interpolation alpha. A photographic preset supplies an explicit observatory instant to the same snapshot, camera, terrain and orbit traces (ADR-0033). clock.time is the tick, which moves in 1/64 s steps. Anything that places, points at, aims at or measures against a body for the picture uses the same instant the picture is drawn at, or it is aiming at where that body used to be by its velocity times up to 15.6 ms, sawtoothing as alpha resets. The cost is that error in units of the thing's own radius, so it is invisible on a planet and enormous on a small fast moon: at clock.time the observatory vibrated Phobos and Deimos by 11 and 19 pixels in the planetarium at 1×, while Mars and Luna held inside a twentieth of a pixel. Three sites have now had to learn this — the terrain streamer, the observatory, and the orbit traces — and nothing mechanical catches a fourth, so it is a rule rather than three comments. ADR-0006.

Rule 39

Never give a mode its chrome without pointer-events-auto. .hud-layer is pointer-events: none so the scene stays reachable.

Rule 40

Never give AnimatePresence mode="wait" over the overlay routes, and key it on the dialog's surface (overlaySurface), not its pathname.

Rule 41

Never guard a "run once" effect with a ref. Reconcile against the state's actual owner (observatory.target?.address === wanted).

Rule 42

Never move a workspace panel by splicing an array at a call site. dock/layout.ts owns every move. Use the updater form of the setter. ADR-0012.

Rule 43

Never derive a stored value from a captured snapshot. set((held) => ({ ...held, field })), never set({ ...value, field }). Two writes composed against one snapshot discard the first, and the window is not a frame: a preference drawn in two places — the dock's panel and its /settings twin — holds two snapshots of one key for as long as both are mounted. The dock's splice rule above is one instance of this, not the general case.

Rule 44

Never put two components in one file. react/no-multi-comp is an error. A constant or type goes in a sibling .ts. Exemption: apps/game/src/components/ui/*.tsx.

Rule 45

Never hand-roll a control the registry already has. Go through hud/Action.tsx, hud/SwitchRow.tsx, or hud/TransportButton.tsx.

Rule 46

Never add a markdown file under docs/ without listing it in scripts/docs/wings.mjs. Everything there is published at /docs and the wing table is what says where, so a file no wing lists is one the site cannot place — pnpm docs:build refuses rather than guessing, and takes pnpm build and pnpm check with it. An ADR is the common case, and the rule has already caught its own author: the record arguing for the documentation site was the one page the site would not publish. ADR-0016.

Rule 47

Never write a plan under docs/. Plans, working reviews and tooling reports go in design/, which is outside the published tree. docs/ is the finished account of what the system does; a reader who reaches a documentation page expects the system to already behave that way, and a plan promises the opposite. Once a plan's phase lands, the decision it settled belongs in an ADR and the plan loses the section — the ADR is what the code and the guides then cite.

Rule 48

Never let a cinematic effect fire off a script. An effect is staging. It belongs in CinematicEffects, where a shot turns it on, and it is 0 everywhere else. Cinematics.

Rule 49

Never fly a scripted camera through the prop it is staging. Beats are authored in screen terms, so nothing in them says how close the camera comes to the geometry — and a frame-diff against a reference cannot tell you, because the inside of a hull is as large and as lit as the outside. It is checked against the shipped asset, headlessly, by apps/headless/src/hullClearance.test.ts, and it is a test rather than a clamp: a director that quietly pushed the camera out would make an authoring mistake invisible and put a conditional inside the one thing a scripted scene has to be, which is reproducible. Cinematics.

Rule 50

Never treat a beat past a shot's last frame as dead. A Catmull-Rom segment is shaped by the knot beyond its far end, so exit beats authored "after the cut" set the tangent of the segment the shot still renders. tng-intro's cruise flew an entire warp-out across its own last twelve frames this way — 432 m to 17.4 km in the clear — and the next shot's entry knot snapped the hull back. Where two shots hand a prop over, they share the knot: change one and change the other. Cinematics.

Rule 51

Never write a label in the case you want to see it in. Source strings are sentence case; text-transform on the type step decides what is shouted.

Rule 52

Never subtract two planetary radii from each other in a shader, and never take a screen-space derivative of a planetary position. At Earth's radius one float32 step is half a meter, so length(anchor + local) − radius arrives quantized to half a meter — and the morph walks it across those steps every frame, which is a coastline visibly warping several times a second from two kilometers up. Use (2(a·l) + l·l)/(|p| + |a|), which never lets the large numbers meet. The derivative is worse than the value: a tenth of noise and biased per patch, which moved the albedo map's mip level at every patch boundary. And local is linear across a triangle, so dFdx(local) is constant over the whole triangle — a detail fade measured that way steps per polygon, where distance times the lens's pixel angle does not. A varying may not take an attribute's name either: both become identifiers in the generated WGSL, and the redeclaration surfaces as [Invalid ShaderModule "vertex"] with the real message on a channel the page console does not carry. ADR-0020.

Rule 53

Never read the drawn ground where the canonical one belongs, or the reverse. groundElevation and surfaceRadius are the field the contact test integrates, the saves record and the survey sites name; drawnElevation and drawnSurfaceRadius are that field plus a presentational tail, and they are what the material and any camera that composes a picture of them are made from; drawnGroundElevation is the same with the sea clamp left off — the seabed — and is what the mesh is made from where the sea is a sheet drawn over it, and only there: a mapped body has no sheet, its photograph is its sea, and its mesh keeps the clamp, which the heightfield request says with seabed. drawnElevation and drawnSurfaceRadius differ from the canonical field by at most drawnDivergence, which is 1.25 m; drawnGroundElevation is below it by the depth of the sea. Physics reading the drawn one puts a landing behind a term the renderer is free to change; a mesh reading the canonical one draws a plane at two meters, because the tolerance a patch is refined against is the amplitude floor the canonical field stops at, so nothing under it can ever deepen the selection. ADR-0021. Under a walker the two are read in one place: bodyGround in packages/rendering/src/ground.ts answers both along a ray, its canonical half is supportUnder — the function World.contactRadius reads — and its drawn half carries a deck onto the drawn ground at its anchor, which is where the scene draws the model.

Rule 54

Never give two attribute names one BufferAttribute object. Two vertex-rate attributes sharing one object is a pipeline that does not build. It reports [Invalid ShaderModule "fragment"] is invalid due to a previous error, with the real message on a channel the page console does not carry and the canvas never presenting — and warmCompile swallows its rejection, so a warm-up making the same mistake fails silently first. The same aliasing on an instanced attribute builds, which is how it was isolated; the mechanism is unexplained and the rule is deliberately the wider claim. Two attribute objects over one array is a few bytes and one fewer trap. ADR-0021.

Rule 55

Never call geometry.dispose() on anything holding the shared index. Go through render/groundWear.ts's disposeKeepingSharedIndex. Every patch geometry references the one session-wide index attribute, and three's dispose path destroys each referenced attribute's GPU buffer with no refcount — so disposing one evicted mesh kills the 98 KB index under every patch still drawn and re-uploads it next frame. It is the churn the shared attribute exists to prevent, it is silent, and the ground, the sheet and the orbital bake all evict through the one helper. ADR-0021.

Rule 56

Never leave a stand-in DataTexture at its nearest default. Every material here runs one graph whether or not its maps have arrived, on the strength of a 1×1 stand-in, and the boot warm-up compiles the object the first time it exists — before setTextures has run. That program is then frozen and the real map is bound into it: a TSL texture() node's value swap changes the binding and nothing else, no cache key observes it, and no WGSL is rebuilt. Measured on the device — a node built over a nearest 1×1 compiles textureLoad with no sampler, and after assigning a linear map the fragment shader is byte-identical. So a nearest stand-in is not a warm-up that misses; it is the filtering the body then draws its 8K albedo with, at mip 0, point sampled, with no anisotropy. The ground's version has no textureLoad path at all: the gradient sample names a sampler that was never declared, Tint refuses the module, and standing on a mapless body streams 706 patches into a black frame with [Invalid ShaderModule "fragment"] on the console. Set both filters linear; materials.gpu.test.ts holds each stand-in and a real map to one program.

Rule 57

Never build two texture nodes over one stand-in object. The builder shares a uniform between nodes whose textures carry one uuid — TextureNode.getUniformHash is the uuid, and UniformNode.generate hands every later node with that hash the first node's binding — so two nodes over one stand-in compile to a single binding owned by the first, the warm-up freezes the program there, and the second node's value swap binds nothing. The sphere's relief record read its slopes and its sea mask out of the reflectance that way: an icy moon's 0.8 of albedo was a sea mask of 0.8, and Enceladus was a dark disk under a sun-glint, over a bake that read back correct to three places. One stand-in object per node — RING_WHITE beside WHITE, BLANK_RELIEF beside BLANK_REFLECTANCE — costs a few texels, and materials.gpu.test.ts holds a bound bake to the stand-in's binding count.

Rule 58

Never take a fine lattice coordinate from an absolute float32 direction, and never take a lattice decision in a float. The GPU tile producer is a port of drawnElevation — of drawnGroundElevation where the request says seabed — held to a measured bound; two things make that bound small and both are easy to undo. A float32 unit vector resolves 6 × 10⁻⁸ of a radian and the tail's one-meter crater on Luna subtends 3 × 10⁻⁷, so direction · cells from an absolute direction quantizes every fine rung to a fifth of a crater and does so differently on either side of every patch edge — a rung reads its tile's frame, the cell and fraction the patch center falls in from float64 (writeTileFrame), plus the sample's own offset. And whether a cell holds a crater is a step: the sphere test's inputs are integers over one float, so successive cells sit 1/cells² apart — 2 × 10⁻⁷ at 2,300 cells, 10⁻¹² across the tail — and a float32 comparison lands on the wrong side wherever that is under its resolution, which was a whole crater on one processor and not the other, 44 m on Luna. The test is Σ m² > floor(cells²) in 48-bit integers on the GPU and in exact float64 on the tail's CPU path (ChordForm), and a crater exists when its hash is under a u32 threshold, never when toUnit(hash) < density. ADR-0023.

Rule 59

Never add a shading term to the ground without adding it to the sphere. render/terrain.ts and render/planet.ts draw the same body on either side of the eight-pixel relief gate — the streamed ground below it, the archive's sphere above — so they share the lunar-Lambert split, the terminator and the published photograph, and a term on one side alone is a step at the switch a descent flies straight through. Measured across it: 3.1% apart in mean value on Mars, 1.5% on Earth. Three terms had to be matched to get there — skylight comes out of the direct beam rather than beside it, the aerial veil the disk wears is carried by the ground as well because the shell only survives outside the silhouette, and where a photograph exists it supplies the albedo outright. Each was 15%, 48% and 9% of the drawn value on its own. ADR-0020.

Rule 60

Never import from three in apps/game. Import three/webgpu and three/tsl. packages/* may not import Three.js at all.

Rule 61

Never hand-write a compile-ahead. render/warmup.ts owns the recipe — the visibility toggle compileAsync silently needs, the renderer cast, the swallowed rejection — and the census the boot progress totals. Registration is idempotent by label, because StrictMode does everything twice.

Rule 62

Never edit a file pnpm brand writes. The mark is design/brand/brandmark.svg. pnpm brand:check is in pnpm check.

Rule 63

Never change what the site says about itself in only one place. src/site.ts supplies route metadata to the server-rendered head and to pages/DocumentMeta.tsx for client navigation. documentHead.ts emits the shared head that scrapers read.

Rule 64

Never load a third-party tag from the document head. src/analytics.ts is the gate: production build, canonical host, no Global Privacy Control.

Rule 65

Never let a readout change the frame or stop it. A census, a diagnostic getter, a timestamp or a byte total is an instrument, and an instrument that can throw or can write is a way for the scene to disappear over a number nobody is reading. The upscaler's byte census wraps the device's own createTexture, so its throw on an unrecognized format abandoned configure() with the kernel half-allocated and then threw on every frame after; it over-counts an unknown format and warns. The sensor's diagnostics getter read the drawing buffer into the closure-scoped vector that is the submitted frame's viewport — the reference the defocus circle, the signature and the meter's readback are interpreted against — so a property read between submission and readback moved it on a resize frame; the report has its own vector. Reach for a library's private field with ?., count an unknown quantity generously, log, and hand back whatever the caller can still use. ADR-0022, ADR-0044.