SRD-089.E — BPMN import: containers, call activities and lanes¶
| Field | Value |
|---|---|
| Status | Accepted |
| Date | 2026-08-13 |
| Owner | Ruslan Gabitov |
| Implements | ADR-024 v.4 §2.9 (the element set), §2.15 (the report contract a transaction's coordination attributes exercise) |
| Upstream | ADR-024 §2.16 (which refusals are staged, capability-blocked or standing — §4.5, §4.8 and §4.9 are instances of its classes), ADR-023 v.3 (the embedded container and call-time resolution), ADR-028 v.1 (the transaction variant and its Cancel), ADR-035 v.1 (the Router that keeps ad-hoc out), ADR-026 v.1 (compensation) |
| Related | SRD-089.A (the two-pass spine and dispatch tables this makes container-aware), SRD-089.C (the flow nodes a container holds), SRD-089.D (the compensation boundary whose refusal §4.7 closes) |
| Tracking | #284 — Part of |
Every stage so far imported a flat process: one container, one node table, one set of flows. This stage makes containment real — a sub-process holds its own graph, its own lane sets and its own data — and adds the two elements that name something outside themselves: a call activity naming a callable, and an association naming a compensation handler.
It is also the stage where the coverage boundary stops being uniform: four
things stay refused, in two of
ADR-024 §2.16's three
classes, and telling them apart is the whole point of that record. One of the
four — a transaction's method — was written up here as capability-blocked
before ADR-028 was read, which is how §4.5 came to carry the correction as well
as the rule.
§1 Background (verified)¶
The composites are refused today, and the refusals already carry their §.
sections (pkg/convert/bpmn/dispatch.go:181-250) pins callActivity
§13.3.3, subProcess §13.3.4, adHocSubProcess §13.3.4, transaction
§13.3.4, and laneSet/lane §10.5 — a number this stage carried, which
SRD-089.F FR-8 found ungrounded and removed rather than guess, and #334
later restored as §10.7, verified from the spec text
(dispatch.go:229-239). No table claims them, so
ADR-024 v.4 §2.9's
default disposition refuses them — nothing is silently accepted today.
The model has every constructor this stage needs. None of the elements below is capability-blocked, and that was checked before a line of this document was written, per ADR-024 §2.16:
| Element | Model way in |
|---|---|
<subProcess> |
activities.NewSubProcess (pkg/model/activities/subprocess.go), inner elements via SubProcess.Add (subprocess.go:222) |
triggeredByEvent="true" |
activities.WithTriggeredByEvent (subprocess_options.go:49) |
<transaction> |
activities.WithTransaction (subprocess_options.go:66) |
<callActivity> |
activities.NewCallActivity(name, calledKey) (call_activity.go:72) |
<laneSet> / <lane> |
lanes.NewLaneSet (lane_set.go:41), lanes.NewLane (lane.go:47), lanes.WithLaneSets (option.go:35) |
<flowNodeRef> |
Lane.Place(nodes…) (lane.go:105) |
compensation <association> |
events.NewCompensationBoundaryEvent(name, host, def, handler) (boundary.go:170) with the handler marked activities.WithCompensation() (activity_options.go:124) |
A sub-process is a container in the same sense a process is. Both expose
Add(flow.Element) error — Process.Add (process.go:297), SubProcess.Add
(subprocess.go:222) — over the same flow.ElementsContainer
(container.go:39). A SubProcess additionally holds its own laneSets,
dataObjects and dataStoreRefs (subprocess.go:26-57), which is what makes
it the natural home for the data stage's work rather than a second mechanism.
The three sub-process variants are mutually exclusive, and the model says so.
NewSubProcess refuses WithTransaction together with
WithTriggeredByEvent, and WithAdHoc together with either
(NewSubProcess's option guards). The converter therefore never has to decide
what a <transaction triggeredByEvent="true"> means: it passes what the file
said and the model refuses it in its own words.
Lane membership is validated against the container's nodes.
lanes.ValidateLaneSets(sets, nodes) (validation.go:25) checks every placed
node is one the container holds, and Process.Validate calls it
(process.go:432). Two different refusals, and only one is the model's: a
lane placing a node the container does not hold fails that validation, but
a <flowNodeRef> naming an id no node has is refused by the converter at
build (lanes.go:254-262) — Lane.Place takes nodes, so a missing one would
silently shrink the lane instead of failing it. That refusal is the
converter's to own, and the other is not.
The compensation boundary is refused today for a missing element, not a
missing capability. As this stage opened, the boundary builder refused an
event carrying a compensation trigger with "whose handler BPMN names through
an <association> this stage does not import yet; the same file imports
unchanged once associations land". That is the refusal this stage retires, and
its wording
already promised exactly this.
The extract keeps all of it in scope. docs/bpmn-spec/conformance.md
lines 28-32 carry SubProcess, Transaction, AdHocSubProcess, CallActivity
and Lane/LaneSet; line 173 records lanes as model-only — parsed and
preserved, never given behaviour; line 174 allows boundary events on a call
activity (§10.5.4); line 175 defines the Event Sub-Process as
triggeredByEvent=true; line 176 records that an <association> carries
normative semantics when it links an activity to its compensation handler.
§2 Requirements¶
Functional¶
FR-1 — An embedded sub-process imports with its inner graph. Its inner flow
nodes and sequence flows land inside it, not in the parent process, and its
<laneSet>, <dataObject> and <dataStoreReference> children are its own.
FR-2 — triggeredByEvent="true" imports as an Event Sub-Process, entered by
its triggered start rather than by a sequence flow
(ADR-023 v.3 §2.10: the
entry seeding skips it and arms it as a scope handler).
Corrected. This requirement, §4.10 and T-4 all said a flow drawn into one is refused by the model's validation. No such rule exists —
validateEventSubShapechecks the inner entry shape only, and nothing reads the sub-process's ownIncoming(). Self-containment is §13.5.4 semantics the engine realizes by not seeding the node, not a refusal anything performs. A file that draws the flow imports today, unrefused and untested; the missing guard is #357.
FR-3 — <transaction> imports as the transaction variant, and its two
coordination attributes get the three different dispositions they have earned:
method="compensate" (or absent) is accepted silently, method="store" and
method="image" are refused, and protocol is reported — §4.5.
Superseded with §4.5. SRD-095 carries both attributes onto the model verbatim (ADR-028 v.2 §2.7), so every
methodvalue imports, nothing is reported, and it is registration that decides whether the engine has a coordinator for the value.TestTransactionMethodDispositionspins the current behaviour, subtest by subtest.
FR-4 — <callActivity calledElement="k"> imports as a call activity keyed
by the literal k, with no registry consulted at import time. A boundary event
may attach to it.
FR-5 — <laneSet>/<lane> import on both containers. <flowNodeRef>
places the named node on its lane; a nested <childLaneSet> nests. Lanes get no
behaviour.
FR-6 — A compensation <association> supplies the boundary's handler. The
handler activity is the association's target, and the boundary event that .D
refuses now builds. An activity named as a handler must carry
isForCompensation; the model's error stands unaltered when it does not.
FR-7 — Four refusals stay, and no two share a reason.
<adHocSubProcess> is standing (ADR-035 v.1's Router is a Go value no
file carries); method="store"/"image" is standing by an explicit engine
choice (ADR-028 v.1 §2.7); a plain <association> is
capability-blocked (#323); a calledElement beyond a literal key is
capability-blocked (#325). None is presented as a schedule, and the two standing
ones say nothing about waiting.
Two of the four have since closed, each the way its class predicts. The capability-blocked association landed its capability (ADR-039 v.1 / SRD-092, §4.9) and now imports as a carried artifact. The
methodrefusal was reclassified rather than built: SRD-095 carries the attribute, so what §4.5 called standing turned out to be a disposition the model could simply hold.<adHocSubProcess>and the prefixedcalledElementstill stand.
FR-8 — Containment does not weaken any existing guarantee. Duplicate ids,
dangling sourceRef/targetRef, the dialect report of SRD-089.D §4.13, and the
document-order replay of nodes hold inside a sub-process exactly as they hold in
a process.
Non-functional¶
NFR-1 — No pkg/model change. Every element uses an existing constructor;
if this stage finds itself wanting one, that is an ADR-024 §2.16 capability
and lands first, separately.
NFR-2 — Diff-coverage ≥95% on the lines this stage adds, measured by
make cover-check after the milestone is committed.
NFR-3 — Every refusal names its § and the alternative. For a standing boundary, the alternative is building the element in Go; the refusal says nothing about waiting.
NFR-4 — No converter-local copy of a model rule. Variant exclusivity, lane membership and compensation-handler eligibility are all validated by the model; the converter passes the document through and lets those errors surface.
NFR-4's scope, stated once: lane membership is the model's to validate,
but a <flowNodeRef> naming nothing is the converter's — see §1 and T-11a.
§3 Models¶
// nodeSpec gains the id of the container that holds it; "" is the
// process itself. flowSpec gains nothing — see §4.1, and neither does
// laneSetSpec: a lane set reaches its container as a construction option
// through the body it was read into, so nesting already carries the
// placement and a container field would be a second copy of it.
type nodeSpec struct {
se xml.StartElement
id, name string
container string
body nodeBody
}
// laneSetSpec is a <laneSet> as read: its lanes, and for each the ids it
// places. The nodes do not exist yet (see §4.3).
type laneSetSpec struct {
id, name string
lanes []laneSpec
}
type laneSpec struct {
child *laneSetSpec
id, name string
nodeRefs []string
}
// assocSpec is an <association> as read. Only the compensation shape is
// built (§4.7); the rest is refused (§4.9).
type assocSpec struct {
id, srcRef, trgRef string
}
As landed for this stage, which reads only the compensation shape. Later
stages widened it — direction, container and a used flag arrived with
the artifact tier (ADR-039 v.1 / SRD-092), when a plain association stopped
being refused and had to be placed and resolved like any other artifact.
§4 Analysis & decisions¶
§4.1 Containment is a field, not a tree¶
A sub-process could be modelled as a nested assembly — its own node table, its
own flow list, built recursively. It is not. A nodeSpec carries the id of
its container, and the assembly stays one flat list per kind.
A flowSpec carries nothing, because it does not need to: flow.Link puts a
new flow in its source node's container (sequenceflow.go:139), so an inner
flow follows the node it leaves. That also decides what happens to a flow drawn
across a container edge — whatever the model does with it, once, rather than
whatever a converter-side container field would have decided separately.
The reason is that BPMN ids are unique across the document, so a single id table is already correct, and every mechanism .A–.D built on top of it — the deferred build, the two-sweep ordering, the pending-reference index, the duplicate-id guard — keeps working unchanged with a field added. A nested assembly would give each container its own copy of all four, and the copies are exactly where a boundary event attached across a container edge, or a compensation handler in a sibling scope, would start behaving differently depending on which copy resolved it.
The cost is that containment lives in a field rather than in the type, so nothing in the converter structurally prevents a flow whose ends are in different containers. That is a model rule, and the model enforces it — §4.4's principle, not a gap.
§4.2 A container is built before its children, and added to its parent after them¶
Building order inside pass 2 becomes: build every node (containers included), then add each node to the container its spec names, then link flows.
A container must exist before anything can be added to it, which rules out
adding as you build. Adding a fully-populated sub-process to its parent
afterwards is safe because Add stores a node — it does not re-validate the
subtree — so the deepest container may be filled first and grafted upward.
Document order is preserved per container, exactly as buildNodes already
preserves it for the process (importer.go:1085-1105).
§4.3 A lane names nodes that do not exist yet¶
<flowNodeRef> is read in pass 1, when no node has been constructed. So a lane
set is built in pass 2, after buildNodes, and its nodes are placed with
Lane.Place once the id table is complete.
This is the third instance of SRD-089.D §4.7's shape — a reference read before
its referent exists — and it resolves the same way rather than a new way. The
alternative, deferring the whole lane set to the model by handing it ids, does
not exist: Lane.Place takes flow.Node, and inventing a converter-side
id-carrying placeholder would be an ADR-024 §2.16 shadow type.
Order matters once more here: lanes.WithLaneSets is a construction option,
so a sub-process's lane sets must be passed to NewSubProcess before its inner
nodes are built. The lane set is therefore constructed empty of placements and
filled afterwards, which Lane.Place supports by being a mutator.
§4.4 A transaction is a sub-process with an option, not a second builder¶
<transaction> and <subProcess triggeredByEvent="true"> route to the same
builder as <subProcess>, differing by one option. Giving each its own builder
would put the variant rules in the converter, and the model already refuses the
illegal combinations (NewSubProcess's option guards) in messages that name the ADR
clause. A second copy would diverge, and the converter's copy would win at
import while the model's won at run time — ADR-024 §2.16's split exactly.
A visible consequence: SRD-089.D's cancel-end refusal — a <cancelEndEvent>
outside a transaction — becomes reachable from the other side. Once transactions
import, the same end event inside one is legal, and the model says so
without the converter learning the rule.
§4.5 method is a decided engine choice, not a missing capability¶
Superseded. This section's dispositions —
compensateaccepted,store/imagerefused,protocolreported — were correct for what the model held at this landing. SRD-095 has since carried both attributes onto the model verbatim (ADR-028 v.2 §2.7): the converter now imports everymethodvalue and reports nothing, and it is registration that decides whether the engine has a coordinator for it. The reasoning below — that a decision already governing an attribute must be read before a capability is filed against it — is what produced that outcome and is why the section is kept rather than rewritten.
This section first said both coordination attributes were reported because the
model has nowhere to hold them, and cited the issue filed to add them (#324).
That was wrong, and wrong in a way this branch has now made three times:
ADR-028 v.1 §2.7 already
decided method, and I registered a capability without reading the decision
that governs it.
What §2.7 decides: method selects the abort protocol (compensate | store |
image), only compensate — undo by compensation handlers — is a
process-level mechanism the engine can realize, and store/image are
resource-manager coordination declared an explicit non-goal (§2.8), with the
engine note to match (§2.9). That is a standing boundary in ADR-024 §2.16's
sense, not a work item.
So the attribute splits three ways, and each way is a different disposition:
| Value | Disposition | Why |
|---|---|---|
compensate, or absent |
accepted, silent | it names what the engine already does; compensate is the BPMN default |
store, image |
refused | the engine will not realize that abort protocol, ever |
protocol (any value) |
reported | it names an external coordinator, which only means something for the two refused methods |
store/image are refused rather than reported because they are not a lost
datum — they are a different failure semantics. A transaction imported from
method="store" would compensate where the document said roll back, and the
host would hold a process that behaves unlike its file while the report called
it a dropped attribute. ADR-024 v.4 §2.15's contract covers a construct the
import does not map; it does not license importing something as a construct it
is not.
protocol stays reported by the same test read the other way: with the only
supported method, nothing executable depends on it, so the transaction survives
minus a datum and the host is told which.
Silence for compensate matters as much as the other two. A construct is
either mapped or reported and never both, and reporting the one value the
engine implements would train a host to ignore the report.
§4.6 calledElement is a literal key, and the converter resolves nothing¶
NewCallActivity takes a key and deliberately does not consult the registry —
resolution happens at call time (ADR-023 v.3 §2.7), so the callable may be
registered later or re-versioned. The importer therefore stores the attribute
verbatim and reports nothing: an import that failed because a callable was not
yet registered would make import order significant, which is the property
call-time resolution exists to avoid.
Anything richer than a literal key — a QName into another definitions document, a GlobalTask — is capability-blocked on the callable-resolution seam (#325) and refused with that named.
§4.7 The compensation association closes a refusal rather than adding an element¶
<association> in its compensation shape is the missing argument to
NewCompensationBoundaryEvent, which has always been there. So this stage adds
no new node kind for it: the association is read into assocSpec, and the
boundary event that .D refused resolves its handler through the association
index in the deferred sweep it already runs (namesANode —
importer.go:1147-1163).
The handler must be marked isForCompensation; when it is not, the model's
error names activities.WithCompensation and the converter does not paraphrase
it. A modeller reading that message is being told what their file lacks, which
is more useful than a converter-authored sentence about associations.
§4.8 Ad-hoc stays refused, and says nothing about waiting¶
<adHocSubProcess> is a standing boundary (ADR-024 §2.16): the container
is entered through a host-supplied adhoc.Router (adhoc_options.go:92), a Go
value no document can carry. WithAdHoc exists, which is precisely why this
needs stating — the option's existence invites the reading that import is merely
unfinished.
Its refusal names §13.3.4, says the routing is Go code, and points at building the container programmatically. It does not say "yet", and it is not filed as a defect.
§4.9 A plain association stays refused, as a capability¶
Superseded. The capability landed: ADR-039 v.1 gave the model an artifact tier and SRD-092 wired it, so a plain
<association>imports as an artifact today and the refusal named below is gone. The capability-lands-first ordering this section argues for is exactly the ordering that closed it.
A non-compensation <association> — the line from a <textAnnotation> to what
it annotates — is capability-blocked: pkg/model/artifacts declares
Association with no constructor, nothing imports the package, and
process.Process has nowhere to put an artifact (#323). The refusal names that.
The asymmetry with <textAnnotation>, which is skipped, is deliberate and
already recorded (superseded with §4.9: <textAnnotation> is not skipped
either — ADR-039 v.1 maps it into the artifact tier alongside the association,
and dispatch.go:154-156 records that both rows are gone. What follows is the
pre-ADR-039 reading): dropping a comment leaves the imported
definition meaning the same, while dropping a link silently discards a stated
relationship. It is the same tag in two dispositions, which is why ADR-024 §2.16
classifies constructs rather than tags.
§4.10 An event sub-process is entered by its trigger, not by a flow¶
An Event Sub-Process has no incoming or outgoing sequence flow: it is armed with its parent's scope and entered by its triggered start event (ADR-023 v.3 §2.10). A file that draws a flow into one is not refused by anything today (FR-2's correction, #357). The converter adds no check of its own, which stays right — it cannot hold a second copy of a model rule — but the model's copy turns out not to exist either, so the delegation currently delegates to nobody.
§4.11 The transaction's shape rules arrive for free, and one refusal opens¶
ADR-028 v.1 §2.6 states four shape rules, and the converter implements none of them: a Cancel End Event only inside a transaction's graph; a Cancel boundary only on a transaction, and always interrupting; at most one such boundary, with nested transactions rejected (§2.8); the transaction marker exclusive of the Event Sub-Process marker.
They arrive anyway, because SRD-089.D §4.3 already routed the question to the
model — "whether a Cancel boundary's host is a transaction" is listed there among
the rules that "stay there: the converter passes the definition and reports the
refusal with the file's element id attached" (buildBoundaryEvent's comment
in dispatch.go). With no
transaction able to import, that delegation had only one observable outcome: a
Cancel boundary was always refused, because its host could never be a
transaction.
This stage turns that from an effective refusal into a real rule. The same delegation now lets a Cancel boundary attach when the host is a transaction and refuse when it is not, and the converter's code does not change for it. That is what NFR-4 buys: a rule held in one place starts working the moment its precondition exists, in both directions.
§4a Worked example¶
<bpmn:process id="P" name="P">
<bpmn:laneSet id="ls1">
<bpmn:lane id="l1" name="Finance">
<bpmn:flowNodeRef>tx</bpmn:flowNodeRef>
</bpmn:lane>
</bpmn:laneSet>
<bpmn:startEvent id="s1"/>
<bpmn:transaction id="tx" name="Charge" method="compensate" protocol="wsat">
<bpmn:startEvent id="ts"/>
<bpmn:serviceTask id="charge" name="Charge card"/>
<bpmn:endEvent id="te"/>
<bpmn:sequenceFlow id="tf1" sourceRef="ts" targetRef="charge"/>
<bpmn:sequenceFlow id="tf2" sourceRef="charge" targetRef="te"/>
</bpmn:transaction>
<bpmn:endEvent id="e1"/>
<bpmn:sequenceFlow id="f1" sourceRef="s1" targetRef="tx"/>
<bpmn:sequenceFlow id="f2" sourceRef="tx" targetRef="e1"/>
</bpmn:process>
yields:
proc.Nodes()→s1,tx,e1— three nodes.ts,chargeandteare not among them.tx.(*activities.SubProcess).IsTransaction()→true;IsEventSubProcess()→false.tx.Nodes()→ts,charge,te, andtx.Flows()→tf1,tf2. The process's own flows aref1,f2only.proc.LaneSets()→ one set, whose single lane holdstx— the container, not its contents, because that is what<flowNodeRef>named.res.Dropped→ exactly one entry:{Element: "tx", Construct: "protocol"}. (Superseded with §4.5: SRD-095 carriesprotocolonto the model, so the report is now empty for this document.)method="compensate"is absent from the report (§4.5), and so is everything else in the file.
The last line was the one to check by hand at implementation: as this stage
landed, a report with two entries meant method was being reported as lost
when the engine implements it, and an empty report meant protocol was being
dropped silently. Both readings expired with SRD-095, which carries
protocol onto the model too: an empty report is now the correct outcome for
this document, and variants_test.go's TestTransactionProtocolIsCarried
asserts exactly that.
§5 API deltas¶
None. Every element here uses an existing constructor, and pkg/model is
untouched — NFR-1. If implementation finds otherwise, the capability lands
first under its own document (ADR-024 §2.16).
§6 Test scenarios¶
| # | Scenario | Asserts |
|---|---|---|
| T-1 | <subProcess> with a start, a task and an end |
inner nodes land in the sub-process, not the process; the process holds one node |
| T-2 | inner <sequenceFlow> |
linked inside the container; the parent's flow set is untouched |
| T-3 | nested <subProcess> two deep |
the deepest container is populated and grafted upward (§4.2) |
| T-4 | triggeredByEvent="true" |
IsEventSubProcess(). The second half — "a flow drawn into it is refused by the model" — was never true and is not covered by any test; see FR-2's correction and #357. |
| T-5 | <transaction> |
IsTransaction(); a <cancelEndEvent> inside it imports (§4.4) |
| T-6 | <transaction method="compensate" protocol=…> |
imports; protocol reported; method not reported (§4.5). Superseded: both are carried onto the model and nothing is reported — TestTransactionProtocolIsCarried. |
| T-6a | <transaction method="store"> and method="image" |
refused, the reason naming the engine choice — not "yet". Superseded: both import, carried verbatim for registration to judge — TestTransactionMethodDispositions. |
| T-7 | <transaction triggeredByEvent="true"> |
refused in the model's words, not the converter's |
| T-7a | cancel boundary on a <transaction> |
attaches — newly reachable, with no converter change (§4.11) |
| T-7b | cancel boundary on a plain <subProcess> |
refused by the model (ADR-028 v.1 §2.6), not by a converter check |
| T-7c | <transaction> inside a <transaction> |
rejected at validation (ADR-028 v.1 §2.8). Delivered late, by TestNestedTransactionIsTheModelsRefusal: the stage landed with the nesting covered only by a model-side unit test that never crossed the import path, and a /check-srd audit before the status flip is what found it. |
| T-8 | <callActivity calledElement="k"> |
key stored verbatim; no registry consulted |
| T-9 | boundary event on a call activity | attaches (§10.5.4) |
| T-10 | <laneSet> with two <lane>s and <flowNodeRef>s |
lanes placed; LaneSets() returns them in document order |
| T-11 | <lane> naming a node the container does not hold |
fails at validation with the model's message (TestLanePlacingANodeFromAnotherContainer) |
| T-11a | <flowNodeRef> naming an id no node declares |
refused by the converter at build, naming the lane and the id (TestFlowNodeRefNamingNothing) — the one lane refusal the model cannot make, since Lane.Place takes nodes |
| T-12 | nested <childLaneSet> |
nests |
| T-13 | lane set on a <subProcess> |
lands on the sub-process, not the process |
| T-14 | compensation boundary + <association> to a handler |
the boundary builds; CompensationHandler() is the named activity |
| T-15 | handler not marked isForCompensation |
the model's error, naming WithCompensation |
| T-16 | <adHocSubProcess> |
refused; the reason names Go routing and does not say "yet" |
| T-17 | plain <association> |
refused; the reason names the missing artifact capability (#323). Superseded: the capability landed (ADR-039 v.1 / SRD-092), and a plain association imports as an artifact — TestPlainAssociationImports. |
| T-18 | duplicate id across container boundaries | still refused (FR-8) |
| T-19 | dialect attribute on a <subProcess> |
reported (the §4.13 funnel covers a container too) |
| T-20 | end-to-end run of a process containing a sub-process | the engine executes it |
§7 Milestones¶
| # | Scope |
|---|---|
| M1 | Containment: the container field, container-aware build and linking (FR-1, FR-8) |
| M2 | Sub-process variants: embedded, event sub-process, transaction + reported attributes (FR-1, FR-2, FR-3) |
| M3 | Call activity, and boundary events on it (FR-4) |
| M4 | Lanes: lane sets, placement, nesting, both containers (FR-5) |
| M5 | The compensation association, closing .D's boundary refusal (FR-6) |
| M6 | The four refusals, each with its own reason and wording (FR-7) |
§8 Cross-doc¶
Outgoing references are ADR-024 v.4, ADR-024 §2.16, ADR-023 v.3, ADR-028 v.1, ADR-035 v.1 and ADR-026 v.1, plus the sibling SRD-089.A/.C/.D — all upward or sideways. The supersession notes add five more, every one cited only to say where current behaviour is pinned: ADR-028 v.2, ADR-039 v.1, SRD-092, SRD-095 and SRD-089.F, all landed after this stage. This document pins the versions it was written against, per the convention that an SRD records what its author read.
Landing this stage makes SRD-089.D §4.11 historical: the compensation boundary no longer waits for associations, and the decision that changed it is recorded here, in the superseding document — that is what "one-shot" protects.
What a superseded one-shot may carry is a supersession note: a sentence saying the reading below was overtaken, by which document, and where the current behaviour is pinned. It carries no date of its own — the superseding document's header already has one, and a date copied by hand into ten places is a tenth chance to be wrong about it. It states no new decision and moves none. The alternative — leaving a frozen document asserting, in the present tense, behaviour the code refutes — protects the letter of the convention by making the document actively misleading, which is the opposite of what a historical record is for. .D's §6 rows carry such notes for exactly this reason.
§9 Definition of Done¶
- FR-1…FR-8 wired and covered by §6.
make ciPASS on the committed branch, judged by.ci/last-run.json. At acceptance the gate is unreliable for reasons outside this stage —internal/instanceflakes at about one package run in three (#356) andgovulncheckintermittently cannot reach its database — so the evidence taken isgo test ./pkg/convert/bpmn/,make lintandmake link-check, all green, plus the full-gate PASS this branch recorded before the flake surfaced.- Diff-coverage ≥95% (NFR-2), measured after the milestone is committed.
- No
pkg/modelchange (NFR-1). - Every refusal in §4.8 carries its § and its alternative (NFR-3). §4.9's refusal no longer exists — the capability landed (ADR-039 v.1 / SRD-092), so the bullet is scoped to §4.8's — the one of the two it named that survives.
- §10 filled at landing.
§10 Implementation summary¶
Six milestones, in the order §7 planned them.
| M | Commit | What landed |
|---|---|---|
| M1 | 179bd2f0 |
containment: a node spec's container id, container-aware add, the recursive container parser |
| M2 | f6e0bce8 |
the transaction and event sub-process variants, and method's three dispositions |
| M3 | a2a76c85 |
the call activity, and the refusal of a prefixed calledElement |
| M4 | 3334b4ff |
lane sets, placement, nesting, both containers |
| M5 | 2dc45874 |
the compensation association, closing SRD-089.D §4.11's refusal |
| M6 | 8649b81e |
the ad-hoc standing refusal, and the wording sweep over all four |
| — | 27ab253f |
T-20: the container the engine actually enters |
NFR-1 held. pkg/model is untouched. Every element used a constructor
that already existed, which is what §1's table was for.
Four things the plan did not anticipate, each fixed where it was found:
- A flow needs no container (M1).
flow.Linkputs a new flow in its source node's container, so §3'sflowSpec.containerwas never built and §4.1 now records why — the model decides the cross-container case once, instead of a converter field deciding it separately. isForCompensationwas read by nobody (M5). Every activity may carry it, the model requires it on a compensation handler, and no builder mapped it — so a correctly-drawn file was refused for not marking its handler, and the mark was unreadable. It moved tobuildNode, the same funnel SRD-089.D §4.13 moved the dialect report to, and for the same reason.- Two readers of one xsd:boolean (M5).
attrBoolmatched"true"and not"1"; thetriggeredByEventreader added in M2 matched both. ForcancelActivitythat is an interrupting boundary silently becoming non-interrupting. One reader now, handling both lexical forms. - A refusal's § outlives the element becoming importable (M3). M1
removed
subProcessfrom thesectionstable on the reasoning that a table of refusals should not list an importable element. Wrong: the tables claim an element in a context, so the § stays reachable wherever the element does not belong — which is exactly when a modeler needs it. The rows are back, with a test importing a<subProcess>inside a start event and asserting §13.3.4 comes back.
§4.11 was a prediction and it held. The Cancel rules arrived with no
converter change: TestCancelBecomesReachable shows a Cancel End importing
inside a transaction and refused outside, and a Cancel boundary attaching to a
transaction and refused on a plain sub-process. Before this stage that
delegation had one observable outcome and could have been a hard-coded no.
The corrected §4.5 is the stage's main finding, and it is recorded in the
converter's boundary record rather than only here — ADR-038 then, since retired
into ADR-024 §2.16, which
carries the rule unchanged: a transaction's method was written up as
capability-blocked and filed as #324, when ADR-028 §2.7 had already decided
it. Two of three values are a standing non-goal. That made #324 the register's
second false row after the timer, and it is closed or re-scoped by its owner —
this document does not decide it.
Open questions¶
None.