1
0
Commit Graph

81 Commits

Author SHA1 Message Date
karti bcac6aa41a feat: the crane grows a mast, the harbour works a shift, and the site is re-shot
**The Asset Factory verdict, and it mostly went against the vote.** Nine
candidates were thumbed up. One was taken.

TOOK the STS crane. Rebuilt in `ports.ts` from 5 unit boxes to 11 — an A-frame
mast and apex cap, a forestay to the boom, a backstay to the tail, a sill, the
truck-lane portal beam, a machinery house — still exactly ONE InstancedMesh.
What was missing is the thing that makes a gantry a gantry: on a real STS the
tallest part of a WORKING crane is the A-frame apex, not the boom, and a parked
raised boom clears its own apex by only 15-25%. Before, 56 gantries read from
altitude as 56 crosses — two coincident verticals with one bar through them and
nothing above it — so a berth flattened into a picket fence.

Proportions came from both upvoted candidates agreeing independently (hinge ~58 m
under an apex at 99-104 m), taken conservatively because Tera's packs already
author an 82 m hinge against a real 55-60.

The apex beacon came across as EMISSION: `craneLights()` returns bare positions,
`nightlights.ts` turns them into one additive Points cloud, 56 points, one draw
call, night only, no THREE.Light anywhere. 0.09 units was invisible against the
port's own cream emissive; 0.17 — half a bridge head light — is right, and the
screenshot at 0.09 is what condemned it.

REJECTED all three bridges, city-lights and both aircraft: the incumbents won on
the picture, decisively for the bridge.

TWO PARTS WERE BUILT FROM THE APPROVED CANDIDATES, PHOTOGRAPHED, AND CUT. Four
legs: 14 m of quay spacing is 0.036 units at 391 m/unit against a 0.032 member
floor, so 90% overlap. A portal X-brace: the bay is 0.115 wide by 0.38 tall, so
both diagonals come out near-vertical and add a lump at mid-leg. Both are among
the best things about the factory cranes AT THE FACTORY'S FRAMING. Neither
survives at board scale. That gap is the whole reason a factory asset is
reference geometry and not a drop-in.

Fixed a defect the rebuild exposed: the backreach started a full rail-gauge
behind the hinge, leaving a gap over the portal with the beam floating below it.
One unbroken girder now. And every inclined member goes through a `strut()` that
takes two points in the (distance-along-boom, height) plane, so the
vertical-exaggeration bug the module header warns about is no longer reachable —
it needs a length and an angle, and there is now no way to start from those.

**The harbour works a shift.** It was a frozen tableau: 19 hulls placed from the
pack's berths that never changed. Vessels now arrive through the channel, are met
by a tug, berth, work and depart — seeded, so two people see the same harbour and
a capture script shoots the same frame twice. A ship loses its wake when it ties
up, because the wake is the information.

**Every still and film re-shot.** The site was showing a Tera that no longer
existed — SHOTS_COMMIT b7f5c41, FILMS_COMMIT 2aa4049, against an engine that has
since gained fires, the whole state, ports, ships and night infrastructure. Two
frames were bad and are fixed by moving the hour, not by retouching:
`bay-relief-day` and `peninsula-day` were white lids of marine layer. Four
captions described a Tera that no longer existed and are rewritten to the
delivered frame. `california-relief-night` is measurably brighter than the frame
it replaces (canvas mean 7.91 -> 10.57) despite the state being 30% larger.

Ten budget cells pass, run twice. socal 1,422,025 -> 1,429,993 triangles against
1,700,000, 218 draws against 320. The measured delta is double the geometry
because the crane mesh casts shadow, so renderer.info counts it in both passes —
worth knowing before anyone reads that number as geometry.

Tests 1,540 -> 1,570, server 295.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-23 03:29:58 -07:00
karti e7a8aa028b fix(satellites): declare uPixelRatio, which was breaking the layer in production
`ShaderMaterial` prepends three's own built-ins — projectionMatrix,
modelViewMatrix, position — but it does NOT prepend the custom uniforms declared
in the `uniforms` object. `uPixelRatio` was in that object and never declared in
the GLSL, so the vertex program failed to compile with

    ERROR: 0:79: 'uPixelRatio' : undeclared identifier

