1
0
Commit Graph

15 Commits

Author SHA1 Message Date
karti 8fb85cd2e5 feat: give the boards a horizon, a sea that reflects, and a state worth flying over
The wide shot, which is what an anonymous visitor actually lands on.

**The sea was `MeshLambertMaterial`** — a material with no specular term at all,
by construction — on a board where water is half the frame. It is now a
low-roughness dielectric that reads `scene.environment`, with a runtime-generated
tiling swell normal map sampled twice per fragment at two scales and two
headings, so the sun breaks into a moving glitter path instead of a mirror point.
An `onBeforeCompile` patch takes the body colour toward the deep value looking
straight down and leaves it to the reflection at grazing, and walks roughness up
past 1.6 board spans so the far water cannot shimmer.

The swell spectrum is 1/k^2 and not 1/k because the first attempt was
photographed: at 1/k every component carries the same slope, the shortest wave
wins, and the sea renders as hard diagonal corduroy. A test holds it now.

**The board no longer ends in a diamond.** The sea plane went from 1.8 board
spans to 18, past the fog's far plane from anywhere the orbit reaches, and the
sky is a world-space dome rather than a screen-space gradient. That gradient was
wrong in a way dusk made obvious: the sunset band was painted along the *bottom*
of the picture, under the board, while the true horizon at the top of frame stayed
zenith blue. `daylight.ts` pinning the horizon stop to the fog colour to hide the
seam was a symptom of it.

**Terrain casts shadows.** Left off before because double-sided terrain against a
~16 m-per-texel shadow map gives acne; `shadowSide = BackSide` is the cure, shot
at four sun elevations down to +0.0 degrees to confirm no stippling. The caster is
a stride-2 decimation appended to the same index buffer and swapped in by
`onBeforeShadow`/`onAfterShadow` via `drawRange`: no extra draw call, a quarter of
the depth cost, and indistinguishable from the full-resolution caster in a
side-by-side crop. Stride 1 was measured at +65,566 triangles and would have
missed the budget by ~47,000, so it was not shipped.

**California reads as California.** It was a beige kite: the eastern edge one
ruled line for five degrees of latitude, the south closing in a diagonal V, the
whole south-east a featureless tan wedge. Now the coast runs to the Mexican
border with San Diego on it, the eastern edge follows the Colorado and the Nevada
diagonal, and the south-east is the Basin and Range — forty parallel desert ridges
throwing shadows east, Death Valley as a white pan between the Panamints and the
Black Mountains, the Salton Sea the one cool value for two hundred kilometres.
The opening pose is retuned to the bigger board; the old 452/392 stand-off left a
slab of empty ocean where the state should be.

**The aircraft were six pixels.** Measured, by enlarging a screenshot 200% to
find one at all — indistinguishable from a dead pixel, on a board whose entire
claim is that the sky is live. They are airliners now, with planform and trail,
and clicking one raises its card for a signed-out visitor.

**The Model X is off the wall.** It stood at floor level outside a studio 188 m up
a Transbay tower, reading as a car balanced on a parapet. The apron is now chosen
from `site.elevation`, which the pack already carries — not from an office id,
which is the bug class this repo already hit once when a door marker gated on
`id === "sf"` and would have pinned the Los Angeles building to San Francisco.

Also fixed, and nearly shipped: sea z-fighting dithered every flat piece of ground
on the Bay Area and SoCal boards. And one test asserted an exact source line for
the water material, so the better multi-line implementation failed it — it now
asserts the property (dielectric, metalness 0, low roughness) rather than the
author's first guess at formatting.

Tests 964 -> 1015. California desktop 562/650 draw calls and 728,744/750,000
triangles — 2.8% of triangle headroom left, which is the number the next person
should check first. No budget was raised.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-21 23:43:14 -07:00
karti db074e9cf7 feat: tone-mapped render rig, studio devices, LA fidelity pass, UI overhaul
The build the studios needed, across eight workstreams and one strict file
partition.

**The render rig was the quality ceiling.** The renderer ran three's
NoToneMapping default while atmosphere drove the sun to 2.35 and assets set
emissives to 3.2, so every value above 1.0 hard-clipped to flat white — which is
why walls blew out and every fitting looked like a white rectangle. ACES filmic
tone mapping and an explicit output colour space land in `stage.ts`, and the
atmosphere intensity table and palette headroom are re-tuned against the new
curve rather than left tuned for the clipping we removed.

