layover_core/validate/
mod.rs1mod agents;
11mod pipelines;
12mod prompts;
13mod reach;
14mod routes;
15mod wiring;
16
17use crate::config::Config;
18use crate::prompt::PromptSource;
19
20#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
22pub enum Severity {
23 Error,
25 Warning,
27}
28
29#[derive(Debug, Clone, PartialEq, Eq)]
31pub struct Diagnostic {
32 pub severity: Severity,
34 pub message: String,
36}
37
38impl Diagnostic {
39 pub(crate) fn error(message: impl Into<String>) -> Self {
41 Self {
42 severity: Severity::Error,
43 message: message.into(),
44 }
45 }
46
47 pub(crate) fn warning(message: impl Into<String>) -> Self {
49 Self {
50 severity: Severity::Warning,
51 message: message.into(),
52 }
53 }
54
55 #[must_use]
57 pub fn is_error(&self) -> bool {
58 self.severity == Severity::Error
59 }
60}
61
62#[must_use]
64pub fn has_errors(diagnostics: &[Diagnostic]) -> bool {
65 diagnostics.iter().any(Diagnostic::is_error)
66}
67
68#[must_use]
74pub fn validate(config: &Config) -> Vec<Diagnostic> {
75 let mut found = Vec::new();
76
77 routes::check_routes_name_known_agents(config, &mut found);
78 agents::check_runners_exist(config, &mut found);
79 agents::check_prompts_are_unambiguous(config, &mut found);
80 agents::check_agents_are_described(config, &mut found);
81 agents::check_fuel_is_usable(config, &mut found);
82 agents::check_reserve_window_is_usable(config, &mut found);
83 agents::check_reserve_cap_is_deliberate(config, &mut found);
84 agents::check_model_reaches_its_runner(config, &mut found);
85 reach::check_entry_points(config, &mut found);
86 routes::check_joins_are_unambiguous(config, &mut found);
87 routes::check_read_write_fan_out(config, &mut found);
88 routes::check_spawns_do_not_join(config, &mut found);
89 reach::check_every_agent_is_within_reach(config, &mut found);
90 pipelines::check_pipelines(config, &mut found);
91 wiring::check_mcp_and_workspaces(config, &mut found);
92
93 found
94}
95
96#[must_use]
101pub fn validate_prompts(config: &Config, source: &dyn PromptSource) -> Vec<Diagnostic> {
102 let mut found = Vec::new();
103 prompts::check_prompt_files(config, source, &mut found);
104 found
105}
106
107#[cfg(test)]
108pub(crate) mod testing {
109 use super::{Severity, validate};
110 use crate::config::Config;
111
112 pub(crate) const RUNNER: &str = r#"
113 [runners.claude]
114 command = ["claude", "-p", "{prompt}"]
115 "#;
116
117 pub(crate) fn parse(body: &str) -> Config {
118 Config::from_toml(&format!("{RUNNER}{body}"), "test.toml").expect("config parses")
119 }
120
121 pub(crate) fn messages(config: &Config, severity: Severity) -> Vec<String> {
122 validate(config)
123 .into_iter()
124 .filter(|d| d.severity == severity)
125 .map(|d| d.message)
126 .collect()
127 }
128
129 pub(crate) fn errors(config: &Config) -> Vec<String> {
130 messages(config, Severity::Error)
131 }
132
133 pub(crate) fn warnings(config: &Config) -> Vec<String> {
134 messages(config, Severity::Warning)
135 }
136
137 pub(crate) fn assert_mentions(found: &[String], needle: &str) {
138 assert!(
139 found.iter().any(|message| message.contains(needle)),
140 "expected a finding mentioning `{needle}`, got {found:?}"
141 );
142 }
143}
144
145#[cfg(test)]
146mod tests {
147 use super::testing::{assert_mentions, errors, parse};
148 use super::{Severity, has_errors, validate};
149
150 #[test]
151 fn a_minimal_factory_is_clean() {
152 let config = parse(
153 r#"
154 [agents.planner]
155 runner = "claude"
156 description = "Breaks a goal into tasks"
157 prompt = "plan"
158 entry = true
159
160 [agents.coder]
161 runner = "claude"
162 description = "Implements a task"
163 prompt = "code"
164
165 [[routes]]
166 from = "planner"
167 to = "coder"
168 "#,
169 );
170
171 assert_eq!(validate(&config), Vec::new());
172 }
173
174 #[test]
175 fn a_factory_with_no_agents_is_an_error() {
176 let config = parse("");
177
178 assert!(has_errors(&validate(&config)));
179 assert_mentions(&errors(&config), "defines no agents");
180 }
181
182 #[test]
183 fn severity_orders_errors_before_warnings() {
184 assert!(Severity::Error < Severity::Warning);
185 }
186}