feat: Mermaid diagram export (B4) + initial-state arm fix#29
Merged
Conversation
… B3 follow-up) Brainstormed B4 (Mermaid diagram export, opt-in via Diagram=true) and the B3 follow-up (initial-state timer arm gap documented as a caveat in v1.4) together since both land in StateMachineWriter / StateMachineGroupWriter and share a release cycle. Key decisions locked: Mermaid-only emission, public const string surface, opt-in attribute property, full-fidelity rendering (composites nested, parts grouped, timed annotated, guards labeled), arm helper invoked from ctor + Reset + ResetTo, partial-void HookConstructor hook with ZSM0021 diagnostic for user-declared ctors that forget to call it. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…B follow-up)
21-task TDD plan walking through:
- Attribute Diagram property (Tasks 1, 3-4)
- Diagnostic descriptors ZSM0020 + ZSM0021 (Task 2)
- ZSM0020 detection (Task 5)
- MermaidDiagramWriter: flat -> composite -> history -> groups (Tasks 6-9)
- Wiring into single-machine + group writers (Tasks 10-11)
- ArmInitialStateTimers helper + HookConstructor partial + default ctor
in StateMachineWriter (Tasks 12-14)
- Per-part arm helpers + group ctor in StateMachineGroupWriter (Task 15)
- ZSM0021 detection (Task 16)
- Snapshot, diagnostic, and runtime tests (Tasks 17-19)
- Docs + PR (Tasks 20-21)
Each task lists exact files, code, build/test commands, snapshot-regen
notes, and commit message. Follows the same shape as prior plans.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…(B4 runtime surface) Two new opt-in boolean properties on the top-level attributes. When set to true, the generator (next commits) emits a public const string MermaidDiagram containing the FSM's stateDiagram-v2 rendering. Default false — existing v1.4 declarations are strictly additive. Generator wiring lands in subsequent commits. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…Invocation) descriptors ZSM0020 (Warning): fires when [StateMachine(Diagram = true)] is declared on a class with zero transitions, since the emitted MermaidDiagram would be empty. ZSM0021 (Error): fires when a class has timed transitions AND a user-declared constructor that does not call HookConstructor(), preventing initial-state timer arming. RS1032 required a trailing period on ZSM0021's messageFormat (it has interior '.'), matching the v1.4 deviation pattern. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds a bool Diagram positional param to StateMachineModel and StateMachineGroupModel. Parse / ParseGroup pass false for now; the attribute-named-arg read lands in Task 4. Existing snapshots and runtime tests are byte-identical - Diagram = false means no emit change. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…chineGroup] Reads the Diagram boolean from each top-level attribute and threads it into the corresponding model. Defaults to false when absent. No writer changes yet - emit-when-true lands in Tasks 10 + 11. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Fires when Diagram = true is declared on a class with zero transitions (or a group with no transitions across all parts). The emitted MermaidDiagram would be empty / useless. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Initial implementation covers the flat-machine case: - stateDiagram-v2 header - [*] --> InitialState marker - From --> To: Trigger transitions - (after Nms) annotation for timed edges - [guard] annotation for When = true - Terminal --> [*] markers for [Terminal] Composite nesting, history pseudo-states, and group rendering land in subsequent commits. Writer is currently unwired — emit-when-Diagram=true lands in Task 10. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…rmaidDiagramWriter
Composite states render as Mermaid 'state X { ... }' blocks containing
the sub-FSM's transitions / terminals / further-nested composites. The
sub-FSM model is resolved by walking the sub-machine's [StateMachine] +
[Transition] attributes via a new BuildModelFromSymbol helper.
Recursive: a sub-FSM that itself has composites renders correctly.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
… blocks For any composite state that also has a matching [HistoryState] declaration, the diagram includes a 'state H as History' pseudo-state line at the top of the composite's nested block. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
… in MermaidDiagramWriter
Each [StateMachinePart] becomes a 'state {Name} { ... }' block at the
top level of the stateDiagram-v2. Per-part initial state + transitions
+ timed annotations + guards render the same way as flat-machine emit.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…en Diagram=true The generator pipeline now combines the per-class model with the CompilationProvider so the diagram writer can resolve composite sub-FSM types to their parsed models for nested rendering. The diagram is emitted as a public const string MermaidDiagram using a verbatim @"..." literal for netstandard2.0 compatibility. Caching note: Combine with CompilationProvider partially invalidates incremental caching for [StateMachine] classes; the cost is paid only when Diagram = true (existing tests still pass byte-identical). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…er when Diagram=true Groups don't have composites (ZSM0018 forbids them), so no sub-machine resolver is needed; the group writer calls MermaidDiagramWriter.Write directly with the group model. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
A private method emitted on every concurrent class with timed edges. Walks all timed edges; for each whose From matches Current, arms the timer using the existing race-safe Interlocked.CompareExchange + dispose-of-loser pattern. Snapshot regen: existing v1.4 SingleTimedEdge / MultipleTimedEdges snapshots gained the new method. Inspected and verified. Tasks 13-14 invoke this helper from the ctor and from Reset/ResetTo. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…riter
For every concurrent class with at least one timed edge, the generator emits:
- private void HookConstructor() — calls ArmInitialStateTimers().
- public {ClassName}() — calls HookConstructor() — only when the user
has NOT declared their own ctor (detected via type.InstanceConstructors).
If the user declares a ctor, they must invoke HookConstructor() themselves
or hit ZSM0021 (Task 16).
Snapshot regen: v1.4 SingleTimedEdge / MultipleTimedEdges now include the
new ctor + hook in the emit.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Both internal state-population methods now call ArmInitialStateTimers() after assigning state. This closes the v1.4 caveat: any path that lands on a state with a timed edge arms that edge's timer. Snapshot regen: ArmInitialStateTimers() call now appears in the Reset and ResetTo bodies in the v1.4 timed-edge snapshots. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…StateMachineGroupWriter
For each part with at least one timed edge: a private
ArmInitialStateTimers_{PartName}() helper using the race-safe
lazy-init pattern. Plus a group-level HookConstructor that calls
each timed part's helper, and a default ctor invoking HookConstructor
when no user ctor exists.
Snapshot regen: TwoPartsOneTimedEdge gains the new arm helpers + ctor.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…structor) Syntactic walk of every user-declared ctor's body looking for an HookConstructor() invocation. If the class has at least one timed edge AND a user ctor AND none of the user ctors invoke HookConstructor(), fire ZSM0021. Best-effort: indirect invocations (via helper methods) are not detected; users can #pragma warning disable ZSM0021 for those edge cases. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Three scenarios:
- Flat_Diagram: initial + transitions + guard-annotated + terminal
- Composite_Diagram: parent diagram includes nested 'state X { ... }'
block with 'state H as History' for the [HistoryState]
- Group_Diagram: top-level 'state Op { ... }' + 'state Conn { ... }'
Snapshots assert correct Mermaid stateDiagram-v2 syntax, including
sub-FSM walking via the new BuildModelFromSymbol helper.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
ZSM0020: fires when [StateMachine(Diagram = true)] has no transitions. ZSM0020: fires when [StateMachineGroup(Diagram = true)] has no parts. ZSM0021: fires when user-declared ctor doesn't call HookConstructor(). ZSM0021: negative -- does NOT fire when user ctor invokes HookConstructor(). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Two scenarios: - Constructor arms initial-state timer; no user TryFire needed. - Reset() re-arms after returning to the initial state. Reset is internal; the test uses reflection to invoke it. Group runtime coverage lands in a future test if a real consumer asks. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- Removed the Caveats section from timeout-transitions.md (no longer
a caveat after the initial-arm fix).
- New core-concepts/diagram-export.md page describing Diagram = true,
the MermaidDiagram const surface, and what gets rendered.
- New diagnostics pages for ZSM0020 + ZSM0021.
- attributes.md adds Diagram entries on [StateMachine] + [StateMachineGroup].
- index.md adds the new pages to the top-level TOC.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…kConstructor() The CtorInvokesHookConstructor syntactic walk only matched bare IdentifierNameSyntax. Users writing the idiomatic this.HookConstructor() hit a false-positive ZSM0021. Extended the match to also accept MemberAccessExpressionSyntax where the receiver is ThisExpressionSyntax or BaseExpressionSyntax and the name is 'HookConstructor'. Added a negative test covering this.HookConstructor() to lock the fix. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This was referenced May 23, 2026
MarcelRoozekrans
added a commit
that referenced
this pull request
May 24, 2026
The original post-v1 graduation backlog (B1 composite states, B2 shallow history, B3 timeout transitions, B4 Mermaid diagram, B5 concurrent state parts) fully shipped across PRs #25, #27, and #29. The backlog file hadn't been updated to reflect this — now it has. Each entry now carries a brief 'Shipped:' note pointing at the relevant attribute / generator file / test file so a reader can navigate from the backlog item to the implementation. No new backlog items added — future graduation candidates land here when real-world friction surfaces them.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Closes out the StateMachine roadmap with two threads:
B4 — Mermaid diagram export. Opt-in via
[StateMachine(Diagram = true)]/[StateMachineGroup(Diagram = true)]. The generator emits apublic const string MermaidDiagramwith a full MermaidstateDiagram-v2rendering: composites nested, history pseudo-states, group parts as top-level state blocks, timed edges annotated(after Nms), guards labeled[guard], terminals as--> [*].Initial-state arm follow-up (closes the v1.4 caveat). Timers now arm at construction, on every entry into the source state, and on
Reset()/ResetTo(state). The generator emitsprivate void HookConstructor()+ a default ctor when the user hasn't declared one; ZSM0021 fires if a user-declared ctor doesn't invokeHookConstructor()(accepts bare-identifier,this., andbase.qualified forms).Two new diagnostics:
ZSM0020(warning: empty diagram request),ZSM0021(error: missing HookConstructor invocation).Design doc:
docs/plans/2026-05-23-mermaid-export-and-initial-arm-design.md.Plan:
docs/plans/2026-05-23-mermaid-export-and-initial-arm.md.Test plan
ArmInitialStateTimers+HookConstructor+ default ctor emit; visually inspectedKnown trade-offs
models.Combine(CompilationProvider)to resolve composite sub-FSMs for nested diagrams. Partially invalidates incremental caching — acceptable cost paid only whenDiagram = true.🤖 Generated with Claude Code