lmjtfy.git / apps / lmjtfy / CLAUDE.md
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- **What the home page shows is kept in the archive object's memory**
20  (`Shelf::home`) until something it is made of changes. Anything new that
21  writes `asked` or `counts` must call `changed`, or the home page shows
22  the old numbers until the object next sleeps. Rows read are metered
23  (five million a day on Workers Free; three million were gone by
24  mid-morning on 2026-10-03), so do not read a whole table on a path every
25  visitor takes.
26- **`view.rs` stays free of `worker` types** so its tests run natively.
27- **Text from the visitor or the LLM reaches the page only through maud's
28  escaping.** Option labels and level descriptions are the LLM's words and
29  are as untrusted as the input.
30- **Do not use workers-rs's `Ai::run`.** It goes through serde-wasm-bindgen,
31  which makes a JS `Map` of every JSON object that is not a Rust struct.
32  `ai.rs` passes JSON text through `JSON.parse` and back through
33  `JSON.stringify`, which is also what lets the page show the reply untouched.
34- **Maud attribute names with a dot are written as string literals**
35  (`"data-on:input__debounce.300ms"`). Bare, the dot does not parse.
36- **The look is typesafe.ai's, on purpose** (the user, 2026-10-02): near-black
37  on white, one pink band, ordered-dither dot fields, old operating system
38  windows, pixel type for labels. `page.css`'s header says what was copied.
39  Their two display faces are commercial; Inter Tight and VT323 stand in.
40  Do not "modernise" it with rounded corners, shadows or a dark theme.
41- **The dither is drawn into CSS variables, not into elements.** The
42  transcript is replaced wholesale on every event, and a canvas inside it
43  would be wiped.
44- **The `/live` sockets are the archive's, accepted with `accept_web_socket`**
45  (hibernation). Do not hold them in a field or await on them: a
46  hibernating object keeps them only through `get_websockets`, and an
47  object kept awake by a socket is billed for every second of it.
48- **A toast shows a question only if the feed may** (`listed`). Anything
49  else is "someone asked". Keep it that way: the toast reaches strangers.
50- **The storage diagrams in README.md are kept beside `MIGRATIONS`.** A
51  test (`archive::tests`) fails if the archive's `erDiagram` lacks a column
52  the migrations make, so a new column changes the diagram in the same
53  commit. The budgets' diagram has no such test: its keys are in
54  `budget::Which::key` and `meter.rs`.
55- **A page's place is the country and city Cloudflare gives the `/live`
56  request, and lives only on its socket** (`serialize_attachment`). The
57  Worker sets `archive::COUNTRY`/`CITY` itself, removing any the page sent;
58  never take a place from anywhere the page controls, or anyone can put
59  words in every visitor's top bar. The same goes for the event the Worker
60  passes with it (`archive::EVENT`).
61- **A socket the new Worker did not place is closed with 1012 after
62  `REPLACE_AFTER_MS`** (`Seen::stale`), so a page that reconnected through
63  an old Worker during a deploy gets its place. The Worker sets `PLACED` on
64  every `/live` it forwards, place or not; drop that header and every page
65  reconnects every ten seconds forever.
66- **"N online" goes out on the alarm, not per socket** (`Archive::soon`): a
67  page that connects is told the state at once, everyone else within
68  `ONLINE_EVERY_MS`. Broadcasting on every connect is pages² messages when a
69  deploy reconnects them all.
70- **The `/live` socket is the SharedWorker's (`live.js`), one per browser
71  per build.** It replays only what a joining tab cannot render itself (the
72  build, the count, the places) and passes everything else on as it came;
73  do not replay toasts or feed patches, a joining tab would show them twice
74  or apply stale ones. Every page that has the nav must load `page.js`
75  (`/rules` did not until 2026-10-02, so its badge never lit).