1
0
Commit Graph

49 Commits

Author SHA1 Message Date
karti eeedfd7d6d feat: one California is the product, not a flag
Behind `?one=1` the merged board was something a visitor could not find
and did not get: the site still landed on the Bay Area, the left column
still offered two other boards, and clicking one still tore the board
down and built another. The board existed and the product did not change.

A bare URL is now one California. `?city=sf` and `?city=socal` still get
the dedicated boards, unchanged and on purpose — twenty-nine capture
guards and the `bay-area` and `socal` budget cells aim at those boards by
name and assert the `data-board` they land on, and a metro id falling
back to the merged board would not degrade them, it would make them
silently measure something else. `?one=0` forces the old arrangement, and
both `california` budget cells now pass it: their caps are the plain
state board's 440,000, and without it they would measure the merged board
that `california-one` already covers.

Also `CALIFORNIA.md`: what a single cohesive California is still missing,
measured rather than listed. Every district on the board by the latitude
of its centre is 1 / 23 / 26 / 1 / 2 / 54 across 32-37 N and **zero above
38 N** — 107 districts, 103 of them in two clusters four hundred
kilometres apart, and four hundred and fifty kilometres of northern
California with not one building on it. Eight airports, all Bay or LA.
Two ports, both in San Pedro Bay. Two freeways of twelve vertices each
across a thousand kilometres of state.

The scale is not the problem and the file says so first: the board is
32.50-42.05 N and -124.50 to -114.00 W at 1,919 m to the unit, every
feature on its real coordinate, nothing schematic. It is not too small.
It is empty.
2026-08-24 16:57:18 -07:00
karti 86033046ae fix: the merged board asked for weather in the middle of the state, and got a 400
`carriesMetroDetail` is true of it, so it watched at `entry.city.center` —
37.30, -119.25, which is Sierra foothills five hundred kilometres from
either station this deployment serves. Found by loading the deployed
build and reading the network log, not by any test.

Aircraft from two regions merge because they are disjoint objects in one
sky; two weather observations do not. A board cannot be foggy at the
Golden Gate and clear over Riverside at once, and applying either one
statewide is a worse lie than the climatology model. So the merged board
takes the model, which is the rule the comment above it already stated.
2026-08-24 16:50:26 -07:00
karti e5b9339015 feat: one California becomes a board you can descend into
Three changes, and the first is the one that mattered.

`orbitMinDistance` was `boardSpan * 0.02`. On the Bay Area that is 20.1
units of 94.3 m — 1.89 km, the range every close photograph of this
product was taken at. On the merged board the same rule is 11.1 units of
1,919 m, which is 21.3 km: eleven times further out, on the one board
that carries both metros' districts, landmarks and bridges. The floor is
a fraction of board *size*, and board size is exactly what `unify.ts`
changed without changing what stands on it — so the merged board could
never resolve a city no matter what the level of detail did, because the
camera was not allowed to arrive. A board that carries `detail`
districts now floors on true metres instead. Boards without them keep
the span rule, which is right for an atlas.

The metro chapters come back. They were merged, photographed and
reverted at `64618cb`, and the recorded reason was specific and true:
the corridor's 4.7 km atlas glyph filled every close frame. `23e4bb8` —
the next commit — added `overviewLayers`, which withdraws that glyph
inside `DETAIL_STANDOFF_M`, naming that same photograph. The blocker was
gone and nobody came back for the rungs it had blocked. Each rung is
restated in this board's units: `distance` on `latScale` alone,
`height` carrying the exaggeration ratio as well, or the camera arrives
at the right altitude over relief drawn 2.6x taller and every authored
angle flattens.

And with `?one=1` there is one entry in `CITIES`. That is what makes the
merge a board rather than a nicer tier on a three-board product: every
ladder rung's `board` is `california`, so `goToPlace` never switches;
the two doors stop intercepting clicks and fly their authored poses; and
`DEFAULT_CITY_ID` is the one board that exists. Without the flag nothing
moves, which keeps the capture guards and every budget cell aiming at
the boards they were written against.

Measured, all twelve budget cells PASS. `california-one` reads 355,859
triangles and 415 draws against 400,000 / 460 — unchanged from the
352,927 / 420 recorded when the board landed. No cap was raised.

What this does NOT fix, photographed rather than assumed: at FiDi's
7.7 km the camera is inside the terrain, and at the Bay's 45 km the
ground is visibly faceted. Both are one cause — `verticalExaggeration`
is a per-board constant, correctly derived at 15x for framing the whole
state, and a board you can traverse from 1,551 km to 7.7 km cannot hold
it constant. See TODO.md.
2026-08-24 16:27:43 -07:00
karti 285b5b19e7 fix: the merged board's live feeds, and two instruments it could not be judged without
An ultracode investigation mapped the single-board work across five parallel
readers and three adversarial reviewers. It found things this session would have
walked into, and two of them are fixed here.

**THE MERGED BOARD WAS SHIPPING WITH THREE FEEDS SILENTLY OFF.** Live ADS-B and
live weather were gated on `id !== "california"` — "not the coarse statewide
board", correct the day it was written, since live aircraft over a board at
1,919 m to the unit are a glyph problem and one station cannot speak for a
thousand kilometres of coast. `cities/unify.ts` then built a board that is the
whole state AND metro-detailed, keeping the `california` id deliberately so the
fire gate, the ladder's region table and every `?city=` deep link keep working.
It inherited a gate meant for something else. Measured before the fix: the
`#source` badge read `""` on the merged board and `live weather · live traffic`
on the Bay Area's. A defect that reads as "it feels less alive" and never as an
error.

`carriesMetroDetail(city)` asks the pack instead: `focusRegions` is the honest
predicate and needs no new field, because the coarse state pack declares none
and every pack with ground worth drawing at metro resolution declares one.

**AND ASKING WAS NOT ENOUGH, BECAUSE THE FEEDS ARE PER-METRO.** With the gate
fixed the board asked — and was refused: `GET /flights?lat=37.30&lng=-119.25&
radiusNm=402 → 400 bad_request, "Nothing this deployment serves is near
37.3,-119.25"`. Correctly: `regionOf` derives its circle from the board's bounds,
which on a statewide board is 402 nautical miles centred on the middle of the
state, and what the deployment serves is San Francisco and the Southland, five
hundred and sixty kilometres apart.

`mergedTraffic` asks for both. In `adapters/http.ts` and not `engine/flights.ts`
for the reason `TrafficSource` itself lives there: the engine draws darts at
coordinates and has no business with provenance, and every interesting part of
this merge is provenance. De-duplicated by id, because overlapping circles both
see the aircraft between them and `flights.ts` measures a track's span from
repeated observations — a duplicate is not merely a double image. `live()` is
`some` and not `every`, so one dark metro does not make the other's observed
traffic claim to be simulated. Verified: `?lat=37.77` → 200 live, 24 aircraft;
`?lat=33.82` → 200 live, 15 aircraft; `#source` now reads `live traffic` on the
merged board and stays `""` on the coarse one.

**TWO INSTRUMENTS, BOTH BECAUSE THIS SESSION KEPT FAILING WITHOUT THEM.**

`scripts/performance-budget.mjs` gains a `california-one` cell. Until now the
only way to measure the merged board was to hand-edit the `california` cell's
query, run, and edit it back — done six times in one session, which is exactly
the procedure that gets half-done. Its `ready` asserts `#sea-section`, not just
the signature chapter: `california-overview` is on the coarse board too, so a
cell whose `?one=1` quietly stopped working would measure the coarse board and
pass. No ports, no section, no readiness. Caps are RECORDED from its first run
with headroom, in the same spirit as bay-area and socal — they were briefly
copied from `california` and that is wrong for the same reason that cell's
numbers are wrong for this board. **No existing cap was raised.**

`scripts/look.mjs` gains `--lat/--lng/--standoff/--height`. Every aim in this
harness goes through a control a reader also uses, which is right and stays the
default — but it means a board can only be photographed where a chapter already
points, and the merged board carries the state pack's six: the whole state, the
north, two corridors, two doors. None is near a city. The board exists to put
cities on the state and there was no way to photograph one; three attempts by
clicking the minimap and guessing wheel notches landed in open ocean twice and
on empty coast once. The seek moves the camera and nothing else.

