1
0

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>
This commit is contained in:
Karti Tripathi
2026-08-05 03:13:32 -07:00
parent 8bcb391455
commit 44c5a79424
25 changed files with 9246 additions and 220 deletions
+333
View File
@@ -0,0 +1,333 @@
/**
* The real adapter: markers, weather and traffic from the Tera API, with the
* bundled sample data underneath it.
*
* This is the one place in the browser build that knows the API exists. The
* engine takes `Marker[]`, a `WeatherObservation` and a `FlightSource` and has
* no idea where any of them came from — that boundary is what lets one renderer
* serve a private career map and a public sector map without either being a
* fork (ARCHITECTURE.md §3.3), and it is why this file is an adapter rather than
* a client scattered through the scene.
*
* **Every call degrades instead of failing.** No server, a 404, a static host
* answering `/api/v1/markers` with its own index.html, a network that has gone
* away mid-session: all of it lands on the sample data in `sample.ts` and the
* synthetic clear day below, and the map keeps rendering. That is not defensive
* habit, it is the acceptance test the whole repo is held to — a stranger clones
* this, runs one command, and gets a city, with no account, no key and no
* network (CONTRACT.md §0). A `npm run build` deployed to any static host is a
* working Tera; pointing it at a server is an upgrade, not a requirement.
*
* The wire types live in `src/server/wire.ts` and are types only, so importing
* them costs the bundle nothing.
*/
import type { WeatherObservation } from "../engine/atmosphere.ts";
import { sampleRoute, SimulatedFlights, type SimRoute } from "../engine/flights.ts";
import type { Aircraft, FlightSource, Marker, MarkerPalette } from "../engine/types.ts";
import { seededRandom } from "../engine/world.ts";
import type {
FlightsBody,
FlightsPlanBody,
HealthBody,
MarkersBody,
OfficeDoc,
WeatherBody,
} from "../server/wire.ts";
import { SAMPLE_MARKERS, SAMPLE_PALETTE, SAMPLE_ROUTES } from "./sample.ts";
/** Where the API lives, per CONTRACT.md §5. Same-origin, behind the site's own proxy. */
const DEFAULT_BASE = "/api/v1";
/** How long any one request may take before the fallback is used instead. */
const DEFAULT_TIMEOUT_MS = 4000;
/** How long to wait before trying the flights endpoint again after it fails. */
const RETRY_SECONDS = 30;
export interface TeraApiOptions {
/**
* Base URL, with no trailing slash. Absolute is allowed and is what a
* self-hoster running the browser build and the API on different origins
* wants; the default assumes they are the same origin.
*/
base?: string;
/** Injected for tests. Absent means `globalThis.fetch`. */
fetch?: typeof fetch;
timeoutMs?: number;
/**
* The palette live markers are coloured by.
*
* The engine looks `colorKey` up and the wire does not carry colours, so
* somebody has to supply this and it cannot be the server: what a key *means*
* is the consuming app's business. When the API is absent this is ignored and
* `SAMPLE_PALETTE` is used instead, because sample keys are not the caller's
* keys.
*/
palette?: MarkerPalette;
}
/**
* A response, plus whether it is real.
*
* `live` is the field that stops a fallback from being a lie. A demo showing
* invented companies and a deployment showing real ones must not look identical
* to the code above them — the caller is expected to say so in the interface,
* and cannot if the adapter quietly papers over the difference.
*/
export interface Feed<T> {
value: T;
live: boolean;
}
export interface MarkerFeed extends Feed<Marker[]> {
/** The palette these markers are meant to be read with. */
palette: MarkerPalette;
/** ISO-8601 snapshot time, or `null` for the sample set, which has no date. */
generatedAt: string | null;
/**
* Rows the server's public-shape gate refused, by reason and count.
*
* Passed through rather than swallowed. A gate that drops rows silently is
* indistinguishable from an empty database, which is exactly the confusion
* `MarkersBody.refused` exists to prevent — and it is the visible end of the
* provenance rule in CONTRACT.md §8.
*/
refused: { reason: string; count: number }[];
attribution: string[];
}
export interface WeatherFeed extends Feed<WeatherObservation> {
attribution: string[];
}
export interface TeraClient {
/** What the deployment turned out to be, or `null` if there is no server. */
health(): Promise<HealthBody | null>;
markers(): Promise<MarkerFeed>;
weather(): Promise<WeatherFeed>;
/**
* The traffic source, built once. It fetches on its own schedule and never
* blocks the render loop; see `HttpFlights`.
*/
flights(): FlightSource;
/**
* One office pack. `null` for anything the server will not serve — including
* a private one, which answers 404 rather than 403 so the endpoint cannot be
* used to enumerate what exists (CONTRACT.md §6).
*
* There is deliberately no fallback here. A missing marker can be stood in for
* by a fictional one; a missing floorplan cannot be invented, and an app with
* a bundled office of its own already has the better answer.
*/
office(id: string): Promise<OfficeDoc | null>;
}
export function createTeraClient(options: TeraApiOptions = {}): TeraClient {
const base = (options.base ?? DEFAULT_BASE).replace(/\/+$/, "");
const timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS;
const doFetch = options.fetch ?? globalThis.fetch?.bind(globalThis);
/**
* One GET, and `null` for every way it can go wrong.
*
* Deliberately undiscriminating. A 404, a timeout, a CORS refusal, a static
* host serving `index.html` with a 200 and an HTML content type — the caller's
* response to all of them is the same, and a taxonomy of failures nobody
* branches on is a taxonomy nobody maintains.
*/
async function get<T>(path: string): Promise<T | null> {
if (!doFetch) return null;
const controller = new AbortController();
const timer = setTimeout(() => controller.abort(), timeoutMs);
try {
const res = await doFetch(`${base}${path}`, {
signal: controller.signal,
headers: { accept: "application/json" },
});
if (!res.ok) return null;
// Checked rather than trusted: a static host answers an unknown path with
// the SPA shell and a 200, and `res.json()` on HTML throws where a content
// type check just returns.
const type = res.headers.get("content-type") ?? "";
if (!type.includes("json")) return null;
return (await res.json()) as T;
} catch {
return null;
} finally {
clearTimeout(timer);
}
}
let flightSource: FlightSource | null = null;
return {
health: () => get<HealthBody>("/health"),
async markers(): Promise<MarkerFeed> {
const body = await get<MarkersBody>("/markers");
if (!body || !Array.isArray(body.markers)) return sampleMarkerFeed();
return {
// A `WireMarker` *is* a `Marker` with a provenance field on it, so this
// is a widening and not a translation — which is the property
// `wire.ts` chose the shape for. The provenance itself is the server's
// to enforce and is deliberately not re-checked here: a browser
// silently dropping rows a self-hoster explicitly allowlisted would look
// exactly like an empty database.
value: body.markers,
live: true,
palette: options.palette ?? {},
generatedAt: body.generatedAt ?? null,
refused: body.refused ?? [],
attribution: body.attribution ?? [],
};
},
async weather(): Promise<WeatherFeed> {
const body = await get<WeatherBody>("/weather");
if (!body) return { value: CLEAR_DAY, live: false, attribution: [] };
// `WeatherBody` is structurally a `WeatherObservation` plus fields no
// renderer reads, which `atmosphere.ts` says in as many words. The extra
// fields ride along harmlessly and the engine never sees them.
return { value: body, live: !body.synthetic, attribution: body.attribution ?? [] };
},
flights(): FlightSource {
flightSource ??= new HttpFlights(get, SAMPLE_ROUTES);
return flightSource;
},
office: (id) => get<OfficeDoc>(`/offices/${encodeURIComponent(id)}`),
};
}
/**
* The clear day a zero-config box serves, restated in the browser.
*
* The server does this too — a weather source configured without what it needs
* is demoted rather than fatal, and it answers `synthetic: true` forever
* (CONTRACT.md §5.1). This is the same answer for the case where there is no
* server at all. Note what it does *not* do: `visibilityKm` stays null, which
* `atmosphere.ts` reads as "nobody measured" rather than as "unlimited", so San
* Francisco's marine layer still runs off its own climatology instead of being
* overruled by a fact nobody observed.
*/
const CLEAR_DAY: WeatherObservation = {
cloudCover: 0.1,
precipitation: 0,
visibilityKm: null,
windKph: null,
windDirDeg: null,
condition: "clear",
};
function sampleMarkerFeed(): MarkerFeed {
return {
value: SAMPLE_MARKERS,
live: false,
palette: SAMPLE_PALETTE,
generatedAt: null,
refused: [],
attribution: [],
};
}
// ---- Traffic --------------------------------------------------------------
/**
* Traffic over HTTP, in whichever of the two shapes the server chose.
*
* `poll()` is synchronous and never awaits the network, which is the whole
* design. `scene.ts` calls it from the render loop, and a source that returned a
* promise resolving on a slow fetch would put a frame's aircraft update behind a
* round trip; instead the network runs in the background on the body's own TTL
* and `poll` answers from whatever is currently in hand.
*
* The two modes are not symmetrical, and `wire.ts` explains why. A *plan* — the
* simulator's routes, a fixed epoch and a seed — is evaluated locally at one
* request per TTL, and because the epoch is fixed rather than the server's start
* time, two people on different machines see the same aircraft in the same
* places. *Live* traffic has no closed form, so it arrives as positions and is
* refetched.
*
* Until the first response lands, and after any failure, this is the simulator
* over `SAMPLE_ROUTES`. An empty sky is a worse answer than an invented one, and
* the invented one is labelled as such in `sample.ts`.
*/
class HttpFlights implements FlightSource {
/**
* One second, which is the *evaluation* cadence and not the request cadence.
* A plan is arithmetic and wants to be evaluated every frame or close to it;
* the network is on `nextFetchAt` and is a great deal slower.
*/
readonly interval = 1;
private readonly fallback: SimulatedFlights;
private plan: FlightsPlanBody | null = null;
private planPhase: number[] = [];
private live: Aircraft[] | null = null;
private nextFetchAt = 0;
private fetching = false;
constructor(
private readonly get: <T>(path: string) => Promise<T | null>,
fallbackRoutes: SimRoute[],
) {
this.fallback = new SimulatedFlights(fallbackRoutes);
}
poll(): Aircraft[] {
this.refreshIfStale();
if (this.plan) return evaluatePlan(this.plan, this.planPhase, Date.now());
if (this.live) return this.live;
return this.fallback.poll();
}
private refreshIfStale(): void {
const now = Date.now();
if (this.fetching || now < this.nextFetchAt) return;
this.fetching = true;
void this.get<FlightsBody>("/flights")
.then((body) => {
if (!body) {
// Hold whatever was already in hand rather than reverting to the
// simulator: a deployment that has been showing real traffic for an
// hour and drops one request should keep showing it, slightly stale,
// not silently swap in fiction.
this.nextFetchAt = now + RETRY_SECONDS * 1000;
return;
}
if (body.mode === "plan") {
this.plan = body;
this.planPhase = phasesFor(body);
this.live = null;
} else {
this.live = body.aircraft;
this.plan = null;
}
this.nextFetchAt = now + Math.max(1, body.ttlSeconds) * 1000;
})
.finally(() => {
this.fetching = false;
});
}
}
/**
* The per-route phase offsets, from the seed the server sent.
*
* Same generator and same order as `SimulatedFlights`, which is what makes the
* server's promise true: every viewer draws the seed once, in route order, and
* arrives at the same sky.
*/
function phasesFor(plan: FlightsPlanBody): number[] {
const rand = seededRandom(plan.seed);
return plan.routes.map(() => rand());
}
function evaluatePlan(plan: FlightsPlanBody, phase: number[], nowMs: number): Aircraft[] {
const seconds = (nowMs - plan.t0) / 1000;
// `WireSimRoute` is structurally `SimRoute`; the restatement in `wire.ts` is
// there so the server can build one without importing three.js.
return plan.routes.map((route, i) => sampleRoute(route, seconds / route.duration + (phase[i] ?? 0)));
}