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>
This commit is contained in:
@@ -0,0 +1,284 @@
|
||||
/**
|
||||
* The HTTP contract: every body that crosses between the browser build and the
|
||||
* Tera API, and nothing else.
|
||||
*
|
||||
* **This file is types only.** It compiles to nothing, which is the whole point.
|
||||
* The browser can import it without paying for a runtime module, and the server
|
||||
* can import it without the browser package becoming one of its dependencies —
|
||||
* so one declaration of each body serves both sides and there is no second
|
||||
* implementation to drift. Everything that imports from here must use
|
||||
* `import type`, which `verbatimModuleSyntax` already enforces.
|
||||
*
|
||||
* Two types here deliberately mirror engine types rather than importing them:
|
||||
* `WireSimRoute` is structurally `SimRoute` from `engine/flights.ts`. The server
|
||||
* must be able to build one and it must not pull three.js in to do it, so the
|
||||
* shape is restated. `WireMarker`, by contrast, genuinely *extends* `Marker`,
|
||||
* because a marker off the wire is handed straight to `setMarkers()` and the two
|
||||
* being the same type is what guarantees that stays true.
|
||||
*
|
||||
* Routes, all under `/api/v1`:
|
||||
*
|
||||
* | route | body | cacheable |
|
||||
* | ---------------- | --------------- | --------- |
|
||||
* | `GET /health` | `HealthBody` | no |
|
||||
* | `GET /flights` | `FlightsBody` | yes |
|
||||
* | `GET /weather` | `WeatherBody` | yes |
|
||||
* | `GET /markers` | `MarkersBody` | yes |
|
||||
* | `GET /offices/:id` | `OfficeDoc` | public offices only |
|
||||
*
|
||||
* See CONTRACT.md §5.
|
||||
*/
|
||||
|
||||
import type { Marker } from "../engine/types.ts";
|
||||
import type { Office } from "../interiors/types.ts";
|
||||
|
||||
/** Path prefix every route lives under. Stated here so both sides read it once. */
|
||||
export type ApiBase = "/api/v1";
|
||||
|
||||
// ---- Errors ---------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* The only error shape. `error` is a stable machine token; `message` is for a
|
||||
* human reading a log and may change without notice.
|
||||
*
|
||||
* Note what is absent: there is no `403`. A request for something the caller may
|
||||
* not see gets `not_found`, because an endpoint that distinguishes "does not
|
||||
* exist" from "exists, but not for you" is an enumeration oracle. See
|
||||
* CONTRACT.md §6.
|
||||
*/
|
||||
export interface ErrorBody {
|
||||
error: "not_found" | "bad_request" | "unauthorized" | "upstream_unavailable" | "internal";
|
||||
message: string;
|
||||
}
|
||||
|
||||
// ---- Health ---------------------------------------------------------------
|
||||
|
||||
export type WeatherSourceId = "none" | "nws" | "metno" | "openmeteo";
|
||||
export type FlightsSourceId = "sim" | "adsb" | "dump1090";
|
||||
export type MarkersSourceId = "none" | "file";
|
||||
export type AuthMode = "none" | "sso" | "jwt";
|
||||
|
||||
/**
|
||||
* What this deployment turned out to be, once the environment had its say.
|
||||
*
|
||||
* `degraded` is the load-bearing field. A source configured without what it
|
||||
* needs is demoted rather than fatal (CONTRACT.md §5.1), and this is where the
|
||||
* demotion is visible to anybody who did not read the boot log — without it, a
|
||||
* misconfigured contact string looks exactly like a clear day.
|
||||
*/
|
||||
export interface HealthBody {
|
||||
ok: true;
|
||||
service: "tera-api";
|
||||
version: string;
|
||||
uptimeSeconds: number;
|
||||
sources: {
|
||||
weather: WeatherSourceId;
|
||||
flights: FlightsSourceId;
|
||||
markers: MarkersSourceId;
|
||||
};
|
||||
auth: {
|
||||
mode: AuthMode;
|
||||
/** Where a browser sends someone to sign in. `null` unless mode is `sso`. */
|
||||
entryUrl: string | null;
|
||||
};
|
||||
/** One human sentence per demotion. Empty on a fully-configured box. */
|
||||
degraded: string[];
|
||||
}
|
||||
|
||||
// ---- Flights --------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* A leg the simulator flies. Structurally identical to `SimRoute` in
|
||||
* `engine/flights.ts`; see the note at the top of this file for why it is
|
||||
* restated rather than imported.
|
||||
*/
|
||||
export interface WireSimRoute {
|
||||
callsign: string;
|
||||
from: [number, number];
|
||||
to: [number, number];
|
||||
/** Metres at the start and end of the leg. */
|
||||
fromAlt: number;
|
||||
toAlt: number;
|
||||
/** Seconds for a full traversal. */
|
||||
duration: number;
|
||||
}
|
||||
|
||||
/** One aircraft, as `engine/types.ts` `Aircraft` wants it. */
|
||||
export interface WireAircraft {
|
||||
id: string;
|
||||
lat: number;
|
||||
lng: number;
|
||||
/** Metres. The wire never carries feet, whatever the upstream feed used. */
|
||||
altitude: number;
|
||||
/** Degrees clockwise from true north. */
|
||||
heading: number;
|
||||
callsign?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* The simulated sky, sent as a *plan* rather than as positions.
|
||||
*
|
||||
* The server hands over the routes, a phase origin and a seed, and every browser
|
||||
* evaluates the same closed-form function of wall-clock time. That means one
|
||||
* cheap cacheable request instead of a poll every second, and — more usefully —
|
||||
* two people looking at the map from different machines see the same aircraft in
|
||||
* the same places, which a per-client simulation cannot promise.
|
||||
*
|
||||
* `t0` is a fixed epoch and emphatically **not** the server's start time: if it
|
||||
* moved on restart, every aircraft would teleport.
|
||||
*/
|
||||
export interface FlightsPlanBody {
|
||||
mode: "plan";
|
||||
source: "sim";
|
||||
/** Epoch milliseconds. The instant at which every route's phase is zero. */
|
||||
t0: number;
|
||||
/** Seed for the per-route phase offsets, so all viewers agree. */
|
||||
seed: number;
|
||||
routes: WireSimRoute[];
|
||||
/** How long the plan may be cached, in seconds. */
|
||||
ttlSeconds: number;
|
||||
}
|
||||
|
||||
/** Real traffic, as positions, because there is no closed form for the sky. */
|
||||
export interface FlightsLiveBody {
|
||||
mode: "live";
|
||||
source: "adsb" | "dump1090";
|
||||
/** Epoch milliseconds at which this snapshot was taken. */
|
||||
observedAt: number;
|
||||
aircraft: WireAircraft[];
|
||||
ttlSeconds: number;
|
||||
/** Attribution the consumer is expected to display, if the feed asks for it. */
|
||||
attribution?: string[];
|
||||
}
|
||||
|
||||
export type FlightsBody = FlightsPlanBody | FlightsLiveBody;
|
||||
|
||||
// ---- Weather --------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Sky conditions, reduced to what a light rig can actually use.
|
||||
*
|
||||
* Not a met report: no dew point, no pressure, no station id. `Atmosphere`
|
||||
* consumes cloud cover, precipitation and visibility and nothing else, and a
|
||||
* field that no renderer reads is a field that gets wrong without anyone
|
||||
* noticing.
|
||||
*/
|
||||
export type WeatherCondition =
|
||||
| "clear"
|
||||
| "partly-cloudy"
|
||||
| "cloudy"
|
||||
| "overcast"
|
||||
| "fog"
|
||||
| "rain"
|
||||
| "snow"
|
||||
| "thunderstorm";
|
||||
|
||||
export interface WeatherBody {
|
||||
/** ISO-8601. The observation time, not the fetch time. */
|
||||
observedAt: string;
|
||||
source: WeatherSourceId;
|
||||
/**
|
||||
* True when nobody was asked and this is the fallback clear day.
|
||||
*
|
||||
* A zero-config box serves `synthetic: true` forever and that is a supported
|
||||
* state, not an error — which is why this is a field and not a 503.
|
||||
*/
|
||||
synthetic: boolean;
|
||||
location: { lat: number; lng: number };
|
||||
/** `null` where the source did not report it. Never silently zeroed. */
|
||||
temperatureC: number | null;
|
||||
windKph: number | null;
|
||||
/** Degrees clockwise from true north, the direction the wind blows *from*. */
|
||||
windDirDeg: number | null;
|
||||
/** 0..1. */
|
||||
cloudCover: number;
|
||||
/** 0..1, an intensity rather than a rate — the renderer wants a dial. */
|
||||
precipitation: number;
|
||||
visibilityKm: number | null;
|
||||
condition: WeatherCondition;
|
||||
/** Attribution the consumer must display for this source, where one is owed. */
|
||||
attribution?: string[];
|
||||
}
|
||||
|
||||
// ---- Markers --------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Where a coordinate came from, and the only thing standing between this repo
|
||||
* and an ODbL share-alike obligation.
|
||||
*
|
||||
* Publishing a snapshot of geocoded coordinates is Public Use of a Derivative
|
||||
* Database. If those coordinates came out of Nominatim, ODbL §4.3 and §4.4
|
||||
* attach to everything served here — no matter that the rows live in a private
|
||||
* database rather than in the repo. Containment was never the discharge.
|
||||
* CONTRACT.md §8.
|
||||
*
|
||||
* The type is a plain string on purpose. The entire point of the gate is that a
|
||||
* value nobody anticipated can arrive and must be *refused at serve time*, which
|
||||
* a closed union would quietly turn into a compile error somewhere upstream
|
||||
* instead.
|
||||
*/
|
||||
export type CoordinateProvenance = KnownProvenance | (string & {});
|
||||
|
||||
/**
|
||||
* The values the public gate accepts by default.
|
||||
*
|
||||
* - `us-census` — geocoding.geo.census.gov, a US Government work in the public
|
||||
* domain. The sanctioned geocoder.
|
||||
* - `hand-placed` — typed by a human from a published address. Original.
|
||||
* - `synthetic` — invented for a demo. Owes nobody anything.
|
||||
*
|
||||
* Deliberately absent: `nominatim`, `osm`, `google`, `mapbox`, `here`. The first
|
||||
* two are share-alike; the rest restrict storing and redistributing what they
|
||||
* return, which is exactly what a public snapshot does.
|
||||
*/
|
||||
export type KnownProvenance = "us-census" | "hand-placed" | "synthetic";
|
||||
|
||||
/**
|
||||
* A marker as it crosses the wire: an engine `Marker` plus where its coordinate
|
||||
* came from.
|
||||
*
|
||||
* It extends `Marker` rather than restating it so that the adapter is a cast and
|
||||
* not a copy — the engine still knows nothing about provenance, and the day
|
||||
* `Marker` gains a field, this follows it.
|
||||
*/
|
||||
export interface WireMarker extends Marker {
|
||||
provenance: CoordinateProvenance;
|
||||
}
|
||||
|
||||
export interface MarkersBody {
|
||||
markers: WireMarker[];
|
||||
/** ISO-8601 timestamp of the snapshot these rows came from. */
|
||||
generatedAt: string;
|
||||
/**
|
||||
* Rows the public-shape gate refused, by count and reason. Served rather than
|
||||
* only logged: a silent drop looks identical to an empty database.
|
||||
*/
|
||||
refused: { reason: string; count: number }[];
|
||||
attribution?: string[];
|
||||
}
|
||||
|
||||
// ---- Offices --------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* One office pack, addressed and wrapped for delivery.
|
||||
*
|
||||
* `floor` is the authored `Office` exactly as `src/interiors/types.ts` defines
|
||||
* it — the pack a self-hoster writes by hand and the pack that arrives over HTTP
|
||||
* are the same bytes, which is the rule that keeps the format from forking.
|
||||
* Everything outside `floor` is deployment metadata the renderer never reads.
|
||||
*/
|
||||
export interface OfficeDoc {
|
||||
id: string;
|
||||
name: string;
|
||||
floor: Office;
|
||||
visibility: OfficeVisibility;
|
||||
/** ISO-8601. */
|
||||
updated?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* `private` means the endpoint answers 404 to anyone who may not see it — see
|
||||
* `ErrorBody`. `unlisted` is served to anybody with the id but never appears in
|
||||
* an index and is never publicly cached.
|
||||
*/
|
||||
export type OfficeVisibility = "public" | "unlisted" | "private";
|
||||
Reference in New Issue
Block a user