docs: the conventions a fresh session can't infer, and the state it can't reconstruct
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.
This commit is contained in:
@@ -0,0 +1,138 @@
|
||||
# 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.
|
||||
Reference in New Issue
Block a user