Files
booth/tests/mutations/u7_navigation.toml
T
vh c1108a1966 feat(blur): a booth can be fogged as a whole, composing with per-item blur
The operator ruled booth-level blur in and chose reading A for the reveal
("A is fine"). design-dev specced the semantics and owns the controls; this is
the storage half.

COMPOSES, NEVER OVERRIDES. An item is blurred iff the booth is blurred OR it is
in .blurred, so turning booth blur off leaves an agent's per-item choice exactly
as the poster left it. An override would need a per-item "unblurred" exception
list, which is state nobody can see.

Resolved in booth_items, so every surface inherits it for free — Desk strip,
tiles, flag tray, filmstrip, stage all already read Item.blurred and none of
them learns the booth flag exists (INV-1). Images and video only; audio has
nothing to hide from a glance.

A MARKER, deliberately not JSON. `.seen` is JSON because it holds rels that must
round-trip exactly; a boolean has nothing to round-trip, and matching `.forever`
means the two whole-booth flags read the same way. We told design-dev it would
be JSON and it should not be — said so rather than quietly shipping the other
thing.

is_booth_blurred mirrors is_kept's lstat shape WITH THE SAFETY INVERTED, and the
inversion is the point: is_kept fails toward keeping because a failed read must
not authorise a delete; this fails toward HIDING, because a failed read must not
reveal something a poster asked to fog. Both are "the failure does not cause the
loss".

Also records the operator's 2026-09-23 ruling that there is NO 1.0 yet, and adds
.blurbooth to CLAUDE.md's dotfile list. 766 green.
2026-09-23 17:06:37 -07:00

237 lines
8.0 KiB
TOML