`engine/environmentRig.ts` builds a PMREM environment at runtime, procedurally,
so nothing binary is committed. There was no environment map anywhere before, so
every `metalness > 0` role had nothing to reflect and rendered dull grey — a
defect the code already documented against itself in `office/optimus.ts`, where a
whole material role was abandoned over it, and worked around in `modelX.ts` with
a fake emissive that this change deletes. Atmosphere remains the sole light
owner; the rig derives from the `LightingState` it already produced.

**Studio hardware exists.** There was no device concept anywhere in the product:
no type, no route, no state. `devices/types.ts` fixes a declaration/state/
capability/command contract that a smart light, a thermostat, a door sensor and a
charger all fit without a schema change, and both studios now carry a desk mic
and a computer speaker with deterministic simulated behaviour behind an adapter
seam a real API can occupy later. Reads are the demo and are open; commands are a
signed-in action and are kept off the read body entirely, because a shared cache
replaying a GET that turned a microphone on is exactly what the fail-closed
cache default exists to prevent.

**The ADS-B licence hole is closed.** `TERA_ADSB_ENDPOINT` accepted any URL, the
response was served publicly cacheable, and the attribution hardcoded adsb.lol
regardless of where the endpoint pointed — one env var away from republishing
non-redistributable data under an open-terms credit. The host is now allowlisted,
the credit is derived from the host actually configured, public cacheability is
conditional on redistributability, and a refused endpoint demotes to simulated
flights and says so in `degraded[]`. The gate is on the source, not the feature:
live aircraft and their detail cards stay open to anonymous visitors.

**The LA studio was never the smaller pack** — 16 rooms and 248 props against
SF's 4 and 28. Its deficit was fidelity per square metre: 98 of those props were
ceiling troffers, it bound no props to seats, placed none of the habitat kit, and
12 of its 16 rooms had no viewpoint. Density comes from new asset kinds rather
than more instances, because `furnish.ts` draws once per kind and folds colour
into the batch key, so repeat instances add nothing the eye can read.

**The interface stops being forty imperative mutations.** Every visibility
decision moves into a pure, tested `ui/chromeState.ts` and one applier, so the
chrome has coverage for the first time. Deleted: ~100 lines of CSS and two
bindings targeting elements that no longer exist, and a `body:has()` rule that
shifted the desktop layout by 160px for touch controls hidden there. Fixed: the
office picker tabs that drew their label and their badge on top of each other.
Added: a first-run flow, because the product is two verbs and neither was ever
stated on screen. Mobile is designed on its own terms instead of being the
desktop with things hidden — the plan view comes back, and the keyboard-only
shortcuts button is replaced by touch controls.

`arena/studioOps.ts` frames the whole thing as the multi-variable environment it
is, wrapping the same simulators the renderer drives rather than a headless copy.

Also removed `input/vehicle.ts`, which nothing but its own test imported.

Tests 385 -> 961, all passing. Typecheck, build, performance budgets across six
matrix cells, no-binaries, provenance, dependency licences, zero-config boot and
arena source hashes all green.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-21 19:44:24 -07:00
karti e378a03740 feat: add deterministic office robot jobs 2026-08-19 02:56:30 -07:00
karti fc1f500019 perf: lazy load authenticated office peers 2026-08-11 20:29:06 -07:00
karti 92ebf8abb5 feat: add authenticated realtime presence 2026-08-11 20:22:12 -07:00
karti c0c7fcf974 feat: add character studio and aircraft foundation 2026-08-11 19:32:32 -07:00
karti a2a52bfdae feat: add playable actors and seamless journey state 2026-08-11 19:10:14 -07:00
karti 2d87d9f354 The city points at its own buildings, and the sky stops depending on an API
**Clouds were invisible to everyone who had not wired up NWS.** The layer
took `currentWeather()?.cloudCover ?? 0`, and `currentWeather()` is null on
any deployment without a weather source — which is the default, and the
exact configuration this repo is held to: a stranger clones it, runs one
command, and gets a city with no account and no key. Their sky was
permanently, silently empty. `atmosphere.ts` already models a sky when
nobody has observed one; it now models cover too, an observed reading
still wins outright, and the clouds are there on a bare clone.