**A regression the new cell caught within one run.** The first version of the
marker fix gated on bounds alone, like the office doors. The coarse state
board's rectangle contains San Francisco, so it picked up forty-four company
markers it has no business drawing at 1,919 m to the unit: 373 draw calls → 417.
Now gated on `carriesMetroDetail` as well, and `california` measures 372,415
triangles / 373 draws — identical to before this commit.

All twelve budget cells pass. 1,705 tests pass.

Recorded for the next round, from the review: **SoCal's two focus rectangles
overlap by 1.7 x 4.6 km** (verified: lat 34.075–34.090, lng −118.300–−118.250),
so any per-rectangle terrain tier must clip them to a disjoint cover first or it
draws that ground twice. Today's per-axis lattice is what hides it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-24 04:16:33 -07:00
karti 15d7accdbb feat: one California as one pack, behind ?one=1
The nesting preview is gone and this replaces it. That one drew each metro's
built scene inside the state board's under a similarity transform; the transform
was right and two independently built terrains then occupied the same ground and
interpenetrated. `cities/unify.ts` is the version with no seam to stitch,
because the way to not have two grids is to not have two grids: one `City`
carrying the state's landform and both metros' detail, so the engine builds one
world, one lattice and one terrain mesh.

**The merge is mostly a union and twice it must not be.** Everything that
describes a place in a `City` is absolute lat/lng, so hills, districts,
landmarks, bridges, ports and airports concatenate. Two do not:

  - `World.elevationAt` *blends* every hill covering a point, so leaving the
    state's coarse hills under a metro's fine ones adds their heights and lifts
    the whole Bay. 25 state hills are dropped where a metro replaces them.
  - The state's 17 districts include coarse stand-ins for both metros against
    the metros' 99 real ones, and both drawn is two cities in one place. 9 go.

Filtering is by centre-in-bounds, which is honest and not exact, and the file
says so rather than hiding it.

**The lattice was the whole question and the answer was measured, twice.**
`TODO.md` records a statewide lattice at metro fidelity as 23.7M points and
dead. That is true at San Francisco's own 45 m cell, because `buildAxis` refines
per *axis* — a plus, not a box — so two metros at opposite corners refine most
of the state. The blowup is a function of the fine cell and is gentle until 45 m,
so the first pass took 150 m / x8, or 1.23M points.

That rendered **1,114,226 triangles at the whole-board pose against a 440,000
cap**, and the useful part is where it did *not* come from: with every metro
building hidden, triangles fell by 43,000 and draw calls by 111. The buildings
were never the cost. Terrain was. So the fine cell is what a statewide board can
afford without terrain LOD, and that is **300 m / x16** — 0.18M points, 11.6x
finer than the state pack's 3,473 m cell and 6.7x coarser than San Francisco's
own board. Measured at that size: california 392,862 triangles of 440,000 and
california-drive 405,124 of 430,000, both green on desktop.

**And the detail LOD is the cheapest kind there is.** `createBlocks` now orders
every `detail` district's lots *last*, so hiding 99 districts' buildings is one
assignment to `InstancedMesh.count` — no second mesh, no second draw call, no
allocation, and nothing for `nightlights.ts` to learn, since it holds the same
mesh either way. Landmarks are one mesh each so theirs is a `visible` flag.

The reveal is two tests and needed both. Stand-off alone passes for the drive
chapter, whose camera sits a few tens of metres off the ground half way up the
state while being two hundred kilometres from the nearest building this governs
— measured, it failed `california-drive` on triangles for lots nobody could see.
So detail draws only when the camera is inside `DETAIL_STANDOFF_M` *and* its
**target** is within `DETAIL_REACH_M` of a detail district. The target rather
than the camera, because the target is what is being looked at.

`src/test/unify.test.ts` pins the two non-union merges, the detail marking and
the lattice size. Writing it turned up something worth keeping: **six hills are
named in both the state pack and a metro pack** — Marin Headlands, San Bruno
Mountain, Mount Diablo, Bolinas Ridge, Mount Gleason, Mount Wilson — so the
first version failed on San Francisco's own Marin Headlands, identified as a
state hill by its name. `ladder.ts` hit this with "Whole Board" on two boards.
A name is not an identity; the test keys on the object.

Known and not fixed: mobile draw calls are 428 of 415 and 315 of 280 on the
merged board, from the port, vessel and bridge layers that the state board never
carried before and that no LOD hides yet. That is why this is still a flag.
Default is untouched and the full budget passes with it off.

1,700 tests pass.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-24 02:19:14 -07:00
karti 122497088c feat(preview): one full California, behind ?nest=1
Not finished, off by default, and shipped anyway — for the reason `reconcile`
was shipped behind a flag: the owner is choosing between two shapes of the
product and an argument about how a board looks is settled by looking at it.

`?nest=1` draws San Francisco's and Southern California's ground inside the
state board instead of beside it. The question it answers is whether one full
California needs a terrain quadtree, or whether the three packs already are the
detail tiers of one map and only ever needed drawing in one scene.

**They are, and the placement is a similarity transform and nothing more.**
`World.project` is a uniform scale in x/z with no vertical term, so a metro
enters the state board's space by scaling x/z by the ratio of metres-per-unit,
scaling y by the ratio of `exaggeration / metresPerUnit` — separately, because
the two boards stretch height differently — and translating to where the state
board projects the metro's own centre. Measured: sf is xz 0.04915, y 0.12764 at
(-146.2, -27.5) on a 551-unit board; socal is xz 0.20351, y 0.89640 at
(55.4, 201.8). Both land where they belong at the size they should be, and the
close pose shows real towers, blocks and a freeway on the state board.

**And the statewide lattice is not the blocker TODO.md says it is.** That file
records a statewide lattice at metro fidelity as 23.7M points and dead. That is
true of `buildAxis`, which refines per *axis* — a focus region refines its whole
row and its whole column, a plus rather than a box — so two metros at opposite
corners refine most of the state. Measured against the packs' real focus regions
(sf's is 22x40 km; socal's two are 9x12 and 8x13), per-*rectangle* refinement is
**2.93M points and about 26 MB** at a 720 m coarse cell, or 1.13M at 1439 m,
against 0.75M for all three boards as they stand today. The verdict in TODO is a
fact about the lattice's shape, not about one California.

**What is broken in the preview, precisely.** Two terrains occupy the same
ground: the state's coarse mounds are still drawn under the metro's fine surface
and they interpenetrate, so a block sits on a smooth hill in one place and is
buried in it three hundred metres away. That is nested-grid stitching — cut a
hole in the coarse lattice over each fine rectangle and sew the seam — and it is
the whole of the remaining work. Nothing about the transform is implicated. Also
not done: no LOD decides which ground draws, the metros are lit by a rig that
belongs to another board, and triangles are additive rather than traded.

`water` and `sea` are deliberately not lifted. The state board already has an
ocean at y=0 and a second one inside it is a z-fight rather than a sea — the same
defect the terrain still has, and the one place it was cheap to avoid.

Inert without the flag, verified: `?city=california` with and without `?nest=1`
both boot clean with zero page errors, and 1,694 tests pass.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-24 01:38:59 -07:00
karti 6fc0b2b60e feat: land on a real place, and let a wheel notch cross the seam
The owner has now asked four times why there are three boards, and the last
answer missed the point: reconciling the packs made them draw one California,
but you still *arrived* on the coarsest tier the product owns and still changed
boards by picking a name off a list. This changes both.

**The behaviour was already built and behind a second flag defaulted off.**
`handover()` in `ladder.ts` — promote/demote by camera stand-off, hysteresis at
0.9/1.15, a drag guard so a board never swaps under a finger — was pure, tested
and shipped a round ago, with `?handover=1` as the only way to see it. It is on
now, `?handover=0` turns it off. Measured on the deployed bundle first:
promotion into the Bay Area fired at the sixteenth wheel notch in from the state
pose, arrival clean, no boot card, no tab, no click on a name.

**And it did not work, for a reason worth writing down.** Landing on the Bay
with the flag on bounced straight back to the state board with no input at all.
Two wrong diagnoses on the way, both from reasoning instead of measuring:

  1. "sf's ceiling equals its own widest pose, so the band is too tight." It is
     not — `chapterStandoffMetres` puts the resting pose at 71.2 km against a
     demote threshold of 81.9 km.
  2. "the arrival flight carries the camera through the threshold, so guard on
     `arriving()`." Right about the cause, wrong about the mechanism: the guard
     went in and the bounce survived it.

