231 lines
15 KiB
Markdown
231 lines
15 KiB
Markdown
# 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](https://raw.githubusercontent.com/eclipse-capella/capella/master/m2/plugins/org.polarsys.capella.core.data.def/model/CapellaCommon.ecore).
|
|
|
|
## Exact model changes
|
|
|
|
1. Extend **both** `StateKind` and corresponding `Kind` entries with `Choice`,
|
|
`Junction`, `Fork`, `Join`, `ShallowHistory`, `DeepHistory`, `EntryPoint`,
|
|
`ExitPoint`, `Terminate`. Continue using
|
|
`SharedBody::State { kind, entry, exit, do_activity }`. No separate graph or
|
|
state-machine framework. Keep `is_state` as the vertex-family query, but add
|
|
`is_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.
|
|
2. Add one `Kind::Event`, shared in OA/SA:
|
|
|
|
```rust
|
|
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.
|
|
3. Extend existing `RelationBody::StateTransition` with
|
|
`transition_kind: TransitionKind`, `triggers: Vec<Id>`, `effects: Vec<Id>`.
|
|
Retain `source`, `target`, `trigger`, `guard`, `effect` exactly. `triggers`
|
|
accepts same-layer Event or Activity/Function IDs; the referenced kind resolves
|
|
their meaning without another wrapper object. `effects` accepts 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; `trigger` is additional
|
|
descriptive text. Empty references plus nonblank `trigger`: legacy/prose-only
|
|
triggering, with `unresolved_trigger` warning. 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](https://download.eclipse.org/modeling/mdt/uml2/javadoc/5.5.0/org/eclipse/uml2/uml/Transition.html),
|
|
[transition kinds](https://download.eclipse.org/modeling/mdt/uml2/javadoc/5.5.0/org/eclipse/uml2/uml/TransitionKind.html).
|
|
|
|
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](https://download.eclipse.org/modeling/mdt/uml2/javadoc/5.5.0/org/eclipse/uml2/uml/Pseudostate.html),
|
|
[OMG history issue](https://issues.omg.org/issues/UMLR-816).
|
|
|
|
## 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.
|