and the whole satellite dome silently drew nothing on the deployed site while
every other layer carried on. The only symptom was a console error nobody was
reading — which is exactly why the deploy check now opens the page and greps the
console rather than trusting a 200.

Found by screenshotting the LIVE site after deploying, not by any test: no test
in this repo compiles a shader, `npm run build` cannot, and `ui-smoke` passed
because the page boots fine with one layer missing.

Verified after the fix at two hours on two boards: zero shader errors, where the
deployed build had one on every load.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-22 23:41:05 -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 b7f5c41da5 feat(brand): a shot can take a body, so the last hand-made stills are generated
Three pictures on /arena — the chase camera behind the EV, the crow in flight, the
aircraft over the board — were the only imagery on the site with no generator
script anywhere in either repo. They were made by hand, and so they were a single
daylight frame served to dark-mode readers too, with no pair to choose from.

`shots.mjs` gains a `play` capability beside `aim`: a shot names a control mode,
the script clicks the app's own mode dock, and only then shoots — with the HUD
deliberately left in frame, because on these three the body and its readout ARE
the subject. `#mode-dock` and `#play-hud` are in FURNITURE.BARE rather than
CLUTTER for that reason, and there is a comment at the `hide()` call so a later
tidy-up does not crop the subject out.

Three guards, each verified by deliberately making it fire:

  - the mode must be OFFERED, not merely present — asking for `drive` on the Bay
    Area board now says `"drive" is not offered here — the dock is showing
    [overview, actor]`;
  - `aria-pressed="true"` AND `#play-hud` unhidden, the same pair
    `performance-budget.mjs` waits on;
  - the HUD's own body chip, so asking for `Explore` when an anonymous visitor
    gets a crow says `the play HUD says "Crow", not "Explore"`.

The body guard runs twice, at the click and again at the shutter, because a
completed route or a bound ends a mode mid-wait and hands back an orbit wide shot
under the right filename.

`untilProgress` exists because the first delivery drifted: `run: 9000` gave 55% of
the leg in a 1x preview and 69% in the 2x render — a different stretch of road,
with a town's edge across the top of the frame. Road traffic runs from page load,
so a duration is really "boot plus a bit". Waiting on the app's own route
percentage lands preview and delivery on the same frame; overshoot throws rather
than shooting. The crow and the aircraft are built inert, so a duration is exact
for them and only the car needed the gate — measured, not assumed.

The run log now echoes the HUD (`[Drive] 56 mph · US-101 | assisted · 55% ·
chase`), because "shot drive-101-day" is equally true of a parked car, a finished
route and a correct frame.

The crow moved from the state board to the Bay Area. On California a one-metre
bird stands against ground drawn at 1,919 m to the unit with an oak the size of a
hill behind it, and the night frame would have been a black rectangle with a bird
in it. `crow-nav-v1`'s `source` said "Tera / California" and that was wrong on the
environment's own terms as well — `crowNav.ts` runs a ±120 m box with no terrain
at all.

One frame is honestly weak and is recorded rather than dressed up: `drive-101`
at night. From behind, on an unlit corridor, the EV is two tail lamps and a roof
strip. Four hours were shot between 21:15 and 20:05 and the corridor's ground is
equally black at all of them, so the hour is not the lever; both real fixes are in
the engine and are in TODO.md. It is still the honest night frame.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-22 13:30:23 -07:00
karti 2aa4049258 feat(brand): re-shoot everything at the new engine, and make it one command
**Seven new shots**, because the world grew the most photogenic things in it
after the last pass: `sfo`, `lax`, `golden-gate`, `bay-bridge`, `freeway`,
`california-relief` and `pacific-sea`. All nine existing ids are unchanged — the
manifest emits a `ShotId` union that lumbridge-v4 imports, so ids are added and
never renamed.

**A shot can now aim itself.** The chapter list has no camera for SFO, LAX, the
bridges, the freeway or the open sea, and the engine has no `?pose=` back door,
so a shot points itself by driving the app's own inputs: a click on the plan view
slides the orbit target to a lat/lng while keeping the chapter's stance, wheel
notches set the standoff, and a drag sets azimuth and elevation. The plan view's
pixel-to-coordinate map is solved at runtime from three hovers of
`#minimap-readout` rather than hard-coded, so it survives a board resize or a
restyle of the widget.