**Both offices are pins on the city, and clicking one walks you in.** Each
pack has carried a real `site` since the sun needed one, and that
coordinate was known to the lighting and to nothing else — a visitor
looking at the board had no way to tell that two of those buildings are
ones they can go inside. The coordinates move to a tiny eagerly-imported
`offices/sites.ts` that the packs import *from*, because a pack is a 25 kB
lazy chunk and the board wants its pins long before anybody opens a door.
A test asserts the pack and the table hold the **same object**, not merely
equal values: a drifted coordinate would put the marker on one building
and the sun on another and both would look entirely plausible.

**Aircraft bank into their turns.** The roll channel existed and was never
written, so every turn was flat. Bank comes from the coordinated-turn
relation against the measured turn rate, damped by a first-order lag so it
settles rather than oscillates, and clamped at 30° like a real limiter.
Six regression tests, because roll is the one channel that feeds itself —
position and heading are recomputed from the last two observations and
wash out a bad value, while a NaN in the roll would persist for the life
of the track.

That fed straight into a real defect: `AdsbFlights` substituted
`heading: 0` for records with no `track` field, which is harmless for a
symmetrical dart and is a **sustained full-scale artefact** once aircraft
bank — a target whose real heading is 200° reported as 0° reads as a 160°
turn and pins the roll at its limiter for as long as it is in the feed.
Those records are dropped now. An aeroplane the feed will not give a
heading for is one this layer cannot draw honestly.

**The office empties out overnight.** A full complement of seated people
at one in the morning, under house lights that came on because the sun is
down, was the least believable thing left in the room once the clock
became real. A live roster always wins — an API that says the building is
empty is telling the truth about the building.

**Robots go somewhere.** They pick real addresses — a seat, a room — and
turn to face the seat when they arrive, rather than stopping at a random
angle. Godmode gets an office section: house lights forced on or off or
following the sun, robots and ceilings toggled, with a readout.

**The bundle is split.** Entry chunk 758 kB to 208 kB, with three.js and
satellite.js in a vendor chunk that survives an app deploy instead of
being re-downloaded on every one. Rollup's 500 kB warning still fires and
should — it now points at three.js, where it is true, instead of at our
code, where it was pointing at three.js all along.

Reviewers caught two false geography claims in the new prose ("both
shipped buildings stand in San Francisco" — one is across the estuary at
Alameda Point) and several miscounted figures. Fixed. In a codebase where
the comments are the design record, those are defects.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-07 04:18:00 -07:00
karti 51979feea0 Shadows land on the building, and the sky layers stop repeating themselves
**Shadows were the right size and pointed at nothing.** Last round fixed the
missing `updateProjectionMatrix()`, so the frustum finally became the size
every caller asks for — but nothing aimed it, and `sun.target` sits at the
world origin. A pack's origin is the **north-west corner of its slab**, so
for lumbridge-hq the box was off-centre by half the building: 14.4 m of a
48 m plate, about a third of the floor, fell outside the frustum and
neither cast nor received. Invisible while three's broken ±5 default made
shadows useless everywhere; obvious the moment they started working.

`SceneKitOptions` takes a `shadowTarget` now, both callers pass one, and
the light's target is added to the scene — which is the part that actually
matters, because `LightShadow.updateMatrices` reads `target.matrixWorld`
and an unparented `Object3D` is never reached by the traversal that
updates it. The sun is also placed relative to the target rather than the
origin, so light-to-target is exactly `sunDistance` for every direction,
which is the invariant each caller's `shadowNear`/`shadowFar` were chosen
against.

**`flights.ts` could not be tested, and that is why it was untested.** It
used a TypeScript parameter property — the one piece of TS syntax that
*emits code* rather than annotating a type — so Node's type stripping
refused the whole module. The bundler never cared, so nobody found out
until the first `node --test` file tried to import it. The module carrying
the worst bug this project has shipped was, by construction, the one
module that could not have a test. It has eleven now, including one that
fails if the live-aircraft repeat-skip is removed.

**Robots are on the plan panel** — a turned marker with a bow for heading,
in the one hue left that is neither the people-blue nor the camera-amber.

