b584fa4da9
Ten more were already pointing at unrelated code. Vendor SDK and SWELL citations left alone — those files don't move.
394 lines
23 KiB
Markdown
394 lines
23 KiB
Markdown
# 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); V1–V4 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 `#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.
|