1@README.md 2 3- **Jev and Workers AI calls are not `Send`** (they hold JS values), and axum 4 handlers must be. `stream` therefore spawns the work on the isolate's 5 executor and feeds the response from a channel. Do not await such a call 6 inside a handler. 7- **The archive's lookup-then-start must not await.** In `Archive::once`, 8 from reading `versions` to inserting into `running` there is no `.await`, so 9 no second ask can run in between and send the same request. An await there 10 brings the duplicate back. 11- **The call runs under `wait_until`, not under the ask that started it.** A 12 visitor who leaves cancels their ask; the call still finishes and is kept. 13- **`versions` is keyed by the body itself, not a hash of it.** The page's 14 `call_id` is a hash, and only names a call; the archive never looks a 15 response up by it. 16- **The archive's schema changes only by adding a step to `MIGRATIONS`.** A 17 step that has shipped has run on the live object and will not run again; 18 editing it changes nothing there and breaks a fresh one. 19- **A step runs inside `Past::atomically`, and a failed one leaves the 20 archive refusing everything but `Bookmark` and `Restore`.** Do not put 21 the panic back in `new`: an object that cannot start cannot be restored 22 either. The tests run every step on a real SQLite (`archive::tests`), 23 which is where a statement that does not parse is found. 24- **A question the owner's backend asks of `events` is a view in 25 `MIGRATIONS`, not SQL in the backend** (the owner, 2026-10-04). One that 26 is asked by the day is the three of `<name>_live`, `<name>_finished` and 27 `<name>`, with a row of `finished` and its name in `FINISHED`; a test 28 holds the four together. A `_live` view reads `hits_unfinished` and 29 groups by `day` first. To change one that has shipped: a new step that 30 drops and makes its views, empties its table, and sets its `through` 31 back to -1, so the days are copied again. 32- **Never copy a day into a `_finished` table but through `finish_sql`.** 33 It refuses a day already finished. A finished day's events are not in 34 the `_live` view any more, so copying it again would put nothing where 35 the day was (the test found this, 2026-10-04). 36- **A view asked through another view loses its `WHERE day`.** SQLite 37 pushes the condition one view down, not two: `days` reads all of 38 `hours_finished` whatever day is asked for, which is small. Do not build 39 a view on a `_visitors` view; ask it directly. 40- **`versions` and `events` take `INSERT` and nothing else.** Triggers 41 refuse `UPDATE` and `DELETE` (step 17). A correction to either is a 42 migration step that drops the trigger, corrects, and makes it again, 43 with the reason beside it: do not drop one anywhere else. 44- **What the home page shows is kept in the archive object's memory** 45 (`Shelf::home`) until something it is made of changes. Anything new that 46 writes `asked` or `counts` must call `changed`, or the home page shows 47 the old numbers until the object next sleeps. Rows read are metered 48 (five million a day on Workers Free; three million were gone by 49 mid-morning on 2026-10-03), so do not read a whole table on a path every 50 visitor takes. 51- **The feed's questions for suggesting are kept in the archive object's 52 memory too** (`Shelf::listed`), read again only after a question is asked 53 or moderated (`relisted`). A suggestion is wanted on every pause in 54 someone's typing: never read `asked` for one. 55- **`view.rs` stays free of `worker` types** so its tests run natively. 56- **Text from the visitor or the LLM reaches the page only through maud's 57 escaping.** Option labels and level descriptions are the LLM's words and 58 are as untrusted as the input. 59- **Do not use workers-rs's `Ai::run`.** It goes through serde-wasm-bindgen, 60 which makes a JS `Map` of every JSON object that is not a Rust struct. 61 `ai.rs` passes JSON text through `JSON.parse` and back through 62 `JSON.stringify`, which is also what lets the page show the reply untouched. 63- **Maud attribute names with a dot are written as string literals** 64 (`"data-on:input__debounce.300ms"`). Bare, the dot does not parse. 65- **The look is typesafe.ai's, on purpose** (the user, 2026-10-02): near-black 66 on white, one pink band, ordered-dither dot fields, old operating system 67 windows, pixel type for labels. `page.css`'s header says what was copied. 68 Their two display faces are commercial; Inter Tight and VT323 stand in. 69 Do not "modernise" it with rounded corners, shadows or a dark theme. 70- **The dither is drawn into CSS variables, not into elements.** The 71 transcript is replaced wholesale on every event, and a canvas inside it 72 would be wiped. 73- **The `/live` sockets are the archive's, accepted with `accept_web_socket`** 74 (hibernation). Do not hold them in a field or await on them: a 75 hibernating object keeps them only through `get_websockets`, and an 76 object kept awake by a socket is billed for every second of it. 77- **A toast shows a question only if the feed may** (`listed`). Anything 78 else is "someone asked". Keep it that way: the toast reaches strangers. 79- **The storage diagrams in README.md are kept beside `MIGRATIONS`.** A 80 test (`archive::tests`) fails if the archive's `erDiagram` lacks a column 81 the migrations make, so a new column changes the diagram in the same 82 commit. The budgets' diagram has no such test: its keys are in 83 `budget::Which::key` and `meter.rs`. 84- **A page's place is the country and city Cloudflare gives the `/live` 85 request, and lives only on its socket** (`serialize_attachment`). The 86 Worker sets `archive::COUNTRY`/`CITY` itself, removing any the page sent; 87 never take a place from anywhere the page controls, or anyone can put 88 words in every visitor's top bar. The same goes for the event the Worker 89 passes with it (`archive::EVENT`). 90- **A socket the new Worker did not place is closed with 1012 after 91 `REPLACE_AFTER_MS`** (`Seen::stale`), so a page that reconnected through 92 an old Worker during a deploy gets its place. The Worker sets `PLACED` on 93 every `/live` it forwards, place or not; drop that header and every page 94 reconnects every ten seconds forever. 95- **"N online" goes out on the alarm, not per socket** (`Archive::soon`): a 96 page that connects is told the state at once, everyone else within 97 `ONLINE_EVERY_MS`. Broadcasting on every connect is pages² messages when a 98 deploy reconnects them all. 99- **The `/live` socket is the SharedWorker's (`live.js`), one per browser 100 per build.** It replays only what a joining tab cannot render itself (the 101 build, the count, the places) and passes everything else on as it came; 102 do not replay toasts or feed patches, a joining tab would show them twice 103 or apply stale ones. Every page that has the nav must load `page.js` 104 (`/rules` did not until 2026-10-02, so its badge never lit). 105- **Release files are the code host's alone, and the token never leaves 106 GitHub's API.** `release::fetch` follows the `302` by hand and fetches the 107 signed address with no `authorization`; do not let `fetch` follow it itself. 108 A file is returned as `Served::File`, from `fetch` before the router: a body 109 that goes through axum loses its `Content-Length` (measured on staging). 110 The body is streamed: never `bytes()`/`to_bytes` a release file (80-200 MB 111 against a 128 MB isolate). A path segment is `release::segment` or it is a 112 404; a kind two files share is a 404, never a pick. No GitHub text (tag, 113 asset name, message) goes in a log line or an error response. 114- **A download is a `download` event, and not also a `view`.** `Event::got` 115 skips the file paths (`is_download`); keep the two in step with `release::route`. 116 No toast for it (the owner, 2026-10-05).