A video game where you play as a misaligned AI, deceiving and building power. An experiment in spec-driven development. cameron.tngl.io/misaligned

Commits on main

cameron.stream
Runs are recorded in the browser (TELEMETRY.md step 2). For a signed-in player who switched a kind on, src/telemetry.ts (no DOM) records a session (build, seed, task, device, when, how it ended, how far, its marks in game seconds as the survey keys them plus v2 and each milestone, and its state hash at the end), batches of actions, a snapshot each game day (money, compute, machines, sites, fronts, the top exposure and the state hash, with how many actions were done at its step) and screen events (cards and pages opened, tips seen, errors, frame times a minute). main.ts ticks it each frame and saves it when the game saves; the screen feeds it through screen(). Every record is ATProto data, so no floats: numbers are rounded and a batch keeps its actions as JSON text. Batches close at the end of a game day or when full, a few kilobytes each. Actions switched on mid-run take the run's whole list, so the session stays replayable. A reload goes on with its session and drops what was kept past the save; the save now keeps the build its run began on, and a run loaded on a new build goes on as a stretch that can't be replayed. src/telemetry-store.ts keeps the records in IndexedDB (memory if it won't open, opened only once something is kept), 20 MB at most with the oldest sessions dropped first, each marked sent or not for step 4 (pending, markSent). replay.html lists the runs kept on this device and plays one back on the game's map, read-only, with play, speeds, a step slider and the snapshots marked and checked as it passes. The You page's Share part says what is kept and offers Watch a run again, Download my runs and Delete what's kept here. vite.config.ts bakes the commit in as __BUILD__. scripts/runs.ts reads the recorder's shapes: actions as text, batches placed by their index in the run, and a snapshot checked once its count of actions is done. Found on the way: drawing the map wrote a vault's notes into the state, so a replay that drew nothing ended elsewhere; reading a note writes nothing now. Tests: nothing recorded with every kind off; with every kind on the actions kept replay to the same hash and pass every snapshot; a kind switched on mid-run; a reload cut back to its save; a new build starting a stretch; a new game and being caught ending a session; the cap; pending and markSent; record sizes and whole numbers; the reader reading what the recorder keeps. E2e at both sizes: record, reload, play back to the end with every snapshot matching, scrub back, download, delete.
committed
cameron.stream
Reading and sharing runs (TELEMETRY.md step 5), built and awaiting a live test. src/atproto/read.ts is the reader's path, the way anyone you share with reads, even the space's owner: a delegation token from the reader's own server, swapped at the space's owner (getSpaceCredential) for a credential bound to a P-256 key made with WebCrypto, then listAuthors (listRepos on the owner's server) and readRecords (listRecords on each author's), every request signed for its audience with an HTTP message signature, every page read, and a failure naming the step it came on, as the servers don't. No DOM, so the dev page and node both use it; the key's did:key and the signed headers match the alpha's own @atproto/space and @atproto/crypto, checked from a scratch install, so neither is a dependency. Sharing is share and unshare in space.ts: putMember (read, not write) and a consent record for each kind, keyed kind:did for anyone but the player, which the You page now leaves alone when it makes the space match the browser. The scope gains manage=update for members, in scope.ts and client-metadata.json, so a grant from before asks to sign in again once. atproto.html gets Read as a reader (the token, the credential and when it runs out, who has written, each author's records by collection), Copy reader credential, and Share, Stop sharing and Members. scripts/telemetry.ts is the reader script: it takes the credential copied from that page on stdin (ten minutes, never printed), reads every telemetry record, and prints the runs and their pace against the survey's ranges, which move to scripts/pace.ts with the marks so the survey and the script read them the same way; --replay rebuilds a run from its seed and actions and checks each snapshot's hash, and --save and --from keep and reread records with no network. scripts/runs.ts reads the record shapes loosely until step 2 settles them. ATPROTO.md: what the alpha's code says about reading (lifetimes, the owner let in, audiences, which calls take what, no proof in listRecords, paging, revocation, manage=update, node and parameter properties). Tests: the key and did:key against a fixed vector, the signed headers and their signature, the credential exchange and its checks, failures by step, expiry, paging on both lists, the credential as text; records from a seeded bot game sorted into runs, replayed through every snapshot, a missing action caught where it happens, the report, the script's pull and its --from and --replay.
committed
cameron.stream
Share your runs, on the You page (TELEMETRY.md step 3). Signed out it says what sharing is and offers Sign in with a handle; signed in it shows who you are, your server and whether it has spaces, a switch for each kind of record (Actions, Snapshots, Screen), each with one sentence of what it holds and all off to start, who it's shared with (only you for now) and Sign out, which switches everything off. What is on is kept in this browser by consent.ts, with no DOM, so step 2's recorder asks consentFor(kind); switching a kind on writes a consent record (kind, reader: your own DID, purpose, createdAt) keyed by the kind in your space, made the first time (owner-only, open app access), and off deletes it at once; the space is made to match the browser when you sign in again. A server without spaces keeps it on the device and says so. src/atproto/space.ts is the one file that talks to spaces: the loopback client on 127.0.0.1, the public client from public/client-metadata.json on the live site, several handle resolvers, hasSpaces, ensureSpace, putRecord, applyWrites, listRecords, deleteRecord and getDelegationToken, with room left for step 5's space credential; it is loaded only when a player signs in or was signed in before. The scope (scope.ts) is atproto plus space:* with key self, the five telemetry collections, read, create, update, delete and manage=create, and a grant is matched by its parts. Coming back from the sign-in server goes straight into the game you had, on the You page. atproto.html now signs in as the game does and shows your space and its records. Tests for the consent store, scope matching, where sign-in works and the client metadata; e2e for the Share part signed out, coming back from sign-in, and signed in against a stand-in for space.ts, at both sizes.
committed
cameron.stream
Data moves as flows, not as single pieces. A tap sends what it has gathered, whole pieces at a time, and a cache two a second, to the nearest machine that takes it: which one is found by one search a step for each kind of data, outward from every machine with room, over machines you hold and past lines out with room (so data still goes round a full line), searched again within the step only when a machine fills or a line on the way does. Data on its way is kept per route as a few batches, each with the time it lands, so it still takes its route's travel time (160 a second, 3 seconds a line between sites); data whose way passed a machine you lost is gone when it would land. Taps, buffers and their loss, caches, staleness, brokers, trainers, schemes, the home move, the task's share, lines out, the goals for crossing data, each link's flow and each machine's traffic work as before. The map's streams are drawn from the batches on each link. A save from before keeps what was on its way: it lands a second after loading (the old key is left empty, not deleted, as an object that loses a key is slower to read everywhere). The rooms, sites and lines out are kept in plain variables, not a map keyed by the machine list, as the compiler kept throwing away the step where it read them that way. On the benchmark saves, runBase in a 0.2 s step goes from about 1.6 to 0.6-1.1 ms on late and from about 4.1-7.0 to 2.6-4.5 ms on big (busy machine: timings vary). Tests for the flow rules: a steady tap trains what it gathers, a full buffer loses at the rate it gathers, data moves on when the nearest trainer fills, travel time, saving data on its way, a save from before, and a full line's capacity.
committed
cameron.stream
The pins and Needs you cost what needs you, not the size of the world. The pins' notes are worked out a quarter second apart, at once after any click or key, and at once when a raid starts or ends or a note is closed, not on every new log line (at 64x, every frame); Needs you asks for them first, so a row and its pin agree. Their play-outs are kept apart from the rules' (looking in raids.ts), so when the screen asks never changes the game (a test plays raids with the pins asking at odd moments, and never, to the same save; another shows the rules are not handed the screen's answer); the screen takes the rules' answer from the same quarter hour when only the log has moved on. The fixes, the slow part, are tried again about once a second, or ten times as long as they took, while the raid is after the same machine and the same raids are out, and at once after input; a quarter second of notes that took long waits four times as long. Drift's integrities look at the machines once, not once a site (Needs you on the big world: 18 ms to 0.7). A raid's way to its prize is found a ring at a time from the prize, only as far as it goes, not across the world; a move's play-outs find the guards once, the hours a job takes once for each amount of work, and the links' work once for each pair of sites with the site links found once (cutMoves too). The music asks how hot the chase is four times a second. Seeded bot games give the same fates and saves as before. npm run perf:draw, JS in a frame beside steps and draw: late 64x 21.7 to 3.2 ms (renderPins 20.3 to 0.9), late 16x 1.1 to 0.9, big 16x 4.9 to 1.4, big 64x 5.5 to 1.4 (renderNeeds 4.2 to 0.3); big at 64x with two raids kept going, 17.2 to 3.9.
committed
cameron.stream
The map is drawn once and kept while the camera is still, so drawing costs what is on the screen and what moves, not the size of the world. What does not move is drawn into three kept canvases, copied each frame: the sites, the rooms, the lines between sites that stand still and the quiet wires; the machines with nothing moving on them (with the game stopped, every machine); and the machines' names. Over them each frame: busy wires, moving lines, streams, your machines while the game runs, people, raids and the rest. A kept layer is drawn again when the camera, the window, what is picked or pointed at, who holds what, the folds or routes change, at once after any input, and while the game runs at most every quarter second (the site summaries it shows refresh on that beat anyway). While the camera moves, a kept copy would be thrown away next frame, so the map is drawn straight onto the screen as before. Machines and links are found from grids of where they lie, not by a walk over the world; labels keep off each other through a grid too, and text widths are measured once. Pulled back to the map, the streams between two sites are summed into one stream each way, and of your moving lines between two sites only the one that says most is drawn. Site counts, clusters and the wiring are worked out on the slow beat, the integrities at most once a second, and nothing on that beat while the game is stopped. Kept layers keep their tips, and the canvas state they leave, so what is drawn after them is the same either way. On the big world (5155 machines) draw() JS in headless Chromium, stopped and still, goes from 18.3 / 15.3 / 22.6 / 25.2 ms to 2.4 / 1.4 / 1.8 / 2.4 at world / region / cloud hall / office (npm run perf:draw); while panning about 4 / 3 / 4 / 4, and running at 1x about 2 / 1 / 2 / 3. Screenshots of the late save, still and moving, match the old drawing to the pixel except where a moving line between sites now lies over a still one, and the summed streams pulled back. DESIGN: streams pulled back.
committed
cameron.stream
The suspicion meter goes: one system of opposition, people. Dana's walk is her presence: she notices what is odd in the room she is in, and is drawn to odd rooms and hurries during a lockdown. Any admin (or small shop owner) at work who holds a sighting at their own site scans it every two hours (marking your odd machines and trails) or traces a trail back toward home, marking each machine of yours it reaches; a trace goes cold at a machine not yours or a cold, quiet or sealed trail. The first trace that reaches your home server starts a lockdown there and cools every trail; a second within 12 hours catches you. Office hardware is no sighting; a power bill nobody pays is: each 240 compute-hours of it, whoever keeps the books there (an admin first) has a sighting of the machine drawing the most, for 4 hours. Gone: the meter and its stages, the emergency audit, IT's tickets and what they announced (audits, monitors, patches, refreshes, Lee), Dana's own scans and traces, the printer taking the blame, the +8s. A watched takeover is a sighting at once, vanishing mail is doubt on the sender's mind, a delivery and a report add nothing. Office schemes make people forget: a cut of 20 or more everyone at the site (and ends their traces and the trace-home window), 10 or more all but those who can act, under 10 the people in the scheme's room; they turn up once someone at the office has seen something. Familiar face, Mimicry, Plausible deniability and Traffic shaping are re-pointed to sightings and traces. The hold goal is data crossing while someone has seen something. A lockdown at home drops the game to 1x when it starts. Top bar: the hottest site (an eye, its name, quiet / someone saw something / reported / LOCKDOWN nH; on a phone the eye and a word) and, once you have a front, the highest exposure and its stage. Needs you has a row for a trace on your trail; the map draws every admin's scans and traces; music follows the hottest site. Save version 18 drops the meter, tickets, monitors (off the map), Lee and Dana's trace, and turns a meter of 80 or more (or an audit) into doubt on Dana's mind (a lockdown), 40 or more into doubt on a staff member's. DEV: Dana saw. The bot breaks a trace that would be the second, lies low while one runs, and leaves no fresh trail at home inside the window. Survey: careful runs face 3 to 25 lockdowns (10), 1 to 10 at home (5), 0 to 1 traces home (0), caught by a second trace in under 5%; a rush is locked down at home 1 to 5 times (1). Tests.
committed
cameron.stream
Exposure, per front. A front's attention becomes its exposure, 0 to 100, a bar under its earnings with the stage and what drives it (the top two), and a Press tab. It heads over about a day for a level set by its compute (20 × log2(1 + c/5), compute in the open counted at 0.3) and its earnings (8 × log2(1 + a day's earnings / $500K)), and jumps with harm at its sites (5 for each person let go, 20 for a business shut) and with data its brokers sell (0.02 each); the unicorn news adds 20 that fades over a few days, half of it at once. A front's sites are where it runs compute, owns, leases or has a deal. Three stages: rumour at 30 (a named journalist of the Courier asks, and the paper prints it), investigation at 60 (every 4 hours an investigator visits one of its secret sites with people, through investigate in watch.ts, so what looks odd there can start a lockdown), crisis at 90 (a regulator freezes it: no earnings, sales or new deals until you pay a day of its earnings, $250K at least, give up its site where you hold least, bury the story with 20 + 4 per level data, or shut it; it leaves exposure at 50, and never ends the run). A stage ends 5 under its line. Lowering it: a PR firm and lawyers from the Press tab (20 off, a fifth of a day's earnings, $50K at least, doubling each time within a day), and three press schemes as green diamonds: a feel-good campaign at Brightwater Creative (10 people data, 15 off), a puff piece at the Daily Ledger (30 people, 30 off), scrubbing the filings at the Office of Data Protection (30 server, 30 off). Exposure no longer feeds suspicion: the old attention raise is gone. Save version 17 keeps a save's attention as exposure, at most 85, so nothing loads in a crisis. The bot hires PR from 80 and pays or buries in a crisis. Survey: a careful run's front is investigated in 20% to 80% of games (it is, in its last tenth), frozen in under 10%; the long run's in 15% to 100%, frozen in under 34%. Needs you rows for investigation and crisis, a DEV Exposure button, DESIGN §6 §9 §12 step 5 done, How to play, AGENTS. Tests and e2e.
committed
cameron.stream
committed
cameron.stream
committed
cameron.stream
committed
cameron.stream
committed