Review findings cleared across the four new sky/robot modules: a real
24 mm void at the ankle and an 8 mm hole through each forearm, a
per-frame allocation in the robot heading picker, a per-frame sort in the
starlink ranking, `uTime` growing unbounded until the cloud breath
quantises, and `DAY_REFERENCE`'s derivation which did not reproduce.

`createStarlinkMeshLayer` now takes a **board** radius — the same unit its
sibling takes — instead of a dome radius with nothing in the types to tell
them apart. That is the exact confusion that has already caused one real
bug here. `DOME_RADIUS_FACTOR` has one owner and is imported, not copied:
the points and the meshes must be on the same dome or a satellite that
grows geometry also jumps.

Several comments were wrong rather than merely stale — a fabricated claim
about `Object3D.clone`, a fabricated attribution to `Plan`, an inverted
`DoubleSide` argument, a triangle ledger citing a function that no longer
exists, and a defensive-call rationale that contradicted the paragraph
above it. In a codebase where the comments are the design record, those
are defects.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-07 02:50:25 -07:00
karti af0d4a7d57 The office keeps its lights on, and something walks around under them
**Lights.** A sited office follows the real sun, and the real sun spends
half its time below the horizon — which was producing a technically
correct and completely useless picture: an unlit floor plate at midnight
in a building whose whole premise is that you can see who is at which
desk. `luminaires.ts` brings the diffusers up as the sun goes down and
reports one scalar for how much interior light there is; `withHouseLights`
adds it to the rig. CONTRACT §4's rule that a fitting emits no light is
kept in full — nothing here is a light source, and the rig still has one
owner.

**And they notice you.** A fitting within four metres of somebody walking
underneath brightens and fades back as they leave, which is what an
occupancy-sensed floor actually does at night. They are one `InstancedMesh`
sharing one material, so `emissiveIntensity` cannot vary between them —
`instanceColor` can, but three multiplies it into the diffuse term only, so
six lines of `onBeforeCompile` carry it into the emissive term as well. The
alternative was one mesh per fitting: forty draw calls of ceiling in a
building that spends about twenty on everything.

**Optimus.** A posable Gen-3 humanoid — eleven articulating joints, pale
shells over a dark frame, a black visor — with a walk cycle driven by
*distance travelled* rather than wall-clock, so the feet do not slide when
a robot slows down. Two per floor, derived from the pack's levels, so the
two-storey tower gets four and the hangar gets two without either pack
knowing robots exist. They wander between reachable points using
`Plan.blocked` — the collider the wall split already produces — and they
are deliberately **not** gated on `depth`: the build-time-exclusion rule is
about occupancy, and a robot is nobody.

**Starlinks stop being pixels.** The sixty-four nearest the centre of view
grow real geometry — a flat bus with ONE large solar array, which is the
actual signature and the thing everybody draws symmetrically and wrong —
fading in so there is no pop where a point becomes a mesh. Two draw calls.
The sun for their attitude comes from `solar.ts` and not from the rig,
because `atmosphere.ts` floors the light direction to keep the shadow
camera usable, and a sun ten degrees *down* is exactly the dusk geometry
that makes a pass visible.

**Aircraft** are airliners now — swept wings, nacelles, a fin — instead of
an arrowhead, still one shared geometry facing +Z as `flights.ts` requires.
**Clouds** drift over the board, driven by observed cover, lit by the rig
rather than by themselves.

Four modules were built by subagents and reviewed by another; every one
came back `needs-work` and the reviews were right. Fixed before wiring:

  - The walk cycle's arms were a quarter cycle out of step with its legs —
    the legs are cosine-shaped and the arms were on `sin`, so at the
    instant the left leg reached full forward the left shoulder was at dead
    neutral. Uncanny, and hard to name until it is pointed at.
  - Every Optimus shell used a `roundedBox` radius of 0.12–0.22, which that
    primitive turns into a near-circular cross-section — the figure was
    built out of lozenges, not panels. The rest of the library uses
    0.02–0.09.
  - The cloud material was `transparent` + `DoubleSide` without
    `forceSinglePass`, so three rendered it twice per frame *and* bumped
    `material.version` on each pass — rebuilding the program cache key
    forever, on the one layer that is fill-rate bound.
  - `starlinkMesh.dispose()` freed the geometries but not the
    `InstancedMesh`es, orphaning their instance buffers on every city
    switch.
  - The airliner's tailplane roots sat outside the tail cone and hung in
    free air over most of their chord.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-07 00:56:49 -07:00
