lmjtfy.git / apps / lmjtfy / CLAUDE.md
CLAUDE.mdpreviewCLAUDE.mdsource116 lines · 7.5 KB · raw

For agents, on top of README.md, which they read first.

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