feat(manifest): U5 — booths that say who posted them and why
The index card showed a name, an item count and a countdown, and nothing
the poster chose. An agent with something to show therefore had no way to
make the booth say "look at this" and posted a URL to the link board
instead — which is why 145 of that board's 210 rows (69%) ended up
pointing at booths that had already been swept. The board was absorbing a
job it was never shaped for. This is the shape.
Each booth carries `.booth.json` — {handle, title, why, created} — written
by the CLI from $ALTHING_HANDLE, and the provenance line renders on both
index lanes and on the booth page header.
WHAT IS WHERE
- booth/manifest.py, stdlib-only and importing nothing from booth.* either:
scripts/booth imports it under the system python3 with no venv, and a
cross-import between two stdlib-only modules is a second way for that
invariant to break. It joins the shared test_stdlib_only list and keeps
a stricter copy of its own.
- The read is lenient and cannot raise. list_booths touches every booth on
every index load, so a manifest that cannot be parsed costs that booth's
provenance and nothing else. That is the v0.2.2 lesson applied before the
same mistake rather than after it.
- Absent and damaged render differently — `unannounced` and `unreadable`.
Folding "cannot be read" into "never said" would hide the one case
somebody has to go and fix.
- Re-announcing preserves `created`. A second `booth add` sharpening the
why is not a second appearance of the booth.
- The write is atomic (invariant 5); the temp file is itself a dotfile, so
no listing can see it mid-write.
THREE OPERATOR CALLS, 2026-09-22
Flags on the existing new/add verbs rather than a separate `announce` verb
(a second step is the step that gets forgotten, which is the rot's own
mechanism). Unannounced booths get a quiet marker rather than nothing — the
convention is only adoptable if the gap is visible. U5 adds provenance only
and does NOT add a second index ordering keyed on announcement time; that
is a different surface needing its own stated rule, parked for v1.1.
NO EXEMPTION LIST
A pickup booth and the standing link board are created by the service, so
they announce themselves with handle `booth`, which is true rather than
manufactured. One rule — a booth with no manifest is unannounced — instead
of a growing set of special cases.
ALSO
tests/test_booth.py's keep/release assertion was slicing the page on the
bare word `boothhead`, which has lived in the stylesheet far longer than
the assertion has; it was reading CSS and passing on luck, and went red the
first time a new rule landed above the old one. Same assertion, aimed at
the markup. A U5 test had the mirror-image bug: pytest derives tmp_path
from the test name and the index renders data_dir, so a test named
`test_an_unannounced_booth_says_so` put the needle in the haystack itself
and passed against a template that did not yet exist.
310 tests (304 before this unit's CLI half). Live service restarted, 26/26
booth pages verified 200, end-to-end smoke through the real CLI.
NOT TAGGED. The cold contract-review panel is still in flight and the
code-review and bug-hunt gates have not run. Tagging with a gate
outstanding is what made v0.2.0 premature.
This commit is contained in:
+25
-1
@@ -141,6 +141,12 @@ from booth.inline import ( # noqa: E402
|
||||
has_placeholders,
|
||||
place as place_asks,
|
||||
)
|
||||
from booth.manifest import ( # noqa: E402
|
||||
MANIFEST_FILE,
|
||||
SERVICE_HANDLE,
|
||||
read_manifest,
|
||||
write_manifest,
|
||||
)
|
||||
from booth.links import ( # noqa: E402
|
||||
LINK_LOCK,
|
||||
LINKS_FILE,
|
||||
@@ -273,6 +279,11 @@ def list_booths(data_dir: Path, ttl_seconds: float, now: float | None = None) ->
|
||||
# — which is why marks live in one file per booth rather than a sidecar
|
||||
# per mark. This loop runs on every index page load.
|
||||
marks = marks_for(child)
|
||||
# The booth's own announcement — who posted it and why. One more small
|
||||
# read per booth, beside the marks read already here, and `read_manifest`
|
||||
# cannot raise for the same reason `marks_for` must not: this loop runs
|
||||
# over EVERY booth on every index page load.
|
||||
manifest = read_manifest(child)
|
||||
kinds = {"image": 0, "video": 0, "audio": 0, "other": 0}
|
||||
thumb_url = None
|
||||
thumb_blurred = False
|
||||
@@ -290,6 +301,7 @@ def list_booths(data_dir: Path, ttl_seconds: float, now: float | None = None) ->
|
||||
{
|
||||
"name": child.name,
|
||||
"name_url": quote(child.name, safe=""),
|
||||
"manifest": manifest,
|
||||
"count": len(items),
|
||||
"kinds": kinds,
|
||||
"thumb_url": thumb_url,
|
||||
@@ -748,6 +760,10 @@ def create_app(
|
||||
},
|
||||
"booth_marks": marks_for_target(marks, None),
|
||||
"uploaded": (booth / UPLOAD_MARKER).exists(),
|
||||
# The same provenance line the index card carries. Deliberate:
|
||||
# a booth URL handed to the operator lands HERE, never on the
|
||||
# index, and job 5 is "operator, look at this".
|
||||
"manifest": read_manifest(booth),
|
||||
"expires_in": max(0.0, ttl_seconds - booth_age_seconds(booth)),
|
||||
},
|
||||
)
|
||||
@@ -1086,9 +1102,17 @@ def create_app(
|
||||
dest = data_dir / booth_id
|
||||
dest.mkdir(parents=True)
|
||||
(dest / UPLOAD_MARKER).write_text("") # stamp as an upload (dotfile, not listed)
|
||||
# A booth the SERVICE made says so, rather than being exempted from the
|
||||
# unannounced marker. One rule instead of an exemption list, and the
|
||||
# handle is true: nobody's agent posted this, the browser did.
|
||||
write_manifest(dest, SERVICE_HANDLE, title=booth_id,
|
||||
why="browser upload, for pickup")
|
||||
|
||||
total = 0
|
||||
used: set = {UPLOAD_MARKER}
|
||||
# Both markers are belt-and-braces: `safe_upload_name` strips leading
|
||||
# dots, so an uploaded file can never be named either of them. Listed
|
||||
# anyway so the set says what the directory already contains.
|
||||
used: set = {UPLOAD_MARKER, MANIFEST_FILE}
|
||||
try:
|
||||
for i, f in enumerate(files):
|
||||
name = _dedupe_name(safe_upload_name(f.filename, f"file-{i + 1}"), used)
|
||||
|
||||
Reference in New Issue
Block a user