fix(probe): the disclosure-opening loop was manufacturing its own findings

`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.
This commit is contained in:
Vuong Hoang
2026-09-22 01:39:43 -07:00
parent c015a917ee
commit f3193fb054
4 changed files with 147 additions and 9 deletions
+15 -4
View File
@@ -1,7 +1,7 @@
# The Booth — roadmap # The Booth — roadmap
Design: [`docs/design/information-architecture.md`](docs/design/information-architecture.md). 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 ## 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 | | 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 | | 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 | | 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 | | 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 | | 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 U7**, with **U6 independent** of all of them (different storage, different
surface) and therefore the safest thing to land first or in parallel. 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. **U1, U2 and U5 are landed.** U3 and U4 are unblocked and unstarted; U6 remains
**U5 is next** (operator, 2026-09-21). U6 remains independent and unstarted. 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 ### 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 | | 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 | | legacy ask import | `(mtime, name)`, which is the order `list_asks` gave them |
| link board rows | pinned first, then newest-first | | 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 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 section ordering and its compare pairing** (sections need a stated order among
+102
View File
@@ -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.
+17
View File
@@ -59,6 +59,23 @@ _As of 2026-09-22:_
## Recent decisions ## 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-22]` **U5's adoption is a stated, falsifiable prediction — RE-MEASURE
2026-09-29.** Operator declined the fleetwide announcement and chose to let the 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: convention sit, explicitly so that adoption can be told apart from design:
+13 -5
View File
@@ -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 # 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 # inside `details.item-addnote` is exactly the kind of control this
# instrument exists to check. Open it and ask the real question. # instrument exists to check. Open it and ask the real question.
for d in page.locator("details:not([open])").all(): #
try: # ⚠ ONE evaluate over the whole document, NOT a locator loop. `.all()` hands
d.evaluate("(el) => el.open = true") # back positional locators that re-resolve against the CURRENT DOM, and
except Exception: # `details:not([open])` stops matching an element the moment it is opened —
pass # 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-<details>
# 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) page.wait_for_timeout(150)
for el in page.locator("button, a.dl-link, a.thumb").all(): for el in page.locator("button, a.dl-link, a.thumb").all():
try: try: