V1 semver via project(VERSION), ext-state version stamp first-wave. V2 plain -beta suffix. V3 console line + panel readout. V4 reverses prior rec: beta ships as a separate coexisting binary with isolated ext-state namespace + command-id prefix.
23 KiB
Versioning & release — product notes
Framing behind two release-milestone (M11-adjacent) capabilities Daniel wants made concrete and decidable:
- A release version scheme — so a "real" stable build can be deployed and identified.
- 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); all four forks settled by Daniel
(2026-07-26). V1 approved as recommended (ext-state version stamp prioritized to
the first wave); V2 plain -beta suffix (git describe decoration rejected); V3
recommendation accepted (console line + panel readout, about-box deferred); V4
reversed the recommendation — Daniel chose beta-in-isolation / full coexistence
(separate binary, isolated ext-state namespace, isolated command-id prefix) rather
than the branch-discipline/one-at-a-time path the note originally recommended. The
"Recommendation" and "Open decisions" sections below have been superseded to reflect
the settled state; each records what was chosen and why. Deploy/build wiring hands off
to dev-ops.
Note on V4 phrasing (provisional). The-boss's read of "beta-in-isolation" as the separate-binary + isolated-namespace + isolated-command-id-prefix coexistence path is provisional pending a final confirm from Daniel. Written that way here; a minor correction is a cheap edit.
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.dllandreaper_reasampler_beta.dllsit inUserPlugins/, 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_STRINGcommand-id contract (CLAUDE.md;main.cpp:41, prefixCEREBELLUM_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 thebanks,view_state,project_guid,tail_settingkeys, plus the retiredbank_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 (legacybank_indexretired after promotion;banksauthoritative 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:2isproject(reaper_reasampler LANGUAGES CXX)— noVERSION. 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) — SETTLED (2026-07-26): semver, sourced from CMake; ext-state stamp lands ASAP / first wave
Decision (V1). Approved as recommended. Semver, single source of truth in CMake
project(reaper_reasampler VERSION x.y.z), threaded into the binary. The"reasampler"ext-state writing-version stamp is prioritized to the first wave, not deferred — Daniel emphasized ASAP because every project saved without the stamp is harder to migrate later, so the migration seam must exist before more real projects accrue un-stamped state. Treat the stamp as an early, high-priority deliverable that ships with (or ahead of) the first versioned build.
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 siblingschema/app_versionfield).
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.gitbuilds "unknown." This was originally recommended for the beta suffix — Daniel rejected it (V2 settled below: a plain-betasuffix is more legible than a decoratedgit describestring).project(VERSION)still owns the release triple; the beta simply carries-beta. (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) — SETTLED (2026-07-26): panel readout + startup log; about box deferred
Decision (V3). Recommendation accepted (Daniel deferred to product-designer judgment — "whatever"). Startup console line (
"ReaSampler x.y.z loaded") + a bank-panel version/channel readout ship; the about-box stays deferred. Panel placement (header corner / footer / folded into existing chrome) remains a small residual polish call, same class as the settled active-bank-indicator placement.
Three candidate homes, cheap to expensive:
- 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. - 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.
- An "about" action / dialog — a bindable
ReaSampler: aboutthat 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:
-
Command-id strings must diverge. Both binaries register actions; if the beta mints
CEREBELLUM_REASAMPLER_CAPTURE_TRACKtoo, REAPER's Actions list has two entries claiming one id — collision, and theSTABLE_FOREVER_STRINGcontract 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. -
The ext-state namespace must diverge — or the beta corrupts stable projects. This is the severe one. If the beta writes
banks/view_stateunder"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. -
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 (
devis the working branch,mainthe 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.
Decision (V4) — SETTLED (2026-07-26): Option A — beta-in-isolation / full coexistence
This reverses the note's original recommendation. The note originally recommended Option B (branch discipline, one installed at a time) built on the Option C channel flag, with Option A (true simultaneous coexistence) seam-designed but deferred. Daniel chose the coexistence path instead. The superseded recommendation prose is retained below the decision (struck through in intent, kept for the reasoning trail) so a future reader sees what was weighed; the decision here governs.
Settled shape: beta ships as a separate, fully isolated binary that coexists with stable in one REAPER. Concretely, all three global identities fork (this is Option A, built through the Option C compile-time channel flag as the mechanism):
- Separate binary —
reaper_reasampler_betaalongsidereaper_reasampler, so REAPER's startup dlopen loads both and the user runs stable and beta side-by-side. - Isolated ext-state namespace — the beta writes under its own namespace (e.g.
"reasampler_beta"), distinct from stable's"reasampler", so a beta can never read or rewrite a stable project's saved bank/view/tail state. The price is accepted: a beta does not see a stable project's bank (it looks empty until re-captured under the beta namespace). That is the correct price — isolation over convenience. - Isolated forever-stable command-id prefix — the beta mints its own prefix (e.g.
CEREBELLUM_REASAMPLER_BETA_), so beta and stable actions never collide in REAPER's one Actions list and their keybindings stay independent.
The compile-time channel flag (-DREASAMPLER_CHANNEL=beta, Option C) remains the
implementation mechanism: one flip coordinates output name + command-id prefix +
ext-state namespace + -beta version suffix + panel channel badge as a single
auditable definition, so the three-way divergence can't drift by hand.
The two permanent commitments this locks in (recorded honestly)
Choosing coexistence over the deferred-seam path accepts two commitments that are permanent once the first beta ships — exactly the prices the original recommendation flagged as reasons to defer:
- A second forever-stable command-id prefix. The beta's ids are their own
forever-family (
STABLE_FOREVER_STRINGcontract applies to them too the moment a beta ships). Beta keybindings do not carry to stable and vice versa. Irreversible. - A second ext-state namespace.
"reasampler_beta"is permanent — and its isolation means beta and stable never share a project's bank. This is the safety property, but it is also a fork of your own test data that you own from day one.
Both are deliberate and accepted; recorded here so no future reader treats them as oversights.
Deploy / CD implication (dev-ops hand-off)
The build now produces two named artifacts per platform — reaper_reasampler
(stable) and reaper_reasampler_beta (beta) — selected by REASAMPLER_CHANNEL.
That is a dev-ops handoff: the pipeline builds, names, and publishes both channels
(three platform artifacts each). See Part 3.
The ext-state safety property, now settled by isolation
The sharp edge the original note flagged as the genuine V4 sub-decision — can a beta
write experimental ext-state that a stable build later reads? — is resolved by the
isolated namespace: no. Beta writes only under "reasampler_beta"; stable reads
only "reasampler". There is no shared-project corruption path across the channels,
neither simultaneous nor sequential. V1's writing-version stamp still lands first-wave
(it guards within-channel forward migration — stable-1.4 reading stable-1.6 state —
which isolation does not address), but it is no longer load-bearing for cross-channel
safety.
Superseded recommendation (retained for the reasoning trail)
The following was the note's original V4 recommendation. It is superseded by the settled decision above (Daniel chose coexistence). Kept because the trade-off reasoning — the permanent-price argument in particular — is what the decision was weighed against.
Start with Option B (branch discipline, one at a time) implemented through a
compile-time channel flag (Option C): ship stable as reaper_reasampler from
main; build beta from dev via the channel flag with only a -beta suffix + panel
badge; do not fork the command-id prefix or ext-state namespace, since under B
only one binary loads and forking prematurely commits a second forever-stable id
family for no gain. Then treat Option A (true coexistence) as a later, deliberate
upgrade if swap-and-restart proved too slow — the upgrade being clean precisely
because B was built on the channel flag (flip A on = extend the flag to fork name +
_BETA_ prefix + "reasampler_beta" namespace). The stated reason to defer A: it
forces minting a second permanent command-id namespace and a second ext-state
namespace on day one — a large, permanent price for a convenience (simultaneous
compare) that might not be needed. Daniel weighed that price and chose to pay it:
the value of running stable and beta side-by-side, with hard isolation guaranteeing a
beta can never corrupt stable state, outweighed avoiding the two permanent
commitments.
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 fromdevcarrygit describein the suffix. - Channel is a build parameter.
-DREASAMPLER_CHANNEL=release|betaselects 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.
Settled decisions (all four — Daniel, 2026-07-26)
- V1 — version scheme: APPROVED as recommended. Semver, single source of truth in
CMake
project(reaper_reasampler VERSION x.y.z), threaded into the binary. The"reasampler"ext-state writing-version stamp is prioritized to the first wave — every project saved without it is harder to migrate later, so the migration seam lands early, not deferred. - V2 — beta suffix: plain
-beta.project(VERSION)owns the release triple; beta builds carry a-betasuffix. Thegit describedecoration was considered and rejected for legibility ("-beta is better than a random string"). - V3 — user-visible home: recommendation accepted. Startup console line
(
"ReaSampler x.y.z loaded") + a bank-panel version/channel readout; about-box deferred. Panel placement remains a small residual polish call. - V4 — beta channel shape: BETA-IN-ISOLATION (full coexistence). Reverses the
original recommendation. Beta ships as a separate binary
(
reaper_reasampler_beta) with an isolated ext-state namespace and an isolated forever-stable command-id prefix, so stable and beta install and run side-by-side with no shared-state corruption path. Locks in two permanent commitments — a second forever-stable command-id prefix and a second ext-state namespace — both accepted. Dev-ops implication: the build now produces two named artifacts (stable + beta) per platform.