bake: make the landing scan say what it saw, so a no-answer names its own cause
Splits Ignore into unreadable vs not-a-request and counts every verdict; the report prints only when the pass answered nobody, so its absence proves the action never ran.
This commit is contained in:
@@ -80,7 +80,7 @@ This directory owns two cross-artifact contracts specifically:
|
||||
- `wire` (`core/wire`) — the ONE length-prefixed ext-state wire codec (Q-W1): `putField`/`parseUnsignedDecimal` + the bounds-checked `Cursor` (`field`/`fieldInt`/`fieldInt64`/`fieldSizeT`/`fieldDouble`), replacing four near-identical copies (`provenance` / `assignment_request` / `sample_usage` / `bank_sync`). `core/wire/bytes.h` is the sibling little-endian byte codec (`putLE`, `ByteReader`, `doubleToBits`/`bitsToDouble`) that `component_state_io` is the biggest consumer of. `core/wire/ext_state_read.h` owns the `GetProjExtState` grow-loop retry policy (Absent/Complete/Overflow) shared by `persist`, `usage_scan`, and `reaper_bridge`. `core/wire/reasampler_uid.h` (the FOREVER-FROZEN VST3 class-UID macros) also lives in this directory.
|
||||
- `reasampler_uid.h` — SDK-free header owning the FOREVER-FROZEN VST3 class-UID integer macros (stable + beta pairs, `REASAMPLER_PROC_UID_*` / `REASAMPLER_PROC_UID_BETA_*`) and the `REASAMPLER_ACTIVE_UID_*` channel-selector macros. Split out of `reasampler_vst.h` so the pure extension side (`instrument_drop`) can derive the `.vstpreset` class-ID hex string without pulling in the VST3 SDK. Both `reasampler_vst.h` (runtime `FUID`) and `instrument_drop` (preset hex string) source from this single header — the binary identity and the preset-file identity cannot diverge.
|
||||
- `assignment_request` — pure ingest-assign wire: typed request record carrying the drop payload from the `ingest` shell through to the VST3 bridge.
|
||||
- `bake_wire` — the resample bake's request/outcome pair on ONE per-instance key (`rsbake_<guid>`): the instrument writes a `BakeRequest`, invokes the extension's action synchronously, and reads the extension's `BakeOutcome` back over the same key inside that one call. Not a handshake — a call and a return, and it must not grow a claim protocol. Also the ONE home of the bake action's command-id suffix and of the leading underscore `NamedCommandLookup` needs but `rec->Register("command_id", …)` does not, so both artifacts name one action. `BakeStatus` values are WIRE INTEGERS: never renumber, only append, and an unrecognized value decodes as `Failed` rather than as the numeric default `Ok`. It owns BOTH ends' reading of that key, since the key's contents are the only evidence either side gets: `classifyBakeAnswer` (instrument side — six kinds, of which `Unanswered`, the request still sitting there untouched, is what separates an extension that never ran the landing from one that refused) and `classifyBakeScan` + `kMaxRequestAgeSeconds` (extension side — the per-key Land / RefuseWrongProject / ClearStale / Ignore verdict over every open tab, stated without a REAPER type so the multi-tab matrix is unit-provable).
|
||||
- `bake_wire` — the resample bake's request/outcome pair on ONE per-instance key (`rsbake_<guid>`): the instrument writes a `BakeRequest`, invokes the extension's action synchronously, and reads the extension's `BakeOutcome` back over the same key inside that one call. Not a handshake — a call and a return, and it must not grow a claim protocol. Also the ONE home of the bake action's command-id suffix and of the leading underscore `NamedCommandLookup` needs but `rec->Register("command_id", …)` does not, so both artifacts name one action. `BakeStatus` values are WIRE INTEGERS: never renumber, only append, and an unrecognized value decodes as `Failed` rather than as the numeric default `Ok`. It owns BOTH ends' reading of that key, since the key's contents are the only evidence either side gets: `classifyBakeAnswer` (instrument side — six kinds, of which `Unanswered`, the request still sitting there untouched, is what separates an extension that never ran the landing from one that refused) and `classifyBakeScan` + `kMaxRequestAgeSeconds` (extension side — the per-key Land / RefuseWrongProject / ClearStale / IgnoreUnreadable / IgnoreNotARequest verdict over every open tab, stated without a REAPER type so the multi-tab matrix is unit-provable). `BakeScanTally` + `describeBakeScan` are that same reading counted and spoken — the rationale lives at the type. The report is empty whenever a pass answered anybody, which makes its ABSENCE from the console evidence too: no line means the landing action never ran.
|
||||
- `instrument_drop` — pure FX-drop payload builder: constructs a Steinberg-format `.vstpreset` image (channel-active class ID + the instrument's own component state, capture pre-selected) the shell applies via `TrackFX_SetPreset`; owns `classifyReaperSurface`, the prefix classifier mapping a `GetThingFromPoint` (info token, track-present) pair onto `core/ui/drag_out`'s `ReaperSurface`. Classifier ordering is load-bearing: the embed strip is matched before the `tcp`/`mcp` panel family, which now claims the WHOLE track panel rather than just its FX sub-elements. All-or-nothing contract — caller rolls back via `TrackFX_Delete` on any failure.
|
||||
- `sample_usage` — instance-usage wire: `UsageRecord`, `planUsagePublish` (fresh/heal/clean-replace/union/remint publish plan), `foldUsageRecords`/`usageHeldPaths` (liveness fold — protect-all when records exist but no instance is live; abort→protect-all on unreadable record; `counted` carries key-attributed live records), `identityMatches` (ReaSampler 9000 FX identity). REAPER-free, unit-tested. The mirror of `assignment_request` on the instrument→extension direction: the wire format and the two safety-critical decisions (what to write on publish, which records count at prune time) are pure so they are provable without a DAW. It lives here because it is a *wire format* with an instrument-side writer; the fold's output is consumed by `core/tracking`'s authority, which owns every consumer-facing decision built on it.
|
||||
|
||||
|
||||
@@ -34,6 +34,10 @@ BakeStatus statusFromWire(int raw) {
|
||||
return BakeStatus::Failed;
|
||||
}
|
||||
|
||||
std::string countOf(int n, const char* noun) {
|
||||
return std::to_string(n) + " " + noun + (n == 1 ? "" : "s");
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
std::string bakeActionLookupName() {
|
||||
@@ -160,9 +164,14 @@ const BakeOutcome* answeredOutcome(const BakeAnswer& answer) {
|
||||
|
||||
BakeScanVerdict classifyBakeScan(const BakeScanContext& session, const BakeScanKey& key,
|
||||
std::int64_t nowSec) {
|
||||
// Listed by the enumerator but not returned whole by the reader. Distinct from the
|
||||
// next case even though both leave the key alone: this one is the extension failing
|
||||
// to read a request that may well be there, and it is invisible from the other end.
|
||||
if (!key.readable) return BakeScanVerdict::IgnoreUnreadable;
|
||||
|
||||
// Not a request: an outcome the writing instance has not collected yet, or a value
|
||||
// from a build we do not read. The writer owns clearing its own key.
|
||||
if (!key.decoded) return BakeScanVerdict::Ignore;
|
||||
if (!key.decoded) return BakeScanVerdict::IgnoreNotARequest;
|
||||
|
||||
// Either direction, so a clock moved backwards is caught too.
|
||||
const std::int64_t age = nowSec - key.generation;
|
||||
@@ -177,4 +186,31 @@ BakeScanVerdict classifyBakeScan(const BakeScanContext& session, const BakeScanK
|
||||
return landable ? BakeScanVerdict::Land : BakeScanVerdict::RefuseWrongProject;
|
||||
}
|
||||
|
||||
std::string describeBakeScan(const BakeScanTally& t) {
|
||||
if (t.answered > 0) return {};
|
||||
|
||||
std::string s = "ReaSampler resample: the landing action ran and scanned " +
|
||||
countOf(t.tabsScanned, "project tab") + ", and answered nothing. ";
|
||||
if (t.keysFound == 0) {
|
||||
s += "No pending bake request was visible to it at all -- if ReaSampler 9000 "
|
||||
"reported publishing one, the plugin and the extension are not reading the "
|
||||
"same project's ext state.";
|
||||
return s + "\n";
|
||||
}
|
||||
|
||||
s += "It found " + countOf(t.keysFound, "pending request key") + ", " +
|
||||
std::to_string(t.activeTabKeys) + " of them in the active tab";
|
||||
if (t.unreadable > 0)
|
||||
s += "; " + std::to_string(t.unreadable) + " could not be read back";
|
||||
if (t.notARequest > 0)
|
||||
s += "; " + std::to_string(t.notARequest) + " held something other than a request";
|
||||
if (t.staleCleared > 0)
|
||||
s += "; " + std::to_string(t.staleCleared) +
|
||||
" were past the age bound and were cleared unanswered";
|
||||
s += ".";
|
||||
if (t.activeTabKeys == 0)
|
||||
s += " The tab this bake was fired against held none of them.";
|
||||
return s + "\n";
|
||||
}
|
||||
|
||||
} // namespace reasampler::wire
|
||||
|
||||
@@ -127,7 +127,8 @@ enum class BakeScanVerdict {
|
||||
Land, // land it into the loaded project's bank
|
||||
RefuseWrongProject, // answer WrongProject — the book in memory belongs to another tab
|
||||
ClearStale, // no reader left: clear the key, never answer it
|
||||
Ignore, // not a request (an uncollected outcome, or a wire we do not read)
|
||||
IgnoreUnreadable, // the enumerator listed it, the reader could not return it whole
|
||||
IgnoreNotARequest, // read whole, but an uncollected outcome or a wire we do not read
|
||||
};
|
||||
|
||||
// The session's side of the verdict: whether the loaded project is the one REAPER will
|
||||
@@ -143,7 +144,8 @@ struct BakeScanContext {
|
||||
// The scanned key's side — per-TAB, which is what makes a request found in a background
|
||||
// tab decidable without any REAPER type crossing into this module.
|
||||
struct BakeScanKey {
|
||||
bool decoded = false; // the value decoded as a BakeRequest
|
||||
bool readable = false; // the enumerated key's value came back WHOLE
|
||||
bool decoded = false; // that value decoded as a BakeRequest (implies readable)
|
||||
std::int64_t generation = 0;
|
||||
bool matchesLoadedProject = false; // this key's tab IS the session's loaded project
|
||||
// — false whenever the session has no loaded
|
||||
@@ -153,4 +155,26 @@ struct BakeScanKey {
|
||||
BakeScanVerdict classifyBakeScan(const BakeScanContext& session, const BakeScanKey& key,
|
||||
std::int64_t nowSec);
|
||||
|
||||
// What ONE scan pass actually saw, accumulated by the shell as it applies the verdicts
|
||||
// above. Counts only, so the report below is provable without a DAW. It exists because
|
||||
// every non-answering verdict leaves the asking instance with the same evidence — its own
|
||||
// request, untouched — and only these counts say which of them happened.
|
||||
struct BakeScanTally {
|
||||
int tabsScanned = 0;
|
||||
int keysFound = 0; // `rsbake_*` keys enumerated across every open tab
|
||||
int activeTabKeys = 0; // of those, in the tab the action was fired against
|
||||
int unreadable = 0; // IgnoreUnreadable
|
||||
int notARequest = 0; // IgnoreNotARequest
|
||||
int staleCleared = 0; // ClearStale
|
||||
int answered = 0; // an outcome was written back (a landing OR a refusal)
|
||||
int landed = 0; // of `answered`, the ones that reached the bank
|
||||
};
|
||||
|
||||
// The console sentence for a pass that answered NOTHING — the one state in which the
|
||||
// asking instance reports a no-answer and has nothing to go on. Empty string when
|
||||
// `answered > 0`, so the caller prints unconditionally and stays quiet on a pass that
|
||||
// spoke for itself. The line's absence is itself evidence: no line means the landing
|
||||
// action never ran.
|
||||
std::string describeBakeScan(const BakeScanTally& tally);
|
||||
|
||||
} // namespace reasampler::wire
|
||||
|
||||
Reference in New Issue
Block a user