Merge Phase Ε: the .rsbank bank package — pure codec, filesystem shell, both verbs, and the frozen compatibility corpus
# Conflicts: # docs/COMPLETED.md
This commit is contained in:
@@ -981,3 +981,138 @@ remains empirical, not proven; and `TailMode::Auto`/`Manual` have no automatic b
|
||||
observation at all — `render_bounds_gate` judges `TailMode::None` only, so both modes are
|
||||
fixed by inference (same bounds path, same floor) rather than by measurement, and only
|
||||
the 0-byte gate covers them until a DAW check closes it.
|
||||
|
||||
### Ε-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). `package_pickers` rides REAPER's own
|
||||
`GetUserFileName` for both directions, as specified (mode 1 import, mode 0 export)
|
||||
— the plan's "REAPER has no save picker" finding was a regex miss in the original
|
||||
research, not a real gap, so there was no asymmetric-picker deviation to land: no
|
||||
SWELL `BrowseForSaveFile`, no Win32 `GetSaveFileNameW`, no `GetUserFileNameForRead`
|
||||
(the SDK header marks it superseded). REAPER owning the dialog on every platform is
|
||||
why there's no platform split; that's separate from `main.cpp` already aborting
|
||||
extension load if any needed API pointer fails to resolve, which is why no fallback
|
||||
path is needed. 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 — The compatibility fixtures
|
||||
|
||||
The phase's third and final wave, and with it Phase Ε's implementation is complete: the
|
||||
version-compatibility policy stated in `docs/product/bank-package.md` is now a property
|
||||
proven against frozen bytes rather than an assertion in a doc.
|
||||
|
||||
**Ε-W3-T1 — `package-compat-fixtures`.** A new checked-in corpus of 23 frozen `.rsbank`
|
||||
fixtures under `tests/fixtures/package_compat/` — one v1 package written by the shipping
|
||||
build (`1.4.0`), a synthetic additive-forward package (`formatVersion` 2 /
|
||||
`minReaderVersion` 1) carrying three keys this build has never heard of, a synthetic
|
||||
structural-refusal package (2/2), nine truncations (one per distinct decode failure
|
||||
site, including one cut at `additive_forward.rsbank`'s own payload boundary), and eleven
|
||||
hostile-name packages (six bad entry names, five bad nested `relativePath` values) —
|
||||
every payload a single 300-byte 16-bit mono WAV, ~15 KB for the whole corpus. Two new
|
||||
test targets decode and exercise it: `package_compat_tests` (frozen bytes decode to
|
||||
exactly what the shipping build wrote, the additive fixture reads with every unknown key
|
||||
skipped, every truncation classifies `Malformed` and never `TooNew`, every hostile name
|
||||
is refused before any planner runs) and `package_round_trip_tests` (the same corpus
|
||||
driven through the actual verbs — export → import → export over `v1_shipping.rsbank`
|
||||
yields byte-identical payloads, and every refusal fixture refuses the whole import with
|
||||
nothing landed and nothing in the index). A new repo-root `.gitattributes` (`*.rsbank
|
||||
binary`) is load-bearing, not decoration: under `core.autocrlf = true`, git's NUL-sniffing
|
||||
heuristic would text-classify a future short, ASCII-heavy fixture and CRLF-mangle it on a
|
||||
Windows checkout, silently breaking the frozen-bytes premise the whole corpus rests on. A
|
||||
standalone DAW verification script, `docs/verify-package-transfer.md`, covers the one
|
||||
claim no unit test can make — a real cross-machine transfer, including the too-new
|
||||
refusal, the truncated-download refusal, and mid-payload corruption, each read off as an
|
||||
exact message string. **Open question resolved:** the recommendation (one-sample
|
||||
packages, a few hundred bytes of payload each) was followed — the corpus holds
|
||||
one-sample packages with a 300-byte payload each. **Deviation from spec:** the plan
|
||||
called for a truncation cut mid-layout; RSBK stores no layout section (the layout is
|
||||
derived from the manifest's entries, not stored as its own section), so the fixture that
|
||||
exercises "the manifest parses, the layout computes, the exact-size proof fails" lands at
|
||||
the payload boundary instead. No production module was touched — the wave adds test-tree
|
||||
files, the corpus, its README, the verification script, and one path variable in the root
|
||||
`CMakeLists.txt`.
|
||||
|
||||
Reference in New Issue
Block a user