15 KiB
R5 state/event implementation contract proposal — 2026-09-05
Status: adopted by the lead after the focused R1/R2/R4 correctness review closed. R5 core, source/API and graphical authoring implementation is in progress; runtime acceptance is not yet claimed. The lead adopted the four Studio choices at the end of this contract; current evidence and ownership are in STATE.md.
Evidence and intended outcome
The native benchmark actually created an H* Deep History node inside
Maintenance's region. It also added [AFTER]30 s beside an existing function
trigger (Load Digital Media) in a transition inspector. It did not establish
history fallback connectivity or validate the resulting transition. Preserve
that distinction when writing acceptance evidence. See
harness/release-capella-workflow-benchmark.md:71.
The implementation currently has State/Mode/Initial/Final, nested regions, StateAction links, availability, and trigger/guard/effect strings. Rendering shows Initial/Final glyphs and ordinary boxes; creation only offers State and Initial. Keep its one typed state payload and existing command/reference boundary. R5 should let an engineer build degraded operation, parallel work, recovery history and a timeout without creating unrelated custom objects.
Capella exposes typed time/change events, transition kinds and additional state vertices. Its relative/absolute time choices distinguish AFTER from AT, and its transition definition retains descriptive triggering text separately from semantic trigger references. Those are the implementation features to retain. CapellaCommon schema.
Exact model changes
-
Extend both
StateKindand correspondingKindentries withChoice,Junction,Fork,Join,ShallowHistory,DeepHistory,EntryPoint,ExitPoint,Terminate. Continue usingSharedBody::State { kind, entry, exit, do_activity }. No separate graph or state-machine framework. Keepis_stateas the vertex-family query, but addis_stable_state(State/Mode only) and use it for behaviors, availability, observation and composite ownership so expanding the family cannot grant pseudostates inappropriate features. This is nine additional vertices. -
Add one
Kind::Event, shared in OA/SA:SharedBody::Event { body: EventBody } enum EventBody { Signal { items: Vec<Id> }, Time { kind: TimeEventKind, expression: String }, Change { expression: String }, } enum TimeEventKind { After, At } enum TransitionKind { External, Local, Internal }An Event is an ordinary root semantic record with durable name/alias/UUID. Signal identifies a named occurrence with optional same-layer ExchangeItem payloads. Time and Change are expression-based definitions. AFTER is relative to activation of the transition's source state; AT names an absolute instant. Change means the named Boolean condition becomes true. No expression evaluator, timer runtime, event queue or signal transport is introduced. Empty time/change expressions warn; invalid XML text, duplicates or invalid item references reject.
-
Extend existing
RelationBody::StateTransitionwithtransition_kind: TransitionKind,triggers: Vec<Id>,effects: Vec<Id>. Retainsource,target,trigger,guard,effectexactly.triggersaccepts same-layer Event or Activity/Function IDs; the referenced kind resolves their meaning without another wrapper object.effectsaccepts same-layer work IDs. Duplicate references reject. The text fields remain authored prose, not inferred UUIDs or executable code. Factory defaults are External and empty lists. Entry/do/exit typed work stays in existing StateAction relations.
Multiple referenced triggers are alternatives (OR), not an implicit conjunction. Work triggers identify that work's occurrence/completion as an engineering trigger; no call to that work is executed. Do not require a trigger function to be allocated to the machine's owner: an external participant's work can cause the observed transition. Existing owner-relative StateAction checks remain.
Activation, hierarchy and transition kinds
The following is the proposed Studio contract, not an execution engine:
- Nonempty
triggers: semantic triggering references;triggeris additional descriptive text. Empty references plus nonblanktrigger: legacy/prose-only triggering, withunresolved_triggerwarning. Do not call this completion or infer an Event by matching its name. Empty references and blank trigger: completion transition from a stable state, or transient routing from a pseudo. - Completion of a simple state means its entry/do behavior is complete; for a composite, all its active regions must complete. Final completes its region; it does not terminate every region or delete the modeled participant. Terminate ends the whole containing state-machine activation. These meanings are documented and rendered; the application never asserts they ran.
- External may leave/re-enter the source configuration. A stable self-transition is permitted and includes exit and entry behavior. Internal requires equal State/Mode endpoints; it preserves the active configuration, including nested regions, and does not imply exit/entry actions.
- Local requires a composite State/Mode source and a proper descendant target; the source remains active while the affected nested configuration changes. Local from an EntryPoint is also permitted when it targets a vertex within that point's boundary composite. Local to self or outside that composite rejects. Pseudostates otherwise use External; outgoing EntryPoint routing uses Local.
The internal/self and local/composite distinction follows the primary UML2 transition model. Its entry-point and fork/join segment restrictions also prevent the common mistake of treating every arrow as an interchangeable external transition. Transition constraints, transition kinds.
Endpoints must be existing vertices in the same layer and StateMachine. A normal transition may cross nested regions of that machine; do not impose a blanket same-immediate-region rule. Stable self-edges remain possible; pseudo self-edges reject. Regions remain directly owned by a StateMachine or State/Mode. Two or more sibling regions represent orthogonal/concurrent regions, without adding a parallel boolean that can disagree with the hierarchy.
Pseudostate ownership and validation policy
All new vertices remain region-owned. For EntryPoint/ExitPoint, the boundary is the containing Region's owner (State/Mode or StateMachine); rendering attaches their symbols to that owner's border while retaining the explicit region destination. This is a deliberate Studio encoding that reuses current ownership, not a claim of byte-identical UML/Capella containment.
Reject invalid references, wrong owners, upper-cardinality contradictions and forbidden directions/transition kinds. Warn about absent required connections, unfinished guards/defaults and unreachable vertices, so insertion can be followed by wiring in separate undoable edits. No pseudostate may own a nested region or typed StateAction. Retained legacy entry/exit/do prose on Initial/Final warns and is preserved, not silently discarded or advertised as executable behavior.
| Vertex | Proposed exact invariant | Incomplete-model warning |
|---|---|---|
| Initial | At most one per Region; no incoming; at most one outgoing; outgoing has no triggers, trigger prose or guard, but may have effect | Missing initial/default-entry path or missing outgoing |
| Final | No outgoing; no nested region or behaviors | No incoming; unreachable |
| Terminate | No outgoing; no nested region or behaviors | No incoming |
| Choice | At most one incoming; outgoing edges are transient guarded alternatives | Missing incoming, fewer than two outgoing, empty branch guard, absent else |
| Junction | One or more incoming/outgoing transient segments; no implicit concurrency | Missing side; ambiguous unguarded alternatives |
| Fork | At most one incoming; outgoing targets are stable states in distinct orthogonal regions of one composite/machine | Missing input, fewer than two outputs |
| Join | At most one outgoing; incoming sources are stable states in distinct orthogonal regions of one composite/machine | Fewer than two inputs, missing output |
| ShallowHistory | At most one per Region; at most one outgoing fallback to a direct stable child in that Region; no fallback trigger/guard | Missing fallback/default-entry path |
| DeepHistory | At most one per Region; at most one outgoing fallback to a stable vertex inside that Region's subtree; no fallback trigger/guard | Missing fallback/default-entry path |
| EntryPoint | Incoming from outside its boundary, outgoing into it; at most one outgoing per destination region; outgoing Local | No outgoing or incomplete affected-region entry |
| ExitPoint | Incoming from inside its boundary; at most one outgoing to outside it; outgoing External | Missing input/output; top-machine exit without a caller is an exit endpoint, not automatic termination |
Fork outputs and Join inputs must not have guards or triggers: conditional dispatch belongs at Choice/Junction, not in a partial synchronization. With more than one arm, using the same immediate orthogonal region twice rejects. Compute the first region below the nearest common composite, not simply each state's immediate region, so deeper nesting does not create false concurrency.
Other pseudo outgoing edges have no new event trigger. Initial restrictions retain the current strict rule. For the newly introduced pseudos, forbidden triggers should be structural errors when committing a complete edge; the native benchmark's accepted Choice trigger was not validated and does not prove a different semantic rule. The UI should move the timed/function trigger onto the incoming stable-state transition, retaining Choice guards on the alternatives. This is an explicit resolution of that observed ambiguity, not hidden loss.
Choice has at most one literal else outgoing guard; duplicate else rejects.
Do not parse arbitrary guards to claim exhaustiveness/disjointness. Junction
chooses a compound path from guard conditions before traversing it; Choice is a
dynamic decision after incoming effects. Keep their symbols and labels distinct.
Shallow history remembers only the immediate state; deeper regions use default
entry. Deep history remembers the nested configuration. Fallback applies where
no resumable history exists; do not synthesize history IDs or running state.
The same-region history policy is a Studio choice supporting independent
orthogonal regions; top-level history is allowed, with no submachine-execution
promise. Primary sources have an acknowledged disagreement about top-level
history placement. Pseudostate constraints,
OMG history issue.
Reference lifecycle, source and REST
Extend the existing visitors for Event Signal items, transition triggers/effects.
Reidentification remaps every reference. Deleting a trigger/effect record removes
its reference but retains the transition and prose; return an actionable cleanup
notice. If removing the last typed trigger would make a transition appear to be
completion, retain a descriptive placeholder in trigger naming the removed
trigger (atomic command) so the edit does not silently change activation meaning.
Undo restores the exact original record and references. No transition deletion
cascades to shared Events or counterpart-layer state machines.
Use oa_event / sa_event, existing oa_<vertex> / sa_<vertex> declarations,
typed Event body constructors, transition_kind External, triggers [@event, @work], and effects [@work]. Reuse the existing typed parser/formatter/form
adapter; update reference/enum field catalogs and every closed selector/schema.
State traces allow matching new vertex kinds and Event definitions in opposite
layers. Required transition closure includes referenced Events, item payloads,
work and effects. R3 reviewed drift must include all new fields and mapped refs.
No REST route is added: current model, element, relation, transaction, validation and transition routes expose these records. OpenAPI must enumerate every vertex, Event body and transition field. A POST Event + EditRelation batch is atomic and conditional; editing the Event name or UUID must not break trigger references.
Old V2 migration preserves all IDs and exact trigger/guard/effect strings. Missing
transition kind becomes External; missing lists become empty. Do not parse
after 30s prose into a Time Event, or invent function references by display-name
match. Old prose-only transitions warn until explicitly linked. No old pseudo
vertex is rewritten. Preserve golden V2 source/JSON, add these defaults to its
comparison allowlist only after schema migration is explicit and tested.
UI and acceptance sequence
Behavior adds a region-context vertex palette with distinct Initial ●, Final ◉, Choice diamond, Junction dot, Fork/Join bars, H/H* circles, entry/exit border symbols and Terminate cross. Every symbol gets a readable text name and usable hit target; nested regions must stay visible and selectable. Add State/Mode internal-transition rows inside the state card rather than a misleading self-loop.
One transition editor shows kind, typed trigger chips, trigger notes, guard,
typed effects and effect notes. Add timed event opens one inline form with
After/At and expression, creates the Event and attaches it in one undo step.
Existing function/event selectors are searchable and filtered by layer. A full
inspector/list displays all trigger names; the edge shows a concise summary and
never clips the only accessible copy. Completion is visibly distinguished
from unresolved descriptive triggering. No nested modal stack is needed.
Implement/verify in observable milestones: (1) events + references + concise
editor and original [AFTER]30 s/function example; (2) nested vertex palette,
choice/history + completion; (3) parallel Fork/Join and transition-kind rendering;
(4) source/API/migration/transition parity and independent review. Meaningful
tests must cover every vertex insertion incomplete state, invalid endpoint and
upper-cardinality case; shallow/deep scope; nested orthogonal arms; internal vs
external self edges; local source retention; trigger/effect UUID edits/deletion
undo; old prose preservation; source/JSON/API equality. Personally exercise the
degraded machine, event creation/rename, H* fallback, branch guard, resize and
save/reopen in the native application. No execution/simulation claim is allowed.
Before implementation, lead should adopt or amend the explicit Studio choices: region-owned entry/exit symbols, history per Region, pseudo-trigger strictness despite the unvalidated benchmark edge, and preserving removed-trigger intent as descriptive text. These are concrete design decisions, not additional user permission questions.