From f3193fb054c4425f8255caacf2ad30a9e5efdaf7 Mon Sep 17 00:00:00 2001 From: Vuong Hoang Date: Tue, 22 Sep 2026 01:39:43 -0700 Subject: [PATCH] fix(probe): the disclosure-opening loop was manufacturing its own findings MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `page.locator("details:not([open])").all()` hands back POSITIONAL locators that re-resolve against the current DOM, and `: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 leaves some closed. Those then report OCCLUDED, which is exactly the false-positive class the block was added to remove. One on booth-redesign, three on cr123a-to-d-sleeve, one on denoise-first-run, and invisible as a bug because a false positive is shaped like a finding. Measured both hypotheses rather than guessing between them: per-element loop against a single document-wide evaluate, 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. One evaluate over the whole document now. All three pages clean. Also carries the ROADMAP U5 row, the two-panel record in persistent-memory.d/, and the memory index line for it. --- ROADMAP.md | 19 +++- persistent-memory.d/2026-09-22-u5-panels.md | 102 ++++++++++++++++++++ persistent-memory.md | 17 ++++ scripts/layout-probe.py | 18 +++- 4 files changed, 147 insertions(+), 9 deletions(-) create mode 100644 persistent-memory.d/2026-09-22-u5-panels.md 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: