Skip to content

R-17 — pooled live rider move; schema v3 audit migration; podium entire hand - #73

Merged
mbuckaway merged 15 commits into
masterfrom
topic/pooled-live-move
Sep 22, 2026
Merged

mbuckaway merged 15 commits into
masterfrom
topic/pooled-live-move

Conversation

@mbuckaway

@mbuckaway mbuckaway commented Sep 21, 2026

Copy link
Copy Markdown
Owner

What this does

Three changes on one branch, landed together:

  1. Live rider moves on a pooled ride (R-17). A rider_pooled ride can move a rider between
    teams
    , team → solo, or solo → team after the start — but only from Stop mode
    (RUNNING + stopped) or REOPENED. A live-RUNNING move is refused with "Stop the ride first".
    The move re-credits the rider's laps and cards to the destination, resets their voided laps,
    recomputes the review surface, and replays exactly. team_relay keeps its permanent start lock.

  2. Whole hand on the podium. The podium poster (HTML + PDF) and the results-page podium cards
    now list each entry's entire hand — every drawn card, in draw order — beneath the best-5,
    using the same All N cards, in draw order line the full-field reports already carry. The podium
    poster stays a single page.

  3. Schema v3 — legacy rides reopen. The v1 → v2 migration added the stable entry key but left
    the audit payloads' plate-based entry_ids, so a ride recorded before the change failed to
    load ("unknown entry key: 93"). The new data-only v2 → v3 step rewrites those audit identities to
    the entries' keys, in place, on open.

Why it was needed

Roster.move_rider was reachable mid-ride on a pooled ride but changed membership only — it never
re-keyed the engine, which keyed crossings and cards by the team's derived (mutable) plate, so a
mid-ride move stranded a rider's laps on the old key and team → solo was DRAFT-only. The stable-key
refactor that fixes this changed what a persisted event's entry_id means, which is why a v1
database's audit trail then needed the v3 rewrite. Separately, the podium showed only the best-5
where every other report listed the whole hand.

Design

  • Stable entry key. Entry.key (a persisted UUID) replaces the derived plate as the engine's
    identity for crossings, laps, hands and tie-breaks; replay resolves recorded entries by key rather
    than by re-resolving a mutable plate.
  • Two engine mutations mirroring the roster's primitives: RideEngine.move_rider (team/solo
    source → team) and RideEngine.extract_rider_to_solo (team → solo), sharing one
    _reattribute_rider core that moves live crossings, credited and held cards, and resets voided
    laps.
  • Retired entries. A move that dissolves a data-bearing source entry keeps it
    (entry.retired = 1) so its key and the pre-move crossings still resolve after a reload.
  • Schema v2 + v3. v2 rebuilds entry (adds key/retired, the live-only plate index); v3 is
    a data-only step that rewrites legacy audit entry ids to keys. Both upgrade a database in place;
    a file newer than the build is still refused.
  • Stop/Reopen gate. Stop (R-35) is the entry lock that keeps a new crossing from interleaving
    with the re-attribution. The CSV import now refuses every pooled reshape outside DRAFT.
  • Podium. The whole-hand line is gated on the existing All cards drawn publish option and is
    drawn from the same ResultRow.drawn the full-field reports use; the poster's card geometry is
    trimmed so the extra line keeps all six cards on one page.

Requirements

  • R-17 — pooled rides editable after the start; riders move with their plate, crossings and
    cards; relay keeps the start lock.
  • R-21 — re-import reshapes stay DRAFT-only; the Rider Editor is the one live move surface.
  • R-61/R-62 — exports; the podium now carries each entry's whole hand.
  • spec.md §2 (schema note), §3, §7, §8/§8b (podium).

Tests

Test-first throughout. nox -s unit: 6655 passed, 1 skipped, 99.85% coverage; nox -s lint typecheck importlint ids_drift css_drift green. The move's replay equivalence (live vs
Store.load_engine) is pinned for held, credited and voided dispositions; the v1→v2 and v2→v3
migrations each have their own suite; the podium whole-hand line is pinned by content tests plus a
worst-case one-page fit test.

Noted residuals (recorded, not hidden)

  • csvio.commit trusts its preview snapshot; the stale-commit window is UI-unreachable (the
    preview dialog is modal).
  • A moved crossing's seq is record/void order, not time order — correct for standings, a footgun
    for corrections that address a crossing by (entry_id, seq).

No functional test was added: the repo permits one (the open/quit smoke), and this change touches
it only indirectly.

Add Entry.key (a persisted UUID surrogate) and key the engine's
crossings, laps, hands and tie-breaks by it instead of the mutable
derived plate. Replay resolves recorded entries by key, so a mid-ride
pooled move no longer re-keys the roster's plates out from under the
engine. The red replay-divergence test is now green.
Relax extract_rider_to_solo to can_move_rider (pooled RUNNING/REOPENED)
and let move_rider take a solo source so a rider with data can move
onto a team without the has-data delete guard. A dissolved solo entry
now logs a distinct dissolve_entry action.
Add RideEngine.move_rider (team->team, solo->team) and
extract_rider_to_solo (team->solo), gated on Stop/Reopen, that
re-attribute the rider's crossings, credited cards, held cards and
reset their voided laps onto the destination key, with replayed
move_rider/extract_rider_to_solo events.

Known gap (documented in the class docstring): a solo->team move that
dissolves the source entry cannot reload yet, because the dissolved
entry's key is not persisted.
Persist dissolved entries that carry recorded data so the pre-move crossings of a solo->team move stay resolvable after a reload; the ride reopens instead of raising UnknownPlateError. Bump SCHEMA_VERSION to 2 and add store/migrations.py, whose v1->v2 step rebuilds the entry table to add the stable key and retired columns and the live-only plate unique index. ensure_schema now migrates older files and still refuses a newer one.
A team change in the Rider Editor now confirms first, is refused with 'Stop the ride first' while the ride is live, and routes through the engine move methods when the ride is stopped or reopened. Closing the editor refreshes the console so standings and the review list recalculate. DRAFT editing keeps the roster-only path.
A pooled membership reshape is now a preview conflict outside DRAFT, so a mid-ride re-import can no longer move a rider roster-only and strand their crossings on the entry left behind. The Rider Editor remains the one live (Stop/Reopen-gated) move surface. Also surface a roster refusal from a stale commit as a validation message instead of an uncaught raise.
The CSV import applies no pooled reshape after the start (DRAFT-only), the Rider Editor is the one live Stop/Reopen-gated move surface, R-15/R-17 name the retired-entry persistence, module-skeletons carry Entry.key, the roster retired members and the engine move signatures, and the dead run_move_rider/RiderMove picker is removed.
Record in the engine docstring that a moved crossing's seq is record/void order, not time order, and that void_card's entry_id is a plate live and a key on replay. State only; no behaviour change.
@mbuckaway mbuckaway changed the title R-17 — pooled live rider move (solo⇄team) requiring Stop/Reopen R-17 — pooled live rider move; schema v3 audit migration; podium entire hand Sep 22, 2026
@mbuckaway
mbuckaway merged commit 2ce7ba2 into master Sep 22, 2026
27 checks passed
@mbuckaway
mbuckaway deleted the topic/pooled-live-move branch September 22, 2026 02:51
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant