#pragma once // assignment_request — the pure core of the S8 ingest assignment-request seam. // // PURE MODULE (CLAUDE.md §load-bearing split): NO REAPER types, NO SWELL, NO VST3, // NO vendor/ includes. Standard library only. Unit-tested outside the DAW — the same // "small pure type + length-prefixed round-trip" pattern as provenance / owned_manifest. // // -- What it is -------------------------------------------------------------- // // When the EXTENSION ingests a sample (S8: arrange capture / Media-Explorer import / // drop-onto-panel) it writes an ASSIGNMENT REQUEST to its own "reasampler" ext-state // namespace: "the active sampler instance should now play THIS sample." The value // names the ingested sample by (bankId, sampleId) plus a monotonic `generation` the // reader compares to decide the request is NEW (a fresh ingest, even of the same id). // // This module owns ONLY the value's WIRE FORMAT — build/parse round-trip. Writing it // to ext-state is the persist shell's job; READING it is the instrument's job in a // LATER dispatch (S8 instrument-side follow-up, after S10 merges). This is why the // format is documented here in the header, not just in code: the reader lands elsewhere // and must decode exactly what this writer produced. // // -- The data-ownership boundary (load-bearing) ------------------------------ // // The EXTENSION writes this; the instrument only READS it. That does not violate the // instrument's read-only-over-the-bank rule: the assignment request is the extension // writing its OWN namespace (a request FROM the extension TO the instrument), never the // instrument writing back into the bank. The instrument, on reading a new generation, // updates its OWN component-state selection (the same selection S4 persists) and reloads. // // -- Why `generation` ------------------------------------------------------- // // Instances reference sample IDs, so re-assigning the SAME id (e.g. a recapture, or a // re-drop of the same file) would be indistinguishable from a stale value without a // changing field. `generation` is a monotonic disambiguator (the ingest writer supplies // a wall-clock unix-epoch stamp today — see the writer shell) so the reader can tell // "assigned again just now" from "already saw this." It is DELIBERATELY the same shape // the S9 bank-generation counter will use, but it is NOT that counter — S9 is a separate // point; this field is self-contained to the request and does not depend on S9 landing. #include #include #include namespace reasampler { // One assignment request: the ingested sample's identity + a monotonic disambiguator. // bankId — the bank the sample was ingested into (the active/target bank). // sampleId — the ingested Sample's stable id (BankIndex key). // generation — a monotonic value the reader compares to detect a NEW request. The // writer supplies a unix-epoch-seconds stamp; the reader treats it as an // opaque "did this change?" token, not a wall-clock it interprets. struct AssignmentRequest { std::string bankId; std::string sampleId; std::int64_t generation = 0; bool operator==(const AssignmentRequest& o) const { return bankId == o.bankId && sampleId == o.sampleId && generation == o.generation; } bool operator!=(const AssignmentRequest& o) const { return !(*this == o); } }; // Encode an assignment request to the wire string. Length-prefixed fields behind a // magic+version tag ("rsassign1"), so arbitrary bytes in an id (a GUID, a display- // derived id) round-trip whole with no escaping ambiguity — the same idiom provenance // uses. Deterministic: the same request always yields the same string. // // FORMAT (documented for the LATER instrument-side reader): // "rsassign1" ':' ':' ':' // where each is the decimal byte length of the field that follows the ':'. std::string encodeAssignmentRequest(const AssignmentRequest& req); // Parse a wire string produced by encodeAssignmentRequest. std::nullopt on any // malformed / truncated / trailing-garbage input (never UB, never a partial value) — // the reader shell treats absence/malformed as "no pending request." Round-trips: // decodeAssignmentRequest(encodeAssignmentRequest(x)) == x. // // READER REQUIREMENT (instrument-side, S8 follow-up dispatch): after successfully // decoding a request, the reader MUST verify that (bankId, sampleId) resolves to an // existing sample before acting on it. An undo on the extension side rolls back the // `banks` ext-state key (removing the sample) but cannot atomically clear the // `assign_request` key if the write happened outside the undo block. Even with the // undo-grouping fix (Major 2), the reader must guard against this: treat an // unresolvable (bankId, sampleId) pair as a stale/no-op request and discard it // silently, never crashing or selecting a nonexistent entry. std::optional decodeAssignmentRequest(const std::string& wire); } // namespace reasampler