SRD-031.A — Definition versioning and the registration handle¶
| Field | Value |
|---|---|
| Status | Accepted |
| Version | v.1 |
| Date | 2026-06-28 |
| Owner | Ruslan Gabitov |
| Implements | ADR-019 v.1 Definition versioning |
This SRD lands the versioning half of ADR-019
v.1: RegisterProcess returns a
registration handle naming a (key, version); the engine snapshot is
isolated from the model at registration time (closing the architecture audit
§3.3 mutation leak/race by construction); instances start by handle, by
(key, version), or by latest-of-key; and registering a newer version
supersedes the previous version's auto-start. The Thresher registry's
concurrency-discipline rework (audit §2.6) is the sibling SRD-031.B, landed
on the same branch after this one. The first-class model-editing API is a
separate ADR, not in scope here.
1. Background & current state (verified against the code)¶
1.1 Registration identity is ambiguous, and re-registration is a silent no-op¶
Thresher.RegisterProcess returns only an error and stores the snapshot keyed
on the process id; a second registration of the same id is silently discarded
(pkg/thresher/thresher.go:485-491):
t.m.Lock()
if _, ok := t.snapshots[s.ProcessID]; ok {
// Already registered — idempotent, keep the first registration.
t.m.Unlock()
return nil
}
StartProcess then takes that id string and starts whatever is stored under
it (pkg/thresher/thresher.go:702,716):
func (t *Thresher) StartProcess(processID string) (*InstanceHandle, error) {
...
s, ok := t.snapshots[processID]
A caller who edits a process and re-registers it expecting the new shape gets the old snapshot, with no error — and addresses the registration by reaching back into the model object for its id, with no way to say which registered shape is meant.
1.2 The snapshot shares the model's node graph — the audit §3.3 leak¶
snapshot.New does not copy the definition's nodes/flows; it stores the model
objects by reference (internal/instance/snapshot/snapshot.go:58,68,113):
Properties: p.Properties(),
...
for _, n := range p.Nodes() {
s.Nodes[n.ID()] = n // shares the model node by pointer
}
...
s.Flows[f.ID()] = f // shares the model flow by pointer
The deep copy happens only later, once per running instance, in Clone
(snapshot.go:149-237): clone every node, relink flows, remap default flows,
rebind boundary events. The process model stays mutable after registration via
its public Add/Remove (pkg/model/process/process.go:168,272), so:
- a node/flow added after registration never enters the already-taken snapshot (invisible), and
- an in-place edit to an existing shared node reaches the snapshot through the
shared pointer and is picked up by the next
Clone, racing any instance already cloning it (no lock on that path).
This is architecture-audit §3.3 ("validation and mutability of the process model"). ADR-019 §2.3 resolves it by isolation (copy at registration) rather than the audit's freeze.
1.3 The registry today¶
The Thresher holds four maps under one mutex (pkg/thresher/thresher.go:109-121):
snapshots map[string]*snapshot.Snapshot // key = ProcessID
instances map[string]instanceReg // key = instance id
starters map[string][]*instanceStarter // key = ProcessID
seenKeys map[string]struct{} // correlation dedup
m sync.Mutex
state State
Auto-start starters are built per process by scanInstantiatingStarts
(instance_starter.go:109) and registered/torn down on the EventHub via
registerStarters (thresher.go:561, RegisterPersistentEvent) and
eventHub.UnregisterEvent(st, st.eDef.ID()) (thresher.go:544). Running
instances are tracked independently in instances and are unaffected by
unregistering a process (UnregisterProcess comment, thresher.go:512-514) —
they keep running their own per-instance clone.
2. Requirements¶
Functional¶
- FR-1 — Snapshot isolation.
snapshot.Newdeep-copies the definition's node graph (nodes, flows, default-flow and boundary wiring) so the snapshot owns an independent graph. AfterRegisterProcessreturns, no edit to the source*process.Process— structural or in-place — changes the taken snapshot or any instance later born from it. Immutable per-definition config (properties, correlation keys) may remain shared by reference. - FR-2 — Registration handle.
RegisterProcessreturns a*ProcessRegistrationexposingKey() string,Version() int,ID() string. - FR-3 — Versioning. Registering a process whose key already has registrations appends a new version. Version numbers come from a per-key monotonic counter, not the slice length: a number is never reused while the key has at least one live version, so removing a non-latest version (FR-8) cannot make the next registration collide with a still-registered version. The counter resets only when the key is fully unregistered (a later registration of that key is v1 again). The silent idempotent no-op (§1.1) is removed. A process with no explicit id (auto-generated UUID) is its own singleton key at version 1.
- FR-4 — Start by handle.
StartProcess(reg *ProcessRegistration)starts the exact version the handle names and returns its*InstanceHandle. - FR-5 — Start by
(key, version).StartVersion(key string, version int)starts that specific version, or errors (ObjectNotFound) if the key or version is unknown. It addresses by the version number, not slice position, so it stays correct across gaps left by non-latest removals (FR-8) — e.g. after v2 is removed,StartVersion(key, 3)still starts v3 andStartVersion(key, 2)errors. - FR-6 — Start latest.
StartLatest(key string)starts the latest registered version of the key, or errors (ObjectNotFound) if the key is unknown. - FR-7 — Latest-supersedes auto-start. Registering a newer version of a key in auto mode tears down the previous version's instance-starters from the EventHub and registers the new version's, so only the latest version spawns new instances from a trigger. Instances already running under older versions continue to completion. Manual-start versions register no starters.
- FR-8 — Unregister one version by handle, promote-on-removal.
UnregisterVersion(reg *ProcessRegistration)removes the single version that handle names (its snapshot and, if it was the live latest, its starters). Running instances survive. Removing the latest auto version promotes the now-newest remaining version: its starters are registered so it becomes the live auto-start version — the invariant latest registration == live starter set holds at all times. (This is how a user re-activates a previous version's auto-start: remove the later versions until it is latest again; a previous version is otherwise manual-only viaStartVersion.) Removing the last version of a key drops the key entirely. Removing a non-latest version touches no hub subscriptions and leaves a gap in the key's version sequence (v1, v3, …) — the surviving versions keep their numbers, addressable byStartVersion(FR-5) and enumerable byRegistrations(FR-10);StartLateststill resolves to the highest surviving version. - FR-9 — Migration. Every in-repo caller of the changed methods adopts the new
API: all
examples/*, allpkg/thresher/*_test.go,README.md,README.ru.md,examples/README.md. Examples build and run green. - FR-10 — List registered versions.
Registrations(key string) []*ProcessRegistrationreturns the key's registered versions ascending by version (empty for an unknown key). Each element is a live handle — read itsVersion()/ID(), or pass it straight toStartProcess/UnregisterVersion— so a caller can discover which versions exist (including gaps from FR-8) before addressing one byStartVersion. - FR-11 — Unregister the whole process by key.
UnregisterProcess(key string)removes every registered version of the key in one call, drops the key, and resets its version counter (a later registration of the key is v1 again). It is the bulk counterpart ofUnregisterVersion; the name matches its scope — a process is the whole keyed definition, a version is one snapshot of it. Only the latest version's starters are live, so only those are torn down; running instances of any version survive. ErrorsEmptyNotAllowedon an empty key,ObjectNotFoundon an unknown key.
Non-functional¶
- NFR-1 — Parameter validation. Every new/changed public method rejects bad input with a self-identifying error (nil handle, empty key, version < 1) — per the project's public-API validation rule — never a silent default or panic.
- NFR-2 — No new races. The change introduces no data race under concurrent
register/start/unregister (
go test -race). This SRD preserves the existing lock discipline; the explicit atomic-state/lock-boundary hardening (audit §2.6) is SRD-031.B. - NFR-3 — Bounded cost. Isolation adds one graph clone per registration (paid
once, bounded by definition size); no per-instance runtime regression (the
per-instance
Cloneis unchanged, now cloning from the isolated snapshot). - NFR-4 — Coverage. Diff-coverage ≥ project standard (95%, aim 100%) on every
touched file;
make cigreen. - NFR-5 — Documented breaking change. The API break is recorded in
CHANGELOG.mdwith a migration note (oldStartProcess(id)→StartProcess(reg)/StartLatest(key)).
3. Models¶
3.1 ProcessRegistration — the registration handle (pkg/thresher/registration.go, new)¶
A read-only receipt wrapping the engine's internal per-version state, mirroring
InstanceHandle's wrap-by-reference pattern (handle.go:24):
// ProcessRegistration is the receipt for one registered version of a process
// definition. It names a (key, version) and is the token passed to StartProcess
// and UnregisterVersion. Read-only: it exposes identity, never the snapshot or
// the engine internals it wraps.
type ProcessRegistration struct {
key string // the versioning key = process id
version int // 1-based, increments per key
id string // opaque registration id (foundation id)
snapshot *snapshot.Snapshot // the frozen version (engine-internal)
starters []*instanceStarter // auto-start starters of this version (nil in manual mode)
manual bool // registered WithManualStart
}
func (r *ProcessRegistration) Key() string { return r.key }
func (r *ProcessRegistration) Version() int { return r.version }
func (r *ProcessRegistration) ID() string { return r.id }
3.2 The versioned registry (pkg/thresher/thresher.go)¶
The snapshots and starters maps collapse into one ordered-by-version map; each
version's snapshot and starters live on its ProcessRegistration. instances,
seenKeys, m, and state are unchanged by this SRD.
// registrations maps a process key (process id) to its versions in registration
// order: index i is version i+1; the last element is the latest.
registrations map[string][]*ProcessRegistration
instances map[string]instanceReg // unchanged
seenKeys map[string]struct{} // unchanged (see §4.5)
3.3 snapshot.New clones the definition directly; a shared wiring helper keeps it DRY (internal/instance/snapshot/snapshot.go)¶
New already makes one pass over the definition's nodes (p.Nodes(),
snapshot.go:67) to validate the process (start/end/instantiating-node checks).
That same pass now clones each node (n.Clone()) into s.Nodes instead of
sharing it by reference (snapshot.go:68), so the snapshot is born isolated —
with no throwaway model-sharing intermediate and no redundant second pass over
the definition (the return s.Clone() shape would build a shared snapshot only
to re-clone it).
The graph still needs the post-node-clone wiring that Clone performs — relink
the flows between the cloned nodes (flow.MustCloneFlow), remap gateway default
flows, rebind boundary events onto their cloned hosts (snapshot.go:166-234).
That wiring is identical whether the cloned nodes came from a process or from a
snapshot, so it is the one piece extracted into an unexported helper (the
node-cloning loop differs per source; the wiring does not):
// wireClonedGraph relinks the flow graph between an already-cloned node set,
// remaps gateway default flows, and rebinds boundary events onto their cloned
// hosts. Shared by New (cloning from the process definition) and Clone (cloning
// from the snapshot). Signature finalized in implementation; conceptually it
// takes the cloned nodes + the source nodes/flows and returns the cloned flows.
func wireClonedGraph(
clonedNodes map[string]flow.Node,
srcNodes map[string]flow.Node,
srcFlows map[string]*flow.SequenceFlow,
) (map[string]*flow.SequenceFlow, error)
New clones nodes in its validation pass and then calls wireClonedGraph;
Clone clones s.Nodes and calls the same helper. Neither duplicates the
relink/remap/rebind logic, and New touches the definition only in its single
existing pass. RegisterProcess then runs scanInstantiatingStarts on the
isolated snapshot, so starters reference the frozen graph. Properties/CorrelationKeys
stay shared (immutable; the Clone shares-Properties invariant anchored by
TestSnapshotCloneSharesProperties on master remains true).
4. Analysis¶
4.1 New clones the definition directly; Clone-shared wiring (decided)¶
The snapshot is born isolated: New clones each node in the single validation
pass it already makes over the definition, rather than sharing the model and then
re-cloning it. The non-trivial post-clone wiring — relink flows, remap default
flows, rebind boundary events — is identical for a process source and a snapshot
source, so it is the one piece extracted into a shared helper (wireClonedGraph,
§3.3); the per-source node-cloning loop stays in each of New and Clone. This
gives single-pass construction with no duplicated wiring logic, and the two
isolation hops still compose cleanly (instance graph ⊂ snapshot graph ⊂ model).
Rejected alternatives: (a) return s.Clone() from New — simplest to write,
but builds a throwaway model-sharing snapshot and re-clones it, a redundant pass
over the definition; (b) inline the full relink/remap/rebind in New as well as
Clone — single-pass, but duplicates ~40 lines of tricky graph-wiring in two
places that must stay correct in lockstep.
4.2 Version assignment is a per-key monotonic counter, not the slice index (decided)¶
The version is not the slice position. An earlier draft assigned version =
len(registrations[key]) + 1 and looked up regs[version-1], coupling the number
to the position — which breaks the moment a non-latest version is removed
(FR-8): the slice [v1, v2, v3] becomes [v1, v3], so regs[version-1] returns
the wrong snapshot for StartVersion(key, 2)/(key, 3), and the next
registration reuses number 3. So version assignment uses a per-key monotonic
counter (nextVersion map[string]int): each registration increments it and
takes the new value, so a number is never reused while the key lives. The counter
is dropped with the key on full unregistration (a fresh key restarts at v1). The
slice stays append-ordered and removals preserve order, so it is always ascending
by version — hence the latest (FR-6) is still regs[len-1] (O(1)), while
lookup-by-version (FR-5) scans for the matching version (O(len), gap-safe)
and enumeration (FR-10) returns a copy of the slice. Registration appends
under t.m; the supersession hub calls run outside t.m (§4.4).
4.3 Latest-supersedes composes existing primitives (decided)¶
FR-7 is the existing teardown + register sequence applied across versions: on
registering a new auto version when the engine is Started, unregister the prior
latest's starters (eventHub.UnregisterEvent(st, st.eDef.ID()), as
UnregisterVersion already does, thresher.go:545) and register the new
version's (registerStarters, thresher.go:561). No new hub mechanism. This is
the direct analogue of Camunda's "subscriptions of the previous version are
canceled" (ADR-019 §2.5). Before Run (engine not yet Started) nothing is on
the hub yet, so supersession is deferred to Run/registerAllStarters, which
registers only each key's latest version's starters.
4.4 Lock discipline preserved (decided; hardening deferred to SRD-031.B)¶
This SRD keeps the current discipline: mutate registrations under t.m; run
hub calls (register/unregister starters) and launchInstance outside t.m
(FIX-002 RC2, thresher.go:498-501). The fragile-comment problem the audit flags
(§2.6) is not fixed here — it is the sibling SRD-031.B's subject, landed next
on the same branch so the hardening operates on this SRD's final registry shape.
4.5 seenKeys stays keyed by process id across versions (decided)¶
Correlation dedup namespaces by s.ProcessID (thresher.go:613), which is the
key — shared across a key's versions. Because only the latest version auto-starts
(FR-7), create-or-join stays coherent in practice. Cross-version correlation (a
conversation begun under v1 routed into a v2 instance) is an ADR-019 §2.8
non-goal; seenKeys semantics are unchanged here.
4.6 The handle is engine-scoped (decided)¶
StartProcess/UnregisterVersion validate the handle is non-nil (NFR-1);
UnregisterVersion additionally verifies the handle is a member of this engine's
registrations (else ObjectNotFound), since removal is bookkeeping on this
registry. StartProcess launches reg.snapshot directly. Passing a foreign
handle is a programming error surfaced by the membership check on unregister.
(UnregisterProcess is key-addressed, not handle-addressed, so it has no
membership check — it errors ObjectNotFound when the key has no versions.)
5. Public API surface¶
// pkg/thresher
func (t *Thresher) RegisterProcess(p *process.Process, opts ...RegisterOption) (*ProcessRegistration, error)
func (t *Thresher) StartProcess(reg *ProcessRegistration) (*InstanceHandle, error)
func (t *Thresher) StartVersion(key string, version int) (*InstanceHandle, error)
func (t *Thresher) StartLatest(key string) (*InstanceHandle, error)
func (t *Thresher) UnregisterVersion(reg *ProcessRegistration) error
func (t *Thresher) UnregisterProcess(key string) error
func (t *Thresher) Registrations(key string) []*ProcessRegistration
func (r *ProcessRegistration) Key() string
func (r *ProcessRegistration) Version() int
func (r *ProcessRegistration) ID() string
RegisterProcess/StartProcess keep their names (parameters/returns change). The
single-version teardown is renamed UnregisterProcess(id) → UnregisterVersion(reg),
and UnregisterProcess(key) is repurposed to remove the whole process (every
version) — so the name now matches its scope. New: StartVersion/StartLatest/
Registrations/UnregisterProcess(key).
snapshot.New's signature is unchanged (its behavior — deep copy — changes).
Thresher.Instance(id) and the SRD-019 discovery surface are unaffected.
6. Test scenarios¶
| # | Scenario | FR | Where |
|---|---|---|---|
| T-1 | After RegisterProcess, mutating the source process (Add a node) leaves the registration's snapshot unchanged; an instance started from it runs the registered shape |
FR-1 | snapshot/snapshot_test.go (isolation) + thresher/versioning_test.go |
| T-2 | snapshot.New graph nodes are distinct pointers from the model's nodes (isolation); a per-instance Clone of the snapshot still produces a working instance graph |
FR-1 | snapshot/snapshot_test.go |
| T-3 | RegisterProcess returns a handle with Key=process id, Version=1; ID non-empty |
FR-2 | thresher/versioning_test.go |
| T-4 | Re-registering the same key yields Version=2,3,…; both versions independently startable | FR-3 | thresher/versioning_test.go |
| T-5 | A process with no explicit id is a singleton v1 (distinct UUID keys) | FR-3 | thresher/versioning_test.go |
| T-6 | StartProcess(reg) starts the exact version; nil handle → self-identifying error |
FR-4, NFR-1 | thresher/versioning_test.go |
| T-7 | StartVersion(key, n) starts version n; unknown key/version → ObjectNotFound; version<1 → error |
FR-5, NFR-1 | thresher/versioning_test.go |
| T-8 | StartLatest(key) starts the newest version; unknown/empty key → error |
FR-6, NFR-1 | thresher/versioning_test.go |
| T-9 | Registering v2 (auto, message-start) moves the start subscription: a trigger spawns a v2 instance, not v1; a running v1 instance is unaffected | FR-7 | thresher/versioning_internal_test.go |
| T-10 | UnregisterVersion(reg) removes that version; a running instance of it survives; foreign/nil handle → error |
FR-8, NFR-1 | thresher/discovery_test.go (survives+removal) + thresher/instance_starter_internal_test.go (nil/foreign) |
| T-11 | Unregistering the latest auto version promotes the now-newest remaining version to live auto-start — it owns the trigger again (a publish spawns its instance) | FR-8 | thresher/versioning_internal_test.go |
| T-12 | go test -race ./pkg/thresher/... green under concurrent register/start |
NFR-2 | existing + new |
| T-13 | Removing a non-latest version leaves a gap: StartVersion(key,3) still starts v3 and (key,2) errors; re-register yields v4 (no reuse); StartLatest→v3; Registrations(key) reports [1,3]→[1,3,4]; full removal via UnregisterVersion resets to v1 |
FR-3, FR-5, FR-8, FR-10 | thresher/versioning_test.go |
| T-14 | UnregisterProcess(key) removes every version at once (Registrations empty, StartLatest errors); empty/unknown key → error; the counter resets so a later registration is v1 |
FR-11, NFR-1 | thresher/versioning_test.go |
7. Milestones¶
- A1 — Snapshot isolation.
Newclones nodes in its validation pass and wires the graph via the sharedwireClonedGraphhelper (also used byClone), so the snapshot is born isolated in one pass; T-1/T-2. (internal/instance/snapshot/.) - A2 — Handle + versioned registry.
ProcessRegistration;registrationsmap;RegisterProcessreturns the handle and appends a version (drop the no-op); T-3/T-4/T-5. - A3 — Start addressing.
StartProcess(reg),StartVersion,StartLatest, with validation; T-6/T-7/T-8. - A4 — Latest-supersedes + unregister. Supersession on register;
UnregisterProcess(reg)with promote-on-removal; plus the membroker subscription-teardown fix it depends on (a stopped waiter now unsubscribes, else a superseding version's publish is swallowed by the dead subscription); T-9/T-10/T-11. - A5 — Migration. All examples,
pkg/thresher/*_test.go,README*.md,examples/README.md; examples build+run;CHANGELOGmigration note (NFR-5). - A6 — Gapped versions + unregister granularity. Decouple the version number
from the slice index: a per-key monotonic
nextVersioncounter (never reused; reset on full removal),StartVersionscans by number, andRegistrations(key)enumerates the versions (FR-10). Split removal by scope: rename the single-version teardownUnregisterProcess(reg)→UnregisterVersion(reg), and repurposeUnregisterProcess(key)to drop the whole process (FR-11). Hardens FR-3/FR-5/FR-8 against non-latest removal; T-13/T-14. - A7 — Verify & land.
/check-style,/check-srd,make ci, fill §10, flip status. (T-12 throughout.)
8. Cross-doc¶
- Implements ADR-019 v.1 — §2.1 key, §2.2 handle, §2.3 isolation, §2.4 three start modes, §2.5 latest-supersedes, §2.7 (concurrency → SRD-031.B), §2.8 deferrals.
- ADR-009 v.1 — the per-instance clone model FR-1 extends with the missing model→snapshot hop.
- ADR-015 v.1 — the instance-starters FR-7 supersedes across versions; the manual-start mode.
- ADR-016 v.1 — the create-or-join dedup (§4.5) that stays coherent because only the latest auto-starts.
- ADR-013 v.1 — the read-only
InstanceHandlepatternProcessRegistrationmirrors. - Architecture audit 2026-06-11 — §3.3 resolved here (isolation); §2.6 is the sibling SRD-031.B.
- Siblings SRD-006 (per-instance node graph / the
Clonereused), SRD-018 (the handle), SRD-015 (the starters) — number-only sideways refs.
9. Definition of Done¶
- [ ] FR-1..FR-11 wired and covered by T-1..T-14.
- [ ] All examples build and run (
go run) green;examples/README+ bothREADME*.mdmigrated. - [ ]
pkg/thresher/*_test.gomigrated; no reference to the removedStartProcess(id)form remains in current (non-frozen) code/docs. - [ ]
CHANGELOG.mdcarries the breaking-change migration note (NFR-5). - [ ]
/check-styleclean;/check-srdPASS. - [ ]
make cigreen incl. diff-coverage ≥95% on touched files (NFR-4);go test -race ./pkg/thresher/...green (NFR-2). - [ ] §8 cross-doc pins consistent; frozen one-shot SRD/FIX (SRD-015/018/025, FIX-002) not retro-edited.
- [ ] §10 filled; status flipped Draft → Accepted (user's call).
10. Implementation summary¶
Landed on feat/adr-019-definition-versioning (off master).
Milestones & commits
- A1 — snapshot isolation (single-pass New clone + shared wireClonedGraph):
2a67914.
- A2 — handle + versioned registry (ProcessRegistration, registrations
map, RegisterProcess returns the handle): 14583dc.
- A3 — start addressing (StartProcess(reg), StartVersion, StartLatest):
ffaa668.
- A4 — latest-supersedes + UnregisterProcess promote-on-removal: 8f1d4d9;
the membroker subscription-teardown fix it depends on: d9a16c7; coverage
completion: 56e2263.
- A5 — caller/doc migration (examples were already on the new API; the
README.md / README.ru.md / examples/README.md snippets; the CHANGELOG
migration note; this §10): 47b5340.
- A6 — gapped versions + unregister granularity: per-key monotonic
nextVersion counter (never reused; reset on full removal), StartVersion
scans by version number, new Registrations(key) (FR-10); rename
UnregisterProcess(reg) → UnregisterVersion(reg) and repurpose
UnregisterProcess(key) for whole-process removal (FR-11); T-13/T-14: the A6
commit on this branch.
Key files
- internal/instance/snapshot/snapshot.go — New clones nodes in its validation
pass; shared wireClonedGraph (relink/remap/rebind) used by both New and
Clone.
- pkg/thresher/registration.go (new) — ProcessRegistration read-only handle
(Key/Version/ID).
- pkg/thresher/thresher.go — registrations map[string][]*ProcessRegistration
+ per-key nextVersion map[string]int; RegisterProcess (monotonic version +
supersession), StartProcess / StartVersion (scan-by-number) / StartLatest,
UnregisterVersion (single version, promote-on-removal; drops the counter on
full removal) + UnregisterProcess(key) (whole-process removal), latest-only
registerAllStarters.
- pkg/thresher/discovery.go — Starters() latest-only; Registrations(key)
enumerates a key's versions (FR-10).
- pkg/messaging/messagebroker.go + pkg/messaging/membroker/membroker.go —
Subscription.Unsubscribe().
- internal/eventproc/eventhub/waiters/message.go — waiter unsubscribes on stop
(synchronously) and on the goroutine exit paths.
Verification (V-results)
- T-1..T-12 implemented and green (file map in §6).
- make ci green: golangci-lint 0 issues, -race across all modules,
diff-coverage 99.2% of changed lines (min 95% — NFR-4), govulncheck clean.
- All 18 examples/* run green (go run -C <dir> ., exit 0).
Open questions¶
None. Scope: in-memory definition versioning — isolation, handle, versioned registry, three start modes, latest-supersedes, unregister-by-handle, and the caller migration. The registry concurrency hardening (audit §2.6) is the sibling SRD-031.B; the model-editing API is a separate ADR; durable versioning / migration / cross-version correlation / version tags are ADR-019 §2.8 deferrals.