docs: correct two false claims in the Phase Ε spec

REAPER's GetUserFileName serves both pickers — the no-save-picker finding was a
regex miss. A new enum value is structural, not additive: BankModel rejects an
out-of-range SourceMode/Tier rather than degrading.
This commit is contained in:
2026-08-02 12:38:41 -04:00
parent 2f8e88880e
commit 2cb0b52884
2 changed files with 83 additions and 35 deletions
+50 -22
View File
@@ -2451,15 +2451,20 @@ root `CMakeLists.txt`. **Does not own:** `export_plan` / `import_plan` (Ε-W2),
- **Two version integers, not one.** `formatVersion` = what this writer emitted;
`minReaderVersion` = the oldest reader that can read it safely. The reader's whole rule is
`minReaderVersion <= kPackageFormatVersion`. An **additive** change (a new optional
manifest key, a new enum value with a defined degrade) bumps `formatVersion` only; a
**structural** change bumps both. The header carries the writer's semver
manifest key, a new `Sample` field with a defined absent-value) bumps `formatVersion`
only; a **structural** change bumps both. **Growing a persisted enum's vocabulary is
structural here, not additive** — `BankModel::deserialize` *rejects* an out-of-range
`SourceMode` or `Tier` rather than degrading it (`bank_model.cpp:232-239`, `:339-346`),
and every enum a package carries rides inside the nested `BankModel` blob, so a new
`SourceMode` or `Tier` value bumps both integers. The header carries the writer's semver
(`version::stampVersion()`) alongside them, informational, so a refusal message can name
what to install.
- **The ladder is documented the way `origin_ledger.cpp:8-21` documents its own** — a header
comment listing every shipped version and what changed, with the read-and-validate rule
stated, not implied.
- **Unknown manifest keys are skipped** (the `bank_book_json.cpp:182` behaviour), and
**unknown persisted enum integers degrade to their defined `Unknown` equivalent**, never to
**unknown persisted enum integers — the manifest's own, not `BankModel`'s nested ones, which
reject per the bullet above — degrade to their defined `Unknown` equivalent**, never to
the numeric default and never to a parse failure (`core/wire/CLAUDE.md`'s `BakeStatus` rule,
verbatim). Both are pinned by tests, not left to inheritance.
- **The manifest nests `BankModel`'s own serialization verbatim**, exactly as
@@ -2510,9 +2515,11 @@ parallel with the format it will carry.
**Surface boundary — owns:** new `src/shell/package/package_io` (read a file's bytes, write
bytes through temp + atomic rename, read one bank file, write one landed file, enumerate the
bank folder's existing names, and the rollback delete), the platform file-picker seam under
the `#ifdef _WIN32` / `#else swell/swell.h` split this codebase already uses
(`src/shell/panel/draw_kit.cpp:11-15`, `src/shell/persist/prune_fs.cpp:35-38`), the
bank folder's existing names, and the rollback delete), the file-picker seam for both verbs —
**one picker, REAPER's own, on every platform**, so this track carries **no**
`#ifdef _WIN32` / `#else swell/swell.h` split. That split is a real pattern in this codebase
(`src/shell/panel/draw_kit.cpp:11-15`, `src/shell/persist/prune_fs.cpp:35-38`); it is simply
not this track's shape, because REAPER owns the dialog. Also owns the
directory's `CMakeLists.txt` and `CLAUDE.md`, and one appended `add_subdirectory` line in the
root `CMakeLists.txt`. **Does not own:** `export_bank` / `import_bank` (Ε-W2), anything under
`core/`, and — emphatically — `prune_fs`, which stays the deletion authority.
@@ -2529,12 +2536,22 @@ root `CMakeLists.txt`. **Does not own:** `export_bank` / `import_bank` (Ε-W2),
`src/shell/persist/prune_fs.cpp:5-11` and states the discriminator it satisfies — this call
created the file, and no index ever referenced it — rather than restating the carve-out's
text. Anything that does not satisfy that discriminator is not this function's business.
- **The two pickers are asymmetric, and the asymmetry is real.** Import uses REAPER's own
`GetUserFileNameForRead(char* filenameNeed4096, const char* title, const char* defext)`
**verified**, `vendor/reaper-sdk/sdk/reaper_plugin_functions.h:3798`. There is **no save
picker in the REAPER API** — a sweep of that header for `FileNameFor|SaveFile|Browse`
returns only the read picker — so export uses Win32 `GetSaveFileNameW` on Windows and
SWELL's `BrowseForSaveFile` elsewhere (`vendor/WDL/WDL/swell/swell-functions.h:167`).
- **Both pickers are REAPER's own, and they are symmetric.** `GetUserFileName(int mode,
const char* caption, const char* initial_file_or_path, const char* extension_list, char*
fnOutNeedBig, int fnOutNeedBig_sz)` — **verified**,
`vendor/reaper-sdk/sdk/reaper_plugin_functions.h:3790`, documented at `:3788` — serves
both directions: `mode=0` ("choose a new file") is export's destination picker, `mode=1`
("existing file") is import's source picker. `extension_list` takes the
`'ReaSampler banks|*.rsbank|All files|*.*'` form, and `initial_file_or_path` may be a bare
`'.rsbank'` to set the default extension. There is **no `#ifdef _WIN32` / SWELL split and
no wide-char round trip here** — REAPER owns the dialog on every platform, so no
`GetSaveFileNameW` and no `BrowseForSaveFile`. `GetUserFileNameForRead` is explicitly
"Superseded, see GetUserFileName" (`:3796`) and is not used.
- **No fallback path.** `src/app/main.cpp:15` defines `REAPERAPI_IMPLEMENT` *without*
`REAPERAPI_MINIMAL`, so the resolver walks the full table — `GetUserFileName` included
(`reaper_plugin_functions.h:9084`) — and `main.cpp:292-293` aborts the extension load if
any single function fails to resolve. No REAPER build that loads this extension can lack
`GetUserFileName`, which makes a fallback unreachable code.
- **No REAPER project state is touched here.** No ext-state read or write, no undo block, no
generation bump; those belong to the verbs in Ε-W2.
@@ -2545,16 +2562,27 @@ root `CMakeLists.txt`. **Does not own:** `export_bank` / `import_bank` (Ε-W2),
test asserts against a seam counter, not against a memory measurement.
- The rollback deletes exactly the files it was given and nothing else, and is a no-op on a
path it did not write.
- The Windows and SWELL picker paths both compile; the SWELL signature matches
`swell-functions.h:167` exactly. `[verify — DAW]` — neither picker is exercised in a live
REAPER session by this track.
- No file exceeds ~600 lines; the platform picker lives in its own TU, following the
`drag_out` / `drag_out_win` precedent.
- **There is exactly one picker call site and it is REAPER's.** Both verbs reach
`GetUserFileName` — export with `mode=0`, import with `mode=1` — and no symbol named
`GetSaveFileNameW`, `BrowseForSaveFile`, or `GetUserFileNameForRead` appears anywhere in
`src/shell/package/`, nor any platform `#ifdef` in the picker's TU. Greppable, so it stays
true. `[verify — DAW]` — the picker is not exercised in a live REAPER session by this track.
- No file exceeds ~600 lines; the picker lives in its own TU with its own header. **Not** for
the `drag_out` / `drag_out_win` reason — that precedent isolates a *Win32-only* TU, and
there is no platform split here — but because the picker is the only REAPER-facing part of
an otherwise REAPER-free, unit-tested module: folding it into a shared header would drag
`reaper_plugin_functions.h` into the testable seam's include graph.
**Open questions.** **[propose at review]** whether the file-picker seam is its own module or
part of `package_io` — the `drag_out_win` precedent argues its own TU; whether it also wants
its own header is a judgment call at the size it lands. **[verify — DAW]** the default
extension and filter strings each platform's picker actually accepts.
**Open questions.** **[propose at review]** where the `extension_list` and default-extension
strings live — this track's picker TU, or the Ε-W2 verbs that call it. They are user-facing
text, and the verbs own the rest of the user-facing text; the counter-argument is that they
are picker plumbing and only one picker exists. **[verify — DAW]** two things the header does
not answer: (1) whether the `mode=0` picker **appends** an extension when the user types a
bare name — `:3788` documents that `initial_file_or_path` may be `'.rsbank'` "to set the
default extension," but not that the dialog enforces it on return, so the verb may still have
to append `.rsbank` itself; (2) **dialog parenting**`GetUserFileName`'s signature
(`:3790`) takes no owner window, where the abandoned Win32 `OPENFILENAME` path would have
passed `GetMainHwnd()`, so modality against the REAPER main window is unobserved.
#### Ε-W1-T3 — `import-origin-kind`
@@ -4321,7 +4349,7 @@ Phase Epsilon — The bank package (none of the seventeen; a direct reque
T1 package-format .............. core/package: framing + TWO version ints
[E-F1 RULED: proprietary RSBK. No ZIP, no zlib]
T2 package-fs-shell ............ shell/package: atomic write, streaming, pickers
[no REAPER save-picker exists; SWELL/Win32 split]
[ONE picker: REAPER GetUserFileName, mode 0/1]
T3 import-origin-kind .......... OriginKind::PackageImport = 5, append-only
W2 The two verbs [2 tracks; disjointness CONDITIONAL — see below]
T1 bank-export ................. export_plan + verb + action; project untouched