1use std::path::Path;
12use std::time::Instant;
13
14use super::helpers::{apply_check_outcome, CheckOutcome};
15use super::types::{CheckItem, CheckStatus, Evidence, EvidenceType, Severity};
16
17fn any_source_matches(project: &Path, glob_suffix: &str, pred: impl Fn(&str) -> bool) -> bool {
19 glob::glob(&format!("{}/{glob_suffix}", project.display()))
20 .ok()
21 .map(|entries| {
22 entries
23 .flatten()
24 .any(|p| std::fs::read_to_string(&p).ok().map(|c| pred(&c)).unwrap_or(false))
25 })
26 .unwrap_or(false)
27}
28
29pub fn evaluate_all(project_path: &Path) -> Vec<CheckItem> {
31 vec![
32 check_precommit_hooks(project_path),
33 check_automated_sovereignty_linting(project_path),
34 check_data_drift_circuit_breaker(project_path),
35 check_performance_regression_gate(project_path),
36 check_fairness_metric_circuit_breaker(project_path),
37 check_latency_sla_circuit_breaker(project_path),
38 check_memory_footprint_gate(project_path),
39 check_security_scan_gate(project_path),
40 check_license_compliance_gate(project_path),
41 check_documentation_gate(project_path),
42 ]
43}
44
45pub fn check_precommit_hooks(project_path: &Path) -> CheckItem {
52 let start = Instant::now();
53 let mut item = CheckItem::new(
54 "JA-01",
55 "Pre-Commit Hook Enforcement",
56 "Pre-commit hooks catch basic issues locally",
57 )
58 .with_severity(Severity::Major)
59 .with_tps("Jidoka — early detection");
60
61 let precommit_yaml = project_path.join(".pre-commit-config.yaml");
63 let has_precommit = precommit_yaml.exists();
64
65 let git_hooks = project_path.join(".git/hooks");
67 let has_git_hooks = git_hooks.exists()
68 && (git_hooks.join("pre-commit").exists() || git_hooks.join("pre-push").exists());
69
70 let husky_dir = project_path.join(".husky");
72 let has_husky = husky_dir.exists();
73
74 let cargo_toml = project_path.join("Cargo.toml");
76 let has_cargo_hooks = cargo_toml
77 .exists()
78 .then(|| std::fs::read_to_string(&cargo_toml).ok())
79 .flatten()
80 .map(|c| c.contains("cargo-husky") || c.contains("[hooks]"))
81 .unwrap_or(false);
82
83 let makefile = project_path.join("Makefile");
85 let has_make_hooks = makefile
86 .exists()
87 .then(|| std::fs::read_to_string(&makefile).ok())
88 .flatten()
89 .map(|c| c.contains("pre-commit") || c.contains("precommit") || c.contains("tier1"))
90 .unwrap_or(false);
91
92 item = item.with_evidence(Evidence {
93 evidence_type: EvidenceType::StaticAnalysis,
94 description: format!(
95 "Pre-commit: yaml={}, git_hooks={}, husky={}, cargo_hooks={}, make_targets={}",
96 has_precommit, has_git_hooks, has_husky, has_cargo_hooks, has_make_hooks
97 ),
98 data: None,
99 files: Vec::new(),
100 });
101
102 item = apply_check_outcome(
103 item,
104 &[
105 (has_precommit || has_cargo_hooks, CheckOutcome::Pass),
106 (
107 has_git_hooks || has_husky || has_make_hooks,
108 CheckOutcome::Partial("Pre-commit configured but not standardized"),
109 ),
110 (true, CheckOutcome::Fail("No pre-commit hooks configured")),
111 ],
112 );
113
114 item.finish_timed(start)
115}
116
117pub fn check_automated_sovereignty_linting(project_path: &Path) -> CheckItem {
124 let start = Instant::now();
125 let mut item = CheckItem::new(
126 "JA-02",
127 "Automated Sovereignty Linting",
128 "Static analysis catches sovereignty violations",
129 )
130 .with_severity(Severity::Major)
131 .with_tps("Jidoka — automated sovereignty check");
132
133 let clippy_toml = project_path.join("clippy.toml");
135 let has_clippy_config = clippy_toml.exists();
136
137 let has_clippy_ci = check_ci_for_content(project_path, "clippy");
139
140 let has_custom_lints = any_source_matches(project_path, "src/**/*.rs", |c| {
142 c.contains("#[deny(")
143 || c.contains("#![deny(")
144 || c.contains("#[warn(")
145 || c.contains("#![warn(")
146 });
147
148 let deny_toml = project_path.join("deny.toml");
150 let has_deny = deny_toml.exists();
151
152 item = item.with_evidence(Evidence {
153 evidence_type: EvidenceType::StaticAnalysis,
154 description: format!(
155 "Linting: clippy_config={}, clippy_ci={}, custom_lints={}, deny={}",
156 has_clippy_config, has_clippy_ci, has_custom_lints, has_deny
157 ),
158 data: None,
159 files: Vec::new(),
160 });
161
162 item = apply_check_outcome(
163 item,
164 &[
165 (has_clippy_ci && (has_deny || has_custom_lints), CheckOutcome::Pass),
166 (
167 has_clippy_ci,
168 CheckOutcome::Partial("Clippy in CI but limited sovereignty-specific rules"),
169 ),
170 (true, CheckOutcome::Fail("No automated linting in CI")),
171 ],
172 );
173
174 item.finish_timed(start)
175}
176
177pub fn check_data_drift_circuit_breaker(project_path: &Path) -> CheckItem {
184 let start = Instant::now();
185 let mut item = CheckItem::new(
186 "JA-03",
187 "Data Drift Circuit Breaker",
188 "Training stops on significant data drift",
189 )
190 .with_severity(Severity::Major)
191 .with_tps("Jidoka — automatic stop");
192
193 let has_drift_detection = any_source_matches(project_path, "src/**/*.rs", |c| {
195 c.contains("drift")
196 || c.contains("distribution_shift")
197 || c.contains("data_quality")
198 || c.contains("schema_validation")
199 });
200
201 let has_data_validation = any_source_matches(project_path, "tests/**/*.rs", |c| {
203 c.contains("data") && (c.contains("valid") || c.contains("schema"))
204 });
205
206 item = item.with_evidence(Evidence {
207 evidence_type: EvidenceType::StaticAnalysis,
208 description: format!(
209 "Data drift: detection={}, validation={}",
210 has_drift_detection, has_data_validation
211 ),
212 data: None,
213 files: Vec::new(),
214 });
215
216 let has_training = any_source_matches(project_path, "src/**/*.rs", |c| {
218 c.contains("train") || c.contains("fit") || c.contains("epoch")
219 });
220
221 item = apply_check_outcome(
222 item,
223 &[
224 (!has_training || has_drift_detection || has_data_validation, CheckOutcome::Pass),
225 (true, CheckOutcome::Partial("Training without data drift detection")),
226 ],
227 );
228
229 item.finish_timed(start)
230}
231
232pub fn check_performance_regression_gate(project_path: &Path) -> CheckItem {
239 let start = Instant::now();
240 let mut item = CheckItem::new(
241 "JA-04",
242 "Performance Regression Gate",
243 "Deployment blocked on performance regression",
244 )
245 .with_severity(Severity::Major)
246 .with_tps("Jidoka — quality gate");
247
248 let benches_dir = project_path.join("benches");
250 let has_benches = benches_dir.exists();
251
252 let cargo_toml = project_path.join("Cargo.toml");
254 let has_criterion = cargo_toml
255 .exists()
256 .then(|| std::fs::read_to_string(&cargo_toml).ok())
257 .flatten()
258 .map(|c| c.contains("criterion") || c.contains("divan") || c.contains("[bench]"))
259 .unwrap_or(false);
260
261 let has_bench_ci = check_ci_for_content(project_path, "bench");
263
264 let makefile = project_path.join("Makefile");
266 let has_perf_make = makefile
267 .exists()
268 .then(|| std::fs::read_to_string(&makefile).ok())
269 .flatten()
270 .map(|c| c.contains("hyperfine") || c.contains("bench") || c.contains("perf"))
271 .unwrap_or(false);
272
273 item = item.with_evidence(Evidence {
274 evidence_type: EvidenceType::StaticAnalysis,
275 description: format!(
276 "Performance: benches_dir={}, criterion={}, ci_bench={}, make_perf={}",
277 has_benches, has_criterion, has_bench_ci, has_perf_make
278 ),
279 data: None,
280 files: Vec::new(),
281 });
282
283 item = apply_check_outcome(
284 item,
285 &[
286 (has_benches && has_criterion && has_bench_ci, CheckOutcome::Pass),
287 (
288 has_benches || has_criterion,
289 CheckOutcome::Partial("Benchmarks exist but not gated in CI"),
290 ),
291 (true, CheckOutcome::Partial("No performance regression detection")),
292 ],
293 );
294
295 item.finish_timed(start)
296}
297
298pub fn check_fairness_metric_circuit_breaker(project_path: &Path) -> CheckItem {
305 let start = Instant::now();
306 let mut item = CheckItem::new(
307 "JA-05",
308 "Fairness Metric Circuit Breaker",
309 "Training stops on fairness regression",
310 )
311 .with_severity(Severity::Major)
312 .with_tps("Jidoka — ethical safeguard");
313
314 let has_fairness_code = any_source_matches(project_path, "src/**/*.rs", |c| {
316 c.contains("fairness")
317 || c.contains("bias")
318 || c.contains("demographic_parity")
319 || c.contains("equalized_odds")
320 || c.contains("protected_class")
321 });
322
323 let has_fairness_tests = any_source_matches(project_path, "tests/**/*.rs", |c| {
325 c.contains("fairness") || c.contains("bias")
326 });
327
328 item = item.with_evidence(Evidence {
329 evidence_type: EvidenceType::StaticAnalysis,
330 description: format!("Fairness: code={}, tests={}", has_fairness_code, has_fairness_tests),
331 data: None,
332 files: Vec::new(),
333 });
334
335 let has_ml = any_source_matches(project_path, "src/**/*.rs", |c| {
337 c.contains("classifier") || c.contains("predict") || c.contains("model")
338 });
339
340 item = apply_check_outcome(
341 item,
342 &[
343 (!has_ml || has_fairness_code || has_fairness_tests, CheckOutcome::Pass),
344 (true, CheckOutcome::Partial("ML without fairness monitoring")),
345 ],
346 );
347
348 item.finish_timed(start)
349}
350
351pub fn check_latency_sla_circuit_breaker(project_path: &Path) -> CheckItem {
358 let start = Instant::now();
359 let mut item = CheckItem::new(
360 "JA-06",
361 "Latency SLA Circuit Breaker",
362 "Deployment blocked on latency regression",
363 )
364 .with_severity(Severity::Major)
365 .with_tps("Jidoka — SLA enforcement");
366
367 let has_latency_monitoring = any_source_matches(project_path, "src/**/*.rs", |c| {
369 c.contains("latency")
370 || c.contains("p99")
371 || c.contains("p95")
372 || c.contains("percentile")
373 || c.contains("sla")
374 });
375
376 let has_timing = any_source_matches(project_path, "src/**/*.rs", |c| {
378 c.contains("Instant::") || c.contains("Duration::") || c.contains("elapsed")
379 });
380
381 item = item.with_evidence(Evidence {
382 evidence_type: EvidenceType::StaticAnalysis,
383 description: format!(
384 "Latency: monitoring={}, timing={}",
385 has_latency_monitoring, has_timing
386 ),
387 data: None,
388 files: Vec::new(),
389 });
390
391 let has_serving = any_source_matches(project_path, "src/**/*.rs", |c| {
393 c.contains("serve") || c.contains("inference") || c.contains("api")
394 });
395
396 item = apply_check_outcome(
397 item,
398 &[
399 (!has_serving || has_latency_monitoring, CheckOutcome::Pass),
400 (has_timing, CheckOutcome::Partial("Timing code exists but no SLA enforcement")),
401 (true, CheckOutcome::Partial("Serving without latency monitoring")),
402 ],
403 );
404
405 item.finish_timed(start)
406}
407
408pub fn check_memory_footprint_gate(project_path: &Path) -> CheckItem {
415 let start = Instant::now();
416 let mut item =
417 CheckItem::new("JA-07", "Memory Footprint Gate", "Deployment blocked on excessive memory")
418 .with_severity(Severity::Major)
419 .with_tps("Muda (Inventory) prevention");
420
421 let has_memory_profiling = any_source_matches(project_path, "src/**/*.rs", |c| {
423 c.contains("memory")
424 || c.contains("heap")
425 || c.contains("allocator")
426 || c.contains("mem::size_of")
427 });
428
429 let has_memory_limits = check_ci_for_content(project_path, "memory")
431 || check_ci_for_content(project_path, "ulimit");
432
433 let makefile = project_path.join("Makefile");
435 let has_heaptrack = makefile
436 .exists()
437 .then(|| std::fs::read_to_string(&makefile).ok())
438 .flatten()
439 .map(|c| c.contains("heaptrack") || c.contains("valgrind") || c.contains("massif"))
440 .unwrap_or(false);
441
442 item = item.with_evidence(Evidence {
443 evidence_type: EvidenceType::StaticAnalysis,
444 description: format!(
445 "Memory: profiling={}, limits={}, heaptrack={}",
446 has_memory_profiling, has_memory_limits, has_heaptrack
447 ),
448 data: None,
449 files: Vec::new(),
450 });
451
452 item = apply_check_outcome(
453 item,
454 &[
455 (has_memory_profiling && (has_memory_limits || has_heaptrack), CheckOutcome::Pass),
456 (
457 has_memory_profiling || has_heaptrack,
458 CheckOutcome::Partial("Memory profiling available but not gated"),
459 ),
460 (true, CheckOutcome::Partial("No memory footprint gate")),
461 ],
462 );
463
464 item.finish_timed(start)
465}
466
467pub fn check_security_scan_gate(project_path: &Path) -> CheckItem {
474 let start = Instant::now();
475 let mut item =
476 CheckItem::new("JA-08", "Security Scan Gate", "Build blocked on security findings")
477 .with_severity(Severity::Critical)
478 .with_tps("Jidoka — security gate");
479
480 let has_audit_ci = check_ci_for_content(project_path, "cargo audit");
482 let has_deny_ci = check_ci_for_content(project_path, "cargo deny");
483
484 let deny_toml = project_path.join("deny.toml");
486 let has_deny_config = deny_toml.exists();
487
488 let security_workflow = project_path.join(".github/workflows/security.yml");
490 let has_security_workflow = security_workflow.exists();
491
492 item = item.with_evidence(Evidence {
493 evidence_type: EvidenceType::StaticAnalysis,
494 description: format!(
495 "Security: audit_ci={}, deny_ci={}, deny_config={}, security_workflow={}",
496 has_audit_ci, has_deny_ci, has_deny_config, has_security_workflow
497 ),
498 data: None,
499 files: Vec::new(),
500 });
501
502 item = apply_check_outcome(
503 item,
504 &[
505 (has_deny_config && (has_audit_ci || has_deny_ci), CheckOutcome::Pass),
506 (
507 has_audit_ci || has_deny_ci || has_deny_config,
508 CheckOutcome::Partial("Security scanning partially configured"),
509 ),
510 (true, CheckOutcome::Fail("No security scanning in CI")),
511 ],
512 );
513
514 item.finish_timed(start)
515}
516
517pub fn check_license_compliance_gate(project_path: &Path) -> CheckItem {
524 let start = Instant::now();
525 let mut item =
526 CheckItem::new("JA-09", "License Compliance Gate", "Build blocked on license violation")
527 .with_severity(Severity::Major)
528 .with_tps("Legal controls pillar");
529
530 let deny_toml = project_path.join("deny.toml");
532 let has_license_config = deny_toml
533 .exists()
534 .then(|| std::fs::read_to_string(&deny_toml).ok())
535 .flatten()
536 .map(|c| c.contains("[licenses]"))
537 .unwrap_or(false);
538
539 let has_deny_licenses_ci = check_ci_for_content(project_path, "cargo deny check licenses");
541
542 let has_license_file = project_path.join("LICENSE").exists()
544 || project_path.join("LICENSE.md").exists()
545 || project_path.join("LICENSE.txt").exists()
546 || project_path.join("LICENSE-MIT").exists()
547 || project_path.join("LICENSE-APACHE").exists();
548
549 item = item.with_evidence(Evidence {
550 evidence_type: EvidenceType::StaticAnalysis,
551 description: format!(
552 "License: config={}, ci_check={}, license_file={}",
553 has_license_config, has_deny_licenses_ci, has_license_file
554 ),
555 data: None,
556 files: Vec::new(),
557 });
558
559 item = apply_check_outcome(
560 item,
561 &[
562 (has_license_config && has_deny_licenses_ci, CheckOutcome::Pass),
563 (
564 has_license_file && (has_license_config || has_deny_licenses_ci),
565 CheckOutcome::Partial("License file exists, partial enforcement"),
566 ),
567 (has_license_file, CheckOutcome::Partial("License file exists but no automated check")),
568 (true, CheckOutcome::Fail("No license compliance setup")),
569 ],
570 );
571
572 item.finish_timed(start)
573}
574
575pub fn check_documentation_gate(project_path: &Path) -> CheckItem {
582 let start = Instant::now();
583 let mut item =
584 CheckItem::new("JA-10", "Documentation Gate", "PR blocked without documentation updates")
585 .with_severity(Severity::Minor)
586 .with_tps("Knowledge transfer");
587
588 let has_doc_tests = check_ci_for_content(project_path, "cargo doc");
590
591 let lib_rs = project_path.join("src/lib.rs");
593 let has_deny_missing_docs = lib_rs
594 .exists()
595 .then(|| std::fs::read_to_string(&lib_rs).ok())
596 .flatten()
597 .map(|c| c.contains("#![deny(missing_docs)]") || c.contains("#![warn(missing_docs)]"))
598 .unwrap_or(false);
599
600 let has_readme = project_path.join("README.md").exists();
602
603 let has_docs_dir = project_path.join("docs").exists() || project_path.join("book").exists();
605
606 item = item.with_evidence(Evidence {
607 evidence_type: EvidenceType::StaticAnalysis,
608 description: format!(
609 "Docs: ci_doc={}, deny_missing={}, readme={}, docs_dir={}",
610 has_doc_tests, has_deny_missing_docs, has_readme, has_docs_dir
611 ),
612 data: None,
613 files: Vec::new(),
614 });
615
616 item = apply_check_outcome(
617 item,
618 &[
619 (
620 (has_doc_tests && has_deny_missing_docs) || (has_readme && has_docs_dir),
621 CheckOutcome::Pass,
622 ),
623 (has_readme, CheckOutcome::Partial("README exists but no documentation enforcement")),
624 (true, CheckOutcome::Fail("No documentation gate")),
625 ],
626 );
627
628 item.finish_timed(start)
629}
630
631fn check_ci_for_content(project_path: &Path, content: &str) -> bool {
633 let ci_configs = [
634 project_path.join(".github/workflows/ci.yml"),
635 project_path.join(".github/workflows/test.yml"),
636 project_path.join(".github/workflows/rust.yml"),
637 project_path.join(".github/workflows/security.yml"),
638 project_path.join(".github/workflows/release.yml"),
639 ];
640
641 for ci_path in &ci_configs {
642 if ci_path.exists() {
643 if let Ok(file_content) = std::fs::read_to_string(ci_path) {
644 if file_content.contains(content) {
645 return true;
646 }
647 }
648 }
649 }
650 false
651}
652
653#[cfg(test)]
654#[path = "jidoka_tests.rs"]
655mod tests;