SRD-089.I — BPMN import: the definitional Collaboration¶
| Field | Value |
|---|---|
| Status | Accepted |
| Date | 2026-08-18 |
| Owner | Ruslan Gabitov |
| Implements | ADR-024 v.5 §2.15 (the document-level capability and the Collaboration obligation), §2.9 (the disposition table's "definitional only" row) |
| Upstream | SAD-001 v.1.1 §14 N4 (the Collaboration metamodel is out of execution conformance; inter-process messaging is Message events), ADR-014 v.2 §1 (a MessageFlow connects pools; inside a process the send/receive is tasks and message events), ADR-016 v.1 (correlation, which the two-pool e2e rides) |
| Related | SRD-089.A (the one-assembly parse this stage multiplies), SRD-089.H (the sibling closure stage) |
| Tracking | #284 — Part of; the import half of #335 |
BPMN's §2.3.2 conformance clause obliges an importer to support Process
diagrams "including its definitional Collaboration", and ADR-024 v.5 §2.15
already decided the shape: a document yields a set of processes through
ImportDocument; <collaboration> is consumed, not represented — read
for which participants exist and which process each refers to;
<messageFlow> is reported as dropped, because it is the drawing of an
exchange the engine performs through message events and correlation. This
stage implements that decision. It is the last import gap that is neither
capability-blocked nor standing.
§1 Background (verified)¶
A second <process> is refused today, and the refusal names this work.
parseProcessElem returns unsupported when an assembly already exists,
its comment saying "the document-level capability arrives with the
collaboration slice" (dispatch.go:312-321). The whole parse threads ONE
*assembly: parse() → parseDefinitions → build(p, asm)
(importer.go:328-341), and handleDefinitionsChild keeps the first
non-nil assembly (importer.go:414-434). Zero processes is already an
error (importer.go:405-410).
The seam is ready; the importer under it is not. convert.Result
carries "the definitions the document carried, in document order"
(document.go:38-39) — a slice — and today's ImportDocument always
returns exactly one (importer.go:146-158). ADR-024 v.5 §2.15's operative
sentences (:546-557): Import stays "the process of the document"
and "becomes precise about the ambiguous cases: a document with exactly one
executable process returns it; a document with none, or with several, is an
error naming what it found and pointing at the document-level call" — with
the co-requirement "Nothing existing breaks". And: "<collaboration> is
read for its definitional content only — which participants exist and which
process each refers to — and <messageFlow> is reported as dropped".
isExecutable is never read on import. The attribute exists only in
the exporter (exporter.go:141); the importer ignores it. §2.15's Import
rule needs it (§4.2).
Per-document vs per-process state already splits, mostly right. The
parser owns the document: the catalogs, items, stores, rootDefs, the
one id ledger (ids), exprLanguage, dropped (importer.go:260-). The
assembly owns the process: proc, byID, declared, specs, flows,
refs, datas, dataElems, spec (importer.go:206-257), with the
catalog shared by pointer because a <message> may follow the <process>.
Two pieces of document work sit inside the per-process build:
buildItems (with its reportUnusedImports) and p.reportDataStores() —
run once per call, they would double-report on a two-process document
(§4.3). The document-wide ledger means cross-process id uniqueness is
already enforced for free (SRD-089.F §4.11).
The engine runs multiple processes and messages between them, today.
Each process registers and versions on its own; a message travels by name
plus correlation key through the MessageBroker (msgflow/send.go:29-97,
waiters/message.go:26-36), and a message start event with no incoming
flow auto-instantiates the receiving process (RegisterProcess doc;
instantiating_starts.go:16-30). The two-process worked example is
examples/inter-instance-correlation — no Collaboration object anywhere.
That is SAD-001 §14 N4's model: "inter-process messaging covered by
Message events".
The Collaboration family has no extract coverage and no § pins. The
vendored extract carries no element pages for Collaboration,
Participant or MessageFlow — only the conformance prose (§2.3.2
quote, conformance.md:6; the out-of-scope table row, :165) and the
moddle source for the attribute shapes: Participant.processRef (attr,
ref → Process), MessageFlow.sourceRef/targetRef (attr, ref →
InteractionNode) and messageRef (attr, ref → Message)
(bpmn-moddle.json:1841-1945). The sections table deliberately pins no
§ for the family (dispatch.go:226-238), and the pin test locks the
absence — restoring them is #334's business, not this stage's.
§2 Requirements¶
FR-1 — a document yields its process set. Every <process> parses
into its own assembly over the shared document state; ImportDocument
returns them all, in document order. The one-per-document work — the
items, the unused-import report, the store obligations — runs once (§4.3).
FR-2 — Import becomes precise, breaking nothing. A document with
exactly one process returns it, whatever its isExecutable says (the
compatibility half). A document with several returns the one marked
isExecutable="true" when exactly one is; none or several executable is
an error naming the counts and pointing at ImportDocument (§4.2).
FR-3 — <collaboration> is consumed definitionally. Its
<participant> children parse; a present processRef must resolve
to a declared process (the converter's reference refusals — a dangling
or wrong-kind ref is a broken file), while an absent one is a
black-box pool — the moddle source makes the attribute optional, and a
pool whose process is someone else's system is definitional by nature:
consumed silently, nothing to validate. Ids join the ledger. Nothing is
built: the participants' one definitional fact — which process each
names, when it names one — is validated and consumed, per §2.15.
FR-4 — <messageFlow> is reported, never mapped. One Dropped entry
per flow, under its own id, the reason saying what the engine does
instead (message events and correlation). A messageRef naming a
declared <message> is consumed silently; a dangling one refuses — the
report must not launder a broken reference (§4.4).
FR-5 — cross-process wiring stays impossible, loudly. A sequence flow or association referencing across process boundaries fails exactly as it does today (per-assembly resolution); the ledger already refuses the shared-id confusions.
NFR-1 — no pkg/model change (there is nothing to model — §2.15's
whole point). NFR-2 — diff-coverage ≥95% per committed milestone.
NFR-3 — no fabricated content: no synthetic collaboration, no invented
participant. NFR-4 — the model's and the seam's own messages keep
their voice. NFR-5 — every § the code emits is one the extract
supports (this stage emits none for the family — none exist, #334).
§3 Models¶
// participantSpec is one <participant> as read: its identity and the
// process it names.
type participantSpec struct {
id, name string
processRef string
}
// collabSpec is one <collaboration> as read — its participants and the
// message flows to report. Nothing here is built.
type collabSpec struct {
id, name string
participants []participantSpec
flows []messageFlowSpec
}
// messageFlowSpec is one <messageFlow>: reported under its own id, its
// messageRef consumed against the catalog.
type messageFlowSpec struct {
id, name string
sourceRef, targetRef string
messageRef string
}
The parser gains asms []*assembly (document order) and
collabs []collabSpec; procSpec gains executable bool.
§4 Analysis & decisions¶
§4.1 One parse, N assemblies¶
parseProcessElem stops refusing: each <process> gets its own
assembly, appended to the parser in document order. The defsParser
signature keeps its shape (the returned assembly is appended by the
caller); everything inside a process — nodes, flows, data, lanes — keeps
its per-assembly world untouched, which is what the .A design already
isolated. The parser-level state the processes share (the catalogs, the
items, the ledger, owner/container) shares exactly as the catalog
already does: by living on the parser.
build runs once per assembly. The document-level work hoisted out of it
(§4.3) runs once around the loop. The ledger keeps refusing duplicate ids
across processes — the §4.11 property, now doing document-scale work.
§4.2 Import's selection rule, grounded in both §2.15 sentences¶
§2.15 says both "exactly one executable process returns it" and "nothing
existing breaks". Read together, with the fact that most real
single-process files never set isExecutable:
| Processes | Marked executable | Import returns |
|---|---|---|
| 1 | any | the process — the compatibility half; a one-process document's meaning cannot depend on a flag nobody sets |
| N > 1 | exactly 1 | that process — "the process of the document" is well-defined |
| N > 1 | 0 or several | the §2.15 error: the counts, and "use ImportDocument" by name |
ImportDocument is indifferent to the flag: the set is the set. The flag
parses into procSpec and is not otherwise consumed — gobpm registers
and runs whatever it is handed (SAD-001 §2.3.1 is the library's target),
so a non-executable process still imports; the flag only disambiguates
Import.
§4.3 The document-level work moves out of the per-process build¶
buildItems (and its unused-import report) and reportDataStores run
inside build today — correct for one process, double-reporting for two.
They hoist to the document loop: items built once and handed to each
assembly (they are definitions-level declarations, shared by nature —
each data element still takes its own copy through copyItem, so
per-process isolation is untouched); the store obligations reported once.
data.CreateDefaultStates hoists with them.
§4.4 The messageFlow report tells the truth twice¶
Reporting a <messageFlow> satisfies §2.15's letter; the decisions are
in the details:
- The reason names the mechanism, not just the drop: the engine performs the exchange through message events and correlation keys, so the reader learns the flow is represented differently, not lost — ADR-024 §2.14's "recognized and deliberately not mapped" contract.
- A dangling
messageRefrefuses. The report is for content the converter understands; a reference to a message that does not exist is a broken file, and reporting it as "dropped" would launder the break.sourceRef/targetRefare consumed without resolution: they name interaction nodes across pools — participants included — and validating them would mean modeling the half of Collaboration §2.15 declines. - Ids join the ledger — collaboration, participant, messageFlow — closing the family's last unclaimed declarations.
§4.5 What stays refused¶
<participant> outside a collaboration, <messageFlow> outside one, and
the rest of the family (participantAssociation, conversation kinds)
keep today's refusals, with no § — the extract pins none (#334), and the
pin test's deliberate-absence rows stay exactly as they are.
§4a Worked example¶
<bpmn:definitions xmlns:bpmn="…">
<bpmn:message id="m1" name="order.placed"/>
<bpmn:collaboration id="c1">
<bpmn:participant id="pa1" name="Sales" processRef="P1"/>
<bpmn:participant id="pa2" name="Fulfilment" processRef="P2"/>
<bpmn:messageFlow id="mf1" sourceRef="send1" targetRef="start2"
messageRef="m1"/>
</bpmn:collaboration>
<bpmn:process id="P1" name="Sales" isExecutable="true">…</bpmn:process>
<bpmn:process id="P2" name="Fulfilment">…</bpmn:process>
</bpmn:definitions>
yields: ImportDocument → Result.Processes = [P1, P2], in that order;
res.Dropped carries exactly one entry — mf1/messageFlow, its reason
naming message events and correlation; Import returns P1 (the one
marked executable). A processRef="ghost" on a participant refuses; a
second process marked executable flips Import to the §2.15 error while
ImportDocument is unchanged.
§5 API deltas¶
None. ImportDocument's signature already returns the set; only its
behavior widens to fill it.
§6 Test scenarios¶
| # | Scenario | Asserts |
|---|---|---|
| T-1 | two plain processes | ImportDocument returns both, document order; both register and run |
| T-2 | each process's elements build in its own world | a node id in P1, data in P2 — no bleed; a duplicate id across them still refuses (§4.11) |
| T-3 | one-process document | Import unchanged, flag or no flag |
| T-4 | two processes, one executable | Import returns it |
| T-5 | two processes, none / both executable | the §2.15 error: counts + "ImportDocument" named |
| T-6 | the document-level reports fire once | one store obligation, one unused-import report, for a two-process document (§4.3) |
| T-7 | collaboration with two participants | consumed; ids in the ledger; nothing in Dropped for them |
| T-8 | participant processRef dangling / wrong kind |
the converter's reference refusals |
| T-9 | <messageFlow> |
one Dropped entry under its id, the mechanism named (§4.4) |
| T-10 | messageFlow with a dangling messageRef |
refused, not laundered into the report |
| T-11 | family ids join the ledger | duplicates refuse |
| T-12 | <participant> outside a collaboration |
still refused, no § (§4.5) |
| T-13 | strangers/foreign children inside <collaboration> |
settled/skipped by the standing rules |
| T-14 | e2e: two pools, a message start with a correlation key | the producer process's send instantiates the consumer — one document, two processes, the engine's own mechanism |
§7 Milestones¶
| # | Scope |
|---|---|
| M1 | N assemblies: the parse loop, per-process build, hoisted document work, ImportDocument returns the set (FR-1, §4.3) |
| M2 | Import's selection rule + isExecutable (FR-2, §4.2) |
| M3 | The collaboration family: participants, message flows, reports, refusals (FR-3, FR-4, §4.4, §4.5) |
| M4 | The two-pool e2e, the sweeps, §10 |
§8 Cross-doc¶
Outgoing: ADR-024 v.5, SAD-001 v.1.1, ADR-014 v.2, ADR-016 v.1, siblings SRD-089.A/.H — up or sideways. No ADR bump: this stage implements §2.15 as written.
§9 Definition of Done¶
- FR-1…FR-5 wired and covered by §6.
make ciPASS on the committed branch, judged by.ci/last-run.json.- Diff-coverage ≥95% per committed milestone (NFR-2).
- No
pkg/modelchange (NFR-1). - §10 filled at landing.
§10 Implementation summary¶
Four milestones landed as two commits — the pairs proved inseparable in practice: the selection rule (M2) is ten lines once N assemblies exist, and the e2e (M4) is the natural closing test of the family (M3).
| M | Commit | What landed |
|---|---|---|
| M1+M2 | e715d238 |
N assemblies in document order, per-process build, the document-level work hoisted to run once (§4.3); ImportDocument returns the set; Import's §4.2 selection rule with the counts-and-pointer error |
| M3+M4 | 2e578e2d |
the collaboration family parsed, validated and consumed; message flows reported under the §4.4 mechanism text; black-box pools legal (FR-3 amended); the T-14 two-pool e2e |
FR-3 amended mid-stage, with approval: the draft demanded every
processRef resolve; the standard's black-box pool — a participant
with no processRef, someone else's system — is legal and
definitional by nature. A present ref must still resolve; an absent one
is consumed silently.
NFR-1 held. No pkg/model change: every construct in this stage
is either consumed (collaboration, participant), reported
(messageFlow), or already buildable (the processes themselves).
Empirical finds worth the next reader's minute:
- The exchange needs no collaboration. T-14 runs the two-pool message hand-off — throw on one side, auto-instantiated message start on the other — on a document whose collaboration is entirely consumed. The engine's mechanism (message name + correlation key) carries it; the drawing adds nothing, which is FR-4's premise proved at runtime, not just asserted.
- The consumer is observed through discovery. An auto-instantiated
instance has no handle to wait on — no
StartLatestreturn, no gofunc channel for a pure-imported process.Thresher.Instances(SRD-084) withStage: StageSettledis the observation mechanism, polled until both pools settle. - A message name may not carry
.— the datum derived fromorderPlaced's draft nameorder.placedrefused at build with the model's reserved-character error, clearly attributed. Dots are path separators in scope data names; a BPMN document using dotted message names will see that refusal verbatim.
Open questions¶
None.