Files
reasampler/docs/product/versioning-and-release.md
T
daniel b584fa4da9 docs: anchor the product-doc citations to symbols, not line numbers
Ten more were already pointing at unrelated code. Vendor SDK and SWELL citations left alone — those files don't move.
2026-08-02 20:46:28 -04:00

23 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. The tickable points' landed history is in docs/ARCHIVE.md; 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); V1V4 implemented and landed (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.


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; app_version.h's commandIdPrefix(), 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 — docs/ARCHIVE.md §Phase B), 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 is project(reaper_reasampler LANGUAGES CXX) — no VERSION. The binary announces itself only as "ReaSampler loaded.\n" to the console (main.cpp). 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 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." This was originally recommended for the beta suffix — Daniel rejected it (V2 settled below: a plain -beta suffix is more legible than a decorated git describe string). 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:

  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.

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 binaryreaper_reasampler_beta alongside reaper_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:

  1. A second forever-stable command-id prefix. The beta's ids are their own forever-family (STABLE_FOREVER_STRING contract applies to them too the moment a beta ships). Beta keybindings do not carry to stable and vice versa. Irreversible.
  2. 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 platformreaper_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 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.

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 -beta suffix. The git describe decoration 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.