Files
reasampler/docs/product/versioning-and-release.md
T
daniel 6a6d305cf2 docs: archive Phase V (V1-V4) to COMPLETED; reconcile CLAUDE.md
Phase V landed: app_version module, ext-state version stamp, show-version
action, beta-in-isolation channel build. CLAUDE.md gains the module entry,
version key, test target, and beta-build note.
2026-07-26 16:56:04 -04:00

394 lines
23 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); **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; `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) — 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 `#ifdef`s.
## 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_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 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 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.