1use crate::falsification::helpers::{apply_check_outcome, CheckOutcome};
13use crate::falsification::types::*;
14use std::path::Path;
15use std::time::Instant;
16
17pub fn evaluate_all(project_path: &Path) -> Vec<CheckItem> {
19 vec![
20 check_declarative_yaml(project_path),
21 check_zero_scripting(project_path),
22 check_pure_rust_testing(project_path),
23 check_wasm_first(project_path),
24 check_schema_validation(project_path),
25 ]
26}
27
28pub fn check_declarative_yaml(project_path: &Path) -> CheckItem {
36 let start = Instant::now();
37 let mut item = CheckItem::new(
38 "AI-01",
39 "Declarative YAML Configuration",
40 "Project offers full functionality via declarative YAML without code",
41 )
42 .with_severity(Severity::Critical)
43 .with_tps("Poka-Yoke — enable non-developers");
44
45 let yaml_patterns = ["*.yaml", "*.yml"];
47 let mut yaml_files = Vec::new();
48
49 for pattern in yaml_patterns {
50 if let Ok(entries) = glob::glob(&format!("{}/{}", project_path.display(), pattern)) {
51 for entry in entries.flatten() {
52 yaml_files.push(entry);
53 }
54 }
55 for dir in ["config", "configs"] {
57 if let Ok(entries) =
58 glob::glob(&format!("{}/{}/**/{}", project_path.display(), dir, pattern))
59 {
60 for entry in entries.flatten() {
61 yaml_files.push(entry);
62 }
63 }
64 }
65 }
66
67 let has_config_module = project_path.join("src/config.rs").exists()
69 || project_path.join("src/config/mod.rs").exists();
70
71 let has_examples = project_path.join("examples").exists() || !yaml_files.is_empty();
73
74 item = item.with_evidence(Evidence::file_audit(
75 format!("Found {} YAML config files", yaml_files.len()),
76 yaml_files.clone(),
77 ));
78
79 item = apply_check_outcome(
80 item,
81 &[
82 (has_config_module && has_examples, CheckOutcome::Pass),
83 (
84 has_config_module || !yaml_files.is_empty(),
85 CheckOutcome::Partial("Config module exists but examples incomplete"),
86 ),
87 (true, CheckOutcome::Fail("No declarative YAML configuration found")),
88 ],
89 );
90
91 item.finish_timed(start)
92}
93
94pub fn check_zero_scripting(project_path: &Path) -> CheckItem {
103 let start = Instant::now();
104 let mut item = CheckItem::new(
105 "AI-02",
106 "Zero Scripting in Production",
107 "No Python/JavaScript/Lua in production runtime paths",
108 )
109 .with_severity(Severity::Critical)
110 .with_tps("Jidoka — type safety, determinism");
111
112 let violations = find_glob_violations(
114 project_path,
115 &["src/**/*.py", "src/**/*.js", "src/**/*.ts", "src/**/*.lua", "src/**/*.rb"],
116 &["/target/", "/pkg/", ".wasm"],
117 );
118
119 let scripting_deps = super::helpers::find_scripting_deps(project_path);
121
122 item = item.with_evidence(Evidence::dependency_audit(
123 "Checked Cargo.toml for scripting runtimes".to_string(),
124 format!("Found: {:?}", scripting_deps),
125 ));
126
127 item = item.with_evidence(Evidence::file_audit(
128 format!("Found {} scripting files in src/", violations.len()),
129 violations.clone(),
130 ));
131
132 let fail_reasons = {
133 let mut r = Vec::new();
134 if !violations.is_empty() {
135 r.push(format!("{} scripting files in src/", violations.len()));
136 }
137 if !scripting_deps.is_empty() {
138 r.push(format!("Scripting deps: {:?}", scripting_deps));
139 }
140 r.join("; ")
141 };
142 item = apply_check_outcome(
143 item,
144 &[
145 (violations.is_empty() && scripting_deps.is_empty(), CheckOutcome::Pass),
146 (true, CheckOutcome::Fail(&fail_reasons)),
147 ],
148 );
149
150 item.finish_timed(start)
151}
152
153fn find_glob_violations(
155 project_path: &Path,
156 patterns: &[&str],
157 excludes: &[&str],
158) -> Vec<std::path::PathBuf> {
159 let mut results = Vec::new();
160 for pattern in patterns {
161 let Ok(entries) = glob::glob(&format!("{}/{}", project_path.display(), pattern)) else {
162 continue;
163 };
164 for entry in entries.flatten() {
165 let path_str = entry.to_string_lossy();
166 if !excludes.iter().any(|ex| path_str.contains(ex)) {
167 results.push(entry);
168 }
169 }
170 }
171 results
172}
173
174fn has_rust_tests(project_path: &Path) -> bool {
176 project_path.join("tests").exists()
177 || glob::glob(&format!("{}/src/**/*.rs", project_path.display()))
178 .ok()
179 .map(|entries| {
180 entries.flatten().any(|p| {
181 std::fs::read_to_string(&p)
182 .ok()
183 .is_some_and(|c| c.contains("#[test]") || c.contains("#[cfg(test)]"))
184 })
185 })
186 .unwrap_or(false)
187}
188
189pub fn check_pure_rust_testing(project_path: &Path) -> CheckItem {
198 let start = Instant::now();
199 let mut item = CheckItem::new(
200 "AI-03",
201 "Pure Rust Testing",
202 "All tests written in Rust, no external test frameworks",
203 )
204 .with_severity(Severity::Critical)
205 .with_tps("Zero scripting policy");
206
207 let mut violations = find_glob_violations(
208 project_path,
209 &[
210 "**/*.test.js",
211 "**/*.spec.js",
212 "**/*.test.ts",
213 "**/*.spec.ts",
214 "**/jest.config.*",
215 "**/vitest.config.*",
216 ],
217 &["node_modules"],
218 );
219
220 violations.extend(find_glob_violations(
221 project_path,
222 &["**/test_*.py", "**/*_test.py", "**/conftest.py", "**/pytest.ini", "**/pyproject.toml"],
223 &["venv", ".venv"],
224 ));
225
226 let package_json = project_path.join("package.json");
227 if let Ok(content) = std::fs::read_to_string(&package_json) {
228 if content.contains("\"test\"") || content.contains("\"jest\"") {
229 violations.push(package_json);
230 }
231 }
232
233 let node_modules = project_path.join("node_modules");
234 if node_modules.exists() {
235 violations.push(node_modules);
236 }
237
238 violations.extend(find_glob_violations(project_path, &["**/__pycache__"], &[]));
239
240 item = item.with_evidence(Evidence::file_audit(
241 format!("Found {} non-Rust test artifacts", violations.len()),
242 violations.clone(),
243 ));
244
245 let has_tests = has_rust_tests(project_path);
246 let fail_msg = format!(
247 "Found {} non-Rust test artifacts: {:?}",
248 violations.len(),
249 violations.iter().take(5).collect::<Vec<_>>()
250 );
251 item = apply_check_outcome(
252 item,
253 &[
254 (violations.is_empty() && has_tests, CheckOutcome::Pass),
255 (
256 violations.is_empty(),
257 CheckOutcome::Partial("No violations but no Rust tests detected"),
258 ),
259 (true, CheckOutcome::Fail(&fail_msg)),
260 ],
261 );
262
263 item.finish_timed(start)
264}
265
266fn is_excluded_js_path(path_str: &str) -> bool {
268 const EXCLUDED_DIRS: &[&str] =
269 &["node_modules", "/pkg/", "/dist/", "/target/", "/book/", "/book-output/", "/docs/"];
270 const EXCLUDED_PREFIXES: &[&str] =
271 &["target/", "pkg/", "dist/", "book/", "book-output/", "docs/"];
272
273 EXCLUDED_DIRS.iter().any(|d| path_str.contains(d))
274 || EXCLUDED_PREFIXES.iter().any(|p| path_str.starts_with(p))
275}
276
277fn find_js_files(project_path: &Path) -> Vec<std::path::PathBuf> {
279 let Ok(entries) = glob::glob(&format!("{}/**/*.js", project_path.display())) else {
280 return Vec::new();
281 };
282 entries.flatten().filter(|entry| !is_excluded_js_path(&entry.to_string_lossy())).collect()
283}
284
285fn detect_js_framework(project_path: &Path) -> bool {
287 let package_json = project_path.join("package.json");
288 let Ok(content) = std::fs::read_to_string(package_json) else {
289 return false;
290 };
291 ["react", "vue", "svelte", "angular", "next", "nuxt"].iter().any(|fw| content.contains(fw))
292}
293
294fn parse_wasm_cargo_info(project_path: &Path) -> (bool, bool) {
296 let cargo_toml = project_path.join("Cargo.toml");
297 let Ok(content) = std::fs::read_to_string(cargo_toml) else {
298 return (false, false);
299 };
300 let has_feature = content.contains("wasm") || content.contains("web");
301 let has_bindgen = content.contains("wasm-bindgen")
302 || content.contains("wasm-pack")
303 || content.contains("web-sys");
304 (has_feature, has_bindgen)
305}
306
307pub fn check_wasm_first(project_path: &Path) -> CheckItem {
316 let start = Instant::now();
317 let mut item = CheckItem::new(
318 "AI-04",
319 "WASM-First Browser Support",
320 "Browser functionality via WASM, not JavaScript",
321 )
322 .with_severity(Severity::Critical)
323 .with_tps("Zero scripting, sovereignty");
324
325 let (has_wasm_feature, has_wasm_bindgen) = parse_wasm_cargo_info(project_path);
326 let has_wasm_module =
327 project_path.join("src/wasm.rs").exists() || project_path.join("src/lib.rs").exists();
328 let js_files = find_js_files(project_path);
329 let has_js_framework = detect_js_framework(project_path);
330
331 item = item.with_evidence(Evidence::file_audit(
332 format!(
333 "WASM: feature={}, bindgen={}, JS files={}",
334 has_wasm_feature,
335 has_wasm_bindgen,
336 js_files.len()
337 ),
338 js_files.clone(),
339 ));
340
341 let too_many_js_msg = format!("Too many JS files ({}) beyond WASM glue", js_files.len());
342 let wasm_partial_msg = format!("WASM support exists but {} JS files found", js_files.len());
343 let has_wasm_support = has_wasm_bindgen || has_wasm_feature;
344 item = apply_check_outcome(
345 item,
346 &[
347 (
348 has_js_framework,
349 CheckOutcome::Fail("JavaScript framework detected (React/Vue/Svelte)"),
350 ),
351 (js_files.len() > 5, CheckOutcome::Fail(&too_many_js_msg)),
352 (has_wasm_support && js_files.is_empty(), CheckOutcome::Pass),
353 (has_wasm_support, CheckOutcome::Partial(&wasm_partial_msg)),
354 (
355 has_wasm_module && js_files.is_empty(),
356 CheckOutcome::Partial("No explicit WASM feature but no JS violations"),
357 ),
358 (true, CheckOutcome::Fail("No WASM support detected")),
359 ],
360 );
361
362 item.finish_timed(start)
363}
364
365pub fn check_schema_validation(project_path: &Path) -> CheckItem {
374 let start = Instant::now();
375 let mut item = CheckItem::new(
376 "AI-05",
377 "Declarative Schema Validation",
378 "YAML configs validated against typed schema",
379 )
380 .with_severity(Severity::Critical)
381 .with_tps("Poka-Yoke — prevent config errors");
382
383 let schema = super::helpers::detect_schema_deps(project_path);
385 let has_serde = schema.has_serde;
386 let has_serde_yaml = schema.has_serde_yaml;
387 let has_validator = schema.has_validator;
388
389 let has_config_struct = super::helpers::has_deserialize_config_struct(project_path);
391
392 let has_json_schema = glob::glob(&format!("{}/**/*.schema.json", project_path.display()))
394 .ok()
395 .map(|mut entries| entries.next().is_some())
396 .unwrap_or(false);
397
398 item = item.with_evidence(Evidence::schema_validation(
399 format!(
400 "serde={}, yaml={}, validator={}, config_struct={}, json_schema={}",
401 has_serde, has_serde_yaml, has_validator, has_config_struct, has_json_schema
402 ),
403 format!(
404 "Schema validation: {}",
405 if has_config_struct || has_json_schema { "PRESENT" } else { "MISSING" }
406 ),
407 ));
408
409 let has_full_serde = has_serde && has_serde_yaml && has_config_struct;
410 item = apply_check_outcome(
411 item,
412 &[
413 (has_full_serde && (has_validator || has_json_schema), CheckOutcome::Pass),
414 (
415 has_full_serde,
416 CheckOutcome::Partial("Basic serde validation but no explicit validator"),
417 ),
418 (
419 has_serde && has_config_struct,
420 CheckOutcome::Partial("Config struct exists but YAML support unclear"),
421 ),
422 (true, CheckOutcome::Fail("No typed schema validation for configs")),
423 ],
424 );
425
426 item.finish_timed(start)
427}
428
429#[cfg(test)]
430#[path = "invariants_tests.rs"]
431mod tests;