1
0

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>
This commit is contained in:
2026-08-07 04:18:00 -07:00
parent 51979feea0
commit 2d87d9f354
14 changed files with 2304 additions and 72 deletions
+327
View File
@@ -442,3 +442,330 @@ export const SAMPLE_PRESENCE_PALETTE: Record<string, number> = {
meeting: 0xc4796f,
guest: 0x7fb886,
};
// ---- The office, over a day ------------------------------------------------
/**
* The fabricated roster as it stands at one particular instant.
*
* `SAMPLE_PRESENCE` is a photograph of one good morning, and a photograph was
* the whole truth for as long as the office had no clock. It is not any more.
* The room is lit by the same sun the city is, the house lights come up as that
* sun goes down, and the robots go on walking the floor after dark — so a
* building with all twenty-five of these people still sitting in it at one in
* the morning is now the least believable thing in it. Everything else in the
* scene has learned what time it is; the people had not.
*
* This is deliberately a **pure function of an instant the caller supplies**,
* not a ticking source. The app already has exactly one clock — the wall clock,
* or whatever the godmode scrubber has overridden it with — and a module that
* read `new Date()` for itself would be a second one, correct until the moment
* somebody drags the scrubber to midnight and finds the desks still full. That
* is the bug being fixed here, so it is not one to reintroduce one layer down.
*
* The result is always a **subset of `SAMPLE_PRESENCE`**, which is where the two
* sharpest constraints go away for free: one entry per seat in, one entry per
* seat or fewer out, so the roster can never name more people than there are
* seats, and nobody is ever moved to a seat they do not sit at. `SAMPLE_PRESENCE`
* itself is untouched and still exported — it is what a caller with no clock,
* and every test written before this existed, is entitled to keep getting.
*
* ### No weekend, on purpose
*
* A Sunday is not modelled and the day of the week is never read. This is a
* judgement call and it is the one worth arguing with: a real building is nearly
* empty all weekend, and modelling that would be more honest than not.
*
* It is also, roughly two days in seven, a first impression of an empty floor
* for somebody who has just walked into the office for the first time — and this
* file's own header is already clear that an empty building teaches nobody what
* the office is for. An empty room at one in the morning is read as "it is one
* in the morning". An empty room at eleven on a Sunday, by a visitor who has not
* thought about what day it is, is read as a floor that failed to load. The
* overnight curve alone buys the thing that was actually missing — a building
* that visibly lives on a clock — and it does it at every hour of every day
* instead of costing us two of them. A deployment that wants weekends can filter
* this result by `when.getDay()` in four lines; a demo that has already been
* dismissed cannot be got back.
*
* ### Which clock
*
* `when.getHours()` — the viewer's local time, matching `#clock`, the godmode
* scrubber and every other reading of a `Date` in this app. That is right for
* the flagship case and imperfect for one that exists: both shipped buildings
* stand in San Francisco, but the sun in the windows is computed from the site's
* latitude and longitude while the roster here follows the viewer, so a visitor
* in another time zone gets a floor that fills up at their nine o'clock under a
* sky that belongs to the building's. Fixing that properly needs a real time
* zone on `OfficeSite`, which is a change to the office contract and not to a
* demo adapter. Guessing one from `site.lng` is *not* the fix and was rejected:
* a longitude gives you mean solar time, and it knows nothing about daylight
* saving. The two shipped sites sit at -122.40° and -122.32°, seven kilometres
* apart, which is about 8 h 10 m of solar offset — ten minutes adrift of Pacific
* Standard Time, and seventy minutes adrift of the clock for the two thirds of
* the year that are daylight time. That is wrong in the way that looks right.
* Until there is a real zone, a caller that genuinely knows the building's can
* shift the `Date` it passes.
*/
export function samplePresenceAt(when: Date): Presence[] {
const t = when.getHours() * 60 + when.getMinutes() + when.getSeconds() / 60;
return SAMPLE_PRESENCE.filter((person) => isPresentAt(person, t));
}
/**
* Minutes since local midnight, as a literal. Every window in this section is
* written with it so that the source reads as a timetable.
*/
function at(hour: number, minute: number): number {
return hour * 60 + minute;
}
/**
* The three shifts that are *not* seeded, and the reason the building is never
* empty during working hours.
*
* Everyone else's day is drawn from a hash, and a hash makes no promises: there
* is a seed for which every single person arrives at ten and the floor is bare
* at half past nine. Rather than clamp the answer afterwards — a clamp is a
* decision that changes as the clock moves, which is exactly the popping this
* function is supposed to be free of — three people are given fixed hours and
* the guarantee is read off them.
*
* They overlap in a deliberate chain: the early ops shift is in before anyone
* and out mid-afternoon, reception opens the front desk before it hands over,
* and one engineer works late and locks up. Each one starts before the previous
* one leaves, so their union is **continuous from 06:15 to 22:40**, and the
* floor cannot be empty inside it. Outside it the building genuinely does empty,
* which is the whole point of the change: at two in the morning there is nobody
* here, and the house lights and the robots are what the night shift looks like.
*
* Anchors take **no lunch break**, unlike everybody else. A break in an anchor
* is a break in the guarantee, and three staggered shifts that all happen to
* step out at half past twelve is precisely the accident that would put an empty
* floor on screen at the one hour nobody would think to check.
*
* The keys are seat ids and they must exist in `SAMPLE_PRESENCE`; see the check
* below, which is there because renaming a seat would otherwise dissolve the
* guarantee without a word.
*/
const PRESENCE_ANCHORS: Record<string, { from: number; to: number }> = {
"ops-01": { from: at(6, 15), to: at(15, 5) },
"reception-01": { from: at(7, 10), to: at(18, 50) },
"eng-11": { from: at(11, 20), to: at(22, 40) },
};
/**
* Two bookings in Alcatraz, and nothing else all day.
*
* A meeting room whose chairs are full at eight in the evening is the same lie
* as a floor whose desks are, one room in — worse, really, because a meeting is
* the one thing in an office that everybody knows has a start and an end.
*/
const PRESENCE_SESSIONS: { from: number; to: number; attendance: number }[] = [
{ from: at(9, 40), to: at(10, 30), attendance: 0.8 },
{ from: at(14, 5), to: at(15, 15), attendance: 0.6 },
];
/**
* Whoever booked the room is in it.
*
* Without this a session can come round and, on this seed or the next one, draw
* nobody — and a lit meeting room with an empty table for fifty minutes reads as
* the seat binding having failed rather than as a meeting that was cancelled.
* The other five chairs are left to the hash, which is what makes the room look
* like a meeting rather than like a roll call.
*/
const PRESENCE_SESSION_CHAIR = "alcatraz-01";
/**
* When the visitor is in the building.
*
* Two short windows, because the lobby seat is the one place on this floor where
* a permanent occupant is obviously wrong: somebody who has been waiting in
* reception since dawn is not a guest, they are furniture.
*/
const PRESENCE_VISITS: { from: number; to: number }[] = [
{ from: at(10, 5), to: at(10, 50) },
{ from: at(15, 20), to: at(15, 55) },
];
/**
* Is this person in the building at `t` minutes past local midnight?
*
* Four models, chosen by what the roster already says about the person rather
* than by a second table of seat ids that would drift out of step with the
* first. `colorKey` is opaque to the engine and to `presence.ts` — that is the
* contract and it is not being bent here — but this module *defines* those keys,
* a few lines up in `SAMPLE_PRESENCE_PALETTE`, so it is the one place in the
* repo entitled to know that `"meeting"` means somebody is in a meeting.
*/
function isPresentAt(person: Presence, t: number): boolean {
const anchor = PRESENCE_ANCHORS[person.seatId];
if (anchor) return t >= anchor.from && t < anchor.to;
if (person.colorKey === "guest") {
return PRESENCE_VISITS.some((visit) => t >= visit.from && t < visit.to);
}
if (person.colorKey === "meeting") {
return PRESENCE_SESSIONS.some(
(session, index) =>
t >= session.from &&
t < session.to &&
(person.seatId === PRESENCE_SESSION_CHAIR ||
unit(person.seatId, `session-${index}`) < session.attendance),
);
}
const day = deskDay(person.seatId);
if (t < day.arrive || t >= day.leave) return false;
// Out at lunch, for those who go. Deliberately checked after the arrive/leave
// pair rather than folded into it, because these are two different facts about
// a person and a single boolean expression covering both is the one that grows
// an off-by-one the first time somebody edits it.
return !(t >= day.lunchFrom && t < day.lunchTo);
}
/** One desk worker's day, in minutes since local midnight. */
interface DeskDay {
arrive: number;
leave: number;
lunchFrom: number;
lunchTo: number;
}
/**
* Memoised because a seat's day never changes. `samplePresenceAt` may be called
* on every clock tick and on every frame of a scrub, and re-hashing six streams
* per person per call to get an answer that is by construction the same answer
* is work nobody asked for.
*/
const deskDays = new Map<string, DeskDay>();
/**
* A believable working day for one seat, from the seat id and nothing else.
*
* ### Why a timetable per person and not an occupancy curve
*
* The obvious shape for "the office fills up and empties out" is a curve — a
* fraction of the floor that is in at time `t` — with each person holding a
* fixed threshold and being in whenever the curve is above it. It was tried and
* it is wrong here, for one reason: **the curve is not monotone**. It has to dip
* at lunch, and everybody whose threshold sits near the bottom of that dip
* blinks out and back in as the curve goes down and up again. Every wobble in
* the curve costs a pop for whoever is parked at that level, and the pop lands
* on the people nearest the middle of the distribution — the ones most likely to
* be on screen. A curve with any noise in it at all strobes.
*
* A per-person timetable has no such failure mode. Each person's day is four
* fixed instants, so the in/out predicate crosses at most four times in
* twenty-four hours and each crossing goes one way. Advancing the clock by a
* second can only change the answer for somebody whose boundary falls inside
* that second, and it changes it once. Scrubbing backwards is symmetric for the
* same reason.
*
* Note what this is *instead of* hysteresis. Hysteresis needs memory of the last
* answer, and this function must give the same answer on a fresh reload as it
* gave the frame before — a page that has just booted has no last answer to
* remember. So rather than damping a boundary that moves, the boundary is made
* not to move at all, which buys the same freedom from chatter without any
* state to get out of sync between two callers. Both the scene and the plan view
* are handed this list; if it depended on history they could disagree.
*
* ### The numbers
*
* Arrivals are `08:45` ± 75 min and departures `17:50` ± 95 min, each drawn
* triangular rather than uniform so the floor fills through a busy middle with
* thin tails instead of a flat trickle — which is what a morning actually looks
* like from a desk. Departure is then pulled by 0.6 of how far the arrival was
* from the mean, because the person who is in at half seven is the person who
* leaves at four, and independent draws produce a floor full of people working
* seven-to-four and ten-to-eight at the same time. Those coefficients put
* arrivals in `07:30``10:00`, departures in `15:30``20:10` at the extremes,
* and the day itself between 7 h and 11 h 10 — the extremes of both draws at
* once, which is rare by construction.
*
* Lunch is `12:20` ± 40 min, taken away from the desk by the roughly two thirds
* who draw under the threshold, and it lasts 24 to 52 minutes. The shortest of
* those is still twenty-four minutes of world time, which is long enough to read
* as somebody having gone to eat rather than as a figure that glitched.
*
* Every window lies inside one calendar day and none of them wraps midnight.
* That is a constraint worth keeping: an interval that wraps needs `from > to`
* handling in `isPresentAt`, and the first person to write one without it gets a
* night owl who is never in rather than always.
*
* Seeded from the **seat** id and not the person id, because the seat is the
* address — it is what a `Presence` binds to and what survives an edit to this
* file. Renaming Tobias Quillon should not change when the person at `eng-01`
* comes in.
*/
function deskDay(seatId: string): DeskDay {
const hit = deskDays.get(seatId);
if (hit) return hit;
const arrive = at(8, 45) + 75 * triangular(seatId, "arrive");
const leave = at(17, 50) + 95 * triangular(seatId, "leave") + 0.6 * (arrive - at(8, 45));
const lunchFrom = at(12, 20) + 40 * triangular(seatId, "lunch");
const lunchLength = unit(seatId, "lunch-length") < 0.65 ? 38 + 14 * triangular(seatId, "bite") : 0;
const made: DeskDay = { arrive, leave, lunchFrom, lunchTo: lunchFrom + lunchLength };
deskDays.set(seatId, made);
return made;
}
/**
* A stable number in `[0, 1)` from a seat id and a named stream.
*
* FNV-1a, which is here because it is eight lines and has no dependencies, not
* because its statistical properties matter — nothing downstream of this is a
* simulation, and the requirement is only that the same seat gets the same
* answer on every reload, in every browser, forever. `Math.imul` keeps the
* multiply in 32 bits; a plain `*` would go through a double and lose the low
* bits that are the whole output.
*
* The stream name is what makes the draws independent. Hashing the seat id once
* and slicing the bits would be cheaper and would tie a seat's arrival time to
* its lunch hour in a way that eventually shows up as everybody who comes in
* early eating at the same moment.
*/
function unit(seatId: string, stream: string): number {
const text = `${seatId}/${stream}`;
let h = 0x811c9dc5;
for (let i = 0; i < text.length; i += 1) {
h ^= text.charCodeAt(i);
h = Math.imul(h, 0x01000193);
}
return (h >>> 0) / 0x1_0000_0000;
}
/**
* A stable number in `(-1, 1)`, peaked at zero: two uniforms added, which is the
* cheapest thing that is not flat.
*
* Uniform arrival times give a floor that fills at a constant rate from half
* seven to ten, and it reads as wrong without being able to say why — there is
* no rush hour in it. One extra hash buys a peak.
*/
function triangular(seatId: string, stream: string): number {
return unit(seatId, `${stream}-a`) + unit(seatId, `${stream}-b`) - 1;
}
/**
* The anchors have to be anchored to something.
*
* A seat id in `PRESENCE_ANCHORS` that is not in `SAMPLE_PRESENCE` matches
* nobody, and the guarantee above quietly becomes a hope — the failure being an
* empty office at eleven in the morning on some seeds and not others, which is
* about the worst shape a bug can have. Twenty-five names against three ids, at
* import, is not a cost worth measuring, and it turns that into a line in the
* console the first time somebody renames a seat.
*/
for (const seatId of Object.keys(PRESENCE_ANCHORS)) {
if (!SAMPLE_PRESENCE.some((person) => person.seatId === seatId)) {
console.warn(
`[tera/sample] anchor seat "${seatId}" is not in SAMPLE_PRESENCE; ` +
"the sample office can now be empty during working hours",
);
}
}