# Hand-history contract (Lyra → RTO) The canonical structured shape for a poker hand. **Lyra owns hands** — it produces this shape (LLM parser today; the tap recorder natively, going forward), stores it, replays it in the viewer, and exports it. **RTO consumes it** over HTTP and never reaches into Lyra. Ownership rule: whoever owns the data owns the tools that produce it. Lyra owns the hand DB, the viewer, and the copilot loop, so hand capture lives here. RTO is a pure engine. Coupling: **one arrow, Lyra → RTO, HTTP only.** RTO is a standalone service (solve / exploit / estimate); Lyra POSTs to it when it wants analysis. No shared package, no shared DB, no shared UI components. If RTO is down, Lyra skips analysis and nothing breaks. ## Schema (`schema_version: 1`) ```jsonc { "schema_version": 1, "game": "NLH", // NLH | PLO | ... "stakes": "1/3", // or null "hero_pos": "BTN", // one of POSITIONS "hero_cards": ["Ah", "Kh"], // convenience mirror of the hero's players[].cards "players": [ // every player in the hand, incl. hero {"pos": "BTN", "stack": 300, "name": "Hero", "cards": ["Ah","Kh"], "hero": true}, {"pos": "BB", "stack": 250, "name": "Sal", "cards": null} // cards: null unless shown ], "actions": [ // one flat chronological list across all streets {"street": "preflop", "pos": "BTN", "action": "raise", "amount": 15}, {"street": "flop", "board": ["7d","2c","5h"]}, // a street begins with its board reveal {"street": "flop", "pos": "BB", "action": "check"} ], "board": ["7d","2c","5h"], // full final board, 0–5 cards "result": {"pot": 40, "hero_net": 25, "summary": "one line"}, "completeness": {"cards": true, "board": true, "actions": true} } ``` ### Conventions (load-bearing) - **Cards are lists of 2-char tokens**, `RankSuit`: rank in `23456789TJQKA` (ten = `T`), suit in `c d h s` (lowercase). E.g. `["As","5d","2c"]`. RTO maps each token via `pokercore.parse_card`. *(Chosen over space-joined strings: unambiguous, no re-splitting, and it's what Lyra already stores + what the viewer reads.)* - **Unknown cards are kept, not dropped:** `"Ax"` = known rank / unknown suit, `"x"` = fully unknown card. The LLM parser emits these when Brian didn't state suits. The tap recorder won't — it captures complete cards by construction — so `"x"` is an import/parser-only concern. - **`completeness`** tells a consumer what's safe to use: `cards`/`board` are `true` only when every relevant card is fully specified (no `"x"`). RTO uses `false`-card hands for positions/frequencies/pairs and skips suit-dependent math (flushes). - **Hero appears in `players[]`** with `"hero": true` and is findable via `pos == hero_pos`. `hero_cards` is a mirror for the viewer; `players[].cards` is the source of truth. - **Positions:** `UTG UTG1 UTG2 MP LJ HJ CO BTN SB BB`. - **Actions:** `post fold check call bet raise allin`. `amount` is a plain number (no `$`), null for non-sized actions (fold/check). Street boards appear as `{street, board}` entries. - **Streets:** `preflop flop turn river`. `lyra/poker.py:normalize_structured()` is the single function that guarantees this shape. It runs on store and on read, and is idempotent. ## Transport (HTTP, Lyra serves on :7078) - `GET /hands/data?limit=N` → `{ "hands": [ {id, position, hole_cards, board, result, tag, at, lesson, venue, stakes, has_structured}, ... ] }` — flat list for browsing. Use `has_structured` to pick which hands have a replayable body worth fetching. - `GET /hand/{id}/data` → the full hand row; `structured` is the object above (or `null` for a flat quick-log that hasn't been reconstructed). RTO's "Lyra bridge" (its `docs/estimator-design.md`, Phase B) walks `structured.actions` to classify each villain decision into `checked_to` / `facing_bet` / `facing_raise`, and uses shown `cards` + that street's `board` for board-relative categories. Everything that walk needs is in the schema above.