SRD-089.D — BPMN import: typed events and the catalogs they refer to¶
| Field | Value |
|---|---|
| Status | Accepted |
| Date | 2026-08-11 |
| Owner | Ruslan Gabitov |
| Implements | ADR-024 v.4 §2.9 (the element set), §2.15 (the report contract this stage exercises) |
| Upstream | ADR-024 §2.16 (which refusals are staged, capability-blocked or standing — §4.6 and §4.11 are instances of it), ADR-006 v.6 (event definitions and subscriptions), ADR-014 v.2 (messages), ADR-018 v.1 (boundary attachment), ADR-026 v.1 (compensation) |
| Related | SRD-089.A (the dispatch tables and deferred references this extends), SRD-089.B (the expression language a timer and a condition are written in), SRD-089.C (the flow nodes this attaches events to) |
| Tracking | #284 — Part of |
Every element .C left refused was refused for one reason: its constructor needs an event definition, and an event definition needs a catalog object the importer does not build. This stage builds them. It is the largest single widening in the slice — a plain start event becomes a message, timer, signal, error, escalation or conditional start event; an activity gains a boundary; and send and receive tasks stop being holes in the task family.
§1 Background (verified)¶
Ten event definitions exist, and none of them is missing from the model:
NewMessageEventDefinition (pkg/model/events/message.go:29),
NewTimerEventDefinition (timer.go:45), NewSignalEventDefinition
(signal.go:71), NewErrorEventDefinition (error.go:27),
NewEscalationEventDefinition (escalation.go:103),
NewConditionalEventDefinition (conditional.go:24),
NewTerminateEventDefinition (terminate.go:20), NewCancelEventDefinition
(cancel.go:31), NewCompensationEventDefinition (compensation.go:35) and
NewLinkEventDefinition (link.go:26). What blocks the importer is not the
definitions but what four of them require.
Four require a catalog object. Message needs a *bpmncommon.Message, signal
a *events.Signal, error a *bpmncommon.Error, escalation a
*events.Escalation — each rejecting nil. Those are <bpmn:message>,
<bpmn:signal>, <bpmn:error> and <bpmn:escalation>, definitions-level root
elements the importer currently refuses.
Two take an expression, and the model already knows how to feed both.
NewConditionalEventDefinition(condition data.FormalExpression, …) rejects a
nil condition and any expression whose ResultType() is not "bool"
(conditional.go:28, :39-46) — which is exactly what .B's lite.Cond
declares (lite/lite.go:185-191). NewTimerEventDefinition is stricter
still, checking each expression's declared result type against a fixed table
(timer.go:75-92), and the text expression language declares none of int or
Duration. That is not the constructor an importer should be calling:
NewISO8601Timer(s string, …) takes ONE ISO 8601 string and disassembles it
into those attributes itself (timer_iso8601.go:43-90), and
NewISO8601TimerExpr(form, e, …) does the same for a value that is an
expression, adapting its result to the type required. §4.6 records the
mapping.
One thing .B built is flow-shaped and must be widened. newCondition(fs
flowSpec, docLang string) (language.go:168) reads fs.condLang,
fs.condBody, fs.condID and fs.id — it is written for a sequence flow. A
conditional event definition carries a body and no flowSpec, so M2 first
extracts the body-level half (resolve language → translate JUEL → mint) and
re-expresses newCondition on top of it. That is a refactor inside landed .B
code, not a new table row, and §7 says so.
One requires a resolved node. NewCompensationEventDefinition(activity
flow.ActivityNode, waitForCompletion bool, …) takes the activity positionally
and does not reject nil — which matches the standard, where a compensation
event without an activityRef compensates its whole scope.
Two constructor families disagree about how a definition arrives.
NewIntermediateCatchEvent(name, def flow.EventDefinition, …) and
NewIntermediateThrowEvent take it positionally and reject nil
(intermediate_catch.go:43, intermediate_throw.go:49). NewStartEvent(name,
opts …) and NewEndEvent(name, opts …) take only options, and receive
definitions through the eight With*Trigger options in event_options.go:50-136
plus WithTerminateTrigger in end_options.go:95.
There is no WithLinkTrigger, and there should not be. The extract's
boundary matrix lists Link as "NO (only in normal flow)"
(semantics/event-handling.md:67), and the standard confines a link event to
the intermediate position — which is the one place the model takes a definition
positionally and therefore accepts one.
Boundary events need three things, all available: NewBoundaryEvent(name,
host flow.ActivityNode, def flow.EventDefinition, cancelActivity bool, …)
(boundary.go:66). The host is attachedToRef — a reference resolved after the
graph is built, which is precisely what .A's deferred-reference machinery does.
cancelActivity defaults to True (elements/events.md:252).
Send and receive tasks need only a message. NewSendTask(name, msg
*bpmncommon.Message, …) (send_task.go:29) and NewReceiveTask (receive_task.go:58).
Once the catalog exists they cost a table row each.
Two mismatches between the standard and the constructors shape §4. The
extract gives Message.itemRef, Signal.structureRef, Error.structureRef and
Escalation.structureRef cardinality 0..1, and name 0..1 for all four
(elements/event-definitions.md:230-318). The model is stricter: NewMessage
rejects a nil item (bpmncommon/message.go:41), NewEscalation rejects a nil
item (escalation.go:38), and all four reject an empty name. A spec-legal
<bpmn:message id="m1"/> is unconstructible as written.
§2 Requirements¶
Functional¶
- FR-1 — The four catalogs import.
<message>,<signal>,<error>and<escalation>at definitions level become model objects, indexed by id for the references that follow.errorCodeandescalationCodeare carried. - FR-2 — All ten event definitions import, each from its own
<*EventDefinition>child, in every event position the model accepts it. A timer imports in all three of its forms, literal or expression (§4.6). - FR-3 — A start or end event carrying a definition imports through the
matching
With*Triggeroption, includingparallelMultipleand the interrupting/non-interrupting distinction the model already models. - FR-4 —
<intermediateCatchEvent>and<intermediateThrowEvent>import, with their definition supplied positionally. An intermediate event with no definition is refused, naming the constructor's requirement — the standard has no untyped intermediate event either. - FR-5 —
<boundaryEvent>imports, itsattachedToRefresolved through .A's deferred references and itscancelActivitydefaulting totrue. Attaching to a non-activity, or to an id that does not exist, is an error that names both ends. - FR-6 —
<sendTask>and<receiveTask>import, resolvingmessageRefagainst the catalog. .C's refusal of both is removed. - FR-7 — A reference to a catalog object that does not exist is an error,
not a silent nil —
messageRef,signalRef,errorRef,escalationRef,activityRefandattachedToRefall resolve or fail by name. - FR-8 — What the model cannot hold is reported, not dropped. A catalog
object's
itemRef/structureRefis not imported (§4.1), and a messaging task'simplementation/operationRefhave nowhere to land. Each occurrence adds aDroppedentry naming the element and the reason.
Non-functional¶
- NFR-1 — Position rules stay the model's. The converter does not
re-implement the boundary trigger matrix (
event-handling.md:54-67); it hands the definition to the constructor and reports the constructor's refusal. Two copies of a rule diverge, and the model's copy is the one the runtime obeys. - NFR-2 —
make cigreen,cover-checkatCOVER_MIN, judged from.ci/last-run.json. - NFR-3 — The stage adds no new disposition kind. The three from .C (refused, not supported yet, not expressible) are sufficient, and this stage adds no member to the last of them: every element it maps is reachable through a constructor the model exports.
§3 Models¶
// The catalog is a second index, parallel to refIndex's nodes and flows.
// It is separate because its contents are NOT flow elements: nothing in it
// can be a sequence flow's source, and a nodes-keyed lookup that also held
// messages would let a <sequenceFlow sourceRef="msg1"> resolve.
type catalog struct {
messages map[string]*bpmncommon.Message
signals map[string]*events.Signal
errors map[string]*bpmncommon.Error
escalations map[string]*events.Escalation
// kinds records which element declared each id, so a duplicate names the
// element that already took it: BPMN ids are unique across a document,
// and the four maps alone cannot see a collision between two of them.
kinds map[string]string
}
A fifth map, itemRefs, was added later by SRD-094 FR-7 to check an event's
data parameter against the item of the definition it pairs with. It is not part
of this stage.
// defBuilder builds one event definition from its XML element. The table is
// keyed by tag exactly as nodeBuilders is, so adding the eleventh definition
// the standard may grow is one row.
//
// It landed taking the assembly and a recorded defSpec, and returning a
// builtDef, rather than the parser and a live xml.StartElement: §4.7 defers
// construction to pass 2, so by the time a builder runs the element is long
// consumed and what it has is the spec pass 1 recorded. §4.8 records the
// return type; the parameters moved for the same reason and in the same
// commit.
type defBuilder func(asm *assembly, owner string, s defSpec) (builtDef, error)
var defBuilders = map[string]defBuilder{
tagMessageEventDef: buildMessageDef,
tagTimerEventDef: buildTimerDef,
tagSignalEventDef: buildSignalDef,
tagErrorEventDef: buildErrorDef,
tagEscalationEventDef: buildEscalationDef,
tagConditionalEventDef: buildConditionalDef,
tagTerminateEventDef: buildTerminateDef,
tagCancelEventDef: buildCancelDef,
tagCompensateEventDef: buildCompensationDef,
tagLinkEventDef: buildLinkDef,
}
// builtDef is one definition plus the option that attaches it to a start
// or end event. Both come from the same builder because only there is the
// definition's concrete type known — see §4.8. A nil trigger is how Link
// is excluded without the converter owning the rule.
type builtDef struct {
def flow.EventDefinition
trigger options.Option
}
// nodeBody gains the definitions collected before construction — the same
// body-before-build order .A introduced and .C first used for <script>.
// Field order is the landed one (script, docs) with defs appended, arranged to
// satisfy govet/fieldalignment rather than to suppress it. (.A's //nolint:govet
// is on the exporter's xmlSequenceFlow, where field order IS the wire contract
// because encoding/xml emits children in it — a different problem from this
// one.)
type nodeBody struct {
script string
docs []docSpec
defs []defSpec
}
Three fields is this stage's shape. It has since grown laneSets (.E),
props, io, loop, dataAssocs, params and extra (.F–.H) — every one a
child that decides what is built, arriving by the same body-before-build order
this stage established.
defs holds specs, not built definitions — the same §4.7 deferral that
reshaped defBuilder. What a definition refers to (a message, a signal, an
error) may be declared later in the document, so pass 1 records the reference
and pass 2 resolves it against the completed catalogs.
§4 Analysis & decisions¶
§4.1 A catalog object gets an empty item, and the loss is reported¶
The standard makes itemRef/structureRef optional; NewMessage and
NewEscalation require one. Something has to give.
| Option | Verdict |
|---|---|
| Synthesize an empty item and report the dropped structure | Chosen. |
| Refuse a message with no resolvable item | Rejected. It refuses nearly every real diagram — a Camunda <message> carries id and name and nothing else — which would make the whole stage useless for its main audience. |
Parse <itemDefinition structureRef="…"> and build a typed value |
Rejected as unreachable, not as unwanted. structureRef is a QName into an XSD or WSDL the converter does not have and cannot fetch, and NewItemDefinition needs a Go data.Value (data/item.go:74). There is no mechanical route from the one to the other. |
| Relax the constructors to accept a nil item | Rejected. It changes a pkg/model contract to suit a converter, and every existing caller's guarantee with it. |
values.NewRecord() with no fields succeeds (values/record.go:37-45), and
NewItemDefinition handles a value that is nil or empty without complaint
(item.go:86-89). So an empty record is a truthful placeholder: the message
exists and carries no payload structure, which is exactly what the document
said.
The report is the load-bearing half of this decision. A structure silently
discarded is how a host discovers at run time that its message has no payload;
a Dropped entry saying so at import is the difference between a converter that
is honest about its limits and one that appears to have understood the file.
§4.2 A nameless catalog object takes its id¶
All four constructors require a non-empty name; the standard makes name
optional. .A already solved this shape for nodes with fallbackName, and the
same answer applies: use the id, which is required in practice and is what a
modeler sees in the file anyway. The alternative — refusing a nameless
<signal id="s1"/> — refuses a legal document over a cosmetic field.
§4.3 The converter does not own the boundary rules¶
The extract's matrix says Error on a boundary is always interrupting, Cancel
attaches only to a transaction sub-process, and Link and None never appear
there at all (event-handling.md:54-70). The converter could enforce all of
that at parse time.
It will not. Those rules belong to the model, which already refuses what it cannot execute, and the runtime obeys the model's copy rather than the converter's. A second implementation adds a way for the two to disagree — and when they do, the converter's copy wins at import and the model's wins at run time, which is the worst possible split. The converter's job is to pass the definition and report the refusal with the file's element id attached, which is the one thing the model cannot do.
The cost is that a bad boundary combination is caught by a constructor error rather than by a purpose-written message. That is acceptable: the error still names the element, and it is right rather than merely well-worded.
§4.4 Two ways in, one table¶
A definition reaches an intermediate event positionally and a start or end event
through an option. Rather than branch on the event position at every build site,
triggerOptions maps definition → option once, and the positional path ignores
it. Link's absence from that table is then not a special case in code — it is a
missing row, which is the same mechanism that will exclude the next definition
the standard confines to one position.
Corrected by §4.8: triggerOptions landed as a function, not a table —
an option cannot be produced without the built definition it decorates. Link's
exclusion is a builder that returns no trigger, not a missing row.
§4.5 The catalog is indexed separately from the nodes¶
refIndex holds nodes and flows; catalog objects are neither. Merging them
would let a <sequenceFlow sourceRef="msg1"> resolve to a message and fail
later with a type error far from the cause. A separate map makes a
wrong-kind reference fail at the reference, which is .A's wrongKind path
already built and tested.
§4.6 The timer is read by the model, not by the converter¶
BPMN carries a timer's value as a bare Expression with no format constraint
(elements/event-definitions.md:56-58), and the value itself is an ISO 8601
string: an instant, a duration, or a recurrence.
NewTimerEventDefinition alone is the wrong entry point. It takes three
already-typed expressions and checks each declared result type — "Time",
"int", "Duration" (timer.go:75-92) — and the text expression language
declares none of the last two. Reaching for it directly is what makes
<timeDuration> and <timeCycle> look unreachable.
They are not. The model exports the reader:
| Constructor | Takes | Produces |
|---|---|---|
NewISO8601Timer(s, …) (timer_iso8601.go:43) |
one ISO 8601 literal | the whole definition — R3/PT10H becomes timeCycle (3) and timeDuration (10h), because the engine carries a recurrence as (count, interval) |
NewISO8601TimerExpr(form, e, …) (:117) |
a form plus an expression | the same, adapting the expression's result to the type the constructor demands |
and the grammar behind them is a public package — iso8601.ParseDateTime,
ParseDuration, ParseRepeat.
| Option | Verdict |
|---|---|
| Hand the value to the model's ISO 8601 constructors | Chosen. The converter decides only which of the three children the file wrote, and whether its body is a literal or an expression. The grammar, the disassembly and the result typing stay in one place. |
| Mint typed expressions in the converter | Rejected. It is a second ISO 8601 implementation inside pkg/convert, and the model's would have to supersede it — the split §4.3 refuses on principle. |
| Refuse the duration and recurrence forms | Rejected — and it is what an earlier draft of this section decided, from a survey of one constructor. The refusal was wrong: the capability it declared missing is exported, and the same wrong reason had already been written into .B's dialect table for failedJobRetryTimeCycle (corrected in M8). A boundary asserted without searching for the constructor that removes it is not a boundary. |
Literal or expression is decided by the language, not by the text. A body
declaring no language and showing no ${…} is a literal, which is what a timer
normally carries; anything else is an expression whose value is computed per
instance and reaches the model through the adapter. An unreadable literal is
refused by the model's own error, which names all three grammars rather than
guessing which one the author meant.
Two timer children are refused. BPMN makes timeDate exclusive with the
other two, and this engine reads a recurrence from a single string — so a pair
has no combined reading, and picking one would change when the timer fires.
§4.7 Every flow node is constructed after the document is read¶
Definitions.rootElements is an unordered 0..* collection and Process is
itself a RootElement (elements/foundation.md:23), so a <message> may
legally be declared after the <process> whose start event refers to it —
and modeler output routinely does exactly that. An event definition is a
positional constructor argument, so a node carrying one cannot be built until
the whole document has been read.
The spine this stage inherited built each node as its element closed, the way
serviceTask still resolves operationRef against a catalog that must already
exist. That is a declare-before-use rule the standard does not impose.
| Option | Verdict |
|---|---|
Record a nodeSpec in pass 1 and construct every node in pass 2 |
Chosen. One construction path for every element, and the id→node table is complete before the first constructor runs. Retaining the xml.StartElement is safe: encoding/xml allocates a fresh Attr slice per element and copies each value. |
| Construct in pass 2 only the nodes that carry a catalog reference | Rejected. Two construction paths means two places an element can be wired differently, and which path a node takes would depend on its children — the least predictable possible rule. |
| Require catalog objects to precede the process, refusing a forward reference | Rejected. It refuses schema-valid files for the convenience of the parser, which is §4.1's rejected option wearing a different hat. |
| Resolve the reference after construction, through .A's deferred references | Rejected as impossible, not unwanted. NewIntermediateCatchEvent and NewBoundaryEvent take the definition positionally and reject nil; there is no post-construction setter to defer to. |
The visible consequence is error order: a constructor's refusal now surfaces after the file has been read, so a parse failure later in the document is reported first. That is the correct order — the parse failure is the earlier fact — but it is a change, and §6 T-22 pins it rather than leaving it to be discovered.
§4.8 A definition carries its own trigger option¶
§4.4 first proposed a triggerOptions table mapping a definition onto the
option a start or end event needs. It cannot be written that way: every
With*Trigger takes a concrete type (*events.MessageEventDefinition, not
flow.EventDefinition), so a table keyed by tag would have to assert the
concrete type back out of the interface it was just widened into.
Each builder therefore returns the definition and its trigger together, in
the one place where the concrete type is still known and no assertion is
needed. triggerOptions remains as the function that renders them, and Link's
exclusion still costs no branch: its builder produces no trigger, because the
model has no WithLinkTrigger to produce.
§4.9 Import initializes the model's default data states¶
A message-typed event builds an ItemAwareElement over the message's payload,
and that requires data.UnavailableDataState to exist (data/item.go:193-201).
It is host setup — every runnable example calls data.CreateDefaultStates()
before building a process — and nothing in the converter had ever needed it,
because no element imported so far carries data.
| Option | Verdict |
|---|---|
(*parser).parse calls data.CreateDefaultStates() before constructing nodes |
Chosen. The call is idempotent and guarded — it fills the three globals only when they are unset (data/state.go:86-91) — so a host that configured its own states keeps them. |
| Document the requirement and let the import fail | Rejected. "Your message start event imports only if you first call an unrelated initializer" is not a usable contract for a converter, and the failure names data states rather than the file. |
Call it from the package's init() |
Rejected. A blank import would then mutate model globals, which is a side effect no import statement should have. |
§4.10 An intermediate event takes exactly one definition¶
BPMN lets a catch event list several triggers; NewIntermediateCatchEvent and
NewIntermediateThrowEvent take one flow.EventDefinition positionally
(NewIntermediateCatchEvent, NewIntermediateThrowEvent).
| Option | Verdict |
|---|---|
| Refuse an intermediate event carrying more than one definition | Chosen. The refusal names the count and says what to do — split the triggers across separate events. |
| Import the first and report the rest | Rejected. The event would then wait for less than the file asked for, and the difference between "waits for A" and "waits for A or B" is not a detail a Dropped entry makes safe. |
| Attach the extras through a multi-trigger option | Rejected as absent: the model has no such option here. newCatchEvent takes a definition slice, but the exported constructor passes a single-element one, and widening it is a pkg/model change (§4.1's rejected shape). |
Zero definitions is refused for the constructor's own reason, and nothing legal is lost: the standard has no untyped intermediate event either.
§4.11 A compensation boundary waits for associations¶
NewBoundaryEvent refuses a compensation trigger outright and points at
NewCompensationBoundaryEvent, which takes a handler activity
(NewCompensationBoundaryEvent in boundary.go). BPMN names that handler
through an
<association> from the boundary event to the activity — an element .A left
deliberately refused as "mapped work for the composites stage", and one this
stage does not import.
The converter therefore refuses a compensation boundary with a message naming
what the IMPORT is missing. Letting the model's own error through would tell a
modeler to call a Go constructor, which is not an action available to someone
holding a .bpmn file. SRD-089.E lands <association> and removes this
refusal; the same file then imports unchanged.
§4.12 A timer waiter needs a data source it is not always given¶
T-16 — the end-to-end run — found what construction tests structurally cannot:
an imported <timeDate> timer built, registered, and then failed to start its
instance. timeWaiter.parseEDef read its data source as
ds, _ := ep.(data.Source), and a start event's event processor is not one, so
ds was nil — which the expression engine refuses before it ever reads the
expression. (Past tense: the fix below is what the line reads now.)
The defect predates this stage and was unreachable from it: every existing timer test uses a functor expression, which evaluates itself and ignores the source. Only a text expression routes through the engine registry that validates the source, and a text expression is exactly what an importer can mint.
Fixed at the call site rather than in the engine: the waiter passes an empty
data.Source when its processor is not one. A literal date reads no variable
and evaluates fine against it; an expression that does read one now fails
inside the lookup naming the variable, instead of failing at a nil guard that
can name nothing. Relaxing the engine's nil check was rejected — validating a
public API's parameters is the rule the guard exists to keep, and the caller
holding a nil is the actual defect.
§4.13 Every node reports its dialect attributes¶
M8 landed the retry policy and could not be tested on a <task>: the attribute
never reached the mapper. camundaOptions — which both maps the recognized
attributes AND calls reportUnmappedAttrs for the rest — is invoked by the
user, service and rule task builders and by nobody else.
So a dialect attribute on a plain <task>, a <manualTask>, a <scriptTask>,
a gateway or an event is silently dropped: not mapped, and not reported
either. That is precisely the failure the report contract exists to prevent,
surviving in the elements nobody had thought to check.
The fix is to make the call unconditional rather than per-builder, so a node kind added later cannot forget it. A builder that maps an attribute claims it; everything unclaimed is reported once, and a construct stays either mapped or reported and never both.
§5 API deltas¶
None. Every element here uses an existing constructor, and pkg/model is
untouched — §4.1 records why relaxing a model contract was rejected.
§6 Test scenarios¶
| # | Scenario | Asserts |
|---|---|---|
| T-1 | <message>, <signal>, <error errorCode>, <escalation escalationCode> |
all four import; codes carried (FR-1) |
| T-2 | A catalog object with itemRef/structureRef |
imports; one Dropped entry per occurrence naming the structure (FR-8, §4.1) |
| T-3 | <signal id="s1"/> with no name |
imports under the id (§4.2) |
| T-4 | A start event per trigger: message, <timeDate> timer, signal, error, escalation |
each imports through its option (FR-2, FR-3). A conditional start was listed here too and is not covered: the model refuses one on a top-level start event, which T-11 asserts and NFR-1 requires the converter to delegate rather than second-guess. A conditional start belongs to an event sub-process (ADR-023 v.5 §2.10), so the row and T-11 contradicted each other and T-11 is the correct one. |
| T-5 | An end event with terminate, error, escalation, compensation | each imports (FR-2, FR-3). A cancel end was listed here too and is refused outside a transaction — by the model, per §8. It became reachable only when .E landed the transaction variant, and is covered there by TestCancelBecomesReachable. |
| T-6 | <intermediateCatchEvent> with <timeDate> timer, message, signal, conditional, link |
each imports positionally (FR-4) |
| T-7 | <intermediateThrowEvent> with link, signal, escalation, compensation |
each imports (FR-4) |
| T-8 | An intermediate event with no definition | refused, naming the requirement (FR-4) |
| T-9 | <boundaryEvent attachedToRef> with and without cancelActivity |
imports; default true (FR-5) |
| T-24 | A <boundaryEvent> declared before the activity it guards |
imports; a process's flow elements are no more ordered than a document's root ones (§4.7) |
| T-25 | A compensation <boundaryEvent> |
refused, naming the <association> the import still lacks (§4.11). Superseded by SRD-089.E, which landed associations: a compensation boundary WITH one now builds its handler wiring, and the refusal that survives is for a file that draws none — so it names the FILE's gap, not the import's (TestCompensationBoundaryWithoutAnAssociation). |
| T-10 | A boundary event attached to a gateway, and to a missing id | each an error naming both ends (FR-5, FR-7) |
| T-11 | A conditional start event at top level, and a cancel end event outside a transaction | the model's refusal surfaces, with the element id attached (NFR-1, §4.3) |
| T-12 | <sendTask messageRef> and <receiveTask messageRef> |
import; the message resolves (FR-6) |
| T-26 | <receiveTask instantiate> |
the flag reaches the model; default false (FR-6) |
| T-27 | A messaging task's implementation and operationRef |
reported — the model holds neither (FR-8) |
| T-13 | Each of the six reference attributes pointing at a missing id | an error naming the attribute and the target (FR-7) |
| T-14 | <messageEventDefinition messageRef> pointing at a <signal> |
wrongKind, not a nil dereference (§4.5) |
| T-15 | A <startEvent> with a <linkEventDefinition> |
refused — no trigger option exists (§4.4, FR-3) |
| T-16 | A process with a <timeDate> timer start and a boundary error handler, run on a thresher |
registers and completes — the events are wired, not merely constructed (§4.12) |
| T-17 | The .A/.B/.C fixture corpus | unchanged verdicts |
| T-18 | <timerEventDefinition> with <timeDate>, <timeDuration>, <timeCycle> |
each imports; a recurrence fills cycle AND duration (FR-2, §4.6) |
| T-19 | An unreadable timer literal, and two timer children at once | each refused at import, by the model's grammar error and by the exclusivity rule (§4.6) |
| T-20 | <timeDuration>${deadline}</timeDuration> |
imports through NewISO8601TimerExpr — a per-instance deadline is expressible, not only a literal (§4.6) |
| T-21 | A <conditionExpression> on a flow and a <conditionalEventDefinition> body, same language attribute |
both mint through the extracted body-level helper; the flow's verdicts are unchanged from .B (§1) |
| T-22 | A file whose node fails to construct and whose LATER element fails to parse | the parse failure is reported — nodes are built only after the document is read (§4.7) |
| T-23 | A <message> declared after the <process> that refers to it |
imports; the reference resolves (§4.7) |
§7 Milestones¶
| M | Content |
|---|---|
| M1 | The four catalogs + the item decision (FR-1, FR-8, §4.1, §4.2) |
| M2 | The spine this stage needs: deferred node construction (§4.7) and .B's body-level expression helper extracted out of newCondition (§1) |
| M3 | defBuilders: the ten definitions, timer narrowed to <timeDate>, attached to start and end events through triggerOptions (FR-2, FR-3, FR-7, §4.4, §4.6) |
| M4 | Intermediate catch and throw (FR-4) |
| M5 | Boundary events (FR-5, §4.3) |
| M6 | Send and receive tasks (FR-6) |
| M7 | Timers through the model's ISO 8601 constructors, correcting §4.6 (FR-2) |
| M8 | The dialect row that carried the same wrong reason (failedJobRetryTimeCycle) |
| M9 | Every node reports its dialect attributes — found while landing M8 (§4.13) |
M2 and M3 are not the split §7 first carried. The definitions were to land before the triggers, but neither is observable alone — a definition with no event to attach it to changes nothing a test can see — and the ordering finding in §4.7 turned out to be the real prerequisite for both.
§8 Cross-doc¶
- Implements ADR-024 v.4 §2.9 and §2.15. The ADR is Draft, accepted when the element set completes at the end of this branch.
- SRD-089.A, SRD-089.B and SRD-089.C are landed stages of this same branch and are not edited here.
- The composites that host event sub-processes and transaction-scoped cancel
events land in SRD-089.E; this stage imports the events themselves, and a
cancel event — which the model accepts only inside or on a transaction — is
therefore refused until that stage exists. Reachable since .E:
TestCancelBecomesReachableimports both positions, and T-5 records it.
§9 Definition of Done¶
- FR-1…FR-8 wired, each with its §6 test.
make cigreen, judged from.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.- T-16 demonstrates the added events running, not merely importing.
/check-srdPASS;/pr-reviewruns once at the branch's end.
§10 Implementation summary¶
Filled retroactively at the .F landing, from the branch history.
Nine milestones, in the order §7 records them — including its own note on why M2 and M3 are not the split first planned.
| M | Commit | What landed |
|---|---|---|
| M1 | a459d785 |
the four catalogs an event definition refers to (FR-1, FR-8) |
| M2 | 2836f964 |
deferred node construction — nodes build after the document, not during it (§4.7) |
| M3 | b1937c04 |
the ten definitions in defBuilders, attached through triggerOptions (FR-2, FR-3, FR-7); a6a1f48d closes their failure paths |
| M4 | 367ae153 |
intermediate catch and throw (FR-4) |
| M5 | f9ad7b15 |
boundary events and the activities they guard (FR-5, §4.3) |
| M6 | 2c9198fe |
send and receive tasks (FR-6) |
| M7 | 6a6c8638 |
all three timer forms through the model's ISO 8601 constructors — the earlier refusal's stated reason was wrong (FR-2, §4.6 corrected) |
| M8 | 0db365b6 |
failedJobRetryTimeCycle maps as a retry policy, closing the dialect row that carried the same wrong reason |
| M9 | 457833da |
every node reports its dialect attributes through one funnel (§4.13) |
M2's deferral kept paying: .F moved the PROCESS's construction to the same
place for the same document-order reason (SRD-089.F §4.6), whose amendment
names nodeSpec's doc comment — the shape §4.7's ordering finding left behind
— as the precedent it followed.
Open questions¶
None. The two genuine tensions are decided rather than deferred: §4.1 answers a standard that makes a payload structure optional against constructors that do not, and §4.6 answers a timer whose two recurrence forms the expression language cannot type. Both carry their alternatives and a reporting obligation; neither is left for the implementer to settle.