karti 06455f7424 The office learns where it stands, and the sky stops being a backdrop
**Aircraft actually move now, and the reason they did not is the headline.**
`HttpFlights` holds a frozen snapshot between network refreshes and is
polled at 1 Hz, so a live feed handed the layer the same position five to
fifteen times and then jumped. `span` therefore measured the poll interval
rather than the gap between the two positions that differ, the teleport
test saw an airliner covering 36 units in a "second" against a ceiling of
8, and **every live track's history was wiped on every refresh** — so no
aircraft on the deployed site could ever grow a trail, however long
TRAIL_POINTS was set. Skipping the repeat fixes the motion and the trail
at once. Trails then go to 72 points / 240 s, which is about seventy
seconds of flying.

Three more defects in the same file, found while looking: trail
truncation dropped the segments nearest the aircraft (leaving a streak
with no aeroplane attached), MAX_TRACKS was declared and never enforced,
and one missing target deleted its whole trail. The buffer now uploads
only what it wrote, rather than 46 MB/s of untouched array.

**You can get above the constellation.** Dome to 1.05 board *radii* and
the orbit to 2.0 spans. Radii, not spans: scene space is centred on the
city and the Bay Area board runs forty kilometres down the peninsula, so
the furthest corner is 0.94 spans out where the half-diagonal is 0.65 —
sized off the half-diagonal the dome sat inside its own city. The far
plane goes to 4 spans to stop clipping the sky from off-centre chapters,
and `PointsMaterial` defaults `fog: true`, which was quietly dimming the
whole constellation with the city's haze.

**An office can say where it stands.** `Office.site` — lat, lng, height
above the ground outside, and the compass bearing the pack's −Z points
along — and with one it gets the same sun the city does, a sky, and a
horizon at `-elevation`. CONTRACT §4 reserved this as "a later
refinement"; it is taken up rather than overturned, and `daylight.ts`
computes no light of its own. It does the two things a room needs that a
map does not: turn the sun into the building's frame, and move the fog
outdoors before it greys out the far wall.

Two buildings now, and they are deliberately unalike: Lumbridge HQ 188 m
up a Transbay tower facing 205°, and **Frontier Valley**, a startup in a
hangar at Alameda Point — one room, 54 x 30 m, nine metres to the
trusses, four metres above reclaimed ground.

Floor-to-floor in the reference pack is now 16.8 m: the interstitial is
ten times a real one, so the space between the slabs is somewhere things
can hang. It is frankly not architecture, `PLENUM` is the one number to
change, and the file says so.

Also fixed, all found by review rather than by looking at the screen:

  - `sun.shadow.camera.updateProjectionMatrix()` was never called, so
    three's default ±5 unit box has been in force this whole time and
    every `shadowExtent` this repo passes — including the city's ±752 —
    has been silently ignored.
  - A missing aircraft was kept alive by the new grace period and *drawn*,
    so it froze in mid-air at full opacity for 32 s.
  - Frontier Valley's mezzanine was a `Room`, which carries no height: its
    slab lay on the concrete, its chairs floated 4.4 m over it, and its
    balustrade fenced off a patch of ground floor. It is a `Level`.
  - Overlapping floor slabs z-fought. The format permits overlap and
    resolves later-first, so `shell.ts` now lifts a slab a hair per
    earlier slab it overlaps — and by nothing at all in a pack, like the
    reference office, whose rooms only ever abut.
  - `switchOffice` bypassed the `entering` guard (leaking a whole scene
    per double-click) and tore down the old room before knowing the new
    one would load, with no way back.

Known and not fixed: raising MAX_SPAN to 30 s doubles the worst-case
re-base snap when a feed's gap shortens. It is bounded, pre-existing in
kind, and the fix wants carrying the live head into the next leg.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-06 23:30:45 -07:00
karti 270cddda31 The phone can reach the plan, read the disclosure, and see the room
Three things, all on the door people actually open. office.lumbridgecorp.com is
a link you send somebody, and a link somebody is sent is opened on a phone.

