# Review Step 2 — On-the-fly Decisions (IMPL_DECISIONS.md) The implementor recorded six decisions made during implementation. Each is evaluated below. --- ## 1. Added `just test` recipe > Added a `just test` recipe so unit tests can be run through the repository's required `just ...` command surface instead of invoking `cargo test` directly. **Verdict: good.** CLAUDE.md explicitly says "Never use normal `cargo ...` commands, use the `just ...` commands instead." A `just test` recipe was a missing primitive, and adding it (`justfile:22-23`: `cargo test --workspace`) closes that gap cleanly. The implementation is the smallest possible — a one-line wrapper that runs the workspace tests. Nothing to argue with. Tiny nit, not blocking: it doesn't take any args. If unit tests grow, you'll want `just test [args]` so the user can target a single test. That's a future polish. --- ## 2. Kept `game-unpack-finished` event name for compatibility > Kept the existing frontend `game-unpack-finished` event name for successful transactional installs. The peer now emits install lifecycle events, but the compatibility event still lets the UI reuse its existing "install complete" path. **Verdict: defensible, but worth a follow-up.** The plan said `game-unpack-finished` "fires only on true transactional install success (peer-driven). It does NOT fire on extract failure or commit-rename failure — those route to `game-install-failed`." Implementation matches this: `PeerEvent::InstallGameFinished` is bridged to `game-unpack-finished` in `src-tauri/src/lib.rs:880-894`, and `InstallGameFailed` is bridged to `game-install-failed`. So the contract is correct. What's questionable is the *name*. There is no longer an "unpack" stage that's distinct from "install" — they're one transaction. A name like `game-install-finished` would be more honest. Keeping the old name makes the React side's `setupUnpackListener` (`src/App.tsx:293-320`) look stale: it's now an install-finished listener wearing an unpack-finished label. The compatibility argument is thin because there is no external consumer of this event — it's an internal name shared between Rust and TS in this repo. The TS side could be renamed in the same change. That said: this is a cosmetic decision and the implementor correctly noted it. Not a blocker. Plan said the event "fires on transactional install success" without prescribing a name. Recommend renaming in a follow-up, but the decision is acceptable as written. --- ## 3. Reused `.lanspread/library_index.json` for per-ID rescan > Implemented watcher rescans by reusing the existing `.lanspread/library_index.json` cache and updating a single game entry in that index. This satisfies the per-ID optimized rescan requirement without adding a second cache format. **Verdict: good call.** The plan said: > ### Optimized rescan (single ID) > 1. Stat `version.ini` (existence + mtime), check `local/` is-a-directory, stat any `*.eti` files at root level. > 2. Compare to cached fingerprint. > 3. Recursive walk only on fingerprint change. It didn't prescribe a storage format — only behavior. Reusing the existing index file (`local_games.rs:111-148`) is the right call: one cache, one schema, one path to keep coherent. `update_index_for_game` (`local_games.rs:394-449`) stats the fingerprint, short-circuits on equality, and rebuilds only when the fingerprint differs. `rescan_local_game` (`local_games.rs:568-589`) is the single-ID entry point. Two minor concerns: - `save_library_index` (`local_games.rs:141-148`) does *not* use the atomic temp+rename pattern. The plan reserves atomic writes for the intent log, so this is consistent — but the index file is at risk of corruption on a crash mid-write. The blast radius is small: corrupt index → rescan rebuilds from scratch on next load. So this is acceptable, just worth knowing. - The index file's `revision` is incremented inside both `scan_local_library` and `rescan_local_game`. If both paths race (fallback scan running while a watcher-triggered rescan finishes), the in-memory views could diverge briefly. They're each protected by reading and writing the same path with no global lock. In practice the gate in `local_monitor.rs:32-36` prevents concurrent rescans for the *same* ID but not across IDs vs. fallback. Worth a thought; not a correctness bug because the next scan reconciles. These caveats don't change the verdict. --- ## 4. Separate `active_downloads` cancellation-token map > Kept a separate `active_downloads` cancellation-token map next to the single `active_operations` table. The operation table is the authoritative state for gates; the token map is only cancellation plumbing for in-flight downloads. **Verdict: pragmatic.** The plan called for a single per-peer operation table. The implementation has it (`active_operations`) and additionally keeps `active_downloads: HashMap` for cancellation tokens (`context.rs:38`). This is justifiable: `CancellationToken` doesn't fit naturally inside `HashMap` because `OperationKind` is `Copy`. Storing the token alongside as a separate map keeps `OperationKind` simple and lets the gate-path (which only needs presence/kind) stay cheap. The download operation guard (`OperationGuard::download`, `context.rs:142-153`) clears both maps on drop. Alternative: store `OperationState { kind: OperationKind, cancel: Option }`. Cleaner conceptually but invasive. The current shape is fine. One thing to verify: the gate-path readers (`local_download_available`, watcher event handler, stream handlers) all check only `active_operations`. They don't need the token map. So the split doesn't leak. Good separation. --- ## 5. Downloaded-but-not-installed routes directly to `InstallGame` > Treated a downloaded-but-not-installed game as immediately installable from Tauri by sending `PeerCommand::InstallGame` directly. A not-downloaded game still uses `GetGame`, and the peer auto-installs after the sentinel commit. **Verdict: matches the plan.** This decision is actually *prescribed* by PLAN.md — see the `install_game(id)` routing rules: > - `version.ini` absent → send `PeerCommand::GetGame { id }` (fetch first; peer auto-fires `InstallGame` on download commit). > - `version.ini` present, `local/` absent → send `PeerCommand::InstallGame { id }` directly (no re-download). > - `version.ini` present, `local/` present → no-op. Looking at `src-tauri/src/lib.rs:88-128`, the implementation matches the plan exactly. So this isn't really a "decision" — it's the plan. The implementor may have flagged it because they wanted to be explicit that they didn't add an intermediate download step. Either way, no concern. --- ## 6. `LocalOnly` badge derived from `installed && !downloaded` > Derived the UI's `LocalOnly` badge from `installed && !downloaded` because the UI-facing `Game` type does not carry the protocol-level `Availability` enum. **Verdict: acceptable, but a smell worth noting.** The protocol-level `Availability` is computed in `build_game_summary` (`local_games.rs:352-356`), then thrown away when `game_from_summary` (`local_games.rs:370-387`) converts to `Game`. The TS side has no `availability` field, so the badge is recomputed from `installed && !downloaded` in `src/App.tsx:742-744`. This works because: - `Ready` ⇔ `downloaded == true` - `LocalOnly` ⇔ `installed && !downloaded` (because by construction we only emit `LocalOnly` when not downloaded) - The third value `Availability::Downloading` is never emitted by `build_game_summary`. So the predicate `installed && !downloaded` covers every case `build_game_summary` will produce. It's correct *today*. The smell: if `Availability::Downloading` ever starts being emitted (or if a future state is added — e.g., `Quarantined`), the UI would silently miss it because the TS side reverse-engineers availability from two booleans. The single source of truth should flow through. A small, future-proofing follow-up is to add `availability: string` to the TS `Game` interface and serialize the protocol value end-to-end. Not a blocker for this change. --- ## Other implementation decisions NOT recorded in IMPL_DECISIONS.md but observed These were made silently. The implementor may want to add them to the doc. ### a. `partition_download_descriptions` keeps version.ini in `transfer_descs` Plan says "partition" into two lists; implementation keeps both in one list and uses an in-memory `version_buffer` to route chunks. The end behavior is correct; the naming is misleading. See REVIEW_STEP_1 §1. ### b. `recover_on_startup` re-runs on every `load_local_library` Re-runs on `SetGameDir` and after every install/uninstall. Plan describes it as a one-shot startup. Likely intentional ("recovery is also a safety net for `SetGameDir`"), but never written down. See REVIEW_STEP_1 §3. ### c. `PeerCommand::UpdateGame` is dead The variant exists but has no sender path. The auto-install path uses `RequestedInstallOperation::Auto` and the Tauri `update_game` command sends `GetGame`. Either remove the variant or wire `update_game` to send it. ### d. `Auto`/`Install` collapse in `run_install_operation` `Auto` and `Install` both pick `Updating` when `local/` exists and `Installing` otherwise (`handlers.rs:356-363`). That means an explicit `PeerCommand::InstallGame` against an already-installed game becomes an update. The user-facing semantics are surprising (calling "Install" produces an update) but the `install_game` Tauri command shields the UI from this by checking `local/` first. Worth recording. --- ## Summary The six recorded decisions are all defensible. Two have follow-up smells worth fixing later (the `game-unpack-finished` legacy name and the UI's reverse-engineered `LocalOnly` predicate). The other four are either pragmatic minor refinements (`just test`, `active_downloads` map split) or directly implement the plan (`InstallGame` routing). There are at least four additional decisions the implementor made but did not record. Recommend adding them to IMPL_DECISIONS.md so the rationale is preserved.