fn validate_state_machine_warnings(machine: &StateMachine, report: &mut ValidationReport) {
for (state_name, state) in &machine.states {
if state.gating && state.agent.is_some() {
report.warnings.push(format!(
"state '{state_name}' declares 'agent' on a gating state; gating states are human-only, so rhei run will not invoke this agent"
));
}
warn_on_supervising_state(machine, state_name, state, report);
warn_on_unbounded_self_loop(machine, state_name, state, report);
}
}
fn warn_on_unbounded_self_loop(
machine: &StateMachine,
state_name: &str,
state: &StateDef,
report: &mut ValidationReport,
) {
if state.poll.is_some() || state.visits.is_some() {
return;
}
let outgoing = || machine.transitions.iter().filter(|rule| rule.from.0 == *state_name);
if !outgoing().any(|rule| rule.to.0 == *state_name) {
return;
}
let has_counted_exit = outgoing().any(|rule| {
rule.to.0 != *state_name
&& rule.condition.as_deref().is_some_and(|cond| cond.contains("visitCount"))
});
if !has_counted_exit {
report.warnings.push(format!(
"state '{state_name}' has a self-loop but declares neither 'visits' nor a transition \
bounded by `visitCount`; nothing ends the loop, so a run may re-enter it forever"
));
}
}
fn warn_on_supervising_state(
machine: &StateMachine,
state_name: &str,
state: &StateDef,
report: &mut ValidationReport,
) {
if state.execute_on().is_none() {
return;
}
let outgoing = || machine.transitions.iter().filter(|rule| rule.from.0 == *state_name);
let has_open_descendants_exit = outgoing().any(|rule| {
rule.to.0 != *state_name
&& machine.states.get(&rule.to.0).map(|def| def.terminal).unwrap_or(false)
&& rule.condition.as_deref().is_some_and(|cond| cond.contains("openDescendants"))
});
if !has_open_descendants_exit {
report.warnings.push(format!(
"state '{state_name}' declares 'execute_on' but no transition from it reaches a final \
state on `openDescendants`; the supervisor has no way to finish"
));
}
let has_exhaustion_edge = outgoing().any(|rule| {
rule.to.0 != *state_name
&& rule.condition.as_deref().is_some_and(|cond| cond.contains("visitCount"))
});
if state.visits.is_none() && !has_exhaustion_edge {
report.warnings.push(format!(
"state '{state_name}' declares 'execute_on' but neither 'visits' nor an exhaustion \
transition on `visitCount`; a subtree that never converges has no safety valve"
));
}
}