lmjtfy.git / apps / lmjtfy / CLAUDE.md
CLAUDE.mdpreviewCLAUDE.mdsource116 lines · 7.5 KB · raw
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).