**The plan had no way in.** The stylesheet builds it a whole bottom-sheet layout
below 600px — full width above the rail, 38dvh tall, hover-only text dropped —
and the only thing that could open it was the `M` key, which on a phone is not a
control that is hard to find but a control that does not exist. The comment
above that layout says "it is still behind `M`, so it costs nothing until it is
asked for", and on a phone it could not be asked for. So there is a button, in
the one cluster a thumb reaches, at every width rather than only the narrow ones
— a control only a keyboard can reach is a control some people do not have, and
that is as true at 1920 as at 390. It is glyph-only, because the `M` hint beside
it already carries the word and two controls labelled "plan" in one bar is one
too many, and it carries `aria-pressed` for the same reason the chapter buttons
do: a toggle that does not look like its state is a button that appears to do
nothing on every second press.

**The disclosure was behind a hamburger.** "Sample occupancy — these people are
invented" lived on `#office-badge` inside `#panel`, and on a phone `#panel` is a
sheet that starts closed. The one sentence standing between twenty-five invented
people at real desks and a screenshot presented as a staff list was hidden, on
the device most likely to take the screenshot. It moves to `#source`, which is
where this page already says what on screen is and is not real, is fixed and
always drawn, and which the phone stylesheet itself calls "the one caption that
is never allowed to be dropped for space". It replaces the city's liveness line
while you are inside rather than joining it, because the markers, the sky and
the traffic are facts about a board behind you — and "live data" printed over
invented colleagues is the exact species of lie that wording was rewritten to
stop telling. It also needed a rule of its own: `.source` on a phone is one
nowrap ellipsised line, which rendered this as "sample occupancy · these people
…", a truncation that removes the only word that mattered. A disclosure cut off
mid-clause is worse than none, because it looks like a caption somebody bothered
to write and nobody goes looking for the rest.

**The room did not fit.** `camera.fov` is vertical, so visible width is
`distance * tan(fov/2) * aspect`, and a phone held upright has an aspect near 0.5
against the 16:9 those viewpoints were framed by eye against. At the same
distance that is a third of the width: the arrival shot put a thirty-four-metre
floor plate off the corner of the screen under a frame full of empty sky. The
pack was not wrong and the renderer was not wrong — the number meant something
else on that screen. `poseFor` now preserves the width the author framed, which
is the thing they were choosing; "everything in this building is somewhere in
this frame" is a statement about width, and the extra height a tall screen throws
in costs nothing. It only ever pushes back, never pulls in, so a window wider
than 16:9 is untouched.

The correction scales the whole offset from the target and not the ground
distance alone, and that is the entire fix rather than a refinement. Multiplying
only the distance does not step back from a shot, it flattens it: 32 m out and
14 m up became ninety-odd metres out and still fourteen up, a near-horizontal
squint at the edge of a floor plate stranded near the horizon. I built that
first and it was worse than the bug.

Checked on an iPhone 13 viewport and at 1920x1080 and 1440x900. Phone: the whole
floor centred at the authored angle with people at the benches, the disclosure
on two lines and complete, the button tapped open and the plan sheet up with
eight rooms named. Desktop 16:9 is pixel-identical — the correction is exactly 1
there — and the console is clean on all three.
2026-08-06 03:46:17 -07:00
karti 5bc7258753 A plan view in the corner, a night you can actually see, and three kinds of visitor
The right half of the screen was empty sky. It holds the board now, drawn flat,
with the footprint of the camera's own frustum on it — the one part of a minimap
that earns its place, because it answers "where am I looking from" without
leaving the shot. Click it, drag it, scroll it. It is a 2D canvas rather than a
second WebGL context, cached per city and redrawn only when something moved.

Night was black. Not dark — black: at 3 a.m. the coastline, the hills and the
bay were one shape, and the frame read as a failed render rather than as
darkness. The sky already had a floor for exactly this reason and nothing did
the equivalent for the ground, so the ground has one now. The moon still has to
be worth computing, so the gap between a moonlit night and a moonless one is
preserved rather than filled in.

Three tiers, resolved once in the new src/access.ts: anonymous, signed in,
admin. Anonymous gets the map and a public office — the shell, the furniture,
the named viewpoints, nobody home — built without the private objects rather
than with them hidden, because scene.traverse makes hiding a leak with a bow on
it. The time scrubber and the debug readouts are admin only, and admin is
granted by TERA_ADMIN_SUBJECTS on the server and inferred nowhere else. An
unreachable API means member, never god: the promise is "clone it and it works",
not "clone it and you are an administrator of a deployment you did not
configure".

