Skip to content

feat(graph): add statum-graph export crate#13

Merged
eboody merged 9 commits intomainfrom
feat/statum-graph-export
Mar 25, 2026
Merged

feat(graph): add statum-graph export crate#13
eboody merged 9 commits intomainfrom
feat/statum-graph-export

Conversation

@eboody
Copy link
Owner

@eboody eboody commented Mar 25, 2026

Summary

Add a new statum-graph crate that exports machine-local topology from statum::MachineIntrospection::GRAPH and renders it as Mermaid.

This includes:

  • a MachineDoc export surface for machine, state, edge, and root metadata
  • a Mermaid renderer for exported machine-local topology
  • tests for linear flows, exact branch targets, multiple roots, zero-root cycles, and macro-generated transitions
  • workspace and docs updates for the new crate

Authority boundary

Claimed authority surface:

  • machine identity
  • states
  • transition sites
  • exact legal targets
  • roots derivable from the static graph itself

Actual observation point:

  • the static MachineIntrospection::GRAPH value emitted by Statum introspection

Unsupported / intentionally out of scope:

  • runtime-selected branch choice for one execution
  • orchestration order across machines
  • consumer-owned explanation or presentation metadata

Adversarial coverage in this PR:

  • macro-generated transition sites in statum-graph/tests/export.rs

Verification

  • cargo test -p statum-graph --offline
  • cargo clippy -p statum-graph --offline --all-targets --all-features -- -D warnings
  • RUSTDOCFLAGS='-D warnings' cargo doc -p statum-graph --offline --no-deps
  • cargo test --workspace --offline
  • cargo clippy --workspace --offline --all-targets --all-features -- -D warnings
  • RUSTDOCFLAGS='-D warnings' cargo doc --workspace --offline --no-deps

Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds a new statum-graph workspace crate that exports machine-local topology from statum::MachineIntrospection::GRAPH into a MachineDoc structure and renders it as Mermaid, with accompanying tests and documentation updates across the repo.

Changes:

  • Introduce statum-graph crate with MachineDoc export API and Mermaid renderer.
  • Add integration tests + insta snapshot coverage for branching, roots, cycles, and macro-generated transitions.
  • Update workspace membership, publish script, and documentation to reference statum-graph.

Reviewed changes

Copilot reviewed 10 out of 10 changed files in this pull request and generated 4 comments.

