# CLAUDE.md — the Booth Mechanical conventions for this repo: **how the code here is written.** Timeless by design — temporal state and dated decisions live in `persistent-memory.md`, the v1 gate lives in `ROADMAP.md`, and the model the code is converging on lives in `docs/design/information-architecture.md`. House-wide rules (SemVer cadence, attribution, roadmap gate, the House Code Discipline) come from `~/.claude/CLAUDE.md` and are **not** restated here. ## What this is A standing FastAPI service on `:8090` that scans `~/booth-data` for drop-folders and renders each as a review surface. Agents post work by making a folder; the operator looks at it, judges it, and the judgment gets back to the agent. Read `docs/design/information-architecture.md` before any structural change. It supersedes `README.md`, which still describes the accreted file-shuttle model and is rewritten as the v1 units land. ## Persistent memory `persistent-memory.md` at the repo root captures durable intent and supporting evidence (goals, decisions, foot-gun warnings, in-flight state) across context resets. Read it at session start; treat it as one input alongside this file and the auto-memory system, not as the single source of truth. It is a lean **index**: the dated log sections keep each over-threshold entry's full body in `persistent-memory.d/.md`. Pull a detail file only when its index line is relevant to your work — never bulk-read `persistent-memory.d/`. When you commit, include any pending `persistent-memory.md` and `persistent-memory.d/` updates in the same commit. Never leave them as a floating uncommitted change while shipping other work — durable memory that lags the code defeats its own purpose. ## The five invariants These are the ones a casual change breaks silently. Each has a test. ### 1. `links.py` and `asks.py` are stdlib-only, on purpose `scripts/booth` — the CLI every fleet session uses — imports them directly: ```sh BOOTH_SRC=… python3 -c 'import sys; sys.path.insert(0, …); from booth.asks import write_ask' ``` It runs under the system `python3` with **no venv**. A single third-party import in either module breaks `booth ask` / `booth answer` / `booth unlink` on every host, and the failure surfaces in an agent's session, not in ours. `items.py` and `app.py` are free to import what they like. Those two are not. ### 2. The filesystem is the state No database. `ls ~/booth-data` tells you everything the service knows. Per-booth operator state is a **dotfile inside the booth**: `.forever` (keep), `.blurred` (one rel per line), `.pins` (link-board pin ids), `.uploaded` (upload-booth marker). `booth_items()` skips `name.startswith(".")`, so a new dotfile costs nothing in item counts, galleries or zips. That skip is why the dotfile is the right shape for new operator state — use it rather than inventing a sidecar-per-item. ### 3. One resolver for item facts `booth.items.booth_items(booth)` is the only place a file is classified, a caption resolved, a section derived or blur state read. **No route body derives an item fact.** Every surface — gallery, zoom, doc view, index card, zip manifest — reads the `Item` record. Contract: `docs/contracts/u1_item_record.contract.md`, INV-1. Falsifiable and tested: no call to `classify` / `doc_kind` / `read_blurred` / `render_doc` survives inside `create_app`. This exists because the zoom view once re-derived the item from scratch and, in doing so, never resolved the caption — the operator's "zoomed images lose their annotations" bug. It was not a rendering bug; it was three readers of one truth. ### 4. Re-export, don't move-and-break Names that moved from `app.py` to `items.py` (`classify`, `doc_kind`, `render_doc`, `CAPTION_MAX`, the extension sets) stay importable from `booth.app`. 20+ test sites import them by name from there. The re-export is load-bearing and **asserted by a test**, not left to convention — a silent drop would be found by a consumer, not by us. ### 5. Sidecar writes are atomic; text bodies stay raw Anything a session may read while the browser writes it goes through temp file + `os.replace` (see `asks.write_answer`). A reader never sees a partial file. `render_doc` returns **raw** text for the non-markdown case on purpose: the template escapes it inside `
`, and pre-escaping here double-encodes under
Jinja autoescape.

## Multi-writer vs single-writer — don't inherit the wrong shape

`links.md` is a **multi-writer** append log: 17 agent handles post to it
concurrently, so it is an `O_APPEND` write with content-hash row identity and
an `fcntl` lock only on the rewrite path. Index-based removal would delete a
neighbour's row when another session appends mid-operation.

Marks, blur and keep state are **single-writer** — the operator, in one
browser — with many readers (sessions polling). That is a different problem and
must not inherit the append-log design. Per-booth file, atomic replace, lock
the read-modify-write.

Ask which one you have before choosing a storage shape.

## Non-goals, standing

- **No auth.** LAN/mesh-internal. Blur is cosmetic and the UI says so. Anything
  that must not be seen by whoever can reach `:8090` must not be in a booth.
- **No database.** See invariant 2.
- **No upload API for booths.** A session makes a folder. That is the whole
  API, and it is why every agent family can use this without a client.
- **Nothing deletes the operator's data on a timer** beyond the documented 24h
  TTL. Liveness is *flagged*, not enforced.

## Working in here

```sh
.venv/bin/python -m pytest -q          # the suite; keep it green
curl -s localhost:8090/healthz         # the live service (systemd --user)
systemctl --user restart booth.service # after a code change, to see it live
```

`booth.service` is a user unit installed to `~/.config/systemd/user/`. The repo
copy is the source; edits there need a `daemon-reload`.

Tests are the spec for behaviour the contracts don't cover yet — `test_booth.py`
carries the accreted service's behaviour and is the regression net for the v1
rewrite. Don't edit an existing assertion to make a change pass; if the
behaviour genuinely changes, the contract says so first.