docs: collapse the landed Phase E track specs to Landed form
Five tracks across W1 and W2 now point at COMPLETED.md. W3 and the phase header stay live.
This commit is contained in:
+88
-368
@@ -2408,6 +2408,10 @@ capture backend.**
|
|||||||
|
|
||||||
### Ε-W1 — The contract, the filesystem, and the ledger's new kind
|
### Ε-W1 — The contract, the filesystem, and the ledger's new kind
|
||||||
|
|
||||||
|
**All three tracks have landed** — Ε-W1-T1 (`package-format`), Ε-W1-T2
|
||||||
|
(`package-fs-shell`), and Ε-W1-T3 (`import-origin-kind`) — see `docs/COMPLETED.md` for
|
||||||
|
the full narrative of each.
|
||||||
|
|
||||||
**Depends on:** nothing in this phase. **Three tracks, disjoint by directory** — the split is
|
**Depends on:** nothing in this phase. **Three tracks, disjoint by directory** — the split is
|
||||||
by *what each track's inputs are*, which is why they genuinely parallelize: T1 knows only
|
by *what each track's inputs are*, which is why they genuinely parallelize: T1 knows only
|
||||||
bytes and structs, T2 knows only paths and bytes, T3 knows only the ledger.
|
bytes and structs, T2 knows only paths and bytes, T3 knows only the ledger.
|
||||||
@@ -2430,212 +2434,63 @@ T3 touches no package code at all.
|
|||||||
|
|
||||||
#### Ε-W1-T1 — `package-format`
|
#### Ε-W1-T1 — `package-format`
|
||||||
|
|
||||||
**Goal.** The container and its version ladder, entirely pure — the contract every later
|
**Landed** — see `docs/COMPLETED.md` for the full narrative. The pure `src/core/package/`
|
||||||
track consumes, landed once so nothing downstream re-litigates the shape.
|
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
|
||||||
**Spec:** `docs/product/bank-package.md` §"The container", §"Version tagging", §"What a
|
integers — `formatVersion` (what the writer emitted) and `minReaderVersion` (the oldest
|
||||||
package carries", §"What a package deliberately does NOT carry", §"Memory".
|
reader that can read it safely) — a length-prefixed JSON manifest, and payloads
|
||||||
|
concatenated in manifest order. `classifyPackageVersion` answers `Readable`/`TooNew`/
|
||||||
**Surface boundary — owns:** new `src/core/package/package_format` (the magic, the header
|
`Malformed`; a `TooNew` header refuses whole, producing no manifest, so the refusal can
|
||||||
layout, `kPackageFormatVersion`, `kPackageMinReaderVersion`, and
|
still name the writer's semver rather than half-succeeding. The ladder's one
|
||||||
`classifyPackageVersion(formatVersion, minReader) -> Readable | TooNew | Malformed`), new
|
counterintuitive rule rides with the contract: **growing a persisted enum's vocabulary is
|
||||||
`src/core/package/package_manifest` (the manifest model + its JSON codec), new
|
structural, not additive** — `BankModel::deserialize` *rejects* an out-of-range `SourceMode`
|
||||||
`src/core/package/bank_package` (header encode, prefix decode, entry-layout arithmetic), the
|
or `Tier` rather than degrading it (`bank_model.cpp:232-239`, `:339-346`), and every enum a
|
||||||
directory's `CMakeLists.txt` and `CLAUDE.md`, and one appended `add_subdirectory` line in the
|
package carries rides inside the nested `BankModel` blob, so a new value bumps both
|
||||||
root `CMakeLists.txt`. **Does not own:** `export_plan` / `import_plan` (Ε-W2), anything under
|
integers, where a new `Sample` field with a defined absent-value bumps `formatVersion`
|
||||||
`shell/`, `core/model`, or `core/tracking`.
|
alone. Landed as three modules: `package_format` (the contract, the version ladder, and
|
||||||
|
three name-validation rules —
|
||||||
**Behavior.**
|
`isValidEntryName`, `sameEntryName`'s ASCII-case fold, `isValidNestedSamplePath`),
|
||||||
- **The container is the hand-rolled `RSBK` (Ε-F1, ruled).** Magic `RSBK`, a fixed
|
`package_manifest` (the manifest model + JSON codec, carrying the bank's `slot_map` and a
|
||||||
little-endian header carrying the two version fields, a length-prefixed JSON manifest, then
|
whole-file `hashBytes` digest per entry — deliberately not `hashWavContent`, which skips
|
||||||
each entry's payload concatenated in manifest order. Framing is built on `core/wire/bytes.h`
|
chunks and so cannot answer "did these bytes survive"), and `bank_package`
|
||||||
(`putLE` / `ByteReader`) and the manifest on `core/json` — both already owned and tested
|
(framing/layout arithmetic: `encodePackage`/`decodePackage`/`requiredPrefixSize`, never
|
||||||
here. **No ZIP, no compressor, no new third-party source in the build**; a link edge to
|
holding or hashing a payload itself). Hostile input is refused, never UB, at every byte
|
||||||
`vendor/WDL/WDL/zlib/` means the ruling was misread.
|
offset.
|
||||||
- **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 `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 — 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
|
|
||||||
`bank_book_json.cpp:15-20` nests it, so per-sample shape has one owner and a future
|
|
||||||
`Sample` field reaches packages for free. Per entry the manifest adds only: the bare file
|
|
||||||
name, the byte length, and a `hashBytes` digest (`core/capture/wav_codec.h:143`) —
|
|
||||||
`hashBytes`, **not** `hashWavContent`, because the latter deliberately skips chunks
|
|
||||||
(`wav_codec.h:145-151`) and so cannot answer "did these bytes survive."
|
|
||||||
- **The bank's `slot_map` rides along** — display positions are part of what the user built.
|
|
||||||
- **Framing only, never a payload.** `bank_package` produces the header bytes and an ordered
|
|
||||||
`[{ name, offset, length }]` layout; it never holds, copies, or hashes an entry's audio.
|
|
||||||
Decode is symmetric: prefix in, manifest + layout out.
|
|
||||||
- **Path expression is structurally impossible.** Entry names are validated to contain no
|
|
||||||
`/`, `\`, `:`, no leading separator, and no `..` component, on both encode and decode.
|
|
||||||
|
|
||||||
**Acceptance criteria.**
|
|
||||||
- `decodePackage(encodePackage(x)) == x` over a manifest fixture exercising every field,
|
|
||||||
including every `Sample` optional in both present and absent states.
|
|
||||||
- A synthetic header with `minReaderVersion` above this build classifies `TooNew` and **no
|
|
||||||
manifest is produced** — the decode does not half-succeed.
|
|
||||||
- A synthetic header with `formatVersion` above this build but `minReaderVersion` at or below
|
|
||||||
it classifies `Readable`, and its unknown manifest keys are skipped without error. This is
|
|
||||||
the additive-forward-compatibility claim, and it is the reason the two-integer design
|
|
||||||
exists; a test that does not exercise it leaves the design unproven.
|
|
||||||
- Truncated input at every byte offset in a valid package returns `Malformed` — never UB,
|
|
||||||
never a partial manifest, never a read past the buffer. Hostile-input hardening at the
|
|
||||||
`bank_model.h:204-206` standard.
|
|
||||||
- Entry names containing `..`, a separator, or an absolute prefix are rejected on encode
|
|
||||||
*and* rejected on decode. Both directions, because a package can arrive from anywhere.
|
|
||||||
- No file in the new directory exceeds ~600 lines; the three-module split above is the
|
|
||||||
responsibility seam, and a fourth module is preferred over a bisection if one is needed.
|
|
||||||
- `package_format_tests`, `package_manifest_tests`, `bank_package_tests` all run without
|
|
||||||
REAPER or a DAW.
|
|
||||||
|
|
||||||
**Open questions.** **No [Daniel] questions — Ε-F1 is RULED** (proprietary `RSBK`), so this
|
|
||||||
track is dispatchable as written. **[propose at review]** whether `package_format` and
|
|
||||||
`bank_package` are genuinely two modules or one — the split is proposed on responsibility
|
|
||||||
grounds (constants and classification vs. offset arithmetic) and may collapse if the
|
|
||||||
arithmetic turns out to be twenty lines.
|
|
||||||
|
|
||||||
#### Ε-W1-T2 — `package-fs-shell`
|
#### Ε-W1-T2 — `package-fs-shell`
|
||||||
|
|
||||||
**Goal.** Every filesystem and dialog act the two verbs need, landed behind an API that knows
|
**Landed** — see `docs/COMPLETED.md` for the full narrative. `src/shell/package/`:
|
||||||
nothing about what a package contains — so it can be authored, reviewed, and tested in
|
streaming, atomic package filesystem I/O (`package_io`'s `PackageFileWriter`/
|
||||||
parallel with the format it will carry.
|
`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 —
|
||||||
**Spec:** `docs/product/bank-package.md` §"Where it lives", §"Failure modes", §"Memory".
|
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
|
||||||
**Surface boundary — owns:** new `src/shell/package/package_io` (read a file's bytes, write
|
`LandedFileJournal`, citing the `prune_fs.cpp` carve-out rather than restating it,
|
||||||
bytes through temp + atomic rename, read one bank file, write one landed file, enumerate the
|
disarmed only after the caller's own write has returned success). `package_pickers` is the
|
||||||
bank folder's existing names, and the rollback delete), the file-picker seam for both verbs —
|
one picker seam, REAPER's own in both directions as specified — `GetUserFileName` with
|
||||||
**one picker, REAPER's own, on every platform**, so this track carries **no**
|
`mode=0` for export's destination and `mode=1` for import's source, so no platform `#ifdef`,
|
||||||
`#ifdef _WIN32` / `#else swell/swell.h` split. That split is a real pattern in this codebase
|
no SWELL `BrowseForSaveFile`, no Win32 `GetSaveFileNameW`, and no `GetUserFileNameForRead`,
|
||||||
(`src/shell/panel/draw_kit.cpp:11-15`, `src/shell/persist/prune_fs.cpp:35-38`); it is simply
|
which the SDK header marks superseded by `GetUserFileName`. No fallback path was needed
|
||||||
not this track's shape, because REAPER owns the dialog. Also owns the
|
either: `main.cpp` aborts the extension load if any API pointer fails to resolve, so no
|
||||||
directory's `CMakeLists.txt` and `CLAUDE.md`, and one appended `add_subdirectory` line in the
|
REAPER build that loads the extension can lack it. The picker is `[verify — DAW]` in both
|
||||||
root `CMakeLists.txt`. **Does not own:** `export_bank` / `import_bank` (Ε-W2), anything under
|
directions, never exercised in a live REAPER session.
|
||||||
`core/`, and — emphatically — `prune_fs`, which stays the deletion authority.
|
|
||||||
|
|
||||||
**Behavior.**
|
|
||||||
- **Atomic write.** A package is written to a temp path in the destination directory and
|
|
||||||
renamed on complete success. A failed or interrupted write leaves no `.rsbank` behind. This
|
|
||||||
is the mono-collapse precedent (Ψ-W2-T2, temp file + atomic rename) applied to a much
|
|
||||||
larger file.
|
|
||||||
- **Streaming, both ways.** Append one payload at a time on write; seek and read one payload
|
|
||||||
at a time on read. The API must make holding the whole package awkward, not merely
|
|
||||||
discouraged.
|
|
||||||
- **The rollback delete is the carve-out, cited.** Its TU header cites
|
|
||||||
`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.
|
|
||||||
- **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.
|
|
||||||
|
|
||||||
**Acceptance criteria.**
|
|
||||||
- A write interrupted before completion leaves the destination path absent or holding its
|
|
||||||
prior contents — never a partial new file. Tested by injecting a failure at the writer seam.
|
|
||||||
- Reading and writing a multi-entry package never holds more than one entry's payload; the
|
|
||||||
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.
|
|
||||||
- **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]** 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`
|
#### Ε-W1-T3 — `import-origin-kind`
|
||||||
|
|
||||||
**Goal.** Give the ledger a birth-record kind for a package import, so an imported file is
|
**Landed** — see `docs/COMPLETED.md` for the full narrative. `OriginKind::PackageImport`
|
||||||
tracked from the moment it lands rather than becoming a permanently unreclaimable foreign
|
appended to the tracking ledger as value 5 — package-sourced vs `Ingest`'s user-picked.
|
||||||
file — landed as its own track, with its own review, because it edits safety-critical
|
Append-only, per `core/tracking/CLAUDE.md`'s persisted-integer rule; an unrecognized kind
|
||||||
territory that nothing else in this phase touches.
|
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:
|
||||||
**Spec:** `docs/product/bank-package.md` §"What a package deliberately does NOT carry" (the
|
`pruneProtection`'s output is unaffected for every existing kind.
|
||||||
origin-ledger bullet); `src/core/tracking/CLAUDE.md` for the invariants it must not weaken.
|
|
||||||
|
|
||||||
**Surface boundary — owns:** `src/core/tracking/origin_ledger.{h,cpp}` and its tests,
|
|
||||||
exclusively. **Does not own:** `tracking_authority` (no decision changes), `shell/persist`,
|
|
||||||
or any consumer.
|
|
||||||
|
|
||||||
**Behavior.**
|
|
||||||
- **Append `OriginKind::PackageImport` as value 5.** Append only — `Capture`=1, `Ingest`=2,
|
|
||||||
`Recapture`=3, `Resample`=4 keep their integers, per `core/tracking/CLAUDE.md`'s
|
|
||||||
"PERSISTED INTEGERS — never renumber, only append".
|
|
||||||
- **A build that does not know value 5 degrades it to `Unknown`**, which is the existing
|
|
||||||
`kindFromInt` behaviour and is the safe direction: the path is still owned, so still
|
|
||||||
protected; only the kind detail is lost. This is the *field-vocabulary* rule, and it must
|
|
||||||
stay distinct from the *document-version* rule right beside it, which blocks
|
|
||||||
(`origin_ledger.cpp:18-21`).
|
|
||||||
- **Nothing else changes.** No new field, no version bump, no lineage semantics. A new enum
|
|
||||||
value in an append-only vocabulary is precisely the change that does **not** need `"v"` to
|
|
||||||
move, and demonstrating that is part of the point.
|
|
||||||
- **`recordCreated` needs no signature change** — it already takes an `OriginKind`
|
|
||||||
(`src/shell/persist/session.h:95`). Confirm that in the same pass; if it turns out
|
|
||||||
otherwise, that discovery is this track's, not Ε-W2's.
|
|
||||||
|
|
||||||
**Acceptance criteria.**
|
|
||||||
- A ledger containing a kind-5 record round-trips through serialize/deserialize unchanged.
|
|
||||||
- A record carrying an *unrecognized* kind integer (6, 99, negative) loads as `Unknown` and
|
|
||||||
the ledger loads `Loaded`, not `Unreadable` — the vocabulary gap does not halt prune.
|
|
||||||
- `kLedgerVersion` is **unchanged** at 2, and a test asserts it, so the append-vs-bump
|
|
||||||
distinction is pinned rather than assumed.
|
|
||||||
- `pruneProtection`'s output is unchanged for every existing kind — this track alters no
|
|
||||||
decision.
|
|
||||||
|
|
||||||
**Open questions.** **[propose at review]** whether the kind is named `PackageImport` or
|
|
||||||
folded onto the existing `Ingest`. The plan's recommendation is a distinct value: `Ingest`
|
|
||||||
means "the user brought in a file," which is close, but losing the distinction makes a future
|
|
||||||
"where did this bank come from" question unanswerable, and an appended integer costs nothing.
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
### Ε-W2 — The two verbs
|
### Ε-W2 — The two verbs
|
||||||
|
|
||||||
|
**Both tracks have landed** — Ε-W2-T1 (`bank-export`) and Ε-W2-T2 (`bank-import`) — see
|
||||||
|
`docs/COMPLETED.md` for the full narrative of each.
|
||||||
|
|
||||||
**Depends on Ε-W1 — all three tracks.** T1 for the format the verbs speak, T2 for every
|
**Depends on Ε-W1 — all three tracks.** T1 for the format the verbs speak, T2 for every
|
||||||
filesystem act they perform, T3 for the kind their birth records carry. No part of either
|
filesystem act they perform, T3 for the kind their birth records carry. No part of either
|
||||||
verb is authorable against a format that has not settled.
|
verb is authorable against a format that has not settled.
|
||||||
@@ -2660,182 +2515,47 @@ second.
|
|||||||
|
|
||||||
#### Ε-W2-T1 — `bank-export`
|
#### Ε-W2-T1 — `bank-export`
|
||||||
|
|
||||||
**Goal.** One bank leaves the project as one file, or the export refuses and says why.
|
**Landed** — see `docs/COMPLETED.md` for the full narrative. New `core/package/export_plan`
|
||||||
|
(pure: which entries, what names, what is missing, and therefore whether the export may
|
||||||
**Spec:** `docs/product/bank-package.md` §"Failure modes" (export rows), §"What a package
|
proceed — verdict `Ready`/`Incomplete`/`Refused`) and `shell/package/export_bank` (the
|
||||||
carries".
|
promptless verb, in three composable public steps — `surveyBankExport`, `digestSources`,
|
||||||
|
`writePackageFile` — arriving with a **const** `ReaSamplerSession&`, so "writes no ext
|
||||||
**Surface boundary — owns:** new `core/package/export_plan` (pure: which entries, what
|
state, opens no undo point, never bumps the generation" holds by the type rather than by
|
||||||
names, what is missing, and therefore whether the export may proceed), new
|
memory), plus `shell/actions/package_export_action`, one `main.cpp` action-table row, and
|
||||||
`shell/package/export_bank` (the promptless verb — takes a `ReaSamplerSession&`, returns an
|
one panel bank-menu row. Nothing is re-encoded; payloads are copied and hashed. The
|
||||||
outcome, **no prompts and no message boxes**, mirroring `src/shell/bank_ops/`), new
|
exported unit is one bank — the pool included, since the pool is structurally one
|
||||||
`shell/actions/package_export_action` (the bindable-action skin, mirroring `prune_action`),
|
`BankIndex` among many — and whole-book export stays out of scope for the phase. Both open
|
||||||
one registration line in `src/app/main.cpp`, one panel menu row. **Does not own:** anything
|
questions were answered at review: affordance ships as **both** the bindable action and
|
||||||
on the import side, `bank_ops`, or `persist`.
|
the panel row, and the default file name derives from the bank's display name through
|
||||||
|
`capture_paths::sanitizeStem`.
|
||||||
**Behavior.**
|
|
||||||
- **The exported unit is one bank** — the pool included, since the pool is structurally a bank
|
|
||||||
(`core/model/CLAUDE.md`'s pool-privileges section). Whole-book export is an explicit
|
|
||||||
non-goal of this phase and is preserved as an additive future by the manifest's shape, not
|
|
||||||
by a promise.
|
|
||||||
- **Refuse-if-incomplete, report-before-acting.** An index entry whose file is missing or
|
|
||||||
unreadable stops the export by default; the "export the N present entries" path exists only
|
|
||||||
behind an explicit confirm that lists what is absent, distinguishing missing from
|
|
||||||
unreadable. This is prune's dry-run-then-confirm discipline applied to a non-destructive
|
|
||||||
act, and it is deliberate: a silently-incomplete package is discovered on the far side, in
|
|
||||||
another project, weeks later.
|
|
||||||
- **The project is not touched.** No ext-state write, no generation bump, no undo point. An
|
|
||||||
export that mutates project state is a defect, and the acceptance criteria name it as one.
|
|
||||||
- **Nothing is re-encoded.** Payload bytes are copied and hashed. `wav_codec` is not asked to
|
|
||||||
rebuild anything.
|
|
||||||
- **A new FOREVER-STABLE command id** is minted through `version::channelCommandId(suffix)`
|
|
||||||
per the root `CLAUDE.md` action contract, with its per-channel display name through
|
|
||||||
`channelActionName`.
|
|
||||||
|
|
||||||
**Acceptance criteria.**
|
|
||||||
- `planExport` is pure and total over its inputs: a bank with a missing file, an unreadable
|
|
||||||
file, zero samples, and one sample all classify without touching a filesystem.
|
|
||||||
- Exporting a bank and re-reading the package yields, for every entry, a `hashBytes` digest
|
|
||||||
equal to the source file's — asserted per entry, not in aggregate.
|
|
||||||
- The exported manifest contains no absolute path and no path separator, asserted by a test
|
|
||||||
that scans the emitted bytes rather than by inspecting the model.
|
|
||||||
- A failure injected mid-write leaves no `.rsbank` at the destination and the prior file, if
|
|
||||||
any, intact.
|
|
||||||
- Project ext state is byte-identical before and after an export, and `bankGeneration()` is
|
|
||||||
unchanged — a direct assertion, because "we did not mean to write anything" is not a
|
|
||||||
property that survives without one.
|
|
||||||
- Exporting an empty bank produces a valid, importable package with zero entries rather than
|
|
||||||
refusing. An empty bank is a legitimate thing to carry.
|
|
||||||
|
|
||||||
**Open questions — both answered at implementation review, recorded at
|
|
||||||
`docs/product/bank-package.md` §"Implementation decisions — Ε-W2-T1".** Affordance:
|
|
||||||
**both** the action and the panel row (the action is the only spelling that can reach
|
|
||||||
the pool; the panel row is the direct gesture on a named bank). Default file name:
|
|
||||||
the bank's **display name**, sanitized through `capture_paths::sanitizeStem`, as
|
|
||||||
recommended.
|
|
||||||
|
|
||||||
#### Ε-W2-T2 — `bank-import`
|
#### Ε-W2-T2 — `bank-import`
|
||||||
|
|
||||||
**Goal.** A package becomes a **new** bank in this project — completely, or not at all —
|
**Landed** — see `docs/COMPLETED.md` for the full narrative. New
|
||||||
with every one of the four collision classes answered explicitly rather than by whatever the
|
`core/package/import_plan` (pure: the id remap table, the parent remap, the per-entry
|
||||||
model happens to do.
|
land/skip-already-present/rename disposition, and the destination bank's display name
|
||||||
|
after `BankBook`'s own uniqueness fold — reached through a new additive
|
||||||
**Spec:** `docs/product/bank-package.md` §"Identity and collision on import" (including the
|
`BankBook::uniqueDisplayName` member, the only `core/model/` edit in the phase), and on
|
||||||
auto-suffix rule), §"Failure modes" (import rows), §"Import under a degraded tracking
|
the shell side a REAPER-free `import_landing` (decode, verify every payload's
|
||||||
ledger", §"Version tagging: both directions".
|
`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
|
||||||
**Surface boundary — owns:** new `core/package/import_plan` (pure: the id remap table, the
|
touching the extension's project state — the undo-batched persist and the generation
|
||||||
parent remap, the per-entry write / skip-already-present / rename disposition, the
|
bump), `shell/actions/package_import_action`, the panel's `.rsbank` drop route, one
|
||||||
destination bank name after uniqueness folding), new `shell/package/import_bank` (the
|
`main.cpp` row, one panel menu row, and a new `src/core/util/ascii_ws.h`. The
|
||||||
promptless verb), new `shell/actions/package_import_action`, the panel's `WM_DROPFILES` route
|
tracking-ledger guard runs before the file picker opens (Ε-F3, ruled: refuse outright on
|
||||||
for a `.rsbank` (routing only — the existing ingest route for audio files is untouched), one
|
`Unreadable`/`FutureVersion`, no confirm-and-proceed); the version gate runs before any
|
||||||
registration line in `src/app/main.cpp`, one panel menu row, and — the **only** `core/model/`
|
byte is written; all four collision classes — sample id, file name, content hash, bank
|
||||||
edit in the phase — **one additive public `const` member on `BankBook`** (recommended
|
display name — are answered explicitly, with the display-name collision auto-suffixed
|
||||||
`std::string uniqueDisplayName(const std::string& seed) const`), so the auto-suffix probe runs
|
and never prompted (Ε-F2, ruled: always a new bank, never a merge); birth records land
|
||||||
behind the model's own name fold. **Does not own:** anything on the export side,
|
via `recordCreated(sample, OriginKind::PackageImport)` in the same straight-line block
|
||||||
`bank_book`'s *rules* (consumed, never re-implemented — the new member exposes the existing
|
as the index add; the index mutation is one Ctrl-Z, and the landed files' survival as
|
||||||
fold, it does not add a second one), `origin_ledger` (Ε-W1-T3's).
|
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
|
||||||
**Behavior.**
|
collision, whenever the package's own entry name isn't spelled the way
|
||||||
- **The ledger guard runs FIRST — before the file picker opens (Ε-F3, ruled: refuse).** If
|
`deriveBankPaths` would spell it — counted separately from a genuine folder-name
|
||||||
`tracking::ledgerDegraded(status)` holds for the project's loaded ledger status
|
collision (`sanitizeRenameCount` vs `collisionRenameCount`) so a hostile or
|
||||||
(`Unreadable` or `FutureVersion`; `core/tracking/origin_ledger.h:94`, `:100-101`), the
|
foreign-spelled entry name (e.g. an unexpected extension) always lands sanitized rather
|
||||||
import **refuses outright** — no picker, no bytes read, no confirm-and-proceed path, no
|
than verbatim.
|
||||||
opt-out. `Fresh` and `Loaded` both proceed. **Do not key this on
|
|
||||||
`PruneReport::blockedByTracking`**: that flag also fires on undecodable `rsusage_*` keys,
|
|
||||||
which govern deletion-time protection and have nothing to do with writing birth records.
|
|
||||||
The refusal is a `ShowConsoleMsg` block mirroring `prune_action.cpp:30-69` in structure and
|
|
||||||
tone, with two variants (malformed / newer-build) and every recovery line naming **this
|
|
||||||
build's** namespace through `version::extStateNamespace()`. Exact wording in the spec doc.
|
|
||||||
**Export is deliberately not gated this way** — that is Ε-W2-T1's, and it stays ungated.
|
|
||||||
- **Version gate second, before any byte is written.** `minReaderVersion` above this build
|
|
||||||
refuses the whole package and reports through `ShowMessageBox`
|
|
||||||
(**verified**, `reaper_plugin_functions.h:6546`) naming three things: the package's
|
|
||||||
requirement, this build's ceiling, and the writer's semver. Two of the three is not enough
|
|
||||||
to act on. A malformed or truncated package reports **distinctly** — the two failures have
|
|
||||||
opposite recoveries, which is exactly why `origin_ledger.cpp:178-185` separates them.
|
|
||||||
- **Four collisions, four answers.** (1) **Sample id** — remint every id and remap
|
|
||||||
`Provenance::parentSampleId` (`bank_model.h:45-50`) through the same map, to the reminted
|
|
||||||
parent when it came in the same package and cleared otherwise; a foreign id never enters
|
|
||||||
the index. (2) **File name** — never overwrite; mint a fresh unique name through
|
|
||||||
`capture_paths::deriveBankPaths`, silently, counted in the summary. (3) **Content hash** —
|
|
||||||
consult `BankModel::findByHash` **before writing the payload**; on a hit, skip the write
|
|
||||||
entirely and let the entry collapse, so a dedup never manufactures an orphan. (4) **Bank
|
|
||||||
display name** — **auto-suffix, no prompt** (Ε-F2, ruled). Seed = the package's recorded
|
|
||||||
source bank name **verbatim** (or the literal `Imported bank` if absent/blank); take the
|
|
||||||
first of `seed`, `seed + " 2"`, `seed + " 3"`, … whose fold is free in the destination
|
|
||||||
book, ascending from 2. Four points that decide the behaviour and must not be re-invented:
|
|
||||||
the seed is **never re-parsed** (`"Drums 2"` colliding lands as `"Drums 2 2"`, not
|
|
||||||
`"Drums 3"` — a bare trailing integer is indistinguishable from `"Kit 808"`); the probe
|
|
||||||
**fills gaps** (first-free, not highest-plus-one, so it is a pure function of the current
|
|
||||||
name set); the probe **terminates** by pigeonhole within `B + 1` candidates for `B` banks,
|
|
||||||
so **no arbitrary cap**; and the fold is `BankBook`'s own (`bank_book.h:263-269`), reached
|
|
||||||
through the new public member, never re-implemented in `import_plan`. Sample display names
|
|
||||||
are **not** suffixed, and `slot_map` positions are untouched.
|
|
||||||
- **Always a new bank; never a merge (Ε-F2, ruled).** The import creates a bank — it never
|
|
||||||
merges into an existing one, never lands into the pool, and offers no target picker. A
|
|
||||||
pool export therefore lands as a **named** bank `"Pool 2"`, which is correct, not a glitch.
|
|
||||||
This track ships **one** action, not two; merge-into-existing is out of scope for the
|
|
||||||
phase, and move/copy already cover the after-the-fact case.
|
|
||||||
- **Birth records at landing.** Every landed file goes through
|
|
||||||
`ReaSamplerSession::recordCreated(sample, OriginKind::PackageImport)` at the same point the
|
|
||||||
`Sample` is added, in the same straight-line block, per `core/tracking/CLAUDE.md`'s
|
|
||||||
no-silent-gaps invariant. An import that lands a file without a record is the exact failure
|
|
||||||
that section exists to prevent.
|
|
||||||
- **All-or-nothing, with rollback.** Any failure after the first write deletes the files
|
|
||||||
*this call wrote* and abandons the index mutation. The rollback cites the
|
|
||||||
`prune_fs.cpp:5-11` carve-out; it does not restate it, and it does not reach outside the set
|
|
||||||
it wrote.
|
|
||||||
- **One Ctrl-Z for the index, and the file residue is stated, not implied.** The index
|
|
||||||
mutation batches through `persistBankOp` (`Undo_BeginBlock2` / `Undo_EndBlock2`, verified at
|
|
||||||
`reaper_plugin_functions.h:7758` / `:7806`). Undo does **not** un-write the files; they
|
|
||||||
remain as orphans until a prune reclaims them — the same designed window a non-empty bank
|
|
||||||
delete already produces (`core/model/CLAUDE.md`'s sample-removal section). The user-facing
|
|
||||||
summary says so.
|
|
||||||
- **`bumpBankGeneration()` on success** (`session.h:121`), so live instances reload.
|
|
||||||
- **No timeline item is placed. Ever.**
|
|
||||||
- **A new FOREVER-STABLE command id**, minted the same way T1's is.
|
|
||||||
|
|
||||||
**Acceptance criteria.**
|
|
||||||
- `planImport` is pure and total, and every one of the four collision classes has a test that
|
|
||||||
exercises it without a filesystem: colliding ids, colliding file names, a hash already
|
|
||||||
present, and a colliding bank name.
|
|
||||||
- Importing a package built from bank B back into the project that already contains B lands
|
|
||||||
a **new** bank named `"B 2"`, with every id reminted, no entry lost, and B itself
|
|
||||||
unmutated. Importing it a third time lands `"B 3"`.
|
|
||||||
- The suffix probe is pinned by pure tests over a name set, covering at minimum: a free seed
|
|
||||||
(no suffix applied), a case/whitespace-folded collision (`"drums"` blocks `"Drums"`), a gap
|
|
||||||
(`"Drums"` + `"Drums 3"` present ⇒ `"Drums 2"`), a seed that already ends in a number
|
|
||||||
(`"Drums 2"` colliding ⇒ `"Drums 2 2"`), an absent/blank recorded name (⇒ `Imported bank`),
|
|
||||||
and a package whose source bank was the pool (⇒ `"Pool 2"`, a named bank).
|
|
||||||
- A degraded ledger (`Unreadable` and `FutureVersion`, both asserted) refuses the import with
|
|
||||||
**no picker shown, zero files written, zero index mutation**, and the message names the
|
|
||||||
channel-correct ext-state namespace. An undecodable `rsusage_*` key with an otherwise
|
|
||||||
`Loaded` ledger **does not** block — asserted, because the tempting reuse of
|
|
||||||
`blockedByTracking` would silently make it.
|
|
||||||
- An entry whose payload fails its `hashBytes` check aborts the import with **zero** files
|
|
||||||
landed and **zero** index mutation — asserted on both, since either alone would pass a
|
|
||||||
weaker test.
|
|
||||||
- A write failure injected at entry k of n leaves exactly zero files from this import on
|
|
||||||
disk and the index unchanged.
|
|
||||||
- Every landed file has a ledger birth record with kind `PackageImport`, asserted by reading
|
|
||||||
the ledger after the import, not by counting calls.
|
|
||||||
- `minReaderVersion` above the build: nothing written, message names all three facts.
|
|
||||||
`formatVersion` above the build with `minReaderVersion` at or below it: **imports cleanly**,
|
|
||||||
unknown keys skipped. Both directions asserted, in this track, against real package bytes.
|
|
||||||
- No timeline item exists after an import; the arrange is byte-identical.
|
|
||||||
- **DAW-verification obligation** (to be discharged by Daniel, not by this track): import a
|
|
||||||
package produced on another machine, confirm the panel shows every sample with its
|
|
||||||
metadata, confirm a live ReaSampler 9000 instance picks up the new bank content on the
|
|
||||||
generation bump, and confirm one Ctrl-Z removes the index entries.
|
|
||||||
|
|
||||||
**Open questions.** **No [Daniel] questions — Ε-F2 and Ε-F3 are both RULED** (new bank
|
|
||||||
always with an auto suffix; refuse on a degraded ledger), so this track is dispatchable as
|
|
||||||
written and ships one action. **[propose at review]** whether the import summary is a console
|
|
||||||
block, a message box, or both; the recommendation is a console block plus a one-line message
|
|
||||||
box, so the detail is copyable and the outcome is unmissable. Note the refusal path is
|
|
||||||
already fixed at a console block by the Ε-F3 spec, so this call is about the *success*
|
|
||||||
summary only.
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user