fix: four defects the U4 bug-hunt panel found in code it did not add

All four pre-date U4 and sit in files it touched, which is why a diff-scoped
robustness lens saw them. They are separated from the unit's own commit so the
feature history stays readable; the release tags both.

* A booth name reached a JS string context. The confirm dialogs interpolated
  the name into a string literal inside `onsubmit`. Jinja's autoescape is
  HTML-attribute escaping, not JS-string escaping: the browser decodes the
  entity back to a quote before the JS parser sees it, so a name crafted to
  close the string executed on submit. Booth names are agent-authored — making
  a folder under the data dir is the whole API — so this was a live path, not a
  theoretical one. The name now travels as a data attribute to a delegated
  handler, where escaping is escaping.

* An unreadable `links.md` returned 500 for the whole booth page. `is_file()`
  then an unguarded `read_text()`. The board is one tile on that page, and a
  page that will not load is worse than one missing a tile — the posture
  `read_blurred`, `marks_for` and `read_manifest` already take.

* The index order had no tie-breaker, which violates the deterministic-order
  invariant. Equal-mtime booths fell back to whatever `iterdir()` yielded, and
  two booths landed by one `rsync` batch share an mtime exactly. Now
  `(mtime, name)` reverse: newest first, then name. The operator refers to
  cards positionally, so a sequence that moves between renders misfiles his
  judgment rather than crashing.

* `/b/<n>/marks.json` reported damage as empty success. `booth marks` exits 3
  on an unreadable file precisely so a caller can tell "not yet" from "broken";
  the HTTP mirror — the only reader a remote session has — returned the same
  empty list for both. It now carries `error` and `detail`. The status stays
  200 deliberately: reads are lenient here, and a pinned status code is a
  promise to remote clients this fix has no business breaking.

Each has a regression test. 410 tests.
This commit is contained in:
vh
2026-09-22 09:51:14 -07:00
parent c3a97c1b64
commit c75d7a2797
6 changed files with 195 additions and 25 deletions
+7 -8
View File
@@ -25,14 +25,13 @@ _As of 2026-09-22:_
`5e41108` → `v0.2.1`, `026a1fc` → `v0.2.2`; U5 `c015a91` + `95beede` →
`v0.3.0`. **U4 landed 2026-09-22** — 396 tests green (341 → 396), deployed and
verified live, 24/24 booth pages 200, layout probe clean.
- **U4 IS NOT YET RELEASED — the version bump is an open operator decision.**
Recommended **v0.3.0 → v0.4.0 (minor)**: U4 changes what `keep` MEANS for 17
agent handles ("stop pressing keep when you are only waiting"), which is
release-note-worthy at the pre-1.0 bar and the same tier U2 and U5 took. The
defensible alternative is v0.3.1 if U4 reads as internal plumbing, since no
CLI verb or URL changed shape. **Commits are not releases**, so the work is
committed unbumped and untagged; the tag waits on his word and nothing is
blocked by the wait.
- **U4 released as `v0.4.0`** (operator approved the minor on 2026-09-22).
`c3a97c1` is the unit; the release commit carries the pre-existing fixes the
bug-hunt panel surfaced in touched files. The tag waited for the last gate to
close, per the `v0.2.0` lesson — see Tried and abandoned.
- ⚠ **The 17 consuming handles have NOT been told** that `keep` no longer means
"waiting on an answer". That is the one coordination this release genuinely
warrants, and a fleetwide post needs operator approval before it is sent.
- **THE NEXT UNIT IS THE OPERATOR'S CALL.** U3 (declared embed seam) and U6
(benches) are both unblocked; U7 waits on the rest. U6 is independent of
everything and was conceptually unblocked by U5 giving job 5 a home; U3 is