A trajectory log settled it in one run. The handover tick arrived *before* the
first `arriving=true` sample, at 108,316 m — 1.5x sf's resting stand-off, which
is `arrivalStart`'s own offset. `arrive()` read:

    kit.setPose(from);
    arrival = { from, to: rest, elapsed: 0 };

`setPose` drives `OrbitControls`, which fires `change` **synchronously**, and
`main.ts` listens on that event. So the listener ran on the line *between* those
two statements: camera already 1.5x out, `arriving()` still false. A one-frame
ordering race, and the guard could not fire because the flag it reads was set one
statement too late. The assignment now goes first.

The guard stays, because the inequality behind it is structural rather than
incidental: `ARRIVAL_STANDOFF` is 1.5 and `DEMOTE` is 1.15, both global, so the
opening frame of *every* board sits outside that board's own retention band.
`handoverArrivalGuard.test.ts` asserts that relation and drives the real rule
through the opening stand-off to watch it demote, so neither the guard nor either
constant can be quietly simplified.

**The landing board is `DEFAULT_CITY_ID`, and it is the Bay Area.** It was
`CITIES[0]` in three places, which meant the state tier: seventeen districts over
1063x930 km at 1,919 m per unit, no city legible, and a left column whose first
offer is somewhere else to go. The detailed boards carry 52 and 47 districts at
94 and 391 m per unit. With free handover on, the state tier stops being a
destination and becomes what you get when you pull back — the role it is good at,
since it is the only board drawing 97.4% of California. A named constant rather
than reordering `CITIES`, because that array's order is the `?city=` fallback and
is read positionally by other consumers.

**Which caught a silent break in the capture harness, and this is the part that
would have cost a week.** `shots.mjs` and `films.mjs` built `?city=` only when a
shot declared one — and 4 of 21 shots and 1 of 4 films declare none, so they
inherited the app's default. Moving that default would have re-pointed five
pieces of marketing imagery at a different place while every filename, caption
and alt text stayed as it was. Both harnesses now name `california` themselves.
`performance-budget.mjs`'s `california` and `california-drive` cells had no query
at all for the same reason; `signature` would have failed them loudly rather than
mismeasuring, which is the harness working, but a harness that depends on an app
default reports someone else's change as its own flake.

Every harness also pins `handover=0`. A planted pose wider than a board's
retention band would otherwise demote to the coarser tier while the shutter is
open, and the frame that comes back is a real photograph of the wrong board.

Verified: bare URL lands on the Bay Area and stays there; `?city=california` and
`?city=socal` still deep-link; `?handover=0` stays put; zooming out from the Bay
demotes to the state tier at the second notch. 1,694 tests pass. All ten budget
cells pass with no cap raised, and every cell now measures the board it names.

Still true and now a decision rather than a doubt: 97.4% of California has no
board below 242 km of stand-off, so zooming into the middle of the state lands on
coarse ground. That picture has been looked at. Authoring Sacramento, Fresno and
the Central Valley is what retires it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-24 00:13:23 -07:00
karti 85be0b13f8 feat: boards load before you ask for them
The owner asked for one map, with detail arriving in the background rather than
behind a wait. The interface already said one place (4bd8481: one ladder, no tab
strip, no boot card). This is the world catching up: on a desktop both metros are
now resident within seconds of landing, and the switch that used to be a build
becomes a fog dip over a cache hit.

**Measured, three runs each, California -> The Bay after an 8 s dwell:**
the longest blocking task falls from **521 ms to 58 ms** (before 700/501/521,
after 58/58/61) and long tasks from 2 to 1.

**And the cost, stated rather than omitted.** Over ten seconds sitting on the
state board: 601 frames -> ~571, p95 frame interval 18.0 -> 18.7 ms, and **one
long task of ~500 ms that did not exist before**. The build is not chunked yet,
so this moves a freeze from the moment of interaction to a moment nobody asked
for. That is a real trade and the follow-on that removes it is chunking the
build. `PREFETCH_ENABLED` in main.ts turns the whole lane off in one line.

A parked board draws nothing: 371 draws per frame before the lane lands a board
and 371 after, patched at the GL entry points. The performance budget reproduces
every cell to the digit.

**Three defects found on the way, two of them latent for a while.**

1. `environmentRig` had no idea what off-stage meant. `createScene` calls
   `apply()` unconditionally, two hundred lines above the `present` check, and
   the rig holds one probe per kind: on a key miss it convolves a new one,
   disposes the one the visible board is using, and repoints every applied scene
   at the replacement. A board built ahead of the camera is observed at its own
   centre — about forty minutes of apparent solar time across the state — so its
   key differs by construction. The board on screen would have changed colour
   because something invisible finished loading. Harmless until today only
   because every build was followed within ~800 ms by the swap that presented it.
   Fixed with `ApplyOptions.offstage`; the regression test was written first and
   failed first.

2. `mounting` was assigned and never cleared. Harmless while the only reader was
   the next `mountCity` wanting something to abort. It stopped being harmless the
   moment a second lane asked "is a foreground build in flight?", because the
   honest answer after the first mount of the session was permanently yes — the
   background lane armed exactly never, and the only symptom was a feature that
   silently did nothing. Found by measuring, not by reading.

3. `buildBoard`'s `onProgress` ends in `bootProgress`, which raises the switch
   pill. A prefetch would have put "Building The Bay Area... terrain 42%" over a
   visitor who did nothing — the exact chrome that removing the tab strip was
   for. `quiet` closes it.

**The policy is pure and lives with the eviction policy it has to agree with.**
`prefetchTarget` and `canAdmit` in boards.ts take scalars, never a camera, so
boards.ts keeps CONTRACT section 1's no-DOM/no-WebGL/no-three promise and
prefetchPolicy.test.ts can assert against the real pack bounds. Two regimes,
because collapsing them was the first version's mistake: with free room the gate
is simply "is there a slot", since the common path is to land on the state at
1,551 km and click a metro, and on that path a proximity trigger fires never;
with the cache full — the handheld case, capacity two with California pinned —
proximity is the only thing that justifies an eviction. The discs are asserted
non-overlapping against the shipped bounds (92 + 146 km of reach across a 314 km
gap), which is the covering-set argument residentCapacity already rests on.

**Also fixed, and separately load-bearing: reconcile's flag parser lied.** It
prefix-matched, so `?reconcile=palette` selected nothing and produced an empty
set — indistinguishable from the flag being absent. Every photograph taken to
judge a rule could have been a photograph of the unreconciled board with no way
to tell. Exact names now, with a warning that says "the flag is NOT off".

And two consumers were reading the raw packs beside a reconciled World:
`buildLadder` derives every rung's stand-off from `focus`, and `createMinimap`
was handed `entry.city` next to `handle.world`. Both now read `world.city`.
Latent with the flag off — `reconciledCity` returns by identity — which is what
kept it alive: it corrupts the measurement rather than announcing itself.

**What I did NOT ship, having tried it.** `roads` on by default. The rule exists
to stop California drawing 1,919 m freeways, and `city.roads` is not what draws
them: California is the one board with `roadTraffic`, so `scene.ts:816` takes the
`createFreewayWorld` branch and `createRoads` — the only reader of `Road.width` —
is never called for it. The visible corridor is a deliberate atlas glyph sized so
DRIVE mode can drive down it. Photographed at three chapters the rule moved
Downtown LA not at all (empty diff bbox), FiDi by RMSE 0.0006, California by
0.001% of pixels; the only measurable effect anywhere was -112 triangles on
Southern California. See DEFAULT_RULES for the whole argument.

1,688 + 295 tests, every gate, chapter-identity OK against the unmodified
fixture, ui-smoke PASS, budget PASS with private-request checks clean on all ten
cells.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-23 13:15:37 -07:00
karti 4bd8481be1 feat: the boards stop being three products
The owner asked twice why there are still three separate boards. The honest
answer, and what this round executes: **it feels like three boards, but not
because the scale jumps 20x — because the three packs draw three different
Californias, and the loudest difference is that the mountains are four times
taller on one of them.**

**THE 20x HORIZONTAL SCALE JUMP IS INVISIBLE**, and measuring that collapsed the
cost of this whole round. `World.project` is a uniform scale in x/z with no
vertical term, and a uniform scale leaves a perspective image identical — so a
camera carried across the seam on matched true-metre offsets draws a
pixel-identical horizontal frame. 1,919 -> 94 m/unit costs nothing to look at.
Rescaling was never the problem. A boot card, a tab strip and a 4.17x vertical
deflation were.

**THE PAUSE WAS MOSTLY FAKE.** A switch covered the screen for 1,715 ms but only
608 ms blocked the main thread; the page drew 46 of 69 frames with nothing to
show, because the outgoing board had already been disposed. `mountCity` now
retains it: the incoming board builds BEHIND a live, interactive picture, and
`stage.setScene` fires only on completion. Measured across all six directions,
three runs each — boot card yes -> **no**, opaque cover 726-1,415 ms -> **0**,
blank frames 21-46 -> **exactly 1**, wall clock down 12-29%, blocked main thread
down 15-47%. A return to a board already seen links **zero** shader programs and
blocks **zero** milliseconds: 298-312 ms of camera flight where it was ~1,600 ms
behind a card. Disposal had been throwing away the shader cache too — linkProgram
ran 38, 59, 78, 109, 127 across five mounts and never reused one.

**The transition is a fog dip, not a crossfade**, through the `setAerialFog` seam
built last round. Every both-boards-live crossfade breaks a budget — ca+sf is
2,640,307 triangles against bay-area's 2,600,000 cap — and a fade never lands
inside the harness's sample window, which is the "a cap you do not measure is a
cap you do not have" failure this repo already argues against. The dip costs zero
triangles and zero draw calls, and it hides the 4.17x deflation, the 4,025 m
projection disagreement and the vanishing 2 km freeway symbols at once, because
all three happen at maximum obscuration. It is also diegetic: a descent through
haze.

The first dip was wrong and the photograph caught it: collapsing to 6% of board
SPAN turned the whole night frame into one flat field — the exact "turns the map
off" failure the risk list named. Re-anchored to 70% of camera STAND-OFF, so the
coastline survives and only the relief melts.

**One ladder, one places list.** 26 authored chapters become 24 rungs sorted
descending by STAND-OFF, not altitude — by altitude they interleave badly and
altitude cannot tell a low oblique from a high plan. The three-board tab strip is
off by default; the left column is now one scrolling list of all 24 rungs under
three region headings that does not change when the board does. Only which row is
lit changes. Label collisions are resolved in the ladder and never in a pack, so
the 29 index-aimed capture guards are untouched.

The minimap stops turning through 90 degrees between boards: every board is
pinned to a rectangle with California's proportions.

**SF and SoCal are not regressed**, and that was the acceptance that mattered:
95.9-98.8% of board pixels are delta-0 against a baseline hash-verified identical
to what the live site serves, and every one of the 34-70 surviving pixels per
frame is an aircraft or a hull.

**A real defect found only by photograph:** `minimap.setMarkers()` had zero call
sites. Every marker on every board was gone — the LA studio's door dot, the Bay
Area's eight company markers — dropped when the minimap went per-board.
Typecheck, tests, budgets and the console were all green with that bug in.

Also fixed: two capture presets that lied. `look.mjs`'s `glyph-la` and `glyph-sf`
claimed California chapter closeups and returned SoCal and Bay Area frames,
because they aimed by chapter index and the indices had moved. Aiming is now by
identity, with a guard test.

NOT SHIPPED, DELIBERATELY: the pack merge. At Bay density it is 34.04M triangles,
13x the highest budget — dead, not a trade. At SoCal density it is 1.99M and fits
today, and the price is San Francisco rendering at 164 m lots instead of 40 m,
i.e. SF looking the way SoCal looks now. SF and SoCal carry every marketing still
on the site. That is the owner's decision and it is worthless as an argument and
decisive as a photograph, so it ships as a measurement artifact with a
side-by-side still and is wired into nothing. The four data reconciliations that
would make one world honest — one exaggeration rule, roads in metres, one
projection centre, one coastline convention — are behind TERA_RECONCILE, default
OFF.

Tests 1,570 -> 1,651, server 295. All ten budget cells pass, no cap raised.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-23 06:20:18 -07:00
karti acf4d1a510 feat: the state becomes California, and the port fills with ships
**Stage 1 of one California.** The owner's complaint had two halves and this is
the first: the state board was a CROPPED SLAB. `california.ts` stopped at 38.05 N,
so the board disagreed with its own minimap about the shape of California in a
single frame, and Bug Fire's 93,733 acres burned off-frame while the panel said
all clear. Bounds now run 32.50-42.05 N / -124.50 to -114.0 W — Cape Mendocino,
the ruled Oregon parallel, the 120th-meridian corner into the Nevada diagonal.

**And it got cheaper.** 391,169 triangles to 375,351, while gaining the North
Coast, the Sacramento Valley, the Klamath knot, the Cascade arc, Shasta at 4,320 m
and Lassen at 3,190 m. Extending the bounds alone would have doubled the lattice
to 168,813 points and blown the mobile cap; coarsening cellLat 0.022 -> 0.0312 and
cellLng 0.027 -> 0.0383 holds it at ~83,800. The cell as a FRACTION of the board
moves 0.0030 -> 0.0033 — unchanged in frame — because the camera retreats to frame
whatever it is given. That argument was already written in the pack's own comment.

The second half — three boards becoming one world you zoom through — is NOT here.
Merging at Bay density would be 34.04M triangles, 13x the highest budget, and
merging at SoCal density would downgrade San Francisco from 40 m lots to 164 m.
Both delete the board every marketing still is shot from. `sf.ts` and `socal.ts`
are untouched by design.

**Aerial perspective, which the state board could not have had before.** The old
fog started at 1.15 board spans = 944 km, on a board whose longest diagonal is
820 km — so no pixel could ever be fogged. Fog now responds to camera altitude,
clamped to the authored pair as a ceiling.

`Atmosphere.aerial(env, view)` is a second pure method returning `{ near, far }`
and **deliberately no colour**. That is structural, not stylistic: it is why a
future camera-dependent term cannot reach `environmentKey()`'s colour fingerprint
and start rebuilding the PMREM cubemap on every camera step. Coarsening the
fingerprint instead would have hidden one instance and armed the mechanism. A
mutation-tested seam guard fails if anyone merges the two paths back together.

**The port.** Terminal Island rendered as a bare tan polygon with generic white
blocks while the chapter text called it the busiest port complex in the
hemisphere. Now six container yards drawn as canvas atlases, 56 gantry cranes at
varied boom angles, the 13 km San Pedro breakwater, the dredged channel. Five
buckets merging ACROSS ports the way airports.ts merges across fields, so a
second complex costs no extra draws: +11 draws and +4,377 triangles for all of it.

At vertical exaggeration 3.4 a 130 m gantry is 1.132 units tall against a 400 m
ship's 1.024 long — the crane is the taller object, and it is what makes a port
read as a port from altitude.

**Ships, and the wake carries the information.** Moored hulls have no foam,
verified at three terminals; a tug under way in the Main Channel trails a clean
Kelvin V. One hull geometry, one InstancedMesh, orientation from the BERTH rather
than the wire. The AIS gate strips sog 102.3, heading 511 and cog 360 — all mean
"not available" — with an explicit test that cog 358.7 SURVIVES, because a naive
range check on cog eats real headings near north.

"Empty or full" is not in AIS position reports and is not invented per ship. The
honest answer is at port level and is a better story: 348,691 of 460,467 boxes
left Los Angeles empty in July 2026, corroborated by FBX01 $7,491 inbound against
FBX02 $347 outbound.

**Radar and birds ship dark, and say why.** California is 0.47% wet and migration
is nocturnal and seasonal, so both layers have nothing to say on most days. The
panel reads "No radar feed is configured, so this board draws no weather. That is
a fact about this box, not about the sky."

Also recorded, and it matters beyond this commit: **the GPU on amd-server never
leaves 500 MHz of a possible 2725**, traced across 80 seconds of sustained load.
`bay-area/desktop` is fragment-bound at that clock and sits on the vsync deadline,
so a trivial change in fragment work flips it between 16.8 and 33.3 with geometry
identical to the digit. Every frame-time number measured on this box is a floor.
Two investigations reached two different wrong conclusions from single-run
comparisons before this was traced. Geometry is the gate; frame time is advisory.
No cap was raised.

Tests 1,340 -> 1,540, server 280 -> 295.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-22 23:35:09 -07:00
karti b25f217e3e feat: real fire on the boards, the LA office as a twin, and a night sky worth reading
The world stops being a simulation of California and starts being California.

