Status: fixed
Decision (2026-08-02, triage with maintainer): Approved, and widened. Both cautions that made this needs-triage dissolved on inspection:
1. The "must match the Python engine's keys" constraint in the mcts.rs header is dead documentation — ai/mcts.py no longer exists (retired with the port scaffolding, ticket 19). The only Python _action_key left is a local JSON-dump helper in tests/test_mcts2_legality.py:59, which does not constrain tree keys.
2. Per-target keys are sound under determinisation for the decider's own zones: determinise (rust/netrunner-core/src/ai/determinise.rs:28) shuffles only the *opponent's* hidden info — the searcher's own zones are fixed (asserted by the test at determinise.rs:538). So a card_index into the decider's own HQ/grip/Archives names the same card in every determinisation. This is exactly why precision_design:{source}:{card_index} already works.
Scope — six actions, not two. Ticket-39's decisions share the identical flaw (every target collapses onto one type-only edge via the fallback at ai/mcts.rs:138) and the identical soundness argument:
| Action | Decider's zone | Key |
|---|---|---|
| ArchivesRecur | Corp Archives | archives_recur:{source}:{card_index} |
| SprintReturn | Corp HQ | sprint_return:{card_index} |
| LongevityTrash | Corp HQ | longevity_trash:{card_index} |
| HanseiTrash | Corp HQ | hansei_trash:{card_index} |
| AnoeticTrash | Corp HQ | anoetic_trash:{card_index} |
| CarnivoreTrash | Runner grip | carnivore_trash:{card_index} |
Decisions on the open design points:
precision_design pattern. ArchivesRecur has no card_code field anyway, and in the mcts2 observer tree index keys bound fan-out at zone size, where code keys would fan out across every code the determiniser deals in.action_key, no per-observer redaction. No such machinery exists today for any action; if observer-specific keying ever proves necessary it is a separable follow-up.DoneArchivesRecur / DoneLongevityTrash keep stable type-only keys.Definition of done:
action_key gives each of the six actions a distinct per-target key as tabled above; single- and double-tree MCTS can expand and select each target.mcts.rs (header lines ~3–5 and the action_key doc comment) are rewritten to state the actual policy: keys are Rust-internal tree identity; aggregate actions whose indices point into zones hidden from the decider (they shuffle across determinisations); give distinct keys to targets in the decider's own zones.action_key fallback action that represents a genuine target choice (at minimum AboveTheLawTarget, SeamlessTarget, RetributionTarget, and the search-card family ChooseSearchCard/InstallFoundCard/GripFoundCard, whose R&D-search indices differ per determinisation and need their own analysis) and file the findings as one new needs-triage ticket.cargo fmt, cargo clippy --workspace -- -D warnings, and cargo test --workspace pass from rust/.
2026-08-02: Filed from the review of ticket 41. This is marked for triage because action_key documents compatibility with the Python reference and deliberately aggregates some indices across determinizations; maintainers should confirm the target-key policy before changing that shared tree identity.
2026-08-02: Implemented. All six actions key per target in ai/mcts.rs::action_key; mcts2::observed_action_key falls through to the shared key for all six, so both trees agree. The stale Python-compat claims in the module header and the action_key doc comment are replaced with the aggregate-hidden / distinguish-own-zone policy. Four new unit tests cover three Archives targets (including the archives_fd / archives_fu split), two Sprint targets, all four ticket-39 trash actions, the two type-only Done* companions, and the both-trees-agree property. The fallback audit is filed as ticket 45 (needs-triage). cargo fmt / clippy -D warnings / cargo test --workspace green (144, up from 140); pytest -q 134 passed, unchanged.
2026-08-02: Triaged with the maintainer (grilling session). Approved; scope widened from ArchivesRecur/SprintReturn to all six own-zone card-choice decisions; index-based keys; same identity in both trees; stale Python-compat docs rewritten in-scope; remaining fallback tail handled as an audit + follow-up ticket rather than widening further. Status flipped to ready-for-agent.