SRD-051 — BPMN 2.0 converter: import & export of the MVP subset¶
| Field | Value |
|---|---|
| Status | Accepted |
| Version | v.2 |
| Date | 2026-07-30 |
| Owner | Ruslan Gabitov |
| Implements | ADR-024 v.1 §2.1–§2.8 (the converter seam + the first BPMN slice) |
| Upstream | ADR-002 v.2 (extension interfaces live in pkg/; the seam follows it), ADR-019 v.1 (version key = process id; the id-preservation constraint), SAD-001 v.1 §4 N7 / §5 / §9 (parser outside the engine; modeler feedback; module layout), docs/bpmn-spec/ (the BPMN 2.0 KB) |
| Refines | — |
§1 Background¶
gobpm builds a process definition exactly one way today — programmatically,
via process.New + Add + flow.Link (pkg/model/process/process.go:47,175,
pkg/model/flow/sequenceflow.go:62). There is no serialization of any kind:
a whole-repo search finds no encoding/xml, no Marshal/UnmarshalXML, no
json: tags on any model type. The engine "accepts pre-built models"
(SAD-001 v.1 §4 N7) and nothing
else — which blocks the BPMN modeler persona
(SAD-001 v.1 §5) who authors
.bpmn XML to be executed.
ADR-024 v.1 decides the
answer: a format-agnostic converter seam in core (pkg/convert —
Importer/Exporter + a register-by-format-key registry), and a first
batteries-included BPMN 2.0 XML converter. This SRD lands slice 1: the seam, plus BPMN import and
export of the executable-core MVP subset, engine-independent, with a semantic
round-trip and unsupported-element feedback.
The seams this lands on already exist:
- The model target — every MVP node has an exported constructor
(
events.NewStartEvent/NewEndEventpkg/model/events/{start,end}.go:54,48;activities.NewManualTask/NewUserTaskactivities/{manual_task,user_task}.go:24,88;gateways.NewExclusiveGateway/NewParallelGatewaygateways/{exclusive,parallel}.go:26,31;flow.Linkflow/sequenceflow.go:62). Identity is set withfoundation.WithID(foundation/options.go:36). - The read-back for export —
Process.Nodes()/Flows()/Name()(process/process.go:120,161,95);SequenceFlow.Source()/Target()/Condition()(flow/sequenceflow.go:299,304,309);Gateway.Direction()/DefaultFlow()(gateways/gateway.go:213,165);BaseElement.ID()/Name()(foundation/base.go:138,flow/element.go:109); node discrimination viaNodeType()/EType()(flow/node.go:193,210). - The registration consumer — an import feeds
thresher.RegisterProcess(thresher/thresher.go:642), which keys the version on the process id (ADR-019).
§2 Requirements¶
Functional — the seam (pkg/convert, core)¶
- §FR-1 — Format & interfaces. A
Formatstring type with aBPMNconstant; splitImporter/Exporterinterfaces overio.Reader/io.Writer, producing/consuming*process.Process. - §FR-2 — Registry.
RegisterImporter/RegisterExporter(+Must…variants forinit()), the façadeImport/Export, andFormats(). Every public argument is validated on entry (emptyFormat, nil impl, duplicate(Format, direction)→ a self-identifying error naming the function and the offending argument — the ADR-002 / project public-API rule).Import/Exporton an unregistered format return a "unknown format %q (registered: …)" error enumeratingFormats(). - §FR-3 — Unsupported-element feedback. An
UnsupportedElementErrorcarrying the offending element's tag, id, and the spec § — the SAD-001 §5 "clear feedback on unsupported elements" requirement, surfaced fromImport.
Functional — the BPMN converter (pkg/convert/bpmn)¶
- §FR-4 — Converter package. A sibling package of the seam,
pkg/convert/bpmn, in the root module. It imports core only (dependency direction inward, SAD-001 §9.1) and adds no third-party dependency — the parser is stdlibencoding/xml. It self-registers both directions viainit()(MustRegisterImporter/Exporter), so the blank import_ "github.com/dr-dobermann/gobpm/pkg/convert/bpmn"turns BPMN on.
v.2 change. v.1 specified a separate top-level module (the SAD-001 §9
doc-source/reservation, renamedconvert/by ADR-024 Q1). It shipped as a package instead — see §4.1. - §FR-5 — Import. Parse BPMN 2.0 XML (namespacehttp://www.omg.org/spec/BPMN/20100524/MODEL) into a*process.Processover the §FR-8 element set, via a namespace-aware token-stream decoder (xml.Decoder). BPMNids are preserved as foundation identity (foundation.WithID) — never auto-generated (ADR-019; §4.4). A missing/blankidon a flow element is an import error. - §FR-6 — Export. Serialize a*process.Processback to BPMN 2.0 XML over the §FR-8 set, writingID()→idandName()→name, emitting thebpmn:namespace. No Diagram Interchange is emitted. - §FR-7 — Diagram-interchange tolerance. Import skipsbpmndi:*/dc:*/di:*and other out-of-execution-scope foreign-namespace subtrees silently (SAD-001 §4 N5); an in-bpmn-namespace element the slice does not map is anUnsupportedElementError(§FR-3), not a silent drop.
Exception — non-executable annotations. <bpmn:documentation> and
<bpmn:extensionElements> are skipped silently wherever they appear, despite
being in the BPMN namespace. Both are universal in bpmn.io / Camunda output
and carry no execution semantics; erroring on them would reject essentially
every real modeler file whose flow graph is fully inside the subset. The
error contract therefore covers unmapped flow elements, not annotations.
- §FR-8 — Element mapping (MVP subset). The executable-core subset
(docs/bpmn-spec/conformance.md §2.1.3):
| BPMN XML | Model target | Spec § |
|---|---|---|
<bpmn:definitions> / <bpmn:process> |
document envelope / process.New(name, WithID(id)) |
§10 |
<bpmn:startEvent> (none) |
events.NewStartEvent |
§13.5.1 |
<bpmn:endEvent> (none) |
events.NewEndEvent |
§13.5.6 |
<bpmn:task> / <bpmn:manualTask> |
activities.NewManualTask (no-op, §13.1) |
§13.3.3 |
<bpmn:userTask> |
activities.NewUserTask |
§13.3.3 |
<bpmn:serviceTask> (operationRef) |
activities.NewServiceTask |
§13.3.2 |
<bpmn:interface> / <bpmn:operation> |
service.NewOperation (catalog stub) |
§10.4 |
<bpmn:sequenceFlow> (sourceRef/targetRef, conditionExpression) |
flow.Link(src, trg, WithCondition) |
§13.2 / §13.3.1 |
<bpmn:exclusiveGateway> (default) |
gateways.NewExclusiveGateway |
§13.4.2 |
<bpmn:parallelGateway> |
gateways.NewParallelGateway |
§13.4.1 |
v.2 change. v.1 deferred
serviceTaskto slice 2. It landed in slice 1 together with theServiceTask.Operation()accessor it needs for export — see §4.6.
Functional — front door¶
- §FR-9 — Example.
examples/bpmn-convert/(own module): blank-import the BPMN converter,convert.Importa bundled.bpmn,RegisterProcess+ run it to completion on a thresher, thenconvert.Exportit back. - §FR-10 — Docs. A converter section in the user guide/README, a changelog entry, and the tracking-issue link.
Non-functional¶
- §NFR-1 — Dependency budget.
pkg/convertis stdlib-only and carries no serialization of its own — noencoding/xml, noencoding/json; enforced by theconvert-seam-format-agnosticdepguard rule, scoped topkg/convert/*.goso the format packages below it stay free to serialize.pkg/convert/bpmnuses stdlibencoding/xmlonly. Neither adds a third-party dependency, so core's "stdlib + uuid" budget (SAD-001 §9.1) is untouched. - §NFR-2 — Streaming. Import/export are
io.Reader/io.Writer-shaped and stream viaxml.Decoder/xml.Encoder— no whole-file buffering mandated. - §NFR-3 — Semantic round-trip.
ImportthenExportyields a structurally/semantically equivalent process (same ids, nodes, flows, conditions, gateway kinds) — not byte-identical XML (ADR-024 §2.8). - §NFR-4 — Public-API validation. Every exported constructor/registry function validates all parameters with self-identifying errors.
- §NFR-5 — CI parity. The converter is part of the root module, so it is
covered by
make ci-core(tidy, lint, build, race, govulncheck) with no Makefile or workflow change — and, unlike a separate module, its code does reach thecover-checkdiff-coverage gate (coverage.txtis written for the root module only). Touched files meet ≥COVER_MIN.
§3 Models¶
§3.1 pkg/convert (core seam)¶
package convert
type Format string
const BPMN Format = "bpmn"
type Importer interface {
Import(ctx context.Context, r io.Reader) (*process.Process, error)
}
type Exporter interface {
Export(ctx context.Context, w io.Writer, p *process.Process) error
}
func RegisterImporter(f Format, imp Importer) error
func RegisterExporter(f Format, exp Exporter) error
func MustRegisterImporter(f Format, imp Importer) // panics on error (init use)
func MustRegisterExporter(f Format, exp Exporter)
func Import(ctx context.Context, f Format, r io.Reader) (*process.Process, error)
func Export(ctx context.Context, f Format, w io.Writer, p *process.Process) error
func Formats() []Format
type UnsupportedElementError struct {
Tag string // local element name, e.g. "inclusiveGateway"
ID string // the element's id attr, if present
Section string // spec §, e.g. "§13.4.3"
}
func (e *UnsupportedElementError) Error() string
Registry state is package-global maps keyed by Format, mutated only by
Register… (typically from a converter package's init()) — the
image.RegisterFormat idiom (ADR-024 §2.2), a deliberate deviation from the
functional-options norm because convert is engine-independent.
§3.2 pkg/convert/bpmn (the BPMN converter)¶
No exported surface: the package is reached through the convert façade and
switched on by a blank import. Import uses a token-stream decoder (not
struct-unmarshal — §4.3); export uses typed XML structs marshalled with
xml.Encoder:
// One element type carries every flow node; Tag picks the concrete name, and
// the unused attributes are omitempty. A per-element struct (v.1's shape) would
// have forced Elements into per-kind slices and lost document order on export.
type xmlNode struct {
XMLName xml.Name
ID string `xml:"id,attr"`
Name string `xml:"name,attr,omitempty"`
Direction string `xml:"gatewayDirection,attr,omitempty"`
Default string `xml:"default,attr,omitempty"`
Implementation string `xml:"implementation,attr,omitempty"`
OperationRef string `xml:"operationRef,attr,omitempty"`
}
// Elements is []any so nodes and flows marshal each under their own tag, in
// the order the model reports them.
type xmlProcess struct {
XMLName xml.Name `xml:"bpmn:process"`
ID string `xml:"id,attr"`
Name string `xml:"name,attr,omitempty"`
Elements []any
IsExecutable bool `xml:"isExecutable,attr"`
}
// The condition is a child element, not an attribute: it carries chardata plus
// its own id and language URI.
type xmlSequenceFlow struct {
XMLName xml.Name
Condition *xmlCondition `xml:"bpmn:conditionExpression,omitempty"`
ID string `xml:"id,attr"`
Name string `xml:"name,attr,omitempty"`
SourceRef string `xml:"sourceRef,attr"`
TargetRef string `xml:"targetRef,attr"`
}
type importer struct{}
type exporter struct{}
func init() { //nolint:gochecknoinits // §FR-4 self-registration
convert.MustRegisterImporter(convert.BPMN, importer{})
convert.MustRegisterExporter(convert.BPMN, exporter{})
}
Field order in these structs is dictated by govet/fieldalignment, which the
repo enforces; it does not affect the emitted document, because encoding/xml
collects attributes in a pass of their own before writing children.
§3.3 Import algorithm (two-pass)¶
- Decode the token stream: collect nodes and flows; skip foreign-namespace
subtrees (
di/dc); an unmappedbpmn:-namespace start element →UnsupportedElementError. - Build nodes first — every node constructor called with
WithID(id)— so the id→node table is complete before wiring. - Link flows —
flow.Link(nodes[sourceRef], nodes[targetRef], …), attachingWithConditionwhenconditionExpressionis present; re-resolve each exclusive gateway'sdefaultby flow id. process.Validate()the assembled graph before returning.
§4 Analysis¶
§4.1 Why a package, not a separate module¶
ADR-024 §4-A chose a separate module on two grounds: SAD-001 §4 N7 ("core
accepts pre-built models") and keeping core's "stdlib + uuid" dependency budget.
The second ground does not survive ADR-024's own §4-D, which chose a
hand-rolled encoding/xml parser: encoding/xml is stdlib, so the
converter adds nothing to the budget — its go.sum requirement set is empty.
What remains of N7 is a statement about responsibility, and that is
preserved by direction, not by a module boundary: the converter imports the
model, the engine never imports the converter, and Import returns a
*process.Process the host registers itself.
Against that, a module costs a go.mod, a replace, its own release tag — and,
decisively, invisibility to the diff-coverage gate: make test-all writes
coverage.txt for the root module only, so ~1 600 lines of parser would never
have been measured. Moving it in-tree put it under the 95 % gate, which is what
surfaced the dead operationRef export path (§4.6).
One constraint had to be handled explicitly. internal/lintcfg/muststyle_test.go
(FIX-026) fails the build on any Must* call under pkg/, and §FR-4's
init() calls MustRegisterImporter/MustRegisterExporter. Rather than dodge
the guard, pkg/convert/bpmn/bpmn.go is listed in that test's exemptFiles
with the reason: the arguments are compile-time constants of the package, the
call happens once at load time, and the only reachable failure is a duplicate
registration — a programming error an init() cannot return.
§4.1a Why the seam is core and the format is not¶
pkg/convert is engine-facing contract (ADR-002: extension interfaces live in
pkg/), so it belongs in core and must stay format-agnostic — enforced by
depguard (§NFR-1). pkg/convert/bpmn is one implementation of that contract;
further formats (XPDL, a vendor dialect) become sibling packages, or
out-of-tree packages in the user's own module, with no core change.
§4.2 Why preserve BPMN ids¶
ADR-019 keys the version lineage on the process id; auto-generating ids on
import would make every import a singleton v1 and silently defeat versioning
(ADR-024 §2.5). Hence foundation.WithID on every mapped element, and a
missing flow-element id is a hard error.
§4.3 Why a token-stream decoder, not struct-unmarshal¶
encoding/xml struct-unmarshal silently ignores any element it has no field
for — which would make §FR-3/§FR-7 impossible (an unsupported bpmn: element
would vanish instead of raising UnsupportedElementError). A namespace-aware
xml.Decoder token loop dispatches on each start element by local-name +
namespace, so it can (a) map known elements, (b) skip DI subtrees by namespace,
and (c) error on an unknown bpmn:-namespace element. Export has no such
problem — we control the output — so it uses struct marshalling.
§4.4 Two-pass import¶
sourceRef/targetRef are id references and flow.Link needs both endpoint
nodes to exist; a single forward pass would fail on a flow whose target appears
later in the document. Nodes are built first, flows linked second (§3.3).
§4.5 DI skip vs unsupported-element error¶
DI/DC (bpmndi:*, dc:*, di:*) is out of execution conformance
(conformance.md:155) — skipped silently. An
in-bpmn-scope element the slice hasn't mapped yet (inclusive gateway, timer
event, boundary event, sub-process) is reported, so the modeler learns what
the engine won't run (SAD-001 §5). The discriminator is the namespace.
§4.6 serviceTask — deferred in v.1, landed in v.2¶
NewServiceTask requires a non-nil service.Operation, which in BPMN is an
operationRef into an <bpmn:interface>/<bpmn:operation> under
<definitions>. v.1 deferred the whole element because faithful export also
needs a ServiceTask.Operation() accessor that did not exist.
Both halves shipped in slice 1:
- Import parses the definitions-level catalog before the process, so
operationRefresolves to aservice.Operationbuilt byservice.NewOperationwith no implementor — a catalog stub the host binds after import. AserviceTaskwith nooperationRefgets a synthetic operation id<taskID>:operation; anoperationRefnaming an unknown operation is a hard import error (ObjectNotFound). - Export writes
operationRefand reconstructs a single synthetic<bpmn:interface>per process (id<processID>-services) holding every operation the process references, so the ref resolves on re-import. activities.ServiceTask.Operation()was added as a read-only accessor. It is the one core API addition in this SRD.
Why the accessor could not be avoided. The converter first shipped a
structural workaround — any(st).(interface{ Operation() service.Operation }) —
so it would compile against a gobpm without the getter and light up against one
with it. Inside the repo that assertion could never succeed, which made the
whole operationRef/interface-catalog export path unreachable: exported
documents silently lost the service binding, and TestServiceTaskRoundTrip
did not notice because it only asserted the node's type survived. The
diff-coverage gate (§4.1) is what exposed it. The workaround is gone; the
round-trip test now asserts Operation().ID() survives.
Message bindings remain deferred. inMessageRef/outMessageRef are parsed
and recorded on the operation spec but not bound to bpmncommon.Message, and
not re-emitted on export. That is the first work of the next slice.
§4.7 Rejected shapes¶
- Wrapping a third-party BPMN library — pulls DI/diagram weight the package
doesn't need for the executable subset; stdlib
encoding/xmlsuffices (ADR-024 §4-D). It would also have breached core's dependency budget, which the stdlib parser leaves untouched (§4.1). - Struct-unmarshal import — silently drops unknowns (§4.3).
- Byte-lossless round-trip — DI is out of scope and textual losslessness is not a conformance requirement (ADR-024 §2.8 / §4-F).
§5 API surface¶
New (core): pkg/convert — Format, BPMN, Importer, Exporter,
RegisterImporter/RegisterExporter, MustRegisterImporter/
MustRegisterExporter, Import, Export, Formats, UnsupportedElementError.
New (core): pkg/convert/bpmn — no exported surface beyond the init()
self-registration (consumers use the convert façade); blank-import to enable.
Changed (core): activities.ServiceTask.Operation() service.Operation — a
read-only accessor for the operation the task was constructed with, needed by
export (§4.6). Additive; never nil, since NewServiceTask rejects a nil
operation.
Unchanged: the other model constructors, thresher.RegisterProcess. No
engine API change.
§6 Test scenarios¶
TestConvertRegistry(pkg/convert) — register/lookup happy path; duplicate registration rejected; nil impl rejected; empty format rejected;Import/Exporton an unknown format returns the enumerating error;Formats()lists registered.TestBPMNImportMVP(pkg/convert/bpmn) — each §FR-8 element imports to the right constructor with id/name preserved; conditional + default flows attach.TestBPMNExportMVP— a programmatically built process exports to XML with correct tags/attrs; no DI emitted.TestBPMNRoundTrip— the §6 worked example: import → export → re-import → structural equality (ids, node kinds, flows, conditions, gateway direction & default). Semantic, not byte (NFR-3).TestBPMNPreservesID— an imported process registered viaRegisterProcesscarries the BPMN process id as its version key (ADR-019).TestBPMNUnsupportedElement— an<bpmn:inclusiveGateway>yieldsUnsupportedElementError{Tag:"inclusiveGateway", …}; a<bpmndi:BPMNDiagram>is skipped silently (no error).TestBPMNImportRegisterRun(e2e,examples/bpmn-convert/orpkg/thresher) — import the bundled.bpmn, register, run to completion.
Worked example (the round-trip fixture — start → userTask → exclusiveGateway with a conditional branch + a default branch → two ends):
<bpmn:definitions xmlns:bpmn="http://www.omg.org/spec/BPMN/20100524/MODEL">
<bpmn:process id="approval" name="Approval" isExecutable="true">
<bpmn:startEvent id="s1" name="start"/>
<bpmn:userTask id="u1" name="review"/>
<bpmn:exclusiveGateway id="g1" name="decide" default="f_no"/>
<bpmn:endEvent id="e_ok" name="approved"/>
<bpmn:endEvent id="e_no" name="rejected"/>
<bpmn:sequenceFlow id="f_su" sourceRef="s1" targetRef="u1"/>
<bpmn:sequenceFlow id="f_ug" sourceRef="u1" targetRef="g1"/>
<bpmn:sequenceFlow id="f_ok" sourceRef="g1" targetRef="e_ok">
<bpmn:conditionExpression>approved == true</bpmn:conditionExpression>
</bpmn:sequenceFlow>
<bpmn:sequenceFlow id="f_no" sourceRef="g1" targetRef="e_no"/>
</bpmn:process>
</bpmn:definitions>
After convert.Import(ctx, convert.BPMN, r): p.Name() == "Approval",
p.ID() == "approval", len(p.Nodes()) == 5, len(p.Flows()) == 4, the flow
f_ok carries a condition, and g1.DefaultFlow().ID() == "f_no". Re-exporting
and re-importing reproduces the same structure (NFR-3).
§7 Milestones¶
| # | Scope |
|---|---|
| M1 | pkg/convert seam — Format, Importer/Exporter, registry (+Must…, façade, Formats), UnsupportedElementError; TestConvertRegistry (§FR-1..3) |
| M2 | pkg/convert/bpmn + import (token-stream, id-preservation, DI-skip, unsupported-element error) of §FR-8; import + preserve-id + unsupported tests (§FR-4/5/7/8) |
| M3 | export (xml.Encoder) of §FR-8 + semantic round-trip tests; the worked-example fixture (§FR-6, NFR-3) |
| M4 | examples/bpmn-convert/ + guide/README + changelog + e2e (§FR-9/10, NFR-5) |
Post-M4: /check-srd, §10 fill, SRD-051 → Accepted, ADR-024 → Accepted, and the
ADR-024 §4-A / §2.6 reconciliation (§4.1, §4.6).
§8 Cross-doc¶
- Implements ADR-024 v.1 §2.1–§2.8 — the seam and the first BPMN slice.
- Upstream ADR-002 v.2 (seam idiom), ADR-019 v.1 (id = version key), SAD-001 v.1 §4 N7 / §5 / §9 (parser outside the engine, modeler feedback, layout).
- Standard docs/bpmn-spec/ — conformance §2.1.3, elements, semantics (§ pins in §FR-8).
- Downstream sync (applied at landing): ADR-024 §4-A re-decided to a package
(§4.1) and Q1 withdrawn — there is no module left to name; ADR-024 §2.6
serviceTaskrestored to the MVP set (§4.6); SAD-001 §9doc-source/reservation retired. - Direction check: SRD → ADR/SAD (up), all pins versioned. No downward ref.
§9 Definition of Done¶
- Every §FR wired to real code; §6 tests present and green.
make cigreen (tidy, lint, build, race, diff-coverage ≥COVER_MINon touched files, govulncheck).- The round-trip and import→register→run tests green; unsupported-element feedback and id→version tie demonstrated.
examples/bpmn-convert/runs to completion; guide/README + changelog updated.- Cross-doc pins consistent (§8); the ADR-024 / SAD-001 reconciliation applied.
§10 Implementation summary¶
Landed on feat/bpmn-converter (2026-07-30). The converter was first built
out-of-tree against the published gobpm v0.9.0 and merged in; every §FR is
wired, make ci-core is green, and the example runs end to end.
Deltas vs v.1¶
| # | v.1 said | Shipped | Where |
|---|---|---|---|
| 1 | BPMN lives in a separate top-level module | a package, pkg/convert/bpmn, in the root module |
§4.1 |
| 2 | serviceTask deferred to slice 2 |
landed in slice 1, with the ServiceTask.Operation() accessor |
§4.6 |
| 3 | any unmapped in-bpmn element errors |
documentation / extensionElements skip silently |
§FR-7 |
| 4 | per-element XML structs (StartEvents []xmlNode, …) |
one xmlNode + Elements []any, preserving document order |
§3.2 |
| 5 | conditionExpression as a string attribute |
a child element carrying chardata + id + language |
§3.2, Q2 |
Empirical findings¶
- The diff-coverage gate earns its keep. Moving the parser in-tree put it
under
cover-check, which immediately exposedinterfacesXMLand half ofsetServiceTaskAttrsas unreachable — the deadoperationRefexport path of §4.6. A separate module would have kept that invisible and shipped a silently lossy round-trip. xml.Encoder.FlushafterEncodeis dead code.Encodeends withenc.p.w.Flush(), so a following explicitFlushcan only ever drain an empty buffer. The redundant block was removed.UserTaskneeds a placeholder output. gobpm requires at least one output resource parameter, so an imported<userTask>gets a synthesized optional output that is not written back on export. Asymmetric but semantically neutral; see the backlog below.- Diff coverage at landing: 96.3 % of 830 changed lines (min 95 %).
pkg/convert100 %,pkg/convert/bpmn92.5 % of statements.
Test-name mapping (§6 → shipped)¶
TestConvertRegistry kept its name. The BPMN cases dropped the redundant
BPMN infix, since they live in pkg/convert/bpmn:
TestBPMNImportMVP → TestImportSubset, TestBPMNExportMVP → TestExportMVP,
TestBPMNRoundTrip → TestRoundTrip (+ TestWorkedExample for the §6
fixture), TestBPMNPreservesID → TestPreservesID,
TestBPMNUnsupportedElement → TestImportInvalidFixtures +
TestExportUnsupportedNode, TestBPMNImportRegisterRun → TestImportRegisterRun.
Backlog (next slices)¶
- Operation message binding —
inMessageRef/outMessageRef→bpmncommon.Message, and re-emitting them on export (§4.6). - Non-text conditions — export currently refuses a condition with no source text (Q2); revisit when a compiled expression engine can round-trip one.
UserTaskplaceholder output — either relax the ≥1-output rule in the model or promote the workaround to a documented rule.FuzzImporton the untrusted-XML boundary.- MIWG conformance corpus (Q1) once the element set grows past the MVP.
- Next element slices, in ADR-024's order: message/timer intermediate events → inclusive and event-based gateways → boundary events and sub-process → send/receive tasks.
- Per-module coverage profiles so
runtime/andadapters/*reachcover-checktoo — the gap this SRD sidestepped by staying in the root module (§4.1). Not converter work: tracked in docs/backlog.md under un-homed items.
Open questions — all resolved at landing¶
- Worked-example fixture source — RESOLVED: hand-authored. The §6 snippet
plus eight files under
pkg/convert/bpmn/testdata/(three valid, five invalid) — the repo's firsttestdata/directory. An MIWG corpus is backlog item 5: it only pays off once the element set outgrows the MVP. conditionExpressionlanguage — RESOLVED: recorded. The importer keeps thelanguageattribute and the expressionidalongside the body on an inertformalExpression, and export writes all three back, so the attribute round-trips. Nothing evaluates it — that stays the expression engine's job — but discarding it would have made the round-trip lossy for every XPath-flavoured file.- Module name (inherited ADR-024 Q1) — WITHDRAWN. There is no module to
name: the converter is
pkg/convert/bpmn(§4.1). The SAD-001 §9doc-source/reservation is retired rather than renamed.