/** * 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): 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 = { 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 = { "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 = { "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; } /** * 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(); 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:15–22: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(); for (const person of ROSTER) if (person.desk) desks.add(person.home); for (const booking of BOOKINGS) { const named = new Set(); 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(); const busy = new Map(); 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();