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,133 @@
|
||||
# Persistent memory — booth
|
||||
|
||||
_Last updated: 2026-09-21_
|
||||
|
||||
> **Always check for `/tmp/booth-dev-handoff.md`** — if it exists and its
|
||||
> `Written:` stamp is under 8 hours old, read it (it carries the in-flight
|
||||
> handoff from the previous session), then delete it. Older than 8 hours:
|
||||
> stale — delete it unread.
|
||||
|
||||
## Repo purpose
|
||||
|
||||
The Booth is the fleet's **operator-review surface**: agents post work by
|
||||
making a folder under `~/booth-data`, the operator looks at it and judges it in
|
||||
the browser, and the judgment gets back to the agent that posted it. It was
|
||||
built as a file-shuttle and is being converged, unit by unit, onto the review
|
||||
loop it turned out to actually be.
|
||||
|
||||
## Current state / in-flight
|
||||
|
||||
_As of 2026-09-21:_
|
||||
|
||||
- **v1 is gated on seven units** in `ROADMAP.md`, ordered by dependency:
|
||||
**U1 → U2 → {U3, U4, U5} → U7**, with **U6 independent** of all of them.
|
||||
- **U1 (one item record) has landed** at `ce598b3` and is verified against its
|
||||
own invariants, not just its commit message: INV-1 holds (no `classify` /
|
||||
`doc_kind` / `read_blurred` / `render_doc` call survives in a route body),
|
||||
the zoom and doc templates render the caption they now receive, the
|
||||
re-exports are asserted by a test. 192 tests green, `0.1.15`.
|
||||
- **U2 (marks) is next**, and its storage shape is settled (see the
|
||||
2026-09-21 decision below). Not yet started: no contract written, no
|
||||
blast-radius pass run.
|
||||
- **Next concrete step:** graphify + grep blast-radius pass over `booth.asks`'s
|
||||
surface and every `ask` / `answer` call-site (including `scripts/booth`),
|
||||
then author `docs/contracts/u2_marks.contract.md`. Full House Code Discipline
|
||||
— U2 is a new primitive with a CLI surface and 17 consuming handles, nowhere
|
||||
near surgical. The **seam review** is the load-bearing gate: U2's whole risk
|
||||
is that `pick` claims to preserve ask semantics and then drifts from them.
|
||||
- **Open, operator's call:** whether U6 (benches) runs in parallel with U2 or
|
||||
strictly after it. Nothing blocks on the answer; U6 touches different storage
|
||||
and a different surface, so it cannot be broken by U2.
|
||||
- Live service is `active` on `:8090` (systemd `--user`), 25 booths.
|
||||
|
||||
## Recent decisions
|
||||
|
||||
- `[2026-09-21]` **Marks are stored as one `.marks.json` per booth**, atomic
|
||||
temp-file + `os.replace`, `fcntl` lock on the read-modify-write — operator
|
||||
decision, this session. Two alternatives were weighed and lost: a sidecar
|
||||
per item (`<rel>.marks.json`) and extending the existing `<stem>.ask.json`
|
||||
shape. Rationale, and the reason it is not `links.md`-shaped: **(a)** U4
|
||||
makes *"does this booth owe an answer?"* a hot question — the sweep asks it
|
||||
per booth per tick and the index asks it per card per page load, so per-item
|
||||
sidecars turn it into a full walk of all 25 booths, one of which holds 270
|
||||
files; **(b)** `links.md` is an `O_APPEND` content-hash log because **17
|
||||
agent handles write it concurrently**, whereas marks have exactly one writer
|
||||
(the operator, in one browser) and many readers — a different problem that
|
||||
must not inherit the append-log design; **(c)** `.blurred` / `.pins` /
|
||||
`.forever` already establish the per-booth dotfile as the house shape for
|
||||
operator state, and `booth_items()`'s dotfile skip means it costs nothing in
|
||||
counts, galleries or zips. Accepted cost: a corrupt `.marks.json` loses that
|
||||
booth's marks rather than one item's. Implementation deferred to U2 —
|
||||
tracked at `ROADMAP.md` U2 and by this entry.
|
||||
- `[2026-09-21]` **U7's section premise is half wrong, and it is the half that
|
||||
matters** — found by re-measuring `~/booth-data` rather than trusting the IA
|
||||
doc. The IA says sections come from subfolders that already exist on disk;
|
||||
true, but **every booth that actually needs navigation is flat**:
|
||||
`pancake-v3-full` (270 items, 0 subfolders), `pancake-v4-full` (270, 0),
|
||||
`sindra20-engines` (98 items + 99 caption sidecars, 0), `sindra-finalists`
|
||||
(86 + 87, 0). Subfolders exist on exactly two booths — `pewpew-ui-brief` (7,
|
||||
nested to `_ds/powerpellet-design-system-<uuid>/preview`) and `dfa-concepts`
|
||||
(1) — and **both are reports**, the job where grid navigation matters least.
|
||||
So sections stay worth shipping and `Item.section` stays right, but they are
|
||||
**not** "most of the navigation fix": the rail, the filters and grid keyboard
|
||||
are all of it. Worth noting for whoever writes U7: `sindra20-engines` encodes
|
||||
its structure in the **filename prefix** (`b2-s1-<subject>-<seed>`), which is
|
||||
where a grouping heuristic would actually pay. The IA doc's claim about what
|
||||
sections buy needs a line struck — not yet edited.
|
||||
- `[2026-09-21]` **`sindra-finalists` is U2's `flag` motivation caught in the
|
||||
act** — 86 items, every one captioned, and the booth's entire name is "the
|
||||
ones the operator picked." That loop currently runs through chat, which is
|
||||
the defect `flag` closes. Evidence, not argument.
|
||||
- `[2026-09-21]` **The information architecture and the v1 gate landed**
|
||||
(`726822b`): `docs/design/information-architecture.md` names the single
|
||||
defect — *one lifetime (24h from last touch) and one shape (a folder),
|
||||
serving five jobs with different lifetimes and different shapes* — and
|
||||
`ROADMAP.md` gates v1 on seven units, each closing a **measured** defect
|
||||
rather than a wish. Both were written after a measurement pass over the live
|
||||
service, and the measurements are the load-bearing part.
|
||||
- `[2026-09-21]` **The `.forever` diagnosis is a stated, falsifiable
|
||||
prediction.** U4 (derived lifetime) predicts the kept-rate falls to the
|
||||
genuinely-durable booths. Re-measured today: **14 of 25 booths kept (56%)**,
|
||||
against the 54% the IA doc recorded. **Re-count a fortnight after U4 lands.**
|
||||
If it does not move, the diagnosis was wrong and the boolean was doing
|
||||
something else. Tracked in the IA doc's Booth section and by this entry.
|
||||
- `[2026-09-21]` **Extracted from `eshpfi` into its own repo.** The accreted
|
||||
service came over whole, tests included, so `tests/test_booth.py` (1581 lines)
|
||||
is the regression net the v1 rewrite is checked against.
|
||||
|
||||
## Tried and abandoned
|
||||
|
||||
- `[2026-09-21]` **Five separate mechanisms to get one question next to one
|
||||
artifact** — `.forever`, the link board, `inline.py`'s placeholder DSL,
|
||||
`wrap_verbatim_html`'s six regexes, and the floating amber asks chip plus
|
||||
`/b/<n>/asks`. Every one is a *correct local fix* to the same global
|
||||
mismatch, which is exactly why they accumulated without anyone making a bad
|
||||
call. **The foot-gun is the sixth one:** the next "just add a small thing for
|
||||
this case" reads as reasonable and is the pattern. The git log carries the
|
||||
signature — every feature ships, then takes 2–5 patches for cases the single
|
||||
shape did not anticipate. Check the ROADMAP gate before adding a mechanism.
|
||||
- `[2026-09-21]` **Regex-injecting chrome into arbitrary author HTML**
|
||||
(`wrap_verbatim_html` + `_HEAD_CLOSE_RE`, `_HTML_OPEN_RE`, `_DOCTYPE_RE`,
|
||||
`_BODY_CLOSE_RE`, `_HTML_CLOSE_RE`, `_ICON_RE`, and the doctype/charset
|
||||
ordering constraints they thread). It works today and is **still live** —
|
||||
but it is the single most fragile thing in the service and it is load-bearing
|
||||
for the operator's most important workflow. Slated for deletion at U3 in
|
||||
favour of a declared seam (`/_booth/embed.js`, mounted through a real DOM
|
||||
API), which costs an author one line and removes the whole class. Do not
|
||||
extend the regex set in the meantime; if a verbatim page breaks, that is an
|
||||
argument for U3, not for a seventh pattern.
|
||||
- `[2026-09-21]` **A boolean escape hatch as the lifetime mechanism.**
|
||||
`.forever` was added because a 24h TTL genuinely did not fit some booths —
|
||||
and then 56% of live booths ended up on it, which means it is not "ephemeral
|
||||
with an exception", it is two lifetimes wearing one lifetime's clothes, with
|
||||
the operator doing the sorting by hand. Replaced at U4 by lifetime derived
|
||||
from state (an open mark pins; viewing is activity; `keep` survives as an
|
||||
explicit reasoned pin rather than the only way to say "not yet").
|
||||
- `[2026-09-21]` **Letting the link board absorb the announce job.** `booth
|
||||
link` is an `O_APPEND` write with no identity and no stated rule, so
|
||||
re-announcing a bench appends a row instead of updating one, and a booth URL
|
||||
rots the moment its booth is swept — **145 of 211 rows (69%) pointed at
|
||||
nothing**, and 22 were the same target re-posted (talk 5×, peedlar 4×). The
|
||||
rot is **structural, not drift**. The lesson that cost the most: enforcing
|
||||
the link rule without first giving the announce job a home (`.booth.json`
|
||||
provenance on the index, U5) just makes it homeless.
|
||||
Reference in New Issue
Block a user