fix: a wrong-shaped answer no longer 500s the gallery and the marks page
Pre-existing, measured at 42ea67f, so it predates U3. `_hydrate` checked only
that `answer` was a dict and never that `answer["answers"]` was one, so
`marks_for` and `hold_read` both reported the mark healthy with no read error
-- and `_ask_inline.html` then asked a list for `.get`. The v0.2.2 lesson was
half-implemented: that outage was a file that could not be PARSED and the
reader was made lenient, while this one parses perfectly and breaks one layer
further in, at render, where no leniency existed.
Closed at the hydration boundary rather than by a third copy of the guard --
one predicate, one place, every surface inherits it. Only the multi case is
checked, because only the multi case indexes; requiring `answers`
unconditionally would break every single-question pick, and that direction has
its own test. Measured before and after: gallery and marks pages 500 -> 200,
the error visible on the page, the booth's other healthy pick untouched.
The placement was the one open operator question of the session. It was
surfaced three times without a ruling, so it is taken under a stated assumption
and is cheap to move: the whole fix is one condition in one function.
Two things fell out of it worth more than the fix.
`_safe_fragments` no longer has a reachable natural trigger. Probed every wrong
answer shape a .marks.json can carry: `answers` as a list, a string or null all
become hydration errors now, and a wrong-typed value INSIDE `answers` renders
without raising, because Jinja absorbs attribute access on a non-mapping. U3's
guard is a pure backstop, and its test now says so and trips it synthetically
through the shared macro module rather than asserting a path nothing reaches.
A guard tested by an unreachable input is an untested guard.
And that guard's handler could not survive the failure it was handling: it
caught a raising `_pick_fragments` and rebuilt the broken-ask box through the
SAME macro module that had just raised, so whenever `whole` was the broken
thing it re-raised and took the whole report. Found by accident while building
the falsifier. Fixed, with its own test.
Both new falsifiers were verified RED against their defeating change rather
than assumed.
607 -> 611 tests.
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
# A wrong-shaped answer 500s the gallery and the marks page — PRE-EXISTING, NOT U3
|
||||
# A wrong-shaped answer 500s the gallery and the marks page — CLOSED 2026-09-22
|
||||
|
||||
_2026-09-22 · booth_
|
||||
|
||||
@@ -72,3 +72,49 @@ whose closing comment points back here.
|
||||
Related: [[2026-09-21-marks-write-wiped-judgment]],
|
||||
[[2026-09-22-lenient-reader-blast-radius]],
|
||||
[[2026-09-22-u3-declared-embed-seam-landed]].
|
||||
|
||||
|
||||
---
|
||||
|
||||
## CLOSED — 2026-09-22, after U6, at option (1)
|
||||
|
||||
Fixed in `_hydrate`, the option this entry argued for: **one predicate, one
|
||||
place, every surface inherits it.** The operator was asked three times where the
|
||||
guard belonged and did not answer; the placement was taken under the stated
|
||||
assumption, and it is cheap to move if he disagrees — the whole fix is one
|
||||
condition in one function.
|
||||
|
||||
**Only the MULTI case is checked**, because only the multi case indexes: a
|
||||
single-question pick's answer IS the record, with no `answers` key to get wrong.
|
||||
Requiring one unconditionally would break every single pick — the direction a
|
||||
too-eager guard fails in, and it has its own test.
|
||||
|
||||
Measured before and after, on the gallery booth (no `index.html`):
|
||||
|
||||
before /b/g/ 500 /b/g/marks 500 / 200 /healthz 200
|
||||
after /b/g/ 200 /b/g/marks 200 / 200 /healthz 200
|
||||
and the error is VISIBLE on the page, and the booth's
|
||||
OTHER, healthy pick still renders
|
||||
|
||||
**Two things fell out of it that are worth more than the fix.**
|
||||
|
||||
1. **`_safe_fragments` lost its natural trigger.** Probed every wrong answer
|
||||
shape reachable from a `.marks.json`: `answers` as a list, a string or null
|
||||
all become hydration errors now, and a wrong-typed VALUE inside `answers`
|
||||
renders without raising because Jinja absorbs attribute access on a
|
||||
non-mapping. So U3's guard is now a pure backstop with **no reachable
|
||||
natural input**. Its test was rewritten to a synthetic trigger that says so —
|
||||
patching the shared macro module through `app.state.templates` — rather than
|
||||
left asserting a path nothing reaches. An untested guard and a guard tested
|
||||
by an unreachable input are the same thing.
|
||||
|
||||
2. **The guard's own handler could not survive the failure it was handling.**
|
||||
Building that falsifier tripped it: `_safe_fragments` caught a raising
|
||||
`_pick_fragments` and then rebuilt the broken-ask box **through the same
|
||||
macro module that had just raised**, so when `whole` itself was broken the
|
||||
handler re-raised and took the whole report. Fixed, with its own test. Found
|
||||
by accident, which is the usual way.
|
||||
|
||||
Both new falsifiers were **verified RED against their defeating change** rather
|
||||
than assumed — the discipline from [[2026-09-22-vacuous-falsifiers]], applied to
|
||||
the fix for the entry that names it.
|
||||
|
||||
Reference in New Issue
Block a user