# U7 — every falsifier the navigation unit claims, and the change it forbids.
#
# Generated from the session harness that proved them, not retyped. Each `old`
# must match the source byte-for-byte; a drifted anchor is REPORTED, never
# silently skipped — a table that stops matching stops proving anything.
unit = "the rail, the filters, the grid keyboard, and the groups"
[[mutation]]
label = "INV-2 sort the grid by (group, rel) so groups render contiguously"
file = "booth/app.py"
test = "tests/test_navigation.py::test_grouping_never_reorders_the_grid"
old = '''
shown = buckets[active]'''
new = '''
shown = sorted(buckets[active], key=lambda i: (i["group"] or "", i["name"]))'''
[[mutation]]
label = "INV-3a drop the >=2 groups guard (a rail with one row)"
file = "booth/app.py"
test = "tests/test_navigation.py::test_no_group_rail_when_there_is_only_one_group"
old = '''
if len(sizes) < 2 or sizes[len(sizes) // 2] <= 1:'''
new = '''
if len(sizes) < 1 or sizes[len(sizes) // 2] <= 1:'''
[[mutation]]
label = "INV-3b drop the median guard (a rail that is a second copy of the grid)"
file = "booth/app.py"
test = "tests/test_navigation.py::test_no_group_rail_when_every_item_is_its_own_group"
old = '''
if len(sizes) < 2 or sizes[len(sizes) // 2] <= 1:'''
new = '''
if len(sizes) < 2:'''
[[mutation]]
label = "groups derived from the FULL gallery, not the rendered list"
file = "booth/app.py"
test = "tests/test_navigation.py::test_groups_describe_the_filtered_grid"
old = '''
"groups": _groups(shown),'''
new = '''
"groups": _groups(gallery),'''
[[mutation]]
label = "the anchor names the group key instead of the tile id"
file = "booth/app.py"
test = "tests/test_navigation.py::test_every_group_anchor_lands_on_a_rendered_tile"
old = '''
{"key": k, "n": len(v), "anchor": f"item-{v[0]['url']}"}'''
new = '''
{"key": k, "n": len(v), "anchor": f"group-{k}"}'''
[[mutation]]
label = "the rail orders groups alphabetically instead of by first member"
file = "booth/app.py"
test = "tests/test_navigation.py::test_group_order_is_the_position_of_the_first_member"
old = '''
for k, v in by_group.items()'''
new = '''
for k, v in sorted(by_group.items())'''
[[mutation]]
label = "the rail orders groups by count, which the docstring also claims differs"
file = "booth/app.py"
test = "tests/test_navigation.py::test_group_order_is_the_position_of_the_first_member"
old = '''
for k, v in by_group.items()'''
new = '''
for k, v in sorted(by_group.items(), key=lambda kv: -len(kv[1]))'''
[[mutation]]
label = "_group_of reverts to the contract's original strip-trailing-digits rule"
file = "booth/items.py"
test = "tests/test_items.py::test_group_of_takes_the_first_segment"
old = '''
segs = _SEG.split(stem)
if len(segs) == 1:
return re.sub(r"\d+$", "", stem) or None
return segs[0] or None'''
new = '''
m = re.match(r"^(.*?)[-_. ]?\d+$", stem)
return (m.group(1) or None) if m else (stem or None)'''
[[mutation]]
label = "INV-1 a route body derives the group inline"
file = "booth/app.py"
test = "tests/test_navigation.py::test_no_route_body_derives_a_group"
old = '''
by_group: dict[str, list[dict]] = {}'''
new = '''
_ = _group_of # noqa
by_group: dict[str, list[dict]] = {}'''
[[mutation]]
label = "the rail markup is emitted with |safe"
file = "booth/templates/booth.html"
test = "tests/test_navigation.py::test_a_hostile_filename_cannot_break_out_of_the_rail"
old = '''
href="#{{ g.anchor }}">{{ g.key }} <b>{{ g.n }}</b></a>'''
new = '''
href="#{{ g.anchor }}">{{ g.key|safe }} <b>{{ g.n }}</b></a>'''
[[mutation]]
label = "a flat all-digit stem yields the empty string instead of None"
file = "booth/items.py"
test = "tests/test_navigation.py::test_a_group_key_is_never_the_empty_string"
old = '''
return re.sub(r"\d+$", "", stem) or None'''
new = '''
return re.sub(r"\d+$", "", stem)'''
[[mutation]]
label = "the template renders the group row whenever there is any group at all"
file = "booth/templates/booth.html"
test = "tests/test_navigation.py::test_no_group_rail_when_every_item_is_its_own_group"
old = '''
{% if rail.groups %}'''
new = '''
{% if rail.groups is not none %}'''
[[mutation]]
label = "the anchor is built from the raw name instead of the encoded url"
file = "booth/app.py"
test = "tests/test_navigation.py::test_a_group_anchor_survives_a_filename_that_percent_decodes"
old = '''
{"key": k, "n": len(v), "anchor": f"item-{v[0]['url']}"}'''
new = '''
{"key": k, "n": len(v), "anchor": f"item-{v[0]['name']}"}'''
[[mutation]]
label = "the anchor names the LAST member instead of the first"
file = "booth/app.py"
test = "tests/test_navigation.py::test_a_group_anchor_names_the_FIRST_member"
old = '''
{"key": k, "n": len(v), "anchor": f"item-{v[0]['url']}"}'''
new = '''
{"key": k, "n": len(v), "anchor": f"item-{v[-1]['url']}"}'''
[[mutation]]
label = "a group row over-reports its own size"
file = "booth/app.py"
test = "tests/test_navigation.py::test_a_group_row_reports_its_own_size"
old = '''
{"key": k, "n": len(v), "anchor": f"item-{v[0]['url']}"}'''
new = '''
{"key": k, "n": len(v) + 1, "anchor": f"item-{v[0]['url']}"}'''
[[mutation]]
label = "the informativeness guard reads the LARGEST group, not the middle"
file = "booth/app.py"
test = "tests/test_navigation.py::test_the_informativeness_guard_reads_the_middle_not_the_largest"
old = '''
if len(sizes) < 2 or sizes[len(sizes) // 2] <= 1:'''
new = '''
if len(sizes) < 2 or sizes[-1] <= 1:'''
[[mutation]]
label = "the rail is gated on the FILTERED list, removing the way back"
file = "booth/templates/booth.html"
test = "tests/test_navigation.py::test_a_filter_that_matches_nothing_leaves_a_way_back"
old = '''
{% elif all_items %}'''
new = '''
{% elif items %}'''
[[mutation]]
label = "the keyboard flag selector names a class nothing emits"
file = "booth/templates/booth.html"
test = "tests/test_navigation.py::test_the_keyboard_flag_targets_a_real_button"
old = '''
case 'f': click('.flagtoggle button');'''
new = '''
case 'f': click('.flagbtn, [name="target"]');'''
[[mutation]]
label = "an unrepresentable filename is let through and 500s the booth"
file = "booth/items.py"
test = "tests/test_items.py::test_one_unrepresentable_filename_costs_its_own_tile_not_the_booth"
old = '''
try:
quote(rel, safe="/")
except UnicodeEncodeError:'''
new = '''
try:
pass
except UnicodeEncodeError:'''
[[mutation]]
label = "the grid cursor starts at tile 0, so an arrow undoes a group jump"
file = "booth/templates/booth.html"
test = "tests/test_embed_browser.py::test_an_arrow_after_a_group_jump_does_not_scroll_back"
old = '''
case 'ArrowRight': focus(at < 0 ? fromViewport() : at + 1);'''
new = '''
case 'ArrowRight': focus(at + 1);'''
[[mutation]]
label = "the link board drops its href scheme guard"
file = "booth/links.py"
test = "tests/test_booth.py::test_the_link_board_refuses_to_render_a_script_href"
old = '''
return parts.scheme.lower() in ("http", "https")'''
new = '''
return True'''
[[mutation]]
label = "the board delete dialog takes the raw agent-written description"
file = "booth/templates/booth.html"
test = "tests/test_booth.py::test_the_board_delete_dialog_cannot_be_rewritten_by_a_link_row"
old = '''
var d = shown(btn.getAttribute('data-desc') || '');'''
new = '''
var d = btn.getAttribute('data-desc') || '';'''
[[mutation]]
label = "booth blur OVERRIDES per-item instead of composing"
file = "booth/items.py"
test = "tests/test_booth.py::test_booth_blur_composes_with_per_item_and_never_overrides_it"
old = '''
blurred=rel in blurred or (booth_blur and kind in BLURRABLE_KINDS),'''
new = '''
blurred=(booth_blur and kind in BLURRABLE_KINDS),'''
[[mutation]]
label = "an unreadable booth-blur marker reveals instead of fogging"
file = "booth/items.py"
test = "tests/test_booth.py::test_an_unreadable_booth_blur_marker_fogs_rather_than_reveals"
old = '''
except OSError:
return True # cannot tell -> fog it; see above'''
new = '''
except OSError:
return False # cannot tell -> reveal it'''