docs: record Phase E waves 1 and 2 as landed

Backfills W1, which was skipped when it merged, and adds W2's two verbs.
Notes the picker deviation: GetUserFileName both directions, not the
spec'd Win32/SWELL split.
This commit is contained in:
2026-08-02 14:27:20 -04:00
parent 2069ae8086
commit 1005c943a2
+99
View File
@@ -942,3 +942,102 @@ own output, the three folder cases, collapsed-mono placement and summing, both m
transitions waited out past a panel timer tick, undo, name/colour clone, and transitions waited out past a panel timer tick, undo, name/colour clone, and
`GetProjectPathEx` against a non-default recording path — none of it is unit-testable `GetProjectPathEx` against a non-default recording path — none of it is unit-testable
and none has been run. and none has been run.
### Ε-W1 — The contract, the filesystem, and the ledger's new kind
Phase Ε's first wave: the `.rsbank` package contract, the filesystem/dialog seam
behind it, and a new tracking-ledger origin kind for package-sourced files — three
tracks, disjoint by directory, dispatched in parallel.
**Ε-W1-T1 — `package-format`.** The pure `src/core/package/` codec for the
hand-rolled `RSBK` container (Ε-F1, ruled — no ZIP, no compressor, no link edge to
`vendor/WDL/WDL/zlib/`): a fixed little-endian header carrying two version
integers — `formatVersion` (what the writer emitted) and `minReaderVersion` (the
oldest reader that can read it safely) — a length-prefixed JSON manifest, and
payloads concatenated in manifest order. `classifyPackageVersion` answers
`Readable`/`TooNew`/`Malformed`; a `TooNew` header refuses whole, producing no
manifest, so the refusal can still name the writer's semver rather than
half-succeeding. Landed as three modules: `package_format` (the contract, the
version ladder, and three name-validation rules — `isValidEntryName`,
`sameEntryName`'s ASCII-case fold, `isValidNestedSamplePath`), `package_manifest`
(the manifest model + JSON codec, carrying the bank's `slot_map` and a whole-file
`hashBytes` digest per entry — deliberately not `hashWavContent`, which skips
chunks and so cannot answer "did these bytes survive"), and `bank_package`
(framing/layout arithmetic: `encodePackage`/`decodePackage`/`requiredPrefixSize`,
never holding or hashing a payload itself). Hostile input is refused, never UB,
at every byte offset.
**Ε-W1-T2 — `package-fs-shell`.** `src/shell/package/`: streaming, atomic package
filesystem I/O (`package_io`'s `PackageFileWriter`/`PackageFileReader`, at most one
entry's payload materialized at a time, backed by a `.rsbanktmp` sibling that
reaches the destination only through a `commit()` rename — process-crash atomic,
not power-loss atomic, deliberately, since an `fsync` over a whole sample bank is a
real stall) and the rollback journal (`package_rollback`'s `LandedFileJournal`,
citing the `prune_fs.cpp` carve-out rather than restating it, disarmed only after
the caller's own write has returned success). **Deviation from spec:** the plan
called for asymmetric pickers — REAPER's `GetUserFileNameForRead` for import, Win32
`GetSaveFileNameW`/SWELL `BrowseForSaveFile` for export, reasoning that the REAPER
API has no save picker. The landed `package_pickers` instead rides `GetUserFileName`
for both directions (mode 1 import, mode 0 export) — no platform split, since
`main.cpp` already aborts extension load if any needed API pointer fails to
resolve. Both pickers are `[verify — DAW]`, never exercised in a live REAPER
session.
**Ε-W1-T3 — `import-origin-kind`.** `OriginKind::PackageImport` appended to the
tracking ledger as value 5 — package-sourced vs `Ingest`'s user-picked. Append-only,
per `core/tracking/CLAUDE.md`'s persisted-integer rule; an unrecognized kind
degrades to `Unknown` rather than failing the parse, and `kLedgerVersion` stays at
2 — a vocabulary addition, not a document-version bump. No decision surface
changed: `pruneProtection`'s output is unaffected for every existing kind.
### Ε-W2 — The two verbs
Two tracks landed on Ε-W1's contract: a bank leaves the project as one `.rsbank`
file, or the export refuses and says why; a `.rsbank` becomes a **new** bank,
completely or not at all. Both tracks were code-reviewed and remediated before
merging; the merged tree (Ε-W1 + Ε-W2) builds clean and passes 100/100 tests.
**Ε-W2-T1 — `bank-export`.** New `core/package/export_plan` (pure: which entries,
what names, what is missing, and therefore whether the export may proceed — verdict
`Ready`/`Incomplete`/`Refused`) and `shell/package/export_bank` (the promptless
verb, in three composable public steps — `surveyBankExport`, `digestSources`,
`writePackageFile` — arriving with a **const** `ReaSamplerSession&`, so "writes no
ext state, opens no undo point, never bumps the generation" holds by the type
rather than by memory), plus `shell/actions/package_export_action`, one
`main.cpp` action-table row, and one panel bank-menu row. Nothing is re-encoded;
payloads are copied and hashed. The exported unit is one bank — the pool included,
since the pool is structurally one `BankIndex` among many — and whole-book export
stays out of scope for the phase. Both open questions were answered at review:
affordance ships as **both** the bindable action and the panel row, and the
default file name derives from the bank's display name through
`capture_paths::sanitizeStem`.
**Ε-W2-T2 — `bank-import`.** New `core/package/import_plan` (pure: the id remap
table, the parent remap, the per-entry land/skip-already-present/rename
disposition, and the destination bank's display name after `BankBook`'s own
uniqueness fold — reached through a new additive `BankBook::uniqueDisplayName`
member, the only `core/model/` edit in the phase), and on the shell side a
REAPER-free `import_landing` (decode, verify every payload's `hashBytes` digest
against the manifest BEFORE the bank folder is created, then land through the
rollback journal) plus a REAPER-facing `import_bank` (the only piece touching the
extension's project state — the undo-batched persist and the generation bump),
`shell/actions/package_import_action`, the panel's `.rsbank` drop route, one
`main.cpp` row, one panel menu row, and a new `src/core/util/ascii_ws.h`. The
tracking-ledger guard runs before the file picker opens (Ε-F3, ruled: refuse
outright on `Unreadable`/`FutureVersion`, no confirm-and-proceed); the version
gate runs before any byte is written; all four collision classes — sample id, file
name, content hash, bank display name — are answered explicitly, with the
display-name collision auto-suffixed and never prompted (Ε-F2, ruled: always a new
bank, never a merge); birth records land via
`recordCreated(sample, OriginKind::PackageImport)` in the same straight-line block
as the index add; the index mutation is one Ctrl-Z, and the landed files'
survival as orphans until the next prune is stated in the user-facing summary, not
left implicit. **Beyond spec:** `import_plan`'s `spelledLikeABankFile` mints a
fresh name even absent a collision, whenever the package's own entry name isn't
spelled the way `deriveBankPaths` would spell it — counted separately from a
genuine folder-name collision (`sanitizeRenameCount` vs `collisionRenameCount`) so
a hostile or foreign-spelled entry name (e.g. an unexpected extension) always
lands sanitized rather than verbatim.
**Ε-W3 (`package-compat-fixtures`) has not landed** and is the only remaining
wave of Phase Ε.