* feat(bpmn): scope variables, trigger opt-out and composability for BpmnProcess
Completes the container W2 left minimal, with the four things it deferred.
Scope variables. BpmnScopeVariables implements IBpmnVariableReader over the
scope's memory register, walking outward so an inner scope sees the enclosing
one's data, and BpmnScopeHost now declares ScopeVariables and hands the reader
to every snapshot. The read is three-valued: false for a name nothing in scope
declares, Null for a declared variable holding nothing, and StoredExternally
for a value JSON cannot carry.
That last case deviates from the issue, deliberately. The issue names the
unmaterialized-driver case, which is not detectable from the container's side:
PersistentVariablesMiddleware loads with no excludeTags, and
VariablePersistenceManager marks a block IsInitialized before testing the
exclusion, so a variable whose driver was never read is indistinguishable from
one whose driver returned null. Closing that needs a change to
Elsa.Workflows.Core, which is out of bounds here, so the reader answers only
what the block actually says and the XML doc records why. The route it does
have is real and in the same spirit: a value the host holds and cannot put on
the wire faults loudly rather than reading as an empty collection.
Trigger opt-out. BpmnProcess.IsRootScope names the BPMN meaning of Elsa's
CanStartWorkflow rather than adding a second flag that could disagree with the
gate TriggerIndexer actually reads. It is off unless something says otherwise,
and the applier refuses to start a BpmnProcess that claims root position as
another scope's work: the damage a mis-flagged subprocess does happens at
publish time, so repairing the object graph at runtime would leave the trigger
registered while every test went green. ITrigger itself remains #7929.
Composability and outcomes. A BpmnProcess in a Flowchart runs and the flowchart
carries on (D11), and a nested transaction completing Cancelled reaches its
parent's completion callback with that outcome intact, which is the only reason
the parent routes the cancel boundary rather than the ordinary sequence flow.
Every guard was mutation-tested red before green: both non-Present answers of
the reader, the reader left unwired, the opt-out's default flipped (7 tests red,
including the pre-existing nested-scope ones), the refusal removed, and the
outcome dropped at each end of the trip to the parent.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* fix(bpmn): apply review findings on scope variables, command batching, and outcome doc
Read a scope variable through Elsa's configured serializer (via IPayloadSerializer,
serialized against the value's own runtime type so a polymorphic value is not wrapped
in Elsa's type-tagged envelope) instead of bare JsonSerializerDefaults, so a value only
Elsa's converters can carry no longer collapses to StoredExternally. Refuse a root-scope
StartWork before any command in the batch is applied, not mid-list, so a refusal cannot
leave scope memory partially mutated under ContinueWithIncidentsStrategy. Document that
BpmnProcess completes with only its interpreter outcome, so a default/null-port
Flowchart connection never fires from it.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* refactor(bpmn): filter the pre-scan explicitly
Use commands.OfType<BpmnHostCommand.StartWork>() in ApplyAsync's
root-scope pre-scan instead of a foreach + type-check, matching the
static analysis suggestion. The apply loop below is unchanged.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
* feat(bpmn): host-side applier for the Bpmn.Semantics port
Translates the interpreter's three host commands onto ActivityExecutionContext
and feeds its four entry points, plus the minimum BpmnProcess container needed
to exercise them end to end through IWorkflowRunner.
StartWork schedules the bound activity, CancelWorkSubtree calls the public
CancelActivityAsync extension (already recursive), and SignalEnclosingScope
sends a BpmnScopeSignal up the ancestor chain. OnWorkFaulted rides the
FaultSignal seam: it asks the interpreter what BPMN made of the fault and calls
StopPropagation only on a Caught disposition, leaving a Propagated one strictly
alone so an enclosing scope or the incident strategy takes it.
A unit of work is keyed on the child ActivityExecutionContext.Id, recorded in
the scope's own persisted ledger, never on Tag: the completion-callback dispatch
rewrites the receiving context's Tag, so a nested scope wears a different tag
than its parent remembers it by. Interpreter correlation travels on the child's
context rather than on the shared activity instance.
Evaluations go through one queue per workflow instance, so a scope signalled
mid-apply is drained after the command list rather than re-entering the
interpreter. Commands are applied in the order returned.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* test(bpmn): cover the teardown refusal path
Adds a focused unit test that drives BpmnWorkTeardown.CancelSubtreeAsync
into the NotSupportedException branch by constructing a real context tree
with a scheduled-but-not-invoked descendant, so a regression that silently
drops the detection is caught. Also records why BpmnWorkLedger's
append-only, handle-keyed Records list cannot strand a context on a
duplicate StartWork for a live (BindingRef, IterationId) slot, a case the
port's own guarantee makes unreachable from this applier.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* fix(bpmn): keep a refused teardown from stranding ledger state
A subtree cancellation refused with NotSupportedException is absorbed
into an incident under ContinueWithIncidentsStrategy rather than
crashing, so the end-of-command ledger save was being skipped and the
persisted ledger kept claiming work BPMN had just torn down. Save the
ledger removal before the possible throw instead of after, so a later
completion callback for the stranded activity finds no live record and
is discarded instead of being fed to the interpreter.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
Adds the valence-works/bpmn package feed and pins Bpmn.Model,
Bpmn.Semantics and Bpmn.Interchange at 0.1.1-preview.19, then wires
two new module projects consuming those libraries without
reimplementing anything they provide (D12): Elsa.Bpmn for BPMN
execution and Elsa.Bpmn.Interchange for XML import/export, kept as a
separate package so hosts that only execute BPMN don't take the XML
reader. Both are marked IsPackable=false until the Bpmn.* packages
are published to nuget.org. Test projects are added for both, unit
and integration, and all four are wired into Elsa.sln so PR CI
discovers and runs them. This unblocks #7925 and the rest of the
BPMN runtime work in #7909.
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>