ROADMAP's U7 row names four components. Three of them -- a sticky rail, filters, and grid keyboard -- are already ratified there and are implemented here. The fourth, replacing directory sections with filename-derived groups, is a scope DEPARTURE the operator has not ruled on and is deliberately not built; test_no_group_rail_is_shipped_yet fails the moment somebody builds it anyway, so it cannot arrive by accident while he is away. Filters are links carrying a query parameter, resolved server-side, so the gallery keeps working with JavaScript off -- U3 already cost the verbatim path its no-JS operation and said so, and the gallery is the surface the operator actually reviews on. An unknown filter falls back to `all` rather than indexing a dict by a value that arrives from an operator-editable URL. `unanswered` means HAS AN OPEN PICK, the U4 hold predicate that already exists. The other reading is a real and different question and stays open on the contract rather than being guessed at. Filtering is a VIEW and never reorders. The grid renders `sorted(rel)` with non-matching items removed, so "the third one" means the same thing with a filter on as with it off, and the zoom ring is untouched by any filter -- a ring that changed with the grid would make `next` depend on how the operator arrived, which is the misfiled-judgment failure invariant 6 exists for. ⚠ The first version of that invariant's test was VACUOUS and the mutation run caught it: it compared each filtered view against the unfiltered RESPONSE, so a reversing mutation reversed both sides and it stayed green under the exact change it forbade. Rewritten against an independent truth -- U1 INV-3 says the order IS sorted(rel) -- and re-verified RED. Written an hour after the entry describing this exact failure class, which is worth recording. 611 -> 623 tests.
12 KiB
contract_version, status, module, purpose, depends_on, language, complexity, estimated_loc, confidence, used_by, touches, assumptions, open_questions
| contract_version | status | module | purpose | depends_on | language | complexity | estimated_loc | confidence | used_by | touches | assumptions | open_questions | ||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 0.1-PROPOSED | PARTIALLY LANDED. The three components ROADMAP already ratifies — rail, filters, grid keyboard — are implemented and deployed (tests/test_navigation.py, 12 tests). The FOURTH, replacing directory sections with filename-derived groups, is NOT built and is the one scope-direction call below. The scope below departs from ROADMAP's U7 row on measured grounds and the operator has not ruled on it. Do not implement, and do not treat this as settled, until he has. | booth.items + booth.app (gallery navigation) | The last unit before the 1.0 cut. A gallery booth renders as one flat wall with no way to filter it, no way to move through it from the keyboard, and no grouping — so a review of sixty-odd renders is a scroll-and-squint. ROADMAP names four components: sections, a sticky rail, filters, grid keyboard. THE MEASUREMENT KILLS THE FIRST AND REPLACES IT: not one of the eleven live gallery booths has a subdirectory, so sections buy nothing, while a filename-prefix heuristic yields 5-16 sensible groups on four of the five large galleries. This unit ships the rail, the filters, the grid keyboard, and GROUPS DERIVED FROM FILENAMES rather than from a directory tree that does not exist. |
|
python + jinja + a little javascript | medium | 300 | 0.6 |
|
|
|
|
U7 — navigation at the size the booths actually are
⚠ PARTIALLY LANDED, DELIBERATELY.
| component | ROADMAP says | state |
|---|---|---|
| sticky rail | ratified | landed — totals + per-filter counts, links not scripts |
| filters | ratified | landed — all / flagged / annotated / unanswered |
| grid keyboard | ratified | landed — ←/→ f n Enter Esc, bound only when a grid exists |
| sections → filename groups | departs from it | NOT BUILT. The one scope-direction call here, and it is the operator's. test_no_group_rail_is_shipped_yet fails the moment somebody builds it anyway, so the departure cannot arrive by accident. |
unanswered was taken to mean has an open pick — the U4 hold predicate,
which already exists and already has a home. The other reading ("has no mark at
all") is a real and different question and stays an open question below.
The defect, re-measured rather than inherited
ROADMAP sizes this unit for 270 items. The largest gallery is now 81 items and 40 images. The four booths it was written against were swept on 2026-09-22 and the set churned again during that session. The defect is real and the sizing is not:
| ROADMAP's premise | measured 2026-09-22 | |
|---|---|---|
| largest gallery | 270 images, one flat wall | sindra-bakeoff, 40 images |
| galleries with subdirectories | "sections come from subfolders, which already exist" | 0 of 11 |
| booths with subdirectories at all | — | 2, and both are reports |
| grouping signal that does exist | — | the filename prefix |
Sections are dead. The prefix is not.
Strip a trailing digit-run from each stem and group on what remains:
| booth | images | groups | |
|---|---|---|---|
sindra-corpus-v1 |
66 | 16 | ac01.png |
sindra-nude-pool |
42 | 12 | flag-rear.png |
sindra-sfw-pool |
59 | 10 | a01.png |
sindra-bakeoff |
40 | 5 | 00-sheet-c1-market-noon.png |
sindra |
30 | 1 | 00-b1-sheet-1.png — degenerates |
Four of five. The competing heuristic — split on the second hyphen — yields 59 "groups" from 59 files and is useless everywhere. The degenerate fifth is the case the design has to carry, not the case that invalidates it.
What ships
Item.group— derived once, in the resolver, besidesection.- A sticky rail — total, per-group counts, per-filter counts, jump-to-group anchors. Absent entirely when there is one group or fewer.
- Filters — all / flagged / annotated / unanswered, as server-resolved query parameters so they work with JS off.
- Grid keyboard —
←/→move focus,fflags,nopens a note,Enterzooms,Escclears focus. Additive; the page is complete without it.
Signatures
def _group_of(rel: str) -> str | None:
"""The grouping key for an item, or None when it has none.
THE RULE: take the stem of the basename, strip ONE trailing run of digits
and any single separator before it, and return what remains. `ac01.png` →
`ac`; `00-sheet-c1-market-noon.png` → `00-sheet-c1-market-noon` (no
trailing digit run, so the whole stem); `flag-rear.png` → `flag-rear`.
Returns None for a stem that is ENTIRELY a digit run — `01.png` has no
prefix to group on, and inventing one would put every numbered file in a
group named after the empty string.
Derived HERE and nowhere else (INV-1). A route body that re-derived it
would be the caption bug in a new field.
"""
Ordering — the rule, because invariant 6 binds
| collection | rule |
|---|---|
| items | unchanged — sorted(rel) (U1 INV-3) |
| the zoom ring | unchanged — item order filtered to images |
| groups among themselves | the position of each group's FIRST member in sorted(rel) — so the rail reads in the same direction the grid does, and adding a file never reshuffles the rail unless it lands first in its group |
| items within a group | unchanged — they are a filtered view of sorted(rel), never re-sorted |
| the filtered grid | unchanged — sorted(rel) with non-matching items hidden |
This closes ROADMAP's outstanding U7 order question. Compare pairing is not this unit's problem — compare mode is parked to v1.1 with the pairing rule.
Invariants
INV-1 — one resolver derives the group. _group_of is called only from
booth_items. Falsifiable: the defeating change is a route or template
computing a prefix inline. The test asserts no call to _group_of survives
inside create_app — the same assertion U1 makes for classify and
render_doc, which is why it is the shape used here.
INV-2 — grouping and filtering never reorder. Falsifiable: the defeating
change is sorting by (group, rel) to make the grid render contiguously, which
looks right and silently changes what "the third one" means. The test renders a
booth whose groups interleave in sorted(rel) order and asserts the rendered
item sequence is byte-identical with grouping on and off, and that
image_chain is unchanged under every filter.
INV-3 — one group renders NO rail. Falsifiable: the defeating change is
{% if groups %}, which is true for a single group. The test uses the real
sindra-shaped fixture (thirty files, one prefix) and asserts the rail element
is absent — not merely that it lists one entry.
INV-4 — a filter is a link, not a script. Falsifiable: the defeating change is binding filters to a click handler. The test fetches the filtered URL directly and asserts the server returned the filtered grid, with no JS executed.
INV-5 — the keyboard never fires on a booth with no grid. Falsifiable:
the defeating change is binding the handler unconditionally, so f on the
standing link board flags nothing and swallows the keystroke. The test asserts
the handler is not bound when items is empty.
Out of scope
- Sections as a rail. Measured worthless;
Item.sectionis untouched. - Compare mode. Parked to v1.1 with its pairing rule.
- Virtualized loading. Parked; measure first.
- A
.groupsoverride file. See open questions. - Anything on the verbatim path. It has no grid.