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

335 lines
19 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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 `#ifdef`s.
## 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.