fix(desk): "everything else" is last UPDATED first, not last activity

The operator, on the live Desk: "how is this last activity first?" It was not,
usefully. The section sorted by `_newest_mtime`, which counts a look (`.viewed`),
so opening a booth moved it up. Tonight two post-deploy checks fetched every
booth page within half a second, which recorded 22 looks at once and collapsed
the section into reverse name order through the (mtime, name) tie-break.
Meanwhile each row shows "updated X ago", which is `landed_at`, a different
clock from the one the list was sorted by.

Operator ruling: "last activity can just be last time the booth was updated,
not necessarily operator's last activity." The section now sorts by
`(-landed_at, name)`, the date the row shows, labelled "last updated first".
Looking, flagging and blurring no longer move a booth. `list_booths` keeps its
own order for its other readers, and `_newest_mtime` still feeds lifetime.

The r2_flow contract (§3, the ordering table, INV-5) and ROADMAP's ordering row
are amended to match. Two tests and two r2_flow.toml rows cover it (25/25).
This commit is contained in:
vh
2026-09-23 22:55:10 -07:00
parent cce6a20abe
commit 64f64889a2
6 changed files with 73 additions and 14 deletions
+1 -1
View File
@@ -105,7 +105,7 @@ Where it already binds, and what the rule is in each case:
| the Desk's sections | fixed: needs you → new since you looked → everything else (R2) | | the Desk's sections | fixed: needs you → new since you looked → everything else (R2) |
| within *needs you* | `(open_since, name)` | | within *needs you* | `(open_since, name)` |
| within *new since you looked* | `(-landed_at, name)` | | within *new since you looked* | `(-landed_at, name)` |
| within *everything else* | `list_booths` order: `(mtime, name)` descending | | within *everything else* | `(-landed_at, name)`: last updated first, the date each row shows. Was `list_booths`' activity order, which counted a look (operator, 2026-09-23) |
| the Desk's bookmarks column | `order_for_display` — pinned first, then newest | | the Desk's bookmarks column | `order_for_display` — pinned first, then newest |
| caption sidecar resolution | sorted scan, so two media files sharing a stem resolve the same way every time (a real non-determinism U1 removed) | | caption sidecar resolution | sorted scan, so two media files sharing a stem resolve the same way every time (a real non-determinism U1 removed) |
| marks in a booth | `(created, id)` — time, with the id as tie-break so two marks written in the same second cannot swap | | marks in a booth | `(created, id)` — time, with the id as tie-break so two marks written in the same second cannot swap |
+7 -2
View File
@@ -1159,8 +1159,12 @@ def create_app(
booth that has one; name breaks ties. booth that has one; name breaks ties.
new — content landed since the booth was last looked at, or never new — content landed since the booth was last looked at, or never
looked at. Newest content first; name breaks ties. looked at. Newest content first; name breaks ties.
rest — everything else, in `list_booths`' own order (last activity rest — everything else, last UPDATED first: `(-landed_at, name)`,
first, name as the tie-break). No second rule is stated. the date the row shows. Not `list_booths`' activity order:
that counts a look, so opening a booth moved it up and a
script fetching every booth reshuffled the section into
reverse name order (operator, 2026-09-23: "last activity can
just be last time the booth was updated").
The kept/ephemeral lanes are gone: 23 of 24 live booths were kept, so The kept/ephemeral lanes are gone: 23 of 24 live booths were kept, so
the lanes sorted nothing. Kept status still shows on every row. the lanes sorted nothing. Kept status still shows on every row.
""" """
@@ -1176,6 +1180,7 @@ def create_app(
in_new = {b["name"] for b in new} in_new = {b["name"] for b in new}
rest = [b for b in everything rest = [b for b in everything
if b["name"] not in in_needs and b["name"] not in in_new] if b["name"] not in in_needs and b["name"] not in in_new]
rest.sort(key=lambda b: (-b["landed_at"], b["name"]))
benches, benches_error = read_benches(data_dir) benches, benches_error = read_benches(data_dir)
board = data_dir / links_board board = data_dir / links_board
bookmarks = [row for row in _board_rows(board) bookmarks = [row for row in _board_rows(board)
+1 -1
View File
@@ -100,7 +100,7 @@
{% endif %} {% endif %}
{% if rest %} {% if rest %}
<section class="desk-sec" data-section="rest"> <section class="desk-sec" data-section="rest">
<h2 class="desk-head">Everything else <span class="desk-rule">last activity first</span></h2> <h2 class="desk-head">Everything else <span class="desk-rule">last updated first</span></h2>
{% for b in rest %}{{ row(b, 'rest') }}{% endfor %} {% for b in rest %}{{ row(b, 'rest') }}{% endfor %}
</section> </section>
{% endif %} {% endif %}
+16 -10
View File
@@ -6,7 +6,7 @@ purpose: "Make the Booth a place where judgment happens rather than a place wher
depends_on: depends_on:
- "booth.items.booth_items + Item (INV-1: the one resolver). Item gains `ordinal`, derived there and nowhere else." - "booth.items.booth_items + Item (INV-1: the one resolver). Item gains `ordinal`, derived there and nowhere else."
- "booth.items.image_chain (the zoom ring). SUPERSEDED for the review route by `review_chain`; image_chain stays importable and unchanged for its existing callers and tests." - "booth.items.image_chain (the zoom ring). SUPERSEDED for the review route by `review_chain`; image_chain stays importable and unchanged for its existing callers and tests."
- "booth.app._newest_mtime (THE definition of activity — booth-dev, 2026-09-23). The Desk's 'last activity' reuses it verbatim. The Desk's 'landed since you looked' is a DIFFERENT question and gets a DIFFERENTLY NAMED helper; see INV-5." - "booth.app._newest_mtime (THE definition of activity — booth-dev, 2026-09-23). It feeds lifetime; the Desk no longer sorts by it (amended 2026-09-23, §3). The Desk's 'landed since you looked' is a DIFFERENT question and gets a DIFFERENTLY NAMED helper; see INV-5."
- "booth.app.record_view / VIEW_MARKER (`.viewed`, U4). The Desk reads its mtime to answer 'new since you looked'." - "booth.app.record_view / VIEW_MARKER (`.viewed`, U4). The Desk reads its mtime to answer 'new since you looked'."
- "booth.app.hold_read / hold_reason / open_marks (INV-2 of U2: the one openness predicate). 'Needs you' is `open_marks(...)` non-empty, or `hold_reason(...) == \"unreadable\"` (C4); nothing else." - "booth.app.hold_read / hold_reason / open_marks (INV-2 of U2: the one openness predicate). 'Needs you' is `open_marks(...)` non-empty, or `hold_reason(...) == \"unreadable\"` (C4); nothing else."
- "booth.marks.as_dict, set_flag, write_note, answer_pick, delete_mark (the write API, UNCHANGED)." - "booth.marks.as_dict, set_flag, write_note, answer_pick, delete_mark (the write API, UNCHANGED)."
@@ -292,13 +292,18 @@ rule — a second renderer in JavaScript would be the same bug in a new language
booth that has one. booth that has one.
2. **New since you looked** — `not in_needs_you and (viewed_at is None or 2. **New since you looked** — `not in_needs_you and (viewed_at is None or
landed_at > viewed_at)`. Ordered by `(-landed_at, name)`, newest first. landed_at > viewed_at)`. Ordered by `(-landed_at, name)`, newest first.
3. **Everything else** — in `list_booths`' own existing order: `(mtime, name)` 3. **Everything else** — last UPDATED first: `(-landed_at, name)`, the date
descending, where `mtime` is today's `_newest_mtime`. That is last activity the row shows as "updated". **Amended 2026-09-23 by the operator** ("last
first, with name as the tie-break (`test_the_index_order_has_a_tie_breaker` activity can just be last time the booth was updated, not necessarily
pins it). The Desk reuses that rule rather than stating a second one. operator's last activity"). This section used to be `list_booths`' order,
- Flagging or viewing a booth moves it up this section. That is intended: `(mtime, name)` descending over `_newest_mtime`, and that clock counts a
it is activity. It never moves the booth into (2), because (2) reads look: opening a booth moved it up, and a script that fetched every booth
`landed_at` (INV-5). (a post-deploy check) collapsed the whole section into reverse name order.
- Flagging, viewing or blurring a booth no longer moves it. Only content
does, which is also what moves a booth into (2).
- `list_booths` keeps its own `(mtime, name)` order for its other readers,
and `_newest_mtime` still feeds lifetime (INV-5). Only the Desk's
section stopped reading it.
The side column holds: The side column holds:
@@ -469,7 +474,7 @@ path) is a 404, as any other unknown rel is — never a 500.
| Desk sections | fixed: needs → new → everything | | Desk sections | fixed: needs → new → everything |
| needs you | `(open_since, name)` | | needs you | `(open_since, name)` |
| new since you looked | `(-landed_at, name)` | | new since you looked | `(-landed_at, name)` |
| everything else | `list_booths` order: `(mtime, name)` descending | | everything else | `(-landed_at, name)`: last updated first (amended 2026-09-23) |
| bookmarks | `order_for_display` | | bookmarks | `order_for_display` |
The notes list keeps `(created, id)`. The notes list keeps `(created, id)`.
@@ -490,7 +495,8 @@ path) is a 404, as any other unknown rel is — never a 500.
is specified in C3 and is the one declared exception. is specified in C3 and is the one declared exception.
- **INV-5 — two named clocks.** - **INV-5 — two named clocks.**
- `mtime` / `_newest_mtime`: activity. It includes dotfiles and excludes - `mtime` / `_newest_mtime`: activity. It includes dotfiles and excludes
locks, and it feeds lifetime and 'everything else'. locks, and it feeds lifetime. (It fed 'everything else' until 2026-09-23;
see §3.)
- `landed_at`: content only (non-dot entries), and it feeds 'new since you - `landed_at`: content only (non-dot entries), and it feeds 'new since you
looked'. looked'.
- Never the one where the other is meant: a mark or a view is not new - Never the one where the other is meant: a mark or a view is not new
+18
View File
@@ -210,3 +210,21 @@ old = '''
new = ''' new = '''
var word = WORDS[form.getAttribute('data-confirm')]; var word = WORDS[form.getAttribute('data-confirm')];
if (word && !confirm(''' if (word && !confirm('''
[[mutation]]
label = "everything else in activity order again (a look moves a booth up)"
file = "booth/app.py"
test = "tests/test_flow.py::test_everything_else_is_ordered_by_last_update_not_by_looking"
old = '''
rest.sort(key=lambda b: (-b["landed_at"], b["name"]))'''
new = '''
pass'''
[[mutation]]
label = "everything else breaks an update tie by name reversed"
file = "booth/app.py"
test = "tests/test_flow.py::test_everything_else_breaks_an_update_tie_by_name"
old = '''
rest.sort(key=lambda b: (-b["landed_at"], b["name"]))'''
new = '''
rest.sort(key=lambda b: (b["landed_at"], b["name"]), reverse=True)'''
+30
View File
@@ -243,6 +243,36 @@ def _desk(body: str) -> dict[str, list[str]]:
return out return out
def test_everything_else_is_ordered_by_last_update_not_by_looking(tmp_path):
"""Operator, 2026-09-23: "last activity can just be last time the booth was
updated". The row shows "updated X ago" (`landed_at`), but the section
sorted by `_newest_mtime`, which counts a look, so opening a booth moved it
up, and a script that fetched every booth reshuffled the whole section into
reverse name order. Defeating change: `rest` left in `list_booths` order."""
t0 = time.time() - 100_000
for name, landed in (("aaa-old", t0), ("mmm-mid", t0 + 250), ("zzz-new", t0 + 500)):
b = _booth(tmp_path, name, {"a.png": PNG})
_at(b / "a.png", landed)
(b / ".viewed").write_bytes(b"")
_at(b / ".viewed", t0 + 1000) # every one looked at since it landed
_at(b, t0)
_at(tmp_path / "aaa-old" / ".viewed", t0 + 5000) # ...and the OLDEST looked at last
body = _client(tmp_path).get("/").text
assert _desk(body)["rest"] == ["zzz-new", "mmm-mid", "aaa-old"]
assert "last updated first" in body
def test_everything_else_breaks_an_update_tie_by_name(tmp_path):
"""CLAUDE.md invariant 6: two booths landed by one rsync share an mtime."""
t0 = time.time() - 100_000
for name in ("bravo", "alpha", "charlie"):
b = _booth(tmp_path, name, {"a.png": PNG})
_at(b / "a.png", t0)
(b / ".viewed").write_bytes(b"")
_at(b / ".viewed", t0 + 10)
assert _desk(_client(tmp_path).get("/").text)["rest"] == ["alpha", "bravo", "charlie"]
def test_the_desk_triages_needs_you_then_new_then_everything_else(tmp_path): def test_the_desk_triages_needs_you_then_new_then_everything_else(tmp_path):
"""The tracer for C4: three sections, always in this order, each booth in """The tracer for C4: three sections, always in this order, each booth in
exactly one of them.""" exactly one of them."""