diff --git a/.specs/STATE.md b/.specs/STATE.md new file mode 100644 index 00000000..e0d585d8 --- /dev/null +++ b/.specs/STATE.md @@ -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 operator’s 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 project’s 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**: T1–T5 (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 diff --git a/.specs/features/switch-save-sav-inbox/spec.md b/.specs/features/switch-save-sav-inbox/spec.md new file mode 100644 index 00000000..4d156db1 --- /dev/null +++ b/.specs/features/switch-save-sav-inbox/spec.md @@ -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 — T1–T5 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 version’s 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 user’s `.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--.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 another’s 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 diff --git a/.specs/features/switch-save-sav-inbox/tasks.md b/.specs/features/switch-save-sav-inbox/tasks.md index 01618c6d..b14e98b6 100644 --- a/.specs/features/switch-save-sav-inbox/tasks.md +++ b/.specs/features/switch-save-sav-inbox/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 (T1–T3) complete; T4 docs complete; remaining T5–T6 +**Status:** Execute nearly complete — T1–T5 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 ---