**The tera share card was a picture of the wrong thing, and had been.** Its art
came from `keyboard.press("2")`, which had landed on the California board's drive
mode once the default board changed — so the card under the headline "Cities from
above." was a chase camera on US-101, showing metre-scale cars driving between
kilometre-wide buildings, with the DRIVE readout and the mode pill baked into the
art. It renders, it looks deliberate, and it is why an unguarded key press has no
place in a capture script. `capture.mjs` now clicks an indexed chapter and
asserts its `shortLabel` the way `shots.mjs` does, waits on `#boot` and
`#chapters` instead of sleeping twenty seconds, and gives each card its own hour.

**`npm run refresh` is the durable half.** One command: build, stills, cards,
films, both manifests, and a hashed before/after diff of every deliverable. It
fails loudly and specifically on the two conditions that otherwise produce
confident wrong output — the renderer coming up as SwiftShader, and a chapter
`expect` guard firing. `--stills-only` / `--cards-only` / `--films-only` compose,
`--dry-run` lists the plan without opening a browser, and
`shots.mjs --list` prints the whole shot plan — board, chapter, expect, aim, both
hours — which is what to run first when a guard does fire.

It also re-stamps `PROVENANCE.json`, narrowly: only entries whose origin is
`repository-generated` and whose `generator` names a script the run actually
executed, by literal hash substitution rather than re-serialising the file.
Without that, every legitimate card re-shoot leaves `npm run provenance` red.

**Every film re-shot.** They were at `9c9e78f`, captured 2026-08-07, and predated
the tone mapping, the reflective sea, the sky dome, terrain shadows, the rebuilt
California board, SFO, LAX, both bridges and the moving aircraft.

Tests 1137, typecheck, build, eight budget cells and every provenance and licence
check pass.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-22 12:49:06 -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 d9f8baf171 docs: track the film re-shoot, which the stills pass did not cover
The stills and both share cards were re-shot at 2b2dd5c. The films were not, and
films.ts records the commit each reel was shot at — so they are visually stale
against a sea that now reflects, a sky that is a dome, terrain that casts shadows
and a California board that has the Mojave on it.

Written down rather than remembered because the KartiOS command board was
unreachable (fetch failed) when this was found.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-22 00:53:43 -07:00
karti 2b2dd5cb64 chore(brand): re-shoot the cards, and stop describing a building that is gone
The share cards were stale in the two ways that survive a code fix, because both
of them end up baked into a picture.

**"Two buildings" was wrong in two files.** The words on the card live in
`og.html`; the words a crawler reads live in `vite.config.ts`. Fixing either one
alone leaves the other saying something else — and there are three packs now, one
of which is a courtyard block in Los Angeles that neither file mentioned. Both now
say three, name the LA block, and mention the microphone on the desk. Lumbridge HQ
also stopped being "a tower floor" some hours ago and is a live/work studio.

**The tera card was cropping around a defect that no longer exists.** Its framing
was pinned to `56% 88%` with a comment explaining that low was necessary to keep
the far edge of the board — where the terrain stopped and the sky began in a hard
diagonal — out of shot, because a card with that seam in it read as a rendering
fault rather than as a map. The sea now runs eighteen board spans and ends past the
fog's far plane, and the sky is a world-space dome, so there is no seam to hide
from. `56% 62%` puts the framing where the picture is best instead.

Also a per-shot `hide` in `shots.mjs`. The studio-hardware card is a
`.panel-group` inside a scrolling card, so when it is taller than the viewport the
last control is below the fold — correct on a screen, where a reader scrolls, and
wrong in a photograph, where the frame edge cuts a gain slider in half and anyone
who does not know the card scrolls reads it as a broken render. It is also a
question of subject: `office-desks` is a picture *of* that hardware and keeps it,
while the LA courtyard is a picture of a courtyard and does not want a column of
audio controls down one side.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-22 00:12:21 -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 8fb85cd2e5 feat: give the boards a horizon, a sea that reflects, and a state worth flying over
The wide shot, which is what an anonymous visitor actually lands on.

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

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

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

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

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

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

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

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

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

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-21 23:43:14 -07:00
karti 655848746d fix(ui): give the rail its own short labels instead of the sheet's sentences
The corner rail rendered `Shortcut.meaning`, which is written for the `?` sheet
where a full sentence is exactly right because somebody is reading a reference.
The rail is a glance over a moving 3D board with room for one line, and it is a
`flex-wrap` container: the sentence wrapped, and the wrapped line drew straight
through the chip beneath it. On the deployed build "Fly to a chapter of the tour,
or to a viewpoint inside a studio" and "Walk into the studio, or step back out to
the city" overlapped and neither could be read, over a bright sky with no
background to hide it. Nothing errored — it just looked broken, on every screen.

Two labels for two jobs. `Shortcut.rail` carries the two-or-three-word form for
the nine ids `railHints` can select; `meaning` keeps the sentence for the sheet.

`.hint` also had no `line-height`, so it inherited the body's and a wrapped chip
had nowhere to go, and no `max-width`, so the rail grew leftward until it met the
mode pill. Chips are now `white-space: nowrap`: wrapping belongs between chips,
which the flex container already does, not inside one.

Tested, because this is a defect a test can hold: every id the rail can return
must have a `rail` label, it must fit on a line, and it must not contain a
clause.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-21 19:56:55 -07:00
karti 75908774f7 fix(flights): identify this service to adsb.lol, which refuses anonymous callers
The real reason the sky was fake, and it was not the timeout.

`api.adsb.lol` answers `403` with the body `"User-Agent too generic; include
valid contact info."` to any request that does not say who is making it. Node's
`fetch` sends nothing useful, so every live flight fetch was refused — in 268 ms,
not at the 6 s timeout, which is why the previous commit's longer budget changed
nothing. `http.ts` already exported `userAgent()` and `weather/metno.ts` already
used it; flights simply never did.

`config.flights.contact` now follows the pattern satellites already established:
`TERA_FLIGHTS_CONTACT`, falling back to `TERA_WEATHER_CONTACT`, because an
operator has one contact address and not four.

This failed in the way worth being angry about. A `403` became `null` in
`getJson`, `null` means "serve the simulated plan" in `flights/index.ts`, and
both of those are correct in isolation. So visitors got fabricated aircraft while
`/health` reported `flights: adsb` — the source *was* configured and the host
*was* reachable — and `degraded[]` stayed empty because nothing had degraded at
boot. Every signal the service publishes about itself said it was fine.

`server/src/test/adsbUserAgent.test.ts` asserts the header carries the contact,
that the URL is still the region asked for, and that a non-allowlisted host is
refused before a single byte reaches the network. A load-bearing header with no
test is a header that comes off in a refactor.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-21 19:52:35 -07:00
karti ab41302993 fix(flights): give adsb.lol a timeout its tail actually fits in
The sky over California has been intermittently fake and nothing said so.

`http.ts` gives every upstream 6 s. Five consecutive calls to
`api.adsb.lol/v2/point` from the box that serves this deployment came back in
1.2 s, 5.7 s, 24.3 s, 3.5 s and 4.2 s — all `200`, all correct, and three of the
five past the shared default. The feed is a volunteer aggregator answering a
geographic query and its tail is long in a way an NWS observation's is not.

The resulting failure was invisible in the worst way. The fetch did not error, it
timed out, `getJson` returned `null` as designed, and `flights/index.ts` did the
right thing with a `null` — served the simulated plan. So visitors got fabricated
aircraft, `/health` still reported `flights: adsb` because the source was
configured and reachable, and `degraded[]` stayed empty because nothing had
degraded at boot. Every indicator this service has said it was fine.

25 s, above the slowest measured answer, and only for this upstream. It costs
nothing when the feed is quick: the call runs behind `createUpstream`'s TTL, not
on any visitor's request path. On a board whose entire claim is that the aircraft
are live, a slow real answer beats a fast invented one.

