use clap::{Parser, Subcommand};
use std::fs;
#[derive(Parser)]
#[command(name = "agi4")]
#[command(about = "AGI/4 specification and reference runner", long_about = None)]
#[command(version)]
struct Cli {
#[command(subcommand)]
command: Commands,
}
#[derive(Subcommand)]
enum Commands {
Attest {
#[arg(long)]
model: String,
#[arg(long)]
fixture: Option<String>,
#[arg(long)]
live: bool,
},
Render {
#[arg(long)]
input: String,
},
Schema,
Version,
}
fn main() {
let cli = Cli::parse();
match cli.command {
Commands::Attest {
model,
fixture,
live,
} => {
if live {
match agi4::live::attest_live(&model) {
Ok(verdict_json) => match serde_json::to_string_pretty(&verdict_json) {
Ok(json_str) => println!("{}", json_str),
Err(e) => {
eprintln!("Error serializing verdict to JSON: {}", e);
std::process::exit(1);
}
},
Err(e) => {
eprintln!("Error during live attestation: {}", e);
std::process::exit(1);
}
}
} else if let Some(fixture_path) = fixture {
match attest_from_fixture(&model, &fixture_path) {
Ok(verdict_json) => println!("{}", verdict_json),
Err(e) => {
eprintln!("Error during attestation: {}", e);
std::process::exit(1);
}
}
} else {
eprintln!("Error: either --fixture or --live must be specified");
std::process::exit(1);
}
}
Commands::Render { input } => match render_verdict_file(&input) {
Ok(markdown) => println!("{}", markdown),
Err(e) => {
eprintln!("Error rendering verdict: {}", e);
std::process::exit(1);
}
},
Commands::Schema => match agi4_schema::schema_json_string() {
Ok(json) => println!("{}", json),
Err(e) => {
eprintln!("Error generating schema: {}", e);
std::process::exit(1);
}
},
Commands::Version => {
println!("agi4 {}", agi4::VERSION);
println!("spec version {}", agi4::SPEC_VERSION);
}
}
}
fn attest_from_fixture(
model: &str,
fixture_dir: &str,
) -> Result<String, Box<dyn std::error::Error>> {
use agi4::consistency_check;
use agi4::core::ConjunctStatus;
use agi4::fixtures::load_evidence_from_fixtures;
use agi4_core::evaluators::{
evaluate_autonomous_agency, evaluate_economic_substitutability,
evaluate_environmental_transfer, evaluate_generality,
};
use agi4_core::sources;
use agi4_schema::{
ConjunctReport, ConjunctsOutput, ConsistencyCheckOutput, EvidenceReport, ModelMetadata,
ProvenanceReport, VerdictOutput,
};
use chrono::Utc;
let now = Utc::now();
let run_timestamp = now.to_rfc3339_opts(chrono::SecondsFormat::Secs, true);
let all_evidence = load_evidence_from_fixtures(fixture_dir, model)?;
let generality_status = evaluate_generality(&all_evidence);
let econ_status = evaluate_economic_substitutability(&all_evidence);
let env_status = evaluate_environmental_transfer(&all_evidence);
let agency_status = evaluate_autonomous_agency(&all_evidence);
let conjunct_statuses = [generality_status, econ_status, env_status, agency_status];
let consistency_result = consistency_check(&all_evidence, &conjunct_statuses);
let overall_verdict = if conjunct_statuses.iter().all(|s| *s == ConjunctStatus::Pass)
&& consistency_result.passed
{
"attested"
} else {
"not_attested"
};
fn status_to_string(status: ConjunctStatus) -> String {
format!("{:?}", status).to_lowercase()
}
fn evidence_to_report(e: &agi4::core::Evidence) -> EvidenceReport {
use agi4::core::SourceValue;
EvidenceReport {
source: e.source.as_str().to_string(),
measurement: e.measurement.as_str().to_string(),
value: match e.value {
SourceValue::Fraction(f) => serde_json::json!(f.value()),
SourceValue::Hours(h) => serde_json::json!(h.value()),
},
threshold: None,
floor: None,
passes_threshold: None,
below_floor: None,
reliability_percentile: e.reliability_percentile,
provenance: ProvenanceReport {
source_url: e.provenance.source_url.as_str().to_string(),
fetch_timestamp: e
.provenance
.fetch_timestamp
.to_rfc3339_opts(chrono::SecondsFormat::Secs, true),
source_version: e.provenance.source_version.clone(),
raw_value: e.provenance.raw_value.clone(),
},
}
}
let generality_evidence: Vec<EvidenceReport> = all_evidence
.iter()
.filter(|e| sources::generality::all().contains(&e.source.as_str()))
.map(evidence_to_report)
.collect();
let econ_evidence: Vec<EvidenceReport> = all_evidence
.iter()
.filter(|e| sources::economic_substitutability::all().contains(&e.source.as_str()))
.map(evidence_to_report)
.collect();
let env_evidence: Vec<EvidenceReport> = all_evidence
.iter()
.filter(|e| sources::environmental_transfer::all().contains(&e.source.as_str()))
.map(evidence_to_report)
.collect();
let agency_evidence: Vec<EvidenceReport> = all_evidence
.iter()
.filter(|e| sources::autonomous_agency::all().contains(&e.source.as_str()))
.map(evidence_to_report)
.collect();
let verdict_reasons = vec![
format!("Generality: {}", status_to_string(generality_status)),
format!(
"Economic Substitutability: {}",
status_to_string(econ_status)
),
format!("Environmental Transfer: {}", status_to_string(env_status)),
format!("Autonomous Agency: {}", status_to_string(agency_status)),
format!(
"Consistency Check: {}",
if consistency_result.passed {
"pass"
} else {
"fail"
}
),
];
let verdict_output = VerdictOutput {
spec_version: agi4::SPEC_VERSION.to_string(),
runner_version: agi4::VERSION.to_string(),
run_timestamp,
model: ModelMetadata {
id: model.to_string(),
provider: None,
version_or_date: None,
},
conjuncts: ConjunctsOutput {
generality: ConjunctReport {
status: status_to_string(generality_status),
evidence: generality_evidence,
margins: None,
},
economic_substitutability: ConjunctReport {
status: status_to_string(econ_status),
evidence: econ_evidence,
margins: None,
},
environmental_transfer: ConjunctReport {
status: status_to_string(env_status),
evidence: env_evidence,
margins: None,
},
autonomous_agency: ConjunctReport {
status: status_to_string(agency_status),
evidence: agency_evidence,
margins: None,
},
},
consistency_check: ConsistencyCheckOutput {
status: if consistency_result.passed {
"pass"
} else {
"fail"
}
.to_string(),
failed_rules: consistency_result
.failed_rules
.iter()
.map(|r| r.to_string())
.collect(),
detail: consistency_result.detail.map(|d| d.to_string()),
},
verdict: overall_verdict.to_string(),
verdict_reasons,
known_gaps_acknowledged: vec![
"Fixture-based attestation uses only provided evidence".to_string(),
],
};
Ok(serde_json::to_string_pretty(&verdict_output)?)
}
fn render_verdict_file(path: &str) -> Result<String, Box<dyn std::error::Error>> {
let json_str = fs::read_to_string(path)?;
let verdict: agi4_schema::VerdictOutput = serde_json::from_str(&json_str)?;
Ok(agi4::render_verdict(&verdict))
}