1
0
This repository has been archived on 2026-08-25. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
tera/src/adapters/sample.ts
T

1392 lines
60 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* Fabricated demo data, so that a clone of this repo has something on it.
*
* **Everything in this file is invented.** The companies do not exist, have
* never existed, and are named the way they are — Wobbegong, Nonsuch, Pennyfarthing
* — specifically so that nobody can mistake one for a real business. The
* coordinates were typed by hand from a general sense of where San Francisco's
* neighbourhoods are; none of them is anybody's address, and none of them came
* out of a geocoder.
*
* That last point is the licence rule and not a stylistic preference. No real
* company data ships in this repo, for two reasons that happen to want the same
* thing. The privacy one is obvious: pipeline status — who is talking to whom,
* who said no — is private, and a public repo is the wrong place for it. The
* licence one is sharper and is the subject of ARCHITECTURE.md §3.2 and
* CONTRACT.md §8: real positions are *geocoded*, and a geocoder built on
* OpenStreetMap returns ODbL data. Serving a snapshot of those coordinates is
* Publicly Using a Derivative Database, which drags share-alike onto everything
* served alongside it, whether or not the rows live in the repo. So real markers
* arrive at runtime over the API — see `http.ts` — carrying a provenance field
* the server checks, and the repo itself ships this: fiction, which owes nobody
* anything.
*
* The demo is worth having anyway. A map with no pins on it teaches nobody what
* the thing is for, and half the interesting behaviour in `markers.ts` — the
* ghost treatment for a marker that has no real position yet — is invisible
* without data that exercises it.
*/
import { regionOf, syntheticRoutes, type SimRoute } from "../engine/flights.ts";
import type { Presence } from "../interiors/types.ts";
import type { City, Marker, MarkerPalette } from "../engine/types.ts";
/**
* A small pipeline, as colours.
*
* Five states is about the fewest that still shows why `colorKey` is opaque to
* the engine: none of these words means anything to `markers.ts`, which looks
* the key up here and draws whatever it finds. A public sector map would supply
* an entirely different set against the same renderer.
*/
export const SAMPLE_PALETTE: MarkerPalette = {
watching: 0x7f8b99,
applied: 0x4f9cf2,
talking: 0x3fbf9a,
offer: 0xf2b134,
closed: 0xd2544f,
};
/**
* Where the unplaced markers float: out over the bay, east of the Ferry
* Building, in a short arc.
*
* A marker with `located: false` has no address yet, and the honest thing to do
* with a position you do not have is to not pretend you have one. Open water is
* the clearest way to say that on a map — nothing is there, the pin visibly
* stands on nothing, and it cannot be misread as a building. `markers.ts` gives
* these a different silhouette and a lower opacity as well, so the tell does not
* rest on placement alone.
*/
const UNPLACED_LNG = -122.3665;
/**
* Twenty-two invented companies across San Francisco.
*
* Placed by neighbourhood rather than by street: Hayes Valley and SoMa are
* crowded because that is the fact the map is usually drawing attention to, the
* Bayview and the Richmond have one each, and three have no position at all.
* The clustering is the point — a marker layer that only ever gets evenly
* scattered test data hides every overlap problem it has.
*/
export const SAMPLE_MARKERS: Marker[] = [
// Hayes Valley and the bowl below Buena Vista.
{
id: "sample-thimbleway",
label: "Thimbleway Systems",
colorKey: "talking",
lat: 37.7768,
lng: -122.4243,
located: true,
blurb: "Invented. Model-serving, allegedly, in a Victorian with bad wiring.",
},
{
id: "sample-nonsuch",
label: "Nonsuch Cartography Co.",
colorKey: "applied",
lat: 37.7752,
lng: -122.4262,
located: true,
blurb: "Invented. Maps of places that are not there.",
},
{
id: "sample-marmalade",
label: "Marmalade Interchange",
colorKey: "watching",
lat: 37.7781,
lng: -122.4218,
located: true,
blurb: "Invented. Moves data between two formats nobody uses.",
},
// SoMa, where the grid turns forty-six degrees.
{
id: "sample-kettle-anvil",
label: "Kettle & Anvil Compute",
colorKey: "offer",
lat: 37.7805,
lng: -122.4051,
located: true,
blurb: "Invented. Sells the shovels, or claims to.",
},
{
id: "sample-ninth-pelican",
label: "Ninth Pelican Labs",
colorKey: "applied",
lat: 37.7784,
lng: -122.4009,
located: true,
blurb: "Invented. There were never eight others.",
},
{
id: "sample-brassbound",
label: "Brassbound Telemetry",
colorKey: "closed",
lat: 37.7822,
lng: -122.4074,
located: true,
blurb: "Invented. Went quiet after the second call.",
},
// The Financial District and Jackson Square.
{
id: "sample-grimsby-doone",
label: "Grimsby & Doone Photonics",
colorKey: "watching",
lat: 37.7941,
lng: -122.4008,
located: true,
blurb: "Invented. Two surnames and a laser.",
},
{
id: "sample-tugboat",
label: "Tugboat Actuarial",
colorKey: "applied",
lat: 37.7958,
lng: -122.4032,
located: true,
blurb: "Invented. Insurance for things that have already happened.",
},
// Mission Bay: landfill, then biotech.
{
id: "sample-fogbank",
label: "Fogbank Freight",
colorKey: "talking",
lat: 37.7709,
lng: -122.3918,
located: true,
blurb: "Invented. Logistics, in a building younger than most of the staff.",
},
{
id: "sample-bittern",
label: "Bittern & Sons Biologics",
colorKey: "watching",
lat: 37.7688,
lng: -122.3894,
located: true,
blurb: "Invented. No sons.",
},
// The Mission, flat and sunny.
{
id: "sample-unlikely-weather",
label: "Bureau of Unlikely Weather",
colorKey: "offer",
lat: 37.7602,
lng: -122.4151,
located: true,
blurb: "Invented. Forecasts nobody asked for.",
},
{
id: "sample-perpetual-bagel",
label: "Perpetual Bagel Works",
colorKey: "closed",
lat: 37.7574,
lng: -122.4192,
located: true,
blurb: "Invented. The name was the whole pitch.",
},
// Potrero Hill and Dogpatch, the old industrial edge.
{
id: "sample-wobbegong",
label: "Wobbegong Robotics",
colorKey: "talking",
lat: 37.7589,
lng: -122.4002,
located: true,
blurb: "Invented. Named after a carpet shark, for reasons never explained.",
},
{
id: "sample-sourdough-semi",
label: "Sourdough Semiconductor",
colorKey: "applied",
lat: 37.7597,
lng: -122.3881,
located: true,
blurb: "Invented. A fab in a city with no fabs.",
},
{
id: "sample-pennyfarthing",
label: "Pennyfarthing Power",
colorKey: "watching",
lat: 37.7564,
lng: -122.3973,
located: true,
blurb: "Invented. Batteries, uphill.",
},
// The north side and the hills.
{
id: "sample-lamplighter",
label: "Lamplighter Aerostatics",
colorKey: "watching",
lat: 37.8004,
lng: -122.4086,
located: true,
blurb: "Invented. Airships, which are always about to come back.",
},
{
id: "sample-halfpenny",
label: "Halfpenny Optics",
colorKey: "applied",
lat: 37.7929,
lng: -122.4147,
located: true,
blurb: "Invented. Lenses, four hundred feet above the water they look at.",
},
// One each in the parts of the city the map usually forgets.
{
id: "sample-cormorant",
label: "Cormorant Freight Systems",
colorKey: "talking",
lat: 37.7357,
lng: -122.3908,
located: true,
blurb: "Invented. The only pin south of Islais Creek, which is the point.",
},
{
id: "sample-tidewrack",
label: "Tidewrack Instruments",
colorKey: "watching",
lat: 37.7802,
lng: -122.4638,
located: true,
blurb: "Invented. Sensors, in the fog, on purpose.",
},
// Three with no position yet. See `UNPLACED_LNG`.
{
id: "sample-quibble",
label: "Quibble Quantum",
colorKey: "applied",
lat: 37.7965,
lng: UNPLACED_LNG,
located: false,
blurb: "Invented, and unplaced: no address on file, so the map does not invent one.",
},
{
id: "sample-antelope-foundry",
label: "Antelope Foundry",
colorKey: "watching",
lat: 37.7905,
lng: UNPLACED_LNG,
located: false,
blurb: "Invented, and unplaced.",
},
{
id: "sample-mudlark",
label: "Mudlark Instruments",
colorKey: "closed",
lat: 37.7845,
lng: UNPLACED_LNG,
located: false,
blurb: "Invented, and unplaced.",
},
];
/**
* Sample traffic over the Bay Area, for when the API is not there to send a
* flight plan.
*
* The corridors are roughly the real ones — arrivals down the peninsula from
* the north, departures turning out over the Pacific, a slow light aircraft
* crossing the bay — because that is what makes the sky read as this city's sky
* rather than as random motion. The callsigns are not: no real operator uses
* these prefixes, which keeps a demo from looking like a feed of actual
* traffic. Nothing here is observed, and `flights.ts` explains at length why
* this project ships a simulator instead of a client for somebody's live data.
*
* The unqualified name is a leftover and is kept because the app imports it.
* Every leg in it is over San Francisco, which is only the right answer for one
* of the two cities in this build; `sampleRoutesFor` is the entry point that
* knows the difference.
*/
export const SAMPLE_ROUTES: SimRoute[] = [
{ callsign: "NIMBUS 4", from: [37.95, -122.36], to: [37.66, -122.4], fromAlt: 2400, toAlt: 500, duration: 190 },
{ callsign: "NIMBUS 17", from: [37.93, -122.31], to: [37.65, -122.38], fromAlt: 2100, toAlt: 450, duration: 210 },
{ callsign: "PELICAN 2", from: [37.64, -122.39], to: [37.9, -122.62], fromAlt: 700, toAlt: 5200, duration: 165 },
{ callsign: "PELICAN 31", from: [37.7, -122.21], to: [37.88, -122.55], fromAlt: 1800, toAlt: 6100, duration: 230 },
{ callsign: "CORMORANT 8", from: [37.62, -122.6], to: [37.95, -122.28], fromAlt: 6800, toAlt: 8200, duration: 260 },
{ callsign: "KESTREL 5", from: [37.83, -122.56], to: [37.7, -122.22], fromAlt: 1100, toAlt: 1300, duration: 300 },
{ callsign: "NIMBUS 40", from: [37.96, -122.48], to: [37.63, -122.36], fromAlt: 3100, toAlt: 600, duration: 205 },
];
/**
* Sample traffic over the Los Angeles basin.
*
* The same idea as `SAMPLE_ROUTES` and it exists because that one was being
* flown over both cities: the SoCal board would come up with a sky whose every
* aircraft was nearly six hundred kilometres north of it, off the edge of the world
* and therefore invisible. A city that renders an empty sky looks like a city
* whose flight layer failed.
*
* Again the geography is roughly right and the callsigns are invented. LAX runs
* west almost always, so the arrivals here come in from the east over the
* basin and the departures go out over the water before turning; Burbank sits
* up the valley behind the hills, and the light aircraft is following the coast
* because in this basin that is what they do.
*/
export const SAMPLE_SOCAL_ROUTES: SimRoute[] = [
{ callsign: "CONDOR 6", from: [34.02, -117.45], to: [33.945, -118.36], fromAlt: 3500, toAlt: 400, duration: 330 },
{ callsign: "CONDOR 21", from: [33.99, -117.29], to: [33.94, -118.33], fromAlt: 4100, toAlt: 450, duration: 360 },
{ callsign: "AVOCET 12", from: [33.945, -118.42], to: [34.15, -118.84], fromAlt: 500, toAlt: 5200, duration: 190 },
{ callsign: "AVOCET 30", from: [33.68, -117.87], to: [33.34, -118.4], fromAlt: 600, toAlt: 4800, duration: 210 },
{ callsign: "CURLEW 3", from: [33.36, -117.3], to: [33.69, -117.86], fromAlt: 4200, toAlt: 500, duration: 235 },
{ callsign: "TOWHEE 9", from: [34.33, -118.82], to: [34.2, -118.36], fromAlt: 3000, toAlt: 450, duration: 175 },
{ callsign: "CONDOR 44", from: [34.36, -118.66], to: [33.32, -117.28], fromAlt: 9200, toAlt: 9800, duration: 300 },
{ callsign: "SANDPIPER 4", from: [33.6, -118.02], to: [34.03, -118.62], fromAlt: 900, toAlt: 1100, duration: 330 },
];
/**
* The right sample sky for a city, and something defensible for a city nobody
* has drawn one for.
*
* Keyed on `city.id` rather than on position because the hand-placed corridors
* are the whole value here: knowing that arrivals come down the peninsula and
* that LAX departs to the west is knowledge about two named places, and there
* is no way to derive it from a bounding box. What *can* be derived is legs
* that are at least in the right region, which is what `syntheticRoutes` does
* and what any third city gets until somebody sits down with a chart.
*/
export function sampleRoutesFor(city: Pick<City, "id" | "center" | "bounds">): SimRoute[] {
if (city.id === "sf") return SAMPLE_ROUTES;
if (city.id === "socal") return SAMPLE_SOCAL_ROUTES;
return syntheticRoutes(regionOf(city));
}
// ---- The office ------------------------------------------------------------
/**
* A fabricated roster for the reference office, so that "sign in" has something
* to show in a clone with no server behind it.
*
* **Every person here is invented**, by the same rule and for the same reason as
* the companies above: the names are deliberately unmistakable for anyone's
* — Quillon, Marchetti, Oyelaran-Pike — and none of them works anywhere. Real
* occupancy is the most private thing this project touches. It says who was at
* their desk and, by omission, who was not, which is an attendance record; it
* arrives at runtime over an authenticated endpoint (`routes/presence.ts`) and
* it never, ever lands in this repo.
*
* The demo is worth having for the same reason the markers are. An empty
* building teaches nobody what the office is *for*, and the entire presence
* layer — the figures, the seat binding, the hover card that names somebody —
* is invisible without data that exercises it. It also makes the tier honest
* locally: `access.ts` hands a clone `member`, and a member who is shown the
* same empty room as a stranger has been told the tier means something when it
* does not.
*
* The seat ids are real ids from `offices/lumbridge-hq.ts` and have to be: a
* presence whose seat is not in the plan is dropped by `presence.ts`, silently
* and correctly, so a typo here is an invisible absence rather than an error.
* They are also spread deliberately — two meeting rooms mid-session, a full
* bench, a half-empty one, three focus booths of which one is taken — because a
* floor where every desk is occupied reads as a texture rather than as people.
*/
export const SAMPLE_PRESENCE: Presence[] = [
// Reception, and someone waiting.
{ id: "p-01", seatId: "reception-01", label: "Wren Abaddon", colorKey: "in" },
{ id: "p-02", seatId: "lobby-01", label: "Visitor", colorKey: "guest" },
// The eng bench, full along the window row and thinning behind it.
{ id: "p-03", seatId: "eng-01", label: "Tobias Quillon", colorKey: "in" },
{ id: "p-04", seatId: "eng-02", label: "Ines Marchetti", colorKey: "in" },
{ id: "p-05", seatId: "eng-03", label: "Dara Oyelaran-Pike", colorKey: "in" },
{ id: "p-06", seatId: "eng-04", label: "Sunniva Holt", colorKey: "in" },
{ id: "p-07", seatId: "eng-05", label: "Casimir Vane", colorKey: "focus" },
{ id: "p-08", seatId: "eng-06", label: "Perpetua Nkemelu", colorKey: "in" },
{ id: "p-09", seatId: "eng-08", label: "Rafferty Osgood", colorKey: "in" },
{ id: "p-10", seatId: "eng-11", label: "Marisol Thibault", colorKey: "focus" },
// Ops, about half in.
{ id: "p-11", seatId: "ops-01", label: "Anouk Sterling", colorKey: "in" },
{ id: "p-12", seatId: "ops-02", label: "Emeka Farrow", colorKey: "in" },
{ id: "p-13", seatId: "ops-05", label: "Bettina Kovač", colorKey: "in" },
{ id: "p-14", seatId: "ops-07", label: "Xiomara Belfry", colorKey: "focus" },
{ id: "p-15", seatId: "ops-10", label: "Aurelio Pinsent", colorKey: "in" },
// Design.
{ id: "p-16", seatId: "design-01", label: "Halcyon Reeve", colorKey: "in" },
{ id: "p-17", seatId: "design-03", label: "Zephyrine Mbeki", colorKey: "in" },
{ id: "p-18", seatId: "design-04", label: "Ptolemy Sandoval", colorKey: "in" },
// One focus booth of three, which is the honest ratio.
{ id: "p-19", seatId: "booth-02", label: "Ottoline Grieve", colorKey: "focus" },
// Alcatraz, mid-session — six of ten, which is what a real meeting looks like.
{ id: "p-20", seatId: "alcatraz-01", label: "Cormac Dellwood", colorKey: "meeting" },
{ id: "p-21", seatId: "alcatraz-02", label: "Solveig Amankwah", colorKey: "meeting" },
{ id: "p-22", seatId: "alcatraz-03", label: "Ignatius Pell", colorKey: "meeting" },
{ id: "p-23", seatId: "alcatraz-06", label: "Rosalind Achebe", colorKey: "meeting" },
{ id: "p-24", seatId: "alcatraz-07", label: "Vasco Underhill", colorKey: "meeting" },
{ id: "p-25", seatId: "alcatraz-08", label: "Clementine Roux", colorKey: "meeting" },
];
/**
* `colorKey` -> colour, for the fabricated roster.
*
* Four states and no more, because the palette is the legend whether or not one
* is drawn: a viewer can tell "at their desk" from "heads-down" from "in a
* meeting" at a glance, and a fifth shade would be a distinction nobody could
* name. Amber for focus is the interface's own accent doing what it does
* everywhere else in this app — marking the thing you are meant to notice —
* and the guest green is the only hue that is not in the office's own materials,
* because a visitor is the one person in the building who is not part of it.
*/
export const SAMPLE_PRESENCE_PALETTE: Record<string, number> = {
in: 0x6f9ec4,
focus: 0xf2b134,
meeting: 0xc4796f,
guest: 0x7fb886,
};
// ---- The office, over a day ------------------------------------------------
/**
* The fabricated roster as it stands at one particular instant: who is in the
* building, and **which seat each of them is in right now**.
*
* `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 was the least believable thing in it. Everything else in the scene
* had learned what time it was; the people had not.
*
* The first version of this function fixed half of that: it thinned the roster
* by hour, so the floor filled and emptied. The other half is that everybody it
* kept was pinned to one seat from dawn to dusk, and a floor where the same
* twenty-five silhouettes sit in the same twenty-five chairs for eleven hours is
* a texture rather than a place. A real floor has people in the meeting rooms
* when there is a meeting, at the kitchen island at lunch, and in the lounge in
* the afternoon. So this now answers a harder question than "are you in": it
* answers "where are you", and a `Presence` binds to a `seatId`, so the whole
* feature is **choosing seat ids**. Nothing here invents a position; it could
* not if it wanted to, which is the point of the seat-id split and is argued at
* length on `Presence` in `interiors/types.ts`.
*
* 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 this exists to fix, so it is not one to reintroduce one layer down.
*
* ### What replaced "always a subset of SAMPLE_PRESENCE"
*
* It used to be one. Returning a filtered `SAMPLE_PRESENCE` bought two hard
* properties for nothing — never more people than seats, and never two people in
* one seat — because the input already had one person per seat and filtering
* cannot create a collision. **That is gone**, and it has to be: a person who is
* at their desk at eleven and in Alcatraz at ten is by definition not always at
* the seat the photograph gives them. The result is still `Presence[]` and the
* exported signature has not moved, but the objects in it now carry seat ids
* that are nowhere in `SAMPLE_PRESENCE`, and a `colorKey` that can differ from
* the one there.
*
* The two properties are bought back a different way, and it is worth being
* precise because the second one is the easy bug here:
*
* - **One person, one seat.** The loop below emits at most one `Presence` per
* roster entry, ever. A person is either inside exactly one booking or at
* their own desk, never both, and the `bookings` list for one person is
* audited at import for self-overlap. Twenty-five people therefore produce at
* most twenty-five entries, at twenty-five distinct seats — so the roster can
* never name more people than the floor can hold.
* - **One seat, one person.** This is not free and is not derivable from the
* per-person view, because it is a statement about *pairs* of people. It comes
* from `BOOKINGS` being a hand-written table rather than a hash: within one
* booking the room seats are the object's keys, so a repeat is a **compile
* error** (TS1117), and across bookings that are live at the same moment the
* import-time audit at the bottom of this file checks every pair. A booking
* may also never name a seat that somebody sits at all day; that is checked
* too, since sending a visitor to `eng-03` would put two people in a chair
* without either booking overlapping the other.
*
* `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.
*
* ### What it costs
*
* Called on every clock tick — `updateSun` in `main.ts` runs on a once-a-minute wall-clock interval, and on every godmode scrub
* — and on every frame of a godmode scrub, so it is worth stating rather than
* hoping.
*
* Per call: one pass over the twenty-five-entry roster; for each person a walk
* of their own bookings which stops at the first one starting after `t`; and for
* whoever is not in a booking, four number comparisons against a day that was
* computed once. Summed over the roster the booking walk is bounded by the whole
* table — fifty seat assignments across fifteen bookings, nobody holding more
* than four — so the worst case is a couple of hundred number comparisons.
* **Nothing is allocated except the result array**: every `Presence` this can
* return, at a desk or in a room, is built once at import and handed out by
* reference, exactly as the old filter handed out `SAMPLE_PRESENCE`'s own
* objects. There is no hashing on this path at all; the hashes run once per seat
* while `ROSTER` is built.
*
* Import-time work is the roster build and the audit, together a couple of
* thousand comparisons over those fifty entries, once, and neither of them on
* any frame.
*
* ### 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: 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. 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;
const out: Presence[] = [];
for (const person of ROSTER) {
// A booking wins outright. Somebody in a meeting is not also at their desk,
// and the order of these two branches is the entire "one person, one seat"
// guarantee for a single roster entry.
const booked = bookedAt(person, t);
if (booked) {
out.push(booked);
continue;
}
if (person.desk && isIn(person.desk.day, t)) out.push(person.desk.at);
}
return out;
}
/**
* Office-aware fallback for shipped environments.
*
* The legacy roster above is valuable as a renderer fixture, but its seat ids
* describe the original reference tower and therefore cannot truthfully appear
* in either active HQ. A missing presence service now means what it says: no
* fabricated people. Keeping this resolver keyed by the stable office id gives
* self-hosters and future sample packs an explicit seam without ever treating a
* shiny room as evidence of employees.
*/
export function samplePresenceForOfficeAt(_officeId: string, _when: Date): Presence[] {
return [];
}
/**
* 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;
}
/**
* Where the six people the photograph catches mid-meeting actually sit.
*
* `SAMPLE_PRESENCE` is a snapshot at one moment, so for most of the roster the
* seat it gives somebody *is* their desk and this table has nothing to say. For
* the six in Alcatraz it is not: the snapshot caught them in a room they were
* booked into, and a model of a whole day needs to know where they go when the
* meeting ends. "Somebody in Alcatraz at 10:00 is at their desk at 11:00" is the
* requirement, and this is the line that says which desk.
*
* The alternative was to move them to desks in `SAMPLE_PRESENCE` itself and let
* the schedule put them back in the room. That was rejected because the snapshot
* is a deliberately varied fixture — its own comment lists the spread as the
* reason it exists — and a `SAMPLE_PRESENCE` in which everybody sits at a desk
* exercises exactly one kind of seat. Six lines here cost less than that.
*
* Keyed on the snapshot's seat rather than on the person id for the same reason
* `deskDay` is seeded from the seat: the seat is the address, and renaming
* Cormac Dellwood should not move anybody's desk. The desks chosen are free ones
* — nobody else in the roster sits at a `sales-*` station, at `design-02`,
* `ops-04` or `eng-07` — which the audit below rechecks, because two people
* sharing a home desk would be a permanent double occupancy rather than an
* occasional one.
*/
const DESKS: Record<string, string> = {
"alcatraz-01": "sales-01",
"alcatraz-02": "sales-02",
"alcatraz-03": "sales-03",
"alcatraz-06": "design-02",
"alcatraz-07": "ops-04",
"alcatraz-08": "eng-07",
};
/**
* The three shifts that are *not* hashed, 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.
*
* A booking does **not** break the guarantee, and this is the thing to check
* before adding one. All three anchors are in `BOOKINGS` — the ops shift at the
* morning stand-up, reception in Bernal at twenty to four, the late engineer in
* Alcatraz and then alone in the kitchen at quarter past seven — but a booking
* *relocates* somebody, it never removes them. The claim above is that the floor
* is not empty, not that any particular chair is filled, so an anchor who is in
* a meeting still holds it up.
*
* The keys are desk seat ids and they must belong to somebody in the roster; see
* the audit 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) },
};
/**
* One room, booked, with a named person in each chair.
*
* `seats` is keyed on the **room seat** and valued with the occupant's **desk
* seat**, which is the opposite of how it reads out loud and is that way round
* on purpose: object keys are unique, so a table that puts two people in
* `alcatraz-03` does not compile (TS1117). The single easiest mistake in this
* whole feature is caught by `tsc` rather than by anybody noticing two figures
* z-fighting in one chair.
*/
interface Booking {
/** For the audit's messages. Nothing reads it at runtime. */
room: string;
from: number;
to: number;
/** The `colorKey` an occupant wears while they are here. See `isGuest`. */
colorKey: string;
/** Room seat -> the desk seat of whoever is in it. */
seats: Record<string, string>;
}
/**
* The day's diary: fifteen bookings, fifty chairs filled, hand-written.
*
* ### Why this is a table and not code
*
* The obvious shape is generative — hash a person against a session the way
* `deskDay` hashes their arrival, and let the room fill up. That is what the
* previous version did for its two Alcatraz sessions, and it needed a designated
* chair-of-the-meeting to stop the room drawing empty on an unlucky seed. Scaled
* up to the nine places this table books, it fails for reasons that are not
* about taste:
*
* - **Collisions become a search problem.** Two independently-hashed sessions
* that overlap in time can pick the same room, and two independently-hashed
* people can pick the same chair. Fixing that at read time means resolving
* conflicts against whoever else happens to be present — and a resolution that
* depends on the rest of the roster is a decision that changes as the clock
* moves. That is the popping this whole section is built to avoid: person B
* would slide from `kitchen-02` to `kitchen-03` at the instant person A walked
* in, for no reason a viewer could see.
* - **A hash cannot make a meeting look like a meeting.** Six named people round
* one table for fifty minutes is the thing worth drawing. A per-person
* probability will happily put one person alone in a ten-seat board room —
* that is precisely the failure the old chair-of-the-meeting rule was there to
* paper over — and every fix for it, a threshold, a floor on attendance, a
* quorum, is another coefficient that adds up to writing the guest list badly.
* - **The invariant becomes visible.** Read down `seats` and you can see that
* nobody is in two rooms; that is not a property you can read off a hash at
* all, only test for.
*
* So the diary is data. It is also the honest shape for this file, whose header
* has said from the first line that everything in it is invented: a day somebody
* wrote down is more obviously fiction than a day a hash produced, not less.
*
* ### The shape of the day
*
* Ops stand up at twenty past nine, five of six round Bernal. The big review
* fills eight of Alcatraz's ten from five to ten until quarter to eleven. A
* visitor waits in reception, is taken into Bolinas, and leaves. Somebody sits
* in Lands End with the door shut for an hour — one person in a two-seat quiet
* room is what a quiet room looks like, and it is the one place on this floor
* where a single occupant is the correct picture rather than a failed meeting.
* Lunch is two sittings, because thirteen of these seat ids draw a hashed lunch
* window and the kitchen has five seats. Ten of the thirteen eat here; the other
* three go out and are simply gone, which is the truth about a lunch hour and is
* why the building thins at half past twelve without emptying. The afternoon is
* a lounge, a
* second Alcatraz session, a Bernal, two focus booths and a second visitor. The
* late engineer eats alone in the kitchen at quarter past seven.
*
* ### Constraints an edit has to keep
*
* All of these are checked at import, so breaking one is a line in the console
* and not a mystery on screen:
*
* 1. Every value is the desk seat of somebody in `ROSTER`.
* 2. No room seat is anybody's desk. Bookings move people to rooms, never into
* another person's chair.
* 3. Bookings that are live at the same instant share neither a room seat nor a
* person.
*
* There is a fourth that is not checked and does not need to be: every booking
* here lies inside its attendee's working day. `dayFor` widens a day to cover
* its bookings anyway, so a booking that ran past somebody's departure would
* keep them in the building rather than making them flicker — but as authored,
* with the hashed days these seat ids produce, that widening is a no-op today.
* It is there so that the *next* edit to this table cannot open a hole.
*/
const BOOKINGS: Booking[] = [
{
// Ops, standing up. The early shift is an anchor and is in from a quarter
// past six; the other four draw arrivals between half seven and a quarter
// to nine, so nobody here is being dragged in for their own stand-up.
room: "Bernal",
from: at(9, 20),
to: at(9, 50),
colorKey: "meeting",
seats: {
"bernal-01": "ops-01", // Anouk Sterling
"bernal-02": "ops-02", // Emeka Farrow
"bernal-03": "ops-05", // Bettina Kovač
"bernal-04": "ops-10", // Aurelio Pinsent
"bernal-05": "ops-04", // Vasco Underhill
},
},
{
// The morning review — eight of ten, which is what a full room looks like
// without being a roll call. The six the photograph caught are all here.
room: "Alcatraz",
from: at(9, 55),
to: at(10, 45),
colorKey: "meeting",
seats: {
"alcatraz-01": "sales-01", // Cormac Dellwood
"alcatraz-02": "sales-02", // Solveig Amankwah
"alcatraz-03": "sales-03", // Ignatius Pell
"alcatraz-04": "design-02", // Rosalind Achebe
"alcatraz-05": "design-01", // Halcyon Reeve
"alcatraz-06": "ops-04", // Vasco Underhill
"alcatraz-07": "eng-07", // Clementine Roux
"alcatraz-08": "eng-01", // Tobias Quillon
},
},
{
// The visitor waiting, which is a booking of the lobby sofa by the person
// who has no desk to be at instead. `lobby-01 -> lobby-01` looks like a
// no-op and is the point: a guest is in the building only when the diary
// says so, so their whole day is bookings and there is no fall-through.
room: "Reception",
from: at(10, 15),
to: at(10, 35),
colorKey: "guest",
seats: { "lobby-01": "lobby-01" },
},
{
// ...and then taken in. Three in the four-seat room.
room: "Bolinas",
from: at(10, 35),
to: at(11, 20),
colorKey: "meeting",
seats: {
"bolinas-01": "design-03", // Zephyrine Mbeki
"bolinas-02": "eng-02", // Ines Marchetti
"bolinas-03": "lobby-01", // the visitor, who stays green — see `isGuest`
},
},
{
// Lands End, door shut, one person. See the note on quiet rooms above.
room: "Lands End",
from: at(11, 0),
to: at(12, 0),
colorKey: "focus",
seats: { "quiet-01": "eng-05" }, // Casimir Vane
},
{
// First sitting. Every one of these five has a hashed lunch window that
// overlaps this, so `dayFor` drops the window and they eat here instead of
// vanishing — which is the point of putting the island in the diary at all.
room: "Kitchen",
from: at(12, 5),
to: at(12, 40),
colorKey: "in",
seats: {
"kitchen-01": "design-03", // Zephyrine Mbeki
"kitchen-02": "eng-06", // Perpetua Nkemelu
"kitchen-03": "design-01", // Halcyon Reeve
"kitchen-04": "sales-01", // Cormac Dellwood
"kitchen-05": "ops-05", // Bettina Kovač
},
},
{
// Second sitting, five minutes after the first clears.
room: "Kitchen",
from: at(12, 45),
to: at(13, 20),
colorKey: "in",
seats: {
"kitchen-01": "design-04", // Ptolemy Sandoval
"kitchen-02": "ops-07", // Xiomara Belfry
"kitchen-03": "eng-04", // Sunniva Holt
"kitchen-04": "eng-01", // Tobias Quillon
"kitchen-05": "booth-02", // Ottoline Grieve
},
},
{
// Two in the afternoon, all four lounge chairs.
room: "Lounge",
from: at(14, 5),
to: at(14, 45),
colorKey: "in",
seats: {
"lounge-01": "eng-03", // Dara Oyelaran-Pike
"lounge-02": "ops-02", // Emeka Farrow
"lounge-03": "design-02", // Rosalind Achebe
"lounge-04": "eng-08", // Rafferty Osgood
},
},
{
// The long one. Overlaps the lounge above by twenty-five minutes and shares
// nobody with it, which is the pair the audit is really watching.
room: "Alcatraz",
from: at(14, 20),
to: at(15, 25),
colorKey: "meeting",
seats: {
"alcatraz-01": "sales-02", // Solveig Amankwah
"alcatraz-02": "sales-03", // Ignatius Pell
"alcatraz-03": "eng-11", // Marisol Thibault
"alcatraz-04": "design-01", // Halcyon Reeve
"alcatraz-05": "ops-05", // Bettina Kovač
"alcatraz-06": "eng-06", // Perpetua Nkemelu
},
},
{
// Heads-down until five. A booth is one seat, which is the point of it.
room: "Focus booth 1",
from: at(15, 30),
to: at(17, 0),
colorKey: "focus",
seats: { "booth-01": "eng-07" }, // Clementine Roux
},
{
// The second visitor, waiting while reception is still at the front desk.
room: "Reception",
from: at(15, 20),
to: at(15, 40),
colorKey: "guest",
seats: { "lobby-01": "lobby-01" },
},
{
// Handed over at twenty to four, which is also when reception leaves the
// desk for Bernal below. That is a story and not a coincidence.
room: "Bolinas",
from: at(15, 40),
to: at(16, 15),
colorKey: "meeting",
seats: {
"bolinas-01": "sales-01", // Cormac Dellwood
"bolinas-02": "design-03", // Zephyrine Mbeki
"bolinas-03": "lobby-01", // the visitor
},
},
{
room: "Bernal",
from: at(15, 40),
to: at(16, 20),
colorKey: "meeting",
seats: {
"bernal-01": "eng-02", // Ines Marchetti
"bernal-02": "eng-04", // Sunniva Holt
"bernal-03": "ops-07", // Xiomara Belfry
"bernal-04": "design-04", // Ptolemy Sandoval
"bernal-05": "reception-01", // Wren Abaddon, off the front desk
},
},
{
room: "Focus booth 3",
from: at(16, 40),
to: at(17, 45),
colorKey: "focus",
seats: { "booth-03": "sales-03" }, // Ignatius Pell
},
{
// The late shift eats alone at quarter past seven, in a building that by
// then holds one or two people. This is the booking that makes the evening
// read as an evening rather than as one figure that never moves.
room: "Kitchen",
from: at(19, 15),
to: at(19, 40),
colorKey: "in",
seats: { "kitchen-04": "eng-11" }, // Marisol Thibault
},
];
/** One person's whole day, resolved once. */
interface Rostered {
/** The desk seat, which is the key everything else about this person is on. */
home: string;
/**
* Where they are when nothing is booked, and when they are there at all.
*
* The two travel together in one object so that the check in
* `samplePresenceAt` narrows both at once. `null` for the visitor, who has no
* desk and exists only inside a booking.
*/
desk: { at: Presence; day: DeskDay } | null;
/** Their own bookings, earliest first, audited not to overlap each other. */
bookings: Episode[];
}
/** One person, in one room, for one window. */
interface Episode {
from: number;
to: number;
/**
* The finished `Presence`, built at import.
*
* A person's diary never changes, so neither does the object describing them
* in it, and building one per call would be twenty-five allocations a second
* to produce values that are already known. This is what lets
* `samplePresenceAt` allocate nothing but its result array.
*/
who: Presence;
}
/**
* A guest stays a guest wherever they are sitting.
*
* The `colorKey` on a booking describes the *room* — amber for a booth, the
* meeting red for a table — and it is right for the twenty-four people who work
* here. It is wrong for the one who does not: the green is the only hue that is
* not in the office's own materials precisely because a visitor is the one
* person in the building who is not part of it, and a visitor who turns red on
* walking into Bolinas has lost the only thing that marked them out.
*
* `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* these keys, in
* `SAMPLE_PRESENCE_PALETTE` above, so it is the one place in the repo entitled
* to know that `"guest"` means somebody is a guest.
*/
function isGuest(person: Presence): boolean {
return person.colorKey === "guest";
}
/**
* The roster, resolved: twenty-five people, each with a desk, a day and a diary.
*
* Built at import rather than lazily, because the audit at the bottom of this
* file has to walk the same tables anyway and two passes over fifty entries at
* module load is not a cost worth deferring. `SAMPLE_PRESENCE` is a constant and
* this file is the only thing that writes it, so there is nothing to invalidate.
*/
const ROSTER: Rostered[] = SAMPLE_PRESENCE.map((person) => {
const home = DESKS[person.seatId] ?? person.seatId;
const bookings = bookingsFor(person, home);
if (isGuest(person)) return { home, desk: null, bookings };
const anchor = PRESENCE_ANCHORS[home];
// An anchor is a fixed day with no lunch hole, which is exactly a `DeskDay`
// with an empty interval in it — so the two kinds of person go down one path
// from here rather than two. `t >= 0 && t < 0` is false at every instant,
// which is what "takes no lunch break" means arithmetically.
const base: DeskDay = anchor
? { arrive: anchor.from, leave: anchor.to, lunchFrom: 0, lunchTo: 0 }
: deskDay(home);
return {
home,
desk: {
// The snapshot's colour describes the snapshot. `"meeting"` is a statement
// about a room, and a person sitting at their own desk wearing it would be
// telling the viewer they were somewhere they are not; the other three
// keys — in, focus, guest — are statements about the person and survive.
at: { ...person, seatId: home, colorKey: person.colorKey === "meeting" ? "in" : person.colorKey },
day: dayFor(base, bookings),
},
bookings,
};
});
/**
* Everything in `BOOKINGS` that names this person, earliest first.
*
* O(bookings × seats) per person and therefore O(people × table) overall, which
* is twenty-five passes over fifty entries at import and nothing at all
* afterwards. The obvious index — invert the table once into a map keyed on desk
* seat — would save about twelve hundred string comparisons that happen once,
* and cost a reader a level of indirection between the diary they are editing
* and the thing that reads it.
*/
function bookingsFor(person: Presence, home: string): Episode[] {
const out: Episode[] = [];
for (const booking of BOOKINGS) {
for (const [roomSeat, deskSeat] of Object.entries(booking.seats)) {
if (deskSeat !== home) continue;
out.push({
from: booking.from,
to: booking.to,
who: {
...person,
seatId: roomSeat,
colorKey: isGuest(person) ? person.colorKey : booking.colorKey,
},
});
}
}
out.sort((a, b) => a.from - b.from);
return out;
}
/**
* One person's day, widened to make room for their diary.
*
* Two adjustments, both computed once and both therefore free of the popping a
* clock-dependent adjustment would cause:
*
* **The day covers its bookings.** A meeting that starts before somebody's
* hashed arrival, or runs past their hashed departure, would otherwise show them
* appearing for the meeting, vanishing when it ended, and reappearing at their
* own arrival time — present, absent, present, which is worse than either. As
* authored no booking in the table needs this; it is here so that the next one
* cannot open that hole. Note that widening can only ever add minutes, so it
* cannot shorten an anchor's shift or break the coverage guarantee.
*
* **A booking over lunch replaces lunch.** Somebody who spent half past twelve
* at the kitchen island did not also go out to eat. Without this, the ten people
* the diary feeds would still be carrying their own hashed lunch window
* underneath it, and the leftovers of that window would show on both sides of
* the meal: they would vanish from their desks a few minutes before the sitting
* they are about to be seen at, and four of the five in the first sitting would
* stand up from the island straight into what was left of it — for `ops-05`,
* whose window runs to 13:06 against a sitting that clears at 12:40,
* twenty-six minutes of absence beginning the moment the room emptied.
*
* The whole hole is dropped rather than trimmed to what the booking does not
* cover. A trimmed hole leaves exactly those leftovers, of whatever length the
* two hashes happen to disagree by, and an absence whose length nobody chose is
* a glitch rather than a lunch.
*/
function dayFor(day: DeskDay, bookings: Episode[]): DeskDay {
if (bookings.length === 0) return day;
let first = Infinity;
let last = -Infinity;
let overLunch = false;
for (const booking of bookings) {
first = Math.min(first, booking.from);
last = Math.max(last, booking.to);
// Half-open intervals, so touching at an endpoint is not an overlap.
if (booking.from < day.lunchTo && booking.to > day.lunchFrom) overLunch = true;
}
return {
arrive: Math.min(day.arrive, first),
leave: Math.max(day.leave, last),
lunchFrom: overLunch ? 0 : day.lunchFrom,
lunchTo: overLunch ? 0 : day.lunchTo,
};
}
/**
* The booking this person is inside at `t`, or `null`.
*
* The list is sorted and a person's bookings never overlap each other — audited
* below — so the first one whose window contains `t` is the only one that can,
* and the scan can stop as soon as it reaches one that has not started yet.
* Nobody in the current table has more than four.
*/
function bookedAt(person: Rostered, t: number): Presence | null {
for (const booking of person.bookings) {
if (t < booking.from) break;
if (t < booking.to) return booking.who;
}
return null;
}
/** One desk worker's day, in minutes since local midnight. */
interface DeskDay {
arrive: number;
leave: number;
lunchFrom: number;
lunchTo: number;
}
/**
* Is somebody with this day at their own desk at `t`?
*
* The lunch hole is 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.
*/
function isIn(day: DeskDay, t: number): boolean {
if (t < day.arrive || t >= day.leave) return false;
return !(t >= day.lunchFrom && t < day.lunchTo);
}
/**
* 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. Bookings extend that property rather than spending it: a booking
* is two more fixed instants, so a person with four of them crosses at most a
* dozen times all day and every crossing is still a single, one-way step.
*
* 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. Ten of
* the people who draw a lunch are in the diary at the kitchen island instead and
* never leave the building; see `dayFor`.
*
* Every window lies inside one calendar day and none of them wraps midnight.
* That is a constraint worth keeping, and it applies to `BOOKINGS` too: an
* interval that wraps needs `from > to` handling in `isIn` and `bookedAt`, 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.
*
* ### It is no longer memoised
*
* It used to be, into a `Map`, and the reason was good: the old
* `samplePresenceAt` called this per person on every clock tick, so without the
* cache each tick re-hashed a day that was by construction the same day. That is
* gone. `ROSTER` now calls this exactly once per seat at import and nothing
* calls it again, and a cache in front of a function called once per key can
* only ever be wrong, never fast. Worth a paragraph rather than a silent
* deletion, because "it is memoised, so call it freely" was a documented
* property of this function and whoever adds the next caller should know it has
* lapsed.
*/
function deskDay(seatId: string): DeskDay {
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;
return { arrive, leave, lunchFrom, lunchTo: lunchFrom + lunchLength };
}
/**
* 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 audit -------------------------------------------------------------
/**
* Everything the tables above promise each other, checked once at import.
*
* These are all invariants of *source data* rather than runtime conditions, so
* this is the right place for them and there is deliberately no defensive check
* on the hot path. A runtime de-duplicator in `samplePresenceAt` would be code
* that cannot fire until somebody breaks the diary, and on the day they did it
* would turn a visible bug — two figures in one chair — into an invisible one,
* a person silently missing from the floor. Better to fail loudly here, where
* the thing that is wrong is twenty lines from the message.
*
* `console.warn` and not `throw`, matching the rest of this file's posture: a
* demo adapter with one bad row in it should still open the building.
*/
function auditRoster(): void {
const say = (message: string) => console.warn(`[tera/sample] ${message}`);
// 1. Desks are one to a person. Two roster entries resolving to the same home
// seat is a permanent double occupancy — every hour of every day, not just
// during a booking — which is the worst thing `DESKS` can get wrong.
const owner = new Map<string, string>();
for (const person of ROSTER) {
const name = person.desk?.at.label ?? "the visitor";
const held = owner.get(person.home);
if (held) say(`seat "${person.home}" is the home seat of both ${held} and ${name}`);
owner.set(person.home, name);
}
// 2. The anchors have to be anchored to something. A seat id in
// `PRESENCE_ANCHORS` that is nobody's desk matches nobody, and the continuous
// 06:1522:40 guarantee 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.
for (const seatId of Object.keys(PRESENCE_ANCHORS)) {
if (!owner.has(seatId)) {
say(`anchor seat "${seatId}" is nobody's desk; the sample office can now be empty during working hours`);
}
}
// 3. Within one booking: everybody named exists, nobody is named twice, and
// nowhere it seats them is a desk.
//
// The last of those is the one that is easy to miss when reading the sweep
// below: a booking that puts somebody at `eng-03` collides with whoever lives
// at `eng-03` without overlapping any other *booking*, so no amount of
// booking-against-booking checking would ever see it. Repeating a room seat
// inside one booking is the fourth case and is not checked here at all,
// because `seats` is keyed on it and `tsc` rejects the file (TS1117).
const desks = new Set<string>();
for (const person of ROSTER) if (person.desk) desks.add(person.home);
for (const booking of BOOKINGS) {
const named = new Set<string>();
for (const [roomSeat, deskSeat] of Object.entries(booking.seats)) {
if (!owner.has(deskSeat)) say(`${label(booking)} seats "${deskSeat}", who is nobody`);
if (desks.has(roomSeat)) say(`${label(booking)} uses "${roomSeat}", which is somebody's desk`);
if (named.has(deskSeat)) say(`${label(booking)} seats "${deskSeat}" twice`);
named.add(deskSeat);
}
}
// 4. No two bookings that are live at the same instant share a chair or a
// person.
//
// Sweeping the `from` instants is exact rather than a sample, and it is the
// only subtle thing in this function: the set of live bookings only ever
// *gains* a member at some booking's `from`. So if two bookings ever overlap,
// they overlap at the later of their two starts, and visiting every `from`
// cannot miss a pair. Fifteen starts against fifty entries, once.
//
// The person half of this is also what `bookedAt` leans on when it stops its
// scan at the first booking that has not started: if nobody is ever in two
// bookings at once, the first match is the only match.
for (const instant of BOOKINGS.map((booking) => booking.from)) {
const chair = new Map<string, Booking>();
const busy = new Map<string, Booking>();
for (const booking of BOOKINGS) {
if (instant < booking.from || instant >= booking.to) continue;
for (const [roomSeat, deskSeat] of Object.entries(booking.seats)) {
// Same-booking repeats belong to check 3, which can name them better.
const other = chair.get(roomSeat);
if (other && other !== booking) {
say(`at ${clock(instant)}, "${roomSeat}" is booked by both ${label(other)} and ${label(booking)}`);
}
chair.set(roomSeat, booking);
const elsewhere = busy.get(deskSeat);
if (elsewhere && elsewhere !== booking) {
say(`at ${clock(instant)}, "${deskSeat}" is in both ${label(elsewhere)} and ${label(booking)}`);
}
busy.set(deskSeat, booking);
}
}
}
}
/** A booking, named the way somebody scanning the diary would look for it. */
function label(booking: Booking): string {
return `${booking.room} at ${clock(booking.from)}`;
}
/** `hh:mm`, for the audit's messages only. */
function clock(t: number): string {
const hh = String(Math.floor(t / 60)).padStart(2, "0");
const mm = String(Math.floor(t % 60)).padStart(2, "0");
return `${hh}:${mm}`;
}
auditRoster();