Not a regression from the studio build — `http.ts` is untouched by it; this was
already happening.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-21 19:49:48 -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 8738367258 docs: make static deploy cache-safe 2026-08-19 03:35:30 -07:00
karti 20deae2a0f feat(actors): build articulated dog v2 2026-08-19 03:08:04 -07:00
karti 264daaab61 feat: unify life-sim play controls 2026-08-19 03:06:01 -07:00
karti 120eac878a fix: align robot jobs with active HQ plans 2026-08-19 02:59:25 -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 afa285bd5c chore(arena): pin integrated simulator source hashes 2026-08-19 01:18:03 -07:00
karti ddf814a657 feat: build deterministic freeway and Lumbridge EV v2 2026-08-19 01:16:38 -07:00
karti 94de2d8bee feat: build articulated crow flight v2 2026-08-19 01:12:37 -07:00
karti 747744fa6c feat(arena): add deterministic headless RL environments 2026-08-19 01:11:33 -07:00
karti 79793faed2 feat: upgrade electric aircraft fidelity and flight model 2026-08-19 00:53:22 -07:00
karti dd18c6775d fix: close California play and asset acceptance gaps 2026-08-11 22:49: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 655c383061 test: enforce simulation and render budgets 2026-08-11 22:02:29 -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 d841575315 feat: add private office media signaling 2026-08-11 20:48:19 -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 df9641cddd The share cards are photographs of the building that exists
Both cards re-rendered off the current engine, and the office card's own
copy fixed at its source: `og.html` said "one floor plan" while the
artwork behind it showed two storeys held apart around a commons. The meta
description in `vite.config.ts` and the words baked into the picture are
two different strings and only one of them had been corrected.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-07 04:37:45 -07:00
karti effa077cc7 The shot list catches up with the building it is photographing
The two office stills were captioned for a building that no longer exists:
"thirty-four metres by eighteen, one storey" against a plate that is now
forty-eight by eighteen over two storeys, and a night note promising the
office "keeps its own light after the sun has gone" — which was
aspirational when it was written and is only now true.

Two new frames. `office-commons` is the double-height wing, which is the
one view that shows why the storeys are held fourteen metres apart.
`office-hangar` is Frontier Valley, and it needed a new capability: the
harness presses the office picker before choosing a chapter, because
switching buildings disposes the scene and rebuilds it, so a chapter click
issued first would land against a legend that no longer exists. Selected by
visible label and asserted, for the same reason `chapter()` asserts its
`shortLabel` — a reordered `OFFICES` table would otherwise photograph one
building and caption it with the other's name.

The office share card said "Lumbridge HQ, one floor: the rooms, the desks
and who is at them". It is the first thing a stranger sees and it was wrong
in three ways.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-07 04:34:39 -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 18dadda917 The surface library can be photographed
`/simulate/assets` on lumbridgecorp.com argues that the art in this repository
is code — eight textures drawn on a 2D canvas from seeded value noise,
thirty-five surface roles pairing one of those with a colour and a roughness,
and no binary art anywhere, enforced by a CI job. It made that argument in about
four thousand pixels of prose with nothing on it to look at, which is a strange
way to talk about the appearance of things.

`textures.mjs` writes the library out: one tile per texture kind drawn by
`TextureBin`, multiplied by the colour of the first role that carries it, and a
neutral tile beside it so a reader can see for themselves that the map has no
hue of its own. The role table goes with them as data, so the site sets it in
its own type rather than baking labels into an image.

It is the one capture here that runs against Vite dev rather than `dist/`, and
the reason is in the header of both files. `shots.mjs` and `films.mjs`
photograph the *application*, and the application a visitor gets is the built
bundle. This photographs a *module*. `TextureBin` is not reachable from the
bundle — it exposes no names — and making it reachable would mean a third Vite
entry, which would ship a texture-sheet page to tera.lumbridgecorp.com so that a
script could screenshot it. Vite dev transforms `/src/assets/*.ts` on request,
so the page imports the same files a reader opens on the repository.

Nothing in it re-implements the tables in `materials.ts`: it asks
`MaterialRegistry` for every role in `DEFAULT_INTERIOR_PALETTE` and reports what
comes back, so a role that changes its texture or its roughness changes the
sheet on the website with it.

No GPU, no WebGL, no renderer string to check — Canvas2D and the browser's own
WebP encoder, about two seconds for all sixteen tiles at 97 kB total.

`check-no-binaries` still passes: these live under `scripts/`, and the tiles are
written into the lumbridge-v4 checkout, not this one.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-06 23:50:23 -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