The repo came out of eshpfi one day ago with neither piece of its house
furniture, so everything non-obvious about it lived in docstrings inside a
998-line app.py — which is a bad place to keep an invariant that breaks
silently on every fleet host when someone violates it.
CLAUDE.md carries the five that do exactly that:
* links.py and asks.py are stdlib-only because scripts/booth imports them
under the system python3 with no venv, so one third-party import breaks
`booth ask` everywhere and fails in an agent's session, not in ours
* the filesystem is the state, and booth_items()'s dotfile skip is why a
per-booth dotfile is the right shape for new operator state
* booth_items() is the only thing that classifies a file or resolves a
caption (U1's INV-1) — the zoom-loses-the-annotation bug was three
readers of one truth, not a rendering bug
* moved names stay importable from booth.app, asserted by a test
* sidecar writes are atomic; render_doc returns raw text on purpose
Plus the distinction that decided this session's storage call: links.md is
an append log because 17 handles write it concurrently, and marks have one
writer. Different problem, different shape — ask which you have first.
persistent-memory.md carries what CLAUDE.md is structurally unable to: the
dated decisions, the `.forever` prediction and its re-measure date, and the
foot-gun log. Two entries are load-bearing for the next unit — the settled
mark storage shape with the reasoning that picked it, and a measured
correction to U7's premise: every booth that actually needs navigation is
flat, so subfolder sections are worth shipping but are not the nav fix.
No version bump — docs and memory, both on the SemVer skip list.
139 lines
6.1 KiB
Markdown
139 lines
6.1 KiB
Markdown
# 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/<slug>.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 `<pre>`, 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.
|