Skip to content

Commit ea5e894

Browse files
montfortclaude
andcommitted
feat: implement Fase 3 — compliance automation, metrics, and governance docs (#27)
Add `devtrail compliance` and `devtrail metrics` CLI commands, ISO 42001 governance documents, NIST AI RMF implementation guides, and OpenTelemetry observability guide. CLI (cli-2.0.0): - New `devtrail compliance` command with EU AI Act, ISO 42001, and NIST AI RMF checkers, supporting text/markdown/json output - New `devtrail metrics` command with period filtering, review compliance, risk distribution, agent activity, and trend analysis - Compliance engine (compliance.rs) with 12 checks across 3 standards - Metrics engine (metrics_engine.rs) with chrono-based date handling - 16 new integration tests (9 compliance + 7 metrics) Framework (fw-3.2.0): - AI-RISK-CATALOG.md — risk registry mapped to NIST AI 600-1 + ISO 42001 - AI-LIFECYCLE-TRACKER.md — AI system lifecycle tracking (ISO 42001 A.6) - AI-KPIS.md — governance KPIs aligned with ISO 42001 Clause 9 - MANAGEMENT-REVIEW-TEMPLATE.md — periodic reviews (ISO 42001 Clause 9.3) - OBSERVABILITY-GUIDE.md — OpenTelemetry integration guide (10 sections) - 5 NIST AI RMF guides (MAP, MEASURE, MANAGE, GOVERN, GenAI Risks 600-1) - All documents available in EN + ES (20 new documents total) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 1f939c0 commit ea5e894

33 files changed

Lines changed: 6245 additions & 52 deletions

cli/Cargo.lock

Lines changed: 81 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

cli/Cargo.toml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[package]
22
name = "devtrail-cli"
3-
version = "1.4.0"
3+
version = "2.0.0"
44
edition = "2021"
55
description = "CLI tool for DevTrail - Documentation Governance for AI-Assisted Development"
66
license = "MIT"
@@ -28,6 +28,7 @@ indicatif = "0.17"
2828
dialoguer = "0.11"
2929
sha2 = "0.10"
3030
anyhow = "1"
31+
chrono = { version = "0.4", default-features = false, features = ["std", "clock"] }
3132
semver = "1"
3233
flate2 = "1"
3334
tar = "0.4"

cli/src/commands/compliance.rs

Lines changed: 189 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,189 @@
1+
use anyhow::Result;
2+
use colored::Colorize;
3+
use std::path::PathBuf;
4+
5+
use crate::compliance::{self, CheckStatus, ComplianceReport};
6+
use crate::document;
7+
use crate::utils;
8+
9+
pub fn run(path: &str, standard: Option<&str>, all: bool, output: &str) -> Result<()> {
10+
let resolved = match utils::resolve_project_root(path) {
11+
Some(r) => r,
12+
None => {
13+
let target = PathBuf::from(path)
14+
.canonicalize()
15+
.unwrap_or_else(|_| PathBuf::from(path));
16+
utils::info(&format!(
17+
"DevTrail is not installed in {}",
18+
target.display()
19+
));
20+
utils::info("Run 'devtrail init' to initialize DevTrail in this directory.");
21+
return Ok(());
22+
}
23+
};
24+
25+
if resolved.is_fallback {
26+
utils::info(&format!(
27+
"Using DevTrail installation at repo root: {}",
28+
resolved.path.display()
29+
));
30+
}
31+
32+
let target = resolved.path;
33+
let devtrail_dir = target.join(".devtrail");
34+
35+
// Discover and parse all documents
36+
let paths = document::discover_documents(&devtrail_dir);
37+
let docs: Vec<_> = paths
38+
.iter()
39+
.filter_map(|p| document::parse_document(p).ok())
40+
.collect();
41+
42+
// Determine which standard(s) to check
43+
// If neither --standard nor --all, default to --all
44+
let run_all = all || standard.is_none();
45+
46+
let mut reports = Vec::new();
47+
48+
if run_all {
49+
reports.push(compliance::check_eu_ai_act(&docs, &devtrail_dir));
50+
reports.push(compliance::check_iso_42001(&docs, &devtrail_dir));
51+
reports.push(compliance::check_nist_ai_rmf(&docs, &devtrail_dir));
52+
} else if let Some(std_name) = standard {
53+
match std_name {
54+
"eu-ai-act" => reports.push(compliance::check_eu_ai_act(&docs, &devtrail_dir)),
55+
"iso-42001" => reports.push(compliance::check_iso_42001(&docs, &devtrail_dir)),
56+
"nist-ai-rmf" => reports.push(compliance::check_nist_ai_rmf(&docs, &devtrail_dir)),
57+
_ => {
58+
utils::warn(&format!("Unknown standard: {}", std_name));
59+
return Ok(());
60+
}
61+
}
62+
}
63+
64+
// Output
65+
match output {
66+
"json" => print_json(&reports),
67+
"markdown" => print_markdown(&reports, docs.len()),
68+
_ => print_text(&reports, &target, docs.len()),
69+
}
70+
71+
Ok(())
72+
}
73+
74+
fn print_text(reports: &[ComplianceReport], target: &std::path::Path, doc_count: usize) {
75+
println!();
76+
println!(" {}", "DevTrail Compliance".bold().cyan());
77+
println!(" {}", target.display().to_string().dimmed());
78+
println!(
79+
" {}",
80+
format!("{} document(s) analyzed", doc_count).dimmed()
81+
);
82+
println!();
83+
84+
for report in reports {
85+
let score_color = if report.score >= 80.0 {
86+
format!("{:.0}%", report.score).green().bold()
87+
} else if report.score >= 50.0 {
88+
format!("{:.0}%", report.score).yellow().bold()
89+
} else {
90+
format!("{:.0}%", report.score).red().bold()
91+
};
92+
93+
println!(
94+
" {} {} {}",
95+
"■".cyan().bold(),
96+
report.standard_label.bold(),
97+
score_color
98+
);
99+
100+
for check in &report.checks {
101+
let status_icon = match check.status {
102+
CheckStatus::Pass => "✓".green().bold(),
103+
CheckStatus::Partial => "~".yellow().bold(),
104+
CheckStatus::Fail => "✗".red().bold(),
105+
};
106+
107+
println!(" {} [{}] {}", status_icon, check.id, check.description);
108+
109+
if !check.evidence.is_empty() && check.status != CheckStatus::Fail {
110+
let evidence_str = if check.evidence.len() <= 3 {
111+
check.evidence.join(", ")
112+
} else {
113+
format!(
114+
"{}, ... (+{} more)",
115+
check.evidence[..3].join(", "),
116+
check.evidence.len() - 3
117+
)
118+
};
119+
println!(" {}", evidence_str.dimmed());
120+
}
121+
122+
if let Some(remediation) = &check.remediation {
123+
if check.status != CheckStatus::Pass {
124+
println!(" {} {}", "fix:".dimmed(), remediation.dimmed());
125+
}
126+
}
127+
}
128+
println!();
129+
}
130+
131+
// Overall summary
132+
if reports.len() > 1 {
133+
let avg_score: f64 = reports.iter().map(|r| r.score).sum::<f64>() / reports.len() as f64;
134+
let summary_color = if avg_score >= 80.0 {
135+
format!(" Overall compliance: {:.0}%", avg_score)
136+
.green()
137+
.bold()
138+
} else if avg_score >= 50.0 {
139+
format!(" Overall compliance: {:.0}%", avg_score)
140+
.yellow()
141+
.bold()
142+
} else {
143+
format!(" Overall compliance: {:.0}%", avg_score)
144+
.red()
145+
.bold()
146+
};
147+
println!("{}", summary_color);
148+
println!();
149+
}
150+
}
151+
152+
fn print_json(reports: &[ComplianceReport]) {
153+
let json = serde_json::to_string_pretty(reports).unwrap_or_else(|_| "[]".into());
154+
println!("{}", json);
155+
}
156+
157+
fn print_markdown(reports: &[ComplianceReport], doc_count: usize) {
158+
println!("# DevTrail Compliance Report");
159+
println!();
160+
println!("**Documents analyzed:** {}", doc_count);
161+
println!();
162+
163+
for report in reports {
164+
println!("## {} — {:.0}%", report.standard_label, report.score);
165+
println!();
166+
println!("| Check | Status | Description |");
167+
println!("|-------|--------|-------------|");
168+
169+
for check in &report.checks {
170+
let status_emoji = match check.status {
171+
CheckStatus::Pass => "✅",
172+
CheckStatus::Partial => "⚠️",
173+
CheckStatus::Fail => "❌",
174+
};
175+
println!(
176+
"| {} | {} | {} |",
177+
check.id, status_emoji, check.description
178+
);
179+
}
180+
println!();
181+
}
182+
183+
if reports.len() > 1 {
184+
let avg_score: f64 = reports.iter().map(|r| r.score).sum::<f64>() / reports.len() as f64;
185+
println!("---");
186+
println!();
187+
println!("**Overall compliance: {:.0}%**", avg_score);
188+
}
189+
}

0 commit comments

Comments
 (0)