SRD-049 — Embedded Sub-Process: nested scopes in the instance¶
| Field | Value |
|---|---|
| Status | Accepted |
| Version | v.1 |
| Date | 2026-07-16 |
| Owner | Ruslan Gabitov |
| Implements | ADR-023 v.1 §2.1–§2.6, §2.9 (the embedded slice; Call Activity is the next slice); GitHub epic #85 (part) |
| Upstream | ADR-001 v.6 (the single-writer loop extended with scope accounting), ADR-010 v.2 §2.2 (the container-scope tree activated), ADR-018 v.1 (the interruption mechanism applied to composite hosts), ADR-006 v.3 §2.6 (the error scope-chain realized), ADR-013 v.2 (the fact stream extended with a scope kind) |
| Refines | — |
§1 Background¶
ADR-023 v.1 decides composition on one concept — the execution scope as a tree inside the instance — and slices the landing: the embedded Sub-Process first. The code today is flat in exactly three places, each with a seam already waiting:
- Model: only type constants exist (
flow.SubProcessActivity,pkg/model/flow/activity.go:11-13);flow.Containeris an interface whose doc names Sub-Process as a container kind (pkg/model/flow/element.go:66-87), withProcessits only implementation.SequenceFlow.Validatealready enforces the same-container rule (sequenceflow.go:257-287) — nested graphs validate for free once a second container exists.Process.Validateruns a per-nodeValidate()hook (process/process.go:245-254) — a SubProcess implementing it gets recursive validation with zero changes to Process. - Snapshot:
snapshot.Newwalks one flat node map (snapshot/snapshot.go:44-169) — cloning, wiring, the completeness check,discoverInstantiatingStartsandhasConditionalsall assume no nesting;Snapshot.Clone/wireClonedGraphrebind boundaries/defaults on the flat graph only. - Runtime: the loop's registries are scope-blind (
loop.go:19-71), completion isactive == 0,Terminatestops the whole instance (end.go:141-151→evTerminate→stopAll), the error catch is single-scope (applyFailed/matchErrorBoundary), and every frame opens at the plane root (internal/instance/scope.go:80-84). Meanwhile the data plane is already a tree:scope.ScopehasOpenScope/CloseScopeand parent-ward resolution (internal/scope/scope.go:273-305, 357-393),Framecarries its container path (frame.goat), andInstance.Newreserves aparentRootparameter (instance.go:100-107).
This SRD lands the embedded Sub-Process end-to-end on those seams: the container-node model with the ADR-023 §2.3 validated shapes, the recursive snapshot, the scope-aware loop (open/seed/park/drain/close), scope-cancel (boundary-on-composite + the scoped Terminate), and the error scope-chain.
§2 Requirements¶
Functional — model layer¶
- FR-1 —
flowcontainer concern in one file (newpkg/model/flow/container.go): the existingflow.Containerinterface moves here fromelement.go(a pure move — no API change;element.gokeepsElement/ElementType/BaseElement), joined by its first embeddable implementationElementsContainer— the graph-holding core (nodes/flowsmaps,Add/Remove/Elements,Nodes()/Flows()accessors, avalidateFlowshelper mirroring the Process endpoint checks atprocess.go:220-239) — and the FR-4 shared clone/wiring helper. The BPMN name (ElementsContainer, §8.x) is the type name.Processmigration onto the core is out of scope (a recorded backlog item — no churn in this change-set). - FR-2 —
activities.SubProcess(newpkg/model/activities/subprocess.go): embeds theactivitybase (boundary events, default flow, properties, roles —activity.go:20-32) andflow.ElementsContainer.ActivityType()returnsflow.SubProcessActivity. ConstructorNewSubProcess(name string, opts ...options.Option); inner elements are added via the container'sAdd(the process-building idiom unchanged: build nodes,sp.Add(...),flow.Link(...)— the same-container rule confines inner flows). - FR-3 — Shape validation (
SubProcess.Validate(), invoked by the existing per-node hook inProcess.Validateand recursively by an enclosing SubProcess): realizes ADR-023 §2.3 — - flow-endpoint membership for the inner graph (FR-1's helper);
- exactly one None Start Event XOR no start event with ≥1 flow-less activity/gateway; a triggered start, a mixed shape, multiple starts, or an empty container → classified errors naming the sub-process and the offending node;
- inner per-node
Validate()hooks run (recursion: a nested SubProcess validates its own body); - boundary events of INNER activities must be inner nodes; the
completeness rule ("an EndEvent requires a StartEvent",
snapshot.go:137-145) applies per container. - FR-4 —
SubProcess.Clone(): deep-clones the inner graph — every inner node via itsClone()(recursion for nested sub-processes), inner flows relinked between the clones, inner gateway default flows remapped, inner boundary events rebound to their cloned hosts — thewireClonedGraphtriad (snapshot.go:285-371) reproduced for the container's own graph via a shared exported helper (flowpackage, beside FR-1) that snapshot's wiring delegates to for the top level too (one wiring implementation, two callers). The activity base's config cloning stays as today (activity.go:105-128); the host's own boundary events stay cleared for the snapshot rebind.
Functional — snapshot layer¶
- FR-5 — Recursive sweeps.
snapshot.Newtreats a SubProcess node as one node of the parent graph (cloned via FR-4 like any node), and the static precomputes go deep:hasConditionalsanddiscoverInstantiatingStartswalk into containers (a newwalkNodesDeepiterator overflow.Containernodes);HasConditionalsis true for a conditional catch/boundary/EBG-arm at ANY depth. Instantiating starts remain top-level only (an inner start is never an instantiation point — ADR-023 §2.3: the parent's token is the trigger); the completeness check stays per container (FR-3). - FR-6 — Clone parity.
Snapshot.Cloneneeds no new code for nesting (FR-4 makes the SubProcess node self-cloning), but the parity is pinned by tests: a cloned instance's inner graph is disjoint from the definition's and from sibling instances' (the FIX-016/SRD-048 Clone-drop bug class).
Functional — runtime layer¶
- FR-7 — Tracks carry a scope path.
trackgainsscopePath scope.DataPath(construction-immutable, thecondDefsprecedent): root tracks get the instance root; tracks seeded inside a sub-process get the child path. Frames open at the track's scope path —instanceScope.openFramegains the path parameter (internal/instance/scope.go:80-84;scope.NewFramealready acceptsat) — so data resolution walks child → parent per §10.5.7, and a frame commit lands in the child scope. The loop-side conditional evaluation (evalCondition) opens its transient frame at the OWNING track's scope path. - FR-8 — The composite parks the host and opens the scope. The track
classifies a SubProcess node as a wait node (the
checkNodeTypetaxonomy): it parks onevtCh(TrackWaitForEvent) and emits a newtrackEventkindevScopeOpen(host track + node). The loop: - derives the child path — the host's scope path + the node's ID-derived segment (stable across clones; names are not unique);
plane.OpenScope(child)— a failure faults the instance (thearmBoundariesfailure idiom);- registers a
scopeEntryin the loop-owned registry (FR-9); - seeds the inner tracks per the validated shape — the none-start
node, or every flow-less inner activity/gateway (the
createTracksfilter,instance.go:265-319, applied to the container's nodes) — each spawned with the child scope path via the existing fork-spawn machinery (no loop-goroutine emits: the SRD-048atConstructionrule); - reports the scope fact (FR-13).
A SubProcess with an armed boundary set arms exactly as today — the
host track occupies the composite node, so
armBoundariessees it viaboundaryHosterunchanged. - FR-9 — Loop scope registry & drain accounting.
loopStategainsscopes map[scope.DataPath]*scopeEntry—{host *track, node flow.Node, parent scope.DataPath, active int}. Spawn increments the track's OWN scope's counter; a terminal event (evEnded/evFailed/evAwaiting) decrements it. When a scope's counter reaches zero (§13.3.4 drain — awaiting/parked tracks count as active, so "no tokens remain" is exact): plane.CloseScope(child)(child data disposed — §10.5.7);- the entry is removed, the scope fact reported;
- the host resumes: the loop delivers a synthetic completion to the
host's
evtCh(the job/task completion idiom,jobs.go/tasks.go) — the host continues to its outgoing selection (conditional/default flows per the activity rules). The instance's own completion is untouched:active == 0on the loop remains the global exit; the root needs noscopeEntry. - FR-10 — Scope-cancel. A new loop helper
cancelScope(path): stop + cancel every live track whosescopePathhaspathas a prefix (cooperative cancellation —t.stop(),t.cancel(), close of parked channels per thestopAllidiom,loop.go:182-205— scoped instead of global), close the affected scopes (children first), drop their entries, and clear the scoped entries from the loop registries (waiting/msgIdx/conds/watchers of inner tracks). Consumers: - an interrupting boundary on the composite (
fireBoundary,boundary_watch.go:167-195): before cancelling the host track, cancel its opened scope — then the existing host-cancel + exception flow continuation runs unchanged; - the scoped Terminate (FR-11);
- the error chain (FR-12).
- FR-11 — Scoped Terminate.
execEnvcarries the executing track's scope path;Terminate()(therenvsurface,execenv.go) routes it: the root path keeps today's semantics (evTerminate→stopAll,end.go:141-151unchanged in behavior); a nested path emits a new kindevScopeTerminate— the loop cancels that scope (FR-10), closes it, and resumes the host through the normal drain path (the composite completes; the parent continues — §13.5.6). Terminate still wins over co-located triggers and runs no other end behavior. - FR-12 — Error scope-chain.
applyFailed(loop.go:503-512) extends: after the failing activity's own boundary miss (matchErrorBoundaryunchanged), walk the scope chain — for each enclosingscopeEntry(by the failing track's scope path, innermost first): match an Error boundary on that entry's composite node (errorRefcode match, thematchErrorBoundarymatching reused); a match cancels that scope (FR-10) and routes the boundary's exception flow in the host's parent; no match up to the root →failFromTrack(the instance fault, unchanged). An Error End Event inside a sub-process stops faulting the instance directly: itsBpmnErrorenters the same walk starting at the enclosing composite (the end-in-error case narrows to the root scope, ADR-023 §2.6). - FR-13 — Observability. New kind
KindScope("scope lifecycle") with phasesOpened/Completed/Terminated/Canceled,NodeID/NodeName= the composite,Details[scope_path](+ the ADR-023 §6 recommendation).NodeProgresson the composite host and the inner nodes' facts flow as today.
Functional — front door¶
- FR-14 — runnable example
examples/embedded-subprocess/(split by concern per the >80-line rule; binary gitignored; smoke-run exits 0); guidedocs/guides/gains a composition page (or the section slots into a newcomposition.md— decided at M-final with the README);CHANGELOG.md [Unreleased];docs/design/conformance-status.md: theSubProcess (embedded)row moves to §1 (same-PR tracker rule) — the epic #85 row stays open for the Call Activity slice;README.md+README.ru.md+examples/README.mdsync.
Non-functional¶
- NFR-1 — Flat processes pay nothing. No SubProcess in the model → no
scope entries, no
evScopeOpen/evScopeTerminatetraffic, frames open at the root as today; the only constant cost is thescopePathfield equality checks on terminal accounting. - NFR-2 — Single-writer preserved. All scope state (
scopes, drain counters) isloopState-owned, mutated on the loop goroutine only; scope open/close of the data plane happens from the loop (the plane's own mutex serializes against executing tracks' commits). - NFR-3 — The wiring is written once. The graph-clone/rewire helper (FR-4) is shared between the snapshot top level and the container — divergence between the two is the bug class to prevent.
- NFR-4 — Standard fidelity. §13.3.4 shapes and drain, §10.5.7 visibility/disposal, §13.5.6 scoped Terminate, §10.5.1 innermost-catcher — each realized as specified; every deviation is already registered in ADR-023 (the boundary-crossing rejection).
- NFR-5 — Coverage. Touched files 100% (min 80%); diff-coverage gate
≥95%;
make cigreen per milestone.
§3 Models¶
§3.1 flow.ElementsContainer (new)¶
// ElementsContainer is the embeddable flow.Container implementation — the
// graph-holding core a Process-like owner composes: id-keyed nodes and
// sequence flows, Add/Remove dispatch by element type, and the
// flow-endpoint validation shared by every container level (ADR-023 §2.2).
type ElementsContainer struct {
nodes map[string]Node
flows map[string]*SequenceFlow
}
The core carries no element identity — the embedding owner (a
Process, a SubProcess) provides foundation.BaseObject, avoiding
ambiguous identity selectors when SubProcess embeds both the activity
base and the core; together the owner and the core satisfy
flow.Container. Add delegates as AddElement(owner, e) so elements
bind to the OWNER and the same-container rule confines nested graphs.
Add/Remove/Elements mirror Process.Add (process.go:175-209,
308-330); ValidateFlows() mirrors the endpoint loop
(process.go:220-239). A CloneInto helper reproduces the
clone-relink-remap-rebind wiring (wireClonedGraph) for any container
graph (FR-4/NFR-3).
§3.2 activities.SubProcess (new)¶
// SubProcess is the embedded Sub-Process (ADR-023 §2.2): an activity in
// its parent's graph AND the container of its own inner graph. Its inner
// flow is seeded when the host token arrives (the §2.3 validated shapes);
// it completes when its scope drains (§13.3.4).
type SubProcess struct {
flow.ElementsContainer
activity // boundary events, default flow, properties, roles
}
SubProcess lives in pkg/model/activities beside the unexported
activity base, so the embed is legal (same-package); it composes the
container from flow with the activity surface the boundary machinery and
outgoing-selection already consume.
Validate() — FR-3 (the shape table). Clone() — FR-4. Exec is never
reached: the track parks the composite before node execution (FR-8), the
same pattern as the UserTask/worker parks.
§3.3 Runtime deltas¶
track:scopePath scope.DataPath(construction-immutable).trackEvent: kindsevScopeOpen(host parks on a composite; ev.node = the SubProcess) andevScopeTerminate(ev carries the scope path);+trackEventKindNamesrows.loopState:scopes map[scope.DataPath]*scopeEntry;
// scopeEntry is one open nested scope, loop-owned (ADR-023 §2.4): the
// parked host resumes when active drains to zero; cancel stops the
// subtree (FR-10).
type scopeEntry struct {
host *track
node flow.Node
parent scope.DataPath
active int
}
instanceScope.openFrame(trackID, nodeID string, at scope.DataPath)— the path threaded from the track (root for today's callers).execEnv: carries the track's scope path for the scopedTerminate.observability:KindScope+PhaseOpened(a NEW phase constant — distinct from the engine-lifecyclePhaseStarted; the scope's other phases reuse the existingCompleted/Terminated/Canceledvocabulary) +AttrScopePath.
§4 Analysis¶
§4.1 Why the host parks on evtCh (not parkCh, not a new channel)¶
The composite is a wait-for-completion node — the same shape as a UserTask
(a human completes it) and a worker ServiceTask (a report completes it),
both of which park on evtCh and resume on a synthetic completion the
loop sends (tasks.go:223, jobs.go:82). The scope drain is one more
completion source; reusing the park keeps the deferred-choice flip,
stop-teardown (stopAll closes evtCh), and the §3.7 discard checkpoint
working for composites with zero new synchronization. parkCh is the
join-suspension channel with different fate semantics (survivor/merged) —
wrong contract.
§4.2 Why seeding reuses the fork-spawn path¶
Inner tracks are ordinary tracks: spawning them through spawnForks-style
loop-side construction (with atConstruction classification — the SRD-048
deadlock rule) gives boundary arming, born-parked recording, position
seeding and per-scope accounting in one place. The alternative — the host
track spawning inner goroutines itself — would put track construction on
two goroutines and break the loop's single-writer ownership of the
registries.
§4.3 Scope identity: ID-derived segments¶
The child path segment derives from the composite node's ID (stable,
unique, clone-independent), not its name (DataPath segments must be
unique under one parent; names aren't). The pretty name travels in the
scope fact's details for operators. Nested instances of the same
definition on parallel branches (two tokens entering the same composite —
the implicit-exclusive-merge case, §13.3.1) are serialized per §4.4.
§4.4 Re-entry: one open scope per composite node¶
If a second token reaches a composite whose scope is open (a parallel
sibling arrived), the second host queues: the loop parks it as waiting
and defers its evScopeOpen until the first scope closes (the entry
records the queue). Rationale: one DataPath cannot hold two concurrent
scopes; the standard routes concurrent re-entry through multi-instance
(#88 — each MI instance gets its own scope, keyed additionally by the
instance index, per ADR-023 §2.8). Sequential re-entry (the common
loop-back case) reopens a fresh scope — data does not survive across
activations (DataObject lifecycle is per activation, §10.5.7).
§4.5 Drain correctness¶
The per-scope counter counts live tracks (spawned minus terminal),
identical to the global active discipline — parked and awaiting tracks
keep the scope open ("no tokens remain … no nested Activity still
active", §13.3.4). Because all mutation happens in apply on the loop
goroutine, drain detection is a zero-race comparison at the decrement
sites; no polling, no second accounting authority.
§4.6 Rejected shapes¶
- A goroutine per scope (a mini-loop per sub-process): re-introduces cross-goroutine lifecycle state for completion/cancel — the exact class ADR-017 removed. Rejected.
- Flattening at snapshot build (inline the inner graph, prefix ids): loses drain semantics, scoped cancel/terminate, per-scope data disposal and the error-chain match points (ADR-023 §4). Rejected.
- Name-derived scope segments: collide for duplicate names; IDs are the invariant. Rejected (§4.3).
§4.7 Unbounded recursion¶
ADR-023 §2.9 keeps nesting depth unbounded by design. This SRD enables it
implicitly — SubProcess.Clone() (FR-4) and walkNodesDeep (FR-5) recurse
naturally, and the loop's scope registry nests by path with no depth
assumption. No static cycle check exists (none is possible for the embedded
form — containment is acyclic by construction: a container physically holds
its children). The operational depth guard of ADR-023 §6 targets the Call
Activity slice (where reference-recursion lives) and is out of this slice's
scope.
§5 API surface¶
Public: flow.ElementsContainer (+ the shared clone/wiring helper),
activities.NewSubProcess/SubProcess (Container + ActivityNode +
boundaryHoster), observability.KindScope/AttrScopePath. Everything
else is internal/instance machinery. Process is untouched (its
migration onto ElementsContainer is backlog).
§6 Test scenarios¶
Model (M1–M2): TestSubProcessShapes (the FR-3 table: one-none-start ok;
flow-less-seeding ok; triggered/mixed/multiple/empty rejected; nested
recursion validates), TestSubProcessContainment (inner flows confined;
cross-boundary link rejected by the same-container rule),
TestSubProcessClone (disjoint inner graphs; inner defaults + inner
boundary rebinds pinned — the Clone-drop class),
TestElementsContainerAddRemove.
Snapshot (M3): TestSnapshotNested (a nested definition snapshots +
clones; inner nodes disjoint per instance), TestHasConditionalsDeep (a
conditional catch inside a sub-process flips the flag),
TestInstantiatingStartsTopLevelOnly, per-container completeness.
Runtime (M4–M5, internal/instance): TestScopeOpenSeedsNoneStart and
TestScopeOpenSeedsFlowlessNodes (each shape seeds; host parks; scope
fact),
TestScopeDrainResumesHost (inner completion → close → host resumes to
outgoing), TestScopeDataVisibility (inner frame reads parent data via
walk-up; inner commits land in the child scope and are disposed at
close), TestScopeReEntryQueues (§4.4), TestNestedScopes (two levels
drain inside-out), TestBoundaryOnCompositeInterrupts (inner tracks
cancelled; exception flow runs), TestScopedTerminate (inner Terminate
kills only its scope; parent continues; root Terminate unchanged),
TestErrorScopeChain (inner failure → composite's Error boundary catches;
no match → instance fault; Error End inside → caught by enclosing
composite), TestConditionalInsideScope (a conditional catch in a
sub-process evaluates at its scope path).
E2E (M6, pkg/thresher): TestEmbeddedSubProcessE2E — a process with a
nested two-level sub-process, data written inside visible to the parent
only through committed outputs, a boundary timeout on the composite, and
the drain-completion path; plus the example smoke.
§7 Milestones¶
| # | Scope |
|---|---|
| M1 | flow.ElementsContainer + SubProcess model + shape validation (FR-1/2/3) + tests |
| M2 | SubProcess.Clone + the shared wiring helper (FR-4, NFR-3) + clone-parity tests |
| M3 | Snapshot recursion: deep sweeps + parity pins (FR-5/6) + tests |
| M4 | Runtime core: scopePath, evScopeOpen, the scope registry, open/seed/park/drain/close/resume, frames-at-path, scoped conditional eval (FR-7/8/9, part of FR-13) + tests |
| M5 | Scope-cancel: boundary-on-composite, evScopeTerminate, the error scope-chain (FR-10/11/12) + tests |
| M6 | Thresher e2e + example + changelog + tracker row + README/guide sync (FR-14) |
Post-M6: /check-srd, §10 fill, status flip (ADR-023 stays Draft until
the Call Activity slice lands — the workstream decision), linked-docs
sync, PR handover.
§8 Cross-doc¶
- Implements ADR-023 v.1 §2.1–§2.6/§2.9 (traced per FR above).
- Extends ADR-001 v.6 accounting;
applies ADR-018 v.1
to composite hosts; realizes
ADR-006 v.3 §2.6;
activates ADR-010 v.2 §2.2
child scopes; extends ADR-013 v.2
with
KindScope.
§9 Definition of Done¶
- [ ] All FR/NFR wired and traced to §6 tests.
- [ ]
make cigreen per milestone (lint 0, race, diff-coverage ≥95%, vuln 0); touched files 100% (min 80%). - [ ] Example runs to completion (exit 0), binary gitignored.
- [ ] Conformance tracker: the
SubProcess (embedded)row flips in this PR; #85 remains open for the Call Activity slice. - [ ] Changelog
[Unreleased]before the PR description. - [ ]
/check-srdPASS; §10 filled; linked docs synced.
§10 Implementation summary¶
§10.1 Milestones by commit (branch feat/sub-processes)¶
| Stage | Commit | Scope |
|---|---|---|
| doc | 29f4b28 |
SRD-049 (after ADR-023 dc02515 and the extract appendix) |
| M1 | a2d88f8 |
flow/container.go (interface move + ElementsContainer) + SubProcess + shape validation |
| M2 | e7d63c9 |
WireClonedGraph moved to flow (snapshot delegates) + CloneGraph + SubProcess.Clone |
| M3 | 895ec13 |
walkNodesDeep + deep hasConditionals; instantiating starts pinned top-level |
| M4 | 38e93f5 |
scopePath, frames-at-path, evScopeOpen, the scope registry, open/seed/park/drain/close/resume, KindScope |
| M5+fix | d2f2c9c |
cancelScope, evScopeTerminate, the error scope-chain — plus the folded conditional lost-wake-up FIX (below) |
| M6 | d10482b |
thresher e2e, examples/embedded-subprocess/, docs/guides/composition.md, changelog, tracker, READMEs |
Every milestone landed make ci green; the branch-final diff-coverage:
96.9% of 715 changed lines (min 95%).
§10.2 Deltas vs the draft¶
- §3.2 said "Exec is never reached" — wrong. The host resumes via the
synthetic completion, and the composite node then executes normally:
SubProcess.Execruns the standard outgoing selection, andProcessEventaccepts the completion delivery (the UserTask post- completion shape). Both landed in M4. SubProcess.Node()override added (the ServiceTask convention): without it, flow targets unwrap to the embedded activity base — stripping the executor/container capabilities. Found by the first failing M4 run; the concrete-type override is now part of the composite's required surface.- FR-1 landed as the concern file (owner direction during review):
container.goholds the movedContainerinterface, the core, and the shared wiring helper together; the §3.1 identity-embed correction (the core carries noBaseElement) landed with M1. clearCondskeyed by trackID (the disarm sites carry the id), andapply's wait/deliver plane extracted intoapplyWaitPlane(gocyclo).- Fixes found on sight (M4):
unregisterEventskips Conditional definitions (never hub-registered — SRD-048 symmetry);KindScopejoined the observability echo-level table (an absent kind echoes at Error);executeNode's NodeExecutor error names the node. - §6's
TestScopeDrainResumesHostlanded folded intoTestScopeOpenSeedsNoneStart/...FlowlessNodes(the drain-resume assert is their outer-task check).
§10.3 Empirical findings¶
- The conditional lost-wake-up flake (fixed in M5, pre-existing on
master from the SRD-048 merge). A track walking onto a conditional
catch as its continuation node armed the subscription (
evWaiting) and then itsevMoved-driven boundary disarm tore the fresh watch down (disarmBoundaries' unconditionalclearConds); the later commit swept an empty registry — a silently lost wake-up, the instance hung Active (~1 in 4-8 under race; fork-born catches unaffected, making the per-instance clone's flow-map iteration order the flake selector). Root-caused from a recorded fact/log trail (the Registered fact's stale node proved arm-before-move). Fix: the disarm is boundary-flavor-scoped (clearCondBoundaries); the arm also uses the wait node carried on the emit for correct fact attribution. Pinned deterministically byTestConditionalSurvivesMove; the e2e passes ×25 under race. - The M1
make cired was a process error, not a code error: the gate ran against a tree still being edited (the recorded concurrent-ci pitfall) — re-run clean on the frozen commit.
§10.4 Backlog (out of SRD-049 scope)¶
Processmigration ontoflow.ElementsContainer— the recorded FR-1 deferral (no churn in this change-set).- The Call Activity slice (ADR-023 §2.7) — the next SRD; #85 remains open for it.
- The fork-born Message-catch synchronous-fire corner (SRD-048 §10.4) remains queued.
Open questions¶
None.