**THE PROMOTION GATE WAS THE FIRST COMMIT, BEFORE ANY ORANGE PIXEL EXISTED.**
On today's live store the SoCal board contains 22 incidents. Every one has NULL
acreage and fifteen are nameless LA County dispatch numbers. Drawn naively that
is 22 orange marks over Los Angeles on a day nothing is burning — in a frame that
contains no other warm colour, so one glyph would be the most salient object on
the board and twenty-two would spend its credibility permanently.

`acres >= 10 AND contained < 80 AND type != 'RX' AND last_seen = max(last_seen)`
returns 0 on SoCal, exactly 5 on California, 0 on the Bay — same body, same day,
three correct answers. The empty board is a deliverable, not a fallback: it says
"No active fire on this board — CAL FIRE and WFIGS, just now", states that 21
records were gated and why, lists the largest fires burning OUTSIDE the frame
with distances, and counts the hot pixels it is deliberately not drawing.

**The privacy leak is structurally impossible rather than carefully avoided.**
cloud-1 serves a projection; the four home-relative columns never leave that box.
`observations.threat` was the one that nearly got through — it is
`(16/distance)^2 x log10(acres) x momentum x containment x wind-alignment`, so
with acreage and containment public it inverts to a distance circle around a
house and three fires give an intersection. A grep of the built bundle for
distance_km, bearing_deg, threat, 7762 and the street name returns nothing.

**Deliberately not used, and both would have produced a confident wrong answer:**
the store's `air` table retains only the last parameter of each poll, so all 78
rows read "Good" while the live feed reports ozone 101 "Unhealthy for Sensitive
Groups" — haze driven off it would clear the sky during a smoke event. And
`weather` is written only inside the NWS alerts loop, so a quiet day stores no
wind at all. Tera's own per-region NWS wind is already correct and already what
the clouds drift on.

Satellite detections are drawn as evidence and never as incidents. The permanent
industrial heat source 4.7 km from the owner's house is flagged persistent and
dropped, asserted by a test that first proves it is present in the fixture.
MODIS integer confidence and VIIRS string confidence are branched on `sat`.

**The LA office is a twin.** Its entire authored second storey — Model Loft,
Model Bay, The Materials Room, 430 lines nobody had ever stood in — is reachable
on foot: a walker crosses level-1 to level-2 in 73 fixed steps, floorY 0 to 5,
verified against the real pack rather than a synthetic plan. Its two studio
devices read real hardware through a field-allowlisted bridge: mute, volume and
reachability only. Never level, because there is no passive level upstream and
obtaining one would record a room with people in it. Never dB, because upstream
is gainPct across four different native scales. The bridge refuses all writes.

Fixed at its root: an anonymous visitor was getting permanently at-rest
instruments backing off against a 401. The tier moves into `createDeviceSource`,
so anon gets the living simulator three file headers already promised.

**Item 8 is closed, not fixed, and the correction is the point.** The Bay Area
"stutter" was GPU power management — the card sat at 500 MHz of 2725 through
every run that reproduced it, 4096/2048/1024/256 shadow maps all render in
1.21-1.31 ms, and two consecutive runs over a byte-identical dist gave 33.4 then
16.7. The allowance is removed and the cell is back to 16.7. Geometry is the
gate; frame time is advisory.

Item 7 was re-scoped after measuring: 1,069,006 of the Bay Area's 2,265,056
triangles were the second submission of the same buildings into the shadow pass.
Mobile now has its own triangle caps and bay-area mobile draws 1,266,096.

Also: bridges and the freeway corridor light up at night as emission, not lights
— 1,614 deck lamps and 18 tower heads on the Bay in two draw calls. The single
change that made US-101 legible was moving its edge lines from the lit material
to the unlit one: retroreflective paint, the argument the SFO night frame already
makes. California went 21,991 lamps to 4,051, clustered at the 17 town districts,
because a rural interurban corridor genuinely is unlit.

Tests 1137 -> 1340, server 280. All ten budget cells pass on first attempt with
no cap raised.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-22 18:01:11 -07:00
karti f81d5218d4 feat: SFO, LAX, both bridges, a road that reads as a road, and aeroplanes that move
**The aeroplanes were stuck because the wire could not describe motion.**
`WireAircraft` carried position, altitude and heading and nothing else, so the
client could only interpolate between the last two observations: every aircraft
replayed a segment it had already flown, arrived at the newest known point, and
sat still until the next poll landed five to fifteen seconds later. The feed had
the missing numbers the whole time and the server threw them away. Sampled live
from `api.adsb.lol/v2/point` while writing this — `gs` ground speed, `track`,
`baro_rate`, plus `r` registration and `t` type designator. They are on the wire
now in SI, aircraft dead-reckon along their own track and correct toward the
truth when a fix lands, and the click card an anonymous visitor gets says
"B739 · N68834". That last part is the enrichment FR24 was wanted for, obtained
from an ODbL feed we may actually republish.

**SFO and LAX exist.** A new `engine/airports.ts` composes an airport from
runways, taxiways, aprons and terminal masses, with markings drawn on a canvas
rather than modelled; the pattern of the runways is what the eye recognises from
altitude, long before any building does. SFO is the two crossing pairs on the bay
fill; LAX is the four parallels either side of the terminal horseshoe, plus the
Southland fields under the traffic that actually flies there.

**The Golden Gate and the Bay Bridge are those bridges.** One kit in
`engine/bridges.ts`, because a suspension bridge is a repeated tower, a catenary
main cable, a series of hangers and a deck — so both are configurations rather
than two private implementations. The Bay Bridge carries the real 2013 topology:
two suspension towers west of Yerba Buena, one east, then the piered causeway.
The freeway stopped being a wireframe overlay and became a road, with shoulders,
a median, and lane markings as texture.

**And the board got faster while all of that landed.** California went from
728,744 triangles and 562 draw calls to 391,169 and 371 — headroom from 2.8% to
47.8%. The Bay Area board is 506,550 triangles lighter than before this work.
Two things paid for it:

- `transmission: 0.08` on the aircraft cockpit glass. three.js runs a full
  transmission backdrop pass whenever any rendered material has transmission
  above zero, re-drawing the entire opaque scene into a second target every
  frame — so the city was rendering terrain, every block and every freeway piece
  TWICE. Measured by patching only that number in a copy of the built bundle:
  703,267 tris / 562 draws with it, 398,608 / 371 without. The material was
  already `transparent: true, opacity: 0.86`, so it was buying nothing.
- Flatness-adaptive terrain LOD, which collapses runs of lattice cells wherever
  the height and colour agree with the quad replacing them. The coastline is
  provably untouched — a patch collapses only when every point is on land and
  agrees about `park` — and a test asserts the drawn footprint matches the
  cell-by-cell area to 1e-6. `createTerrain` got *faster*: the vertices it stops
  emitting cost more than the flatness scan costs to run.

**The budget now watches the boards this was built on.** There was no `bay-area`
or `socal` cell — so SFO, LAX and both bridges all landed in frames nothing
measured, which is how a cap you do not have looks from the inside. Both are in
the matrix now with caps set from measurement, and the rationale lives in the
harness because JSON cannot hold a comment.

Two known defects ship with this, both recorded in TODO.md rather than hidden:

- `bay-area.desktop` drops about one frame in twenty (p50 16.7, p95 33.3). It is
  desktop-only and not fill rate — mobile runs the same 2.26 M triangles at a
  comparable pixel count and holds 16.7 flat — which points at the 2048 shadow
  map desktop uses against handheld's 1024. Measured at the commit before this
  work with the same harness: identical p95 33.3. Pre-existing, and invisible
  until the cell existed.
- The aeroplane glyph is still about 1.5x the Golden Gate's main span at chapter
  zoom, down from 2.5x. `GLYPH_MAX_SCALE` is 52 because the raw scale at the far
  end of the California orbit is 51.0 at a 60-degree field of view, and 26 —
  tried first — put the glyph at 0.0123 of the frame against the 0.012 where the
  wings stop resolving. The real fix is to clamp against the camera's focus
  distance rather than the aircraft's, which is a signature change.

Tests 1020 -> 1137. Typecheck, build, eight budget cells, no-binaries,
provenance, zero-config boot, dependency licences, arena source hashes and the
UI smoke across two viewports and two access tiers all pass.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-22 05:06:12 -07:00
karti 3c4649d078 fix: make the app boot on an insecure origin, and re-aim the capture harness
Three things, all found by trying to re-shoot the product imagery and failing.