Show a summary per file
File Description
Cargo.toml Adds statum-graph to workspace members and publish script.
README.md Mentions statum-graph as a ready-made renderer for MachineIntrospection::GRAPH.
docs/introspection.md Adds a pointer to statum-graph for exporting/rendering the static graph surface.
statum/README.md Notes statum-graph companion crate for topology + Mermaid output.
statum-graph/Cargo.toml New crate manifest (depends on workspace statum, adds insta for snapshots).
statum-graph/README.md New crate README with install + example usage.
statum-graph/src/lib.rs Defines MachineDoc / StateDoc / EdgeDoc and graph export + sorting/root derivation.
statum-graph/src/render.rs Implements Mermaid rendering for an exported MachineDoc.
statum-graph/tests/export.rs Integration tests covering linear, branching, multiple roots, zero-root cycles, and macro transitions.
statum-graph/tests/snapshots/export__branching_flow_mermaid.snap Snapshot for stable Mermaid output of a reconverging branching graph.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +24 to +28
pub struct MachineDoc<S: 'static, T: 'static> {
/// Descriptor for the exported machine family.
pub machine: MachineDescriptor,
/// Exported states in the same order as the underlying static graph.
pub states: Vec<StateDoc<S>>,
Copy link

Copilot AI Mar 25, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

MachineDoc exposes machine/states/edges as public fields, so callers can construct or mutate an inconsistent document (e.g., an edge referencing a state id not present in states). Downstream code (e.g., the Mermaid renderer) assumes these invariants and may panic; consider making fields private and exposing read-only accessors/constructors, or providing a validated try_* constructor.

Copilot uses AI. Check for mistakes.
Comment on lines +35 to +37
let from = node_id(state_positions[&edge.descriptor.from]);
for target in edge.descriptor.to {
let to = node_id(state_positions[target]);
Copy link

Copilot AI Mar 25, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These state_positions[...] lookups will panic if an edge references a state id missing from doc.states (possible since MachineDoc is publicly constructible/mutable). Prefer using get() with a descriptive expect, or make the renderer return a Result so invalid docs are reported without an unhelpful panic.

Suggested change
let from = node_id(state_positions[&edge.descriptor.from]);
for target in edge.descriptor.to {
let to = node_id(state_positions[target]);
let from_index = state_positions
.get(&edge.descriptor.from)
.expect("invalid MachineDoc: edge 'from' state id not present in states");
let from = node_id(*from_index);
for target in edge.descriptor.to {
let to_index = state_positions
.get(&target)
.expect("invalid MachineDoc: edge 'to' state id not present in states");
let to = node_id(*to_index);

Copilot uses AI. Check for mistakes.
Comment on lines +138 to +140
state_positions[&left.descriptor.from]
.cmp(&state_positions[&right.descriptor.from])
.then_with(|| {
Copy link

Copilot AI Mar 25, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

state_positions[&left.descriptor.from] / state_positions[&right.descriptor.from] will panic if graph.transitions contains a from id that is not present in graph.states. Since from_graph is public, consider validating graph consistency or using get().expect("...") to provide a clearer failure mode.

Copilot uses AI. Check for mistakes.
Comment on lines +156 to +157
let left = left.iter().map(|state| state_positions[state]);
let right = right.iter().map(|state| state_positions[state]);
Copy link

Copilot AI Mar 25, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

compare_targets indexes state_positions[state], which will panic if any transition target id is missing from graph.states. Consider switching to get() with a descriptive expect, or validating the graph once up-front in from_graph so sorting can’t panic.

Suggested change
let left = left.iter().map(|state| state_positions[state]);
let right = right.iter().map(|state| state_positions[state]);
let left = left.iter().map(|state| {
*state_positions
.get(state)
.expect("compare_targets: transition target id missing from state_positions")
});
let right = right.iter().map(|state| {
*state_positions
.get(state)
.expect("compare_targets: transition target id missing from state_positions")
});

Copilot uses AI. Check for mistakes.
Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 10 out of 10 changed files in this pull request and generated 1 comment.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +176 to +195
fn validate_graph<S, T>(graph: &MachineGraph<S, T>) -> Result<(), MachineDocError>
where
S: Copy + Eq + 'static,
T: Copy + Eq + 'static,
{
for transition in graph.transitions.iter() {
if graph.state(transition.from).is_none() {
return Err(MachineDocError::MissingSourceState {
machine: graph.machine.rust_type_path,
transition: transition.method_name,
});
}

if transition
.to
.iter()
.copied()
.any(|target| graph.state(target).is_none())
{
return Err(MachineDocError::MissingTargetState {
Copy link

Copilot AI Mar 25, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

validate_graph verifies that each transition’s source/targets exist, but it doesn’t validate that graph.states contains unique state ids. If an external MachineGraph supplies duplicate state ids, state_positions()/Mermaid rendering can become inconsistent (HashMap overwrites one entry and edges may point at the “wrong” duplicate). Consider extending validation to reject duplicate state ids (e.g., track seen ids in a HashSet) and reuse that set for O(1) membership checks instead of repeatedly calling graph.state(...) in the inner loops.

Copilot uses AI. Check for mistakes.
Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 10 out of 10 changed files in this pull request and generated 1 comment.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +38 to +41
lines.push(format!(
" {from} -->|{}| {to}",
escape_label(edge.descriptor.method_name)
));
Copy link

Copilot AI Mar 25, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

render::mermaid inserts edge.descriptor.method_name inside the Mermaid edge-label delimiter (-->|...|). escape_label currently doesn’t escape |, so an externally supplied TransitionDescriptor.method_name containing | will produce invalid Mermaid (and can break downstream renderers). Consider either escaping | (and other Mermaid edge-label metacharacters) for edge labels specifically, or switching to a Mermaid syntax that safely quotes/encodes labels.

Copilot uses AI. Check for mistakes.
Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 10 out of 10 changed files in this pull request and generated 1 comment.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +269 to +277
let mut transition_ids = Vec::with_capacity(transitions.len());
for transition in transitions.iter() {
if transition_ids.contains(&transition.id) {
return Err(MachineDocError::DuplicateTransitionId {
machine: machine.rust_type_path,
transition: transition.method_name,
});
}
transition_ids.push(transition.id);
Copy link

Copilot AI Mar 25, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

validate_graph detects duplicate transition ids by pushing ids into a Vec and calling contains for every transition, which makes this check O(n²) in the number of transitions. Consider switching to a set-based approach (e.g., a HashSet if you can require T: Hash, or another structure that avoids repeated linear scans) to keep export/validation time predictable for large graphs.

Suggested change
let mut transition_ids = Vec::with_capacity(transitions.len());
for transition in transitions.iter() {
if transition_ids.contains(&transition.id) {
return Err(MachineDocError::DuplicateTransitionId {
machine: machine.rust_type_path,
transition: transition.method_name,
});
}
transition_ids.push(transition.id);
let mut transition_ids = HashSet::with_capacity(transitions.len());
for transition in transitions.iter() {
if !transition_ids.insert(transition.id) {
return Err(MachineDocError::DuplicateTransitionId {
machine: machine.rust_type_path,
transition: transition.method_name,
});
}

Copilot uses AI. Check for mistakes.
@eboody eboody merged commit 3da4bac into main Mar 25, 2026
11 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants