tracking: one ledger, one authority — prune protection and replace-vs-add answered from the same records, fail-safe on unreadable state

This commit is contained in:
2026-07-30 19:44:11 -04:00
parent 7bd911d58b
commit 7f70d94228
40 changed files with 1546 additions and 633 deletions
+3 -2
View File
@@ -26,11 +26,12 @@ is owned by other directories and only skinned here.
one Ctrl-Z.
- **The prune action is the ONLY file-deletion action in the system**; it opens no
undo point (file deletion is not REAPER-undoable). It halts on
`abortedUnreadableUsage` and prints the offending `rsusage_*` key names.
`blockedByTracking` and prints whichever blockers fired — the malformed ledger,
the offending `rsusage_*` key names, or both.
## Modules
- `shell/actions` (`action_registry` / `design_view_actions` / `bank_actions` / `prune_action`) — the bindable action families, all routed via the `command_id`/`gaccel`/`hookcommand` contract. `action_registry` owns the shared registration plumbing (interned channel-qualified id strings; register and mirror-unregister present the identical pointer) **and the Q-W6 registration TABLE**: `main.cpp`'s own family (capture scopes, panel toggle, insert, batch, realtime, recapture, version) is one `ActionTableRow` array — suffix, phrase, flat function-pointer handler — that registration, hookcommand dispatch, and the unload mirror-unregister all iterate, so adding an action touches the table only (OCP). Bank mutations flow through the promptless `shell/bank_ops` verbs (`bankOp*` + `persistBankOp`, taking `ReaSamplerSession&`), which the panel menus and `bank_actions` consume as thin UX skins. **Every bank index verb wraps its mutation in a batched REAPER undo point (`Undo_BeginBlock2`/`EndBlock2`, `UNDO_STATE_MISCCFG`) so one bank operation is one Ctrl-Z.** The prune action (`prune_action`, `BANK_PRUNE_FOLDER`) is **the ONLY file-deletion action in the system**; it opens no undo point (file deletion is not REAPER-undoable). **pS-usage:** `BANK_PRUNE_FOLDER` halts on `abortedUnreadableUsage` and prints the offending `rsusage_*` key names with clear instructions.
- `shell/actions` (`action_registry` / `design_view_actions` / `bank_actions` / `prune_action`) — the bindable action families, all routed via the `command_id`/`gaccel`/`hookcommand` contract. `action_registry` owns the shared registration plumbing (interned channel-qualified id strings; register and mirror-unregister present the identical pointer) **and the Q-W6 registration TABLE**: `main.cpp`'s own family (capture scopes, panel toggle, insert, batch, realtime, recapture, version) is one `ActionTableRow` array — suffix, phrase, flat function-pointer handler — that registration, hookcommand dispatch, and the unload mirror-unregister all iterate, so adding an action touches the table only (OCP). Bank mutations flow through the promptless `shell/bank_ops` verbs (`bankOp*` + `persistBankOp`, taking `ReaSamplerSession&`), which the panel menus and `bank_actions` consume as thin UX skins. **Every bank index verb wraps its mutation in a batched REAPER undo point (`Undo_BeginBlock2`/`EndBlock2`, `UNDO_STATE_MISCCFG`) so one bank operation is one Ctrl-Z.** The prune action (`prune_action`, `BANK_PRUNE_FOLDER`) is **the ONLY file-deletion action in the system**; it opens no undo point (file deletion is not REAPER-undoable). `BANK_PRUNE_FOLDER` halts on `blockedByTracking` and prints each blocker that fired, with recovery instructions.
- `drag_out_win` — OS drag-out shell: Windows OLE `DoDragDrop`/`CF_HDROP`, copy-only (`DROPEFFECT_MOVE` not offered); macOS/Linux via `SWELL_InitiateDragDropOfFileList`.
- `instrument_drop_win` — FX-button drop shell: resolves a screen point to a track + FX-surface hotspot, then adds a ReaSampler 9000 instance and applies the dragged capture's state via a transient `.vstpreset` + `TrackFX_SetPreset` (the former `TrackFX_SetNamedConfigParm` "vst_chunk" write was silently unappliable for VST3). Exposes `loadInstrumentOntoTrack` (inner half, no own undo block) and `performInstrumentDrop` (wraps in its own undo block). **Never captures, never writes the bank, never inserts a timeline item.**
- `ingest` — ingest-through-the-bank shell on the EXTENSION side: three surfaces — (1) arrange capture→bank→assign (bindable action), (2) Media-Explorer import→bank→instrument on the selected track, (3) file drop onto the bank panel→bank only. Only surface (1) writes the `assignment_request` ext-state wire. **ingest NEVER inserts a timeline item.**
+2 -2
View File
@@ -273,9 +273,9 @@ ImportResult importFileIntoActiveBank(const std::string& absoluteSourcePath) {
s.createdTimestamp = nowSec;
const AddResult r = book.activeIndex().add(s);
// Record as owned regardless of outcome — the tool WROTE the file, so prune must
// Record the birth regardless of outcome — the tool WROTE the file, so prune must
// attribute it even in the narrow Collapsed race below.
g_session->owned().add(paths.relativePath);
g_session->recordCreated(s, tracking::OriginKind::Ingest);
switch (r) {
case AddResult::Added:
+23 -13
View File
@@ -23,20 +23,30 @@ namespace reasampler {
void doBankPruneFolder(ReaSamplerSession& session) {
const reclaim::PruneReport report = session.pruneDryRun();
// FAIL-SAFE: an unreadable instance-usage record makes the protected set
// unknowable, so the prune HALTS outright rather than proceed with degraded
// protection.
if (report.abortedUnreadableUsage) {
// FAIL-SAFE: tracking state the authority could not read makes the protected
// set unknowable, so the prune HALTS outright rather than proceed with degraded
// protection. Both blockers can fire at once; report each one that did.
if (report.blockedByTracking) {
std::string msg =
"ReaSampler prune: ABORTED -- one or more instance usage records could not "
"be read or decoded. Nothing was deleted.\n"
"If the owning instance is still loaded it will republish its record on the "
"next poll tick, clearing the abort. If the instance no longer exists (the "
"key is an orphaned corrupt record), clear it manually via ReaScript:\n"
" reaper.SetProjExtState(0, \"reasampler\", \"<key>\", \"\")\n"
"Offending key(s):\n";
for (const std::string& key : report.offendingUsageKeys) {
msg += " " + key + "\n";
"ReaSampler prune: ABORTED -- the file-tracking state could not be read. "
"Nothing was deleted.\n";
if (report.ledgerUnreadable) {
msg += "The stored file-tracking ledger is malformed. It has been left "
"intact rather than overwritten, so it can be repaired or cleared:\n"
" reaper.SetProjExtState(0, \"reasampler\", \"owned_files\", \"\")\n"
"Clearing it makes every existing bank file un-reclaimable (they stop "
"being attributable to ReaSampler); no file is lost.\n";
}
if (!report.unreadableUsageKeys.empty()) {
msg += "One or more instance usage records could not be read or decoded. "
"If the owning instance is still loaded it will republish its record "
"on the next poll tick, clearing the abort. If the instance no longer "
"exists (the key is an orphaned corrupt record), clear it manually:\n"
" reaper.SetProjExtState(0, \"reasampler\", \"<key>\", \"\")\n"
"Offending key(s):\n";
for (const std::string& key : report.unreadableUsageKeys) {
msg += " " + key + "\n";
}
}
ShowConsoleMsg(msg.c_str());
return;
+3 -3
View File
@@ -4,9 +4,9 @@
// module. Registration/dispatch for its FOREVER-STABLE id (BANK_PRUNE_FOLDER) stay
// with bank_actions; one guarded body here.
//
// Contract (preserve exactly): dry-run first; abort outright on unreadable usage
// records (fail-safe); confirm-with-manifest before any deletion; opens NO undo
// point and writes NO ext state (file deletion is not REAPER-undoable).
// Contract (preserve exactly): dry-run first; abort outright when the tracking
// authority reports a block (fail-safe); confirm-with-manifest before any deletion;
// opens NO undo point and writes NO ext state (file deletion is not REAPER-undoable).
namespace reasampler {