Status: fixed
Decision (2026-08-02, triage with maintainer): Option A. The snapshot emits hook-derived totals; the Python mirror tables are deleted. The original rationale for the mirror ("avoid growing the snapshot schema for a few ints", serialize.py:26–33) lost: the schema cost is 2–3 integer fields once, while the table grows with every bonus card and fails silently in a user-visible way (the client sizes its discard prompt and MU display off these numbers). Field naming decided too: keep the existing snapshot keys (max_hand_size, memory_total) and fill them with the derived values — that is the wire format static/game.js:602,1426–1427 and tests/test_api.py:134 already depend on, since serialize.py overwrites the base values before the client sees them. No consumer anywhere reads the base value. Do not add total_* keys.
What to do:
rust/netrunner-core/src/snapshot.rs, emit hooks::corp_total_hand_size(state) for corp.max_hand_size (snapshot.rs:211), hooks::runner_total_hand_size(state) for runner.max_hand_size (snapshot.rs:234), and hooks::runner_total_memory(state) for runner.memory_total (snapshot.rs:236)._MU_BONUS, _HAND_SIZE_BONUS, _rig_codes, _runner_total_memory, _runner_total_hand_size, _corp_total_hand_size and the "mirrors hooks.rs" header comment from server/serialize.py (lines ~25–66); the call sites at serialize.py:367, 389–390 become plain pass-throughs of the snapshot fields.Definition of done:
git grep -i "hand_size_bonus\|mu_bonus" -- '*.py' is clean.card_30035 assertions in rust/netrunner-core/src/cards/sg.rs:2882); a pytest asserts the serialized snapshot reflects a bonus card end-to-end (e.g. Precision Design identity → corp.max_hand_size == 6, already the shape of tests/test_api.py:134).cargo fmt, cargo clippy --workspace -- -D warnings, cargo test --workspace (from rust/), and uv run pytest -q (from the repo root).
2026-08-01: Filed from the ticket 40 review, which flagged the duplication but kept it in scope-neutral form. The flag named only corp_total_hand_size; this ticket widens scope to all three mirrored totals since they share the tables and the same drift risk — deciding for one decides for all.
2026-08-02: Fixed as decided (Option A, existing key names). snapshot.rs now emits hooks::corp_total_hand_size / runner_total_hand_size / runner_total_memory under max_hand_size / memory_total; the mirror tables and helpers are gone from server/serialize.py, whose call sites are plain pass-throughs. Coverage: the existing end-to-end test_precision_design_identity_serializes_a_six_card_corp_hand_limit (tests/test_api.py) now runs through the derived path, plus a new Rust test snapshot_reports_hook_derived_totals (cards/sg.rs) covering the runner MU/hand-size fields, which no pytest can reach without a scripted rig. Noted in the snapshot.rs module doc that port-era recorded traces can differ on these two keys under netrunner-replay. Suite green: cargo fmt/clippy/test (135 core tests) and uv run pytest -q (134 passed).
2026-08-02: Triaged with the maintainer (grilling session). Option A chosen over keeping the mirror with a drift test — the drift test would be more machinery (dumping registry tables across the FFI boundary) than simply emitting the totals hooks.rs already computes. Keep-existing-keys naming chosen over new total_* keys because every consumer already receives derived values under the existing names and the base values are provably unused. Status flipped to ready-for-agent.