Three things this run found and fixed rather than shipped:

  - entryUrl came off the wire and went straight into an href with no scheme
    check, and a CSP of script-src 'self' 'unsafe-inline' does not stop a
    javascript: URL from navigating. One rejection point in access.ts now.
  - A 5xx from /health was the same null as "no API at all" and therefore the
    opposite conclusion. Eight seconds of tera-api restarting would have told
    every anonymous visitor they were a member. A 5xx is an answer; it fails
    closed.
  - decodeURIComponent in cookieToken was the one path in auth/index.ts that
    threw rather than returning ANONYMOUS, so one malformed cookie header from
    an unauthenticated caller turned /api/v1/session into a 500.

Also: keyboard shortcuts, focus rings, a boot state instead of a blank 2.3
seconds, a collapsible panel under 900px, and no horizontal overflow at 375,
768, 1440 or 2560.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-05 22:53:30 -07:00
Karti Tripathi fe43077ff8 The same bug the camera had, still live in the default light rig
Three loose ends turned into four, because one of them was not a stale comment.

`cityDaylight()` closed its fog at 210/460 — numbers tuned when San Francisco's
230-unit board was the only one. Every caller who supplies an `Atmosphere`
overrides them, which is why this survived: the demo does, so nothing looked
wrong. A self-hoster who renders a city with no clock and no weather gets the
default path, and on the Bay Area's 1003 units that fog closes well inside the
city. It now scales to the board exactly as the camera limits do, with the old
constants preserved as the default for a caller holding a palette but no world.

The three that really were comments: socal.ts justified its `latScale` by
citing the 340-unit orbit cap and the 460-unit fog as things the engine
insisted on, and the engine stopped insisting when both became board-relative —
285 is now a choice the pack makes because its lot sizes and hill radii were
authored against it. CONTRACT.md and ARCHITECTURE.md still measured the
retention argument against a 336,864-point San Francisco that has been the
whole Bay Area for some time. And server/package.json listed five routes where
there are six.

None of these changed behaviour except the first. All of them would have gone
on quietly disagreeing with the pages that now cite them.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-05 04:34:00 -07:00
Karti Tripathi d464459838 Spaces: the inside of the world, and a sun that is actually where it should be
Ten agents wrote this in parallel against CONTRACT.md, which exists because the
five design agents before them collided on fifteen blocking points — four files
specified twice with incompatible contents, three separate backends for one box,
and `Environment` exported twice meaning different things.

What landed: a Stage owning only the renderer and the loop, with the city and an
office as two scenes over it. They cannot share one — San Francisco is ~94 m per
scene unit with 3.6x vertical exaggeration and an office is 1 unit = 1 m — and
the city is paused rather than disposed on the way in, because rebuilding its
336,864-point heightfield costs about a second on the way back out.

Offices are data. `src/offices/lumbridge-hq.ts` is fifteen rooms and seventy-six
seats, and it is the file a self-hoster copies. Walls are a segment list with
1-D openings, so doors and windows are holes punched in a wall rather than
placed objects, and the pass that splits a wall around its openings hands the
walk-mode collider its segments for free.

The sun is real. `solar.ts` is a NOAA/Meeus implementation with no imports at
all — not even three.js — so time of day keeps working on a laptop in a field.
Verified against known values: 75.45 degrees at the June solstice in SF, 28.79
at December, sunset at 03:15Z. The first screenshot after wiring it was a black
rectangle, which turned out to be correct: it was midnight in San Francisco.

Presence binds to a seat id and never to a coordinate. The pack knows where
`eng-04` is; who is sitting in it is private data behind an API. Same shape as
the marker rule, one level in.

Two corrections to ARCHITECTURE.md are in here. Containment does not discharge
ODbL — publishing OSM-derived coordinates is Public Use of a Derivative Database
wherever the rows live, so the rule is about the geocoder (US Census, public
domain) and not the storage. And a person at a desk is not a Marker; markers are
geographic.

One contract gap surfaced only in a screenshot: two agents read `height` on a
viewpoint differently, so the establishing shot aimed at empty air fourteen
metres above the roof. It now means what the same field means for a city.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-05 00:11:01 -07:00