///|
/// Structural facts used to review a workflow before it is deployed.
pub(all) struct GraphSummary[S] {
state_count : Int
transition_count : Int
terminal_states : Array[S]
branching_states : Array[S]
reachable_states : Array[S]
unreachable_states : Array[S]
has_cycle : Bool
maximum_exploration_depth : Int
} derive(Eq)
///|
fn[S : Eq, E, Ctx] outgoing_count(
builder : Builder[S, E, Ctx],
state : S,
) -> Int {
let mut count = 0
for transition in builder.config.transitions {
if transition.from == state {
count += 1
}
}
count
}
///|
fn[S : Hash + Eq, E, Ctx] reachable_from(
builder : Builder[S, E, Ctx],
start : S,
target : S,
) -> Bool {
let queue = [start]
let seen = Map([])
seen.set(start, true)
let mut head = 0
while head < queue.length() {
let current = queue[head]
head += 1
if current == target && head > 1 {
return true
}
for transition in builder.config.transitions {
if transition.from == current && !seen.contains(transition.to) {
seen.set(transition.to, true)
queue.push(transition.to)
}
}
}
false
}
///|
fn[S : Hash + Eq, E, Ctx] has_cycle(builder : Builder[S, E, Ctx]) -> Bool {
for transition in builder.config.transitions {
if reachable_from(builder, transition.to, transition.from) {
return true
}
}
false
}
///|
fn[S : Hash + Eq, E, Ctx] exploration_depth(
builder : Builder[S, E, Ctx],
initial : S,
) -> Int {
let queue : Array[(S, Int)] = [(initial, 0)]
let seen = Map([])
seen.set(initial, true)
let mut head = 0
let mut deepest = 0
while head < queue.length() {
let (current, depth) = queue[head]
head += 1
if depth > deepest {
deepest = depth
}
for transition in builder.config.transitions {
if transition.from == current && !seen.contains(transition.to) {
seen.set(transition.to, true)
queue.push((transition.to, depth + 1))
}
}
}
deepest
}
///|
/// Computes graph shape, reachability, branching, cycles, and terminal states.
pub fn[S : Hash + Eq, E, Ctx] graph_summary(
builder : Builder[S, E, Ctx],
initial_state : S,
) -> GraphSummary[S] {
let all_states = collect_states(builder)
let reachable = reachable_states(builder, initial_state)
let disconnected = find_unreachable_states(builder, initial_state)
let terminal = []
let branching = []
for state in all_states {
let count = outgoing_count(builder, state)
if count == 0 {
terminal.push(state)
}
if count > 1 {
branching.push(state)
}
}
{
state_count: all_states.length(),
transition_count: builder.config.transitions.length(),
terminal_states: terminal,
branching_states: branching,
reachable_states: reachable,
unreachable_states: disconnected,
has_cycle: has_cycle(builder),
maximum_exploration_depth: exploration_depth(builder, initial_state),
}
}
///|
/// Produces a stable integer risk score for dashboards and review gates.
/// The score rewards reachable structure and penalizes unreachable states,
/// duplicate definitions, and terminal states that are not explicitly expected.
pub fn[S : Hash + Eq, E : Eq, Ctx] workflow_risk_score(
builder : Builder[S, E, Ctx],
initial_state : S,
) -> Int {
let report = validate_report(builder, initial_state)
let summary = graph_summary(builder, initial_state)
let mut score = summary.state_count + summary.transition_count
score -= report.unreachable_states.length() * 3
score -= report.duplicate_transitions.length() * 5
score -= report.dead_end_states.length()
if summary.has_cycle {
score += 2
}
if score < 0 {
0
} else {
score
}
}
///|
/// Returns states with more than one outgoing event, useful for manual review.
pub fn[S : Hash + Eq, E, Ctx] branching_states(
builder : Builder[S, E, Ctx],
) -> Array[S] {
graph_summary(builder, collect_states(builder).get(0).unwrap()).branching_states
}