**`crypto.randomUUID` is secure-context only.** `main.ts` called it at module top
level for its three wire identities, so on any origin that is not HTTPS and not
`localhost` the call threw before the scene was built and the app stopped at
"Starting up" with one TypeError and no other symptom. Every way a developer
normally opens this app is a secure context — `vite dev` and `vite preview` serve
localhost, the deployed site is HTTPS — which is why this survived since 3326d2e.
It breaks the brand-capture harness, which serves `dist/` over
`http://tera.lumbridgecorp.com:5210` so the app can read its own hostname and
decide which door it is; it breaks the plain `http://` static host STATIC.md
explicitly invites; and it breaks opening the dev server by LAN IP to try it on a
phone. `src/ids.ts` prefers the platform's `randomUUID` and falls back to
`getRandomValues`, which carries no such restriction. The tests exercise the
fallback specifically, because the happy path was never the broken one.

**`waitForFunction` was ignoring its own timeout.** Playwright's signature is
`(pageFunction, arg, options)` and all four call sites in `shots.mjs` and
`films.mjs` passed `{ timeout: 180_000 }` second, binding it as the predicate's
argument. The wait silently used the 30 s default, which was invisible for as
long as the app booted inside thirty seconds and started failing the moment the
California board grew its relief — with "Timeout 30000ms exceeded" reported
against a line that plainly reads 180_000.

**The office shot list photographed a building that no longer exists.** The
`expect` guard caught it and refused to shoot, which is exactly what it is for:
chapter 0 is "Front Door" now, not "The Floor". But the captions were staler than
the labels — they described forty-eight metres by eighteen, thirty-six seats in
four benches and a fourteen-metre interstitial commons, and Lumbridge HQ is a
live/work studio now. Re-aimed at the buildings that exist: `office-floor` and
`office-desks` at the SF studio (the second specifically at the bench, because the
desk mic and the machine speaker are the new thing there), `office-commons` at the
LA courtyard, `office-hangar` still at Frontier Valley and now honest about being
in development. The four ids are deliberately unchanged: the manifest emits a
`ShotId` union that v4 imports, so renaming one fails v4's typecheck at push.

Also `#onboarding-host` joins the harness's clutter list. The first-run card is
correct behaviour for a real visitor and wrong in a product photograph, and it had
quietly placed itself in the middle of every frame — which is the general hazard
that list exists for, because a shot with a stray card still renders and still
looks deliberate.

`bay-relief`'s daylight frame moves from 07:40 to 09:10. At 07:40 the marine layer
buried the heightfield the shot exists to demonstrate; the version currently on the
site is almost entirely white. Its `note` moves with it, since the note names the
hour and a stale one describes light that is not in the picture.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-22 00:03:20 -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 264daaab61 feat: unify life-sim play controls 2026-08-19 03:06:01 -07:00
karti e378a03740 feat: add deterministic office robot jobs 2026-08-19 02:56:30 -07:00
karti 0557a26e6b feat: add active SF and LA HQ environments 2026-08-19 02:51:56 -07:00
karti 94de2d8bee feat: build articulated crow flight v2 2026-08-19 01:12:37 -07:00
karti d4859b33c6 feat: add ephemeral webcam faces and adaptive screens 2026-08-11 22:38:56 -07:00
karti 3326d2e6d0 feat: complete actor handoff and piloted aircraft presence 2026-08-11 22:17:06 -07:00
karti 19f022f71a security: bind TURN grants to screen sessions 2026-08-11 21:48:11 -07:00
karti 5ca214e4bb feat: stream private office screens 2026-08-11 21:31:15 -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 16dc85a6f8 feat: add playable flight and office screen sharing 2026-08-11 19:46:47 -07:00
karti c0c7fcf974 feat: add character studio and aircraft foundation 2026-08-11 19:32:32 -07:00
karti 1c37ef8f3e feat: add private profile media and realtime contracts 2026-08-11 19:21:49 -07:00
karti a2a52bfdae feat: add playable actors and seamless journey state 2026-08-11 19:10:14 -07:00
karti 4e313c4a79 Keep route controls on the California board 2026-08-11 18:31:57 -07:00
karti 000036450d Keep corridor weather at corridor scale 2026-08-11 18:30:26 -07:00
karti fe58290728 California gets roads, traffic, and a car to follow 2026-08-11 18:24:53 -07:00
karti 9c9e78f6f9 The Southland gets a door, and the people in it move
**Mateo Court**, a courtyard block in the Arts District — a third pack and
a third *kind* of building. Two office floors and a shed already existed,
so this is the plan the format had not been shown: a ring of rooms round
an open-air yard with **no corridor anywhere**. Every door opens onto the
yard, and the yard does the job a corridor does in the other two. It is
also the first pack whose `ceiling: null` means there is genuinely no
ceiling at any height rather than "take the lid off so the shot can see
in", and the first sited off the Bay Area board at all.

That last part needed a fix, not just a coordinate. `OFFICE_MARKERS` was
gated on `id === "sf"`, which was correct for exactly as long as every
office was in the Bay Area — it would have kept the Los Angeles building
off the Los Angeles board and pinned it to San Francisco's. The gate is
the board's own bounds now, which is the same question asked honestly.

Every door through the 0.25 m courtyard skin is 1.2 m rather than the
usual 0.9, and that is not a style choice: `blocked` inflates by the
walker's radius *and* the wall's thickness, so a 0.9 m leaf through a wall
that thick seals the room behind it while rendering perfectly. Two rooms
were sealed exactly that way on the first pass. The test says so, because
a well-meaning edit back to 0.9 for consistency would do it again.

**People move.** `samplePresenceAt` thinned the roster by hour, which
fixed a building that was full at 1 a.m., but everybody was still pinned
to their own desk all day. Occupancy is a hand-written booking table now:
meetings fill a room for a plausible length, the kitchen island fills at
lunch, and a `Presence` binds to a seat id — so "in a meeting" means
occupying a meeting-room seat, and the whole thing is choosing seat ids
rather than inventing positions.

The flood fill that checks a pack is walkable existed three times over.
One copy now, since the third is where a divergence lives.

**Films.** A new `office-dusk` reel — six hours over Lumbridge HQ catching
the moment the house lights take over from the sun, which is the one thing
only a time-lapse can show and which did not exist. The three existing
reels are re-rendered rather than re-captioned: `films.ts` in v4 named a
commit eleven behind HEAD, so the published reels were shot before there
were any clouds and before the city drew office pins.

Reviewers caught roughly a dozen false statements in the new prose across
these files — a room census that did not add up, a wall-thickness count,
a claim that every room has daylight when one does not, and a cost figure
saying `updateSun` runs once a second when it runs once a minute. The
consequential ones are fixed. In a codebase where the comments are the
design record, a confident wrong number is a defect.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-07 05:50:32 -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 a229fb2721 The sky gets the things above the aeroplanes
Satellites, end to end: CelesTrak element sets behind the same TTL cache
the weather and the flights use, served as TLEs rather than as positions,
and propagated in the browser with SGP4.

Sending elements is the same trick `flights/plan.ts` plays and it has a
better excuse here — a TLE *is* the closed form, valid for days either
side of its epoch, so one cacheable fetch every six hours replaces a poll
and every viewer agrees about where everything is.

Two things are worth knowing about the shape of it:

  - There is no region parameter. An aeroplane at 10,000 m is local and
    a satellite at 550 km is above the horizon for a circle two thousand
    kilometres across, so one catalogue serves both boards and the client
    decides what is above its own horizon. Only the observer is per-city,
    which is why `main.ts` shares the elements and rebuilds the catalogue.
  - The layer draws on a dome, because it cannot draw anywhere else.
    `world.metres(550_000)` is 21,000 scene units against a far plane at
    3,000. Azimuth and elevation are real; the radius carries nothing.

Off by default: a clone that started pulling CelesTrak on `npm run dev`
would have volunteered somebody else's bandwidth for its onboarding.

Godmode gets the two dials that point at the sky rather than at the
light — fabricated traffic, which composes with a live ADS-B feed instead
of replacing it, and a switch for the satellite layer with a count beside
it. Both are god-only lies about the inputs, in the manner of the weather
override.

