use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
pub struct VerdictOutput {
pub spec_version: String,
pub runner_version: String,
pub run_timestamp: String,
pub model: ModelMetadata,
pub conjuncts: ConjunctsOutput,
pub consistency_check: ConsistencyCheckOutput,
pub verdict: String,
#[serde(skip_serializing_if = "Vec::is_empty", default)]
pub verdict_reasons: Vec<String>,
#[serde(skip_serializing_if = "Vec::is_empty", default)]
pub known_gaps_acknowledged: Vec<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
pub struct ModelMetadata {
pub id: String,
pub provider: Option<String>,
pub version_or_date: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
pub struct ConjunctsOutput {
pub generality: ConjunctReport,
pub economic_substitutability: ConjunctReport,
pub environmental_transfer: ConjunctReport,
pub autonomous_agency: ConjunctReport,
}
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
pub struct ConjunctReport {
pub status: String,
#[serde(skip_serializing_if = "Vec::is_empty", default)]
pub evidence: Vec<EvidenceReport>,
pub margins: Option<MarginReport>,
}
pub type ConjunctOutput = ConjunctReport;
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
pub struct EvidenceReport {
pub source: String,
pub measurement: String,
pub value: serde_json::Value,
pub threshold: Option<f64>,
pub floor: Option<f64>,
pub passes_threshold: Option<bool>,
pub below_floor: Option<bool>,
pub reliability_percentile: u8,
pub provenance: ProvenanceReport,
}
pub type EvidenceOutput = EvidenceReport;
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
pub struct ProvenanceReport {
pub source_url: String,
pub fetch_timestamp: String,
pub source_version: Option<String>,
pub raw_value: String,
}
pub type ProvenanceOutput = ProvenanceReport;
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
pub struct MarginReport {
pub min: f64,
pub max: f64,
}
pub type MarginOutput = MarginReport;
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
pub struct ConsistencyCheckOutput {
pub status: String,
#[serde(skip_serializing_if = "Vec::is_empty", default)]
pub failed_rules: Vec<String>,
pub detail: Option<String>,
}
pub fn generate_schema() -> schemars::schema::RootSchema {
schemars::schema_for!(VerdictOutput)
}
pub fn schema_json_string() -> Result<String, serde_json::Error> {
let schema = generate_schema();
serde_json::to_string_pretty(&schema)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn verdict_output_serialize_deserialize() {
let output = VerdictOutput {
spec_version: "0.1.0".to_string(),
runner_version: "0.1.0".to_string(),
run_timestamp: "2026-05-26T00:00:00Z".to_string(),
model: ModelMetadata {
id: "test-model".to_string(),
provider: Some("test-lab".to_string()),
version_or_date: Some("2026-05-26".to_string()),
},
conjuncts: ConjunctsOutput {
generality: ConjunctReport {
status: "pass".to_string(),
evidence: vec![],
margins: None,
},
economic_substitutability: ConjunctReport {
status: "pass".to_string(),
evidence: vec![],
margins: None,
},
environmental_transfer: ConjunctReport {
status: "partial".to_string(),
evidence: vec![],
margins: None,
},
autonomous_agency: ConjunctReport {
status: "pass".to_string(),
evidence: vec![],
margins: None,
},
},
consistency_check: ConsistencyCheckOutput {
status: "pass".to_string(),
failed_rules: vec![],
detail: None,
},
verdict: "not_attested".to_string(),
verdict_reasons: vec!["environmental_transfer".to_string()],
known_gaps_acknowledged: vec!["nes_underspecified".to_string()],
};
let json = serde_json::to_string(&output).expect("should serialize");
assert!(!json.is_empty());
let deserialized: VerdictOutput = serde_json::from_str(&json).expect("should deserialize");
assert_eq!(deserialized.spec_version, output.spec_version);
assert_eq!(deserialized.model.id, output.model.id);
assert_eq!(deserialized.verdict, output.verdict);
assert_eq!(deserialized.verdict_reasons.len(), 1);
}
#[test]
fn conjunct_report_serialize() {
let report = ConjunctReport {
status: "pass".to_string(),
evidence: vec![],
margins: Some(MarginReport {
min: 0.85,
max: 0.95,
}),
};
let json = serde_json::to_string(&report).expect("should serialize");
assert!(json.contains("\"status\":\"pass\""));
assert!(json.contains("\"min\":0.85"));
}
#[test]
fn evidence_report_with_provenance() {
let evidence = EvidenceReport {
source: "arc-agi-3".to_string(),
measurement: "interactive-private-pass".to_string(),
value: serde_json::json!(0.75),
threshold: Some(0.50),
floor: Some(0.05),
passes_threshold: Some(true),
below_floor: Some(false),
reliability_percentile: 80,
provenance: ProvenanceReport {
source_url: "https://arcprize.org".to_string(),
fetch_timestamp: "2026-05-26T00:00:00Z".to_string(),
source_version: Some("v1.0".to_string()),
raw_value: "0.75".to_string(),
},
};
let json = serde_json::to_string(&evidence).expect("should serialize");
let deserialized: EvidenceReport = serde_json::from_str(&json).expect("should deserialize");
assert_eq!(deserialized.source, "arc-agi-3");
assert_eq!(deserialized.passes_threshold, Some(true));
assert_eq!(deserialized.provenance.source_url, "https://arcprize.org");
}
#[test]
fn model_metadata_with_optional_fields() {
let model = ModelMetadata {
id: "model-v1".to_string(),
provider: None,
version_or_date: None,
};
let json = serde_json::to_string(&model).expect("should serialize");
assert!(json.contains("\"id\":\"model-v1\""));
let deserialized: ModelMetadata = serde_json::from_str(&json).expect("should deserialize");
assert_eq!(deserialized.id, "model-v1");
assert!(deserialized.provider.is_none());
}
#[test]
fn margin_report_serialize() {
let margin = MarginReport {
min: 0.12,
max: 2.98,
};
let json = serde_json::to_string(&margin).expect("should serialize");
let deserialized: MarginReport = serde_json::from_str(&json).expect("should deserialize");
assert_eq!(deserialized.min, 0.12);
assert_eq!(deserialized.max, 2.98);
}
#[test]
fn consistency_check_output_serialize() {
let check = ConsistencyCheckOutput {
status: "fail".to_string(),
failed_rules: vec!["margin_variance_ratio".to_string()],
detail: Some("min/max ratio = 0.12, below required 0.5".to_string()),
};
let json = serde_json::to_string(&check).expect("should serialize");
assert!(json.contains("margin_variance_ratio"));
}
#[test]
fn conjuncts_output_all_variants() {
let conjuncts = ConjunctsOutput {
generality: ConjunctReport {
status: "pass".to_string(),
evidence: vec![],
margins: None,
},
economic_substitutability: ConjunctReport {
status: "fail".to_string(),
evidence: vec![],
margins: None,
},
environmental_transfer: ConjunctReport {
status: "partial".to_string(),
evidence: vec![],
margins: None,
},
autonomous_agency: ConjunctReport {
status: "insufficient_data".to_string(),
evidence: vec![],
margins: None,
},
};
let json = serde_json::to_string(&conjuncts).expect("should serialize");
let deserialized: ConjunctsOutput =
serde_json::from_str(&json).expect("should deserialize");
assert_eq!(deserialized.generality.status, "pass");
assert_eq!(deserialized.economic_substitutability.status, "fail");
assert_eq!(deserialized.environmental_transfer.status, "partial");
assert_eq!(deserialized.autonomous_agency.status, "insufficient_data");
}
#[test]
fn json_schema_generation() {
let schema = schemars::schema_for!(VerdictOutput);
assert!(schema.schema.metadata.is_some());
let schema_json = serde_json::to_string(&schema).expect("should serialize schema");
assert!(!schema_json.is_empty());
}
#[test]
fn json_schema_for_conjunct_report() {
let schema = schemars::schema_for!(ConjunctReport);
let schema_json = serde_json::to_string(&schema).expect("should serialize schema");
assert!(schema_json.contains("status"));
assert!(schema_json.contains("evidence"));
}
#[test]
fn skip_serializing_if_empty() {
let output = VerdictOutput {
spec_version: "0.1.0".to_string(),
runner_version: "0.1.0".to_string(),
run_timestamp: "2026-05-26T00:00:00Z".to_string(),
model: ModelMetadata {
id: "test".to_string(),
provider: None,
version_or_date: None,
},
conjuncts: ConjunctsOutput {
generality: ConjunctReport {
status: "pass".to_string(),
evidence: vec![],
margins: None,
},
economic_substitutability: ConjunctReport {
status: "pass".to_string(),
evidence: vec![],
margins: None,
},
environmental_transfer: ConjunctReport {
status: "pass".to_string(),
evidence: vec![],
margins: None,
},
autonomous_agency: ConjunctReport {
status: "pass".to_string(),
evidence: vec![],
margins: None,
},
},
consistency_check: ConsistencyCheckOutput {
status: "pass".to_string(),
failed_rules: vec![],
detail: None,
},
verdict: "attested".to_string(),
verdict_reasons: vec![],
known_gaps_acknowledged: vec![],
};
let json = serde_json::to_string(&output).expect("should serialize");
assert!(!json.contains("\"verdict_reasons\":[]"));
assert!(!json.contains("\"known_gaps_acknowledged\":[]"));
}
#[test]
fn schema_drift_check() {
let committed_schema_str = include_str!("../../../../schema/agi4-output-v0.1.0.json");
let committed_schema: serde_json::Value = serde_json::from_str(committed_schema_str)
.expect("committed schema should be valid JSON");
let generated_schema = schemars::schema_for!(VerdictOutput);
let generated_json = serde_json::to_value(&generated_schema)
.expect("generated schema should serialize to JSON");
if committed_schema != generated_json {
let committed_pretty =
serde_json::to_string_pretty(&committed_schema).unwrap_or_default();
let generated_pretty =
serde_json::to_string_pretty(&generated_json).unwrap_or_default();
panic!(
"Schema drift detected!\n\nCommitted schema:\n{}\n\nGenerated schema:\n{}\n\n\
To fix, run: `cargo run -p agi4 -- schema > schema/agi4-output-v0.1.0.json`",
committed_pretty, generated_pretty
);
}
}
}