c0bf57bf8d
The visible label was right but the contract behind it was not. The clamp lived in three places — DecisionOffer.callAmount (unclamped, so it still reported the full amount owed), buttonsFor(), and PokerViewModel — and only the presentation copy was tested. Three copies of a rule is how the original mismatch happened. DecisionOffer.callAmount is now minOf(toCall, stack), with callIsAllIn beside it, and both the label and the submitted action read from it. Nothing recomputes the clamp. Engine sanitisation also left an oversized CALL amount unchanged in HandEvent even though commit() caps the commitment, so history described chips that never moved. Since replay and the coach both read that history, calls are now recorded at what was actually committed. Note the arithmetic: heads-up, seat 1 is the big blind, so with a 40 stack facing a raise to 100 the call commits the remaining 30, not 40. My first test asserted 40 and the engine was right. Verified by reverting: both contract fixes fail their tests. Tests: 144 -> 150 (67 engine JVM, 67 Android host, 16 app). Simulation figures unchanged (289.12 / 113.19 / -195.64), chips conserved. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>