`satellite.js` is the second runtime dependency this package has taken.
Its entry point star-exports an Emscripten build that cannot be shaken
out, so `noWasmPropagator` in the Vite config cuts it: 308 kB of WASM
loader for a bulk propagator nothing calls, against 26 kB for the SGP4
that does the work.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-06 20:57:14 -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 dac12cecec The floor keeps up with the room
Occupancy was fetched once, on the way in. Somebody sat down and you did not
find out until you left the office and came back, which for a view whose entire
subject is who is in the building is the point missed by one request.

`watchPresence` is `watchWeather`'s shape without the two pieces of judgement
that one needs and this does not: weather has to decide whether an observation
is too old to be honest about and whether it describes the place you are looking
at, and a roster is neither — it is true when it is served, and it is addressed
by office id, so it cannot arrive about somewhere else.

Thirty seconds, and the number comes from what the data does rather than from
what the network will stand. Ten minutes is right for weather because nothing
upstream of it moves faster; occupancy moves when a person stands up. Backoff
tops out at five minutes rather than the weather watch's hour, because the
difference between the two is what the user is doing while the box is down:
nobody is staring at the sky waiting for it to be redescribed, and somebody *is*
standing in a room they expect to see people arrive in. An hour of silence there
reads as a broken feature rather than as a quiet API.

Two things it does that the weather watch does not, both because this one runs
while a person is looking at the thing it describes. It stops dead while the tab
is hidden and asks again the moment it comes back — a backgrounded tab polling a
roster nobody can see is waste on both ends, and it is the commonest state a
long-lived office tab is in; coming back has to be immediate rather than at the
next tick, or you return to a floor up to thirty seconds stale at exactly the
moment you are looking hardest. And it publishes only on change, by signature
rather than by identity: every answer is a fresh array, so without the
comparison a still floor would rebuild its presence meshes twice a minute
forever.

The watch belongs to the visit and not to the page, which is `weatherWatch`'s
rule for a sharper reason — a roster is requested with a credential and names
people, so one left running after somebody stepped out to the city is a page
quietly asking about a room nobody is looking at. Stopped before the scene swap
in `leaveOffice`, so the last thing it can do is abort a request rather than
publish into a room already left, and stopped again in `mountCity`, which
disposes the office under it.

Checked in the browser, both exits. Standing in the office: one ask on arrival,
the next 31 s later under failure backoff, not a flood. Stepping out by the `O`
key on the office host (a real navigation) and by the scene swap on
`?view=office` (which is what actually exercises `stop()`): zero requests in the
following 45 s, in both.
2026-08-06 03:46:01 -07:00
karti d188db9299 The office plan takes the phone's pixel ceiling too
`minimap.ts` is handed `deviceProfile().maxPixelRatio` and `officeMinimap.ts`
was not, so it defaulted to 2 and rendered a phone's floor plan at twice the
fill rate the city plan had already decided that handset could afford. Two 2D
canvases in one corner of one page disagreeing about what a phone is: exactly
the drift `deviceProfile` lives in a single file to prevent, and stage.ts says
so at the top. Mine to fix — it arrived with the plan two commits ago.
2026-08-06 03:46:01 -07:00
karti 3e9b97ed8b Signing in puts people in the building
`member` and `anon` were told apart inside the office by the client and by
nothing else. `access.ts` picked an `officeDepth`, `createOfficeScene` built a
presence layer at full depth — and then nobody ever called `setPresence`, so
both tiers rendered the identical empty room. A tier that changes nothing you
can see is not a tier, and `routes/markers.ts` had already written down why one
drawn only in the browser is worse than none: it is a UI hiding a control over a
body the API hands to whoever asks.

So the refusal happens on the server now.

`GET /api/v1/offices/:id/presence` is the one route that always takes a session,
whatever else the deployment is configured for. `markers.ts` serves its feed to
anonymous callers when no feed is configured, on the grounds that there is
nothing there to protect; that reasoning does not transfer, and the difference is
the whole point — a marker is a company at an address and a presence is a person
at a desk.

The ordering inside the handler is the security property, not a detail. It
resolves the viewer *before* it looks at the id, so an anonymous caller gets an
identical 401 for a real office, a private one and one that was never created.
Check the office first and 404-for-unknown against 401-for-known tells them apart
perfectly, which is the enumeration oracle CONTRACT.md §6 forbids, wearing a
different status code. Three requests and one `deepEqual` hold that down.

`TERA_PRESENCE_DIR` is a second directory rather than a `people` field on the
pack, and that is the design. `types.ts` says a `Presence` binds to a `seatId`
and never to a coordinate so the geometry can be published while the people
cannot — which buys nothing if both live in one file, because an operator who
wants a public floorplan then has to strip the roster out by hand, and the first
time they forget the leak is permanent. Two directories makes the safe thing the
default thing. An office with no roster is 200 and empty, never 404: "no such
office" and "nobody has told me who is in this one" are different problems with
different fixes, and one 404 sends an operator after the wrong one.

On the client, occupancy arrives after the room is on screen rather than before —
the building is worth looking at while a second request is in flight. An API that
answers is believed, including when it answers with nobody; an office where
everyone has gone home is a real fact and overwriting it with invented people to
liven up the demo is the one thing this must never do. An API that does not
answer falls back to a fabricated roster, exactly as the markers do, because a
clone with no server is the flagship case and a member shown the same empty room
as a stranger has been told the tier means something when it does not.

Those twenty-five people are invented and the page says so. `sample.ts` says it
to a reader of the source; `#office-badge` now says "Sample occupancy — these
people are invented" to the person looking at the room, and it is not suppressed
when a real deployment's API merely happened to be down — that is exactly the
case where a member would otherwise read invented names as their colleagues.
Fabricated names at real desks look like a staff list, and a screenshot of one
must not be possible to take without the caption.

The floor plan marks the occupied desks, one colour for everybody where the
scene has four: at three device pixels a hue is a guess. The plan answers "is
anyone there" and the room answers "who, and what are they doing". Hovering a
desk names them, and the readout reads as an address getting more specific —
metres, then room, then person.

server: 127 tests pass, 11 of them new. Client typechecks and builds; the office
chunk absorbed the plan renderer and the entry chunk moved 2.3 kB for the sample
roster. Checked in the browser at office.lumbridgecorp.com: FULL VIEW, the badge,
figures at the benches, dots on the plan, and "3.7, 16.7 m · Alcatraz ·
Clementine Roux" under the pointer.
2026-08-06 03:46:01 -07:00
karti df534c3530 On the office door, the map is of the office
office.lumbridgecorp.com and tera.lumbridgecorp.com are one bundle behind two
names, and the office door had two ways of forgetting which one you came in
through.

The plan panel was the loud one. `createMinimap` is built per city, from
`city.world`, and nothing swapped it when you stepped inside — so the one piece
of chrome whose entire job is to answer "where am I" went on drawing the Bay
Area while the scene in front of it was a thirty-four-metre floor plate. Not
merely unhelpful: pointing at another county.

So there is a second widget for the other place. `officeMinimap.ts` reads the
`Plan` the scene was already built from — no second resolution pass, no geometry
re-derived — and draws it: room slabs, the walls as their `solid` runs only,
which leaves a gap at every door and arch because `Plan` had already split them
for the collider; a glazing hairline across each window, without which the
reference office's north edge simply is not there; furniture at its real
footprint from the asset registry, turned by its own yaw, so four benches of
twelve read as four benches of twelve; room names over a halo, because a room's
centroid is usually the middle of its own desks. Same corner, same `M`, same
camera footprint, same click-to-seek, same keyboard aim. The footprint is cut
against `level.floorY` rather than y=0, which is the same number on a one-storey
pack and a whole storey out on any other.

It is a second module rather than a mode inside the first because the two share
their shape and almost none of their content: one projects lat/lng through a
`World`, rasterises a coastline and a hillshade and follows the sun; this one is
already in metres and lives under a fixed interior rig where the sun does not
reach. One file would have been an `if (city)` at the top of every function.

It arrives in the office chunk, not the entry chunk. It reads prop footprints
off the asset registry, so it is downstream of the furniture catalogue that the
`await import()` in `loadOffice` exists to hold back; a value import in main.ts
would have pulled all of it forward and silently undone the split. So main.ts
takes the constructor as an argument and names the module type-only, and the
build agrees: officeMinimap is its own 10.3 kB chunk and the entry is unmoved.

