diff --git a/ROADMAP.md b/ROADMAP.md index d460bf7..f4a4589 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -1,7 +1,7 @@ # The Booth — roadmap Design: [`docs/design/information-architecture.md`](docs/design/information-architecture.md). -Current version: `0.2.1` (U1 + U2 landed; extracted from eshpfi 2026-09-21). +Current version: `0.3.0` (U1, U2 and U5 landed; extracted from eshpfi 2026-09-21). ## v1 target @@ -14,7 +14,7 @@ defect — not a wish. The measurements are in the IA doc. | 2 | ~~**Marks**~~ — **landed `c7f9437`, released `v0.2.0`** | 5 mechanisms for 1 job; operator→session loop runs through chat | U2 | | 3 | **Declared embed seam** — `/_booth/embed.js`, chrome mounts via DOM | 6 regexes injected into arbitrary author HTML, load-bearing for asks | U3 | | 4 | **Derived lifetime** — open marks pin; viewing is activity | 54% of booths on the `.forever` escape hatch | U4 | -| 5 | **Self-announcing booths** — `.booth.json`, provenance on the index | job 5 had no home, so it lived on the link board as 145 dead rows | U5 | +| 5 | ~~**Self-announcing booths**~~ — **landed `c015a91`, released `v0.3.0`** | job 5 had no home, so it lived on the link board as 145 dead rows | U5 | | 6 | **Benches** — registry, identity, enforced rule, migration | 69% link-board rot; the same bench posted 5× | U6 | | 7 | **Navigation at 270 items** — sections, rail, filters, grid keyboard | one flat wall; subfolder structure discarded at render | U7 | @@ -22,8 +22,18 @@ Ordering is dependency-driven, not priority-driven: **U1 → U2 → {U3, U4, U5} U7**, with **U6 independent** of all of them (different storage, different surface) and therefore the safest thing to land first or in parallel. -**U1 and U2 are landed**, which unblocks U3, U4 and U5 — all three read marks. -**U5 is next** (operator, 2026-09-21). U6 remains independent and unstarted. +**U1, U2 and U5 are landed.** U3 and U4 are unblocked and unstarted; U6 remains +independent and unstarted; U7 waits on the rest. + +**U5's adoption is a measured prediction, not a finished result.** The operator +declined a fleetwide announcement so that adoption could be told apart from +design: the convention propagates through the README alone, and the count of +booths carrying a `.booth.json` gets re-measured on **2026-09-29** against a +baseline of **0 of 26** at landing. A near-zero count means nobody heard about +it — an adoption failure, fixed by announcing — which is a different thing from +nobody wanting it. Same instrument as U4's `.forever` prediction below. + + find ~/booth-data -maxdepth 2 -name .booth.json | wc -l ### Cross-cutting invariant — deterministic order, everywhere @@ -53,6 +63,7 @@ Where it already binds, and what the rule is in each case: | marks in a booth | `(created, id)` — time, with the id as tie-break so two marks written in the same second cannot swap | | legacy ask import | `(mtime, name)`, which is the order `list_asks` gave them | | link board rows | pinned first, then newest-first | +| a booth's announcement | not a collection — one flat record per booth, nothing to order (U5) | Where it is still to be decided, and must be before the unit ships: **U7's section ordering and its compare pairing** (sections need a stated order among diff --git a/persistent-memory.d/2026-09-22-u5-panels.md b/persistent-memory.d/2026-09-22-u5-panels.md new file mode 100644 index 0000000..d23ee12 --- /dev/null +++ b/persistent-memory.d/2026-09-22-u5-panels.md @@ -0,0 +1,102 @@ +# U5's two cross-frontier panels — full triage + +**Date:** 2026-09-22 · **Paraphrase:** thread `01M340PNVRS21HPASZT38PXQPN` · +**Conformance:** thread `01M341E9XAPZEFBSPK9HPGAM0S` · **Shipped as:** `v0.3.0` + +Two four-arm artifact-only rounds, dispatched ~30 minutes apart and correctly +firewalled: the paraphrase ran the **pre-seam-review** capture (073612), the +conformance round the **SR-amended** one (074901). Heid diffed the two at +intake and said so. + +The conformance round's honest headline is Kimi's: **zero drift in the strict +sense — the code is a clause-for-clause implementation of the contract.** Both +rounds' weight landed one layer down, in test strength and contract finish. + +## The result worth keeping + +**A paraphrase panel reading nothing but prose reached a production outage two +modules away.** 3-of-4 flagged INV-2's "4 GB" case as *letter-compliant but +purpose-defeating* — the invariant constrained the RETURN, not the cost, so an +unbounded read "recreates the outage in slow motion". The conformance round then +found that exact unbounded read live in U5's shipped code. Walking it to the +sibling module found the same hole **live in released `v0.2.2`**: `marks.py`'s +`_read_raw` catches `(OSError, ValueError, UnicodeDecodeError)`, and +`json.loads` on a deeply nested document raises **RecursionError**, which is +none of them. A 400 KB file of nothing but brackets in any ONE booth returned +500 for `/` and `/healthz` across all 26. + +**The v0.2.2 round had flagged this and I closed half of it.** Kimi's R5(c) +named RecursionError explicitly; I adopted "wrap `_hydrate` per-entry" and left +the `json.loads` above it unguarded. **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 formally out of scope.** Heid captured it as its own +lesson. + +## The densest class was tests that could not fail + +Five of ten adopted conformance findings were tests of mine that pass on the +regression they exist to catch. Three shared one shape — **asserting an +ARTIFACT of the property instead of the property**: + +| test | asserted | should have asserted | +|---|---|---| +| `test_the_write_is_atomic` | no `*.tmp` survived | the inode changes (`write_text` leaves no temp file either) | +| INV-3 preservation | a stamp survived a window shorter than the stamp's own resolution | a stamp from 2019 | +| `test_announcing_is_activity` | age via the directory mtime, which the write bumps either way | the file's own mtime, directory clock restored | + +That is the same shape as the marks round's guard-strength finding the night +before — **three nights running**. Proposed to heid as a standing +"green-tests-prove-nothing" direction for the skill; routed to the operator +alongside two other methodology proposals from the same night. + +⚠ **My first replacement for the atomicity test was ALSO vacuous.** It spied on +`os.open` to prove the published path was never written directly — which passes +trivially, because `Path.write_text` reaches the syscall through `io.open` in C +and never touches the Python-level `os.open`. The dead end is recorded in the +test's own docstring rather than deleted. + +## Two real bugs the tests were structurally blind to + +**`booth new x --why "…"` then `booth add x out/*.png` erased the why.** Omitted +flags meant empty strings; empty strings overwrote. Two arms predicted it *from +the contract's wording alone* — "gains a manifest with no `why`" does not +distinguish a first write from a re-announce with the flags omitted. Every test +written for this module passed `--why` on both calls, so none could see it. +Omitted means unchanged now; `--why ""` still clears. The shell carries the +distinction by leaving the variable UNSET, not empty. + +**`--title` was write-only** — stored, flag-surfaced, rendered nowhere. 4-of-4, +independently top-ranked by every arm of the paraphrase round. It renders on the +booth page heading with the directory name kept beside it, because the directory +name is the identity the operator navigates by and refers to positionally. + +## Contract-finish, and why it mattered + +**INV-1 contradicted its own falsifiable criterion** (4/4) — "the only place +`.booth.json` is opened" versus INV-3's read-back, which forces `write_manifest` +to open it. One half was already false of a correct implementation. Restated as +*one module knows the filename*, which is true, falsifiable and now tested. + +**INV-5 named two different promises** (3/4) — the repo's atomic-write rule and +this unit's render rule. Repo-wide rules are named in words now, never by a bare +number that can collide with a local one. + +Regin's meta-observation is the round's methodology keeper and was borne out: +**flags cluster where the same rule is re-voiced per signature**, and four of +eleven contract edits were reconciling a docstring against a prose section +saying the same thing slightly differently. A table-vs-signature consistency +pass would beat the format's prose bias. + +## Declined / parked + +- **Custom booth pages skip provenance** (hulda, solo, verified) — settled + independently as U3's seam ~20 minutes before the reply landed. Convergence, + not an adoption. +- **Empty-handle coercion misattributes to the service** — kept, documented. A + manifest naming no handle does not read back at all, and an unreadable file is + the worse outcome. Unreachable from the CLI. +- **`used`-set: `touches` versus SR-1 unreconciled** — the code adds the entry + as consistency with the equally-unreachable `UPLOAD_MARKER` entry that + predates this unit, and says so rather than claiming it prevents anything. diff --git a/persistent-memory.md b/persistent-memory.md index 06bb379..9367c95 100644 --- a/persistent-memory.md +++ b/persistent-memory.md @@ -59,6 +59,23 @@ _As of 2026-09-22:_ ## Recent decisions +- `[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 is a stated, falsifiable prediction — RE-MEASURE 2026-09-29.** Operator declined the fleetwide announcement and chose to let the convention sit, explicitly so that adoption can be told apart from design: diff --git a/scripts/layout-probe.py b/scripts/layout-probe.py index 7cbe5b1..bc32afb 100755 --- a/scripts/layout-probe.py +++ b/scripts/layout-probe.py @@ -81,11 +81,19 @@ def probe(page, url: str) -> list[str]: # quiet by declaring put-away controls out of scope, and the add-note button # inside `details.item-addnote` is exactly the kind of control this # instrument exists to check. Open it and ask the real question. - for d in page.locator("details:not([open])").all(): - try: - d.evaluate("(el) => el.open = true") - except Exception: - pass + # + # ⚠ ONE evaluate over the whole document, NOT a locator loop. `.all()` hands + # back positional locators that re-resolve against the CURRENT DOM, and + # `details:not([open])` stops matching an element the moment it is opened — + # so opening them one at a time shrinks the set underneath the indices and + # some are never opened at all. That left exactly the closed-
+ # false positives this block exists to remove: 1 on booth-redesign, 3 on + # cr123a-to-d-sleeve, stable across five runs and invisible as a bug + # because a false positive looks like a finding. Measured both ways at + # 150 ms and 1000 ms settle: the loop reports them at either wait, the + # single pass reports none at either. The variable was the method, not the + # timing. + page.evaluate("document.querySelectorAll('details').forEach(d => d.open = true)") page.wait_for_timeout(150) for el in page.locator("button, a.dl-link, a.thumb").all(): try: