Files
reasampler/docs/product/versioning-and-release.md
T
daniel cce9016279 docs: frame versioning scheme + beta side-channel (Phase V)
Add docs/product/versioning-and-release.md and a PLAN.md Phase V pointer
laying out release versioning and the beta side-channel as decidable
options (V1-V4, unsettled - awaiting Daniel).
2026-07-26 05:48:45 -04:00

19 KiB
Raw Blame History

Versioning & release — product notes

Framing behind two release-milestone (M11-adjacent) capabilities Daniel wants made concrete and decidable:

  1. A release version scheme — so a "real" stable build can be deployed and identified.
  2. A beta side-channel — so development can continue and a beta build run alongside the stable one without the beta clobbering the release.

This doc holds the why, the forks, and a recommendation. When Daniel picks, the tickable points land in PLAN.md and the deploy/build wiring hands off to dev-ops. This is a framing note; it changes no source or CMake.

Status: framed by product-designer (2026-07-26). Forks V1V4 open — awaiting Daniel. Nothing here is settled.


The one constraint that shapes everything: REAPER's startup dlopen

REAPER, at startup only, scans UserPlugins/ and dlopen()s every file matching reaper_*.dll|dylib|so, then calls each one's ReaperPluginEntry (CLAUDE.md §REAPER extension contract). Three consequences drive every decision below:

  • Two matching files load simultaneously. If both reaper_reasampler.dll and reaper_reasampler_beta.dll sit in UserPlugins/, REAPER loads both — two independent extension instances in one REAPER process. This is the mechanism a beta side-channel would exploit, and also its central hazard.
  • No hot reload. Deploy = copy the binary in, restart REAPER. Any scheme is "restart to pick up," never live-swap.
  • Everything is process-global inside REAPER. Two coexisting instances share one REAPER, one Actions list, one project, one ext-state store. Anything keyed by a global string (command ids, ext-state namespace, docked-window identity) is a potential collision surface between the two.

Two sharp edges follow directly and recur throughout this note:

  • The STABLE_FOREVER_STRING command-id contract (CLAUDE.md; main.cpp:41, prefix CEREBELLUM_REASAMPLER_). Command-id strings are minted once and never changed after shipping — user keybindings key off them. Two coexisting binaries that register the same id strings collide in REAPER's Actions list.
  • The "reasampler" project ext-state namespace (shared by the banks, view_state, project_guid, tail_setting keys, plus the retired bank_index). Both binaries reading/writing the same namespace on the same open project means a beta can read — and rewrite — a stable project's saved bank/view state. Given the forward-only migrations already in the design (legacy bank_index retired after promotion; banks authoritative thereafter — CONTEXT.md §Multi-bank), a beta that writes a newer schema into a project a user then reopens in stable is a real corruption path, not a theoretical one.

Everything below is really about how much of that global surface a beta channel is allowed to touch.


Part 1 — Version scheme + where the version lives

What we have today

  • No version anywhere. CMakeLists.txt:2 is project(reaper_reasampler LANGUAGES CXX) — no VERSION. The binary announces itself only as "ReaSampler loaded.\n" to the console (main.cpp:960). There is no number a user, a bug report, or a future migration can key off.
  • The natural user-visible readout already exists: the docked LICE bank panel, and the console (ShowConsoleMsg). A version has cheap homes; none is wired.

The number itself (V1) — recommendation: semver, sourced from CMake

Recommend semantic versioning (MAJOR.MINOR.PATCH) with the number's single source of truth in CMakeLists.txt via project(reaper_reasampler VERSION x.y.z), threaded into the binary as a compile definition and surfaced to the user.

Semver fits because ReaSampler already has the two events semver exists to signal, and they matter here specifically:

  • MAJOR / MINOR track user-visible capability (a new pillar landing: Phase R prune, M9 slots).
  • PATCH tracks fixes.
  • Most importantly, ReaSampler carries persisted, migrating project state (the "reasampler" ext-state, forward-only migrations). A version stamped into the saved blob is what lets a future build say "this project was written by 1.4, I am 1.6, run the 1.4→1.6 migration" — or refuse gracefully. That is a concrete, already-latent need, not ceremony. Recommend stamping the writing version into the ext-state blob as part of whichever release wave ships (small addition to the persist section; a sibling schema/app_version field).

Source of truth: CMake project(VERSION). One number, in the build system, flowed outward — never hand-edited in a header. project(... VERSION x.y.z) populates PROJECT_VERSION / PROJECT_VERSION_MAJOR|MINOR|PATCH, which a target_compile_definitions (e.g. REASAMPLER_VERSION="…") threads into the binary. This is the standard CMake idiom and keeps the tag, the binary, and any about-string in lockstep from one edit.

Alternatives considered:

  • Git-derived version (git describe --tags) baked at configure time. Pro: the build literally cannot disagree with the tag; encodes commits-since-tag + dirty state, which is excellent for a beta ("1.4.0-beta.3+7.gab12cd"). Con: needs git present at build and a tag discipline; a source tarball without .git builds "unknown." Recommendation: use it for the beta channel's suffix, not as the base number. Best of both: project(VERSION) owns the release triple; git describe decorates beta builds. (See V2.)
  • A hand-maintained version.h. Rejected — a second source of truth that drifts from the tag. The whole point of one source is that release can't ship a binary that lies about its number.
  • Date-based / CalVer (2026.07). Coherent, but ReaSampler's changes are capability-shaped (pillars landing), not time-shaped, and CalVer says nothing about migration compatibility, which is the load-bearing use here. Semver earns its place; CalVer doesn't.

Where the user sees it (V3) — recommendation: panel readout + startup log, defer an about box

Three candidate homes, cheap to expensive:

  1. Startup console line — upgrade "ReaSampler loaded.\n" to "ReaSampler x.y.z loaded.\n". Nearly free, and it lands the version in exactly the place a user copies from when filing a bug. Do this regardless of the other choices.
  2. Bank-panel readout — a small version string in the docked LICE panel (header corner or footer, near the existing tail toggle / mode switch). Always visible, no new window, matches the LICE-drawn house style. Recommend this as the primary user-facing home.
  3. An "about" action / dialog — a bindable ReaSampler: about that pops version + build channel + build hash. More than the moment needs; a SWELL dialog is real surface to maintain. Defer — the panel readout + console line cover the actual need (identify the running build). Pick this up only if a beta channel makes "which build am I running" a frequent question.

Recommendation: (1) + (2) now, (3) deferred. The version wants to be visible passively (panel) and copyably (console), and a beta channel makes the panel readout do double duty as the channel indicator (Part 2).

Open decision V3 for Daniel: panel placement — header corner vs. footer strip vs. folded into the existing mode-switch/tail-toggle chrome. This is a small panel polish call, same class as the settled "active-bank indicator placement" residual.


Part 2 — Beta side-channel

The goal: keep shipping a stable build users depend on, while running a beta of in-progress work alongside it, so the beta can be exercised in real projects without the beta's newer/rougher state clobbering the stable install or stable projects.

The design axis is coexistence: must stable and beta load into the same REAPER at the same time (true side-by-side), or is it enough to run one at a time with clean, safe switching? That axis splits the options.

Option A — Separate binary name, both load simultaneously (true coexistence)

Ship reaper_reasampler_beta.dll next to reaper_reasampler.dll. REAPER loads both; the user has a stable ReaSampler and a beta ReaSampler live in one session.

This is the most powerful shape — and the most dangerous, because of the three global-collision surfaces the startup-dlopen constraint creates. A separate binary name alone does not isolate them; it just makes them coexist. For Option A to be safe, three things must diverge in lockstep, not just the filename:

  1. Command-id strings must diverge. Both binaries register actions; if the beta mints CEREBELLUM_REASAMPLER_CAPTURE_TRACK too, REAPER's Actions list has two entries claiming one id — collision, and the STABLE_FOREVER_STRING contract is violated. The beta needs its own prefix (e.g. CEREBELLUM_REASAMPLER_BETA_). Sharp edge: that means the beta's ids are a distinct forever-family — a user's beta keybindings won't carry to stable, and vice versa. That is arguably correct (they're different installs), but it must be a deliberate decision, because once the beta ships those beta ids are also forever-stable. You are minting a second permanent id namespace, not a throwaway.

  2. The ext-state namespace must diverge — or the beta corrupts stable projects. This is the severe one. If the beta writes banks/view_state under "reasampler", a project saved by the beta carries beta-schema state that stable then reads (and the forward-only migration may have already retired the key stable expected). The beta must write under its own namespace (e.g. "reasampler_beta"). Consequence, and it cuts both ways: a beta cannot see a stable project's bank (different namespace), so testing the beta against a real populated project means the bank looks empty until re-captured. That is the price of isolation, and it is the right price — a beta that shares stable's namespace is a data-loss bug waiting to happen. This divergence is non-negotiable if Option A is chosen.

  3. The docked-window / docker identity should diverge, so the two panels are distinguishable and don't fight over one dock slot. Lower-severity (a UX annoyance, not corruption), but part of the same "everything global must fork" picture.

Net: Option A delivers genuine side-by-side at the cost of forking every global identity the extension owns. It is a compile-time-parameterized second product, not a branch artifact — which points straight at Option C as the mechanism for how you'd actually build it.

Option B — One binary, branch discipline, one installed at a time

dev → beta builds, main → release builds; the version string carries a -beta/-dev suffix so the running build self-identifies; only one is installed at a time. No coexistence — you swap the binary and restart REAPER to change channels.

  • Pro: zero collision surface. Same command ids, same ext-state namespace, one file — because only one is ever loaded. Nothing forks. Simplest by a wide margin.
  • Pro: matches the repo's existing branch reality (dev is the working branch, main the release branch — visible in the current git state).
  • Con: no true side-by-side. To A/B stable against beta you swap files and restart. For a solo developer this is often entirely fine — the friction is a file copy + restart, not a corruption risk.
  • Sharp edge (the reason it's still not free): if the beta writes a newer/experimental ext-state schema into a real project, then you swap back to stable and open that project, stable reads beta-written state. One-binary branch discipline removes the simultaneous collision but not the sequential one — the project file is the shared surface across a channel swap. Mitigation: either (a) keep the beta strictly schema-compatible with stable (no ext-state shape changes on the beta channel — often true, since most beta work is behavior, not persistence), or (b) accept "don't open beta-touched real projects in stable" as a discipline, or (c) stamp the writing version into the blob (V1) so stable can at least detect and refuse/migrate rather than silently mis-read.

Option C — Compile-time build flag (the mechanism, usable under A or B)

A CMake/preprocessor switch (-DREASAMPLER_CHANNEL=beta) that, in one build tree, sets: output name, command-id prefix, ext-state namespace, version suffix, and the panel channel indicator. This is not really a third strategy — it's how you implement the divergence Option A demands, or how you stamp the suffix Option B wants. It's the knob; A and B are policies for the knob.

  • Under Option A, the flag is what forks the two binaries from one source cleanly — flip REASAMPLER_CHANNEL, get the beta's name/prefix/namespace/suffix as a coordinated set, so the three-way divergence can't get out of sync by hand.
  • Under Option B, the flag just sets the suffix + a channel tag; name/prefix/ namespace stay shared because only one loads.

The flag is worth having either way because it makes "what makes a beta a beta" a single, auditable definition instead of scattered #ifdefs.

Recommendation: B now, built on C, with A as the deliberate upgrade later

Start with Option B (branch discipline, one at a time) implemented through a compile-time channel flag (Option C):

  • Ship the release from main as reaper_reasampler with a clean semver (x.y.z).
  • Build beta from dev through -DREASAMPLER_CHANNEL=beta, which stamps a -beta/-dev.<git-describe> version suffix and a panel channel indicator (so a beta is unmistakable at a glance — the version readout from Part 1 doubles as the channel badge). This is where git describe earns its keep (V2): the beta suffix carries commits-since-tag so two betas are distinguishable.
  • Do not fork the command-id prefix or the ext-state namespace yet — because under B only one binary loads, so there is nothing to collide with, and forking them prematurely commits you to a second forever-stable id family and splits your own test data for no gain.

Then treat Option A (true coexistence) as a later, deliberate upgrade if and only if "swap-and-restart" proves too slow — i.e. you find you genuinely need stable and beta live in one REAPER to compare them. The upgrade is clean precisely because you built B on the channel flag: flipping A "on" means extending the same flag to also fork the output name, the command-id prefix (_BETA_), and the ext-state namespace ("reasampler_beta"). The seam is designed now; the feature is deferred (the same defer-the-feature-design-the-seam discipline the project already applies to the owned-file manifest and Phase D adaptability).

Why not A first: A's power is real but it forces you to mint a second permanent command-id namespace and a second ext-state namespace on day one — both irreversible-ish commitments (the ids are forever-stable; the namespace split means beta and stable never share a project's bank). That is a large, permanent price for a convenience (simultaneous compare) you may not need. B pays nothing permanent and keeps A one flag away.

Why not B without the flag: hand-managing the suffix and channel badge across two branches drifts. The flag makes "beta vs. release" one definition and is the exact thing you'd extend for A — so building B through C costs little now and saves the A upgrade later.

The sharp edge that survives every option: ext-state across a channel swap

Whichever way this goes, call out loudly (and this is a genuine Daniel decision, V4): can a beta build write experimental "reasampler" ext-state schema that a stable build might later read?

  • Under A with a forked namespace: no — isolated, safe, at the cost of beta not seeing stable's bank.
  • Under B: yes, sequentially — the project file is shared across a swap. Needs a policy: beta stays schema-compatible, or the version-stamp-in-blob (V1) lets stable detect-and-refuse, or "don't open beta-touched real projects in stable" is an accepted discipline.

The cheapest robust mitigation across both is V1's version stamp in the ext-state blob — it turns a silent mis-read into a detectable "this project was written by a newer/other build." Recommend adopting it as part of the first release wave regardless of the A/B choice, because it's the seam that makes every later channel and migration decision safe.


Part 3 — Deploy / CD touchpoints (hand-off to dev-ops)

Framing only — the actual pipeline is dev-ops's to author. The strategy above implies these touchpoints:

  • Tag → version. Release is cut from a git tag on main; project(VERSION) is bumped to match the tag (or the tag is derived from it — pick one direction and keep it one-way). Beta builds from dev carry git describe in the suffix.
  • Channel is a build parameter. -DREASAMPLER_CHANNEL=release|beta selects the suffix/badge now (Option B) and, if Option A is ever turned on, the output name/prefix/namespace too. One flag, one definition of "what is a beta."
  • Artifact per platform. The binary is reaper_*.dll|.dylib|.so; the macOS/Linux builds need the SWELL resgen step (CLAUDE.md §SWELL dialog resources) baked into the pipeline. Three platform artifacts per channel per release.
  • Install is copy-in + restart (no hot reload). A release "deploy" is publishing the artifact for the user to drop into UserPlugins/; there is no server-side rollout. Any auto-update story is out of scope here and would be its own note.
  • The ext-state version stamp (V1) is the one piece of forward-compatibility plumbing the pipeline should ensure ships in the first versioned release, so every subsequent build can reason about older saved state.

Open decisions for Daniel

  • V1 — version scheme. Recommend semver via project(VERSION) as single source of truth, plus a writing-version stamp in the "reasampler" ext-state blob for forward migration/detection. Approve scheme + whether the ext-state stamp lands in the first release wave.
  • V2 — beta suffix source. Recommend git describe decorating beta builds (x.y.z-beta.N+g<hash>) while project(VERSION) owns the release triple. Approve or simplify to a plain -beta suffix.
  • V3 — user-visible home + placement. Recommend startup console line + a bank-panel version/channel readout now; about-box deferred. Panel placement (header corner / footer / folded into existing chrome) is the residual call.
  • V4 — beta channel shape (the big one). Recommend Option B (branch discipline, one at a time) built on an Option C compile-time channel flag, with Option A (true simultaneous coexistence) designed-as-a-seam but deferred. The sub-decision that must be made explicitly either way: the ext-state-across-a- channel-swap policy — isolate (A), stay schema-compatible / stamp-and-detect (B), or accept the discipline. This is genuinely Daniel's call, not one to default.