docs(specs): AD-012 NX save .sav inbox

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Andrew Quenehen
2026-08-03 08:06:16 -03:00
parent 4afb54c54f
commit 7e0c64cb78
3 changed files with 321 additions and 2 deletions
+110
View File
@@ -0,0 +1,110 @@
# STATE
## Decisions
### AD-001
- **Decision**: The Nintendo Switch port runs on pinned love-nx (initially tag `11.5-nx1` with SHA-256-pinned `love.nro`/`love.elf`), not a native libnx/NVK rewrite.
- **Reason**: Gen1Recomp already boots under love-nx; video/audio/input/FS are provided; keeps the patch small and upstreamable.
- **Trade-off**: Native defects may later require a love-nx fork; deferred until a minimal probe proves the bug is below Lua.
- **Scope**: All Switch packaging, runtime, and diagnostics work
- **Date**: 2026-08-01
- **Status**: active
### AD-002
- **Decision**: Platform differences are expressed as capability queries in `src/core/Platform.lua` (e.g. `romImportMode`, `canSpawnProcess`, `networkValidated`), never by overloading Android flags for NX.
- **Reason**: Reusing `self.android` would trigger mobile side effects such as deleting a user-copied ROM.
- **Trade-off**: Slightly more refactor in RomImporter than a one-line OS check.
- **Scope**: Import, updater, shell, conf, any `getOS` branching
- **Date**: 2026-08-01
- **Status**: active
### AD-003
- **Decision**: On NX, ROM import uses a writable inbox under `love.filesystem.getSaveDirectory()/imports/` with explicit rescan; no Horizon native file picker.
- **Reason**: love-nx/Gen1Recomp have no usable Switch picker; issue #531 fails before gameplay.
- **Trade-off**: Users must copy dumps via MTP into the shown path.
- **Scope**: RomImporter UI and scan logic on Switch
- **Date**: 2026-08-01
- **Status**: active
### AD-004
- **Decision**: All Mac↔Switch file transfer for NROs, `game.love`, ROMs, logs, and crash reports uses OpenMTP + DBI `Run MTP responder` on `1: SD Card` only (no SD removal, Finder mount, FTP, or `nxlink` artifact transport).
- **Reason**: Keeps the SD in-console and matches the operators established workflow; avoids false POSIX assumptions.
- **Trade-off**: Transfers are manual/UI-driven; scripts verify hashes locally, not via `/Volumes`.
- **Scope**: Development runbooks, release deploy, diagnostics collection
- **Date**: 2026-08-01
- **Status**: superseded by AD-009
### AD-005
- **Decision**: Release ships a single fused `gen1recomp.nro` (game.love in romfs); loose `nro`+`game.love` is development-only. Payload must never contain ROM, extracted cache, or saves; CI/release pins love-nx and fails on checksum/payload violations.
- **Reason**: Prevents version skew for players and preserves the projects legal/technical model.
- **Trade-off**: Fused builds need devkitPro/container (`nacptool`/`elf2nro`).
- **Scope**: `scripts/build_switch.sh`, verify gates, release artifacts
- **Date**: 2026-08-01
- **Status**: active
### AD-006
- **Decision**: On NX, community mod `.zip` import uses a writable inbox at `love.filesystem.getSaveDirectory()/imports/mods/` with explicit rescan; separate from ROM `imports/`.
- **Reason**: Mirrors AD-003 without mixing ROM dumps and mod archives; no Horizon picker.
- **Trade-off**: Users must MTP zips into the shown path; FIND MODS remains off (`networkValidated`).
- **Scope**: RomImporter MODS tab, Switch docs, related NX tests
- **Date**: 2026-08-01
- **Status**: active
### AD-007
- **Decision**: Switch fused packaging uses host `nacptool`/`elf2nro` when available (including via `$DEVKITPRO/tools/bin`), then falls back to Docker using the image pin in `scripts/switch/dkp-docker.image` (override `GEN1_DKP_IMAGE`); CI never compiles love-nx from source.
- **Reason**: Matches contributor decision 2B and Mac self-hosted release (3A) while staying portable when only Docker exists.
- **Trade-off**: Two packaging paths to maintain; Docker bind-mount quirks on some Windows bash setups.
- **Scope**: `scripts/build_switch.sh`, `scripts/switch/build_fused.sh`, release Switch artifact, switch-build docs
- **Date**: 2026-08-01
- **Status**: active
### AD-008
- **Decision**: Switch packaging entrypoints remain bash scripts; supported Windows hosts are Git Bash, MSYS2 (devkitPro), or WSL — not cmd.exe or PowerShell-native rewrites.
- **Reason**: All existing pack/release scripts are bash; a parallel PowerShell stack would diverge.
- **Trade-off**: Windows contributors must use a bash environment (documented in switch-build.md).
- **Scope**: Switch build scripts and docs; any future NX packaging helpers
- **Date**: 2026-08-01
- **Status**: active
### AD-009
- **Decision**: Canonical Switch file transfer for NROs, loose `game.love`, ROM inbox, mod zips, logs, and crash pulls is any method that lands bytes at the documented SD / save-dir paths: **MTP** (e.g. DBI `Run MTP responder` + an MTP client), **direct SD** (Hekate UMS and/or physical microSD reader), or **FTP** (any Switch-side FTP homebrew that exposes the SD). macOS + OpenMTP is one documented example, not the product contract. **`nxlink` / hbmenu netloader remains deferred** — not a supported path yet (future contributor fast-loop only).
- **Reason**: Contributors on Linux/Windows (and Mac users who prefer UMS/FTP) must not be blocked by an OpenMTP-only narrative; destinations matter, not the host tool.
- **Trade-off**: More transfer recipes to maintain; FTP/SD details stay destination-first with example apps only. No automated push scripts in this decision.
- **Scope**: Switch transfer/install/development docs, contributor runbooks, future deploy tooling decisions
- **Date**: 2026-08-01
- **Status**: active
### AD-010
- **Decision**: Switch CI mirrors the iOS safety net: path-gated offline selftest on `ubuntu-latest` for all repos (forks included); fused `--fetch --fused` + artifact `gen1recomp-switch-nro` only on the canonical repo (`bryanthaboi/gen1recomp`) self-hosted Mac runner; PR artifact comment mirrors iOS (`switch-build-result`); release Switch remains a hard-fail gate (no `continue-on-error`).
- **Reason**: Catch packaging regressions before merge without requiring Switch toolchain on hosted runners for forks; keep ship integrity on `main` while giving maintainers a downloadable fused NRO on path-gated PRs.
- **Trade-off**: Extra self-hosted Mac CI when Switch paths change on the canonical repo; forks never get a fused CI artifact.
- **Scope**: `.github/workflows/ci.yml`, `switch-artifact-comment.yml`, `release.yml` Switch step, Switch CI docs
- **Date**: 2026-08-02
- **Status**: active (amended by AD-011 for fork→canonical PRs)
### AD-011
- **Decision**: Switch fused CI (`switch-build`) runs on the canonical self-hosted Mac only when the workflow head is the canonical repo: same-repo push/PR. Fork→canonical pull requests skip Switch fused (ubuntu selftest still runs). iOS `ios-build` eligibility is unchanged by this decision.
- **Reason**: Avoid executing untrusted fork head packaging scripts on the self-hosted Mac while keeping offline Switch verification for external PRs.
- **Trade-off**: Reviewers do not get a Switch NRO artifact on fork PRs; they still get selftest + (when iOS paths change) iOS artifacts as before.
- **Scope**: `.github/workflows/ci.yml` `switch-build` `if:`, Switch CI docs
- **Date**: 2026-08-02
- **Status**: active
### AD-012
- **Decision**: On NX, raw Gen1 `.sav` import uses a writable inbox at `love.filesystem.getSaveDirectory()/imports/saves/` with **Import save** ensuring the dir and rescanning; export success surfaces `exports/` via an MTP path notice (no `openURL` / Open folder). Resilience guards RES-01..11 in `.specs/features/switch-save-sav-inbox/spec.md` apply (nested ensure, AppleDouble skip, retain inbox bytes, `isNX`-only branching, inbox isolation, non-silent notices).
- **Reason**: love-nx has no usable Horizon file picker (same scar as AD-003/AD-006); players need MTP/SD/FTP parity for continuing cart/PC saves and pulling slots off-console.
- **Trade-off**: Users must copy `.sav` into the shown inbox and pull exports manually; desktop/Android picker paths stay unchanged.
- **Scope**: RomImporter SAVE FILES on Switch, Switch install/transfer/development/launcher docs, `tests/rom_importer_nx_saves_inbox_test.lua`
- **Date**: 2026-08-03
- **Status**: active
## Handoff
- **Feature**: switch-save-sav-inbox / `.specs/features/switch-save-sav-inbox`
- **Phase / Task**: Execute nearly complete — pending Verifier
- **Completed**: T1T5 (inbox + Import/Export + docs + AD-012); T6 gate next or in flight
- **In-progress**: T6 full gate / runner wiring
- **Next step**: Verifier sub-agent after T6
- **Blockers**: none
- **Branch**: `feat/switch-nx`
- **Report**: pending
@@ -0,0 +1,208 @@
# Switch Save (.sav) Inbox — Specification
**Related:** `.specs/features/switch-port-love-nx/` (ROM inbox), `.specs/features/switch-mod-zip-inbox/` (mod zip inbox)
**Context:** `.specs/features/switch-save-sav-inbox/context.md`
**Tasks:** `.specs/features/switch-save-sav-inbox/tasks.md`
**Status:** Execute nearly complete — T1T5 done; pending T6 + Verifier
## Problem Statement
On Switch, **Import save** / **Export save** rely on a native file picker (desktop HostShell or Android SAF). love-nx has no usable explorer, so Import is a silent no-op and Export has no player-facing pull path. Players who want to continue a cart save on Switch (or take a slot off-console as `.sav`) need the same MTP inbox + rescan pattern already shipped for ROMs and mod zips — including the same resilience against Mac MTP junk, nested dirs, and silent failures that burned the ROM/mod paths.
## Goals
- [ ] Import a valid 32 KB Gen1 `.sav` from `imports/saves/` via MTP + **Import save** rescan on NX into a new active slot
- [ ] Export the active slot to `exports/` and surface an MTP-oriented path notice on NX (no `openURL` dependency)
- [ ] Document inbox + export destinations in Switch install / transfer / development docs and launcher.md
- [ ] Headless tests mirror `rom_importer_nx_mods_inbox_test.lua` resilience cases (AppleDouble, retain, nested ensure, no HostShell, isolation)
## Out of Scope
| Feature | Reason |
| ------- | ------ |
| Horizon native file picker | Unavailable; inbox only (AD-003/AD-006) |
| Changing desktop/Android Import/Export | Already works |
| Changing SaveConvert / slot format | Existing glue; NX only wires inbox |
| Deleting inbox `.sav` after import | Retain policy matches ROM/mod |
| Hardware OLED smoke as CI gate | Optional P2 evidence only |
| Reusing `self.android` for NX | Forbidden by AD-002 |
---
## Assumptions & Open Questions
| Assumption / decision | Chosen default | Rationale | Confirmed? |
| --------------------- | -------------- | --------- | ---------- |
| Save inbox path | `getSaveDirectory()/imports/saves/` | User chose 1A (separate from ROM + mods) | y |
| Import save button | Ensure dir + immediate rescan | User chose 2A (mirrors mod Import) | y |
| Retain `.sav` after import | Keep in inbox | Matches ROM dump / mod zip retain | y (assumption) |
| Export UX on NX | Notice + MTP hint to `exports/`; no openURL | Picker/`openURL` useless on NX | y (assumption) |
| Multi-file rescan | Import each real `*.sav`; overall notice like mod rescan | Mirrors `rescanModsAction` | y (assumption) |
| Transfer methods | MTP / SD / FTP to save-dir paths (AD-009) | Inherited | y |
| Platform branching | `isNX` / `Platform.isNX()` only — never `android` | AD-002 | y |
**Open questions:** none — all resolved or logged above.
---
## Resilience / Regression Guards (from ROM & mod scars)
These are **hard requirements**, not soft tips. They encode failures already hit on NX MTP:
| Guard ID | Scar (ROM/mod) | Required behavior for `.sav` inbox |
| -------- | -------------- | ---------------------------------- |
| RES-01 | Nested `createDirectory` fails without parent | `ensureSavesInboxDir` SHALL call `ensureImportsDir` before creating `imports/saves/` |
| RES-02 | Mac MTP AppleDouble `._*.gb` / `._*.zip` blocked scans | Scan SHALL skip names starting with `.` (including `._foo.sav`); AppleDouble-only inbox ≡ empty |
| RES-03 | AppleDouble sibling invented a “mixed failure” line | Skipping `._*` SHALL NOT count as an import failure in the notice |
| RES-04 | Silent no-op when picker missing | On NX, **Import save** SHALL always set `saveNotice` (empty hint, success, or failure) — never return with no feedback |
| RES-05 | Deleted user MTP drop after “success” | Success and failure SHALL retain inbox `.sav` bytes (no `remove` of user drops) |
| RES-06 | HostShell / desktop dialog on NX | `chooseSaveImport` on NX SHALL NOT call `chooseSav` / HostShell / Android `pickFile` |
| RES-07 | Wrong flag (`android`) triggered ROM delete side effects | NX path SHALL use `isNX` only (AD-002) |
| RES-08 | Cross-contamination of inboxes | Save scan: only `imports/saves/*.sav`. ROM `scanInbox` / mod `scanModsInbox` SHALL ignore `.sav`. Save scan SHALL ignore `.gb`/`.gbc`/`.zip` |
| RES-09 | Export “Open folder” / `openURL` useless or crashy on NX | NX export success SHALL set notice + MTP hint; SHALL NOT require or call `openURL` |
| RES-10 | Docs omitted `._*` MTP tip | Switch docs MTP tip SHALL mention `._*.sav` alongside ROM/mod sidecars |
| RES-11 | Default hint still said “system file picker” | On NX, SAVE FILES default hint SHALL mention `imports/saves/` / MTP (not desktop picker wording) |
---
## Implicit-Requirement Dimensions Sweep (Medium)
| Dimension | Resolution |
| --------- | ---------- |
| Input validation & bounds | Only non-hidden `*.sav`; size/checksum via `SaveFileIO.importToSlot` / SaveConvert |
| Failure / partial-failure | Red notice; retain file; RES-04 forbids silent failure |
| Idempotency / retry | Rescan may re-import same file → new slot each success; acceptable |
| Auth / rate limits | N/A — local MTP only |
| Concurrency / ordering | Single-threaded; MTP with app closed when copying |
| Data lifecycle | Retain inbox `.sav`; export files user-managed under `exports/` |
| Observability | `saveNotice` always set on NX Import/Export outcomes |
| External-dependency failure | N/A — no network |
| State-transition integrity | Import requires ROM ready for panel version (existing guard) |
**Remaining dimensions N/A for this scope.**
---
## User Stories
### P1: NX `.sav` import inbox ⭐ MVP
**User Story**: As a Switch player, I want to copy a Gen1 `.sav` into a shown inbox folder and press Import save so it becomes a playable slot without a file picker.
**Why P1**: Without this, continuing a cart / PC save on Switch is blocked.
**Acceptance Criteria**:
1. WHEN the user activates **Import save** on NX THEN system SHALL ensure `imports/saves/` exists (**parent `imports/` first** — RES-01) and SHALL scan that folder for non-hidden `*.sav` files (RES-02)
2. WHEN the inbox is empty (including AppleDouble-only) THEN system SHALL show a notice with the save-dir path and an MTP-oriented hint for `imports/saves/` (RES-04) and SHALL NOT call HostShell/`chooseSav` (RES-06)
3. WHEN a valid `.sav` is present and the panel versions ROM is ready THEN system SHALL import it via `SaveFileIO.importToSlot` into a new slot, refresh the SAVE SLOT list, show a success notice, and **retain** the inbox file (RES-05)
4. WHEN import fails (wrong size, bad checksum, ROM not ready, read error) THEN system SHALL show a clear red notice and SHALL NOT delete the users `.sav` from `imports/saves/` (RES-05)
5. WHEN NX is active THEN system SHALL branch on `isNX` only (RES-07) and SHALL NOT require a desktop or Android file picker (RES-06)
6. WHEN `._foo.sav` sits beside a real `foo.sav` THEN system SHALL import only the real file and SHALL NOT append a sibling “failed” line for the AppleDouble (RES-03)
7. WHEN the SAVE FILES card is shown on NX with no prior notice THEN the default hint SHALL mention the `imports/saves/` MTP path, not a system file picker (RES-11)
**Independent Test**: `tests/rom_importer_nx_saves_inbox_test.lua` (mirror mods suite) — see tasks.md Test Coverage Matrix.
---
### P1: NX export path notice ⭐ MVP
**User Story**: As a Switch player, I want Export save to write a `.sav` I can pull via MTP and to tell me where it landed.
**Why P1**: Export already writes via `love.filesystem`; without a path hint the file is invisible.
**Acceptance Criteria**:
1. WHEN the user activates **Export save** on NX with an existing active slot THEN system SHALL write `exports/gen1recomp-<version>-<slotId>.sav` (existing `SaveFileIO.exportActiveSlot` behavior)
2. WHEN export succeeds on NX THEN system SHALL show a success notice that includes the exports path and an MTP-oriented hint and SHALL NOT call `love.system.openURL` (RES-09)
3. WHEN export fails (no save) THEN system SHALL show the existing failure notice (RES-04 — not silent)
**Independent Test**: NX-flagged unit case in the same saves-inbox test file.
---
### P1: Inbox isolation ⭐ MVP
**User Story**: As a player, I want ROMs, mods, and saves in separate inboxes so one file type cannot break anothers scan.
**Why P1**: Cross-contamination was a class of MTP confusion; AD-006 already separated mods.
**Acceptance Criteria**:
1. WHEN scanning the save inbox THEN system SHALL ignore `.gb` / `.gbc` / `.zip` under `imports/saves/` (RES-08)
2. WHEN ROM `scanInbox` or mod `scanModsInbox` runs THEN they SHALL NOT treat `imports/saves/*.sav` as ROM/mod candidates (RES-08)
**Independent Test**: Isolation cases in the NX saves inbox test file.
---
### P1: Docs — save inbox + export destinations ⭐ MVP
**User Story**: As a Switch operator/player, I want documented paths so I can move `.sav` files the same way as ROMs and mods.
**Why P1**: Missing docs blocks adoption of the inbox.
**Acceptance Criteria**:
1. WHEN reading `docs/switch-install.md` THEN it SHALL document copying a `.sav` into `imports/saves/` and using **Import save**, plus pulling exports from `exports/`
2. WHEN reading `docs/switch-transfer.md` THEN the destinations table SHALL list the save inbox and exports folder
3. WHEN reading `docs/switch-development.md` THEN it SHALL note the NX save inbox alongside ROM/mod inboxes
4. WHEN reading `docs/launcher.md` Import/Export section THEN it SHALL mention the NX inbox path (not only desktop/Android pickers)
5. WHEN reading Switch MTP tips THEN they SHALL mention ignoring / deleting `._*.sav` AppleDouble sidecars (RES-10)
**Independent Test**: Doc review checklist in tasks.
---
### P2: Project decision AD-012
**User Story**: As a maintainer, I want the NX save inbox recorded in `.specs/STATE.md` like AD-003/AD-006.
**Why P2**: Keeps platform decisions discoverable for future import work.
**Acceptance Criteria**:
1. WHEN the feature ships THEN STATE.md SHALL include an active decision: NX raw `.sav` import uses `imports/saves/` + Import-save rescan; export surfaces `exports/` via MTP hint; resilience guards RES-01..11 apply
**Independent Test**: STATE.md review.
---
## Edge Cases
- WHEN `imports/saves/` contains only `._foo.sav` / hidden names THEN system SHALL treat as empty and show MTP notice (RES-02, RES-04)
- WHEN ROM is not imported for the panel version THEN system SHALL refuse with the existing “Import the … ROM before importing a save” notice (no silent no-op)
- WHEN multiple valid `.sav` files exist THEN system SHALL attempt each; success wins overall when any ok; real failures still surface (mod-rescan pattern); AppleDouble never counts as failure (RES-03)
- WHEN not on NX THEN Import save / Export save SHALL keep existing desktop/Android behavior
- WHEN `workState == "working"` THEN Import/Export SHALL no-op without clearing an existing useful notice (same as other launcher actions)
---
## Requirement Traceability
| Requirement ID | Story | Phase | Status |
| -------------- | ----- | ----- | ------ |
| NXSAV-01 | P1: Import ensure + scan (RES-01/02) | Tasks | Pending |
| NXSAV-02 | P1: Empty / AppleDouble-only MTP notice (RES-04/06) | Tasks | Pending |
| NXSAV-03 | P1: Valid `.sav` → slot + retain (RES-05) | Tasks | Pending |
| NXSAV-04 | P1: Failure notice + retain (RES-05) | Tasks | Pending |
| NXSAV-05 | P1: No HostShell; `isNX` only (RES-06/07) | Tasks | Pending |
| NXSAV-06 | P1: AppleDouble sibling no false failure (RES-03) | Tasks | Pending |
| NXSAV-07 | P1: Default NX SAVE FILES hint (RES-11) | Tasks | Pending |
| NXSAV-08 | P1: Export writes `exports/` | Tasks | Pending |
| NXSAV-09 | P1: Export NX MTP notice; no openURL (RES-09) | Tasks | Pending |
| NXSAV-10 | P1: Inbox isolation (RES-08) | Tasks | Pending |
| NXSAV-11 | P1: Docs + `._*.sav` MTP tip (RES-10) | Tasks | Pending |
| NXSAV-12 | P2: AD-012 in STATE.md | Tasks | Pending |
**Coverage:** 12 total — see `tasks.md` for mapping.
---
## Success Criteria
- [ ] On NX, Import save never silently no-ops; empty/AppleDouble-only → path hint; valid file → new slot; junk skipped without fake failures
- [ ] Nested `imports/saves/` creates reliably; user `.sav` never auto-deleted
- [ ] On NX, Export save success notice points at `exports/` for MTP pull without `openURL`
- [ ] Switch + launcher docs mention `imports/saves/`, `exports/`, and `._*.sav`
- [ ] `rom_importer_nx_saves_inbox_test.lua` passes with the resilience matrix in tasks.md
@@ -2,7 +2,7 @@
**Spec:** `.specs/features/switch-save-sav-inbox/spec.md`
**Context:** `.specs/features/switch-save-sav-inbox/context.md`
**Status:** Execute in progress — Batch 1 (T1T3) complete; T4 docs complete; remaining T5T6
**Status:** Execute nearly complete — T1T5 done; pending T6 gate + Verifier
---
@@ -76,12 +76,13 @@
- **Commit**: `docs(nx): save .sav inbox and exports paths`
- **Status**: ✅ complete
### T5: AD-012 in STATE.md + handoff
### T5: AD-012 in STATE.md + handoff
- **What**: Record AD-012 (inbox path, rescan on Import save, export MTP hint, RES guards); update Handoff for this feature.
- **Done when**: STATE.md lists AD-012 active; Handoff points at this feature.
- **Requires**: T4
- **Reqs**: NXSAV-12
- **Commit**: `docs(specs): AD-012 NX save .sav inbox`
- **Status**: ✅ complete
---