memory: snapshot — U5 released at v0.3.0, and the index goes two-tier
The two dated log sections had never been split, so every one of their 29 entries sat inline and the startup index had grown to 372 lines — which is the cost the two-tier scheme exists to remove, paid on every session that reads the file. 27 entries were over threshold. All 29 now have a detail file under persistent-memory.d/ and a one-line index entry that routes rather than restates. Index: 372 -> 93 lines. No archival. The soft cap fired, but every entry in this repo is dated 2026-09-21 or later, so the under-14-days guard held all of them back — and the split alone took the index well under the target without moving anything out of the active file. The in-flight section is rewritten for the post-release state: nothing is in flight, no gate is outstanding, and the next unit is explicitly recorded as the operator's undecided call rather than as a plan. The session's recommendation (U4, on three grounds) is written down so it does not have to be re-derived, alongside the two alternatives and why they are alternatives. Two dated predictions are carried forward with their dates and their instruments: the U5 adoption re-measure on 2026-09-29, which already reads 3 of 24 announced and 2 with a why from peers told nothing, and the .forever re-count a fortnight AFTER U4 lands, which is U4's own success criterion and is destroyed by running it early.
This commit is contained in:
@@ -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.
|
||||||
@@ -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").
|
||||||
@@ -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**.
|
||||||
@@ -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.
|
||||||
@@ -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/<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.
|
||||||
@@ -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.
|
||||||
@@ -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.
|
||||||
@@ -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.
|
||||||
@@ -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.
|
||||||
@@ -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 (`<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.
|
||||||
@@ -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.
|
||||||
@@ -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.
|
||||||
@@ -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.
|
||||||
@@ -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.
|
||||||
@@ -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.
|
||||||
@@ -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.
|
||||||
@@ -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.**
|
||||||
@@ -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/<n>/marks.json` was added because remote sessions polled
|
||||||
|
`<stem>.answer.json` over HTTP and the sidecar's removal would have taken that
|
||||||
|
capability with it; `/b/<n>/asks` 308s to `/marks`.
|
||||||
@@ -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.
|
||||||
@@ -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-<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.
|
||||||
@@ -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.**
|
||||||
@@ -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.
|
||||||
@@ -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.**
|
||||||
@@ -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.
|
||||||
@@ -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.
|
||||||
@@ -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`.
|
||||||
@@ -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 `.<name>.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`.
|
||||||
@@ -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.
|
||||||
@@ -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`.
|
||||||
+64
-343
@@ -21,352 +21,73 @@ _As of 2026-09-22:_
|
|||||||
|
|
||||||
- **v1 is gated on seven units** in `ROADMAP.md`, dependency-ordered
|
- **v1 is gated on seven units** in `ROADMAP.md`, dependency-ordered
|
||||||
**U1 → U2 → {U3, U4, U5} → U7**, with **U6 independent**.
|
**U1 → U2 → {U3, U4, U5} → U7**, with **U6 independent**.
|
||||||
- **U1 and U2 are landed and released.** Current version `0.2.2`, deployed to the
|
- **U1, U2 and U5 are landed and released.** Current version `0.3.0` (`95beede`,
|
||||||
live service, 275 tests green, tree clean, 25/25 booth pages verified 200 after
|
tagged), deployed, 341 tests green, tree clean, 24/24 booth pages verified 200
|
||||||
the deploy. U1 `ce598b3`; U2 `c7f9437` released as `v0.2.0`, then `5e41108` as
|
and a full layout-probe sweep at 0 occlusions across 27 pages. U1 `ce598b3`;
|
||||||
`v0.2.1` (four contract-panel findings), then `v0.2.2` carrying the
|
U2 `c7f9437` → `v0.2.0`, `5e41108` → `v0.2.1`, `026a1fc` → `v0.2.2`;
|
||||||
**bug-hunt panel's** nine (below).
|
U5 `c015a91` + `95beede` → `v0.3.0`.
|
||||||
- **U5 is IMPLEMENTED and unreleased** as of 2026-09-22. `booth/manifest.py`
|
- **THE NEXT UNIT IS THE OPERATOR'S CALL and has not been made.** The session's
|
||||||
(stdlib-only, INV-1), `.booth.json` per booth, the provenance line on both
|
recommendation is **U4 (derived lifetime)**, on three grounds, none of which
|
||||||
index lanes and the booth page header, `--why` / `--title` on `booth new` and
|
need re-deriving: (1) it is the only remaining unit whose defect was
|
||||||
`booth add`, and the link board + pickup booths announcing themselves as the
|
re-measured this session — the `.forever` rate is **17 of 24 (70%)**, up from
|
||||||
service's own. 310 tests, live service restarted, 26/26 booth pages verified
|
the 54% in the IA doc and the 56% measured on 2026-09-21, and the absolute
|
||||||
200 and all 26 rendering `unannounced`. **Deliberately NOT tagged yet**: the
|
count rose too (14 → 17), so it is not only the denominator shrinking;
|
||||||
cold `/heid-contract-review` panel is still in flight and the code-review and
|
(2) U2 built `open_marks` FOR it and says so in the predicate's docstring;
|
||||||
bug-hunt gates have not run. That ordering is the 2026-09-21 lesson applied —
|
(3) the bug-hunt panel's mtime-restore finding was deliberately left open
|
||||||
a release whose gate is outstanding is premature even when the tier is right.
|
because closing it properly is a TTL doctrine change — and U4 is the unit that
|
||||||
Contract: `docs/contracts/u5_booth_manifest.contract.md` (carries its own
|
owns TTL doctrine, so it closes a loop this session opened on purpose.
|
||||||
seam-review section).
|
U3 and U6 are the alternatives: U6 is independent and was *conceptually*
|
||||||
- **U5's original framing** (operator, 2026-09-21): **self-announcing booths.**
|
unblocked by U5 giving job 5 a home, U3 is where verbatim-booth provenance was
|
||||||
`.booth.json` carrying `{handle, title, why, created}`, written by the CLI from
|
deferred to.
|
||||||
`$ALTHING_HANDLE`; the index card gains provenance and a one-line purpose, and
|
- **No gate is outstanding.** All three ran on U5 and were folded in: the cold
|
||||||
the index becomes the "what landed" feed the link board was being used as. It
|
`/heid-contract-review` panel (thread `01M340PNVRS21HPASZT38PXQPN`), the
|
||||||
closes job 5 of the five jobs — the one nobody named, and the reason 145 dead
|
`/heid-code-review` conformance panel (`01M341E9XAPZEFBSPK9HPGAM0S`) and the
|
||||||
link rows existed. Nothing started: no contract, no blast-radius pass.
|
`/heid-bug-hunt` (`01M343SXX27Z47C3STXXRC7M42`). All three loops are closed
|
||||||
- **Two things about U5 are already settled and should not be re-derived.**
|
with heid.
|
||||||
(1) `.booth.json` is a DOTFILE, so `booth_items`' existing `startswith(".")` skip
|
- **Two dated predictions are pending and must not be forgotten.** U5's adoption
|
||||||
already keeps it out of tiles, counts and zips — the same reason `.marks.json`
|
re-measure on **2026-09-29** (two counts, see its entry — already at 3 of 24
|
||||||
needed no new exclusion rule. (2) The deterministic-order invariant applies to
|
announced and 2 with a `why`, all from peers told nothing), and the `.forever`
|
||||||
whatever U5 adds to the index; the index is ordered newest-first by mtime today
|
re-count **a fortnight after U4 lands**, which is U4's success criterion.
|
||||||
and that rule must stay stated. Also worth knowing before scoping: enforcing the
|
- **Three methodology proposals from this session sit with the operator**, routed
|
||||||
link rule without giving job 5 a home first just makes it homeless — that is the
|
by heid rather than decided unilaterally: reshaping the paraphrase gate toward
|
||||||
lesson from the 69% rot, and U5 is the home.
|
a drift-check for narrative-heavy contracts, a standing
|
||||||
- **No heid dispatch is outstanding.** The `/heid-bug-hunt` on U2's diff landed
|
"green-tests-prove-nothing" direction for the code-review gate, and regin's
|
||||||
2026-09-22 and shipped as `v0.2.2`; see the dated entry below.
|
table-vs-signature consistency pass. They are changes to the `/heid*` skills,
|
||||||
- Live service `active` on `:8090`, 25 booths, verified 25 × 3 page types after the
|
not to this repo.
|
||||||
last deploy. The booth set churns: `sindra20-engines` and `sindra-finalists` were
|
- The booth set churns hard: 26 → 24 during this session as the sweeper ran.
|
||||||
swept during the session, `cr123a-to-d-sleeve` and `sindra` appeared.
|
Re-count rather than trusting any number written here.
|
||||||
|
|
||||||
## Recent decisions
|
## Recent decisions
|
||||||
|
|
||||||
- `[2026-09-22]` **The U5 bug-hunt panel found a service-wide hang that the
|
- `[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`
|
||||||
SIZE CAP ITSELF opened — two hours after I added the cap.** `stat` reports
|
- `[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`
|
||||||
size 0 for a FIFO and 0 for a symlink to `/dev/zero`, so both sail under a
|
- `[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`
|
||||||
byte cap and then `read_text` blocks with no EOF or allocates until the kernel
|
- `[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`
|
||||||
intervenes. `list_booths` reads every booth on every `GET /`, so ONE such file
|
- `[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`
|
||||||
stalls the front page for the whole service with no error and no recovery
|
- `[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`
|
||||||
short of a restart. Reproduced (`timeout` returned 124), fixed with an
|
- `[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`
|
||||||
`S_ISREG` check BEFORE the size check in both modules, verified live: the
|
- `[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`
|
||||||
index answered 200 in 36 ms with two FIFOs planted. **The reusable shape:
|
- `[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`
|
||||||
`st_size` answers a different question than "can this be read", and a bound
|
- `[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`
|
||||||
that trusts it inherits everything it does not mean — a hardening fix opened
|
- `[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`
|
||||||
a worse hole than the one it closed.** Also adopted: the upload path wrote the
|
- `[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`
|
||||||
manifest ABOVE its own cleanup guard (4/4), so a failure orphaned a half-booth
|
- `[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`
|
||||||
whose uniquely-named leaked temp then kept it alive forever; replace-over-
|
- `[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`
|
||||||
damaged destroyed recoverable bytes (4/4, now QUARANTINED rather than refused
|
- `[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`
|
||||||
— marks refuse because judgment is not restatable, a booth's description is);
|
- `[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`
|
||||||
and `booth answer` spelled out its own openness test, disagreeing with
|
- `[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`
|
||||||
`booth marks` about a partially-answered pick, which is a direct violation of
|
- `[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`
|
||||||
U2's INV-2. Full triage in `persistent-memory.d/2026-09-22-u5-panels.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-22]` **An existing test stopped me retiring documented behaviour
|
- `[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`
|
||||||
while fixing a race.** The mtime-restore race is real, and the clean fix —
|
- `[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`
|
||||||
ignoring a booth directory's own mtime whenever the booth holds anything —
|
- `[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`
|
||||||
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 `.<name>.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/<n>/marks.json` was added because remote sessions polled
|
|
||||||
`<stem>.answer.json` over HTTP and the sidecar's removal would have taken that
|
|
||||||
capability with it; `/b/<n>/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 (`<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
|
## Tried and abandoned
|
||||||
|
|
||||||
- `[2026-09-21]` **Tagging a release while a review gate was still in flight.**
|
- `[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`
|
||||||
`v0.2.0` was cut and announced to 15 consuming handles; the
|
- `[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`
|
||||||
`/heid-contract-review` panel — dispatched BEFORE implementation, as the
|
- `[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`
|
||||||
discipline says — replied afterwards with three defects in the code that had just
|
- `[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`
|
||||||
shipped, one of them silent data loss. Nothing about the tier decision was wrong;
|
- `[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`
|
||||||
the *timing* was. **If a gate is outstanding on the work being released, the tag
|
- `[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`
|
||||||
waits for it.** The cost was a same-hour `v0.2.1` and a correction note to peers
|
- `[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`
|
||||||
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/<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