Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 22 additions & 7 deletions design/docs-md/module-skeletons.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,8 +46,11 @@ rivercrossing/
│ ├── rider_issues.py # roster defect report (R-78, §S4)
│ ├── store/
│ │ ├── __init__.py # Store facade (public API); audit reads via Store.audit_rows
│ │ ├── schema.py # DDL v1 + PRAGMAs (WAL, foreign_keys); one flattened v1
│ │ │ # baseline — no migrations module (Phase 2, SCHEMA_VERSION=1)
│ │ ├── schema.py # latest DDL + PRAGMAs (WAL, foreign_keys); SCHEMA_VERSION
│ │ │ # gate: create on empty, migrate older, refuse newer
│ │ ├── migrations.py # MIGRATIONS: source version -> the step to the next
│ │ │ # (v1 -> v2 rebuilds entry; v2 -> v3 rewrites legacy
│ │ │ # audit entry_id -> key); run_migrations(conn, from, to)
│ │ └── backup.py # open + hourly + manual, keep 20 (R-54)
│ ├── csvio.py # §7 import/export, preview-then-commit
│ ├── htmlexport.py # §8 Jinja2 renderer (self-contained page; + poster page)
Expand Down Expand Up @@ -196,7 +199,11 @@ class RideEngine: # pure; wall-clock injected for tests
undo_last() -> Event · edit_crossing(entry_id, seq, crossed_at, reason)
void_crossing(entry_id, seq, reason) · reassign_crossing(seq, new_plate, reason)
deal_manual(plate, reason) · void_card(entry_id, card, reason) · mark_dnf(plate, reason)
# rider moves are not the engine's: Roster.move_rider(rider, *, to_entry); pooled only (R-17)
move_rider(rider_plate: str, *, to_team: str, reason: str) -> Event # team->team, solo->team
extract_rider_to_solo(rider_plate: str, *, reason: str) -> Event # team->solo
# pooled rider moves: the Roster owns membership (move_rider(rider, *, to_entry);
# extract_rider_to_solo -- a solo source allowed); these two engine methods own the
# re-attribution (plate, crossings, cards; voided laps reset), Stop/Reopen-gated (R-17)
stop() -> Event · finish(*, self_test_failed_checks=()) -> Event · reopen() -> Event
# REOPENED = corrections only; finish() also performs and records R-14's
# high-card draw (one tiebreak_draw event) and writes any overridden
Expand Down Expand Up @@ -228,8 +235,9 @@ rivercrossing.roster — in-memory roster & lock matrix (§1–§2 · R-11/12/15

```
class EntryMode(StrEnum): SOLO MIXED · class PlateModel(StrEnum): RIDER_POOLED TEAM_RELAY
@dataclass Entry(plate, display_name, type, riders, status, notes, has_data, logo_card)
# identity, not value; has_data is the delete guard (R-15)
@dataclass Entry(plate, display_name, type, riders, status, notes, has_data, logo_card, key)
# identity, not value; key = stable UUID the engine files crossings/hands under
# (not the mutable derived plate); has_data is the delete guard (R-15)
@dataclass Rider(first_name, last_name="", plate: str | None = None,
sex: str | None = None, sort_order=0)
class Roster: # one ride's entries/riders; status set by the E4 engine
Expand All @@ -240,6 +248,8 @@ class Roster: # one ride's entries/riders; status set by the E4
next_free_plate() -> str # highest numeric + 1
validate_for_start() -> list[StartViolation] # R-12's floor, checked at start
entries · audit_log · status · take_audit_log() # audit events persist via the E5 store
entry_by_key(key) -> Entry | None # stable-key lookup; searches retired too
retired_entries · load_retired_entries() # dissolved data-bearing entries, kept for replay
can_edit_structure(status) · can_delete_entry(status, has_data)
can_move_rider(status, plate_model) · can_add_entry() · can_fix_name()
team_name_key(name) -> str # fuzzy team key; the CSV preview and rider_issues share it
Expand Down Expand Up @@ -271,8 +281,13 @@ class Store: # facade; sqlite3, WAL, foreign_keys ON
# nothing calls it yet, and append() commits synchronously (store/__init__.py)
backup.run(path, keep=20) · backup.schedule_hourly(…) · backup.restore(src, dst)
schema.py: ride · entry · rider · crossing · card · app_session · audit (+ schema_version)
(columns per Spec §2, incl. status enum with REOPENED, shoe seed, plate_model; one flattened
v1 baseline — no migrations, and no settings table: E8.1.1 keeps settings in a JSON config file)
ensure_schema(conn) -> None # create on an empty file · run MIGRATIONS on an older one ·
# no-op on the current one · SchemaVersionMismatchError on newer
migrations.py: MIGRATIONS: dict[int, Callable[[sqlite3.Connection], None]] ·
run_migrations(conn, from_version, to_version) -> None
(columns per Spec §2, incl. status enum with REOPENED, shoe seed, plate_model; SCHEMA_VERSION
is 3 and every schema change ships the step that upgrades an older file — v3 is a data-only
audit-identity rewrite, no DDL — no settings table: E8.1.1 keeps settings in a JSON config file)
```

rivercrossing.csvio / htmlexport / pdfexport (§7/§8/§8b · R-21/61/62/63)
Expand Down
6 changes: 3 additions & 3 deletions design/docs-md/requirements.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,9 +27,9 @@ Each requirement is testable and traces to the [engineering spec](spec.md) (§)
| R-13 | MUST | Shoe config per ride: deck count, jokers per deck (0–10, default 1, jokers wild) and the jokers mode — **total** (the default: those jokers are spent once across the ride, a spent budget dealing naturals only) or **per deck** (re-dealt every cycle); the optional card cap is the setup dialog's **Card cap** dropdown — **Disabled** by default and otherwise a cap N from 5 to 20, where N scores an entry's best 5 of its first N credited cards. Laps past the cap still count. | §4/§5 · [setupdlg](xrc-windows.md) |
| R-14 | MUST | Tie-break order (default ① high-card draw ② most laps ③ total time) set at ride setup, draggable, and changeable after the finish with instant re-ranking; the stored order applies to a FINISHED ride's results, while a not-yet-finished ride auto-ranks most laps then total time. The high-card draw is performed at the finish: `RideEngine.finish()` draws one card per tied entry from one fresh 52-card deck seeded from the ride's stored seed (highest card wins — rank first, then suit, spades highest), records it as an audited `tiebreak_draw` event, and the drawn card shows in the results (the Standings window's Draw column and the exports). Reopening or continuing discards the draws, so a corrected ride redraws at its next finish. | §5 · [setupdlg](xrc-windows.md)/[resultsframe](xrc-windows.md) |
| R-16 | MUST | Mixed rides choose a plate model: rider plates pooled to the team (**default** — each rider draws against their own unique plate, uncapped: one card per lap for as many laps as they ride; the team hand scores from the pooled cards, with the optional ride-level cap X applying to the pooled total) or team plate (relay — the EPIC's format). | §1/§2 · [setupdlg](xrc-windows.md) |
| R-17 | MUST | Rider-pooled rides remain editable while running: riders move between teams with their plate, crossings and cards; every move audit-logged. Relay rides keep the start lock. | §3/§7 · [entrydetail](xrc-windows.md) |
| R-17 | MUST | Rider-pooled rides stay editable after the start, but only from a **stopped** ride or **REOPENED**: riders move between teams, from a team to solo, or from solo onto a team. A live-RUNNING move is refused — "Stop the ride first" (Stop, R-35, is the entry lock that keeps a new crossing from interleaving). The rider's plate, crossings and cards are re-credited to the destination, the source entry recalculates without them, the rider's voided crossings are reset, and the review surface recomputes; every move is audit-logged, and a source entry the move empties is kept as a **retired entry** so its key and the rider's re-attributed history survive a reload. Relay rides keep the start lock. | §3/§7 · [ridereditor](xrc-windows.md) |
| R-18 | MUST | Ride library offers Delete: type the ride's name to confirm, automatic backup written first, never available on a RUNNING ride. | §3 · [librarydlg](xrc-windows.md)/[deletedlg](xrc-windows.md) |
| R-15 | MUST | Rides are duplicable (setup + roster, no timing data). Entries and teams are freely editable and deletable only until the start; after start, DNF/void only (a DNF mark is per rider: a pooled team member's own number drops that rider alone, their cards forfeit from the team hand, and a team drops only when every rider is out) — nothing with recorded data is ever deleted. The Rider Editor's Delete removes the selected *rider* (a team member leaves the team, a solo rider's entry is deleted) — never the whole team. | §3 · [ridereditor](xrc-windows.md)/[librarydlg](xrc-windows.md) |
| R-15 | MUST | Rides are duplicable (setup + roster, no timing data). Entries and teams are freely editable and deletable only until the start; after start, DNF/void only (a DNF mark is per rider: a pooled team member's own number drops that rider alone, their cards forfeit from the team hand, and a team drops only when every rider is out) — nothing with recorded data is ever deleted (a pooled move that empties a data-bearing source entry keeps it as a retired entry so its key survives a reload). The Rider Editor's Delete removes the selected *rider* (a team member leaves the team, a solo rider's entry is deleted) — never the whole team. | §3 · [ridereditor](xrc-windows.md)/[librarydlg](xrc-windows.md) |

### 3 · Riders, teams & CSV

Expand Down Expand Up @@ -84,7 +84,7 @@ Each requirement is testable and traces to the [engineering spec](spec.md) (§)
|---|---|---|---|
| R-60 | MUST | Results are computable the moment the ride finishes: top 10 by hand with card graphics — a mixed ride ranks per kind instead, top 5 teams + top 5 solo riders — full field, DNF riders excluded from the ranking. | §5 · [resultsframe](xrc-windows.md) |
| R-61 | MUST | HTML export: one self-contained file rendered with Jinja2 (autoescape, StrictUndefined; base template + macros) from frozen payload dataclasses — Tailwind 4 CSS compiled + inlined, results JSON embedded with </ escaped as <\/, logo base64, zero script logic — static markup renders with JS disabled and the JSON block is the machine-readable record; golden-file + JSON round-trip tests, zero external references (CI-checked) that opens from file:// with no network; flags for times, laps board, time board, full field, all cards drawn (every card per entry, with the drawing rider on pooled rides); the light template is the only published look — no theme option. Page CSS is compiled once at package build (Tailwind CLI in CI against the frozen template) and vendored with base64 font subsets — no Node/CDN at export or runtime; markup is plain Tailwind utilities + the frozen custom classes (§8), **no component library** (the page is read-only; daisyUI is the pre-approved CSS-only fallback if v2 adds interactive widgets). | §8 · [sample](../exports/epic-2026-results.html)/[no-times](../exports/epic-2026-results-no-times.html)/[solo](../exports/epic-2026-results-solo.html) |
| R-62 | MUST | PDF export via fpdf2 with the same sections and flags; deterministic output; optional one-page podium poster — a team event stacks top 3 teams then top 3 solo riders with reduced card faces, a solo event lists the top 5 solo riders. The same poster ships as a self-contained HTML page (`htmlexport.render_poster` → `{ride-slug}-podium.html`, Results ▸ Podium Poster HTML…), with its own Preview row. | §8b · [5a–5d](ui-designs-retired.md) (design doc) |
| R-62 | MUST | PDF export via fpdf2 with the same sections and flags; deterministic output; optional one-page podium poster — a team event stacks top 3 teams then top 3 solo riders with reduced card faces, a solo event lists the top 5 solo riders, and every podium card also lists the entry's entire hand (every drawn card, in draw order) beneath the best-5, gated on the all-cards-drawn flag. The same poster ships as a self-contained HTML page (`htmlexport.render_poster` → `{ride-slug}-podium.html`, Results ▸ Podium Poster HTML…), with its own Preview row. | §8b · [5a–5d](ui-designs-retired.md) (design doc) |
| R-63 | MUST | Times appear in published results only when the export setting says so — hidden by default, no toggle on the page, and with times off the time data is not embedded at all; laps/time leaderboards are opt-in. | §8 · [resultsframe](xrc-windows.md) |
| R-64 | SHOULD | Finished rides reopen into a REOPENED state for corrections (clock closed, add-at-time/edit/void, moves on pooled rides, and Ride ▸ Clear Ride… — which unloads the ride from memory, never from the store); Finish again re-locks; stale exports are flagged. | §3 · [resultsframe](xrc-windows.md) |
| R-65 | MUST | Mixed-ride results split into **two sections — Teams and Solo**: teams rank among teams and solos among solos, each section renumbered from 1 with DNF entrants excluded outright (`standings.rank_by_kind`) — never one combined field. The exported pages present per kind — podium top 3 + top 3, top lists top 5 + top 5, laps boards top 5 + top 5 (a solo ride: top 3 / top 10 / top 10) — and a team row never shows a plate on an exported page; the results window's own Team list hides its Plate column on a `rider_pooled` ride (a pooled team's plate derives from its members), keeping it under `team_relay` and on the solo list. The results window, the HTML full field and the PDF report render the two sections (a kind absent from the ride has no section); the standings CSV carries the `type` column and the R-14 drawn card: `place, plate, entry, type, sex, laps, hand, draw[, total_time]`. | §5/§8/§8b · [resultsframe](xrc-windows.md) |
Expand Down
Loading
Loading