Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
af2099c
feat(migrate): preserve opaque bodies as textual representations
devin-ai-integration[bot] Oct 4, 2026
e9bb778
feat(migrate): map terminate pseudostates to actions
devin-ai-integration[bot] Oct 4, 2026
8f78697
fix(lower): accept metadata annotations in parallel bodies
devin-ai-integration[bot] Oct 9, 2026
114f877
feat(runtime): keep unstarted composite regions inactive
devin-ai-integration[bot] Oct 9, 2026
31b7af7
feat(migrate): wrap single-region composite states
devin-ai-integration[bot] Oct 9, 2026
c04a40c
chore(census): regenerate migration inventories
devin-ai-integration[bot] Oct 9, 2026
fe49633
fix(runtime): enter region stand-ins without completing composites
devin-ai-integration[bot] Oct 9, 2026
d5b73e8
Merge remote-tracking branch 'origin/develop' into feature/census-sta…
devin-ai-integration[bot] Oct 10, 2026
0f39297
fix(runtime): keep a composite without an active substate from comple…
devin-ai-integration[bot] Oct 10, 2026
33ccd9c
test(parser): add AST golden for composite completion fixture
devin-ai-integration[bot] Oct 10, 2026
bf4830e
Merge remote-tracking branch 'origin/develop' into feature/census-sta…
devin-ai-integration[bot] Oct 10, 2026
494cce6
Revert "test(parser): add AST golden for composite completion fixture"
devin-ai-integration[bot] Oct 10, 2026
f72659f
Revert "fix(runtime): keep a composite without an active substate fro…
devin-ai-integration[bot] Oct 10, 2026
263a11b
fix(runtime): count a region left inactive by default entry as complete
devin-ai-integration[bot] Oct 10, 2026
742792e
fix(runtime): enter a region without a start through an empty history
devin-ai-integration[bot] Oct 10, 2026
8c807a1
fix(migrate): keep opaque body whitespace in textual representations
devin-ai-integration[bot] Oct 10, 2026
86bd9a1
fix(runtime): wait for a region stand-in's do action before completin…
devin-ai-integration[bot] Oct 10, 2026
21fc672
test(migrate): align diagram output with raw opaque bodies
devin-ai-integration[bot] Oct 10, 2026
1260c1c
fix(migrate): skip textual representations for whitespace-only opaque…
devin-ai-integration[bot] Oct 10, 2026
a8ed112
Merge remote-tracking branch 'origin/develop' into feature/census-sta…
devin-ai-integration[bot] Oct 10, 2026
ecd1a51
Merge remote-tracking branch 'origin/develop' into feature/census-sta…
devin-ai-integration[bot] Oct 10, 2026
99fbff8
fix(census): keep develop's use-case actor verdicts
devin-ai-integration[bot] Oct 10, 2026
8f0eaf4
docs(compliance): keep develop's state rows alongside the region entr…
devin-ai-integration[bot] Oct 10, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions changes/unreleased/census-states-events.changed.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
- **Preserve UML opaque body text during migration.** Each raw body and matching language now appear as a non-executed textual representation with whitespace preserved, while supported body translations continue to use the trimmed first body and remain executable.
- **Retain UML state-machine structure and termination semantics.** Composite states with one region retain that region as a parallel sub-state. A region left without an active child by default entry counts as complete, whether its stand-in was entered or it has no stand-in or start; a stand-in's running do action defers that completion until it ends. A region activated later by a transition or fork branch must reach its final state. This matches PSSM *Entering 004*. A first entry through history uses the same default, while an owner with no default entry remains an error. Terminate pseudostates now terminate the whole state machine.
- **Clarify the v1-to-v2 transformation census.** Timed triggers and preserved opaque/state behavior are counted as faithful, while trigger-scoped event acceptance is recorded as deliberate.
67 changes: 28 additions & 39 deletions docs/internals/design/precise-semantics-alignment.md
Original file line number Diff line number Diff line change
Expand Up @@ -1688,7 +1688,7 @@ which supersede the hand count this section was first written with — the moves
| The UML `StateMachine` as the class under test (a standalone machine with attributes, operations, a constructor) | `part def` with `attribute`s, `action def`s and `exhibit state`, as an owned machine's: the reader (`reader.go`) reads the machine as the `Target` whose `Machine` is itself, with its attributes, operations and their methods, and its constructor | standard |
| A guard whose behavior acts on the model (calls `trace(...)` before returning its value) | none: a v2 guard is a Boolean expression (§7.18.3, `validateTransitionFeatureMembershipGuardExpression`; `bool guard[*]` in `TransitionPerformances.kerml`, the effect a separate `step`), and an expression has no spelling for an action. UML 2.5.1 §14.5.11 `Transition::guard` itself calls such a guard ill formed. Recording the runtime's guard reads as the referee's observable instead is refused too: the reads the suite traces are a junction's on the target's default entry, read at the incoming transition's selection, where the library orders every transition inside a state after the state's `entry` — see [A guard whose behavior acts on the model](#a-guard-whose-behavior-acts-on-the-model) | no translation |
| A guard whose behavior is an opaque behavior, not an activity | none: the reader follows an activity's nodes to tell whether the behavior acts, and does not read an opaque body, so the guard is refused rather than carried as its Boolean text alone. A `FunctionBehavior` is the exception — it accesses no object by UML's contract (§13.2.3.3) — and is translated as the expression it spells | no translation |
| Fork into states of orthogonal regions that have no initial pseudostate | `parallel` regions spell the shape and the `fork` extension the fork; a region a fork enters needs no `entry; then` (finding 6 below, fixed) | extension |
| Fork into states of orthogonal regions that have no initial pseudostate | `parallel` regions spell the shape and the `fork` extension the fork; a branch enters its target without an `entry; then`. Default entry enters a region's state stand-in even without a start in its body; if no child becomes active, the region counts as complete. A region with no stand-in and no start remains inactive and also counts as complete on default entry. A branch that activates a region must reach its final state | extension |

The classification is by construct, in the order of the table: a test whose model uses any
construct with no spelling or no translation is counted as not expressible whatever else it
Expand Down Expand Up @@ -1755,18 +1755,19 @@ is where a later translation moves them back.
| *Event 019-E* | standard | *translated since the emitter binds entry parameters and returns outputs:* `T2` accepts `'or'(left, right)` and stores both in `trigger_v_or_left`, `trigger_v_or_right`; the two regions' substate entries declare `in left = trigger_v_or_left; in right = trigger_v_or_right;` and assign the operation's two outputs (`out result`, `out 'return'`), each region's entry writing them in the order the regions are entered, so the caller receives the second region's values and the two admitted traces are both reached and nothing else |
| *Deferred 007* | extension | *translated since the emitter spells a returning effect with inputs:* the deferred `op(p1)` call fires `T4` once the second state is active; `T4`'s effect is `action def T4_effect` with `in p` bound to the accept's `p1` and `out 'return'`, its `return` an assignment of `not p`; the one admitted trace, the first state's exit, `T3(effect)`, `T4(effect)[in=true][out=false]` and the tester's `[out=false]`, is reached |
| *Standalone 003* | standard | *the standalone machine is read as the target since the reader does so, and translated since the emitter binds entry parameters and returns outputs:* the same shape as *Event 019-E*, the machine itself the class under test with `or` its operation; both admitted traces are reached |
| *Fork 002* | extension | *translated since finding 6 was fixed:* the fork enters the two regions of a nested composite state, which have no initial pseudostate; the lowerer used to refuse a `parallel` region with no `entry; then` — this project's gap, not v2's |
| *Join 001* | extension | *translated since finding 6 was fixed:* the fork enters the two regions of the top-level composite state, which have no initial pseudostate; the same lowerer refusal |
| *Fork 002* | extension | *translated:* the fork enters two regions without initial pseudostates; default entry enters their state stand-ins with no active child, and the fork activates its named targets |
| *Join 001* | extension | *translated:* the fork enters two regions without initial pseudostates; an omitted region's stand-in is entered with no active child and counts as complete on default entry |
| *Choice 005* | extension | *refused, settled:* the guards of the junction's and the choice's four outgoing transitions each call `trace("T1.n(guard)")` and the admitted trace records the calls, to show when each guard is read; a v2 guard is an expression with no room for an action, so the translation keeps only the guard's value and cannot reach the trace, and is refused rather than run short. Making the runtime's guard reads the referee's observable would not reach the trace either: the junction sits on the composite's default entry and the suite reads its guards before `T2(effect)` and the composite's entry, where the library reads a transition inside a state after the state's `entry` — see [A guard whose behavior acts on the model](#a-guard-whose-behavior-acts-on-the-model) |

The last two were kept apart from the other seven and from the 29 with no spelling: UML allows
a fork to target states inside orthogonal regions that have no initial pseudostate, SysML v2
`parallel` regions can spell the shape, and only the lowerer's check stood in the way. The
lowerer now accepts a region a fork enters (finding 6), so the two run and the referee reports
them in its expressible buckets; a region with neither an entry transition nor a fork branch
into it is still refused, and the classifier names that *lowerer refuses an orthogonal region
with neither an entry transition nor a fork branch into it* (*Entry 002 E*, which is not
expressible on other grounds too).
a fork to target states inside orthogonal regions that have no initial pseudostate, and SysML
v2 `parallel` regions can spell the shape. Default entry enters a region's state stand-in even
without an entry transition; if default entry leaves that stand-in with no active child, the
region counts as complete. A region with no stand-in and no start remains inactive and also
counts as complete on default entry. A later transition or fork branch that activates a region
does not make it complete; that region must reach its final state. PSSM *Entering 004* requires
immediate completion for the single-region default-entry case. Entry 002 E remains
not-expressible on its entry-point construct.

#### Behavior parameters, operation results, tester traces and standalone machines

Expand Down Expand Up @@ -2352,35 +2353,23 @@ sites of the runtime's fixed, the pool's order and the do step drawn on the entr
of them has to change (second open decision). `state_choice_pseudostate` does not reach it.
*Fixed* with the second open decision: the code now matches the note (SM30's *Decided*
sentence), and Track E records the finding as landed.
6. **The lowerer refuses a fork into orthogonal regions that have no initial pseudostate.**
UML lets a fork's outgoing transitions enter states inside a composite state's orthogonal
regions directly, with no initial pseudostate in those regions (PSSM *Fork 002* and *Join
001* are built this way); SysML v2 `parallel` regions can spell the shape, and this
project's `fork` extension can spell the fork. `lower.ToStateGraph` refuses it — "region
`<name>` has no initial state; write `entry; then <state>;` inside the region" — because it
required every region to name its own start even when a fork was the only way in. A gap of
ours, which the PSSM referee's classifier recorded as *lowerer refuses fork into a region
without an entry transition*. *Fixed:* `lower/fork_plan.go:planForks` reads every fork's
branches into a `ForkPlan` — one target state per orthogonal region of one composite state,
no guard, at least two branches — and `ToStateGraph` accepts a region with no entry
transition when `ForkStarted` says a fork enters it, still refusing one with neither
(`robustness_test.go:fork_leaves_a_region_without_a_way_in`). Such a region has no default
start, so `checkForkOnlyRegion` also refuses a machine where any other way into the composite
— a transition to the composite itself, to a state in another of its regions or to its
history, its own self-transition, or the machine's entry naming it, directly or through a
junction, choice or join — would start the region by default, naming that way in
(`robustness_test.go:fork_only_region_entered_by_default`). The runtime consumes the plan
(`state_executor.go:fireForkTransition` → `state_region_entry.go:enterForkBranches`): the
source configuration is left down to the least common ancestor of the source and the
composite, as for a move to a single state (`leaveForFork`), so an active ancestor is
neither exited nor entered again; then each branch runs its effect, enters what is left of
the way down to the composite and its target directly (PSSM §8.5.7), a region no branch
names taking its own initial. Pinned by `state_fork_enters_regions_without_initial`,
`state_fork_in_composite_enters_parallel_substate`, `state_fork_within_active_ancestor`,
`state_fork_within_active_region` (all with trace goldens) and `lower/fork_plan_test.go`.
*Fork 002* and *Join 001* translate and run; the branches are still entered in the regions'
declaration order, so the interleavings PSSM admits beyond that one are SM22's open decision,
and `docs/project/pssm-referee.md` records where each landed.
6. **A region left without an active child by default entry completes its composite.**
UML permits a region without an initial pseudostate, and SysML v2 `parallel` can represent
it. When `RegionState[region]` supplies a stand-in, `regionStart` enters it even without a
start transition in its body; if no child becomes active, the region is complete on default
entry. When no stand-in and no start exist, the region remains inactive and is likewise
complete on default entry. `enterRegion` records this default-entry case and
`enterStateInto` queues the composite's completion after its regions are entered. A region
activated later by a transition or fork branch is not default-complete and must reach its
final state; a running do-action still defers completion. PSSM *Entering 004* verifies the
single-region case. Coverage includes `state_parallel_region_without_entry_completes`,
`TestRuntimeRobustnessInactiveRegion`,
`TestRuntimeRobustnessParallelWrapperLeavesInnerRegionInactive`,
`state_parallel_stateless_region_with_behaviors`,
`state_parallel_region_without_entry_inactive`, `state_fork_only_region_entered_by_default`,
`state_nested_fork_starts_outer_region_inactive` and
`state_history_restores_inactive_parallel_region`. The PSSM classifier no longer reports
the old lowerer refusal; Entry 002 E remains not-expressible on its entry-point construct.
7. **A transition from a composite state into its own history pseudostate reads the record
before the state is left.** The configuration a history restores is written when its owner
is exited (`state_executor.go:exitState` → `recordChildHistory`, `recordRegionHistory`), but
Expand Down
6 changes: 3 additions & 3 deletions docs/project/pssm-referee-baseline.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,8 @@
"url": "https://www.omg.org/spec/PSSM/20181101/PSSM_TestSuite.xmi",
"suiteDigest": "c355b249c356774377a46b60345019d827af1ce417bde88e533aa5f39206ae07",
"tests": 103,
"recorded": "2026-10-03",
"develop": "3d45ca6fb200339461594cd086004e461633e6d1"
"recorded": "2026-10-10",
"develop": "02db635bed117511e859db8d90b4626dd5da1a85"
},
"buckets": {
"differs-by-design": 7,
Expand Down Expand Up @@ -806,7 +806,7 @@
"class": "not-expressible",
"bucket": "not-expressible",
"reasons": [
"entry point EntryPoint1; lowerer refuses an orthogonal region with neither an entry transition nor a fork branch into it S1/Region1; lowerer refuses an orthogonal region with neither an entry transition nor a fork branch into it S1/Region2; local transition T1.1; local transition T2.1"
"entry point EntryPoint1; local transition T1.1; local transition T2.1"
]
},
{
Expand Down
Loading
Loading