fix(marks): a write over a damaged mark file was wiping the booth's judgment
Three defects and a missing test, all surfaced by the cross-frontier contract panel dispatched before implementation and triaged after it (heid, four arms, artifact-only, thread 01M33VSNFER4N1554G0Y0VC9C8). v0.2.0 was already tagged and announced to fifteen handles when they landed, which is the argument for running the gate at all. DATA LOSS. `marks_for` is deliberately lenient — an unparseable `.marks.json` reads as "no marks" so a review page still loads. The write path inherited that leniency through the same reader, so one flag click appended a single entry to an empty list and atomically replaced the file: every mark in the booth gone, silently, from a click. Reproduced first, then fixed. The fix is an asymmetry, not a retreat from leniency. Reads stay lenient; writes go strict through `_read_raw_strict`, which distinguishes bytes-present-but- unreadable from absent and valid-but-empty, and raises `MarksCorrupt`. The damaged bytes are left on disk. Routes answer 409 rather than 500 — the service is fine and the request was well-formed, the state on disk is not — and the body says what to do, because the alternative the operator reaches for otherwise is deleting the file, which is the thing being protected. The CLI says it in one line instead of a traceback. A PICK COULD NOT TARGET AN ITEM. `Mark.target` carried one, `marks_for_target` retrieved by it, and the panel already rendered "on <item>" — but `declare_pick` had no parameter for it, so no session could produce one. A question about one artifact is the whole point of the 2026-09-09 inline-placement ruling; the door was simply missing. THE IMPORTER STRANDED AN ANSWER. A stem already present as a mark was skipped wholesale. If a session had re-declared that stem through marks while the operator's choice sat in the legacy sidecar, that choice was lost permanently — reads are forbidden from looking at sidecars. The declaration is still skipped (idempotence holds) but a legacy answer is now adopted when the existing mark is an unanswered pick, and an answer made through marks is never overwritten. INV-3 NAMED A SURFACE NOTHING TESTED. All four arms converged on it: the rule protects gallery tile, zoom view and doc view; the falsifiable check covered one. The doc view was implemented and untested, so shipping it unmarked would have passed. Three tests now, one per surface. The contract carries the full triage, including two findings accepted and NOT closed: INV-2's and INV-5's checks comply in letter — openness can be re-derived without spelling the grepped pattern, and importlib inside a function defeats the AST walk. Both describe a future careless change, and the honest statement is that these checks raise the cost of drifting rather than making it impossible. Recorded rather than papered over. Also pins the three prose ambiguities the panel found, normatively and once each: what counts as open, the three distinct broken-declaration cases, and INV-6, which had named a helper that does not exist and forbidden the calls that helper must make. 253 tests.
This commit is contained in:
@@ -258,6 +258,14 @@ declare_pick(booth, "batch", {
|
||||
# "unanswered", "complete", "notes", "answered_at", "answered_by"}
|
||||
```
|
||||
|
||||
**A pick can be about ONE item, not just the booth.** Pass `target` — an item's
|
||||
booth-relative path — and the question renders beside that artifact:
|
||||
|
||||
```python
|
||||
declare_pick(booth, "which-crop", {"prompt": "Which crop?", "options": ["tight", "wide"]},
|
||||
target="v3/DSC03389.jpg")
|
||||
```
|
||||
|
||||
**A partial answer is recorded, not refused.** A question left blank is a
|
||||
deliberate outcome — "none of these", "not yet", "ask me later" — so it lands in
|
||||
`unanswered`, stays absent from `answers` unless it carried a note, and
|
||||
@@ -272,6 +280,14 @@ files (`<stem>.ask.json` / `<stem>.answer.json`) are imported, never deleted:
|
||||
booth marks-import r18-ab # idempotent; the sidecars stay on disk
|
||||
```
|
||||
|
||||
If the stem is already a mark the declaration is skipped, but a legacy answer
|
||||
still gets adopted, so the operator's recorded choice is never stranded on disk.
|
||||
|
||||
**If a booth's `.marks.json` is damaged**, reads degrade to "no marks" so the page
|
||||
still loads, and every WRITE refuses with a 409 rather than replacing the file —
|
||||
which would otherwise wipe every judgment in that booth. Repair or move the file
|
||||
by hand; nothing deletes it for you.
|
||||
|
||||
## Upload for pickup
|
||||
|
||||
The reverse direction — put files in through the web, pick them up by id:
|
||||
|
||||
Reference in New Issue
Block a user