The quiet one was the URL. Leaving the office swapped the scene and left the
address bar saying `office.` — which is the thing you copy, bookmark and send to
someone, and it took them somewhere other than what you were looking at. From
the `office.` host the exit is now a real navigation to the same hostname with
its first label swapped: the same string comparison that decided this was the
office door decides where the city is, so a deployment serving one name and not
the other gets an honest 404 on a name it chose not to serve rather than a
silent lie. `?city=` carries the metro across so stepping out into SoCal lands
in SoCal, and is read unconditionally because a deep link to a city is a
reasonable thing to want on its own. On `?view=office` there is no other host to
go to, so the scene swap stands and the query is dropped on the way out.

Checked against the production build with both real hostnames resolved at a
local static server: office.lumbridgecorp.com boots into the office with the
floor plan in the panel, the hover readout gives office metres and the room
under the pointer ("9.8, 16.7 m · Bernal"), `O` lands on tera.lumbridgecorp.com
showing the Bay Area, and the console is clean.
2026-08-06 03:45:53 -07:00
karti 1db868ad70 The sky is not something an account grants you
`liveData` was one flag meaning two unrelated things, and it was set to
`tier !== "anon"`. That reasoning does not survive asking what the data actually
is. Cloud cover over San Francisco is a reading from a government sensor. The
aircraft are broadcasting their positions, unencrypted, to anyone within range
who owns a forty-dollar receiver. Neither is withheld from anybody by anybody,
so neither is a thing an account can grant access to — and putting them behind a
sign-in cost the only moment that makes this project land: real fog rolling off
the Pacific onto a city you recognise, at the real time of day, on a first
visit. On an SSO-gated deployment it cost that moment for every visitor there
currently is.

So `liveData` splits. `liveEnvironment` is public and unconditional.
`liveMarkers` is asked for by everyone and granted by the server, because the
marker set is the one feed here that can carry something private — a company's
pipeline, a person's job search — and whether it is public is a property of the
deployment, not of a file in this repo.

`TERA_MARKERS_ACCESS` is therefore a server switch and its default is `members`,
which is the safe answer rather than the common one. The failure mode of getting
this wrong is silent: nothing throws, nothing looks broken, the data is simply
readable by the internet. An operator who wires real markers up gets the shut
door without having chosen it and has to say `public` out loud — and saying it
appends a line to `degraded[]`, so `/api/v1/health` reports that this box is
publishing its map without anyone having to go and read the env file. Same
reasoning as `TERA_ADMIN_SUBJECTS=*`.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-06 03:36:12 -07:00
karti e41c90fe8d Real weather, real aircraft, a heightfield off the main thread, and instruments
Three things that were built and never connected, connected.

**The weather was already there.** `observe()` has always taken a
`WeatherObservation` and `main.ts` has always passed null, so the cloud,
precipitation, visibility and marine-layer paths in atmosphere.ts had never run
outside a test. The server already shipped NWS, met.no and Open-Meteo, all
configured off. What was actually missing was that a single TERA_ORIGIN_LAT/LNG
served one metro and lied to the other — so weather and traffic are per-region
now, derived from the city's own bounds, and the Bay Area gets its fog while
Long Beach gets its own sky. The route takes ?city= or a validated ?lat=&lng=
and refuses to become an open geocoding proxy for the planet.

**The heightfield moved to a Worker.** 2.3 s of blocked main thread at boot, and
another ~950 ms of point-in-polygon on top of it: the park mask is filled in the
worker now, and block placement samples four corners and only runs the exact
test on a cell that straddles an edge — 8 buildings differ out of 185,036.
createScene is async and takes a Stage as a consequence, and there is a
main-thread fallback because "clone it and it works" has no exception clause.

**Spaces is a chunk you fetch when you reach for the door**, not one everybody
downloads. Same for the godmode tools. The entry chunk is 722 kB rather than
772; three.js is most of what is left and splitting it is a different job.

**Godmode is an instrument panel now** rather than one slider: the date and the
season, not just the hour, so the Meeus moon and the sun's seasonal arc become
visible instead of merely correct; a weather override that says on screen when
it is lying; a frame-time and draw-call readout; and a pose editor that emits a
paste-ready Chapter block, which is the thing that makes adding New York cheap.

Two blockers the review caught:

  - Every city switch leaked 8 GPU textures — one of them a 2048x2048 shadow map
    — and ~10.5 shader programs, and deleteTexture had never been called once in
    the app's lifetime. The renderer was being built per scene; it belongs to the
    canvas, for the life of the page.
  - An upstream fetch that threw rather than returning null skipped the cache
    stamp, so the TTL — the only rate limit on outbound calls — collapsed to one
    upstream request per inbound request, and the caller got a 500.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-06 03:25:31 -07:00
karti a6f6a91813 A disclosure nobody reads is a watermark
"sample data · fabricated, not real companies" sat in the corner of every load,
because no deployment has a markers source wired by default and the sample set
is therefore the overwhelmingly common case. A caption that is always on screen
stops being read after the first second, which is the opposite of what a
disclosure is for.

So the corner keeps only the rare half — "live data", which is genuinely worth
interrupting someone to say — and the fact that the markers are invented moves
to the two places someone actually looks: the boot card everyone passes through,
and the `?` card, which is one keypress away from any state the app can be in.
It did not go into a README nobody opens.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-05 23:08:50 -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 44c5a79424 SoCal, the whole bay, a moon, and gates that actually run
Six agents in parallel, and the two city packs independently reported the same
blocker: `focusRegions` and `coarseFactor` existed on the `City` type and
nothing implemented them. Uniform lattices would have been 2.9M points for
Southern California and 3.7M for the expanded bay. Both packs were unloadable
as written.

`buildAxis` is the answer, and it is honest about its limits: refinement is per
axis, not per rectangle, so a focus region sharpens its whole row *and* its
whole column. Two regions at opposite corners refine nearly everything between
them. Measured, not guessed — the bay went 0.53M points with one region and
1.64M with three, for detail nobody is looking at from a board this wide. One
region each, coarse factor ten, and the builds land at 3.8 s and 2.3 s.

Then three things that were only ever right because San Francisco was the only
city. `maxDistance: 340` and a 170-unit shadow box were constants tuned for a
230-unit board; the bay is 1003 units across and the camera physically could
not retreat far enough to frame it. Fog distances were scene units pinned to
the same assumption. And `minVisibilityM` defaulted to 4.5 km of honest
weather, which over ninety-four kilometres of bay correctly hides three
quarters of it — the night view was a black rectangle for a completely
reasonable reason. All three now derive from the board.

The moon is a real ephemeris and its light is a deliberate lie: 1.15, against a
physical ratio of one to four hundred thousand. What is being reproduced is
what a moonlit night looks like on a screen in a lit room.

The CI gate caught itself, which is the part worth keeping. Port 8431 was
already held by a server from an earlier session, so the boot check polled a
healthy stranger while the process it started died on EADDRINUSE. It now
refuses to run rather than pass.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-05 03:13:32 -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
Karti Tripathi 67f8df8d53 Lumbridge Simulate Engine — the city, and the licence it can actually ship under
LSE is the third of the three, beside lumbridge-compute and lumbridge-bench: a
3D engine for walkable places. This first commit is the outside of the world —
San Francisco — plus the seams the inside will attach to.

The engine renders a City and a list of Markers and knows nothing else. It does
not know markers are usually companies and it will never learn that "rejected"
is red; that mapping lives in an adapter. Which is what lets one renderer serve
a private map, a public one, and a self-hoster with no Lumbridge account, none
of them a fork of the others.

Three things were designed around the licence rather than discovered after it,
because each one is a promise Apache 2.0 makes that is easy to break by
accident. No trademarks in the repo — logos are fetched at runtime, and
public/logos/ is gitignored. No OpenStreetMap-derived coordinates, which is why
every coastline in cities/sf.ts was traced by hand: Nominatim output is ODbL,
share-alike, and would attach to the whole pack. And no FlightRadar24 client —
their terms forbid scraping and redistribution, so flights are an interface
with a simulator and open community ADS-B behind it.

The privacy constraint and the licence constraint turned out to want the same
thing. Geocoded company positions and pipeline status both stay behind Workie's
API; the open repo holds the city and the renderer. The tempting shortcut —
commit an sf-companies.json — breaks both at once.

Ported out of Workie, where a 3D city engine had no business living. Workie's
/live is deleted rather than deprecated.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-04 21:49:08 -07:00