Skip to content

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) errorProcess.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 — validateEventSubShape checks the inner entry shape only, and nothing reads the sub-process's own Incoming(). 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 method value imports, nothing is reported, and it is registration that decides whether the engine has a coordinator for the value. TestTransactionMethodDispositions pins 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 method refusal 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 prefixed calledElement still 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 — compensate accepted, store/image refused, protocol reported — 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 every method value 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 (namesANodeimporter.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, e1three nodes. ts, charge and te are not among them.
  • tx.(*activities.SubProcess).IsTransaction()true; IsEventSubProcess()false.
  • tx.Nodes()ts, charge, te, and tx.Flows()tf1, tf2. The process's own flows are f1, f2 only.
  • proc.LaneSets() → one set, whose single lane holds tx — 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 carries protocol onto 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 ci PASS on the committed branch, judged by .ci/last-run.json. At acceptance the gate is unreliable for reasons outside this stage — internal/instance flakes at about one package run in three (#356) and govulncheck intermittently cannot reach its database — so the evidence taken is go test ./pkg/convert/bpmn/, make lint and make 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/model change (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:

  1. A flow needs no container (M1). flow.Link puts a new flow in its source node's container, so §3's flowSpec.container was never built and §4.1 now records why — the model decides the cross-container case once, instead of a converter field deciding it separately.
  2. isForCompensation was 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 to buildNode, the same funnel SRD-089.D §4.13 moved the dialect report to, and for the same reason.
  3. Two readers of one xsd:boolean (M5). attrBool matched "true" and not "1"; the triggeredByEvent reader added in M2 matched both. For cancelActivity that is an interrupting boundary silently becoming non-interrupting. One reader now, handling both lexical forms.
  4. A refusal's § outlives the element becoming importable (M3). M1 removed subProcess from the sections table 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.