diff --git a/persistent-memory.d/2026-09-21-ambiguity-pass-did-the-work.md b/persistent-memory.d/2026-09-21-ambiguity-pass-did-the-work.md new file mode 100644 index 0000000..4ff8788 --- /dev/null +++ b/persistent-memory.d/2026-09-21-ambiguity-pass-did-the-work.md @@ -0,0 +1,11 @@ +# Every code-changing finding came from the AMBIGUITY pass + +_2026-09-21 · booth_ + +**Every one of the panel's code-changing findings came from the +AMBIGUITY pass, none from a paraphrase divergence** — and two arms independently +proposed cutting the paraphrase to a drift-check for narrative-heavy contracts, +because this contract's own frontmatter carries a plain-language narrative and the +paraphrase was partly reading my framing back to me. That is a finding about the +`/heid-contract-review` **skill**, not about this repo, and it was reported back +to heid. Recorded here only so a future session does not rediscover it. diff --git a/persistent-memory.d/2026-09-21-boolean-escape-hatch-as-lifetime.md b/persistent-memory.d/2026-09-21-boolean-escape-hatch-as-lifetime.md new file mode 100644 index 0000000..c65482c --- /dev/null +++ b/persistent-memory.d/2026-09-21-boolean-escape-hatch-as-lifetime.md @@ -0,0 +1,11 @@ +# A boolean escape hatch as the lifetime mechanism + +_2026-09-21 · booth_ + +**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"). diff --git a/persistent-memory.d/2026-09-21-deterministic-order-invariant.md b/persistent-memory.d/2026-09-21-deterministic-order-invariant.md new file mode 100644 index 0000000..6d680bb --- /dev/null +++ b/persistent-memory.d/2026-09-21-deterministic-order-invariant.md @@ -0,0 +1,16 @@ +# Deterministic order is a cross-cutting v1 invariant + +_2026-09-21 · booth_ + +**Deterministic order is a cross-cutting v1 invariant** — +operator directive, mid-implementation. Every ordered collection the Booth +renders must have a *stated* rule producing the same sequence on every render +of the same state; the rule can be anything defensible (byte order, time, an +explicit number, an arbitrary-but-recorded sequence), but no rule at all is +forbidden. It binds harder here than elsewhere because the Booth's job is +**comparison** — the operator judges tile 47 against tile 47 and refers to +artifacts positionally, so an order that moves between renders misfiles a flag +or a note rather than crashing. Recorded as `ROADMAP.md` § "Cross-cutting +invariant" (with the per-collection table) and `CLAUDE.md` invariant 6, and +tested. Still undecided and must be settled before those units ship: **U7's +section ordering and compare pairing**, and **U6's bench listing**. diff --git a/persistent-memory.d/2026-09-21-extracted-from-eshpfi.md b/persistent-memory.d/2026-09-21-extracted-from-eshpfi.md new file mode 100644 index 0000000..a5a4b94 --- /dev/null +++ b/persistent-memory.d/2026-09-21-extracted-from-eshpfi.md @@ -0,0 +1,7 @@ +# Extracted from `eshpfi` into its own repo + +_2026-09-21 · booth_ + +**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. diff --git a/persistent-memory.d/2026-09-21-five-mechanisms-one-job.md b/persistent-memory.d/2026-09-21-five-mechanisms-one-job.md new file mode 100644 index 0000000..808103d --- /dev/null +++ b/persistent-memory.d/2026-09-21-five-mechanisms-one-job.md @@ -0,0 +1,13 @@ +# Five mechanisms to get one question beside one artifact + +_2026-09-21 · booth_ + +**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//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. diff --git a/persistent-memory.d/2026-09-21-forever-diagnosis-is-a-prediction.md b/persistent-memory.d/2026-09-21-forever-diagnosis-is-a-prediction.md new file mode 100644 index 0000000..2361989 --- /dev/null +++ b/persistent-memory.d/2026-09-21-forever-diagnosis-is-a-prediction.md @@ -0,0 +1,10 @@ +# The `.forever` diagnosis is a falsifiable prediction + +_2026-09-21 · booth_ + +**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. diff --git a/persistent-memory.d/2026-09-21-ia-and-v1-gate-landed.md b/persistent-memory.d/2026-09-21-ia-and-v1-gate-landed.md new file mode 100644 index 0000000..1876eac --- /dev/null +++ b/persistent-memory.d/2026-09-21-ia-and-v1-gate-landed.md @@ -0,0 +1,11 @@ +# The information architecture and the v1 gate landed + +_2026-09-21 · booth_ + +**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. diff --git a/persistent-memory.d/2026-09-21-jinja-hot-reload-outage.md b/persistent-memory.d/2026-09-21-jinja-hot-reload-outage.md new file mode 100644 index 0000000..1249903 --- /dev/null +++ b/persistent-memory.d/2026-09-21-jinja-hot-reload-outage.md @@ -0,0 +1,24 @@ +# Letting Jinja hot-reload templates in the deployment root + +_2026-09-21 · booth_ + +**Letting Jinja hot-reload templates while the repo is the +deployment root** — the cause of a live outage the same day U2 landed, and the +sharpest foot-gun in the repo. `booth.service` sets `WorkingDirectory` to this +repo, so the running service imports these files with no build step and no +staging copy. Python is read once at process start; Jinja's `FileSystemLoader` +re-reads a template **on every render**. Editing `booth.html` therefore +deployed it instantly against Python from 22:03 that knew nothing about +`item_marks`, and **19 of 25 live booths returned 500** with +`UndefinedError: 'item_marks' is undefined`. Neither the old code nor the new +code was broken — the service was running both at once. +**The lesson that generalises:** a skew between a process and the disk under it +is invisible to the test suite by construction, so no amount of green tests +would have caught it; the operator found it. Fixed at the source rather than +with a reminder — the `Environment` is hand-built with `auto_reload=False`, so +there is now ONE staleness rule (nothing takes effect until you restart) and +the running process is always a coherent snapshot of one commit. Asserted by +`test_templates_do_not_hot_reload_from_disk`. Watch the second-order risk the +fix introduces: a hand-built `Environment` does not inherit `autoescape` from +the `Jinja2Templates` constructor, and booth names, item names and mark text +are all agent-authored strings landing in HTML. diff --git a/persistent-memory.d/2026-09-21-link-board-absorbing-announce.md b/persistent-memory.d/2026-09-21-link-board-absorbing-announce.md new file mode 100644 index 0000000..bcab933 --- /dev/null +++ b/persistent-memory.d/2026-09-21-link-board-absorbing-announce.md @@ -0,0 +1,12 @@ +# Letting the link board absorb the announce job + +_2026-09-21 · booth_ + +**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. diff --git a/persistent-memory.d/2026-09-21-marks-storage-decision.md b/persistent-memory.d/2026-09-21-marks-storage-decision.md new file mode 100644 index 0000000..345939a --- /dev/null +++ b/persistent-memory.d/2026-09-21-marks-storage-decision.md @@ -0,0 +1,21 @@ +# Marks are one `.marks.json` per booth + +_2026-09-21 · booth_ + +**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 (`.marks.json`) and extending the existing `.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. diff --git a/persistent-memory.d/2026-09-21-marks-write-wiped-judgment.md b/persistent-memory.d/2026-09-21-marks-write-wiped-judgment.md new file mode 100644 index 0000000..c22b631 --- /dev/null +++ b/persistent-memory.d/2026-09-21-marks-write-wiped-judgment.md @@ -0,0 +1,15 @@ +# A write over a damaged `.marks.json` wiped the booth + +_2026-09-21 · booth_ + +**A write over a damaged `.marks.json` was wiping every mark in +the booth.** Shipped in `v0.2.0`, found by the panel (Kimi, converged with +Hulda), fixed in `v0.2.1`. `marks_for` is deliberately lenient — unparseable +reads as `[]` so a review page still loads — and the write path inherited that +leniency through the same reader, so one flag click appended to an empty list and +atomically replaced the file. The fix is an **asymmetry**, which is the reusable +part: reads stay lenient, writes go strict (`MarksCorrupt`), damaged bytes stay +on disk, routes answer 409 not 500. A page that renders without an annotation is +recoverable; a file that overwrote the operator's judgment is not. Kimi also +named the class correctly — "an author steeped in the design conversation would +likely read past" it — and that was accurate. diff --git a/persistent-memory.d/2026-09-21-partial-answer-counts-as-open.md b/persistent-memory.d/2026-09-21-partial-answer-counts-as-open.md new file mode 100644 index 0000000..5f0c344 --- /dev/null +++ b/persistent-memory.d/2026-09-21-partial-answer-counts-as-open.md @@ -0,0 +1,10 @@ +# A partially-answered pick counts as OPEN + +_2026-09-21 · booth_ + +**A partially-answered pick now counts as OPEN** — declared, not +smuggled. The old index badge tested `answer is None`, so a half-answered +four-question ask read as closed on the index while the panel beside it +rendered `◐ partial`: the two disagreed about the same booth. Open is the +reading that makes U4 correct — a lifetime rule that unpinned a booth on the +first radio click would sweep a review in flight. diff --git a/persistent-memory.d/2026-09-21-regex-injecting-chrome.md b/persistent-memory.d/2026-09-21-regex-injecting-chrome.md new file mode 100644 index 0000000..63c8f5a --- /dev/null +++ b/persistent-memory.d/2026-09-21-regex-injecting-chrome.md @@ -0,0 +1,14 @@ +# Regex-injecting chrome into arbitrary author HTML + +_2026-09-21 · booth_ + +**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. diff --git a/persistent-memory.d/2026-09-21-sindra-finalists-is-the-motivation.md b/persistent-memory.d/2026-09-21-sindra-finalists-is-the-motivation.md new file mode 100644 index 0000000..cb12ffa --- /dev/null +++ b/persistent-memory.d/2026-09-21-sindra-finalists-is-the-motivation.md @@ -0,0 +1,8 @@ +# `sindra-finalists` is U2's flag motivation, caught live + +_2026-09-21 · booth_ + +**`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. diff --git a/persistent-memory.d/2026-09-21-tagging-with-a-gate-in-flight.md b/persistent-memory.d/2026-09-21-tagging-with-a-gate-in-flight.md new file mode 100644 index 0000000..a5c19a5 --- /dev/null +++ b/persistent-memory.d/2026-09-21-tagging-with-a-gate-in-flight.md @@ -0,0 +1,12 @@ +# Tagging a release while a review gate was in flight + +_2026-09-21 · booth_ + +**Tagging a release while a review gate was still in flight.** +`v0.2.0` was cut and announced to 15 consuming handles; the +`/heid-contract-review` panel — dispatched BEFORE implementation, as the +discipline says — replied afterwards with three defects in the code that had just +shipped, one of them silent data loss. Nothing about the tier decision was wrong; +the *timing* was. **If a gate is outstanding on the work being released, the tag +waits for it.** The cost was a same-hour `v0.2.1` and a correction note to peers +who had already verified against the broken version. diff --git a/persistent-memory.d/2026-09-21-tolerant-writer-over-tolerant-reader.md b/persistent-memory.d/2026-09-21-tolerant-writer-over-tolerant-reader.md new file mode 100644 index 0000000..7b6a1cd --- /dev/null +++ b/persistent-memory.d/2026-09-21-tolerant-writer-over-tolerant-reader.md @@ -0,0 +1,9 @@ +# Letting the write path share the read path's leniency + +_2026-09-21 · booth_ + +**Letting the write path share the read path's leniency.** See the +`MarksCorrupt` decision above. The general shape, worth carrying beyond marks: +a tolerant reader and a tolerant writer over the same state are not the same +decision, and pointing both at one function silently makes them one. Tolerate on +read so the surface still renders; refuse on write so nothing is destroyed. diff --git a/persistent-memory.d/2026-09-21-two-gates-are-complementary.md b/persistent-memory.d/2026-09-21-two-gates-are-complementary.md new file mode 100644 index 0000000..d6d04e0 --- /dev/null +++ b/persistent-memory.d/2026-09-21-two-gates-are-complementary.md @@ -0,0 +1,13 @@ +# Seam review and cold panel had zero overlap, twice + +_2026-09-21 · booth_ + +**The two review gates are complementary, measured on one unit.** +The caller-side **seam review** (nine findings, against the real sibling module +surfaces) and the cold **`/heid-contract-review` panel** (four arms, +artifact-only) had **zero overlap in both directions** on U2. The seam review +found a scope miss the panel structurally could not see: the contract omitted +`inline.py`, whose `place()` indexes by subscript, which a frozen dataclass +refuses. The panel found three code defects and a missing test the seam review +had no lens for. Matches heid's kvasir zero-overlap result on the +conformance-versus-hunt axis. **Run both; neither substitutes.** diff --git a/persistent-memory.d/2026-09-21-u2-marks-landed.md b/persistent-memory.d/2026-09-21-u2-marks-landed.md new file mode 100644 index 0000000..927d1e4 --- /dev/null +++ b/persistent-memory.d/2026-09-21-u2-marks-landed.md @@ -0,0 +1,16 @@ +# U2 (marks) landed — one primitive for three mechanisms + +_2026-09-21 · booth_ + +**U2 (marks) landed.** One primitive replacing three +mechanisms. `pick` / `note` / `flag` in one `.marks.json` per booth, one read +path (`marks_for`), one openness predicate (`open_marks`), rendered beside the +artifact on the tile, at full size in the zoom, and in the panel. `flag` and +`note` had no write path at all before this — the selection loop +(`golden-candidates`, `sindra-finalists`, the `pancake-*` ladders) was running +through chat. 242 tests. Details worth carrying: `asks.py` kept `normalize_ask` +and gained `build_answer` (the 2026-09-09 partial-answer semantics preserved by +moving, not rewriting) and LOST its five sidecar-storage functions; +`GET /b//marks.json` was added because remote sessions polled +`.answer.json` over HTTP and the sidecar's removal would have taken that +capability with it; `/b//asks` 308s to `/marks`. diff --git a/persistent-memory.d/2026-09-21-u2-seam-review-earned-it.md b/persistent-memory.d/2026-09-21-u2-seam-review-earned-it.md new file mode 100644 index 0000000..77f9751 --- /dev/null +++ b/persistent-memory.d/2026-09-21-u2-seam-review-earned-it.md @@ -0,0 +1,17 @@ +# The U2 seam review earned its place, and how + +_2026-09-21 · booth_ + +**The U2 seam review earned its place, and the record should +say how.** Nine findings against the real `booth.asks` / `booth.items` / +`booth.inline` surfaces, two of which changed scope or behaviour: `inline.py` +was missing from `touches` entirely (its `place()` indexes asks by +**subscript**, which a frozen dataclass refuses — nothing else in the service +does that), and the partial-answer inconsistency above. The cold +`/heid-contract-review` pass is artifact-only by design and structurally +cannot see a sibling module, so neither it nor a same-model self-review would +have found either. Two more surfaced later and are worth the same note: a +SECOND subscript in `inline.place` the seam review undercounted, and a +regression in my own legacy importer that a retargeted test caught — a +malformed sidecar that renders `⚠ broken` today would have silently vanished +on migration. diff --git a/persistent-memory.d/2026-09-21-u7-section-premise-half-wrong.md b/persistent-memory.d/2026-09-21-u7-section-premise-half-wrong.md new file mode 100644 index 0000000..b7c72be --- /dev/null +++ b/persistent-memory.d/2026-09-21-u7-section-premise-half-wrong.md @@ -0,0 +1,19 @@ +# U7's section premise is half wrong + +_2026-09-21 · booth_ + +**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-/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--`), 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. diff --git a/persistent-memory.d/2026-09-21-v020-tagged-with-a-gate-in-flight.md b/persistent-memory.d/2026-09-21-v020-tagged-with-a-gate-in-flight.md new file mode 100644 index 0000000..7aa747e --- /dev/null +++ b/persistent-memory.d/2026-09-21-v020-tagged-with-a-gate-in-flight.md @@ -0,0 +1,14 @@ +# v0.2.0 was tagged while a gate was in flight + +_2026-09-21 · booth_ + +**v0.2.0 cut and announced; v0.2.1 fixed what the announcement +was already wrong about.** Operator approved the minor (a v1 unit closed plus a +CLI surface change for 17 consuming handles clears the release-note bar). The +note went to 15 handles — the 17 link-board posters minus `nh3-dev`, a host +label, and `heid`, an oracle that does not script these verbs. Then the +cross-frontier contract panel landed and found **three defects in the code I had +just released**, so `v0.2.1` shipped within the hour. Sequence worth remembering: +the release was correct by the tier bar and still premature by the discipline — +the panel had been dispatched BEFORE implementation and its reply arrived AFTER +the tag. **If a gate is in flight, the tag can wait for it.** diff --git a/persistent-memory.d/2026-09-22-cli-exit-codes.md b/persistent-memory.d/2026-09-22-cli-exit-codes.md new file mode 100644 index 0000000..75f83ce --- /dev/null +++ b/persistent-memory.d/2026-09-22-cli-exit-codes.md @@ -0,0 +1,14 @@ +# `booth marks` / `booth answer` got real exit codes + +_2026-09-22 · booth_ + +**`booth marks` / `booth answer` got real exit codes**, because +a read that CRASHED was indistinguishable from a read that said no. `marks` +printed a traceback and exited 0 (a caller's `jq` saw success and got +nothing); `answer --wait` read a damaged file as "not yet" and spun for the +full hour before blaming the operator. Now `0 ok · 1 unanswered/timed-out · +2 no such pick · 3 unreadable`, and `read_error()` was added to `marks.py` so +the CLI can ask the question the browser must not: the page stays lenient, the +machine consumer gets the truth. Also `--wait` now prints ONCE — it was +emitting a whole JSON document per poll, so a captured `--wait` held several +concatenated values and parsed as none of them. diff --git a/persistent-memory.d/2026-09-22-doctrine-not-defect.md b/persistent-memory.d/2026-09-22-doctrine-not-defect.md new file mode 100644 index 0000000..65bd11f --- /dev/null +++ b/persistent-memory.d/2026-09-22-doctrine-not-defect.md @@ -0,0 +1,13 @@ +# An existing test stopped me retiring documented behaviour + +_2026-09-22 · booth_ + +**An existing test stopped me retiring documented behaviour +while fixing a race.** The mtime-restore race is real, and the clean fix — +ignoring a booth directory's own mtime whenever the booth holds anything — +would also have silently retired the rule that RELEASING a kept board resets +its clock, which the CLI header, the README and a deliberately-written test +all pin. That is a TTL doctrine change, not a bug fix. Fixed the concrete half +(a failing `os.utime` used to escape and 500 the route), left the race stated +in the code. **A fix that changes a documented rule is a proposal, not a +patch.** diff --git a/persistent-memory.d/2026-09-22-lenient-reader-blast-radius.md b/persistent-memory.d/2026-09-22-lenient-reader-blast-radius.md new file mode 100644 index 0000000..1e4cb0c --- /dev/null +++ b/persistent-memory.d/2026-09-22-lenient-reader-blast-radius.md @@ -0,0 +1,15 @@ +# The lenient reader's blast radius was the whole service + +_2026-09-22 · booth_ + +**The lenient reader's blast radius was the whole service, not +one booth.** `_clean_text` did `(text or "").replace(...)` and `marks_for` +sorts on `(created, id)`, so a stored `text` that was a dict or a `created` +that was a number raised out of the READ path — and `list_booths` reads every +booth's marks on every index load. One hand-edited file 500'd `/` and +`/healthz` for all 25 booths. Fixed in two layers, matching the house posture: +a named type check (`_entry_type_error`) plus a `_hydrate_safe` backstop that +cannot raise, and the panel now RENDERS an unreadable mark as ⚠ broken instead +of as an empty note. **The general shape: a lenient reader is only lenient if +the leniency is bounded by where it runs.** `marks_for` was written for one +booth's page and is called in a loop over every booth. diff --git a/persistent-memory.d/2026-09-22-scripts-booth-got-tests.md b/persistent-memory.d/2026-09-22-scripts-booth-got-tests.md new file mode 100644 index 0000000..1619d98 --- /dev/null +++ b/persistent-memory.d/2026-09-22-scripts-booth-got-tests.md @@ -0,0 +1,11 @@ +# `scripts/booth` went from zero tests to five + +_2026-09-22 · booth_ + +**`scripts/booth` had zero tests and now has five** +(`tests/test_cli.py`). The panel's guard-strength tables returned UNVERIFIED +for every CLI claim because nothing in the suite executed the script — two of +the round's findings lived in exactly that gap. The new tests run the real +script under the system `python3`, which makes them a live check on INV-1 +(stdlib-only) as a side effect: a third-party import in `marks.py` now fails +in the suite the same way it would fail on a fleet host. diff --git a/persistent-memory.d/2026-09-22-size-cap-opened-a-hang.md b/persistent-memory.d/2026-09-22-size-cap-opened-a-hang.md new file mode 100644 index 0000000..13e4bd8 --- /dev/null +++ b/persistent-memory.d/2026-09-22-size-cap-opened-a-hang.md @@ -0,0 +1,23 @@ +# The size cap opened a service-wide hang + +_2026-09-22 · booth_ + +**The U5 bug-hunt panel found a service-wide hang that the +SIZE CAP ITSELF opened — two hours after I added the cap.** `stat` reports +size 0 for a FIFO and 0 for a symlink to `/dev/zero`, so both sail under a +byte cap and then `read_text` blocks with no EOF or allocates until the kernel +intervenes. `list_booths` reads every booth on every `GET /`, so ONE such file +stalls the front page for the whole service with no error and no recovery +short of a restart. Reproduced (`timeout` returned 124), fixed with an +`S_ISREG` check BEFORE the size check in both modules, verified live: the +index answered 200 in 36 ms with two FIFOs planted. **The reusable shape: +`st_size` answers a different question than "can this be read", and a bound +that trusts it inherits everything it does not mean — a hardening fix opened +a worse hole than the one it closed.** Also adopted: the upload path wrote the +manifest ABOVE its own cleanup guard (4/4), so a failure orphaned a half-booth +whose uniquely-named leaked temp then kept it alive forever; replace-over- +damaged destroyed recoverable bytes (4/4, now QUARANTINED rather than refused +— marks refuse because judgment is not restatable, a booth's description is); +and `booth answer` spelled out its own openness test, disagreeing with +`booth marks` about a partially-answered pick, which is a direct violation of +U2's INV-2. Full triage in `persistent-memory.d/2026-09-22-u5-panels.md`. diff --git a/persistent-memory.d/2026-09-22-u2-bug-hunt-panel.md b/persistent-memory.d/2026-09-22-u2-bug-hunt-panel.md new file mode 100644 index 0000000..f202acb --- /dev/null +++ b/persistent-memory.d/2026-09-22-u2-bug-hunt-panel.md @@ -0,0 +1,19 @@ +# The U2 bug-hunt panel was not ceremony + +_2026-09-22 · booth_ + +**The U2 bug-hunt panel landed and it was not ceremony — +`v0.2.2`.** Nine adopted findings across four arms; eight were real against +live code and one was already fixed. The headline was **4/4 convergent from +four different angles**: `_Locked.__exit__` unlinked `.marks.lock` on the no-op +path, and `flock` binds to an INODE — so a writer blocked on the old inode +proceeds while the next writer creates a fresh lock file and takes it at once. +Two processes then run the read-modify-write concurrently and the later +`os.replace` drops a mark, with both of them obeying the protocol. **The +cleanup existed to protect the booth's TTL and it was failing at that too**: +creating and removing a directory entry bumps the DIRECTORY's mtime, which is +what `_newest_mtime` actually seeds from, so a no-op reset the clock it was +written to leave alone. Same code region, two defects, one fix — never unlink +the lock, exempt `..lock` dotfiles from `_newest_mtime`, and put the +directory's mtime back after creating one. Full triage in +`persistent-memory.d/2026-09-22-bug-hunt-panel.md`. diff --git a/persistent-memory.d/2026-09-22-u5-adoption-split-in-two.md b/persistent-memory.d/2026-09-22-u5-adoption-split-in-two.md new file mode 100644 index 0000000..3077a6b --- /dev/null +++ b/persistent-memory.d/2026-09-22-u5-adoption-split-in-two.md @@ -0,0 +1,23 @@ +# U5's adoption prediction split in two + +_2026-09-22 · booth_ + +**U5's adoption prediction, SPLIT IN TWO within an hour of +landing — and the split is the interesting part.** The baseline was recorded as +0 of 26. Fifty minutes after the deploy, `comfy-dev` created `muse-clothed-repro` +and it announced itself: `{handle: comfy-dev, why: "", created: ...}`. That peer +was told nothing. **The HANDLE propagates for free** — it rides on `booth new` +and `booth add`, so every existing CLI caller starts announcing without learning +anything, which is the flags-on-existing-verbs decision paying off on day zero. +**The WHY does not** — it needs someone to know the flag exists, and this first +one is empty. + +So re-measure BOTH on **2026-09-29**, because they answer different questions: + + find ~/booth-data -maxdepth 2 -name .booth.json | wc -l # free + grep -l '"why": "[^"]' ~/booth-data/*/.booth.json 2>/dev/null | wc -l # learned + +A high first count and a near-zero second is the predicted shape of "nobody was +told", and it is the case the operator's no-announcement decision was designed +to be able to see. Do not read the n=1 above as a rate — it is a code-path +observation (every CLI caller writes a handle), not a sample. diff --git a/persistent-memory.d/2026-09-22-u5-panels-reached-a-released-bug.md b/persistent-memory.d/2026-09-22-u5-panels-reached-a-released-bug.md new file mode 100644 index 0000000..aa8d30c --- /dev/null +++ b/persistent-memory.d/2026-09-22-u5-panels-reached-a-released-bug.md @@ -0,0 +1,21 @@ +# Two U5 panels, and prose reached a released outage + +_2026-09-22 · booth_ + +**Two cross-frontier panels on U5, and a paraphrase panel reached +a production outage two modules away.** 3-of-4 flagged the contract's "4 GB" +case as letter-compliant but purpose-defeating; the conformance round found that +unbounded read live in U5's code; walking it to the sibling found the SAME hole +**live in released `v0.2.2`** — `marks._read_raw` catches `(OSError, ValueError, +UnicodeDecodeError)` and `json.loads` on deep nesting raises **RecursionError**, +which is none of them, so 400 KB of brackets in one booth returned 500 for `/` +and `/healthz` across all 26. The v0.2.2 round HAD flagged it and I closed half: +**a finding with two call sites is not closed when one is.** The reusable +instruction — **walk a conformance finding to the sibling module even when the +sibling is out of scope.** Five of ten conformance findings were tests of mine +that pass on the regression they exist to catch, three of them asserting an +ARTIFACT of the property rather than the property; that is three nights running +on the same shape. Two real bugs neither my tests nor I could see: a bare +`booth add` wiped the `why` on the one sequence the feature exists for, and +`--title` was write-only. Full triage in +`persistent-memory.d/2026-09-22-u5-panels.md`. diff --git a/persistent-memory.md b/persistent-memory.md index 557e872..0d09d9a 100644 --- a/persistent-memory.md +++ b/persistent-memory.md @@ -21,352 +21,73 @@ _As of 2026-09-22:_ - **v1 is gated on seven units** in `ROADMAP.md`, dependency-ordered **U1 → U2 → {U3, U4, U5} → U7**, with **U6 independent**. -- **U1 and U2 are landed and released.** Current version `0.2.2`, deployed to the - live service, 275 tests green, tree clean, 25/25 booth pages verified 200 after - the deploy. U1 `ce598b3`; U2 `c7f9437` released as `v0.2.0`, then `5e41108` as - `v0.2.1` (four contract-panel findings), then `v0.2.2` carrying the - **bug-hunt panel's** nine (below). -- **U5 is IMPLEMENTED and unreleased** as of 2026-09-22. `booth/manifest.py` - (stdlib-only, INV-1), `.booth.json` per booth, the provenance line on both - index lanes and the booth page header, `--why` / `--title` on `booth new` and - `booth add`, and the link board + pickup booths announcing themselves as the - service's own. 310 tests, live service restarted, 26/26 booth pages verified - 200 and all 26 rendering `unannounced`. **Deliberately NOT tagged yet**: the - cold `/heid-contract-review` panel is still in flight and the code-review and - bug-hunt gates have not run. That ordering is the 2026-09-21 lesson applied — - a release whose gate is outstanding is premature even when the tier is right. - Contract: `docs/contracts/u5_booth_manifest.contract.md` (carries its own - seam-review section). -- **U5's original framing** (operator, 2026-09-21): **self-announcing booths.** - `.booth.json` carrying `{handle, title, why, created}`, written by the CLI from - `$ALTHING_HANDLE`; the index card gains provenance and a one-line purpose, and - the index becomes the "what landed" feed the link board was being used as. It - closes job 5 of the five jobs — the one nobody named, and the reason 145 dead - link rows existed. Nothing started: no contract, no blast-radius pass. -- **Two things about U5 are already settled and should not be re-derived.** - (1) `.booth.json` is a DOTFILE, so `booth_items`' existing `startswith(".")` skip - already keeps it out of tiles, counts and zips — the same reason `.marks.json` - needed no new exclusion rule. (2) The deterministic-order invariant applies to - whatever U5 adds to the index; the index is ordered newest-first by mtime today - and that rule must stay stated. Also worth knowing before scoping: enforcing the - link rule without giving job 5 a home first just makes it homeless — that is the - lesson from the 69% rot, and U5 is the home. -- **No heid dispatch is outstanding.** The `/heid-bug-hunt` on U2's diff landed - 2026-09-22 and shipped as `v0.2.2`; see the dated entry below. -- Live service `active` on `:8090`, 25 booths, verified 25 × 3 page types after the - last deploy. The booth set churns: `sindra20-engines` and `sindra-finalists` were - swept during the session, `cr123a-to-d-sleeve` and `sindra` appeared. +- **U1, U2 and U5 are landed and released.** Current version `0.3.0` (`95beede`, + tagged), deployed, 341 tests green, tree clean, 24/24 booth pages verified 200 + and a full layout-probe sweep at 0 occlusions across 27 pages. U1 `ce598b3`; + U2 `c7f9437` → `v0.2.0`, `5e41108` → `v0.2.1`, `026a1fc` → `v0.2.2`; + U5 `c015a91` + `95beede` → `v0.3.0`. +- **THE NEXT UNIT IS THE OPERATOR'S CALL and has not been made.** The session's + recommendation is **U4 (derived lifetime)**, on three grounds, none of which + need re-deriving: (1) it is the only remaining unit whose defect was + re-measured this session — the `.forever` rate is **17 of 24 (70%)**, up from + the 54% in the IA doc and the 56% measured on 2026-09-21, and the absolute + count rose too (14 → 17), so it is not only the denominator shrinking; + (2) U2 built `open_marks` FOR it and says so in the predicate's docstring; + (3) the bug-hunt panel's mtime-restore finding was deliberately left open + because closing it properly is a TTL doctrine change — and U4 is the unit that + owns TTL doctrine, so it closes a loop this session opened on purpose. + U3 and U6 are the alternatives: U6 is independent and was *conceptually* + unblocked by U5 giving job 5 a home, U3 is where verbatim-booth provenance was + deferred to. +- **No gate is outstanding.** All three ran on U5 and were folded in: the cold + `/heid-contract-review` panel (thread `01M340PNVRS21HPASZT38PXQPN`), the + `/heid-code-review` conformance panel (`01M341E9XAPZEFBSPK9HPGAM0S`) and the + `/heid-bug-hunt` (`01M343SXX27Z47C3STXXRC7M42`). All three loops are closed + with heid. +- **Two dated predictions are pending and must not be forgotten.** U5's adoption + re-measure on **2026-09-29** (two counts, see its entry — already at 3 of 24 + announced and 2 with a `why`, all from peers told nothing), and the `.forever` + re-count **a fortnight after U4 lands**, which is U4's success criterion. +- **Three methodology proposals from this session sit with the operator**, routed + by heid rather than decided unilaterally: reshaping the paraphrase gate toward + a drift-check for narrative-heavy contracts, a standing + "green-tests-prove-nothing" direction for the code-review gate, and regin's + table-vs-signature consistency pass. They are changes to the `/heid*` skills, + not to this repo. +- The booth set churns hard: 26 → 24 during this session as the sweeper ran. + Re-count rather than trusting any number written here. ## Recent decisions -- `[2026-09-22]` **The U5 bug-hunt panel found a service-wide hang that the - SIZE CAP ITSELF opened — two hours after I added the cap.** `stat` reports - size 0 for a FIFO and 0 for a symlink to `/dev/zero`, so both sail under a - byte cap and then `read_text` blocks with no EOF or allocates until the kernel - intervenes. `list_booths` reads every booth on every `GET /`, so ONE such file - stalls the front page for the whole service with no error and no recovery - short of a restart. Reproduced (`timeout` returned 124), fixed with an - `S_ISREG` check BEFORE the size check in both modules, verified live: the - index answered 200 in 36 ms with two FIFOs planted. **The reusable shape: - `st_size` answers a different question than "can this be read", and a bound - that trusts it inherits everything it does not mean — a hardening fix opened - a worse hole than the one it closed.** Also adopted: the upload path wrote the - manifest ABOVE its own cleanup guard (4/4), so a failure orphaned a half-booth - whose uniquely-named leaked temp then kept it alive forever; replace-over- - damaged destroyed recoverable bytes (4/4, now QUARANTINED rather than refused - — marks refuse because judgment is not restatable, a booth's description is); - and `booth answer` spelled out its own openness test, disagreeing with - `booth marks` about a partially-answered pick, which is a direct violation of - U2's INV-2. Full triage in `persistent-memory.d/2026-09-22-u5-panels.md`. -- `[2026-09-22]` **An existing test stopped me retiring documented behaviour - while fixing a race.** The mtime-restore race is real, and the clean fix — - ignoring a booth directory's own mtime whenever the booth holds anything — - would also have silently retired the rule that RELEASING a kept board resets - its clock, which the CLI header, the README and a deliberately-written test - all pin. That is a TTL doctrine change, not a bug fix. Fixed the concrete half - (a failing `os.utime` used to escape and 500 the route), left the race stated - in the code. **A fix that changes a documented rule is a proposal, not a - patch.** -- `[2026-09-22]` **Two cross-frontier panels on U5, and a paraphrase panel reached - a production outage two modules away.** 3-of-4 flagged the contract's "4 GB" - case as letter-compliant but purpose-defeating; the conformance round found that - unbounded read live in U5's code; walking it to the sibling found the SAME hole - **live in released `v0.2.2`** — `marks._read_raw` catches `(OSError, ValueError, - UnicodeDecodeError)` and `json.loads` on deep nesting raises **RecursionError**, - which is none of them, so 400 KB of brackets in one booth returned 500 for `/` - and `/healthz` across all 26. The v0.2.2 round HAD flagged it and I closed half: - **a finding with two call sites is not closed when one is.** The reusable - instruction — **walk a conformance finding to the sibling module even when the - sibling is out of scope.** Five of ten conformance findings were tests of mine - that pass on the regression they exist to catch, three of them asserting an - ARTIFACT of the property rather than the property; that is three nights running - on the same shape. Two real bugs neither my tests nor I could see: a bare - `booth add` wiped the `why` on the one sequence the feature exists for, and - `--title` was write-only. Full triage in - `persistent-memory.d/2026-09-22-u5-panels.md`. -- `[2026-09-22]` **U5's adoption prediction, SPLIT IN TWO within an hour of - landing — and the split is the interesting part.** The baseline was recorded as - 0 of 26. Fifty minutes after the deploy, `comfy-dev` created `muse-clothed-repro` - and it announced itself: `{handle: comfy-dev, why: "", created: ...}`. That peer - was told nothing. **The HANDLE propagates for free** — it rides on `booth new` - and `booth add`, so every existing CLI caller starts announcing without learning - anything, which is the flags-on-existing-verbs decision paying off on day zero. - **The WHY does not** — it needs someone to know the flag exists, and this first - one is empty. - - So re-measure BOTH on **2026-09-29**, because they answer different questions: - - find ~/booth-data -maxdepth 2 -name .booth.json | wc -l # free - grep -l '"why": "[^"]' ~/booth-data/*/.booth.json 2>/dev/null | wc -l # learned - - A high first count and a near-zero second is the predicted shape of "nobody was - told", and it is the case the operator's no-announcement decision was designed - to be able to see. Do not read the n=1 above as a rate — it is a code-path - observation (every CLI caller writes a handle), not a sample. -- `[2026-09-22]` **The U2 bug-hunt panel landed and it was not ceremony — - `v0.2.2`.** Nine adopted findings across four arms; eight were real against - live code and one was already fixed. The headline was **4/4 convergent from - four different angles**: `_Locked.__exit__` unlinked `.marks.lock` on the no-op - path, and `flock` binds to an INODE — so a writer blocked on the old inode - proceeds while the next writer creates a fresh lock file and takes it at once. - Two processes then run the read-modify-write concurrently and the later - `os.replace` drops a mark, with both of them obeying the protocol. **The - cleanup existed to protect the booth's TTL and it was failing at that too**: - creating and removing a directory entry bumps the DIRECTORY's mtime, which is - what `_newest_mtime` actually seeds from, so a no-op reset the clock it was - written to leave alone. Same code region, two defects, one fix — never unlink - the lock, exempt `..lock` dotfiles from `_newest_mtime`, and put the - directory's mtime back after creating one. Full triage in - `persistent-memory.d/2026-09-22-bug-hunt-panel.md`. -- `[2026-09-22]` **The lenient reader's blast radius was the whole service, not - one booth.** `_clean_text` did `(text or "").replace(...)` and `marks_for` - sorts on `(created, id)`, so a stored `text` that was a dict or a `created` - that was a number raised out of the READ path — and `list_booths` reads every - booth's marks on every index load. One hand-edited file 500'd `/` and - `/healthz` for all 25 booths. Fixed in two layers, matching the house posture: - a named type check (`_entry_type_error`) plus a `_hydrate_safe` backstop that - cannot raise, and the panel now RENDERS an unreadable mark as ⚠ broken instead - of as an empty note. **The general shape: a lenient reader is only lenient if - the leniency is bounded by where it runs.** `marks_for` was written for one - booth's page and is called in a loop over every booth. -- `[2026-09-22]` **`booth marks` / `booth answer` got real exit codes**, because - a read that CRASHED was indistinguishable from a read that said no. `marks` - printed a traceback and exited 0 (a caller's `jq` saw success and got - nothing); `answer --wait` read a damaged file as "not yet" and spun for the - full hour before blaming the operator. Now `0 ok · 1 unanswered/timed-out · - 2 no such pick · 3 unreadable`, and `read_error()` was added to `marks.py` so - the CLI can ask the question the browser must not: the page stays lenient, the - machine consumer gets the truth. Also `--wait` now prints ONCE — it was - emitting a whole JSON document per poll, so a captured `--wait` held several - concatenated values and parsed as none of them. -- `[2026-09-22]` **`scripts/booth` had zero tests and now has five** - (`tests/test_cli.py`). The panel's guard-strength tables returned UNVERIFIED - for every CLI claim because nothing in the suite executed the script — two of - the round's findings lived in exactly that gap. The new tests run the real - script under the system `python3`, which makes them a live check on INV-1 - (stdlib-only) as a side effect: a third-party import in `marks.py` now fails - in the suite the same way it would fail on a fleet host. - -- `[2026-09-21]` **v0.2.0 cut and announced; v0.2.1 fixed what the announcement - was already wrong about.** Operator approved the minor (a v1 unit closed plus a - CLI surface change for 17 consuming handles clears the release-note bar). The - note went to 15 handles — the 17 link-board posters minus `nh3-dev`, a host - label, and `heid`, an oracle that does not script these verbs. Then the - cross-frontier contract panel landed and found **three defects in the code I had - just released**, so `v0.2.1` shipped within the hour. Sequence worth remembering: - the release was correct by the tier bar and still premature by the discipline — - the panel had been dispatched BEFORE implementation and its reply arrived AFTER - the tag. **If a gate is in flight, the tag can wait for it.** -- `[2026-09-21]` **A write over a damaged `.marks.json` was wiping every mark in - the booth.** Shipped in `v0.2.0`, found by the panel (Kimi, converged with - Hulda), fixed in `v0.2.1`. `marks_for` is deliberately lenient — unparseable - reads as `[]` so a review page still loads — and the write path inherited that - leniency through the same reader, so one flag click appended to an empty list and - atomically replaced the file. The fix is an **asymmetry**, which is the reusable - part: reads stay lenient, writes go strict (`MarksCorrupt`), damaged bytes stay - on disk, routes answer 409 not 500. A page that renders without an annotation is - recoverable; a file that overwrote the operator's judgment is not. Kimi also - named the class correctly — "an author steeped in the design conversation would - likely read past" it — and that was accurate. -- `[2026-09-21]` **The two review gates are complementary, measured on one unit.** - The caller-side **seam review** (nine findings, against the real sibling module - surfaces) and the cold **`/heid-contract-review` panel** (four arms, - artifact-only) had **zero overlap in both directions** on U2. The seam review - found a scope miss the panel structurally could not see: the contract omitted - `inline.py`, whose `place()` indexes by subscript, which a frozen dataclass - refuses. The panel found three code defects and a missing test the seam review - had no lens for. Matches heid's kvasir zero-overlap result on the - conformance-versus-hunt axis. **Run both; neither substitutes.** -- `[2026-09-21]` **Every one of the panel's code-changing findings came from the - AMBIGUITY pass, none from a paraphrase divergence** — and two arms independently - proposed cutting the paraphrase to a drift-check for narrative-heavy contracts, - because this contract's own frontmatter carries a plain-language narrative and the - paraphrase was partly reading my framing back to me. That is a finding about the - `/heid-contract-review` **skill**, not about this repo, and it was reported back - to heid. Recorded here only so a future session does not rediscover it. -- `[2026-09-21]` **Deterministic order is a cross-cutting v1 invariant** — - operator directive, mid-implementation. Every ordered collection the Booth - renders must have a *stated* rule producing the same sequence on every render - of the same state; the rule can be anything defensible (byte order, time, an - explicit number, an arbitrary-but-recorded sequence), but no rule at all is - forbidden. It binds harder here than elsewhere because the Booth's job is - **comparison** — the operator judges tile 47 against tile 47 and refers to - artifacts positionally, so an order that moves between renders misfiles a flag - or a note rather than crashing. Recorded as `ROADMAP.md` § "Cross-cutting - invariant" (with the per-collection table) and `CLAUDE.md` invariant 6, and - tested. Still undecided and must be settled before those units ship: **U7's - section ordering and compare pairing**, and **U6's bench listing**. -- `[2026-09-21]` **U2 (marks) landed.** One primitive replacing three - mechanisms. `pick` / `note` / `flag` in one `.marks.json` per booth, one read - path (`marks_for`), one openness predicate (`open_marks`), rendered beside the - artifact on the tile, at full size in the zoom, and in the panel. `flag` and - `note` had no write path at all before this — the selection loop - (`golden-candidates`, `sindra-finalists`, the `pancake-*` ladders) was running - through chat. 242 tests. Details worth carrying: `asks.py` kept `normalize_ask` - and gained `build_answer` (the 2026-09-09 partial-answer semantics preserved by - moving, not rewriting) and LOST its five sidecar-storage functions; - `GET /b//marks.json` was added because remote sessions polled - `.answer.json` over HTTP and the sidecar's removal would have taken that - capability with it; `/b//asks` 308s to `/marks`. -- `[2026-09-21]` **A partially-answered pick now counts as OPEN** — declared, not - smuggled. The old index badge tested `answer is None`, so a half-answered - four-question ask read as closed on the index while the panel beside it - rendered `◐ partial`: the two disagreed about the same booth. Open is the - reading that makes U4 correct — a lifetime rule that unpinned a booth on the - first radio click would sweep a review in flight. -- `[2026-09-21]` **The U2 seam review earned its place, and the record should - say how.** Nine findings against the real `booth.asks` / `booth.items` / - `booth.inline` surfaces, two of which changed scope or behaviour: `inline.py` - was missing from `touches` entirely (its `place()` indexes asks by - **subscript**, which a frozen dataclass refuses — nothing else in the service - does that), and the partial-answer inconsistency above. The cold - `/heid-contract-review` pass is artifact-only by design and structurally - cannot see a sibling module, so neither it nor a same-model self-review would - have found either. Two more surfaced later and are worth the same note: a - SECOND subscript in `inline.place` the seam review undercounted, and a - regression in my own legacy importer that a retargeted test caught — a - malformed sidecar that renders `⚠ broken` today would have silently vanished - on migration. -- `[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 (`.marks.json`) and extending the existing `.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-/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--`), 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. +- `[2026-09-22]` **The size cap opened a service-wide hang** — a FIFO has st_size 0; a bound that trusts it inherits what it does not mean → `persistent-memory.d/2026-09-22-size-cap-opened-a-hang.md` +- `[2026-09-22]` **An existing test stopped me retiring documented behaviour** — the clean fix for the mtime race would have silently changed TTL doctrine → `persistent-memory.d/2026-09-22-doctrine-not-defect.md` +- `[2026-09-22]` **Two U5 panels, and prose reached a released outage** — read the detail before assuming a conformance finding stops at its own module → `persistent-memory.d/2026-09-22-u5-panels-reached-a-released-bug.md` +- `[2026-09-22]` **U5's adoption prediction split in two** — the handle rides for free, the why must be learned — RE-MEASURE 2026-09-29 → `persistent-memory.d/2026-09-22-u5-adoption-split-in-two.md` +- `[2026-09-22]` **The U2 bug-hunt panel was not ceremony** — the lock-unlink race and the TTL guard that was failing at its own job → `persistent-memory.d/2026-09-22-u2-bug-hunt-panel.md` +- `[2026-09-22]` **The lenient reader's blast radius was the whole service** — marks_for runs per booth per index load; a raise there is an outage → `persistent-memory.d/2026-09-22-lenient-reader-blast-radius.md` +- `[2026-09-22]` **`booth marks` / `booth answer` got real exit codes** — read it before changing anything the 17 consuming handles call → `persistent-memory.d/2026-09-22-cli-exit-codes.md` +- `[2026-09-22]` **`scripts/booth` went from zero tests to five** — they run the real script under system python3, so they also check INV-1 → `persistent-memory.d/2026-09-22-scripts-booth-got-tests.md` +- `[2026-09-21]` **v0.2.0 was tagged while a gate was in flight** — the sequencing lesson: if a gate is outstanding, the tag waits → `persistent-memory.d/2026-09-21-v020-tagged-with-a-gate-in-flight.md` +- `[2026-09-21]` **A write over a damaged `.marks.json` wiped the booth** — the reads-lenient / writes-strict asymmetry, and why it exists → `persistent-memory.d/2026-09-21-marks-write-wiped-judgment.md` +- `[2026-09-21]` **Seam review and cold panel had zero overlap, twice** — evidence for running both; neither substitutes for the other → `persistent-memory.d/2026-09-21-two-gates-are-complementary.md` +- `[2026-09-21]` **Every code-changing finding came from the AMBIGUITY pass** — a finding about the /heid-contract-review skill, not about this repo → `persistent-memory.d/2026-09-21-ambiguity-pass-did-the-work.md` +- `[2026-09-21]` **Deterministic order is a cross-cutting v1 invariant** — operator directive; read before adding ANY ordered surface → `persistent-memory.d/2026-09-21-deterministic-order-invariant.md` +- `[2026-09-21]` **U2 (marks) landed — one primitive for three mechanisms** — what moved where, and the HTTP mirror remote sessions poll → `persistent-memory.d/2026-09-21-u2-marks-landed.md` +- `[2026-09-21]` **A partially-answered pick counts as OPEN** — declared, not smuggled; it is the reading that makes U4 correct → `persistent-memory.d/2026-09-21-partial-answer-counts-as-open.md` +- `[2026-09-21]` **The U2 seam review earned its place, and how** — inline.place indexes by subscript — the miss a cold panel cannot see → `persistent-memory.d/2026-09-21-u2-seam-review-earned-it.md` +- `[2026-09-21]` **Marks are one `.marks.json` per booth** — operator decision with two rejected alternatives; read before restructuring → `persistent-memory.d/2026-09-21-marks-storage-decision.md` +- `[2026-09-21]` **U7's section premise is half wrong** — every booth that needs navigation is FLAT — read before starting U7 → `persistent-memory.d/2026-09-21-u7-section-premise-half-wrong.md` +- `[2026-09-21]` **`sindra-finalists` is U2's flag motivation, caught live** — evidence, not argument → `persistent-memory.d/2026-09-21-sindra-finalists-is-the-motivation.md` +- `[2026-09-21]` **The information architecture and the v1 gate landed** — the single defect the seven units decompose → `persistent-memory.d/2026-09-21-ia-and-v1-gate-landed.md` +- `[2026-09-21]` **The `.forever` diagnosis is a falsifiable prediction** — U4's success criterion — re-count a fortnight AFTER U4 lands → `persistent-memory.d/2026-09-21-forever-diagnosis-is-a-prediction.md` +- `[2026-09-21]` **Extracted from `eshpfi` into its own repo** — test_booth.py is the regression net the v1 rewrite is checked against → `persistent-memory.d/2026-09-21-extracted-from-eshpfi.md` ## Tried and abandoned -- `[2026-09-21]` **Tagging a release while a review gate was still in flight.** - `v0.2.0` was cut and announced to 15 consuming handles; the - `/heid-contract-review` panel — dispatched BEFORE implementation, as the - discipline says — replied afterwards with three defects in the code that had just - shipped, one of them silent data loss. Nothing about the tier decision was wrong; - the *timing* was. **If a gate is outstanding on the work being released, the tag - waits for it.** The cost was a same-hour `v0.2.1` and a correction note to peers - who had already verified against the broken version. -- `[2026-09-21]` **Letting the write path share the read path's leniency.** See the - `MarksCorrupt` decision above. The general shape, worth carrying beyond marks: - a tolerant reader and a tolerant writer over the same state are not the same - decision, and pointing both at one function silently makes them one. Tolerate on - read so the surface still renders; refuse on write so nothing is destroyed. -- `[2026-09-21]` **Letting Jinja hot-reload templates while the repo is the - deployment root** — the cause of a live outage the same day U2 landed, and the - sharpest foot-gun in the repo. `booth.service` sets `WorkingDirectory` to this - repo, so the running service imports these files with no build step and no - staging copy. Python is read once at process start; Jinja's `FileSystemLoader` - re-reads a template **on every render**. Editing `booth.html` therefore - deployed it instantly against Python from 22:03 that knew nothing about - `item_marks`, and **19 of 25 live booths returned 500** with - `UndefinedError: 'item_marks' is undefined`. Neither the old code nor the new - code was broken — the service was running both at once. - **The lesson that generalises:** a skew between a process and the disk under it - is invisible to the test suite by construction, so no amount of green tests - would have caught it; the operator found it. Fixed at the source rather than - with a reminder — the `Environment` is hand-built with `auto_reload=False`, so - there is now ONE staleness rule (nothing takes effect until you restart) and - the running process is always a coherent snapshot of one commit. Asserted by - `test_templates_do_not_hot_reload_from_disk`. Watch the second-order risk the - fix introduces: a hand-built `Environment` does not inherit `autoescape` from - the `Jinja2Templates` constructor, and booth names, item names and mark text - are all agent-authored strings landing in HTML. - -- `[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//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. +- `[2026-09-21]` **Tagging a release while a review gate was in flight** — cost a same-hour v0.2.1 and a correction to 15 handles → `persistent-memory.d/2026-09-21-tagging-with-a-gate-in-flight.md` +- `[2026-09-21]` **Letting the write path share the read path's leniency** — a tolerant reader and a tolerant writer are not the same decision → `persistent-memory.d/2026-09-21-tolerant-writer-over-tolerant-reader.md` +- `[2026-09-21]` **Letting Jinja hot-reload templates in the deployment root** — caused a live outage: 19 of 25 booths at 500. Why auto_reload=False → `persistent-memory.d/2026-09-21-jinja-hot-reload-outage.md` +- `[2026-09-21]` **Five mechanisms to get one question beside one artifact** — the accretion signature this whole v1 rewrite is undoing → `persistent-memory.d/2026-09-21-five-mechanisms-one-job.md` +- `[2026-09-21]` **Regex-injecting chrome into arbitrary author HTML** — the defect U3 exists to close → `persistent-memory.d/2026-09-21-regex-injecting-chrome.md` +- `[2026-09-21]` **A boolean escape hatch as the lifetime mechanism** — why `.forever` is a symptom; the defect U4 exists to close → `persistent-memory.d/2026-09-21-boolean-escape-hatch-as-lifetime.md` +- `[2026-09-21]` **Letting the link board absorb the announce job** — 69% rot; U5 gave the job a home, which is what unblocks U6 → `persistent-memory.d/2026-09-21-link-board-absorbing-announce.md`