use serde::{Deserialize, Serialize};
use crate::index::{AnnotationId, AnnotationKind, Sha256, VerifyMethod};
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct ProofFile {
pub verdict: VerdictMeta,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub verified: Option<VerifiedBody>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub counterexample: Option<CounterexampleBody>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub inconclusive: Option<InconclusiveBody>,
}
#[aristo::intent(
"Proof steps are a flat list indexed by dotted-path strings \
(\"0\", \"0.0\", \"0.1.2\"), not a recursive Rust struct. TOML \
doesn't serialize recursive heterogeneous structures cleanly, \
and the path encoding lets the validator and the promotion \
flow reference any node by stable string. A recursive-enum \
refactor breaks both.",
verify = "neural",
id = "proof_tree_uses_path_addressed_flat_list"
)]
impl ProofFile {
pub fn parse(raw: &str) -> Result<Self, toml::de::Error> {
toml::from_str(raw)
}
pub fn to_toml(&self) -> Result<String, toml::ser::Error> {
toml::to_string_pretty(self)
}
pub fn body(&self) -> Result<VerdictBody<'_>, BodyShapeError> {
let count = [
self.verified.is_some(),
self.counterexample.is_some(),
self.inconclusive.is_some(),
]
.iter()
.filter(|p| **p)
.count();
if count != 1 {
return Err(BodyShapeError::WrongBodyCount(count));
}
let body = if let Some(v) = &self.verified {
VerdictBody::Verified(v)
} else if let Some(c) = &self.counterexample {
VerdictBody::Counterexample(c)
} else {
VerdictBody::Inconclusive(self.inconclusive.as_ref().expect("checked above"))
};
let agrees = matches!(
(self.verdict.r#type, &body),
(VerdictType::Verified, VerdictBody::Verified(_))
| (VerdictType::Counterexample, VerdictBody::Counterexample(_))
| (VerdictType::Inconclusive, VerdictBody::Inconclusive(_))
);
if !agrees {
return Err(BodyShapeError::TagBodyMismatch {
tag: self.verdict.r#type,
});
}
Ok(body)
}
}
#[derive(Debug)]
pub enum VerdictBody<'a> {
Verified(&'a VerifiedBody),
Counterexample(&'a CounterexampleBody),
Inconclusive(&'a InconclusiveBody),
}
#[derive(Debug, thiserror::Error)]
pub enum BodyShapeError {
#[error("proof file has {0} body sub-tables; must have exactly 1")]
WrongBodyCount(usize),
#[error("verdict.type = {tag:?} but the present body sub-table doesn't match")]
TagBodyMismatch { tag: VerdictType },
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct VerdictMeta {
pub r#type: VerdictType,
pub method: VerifyMethod,
pub produced_at_text_hash: Sha256,
pub produced_at_body_hash: Sha256,
pub produced_by: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub verifier_model: Option<String>,
pub attempts: u32,
pub property_kind: PropertyKind,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "kebab-case")]
pub enum VerdictType {
Verified,
Counterexample,
Inconclusive,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "kebab-case")]
pub enum PropertyKind {
Invariant,
Postcondition,
Precondition,
Equivalence,
Safety,
Progress,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct VerifiedBody {
pub proof: Proof,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct Proof {
pub conclusion: String,
pub steps: Vec<ProofStep>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct ProofStep {
pub path: String,
pub claim: String,
pub relation_to_parent: RelationKind,
pub grounds: Vec<Ground>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub subgoal_paths: Vec<String>,
#[serde(default, skip_serializing_if = "std::ops::Not::not")]
pub proposed_promotion: bool,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "kebab-case")]
pub enum RelationKind {
Decomposes,
Instantiates,
Restricts,
Composes,
ExcludesCounterexample,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(tag = "kind", rename_all = "kebab-case")]
pub enum Ground {
Intent {
id: AnnotationId,
#[serde(default, skip_serializing_if = "Option::is_none")]
at_text_hash: Option<Sha256>,
relation: GroundRelation,
#[serde(default, skip_serializing_if = "Option::is_none")]
reason: Option<String>,
},
Assume {
id: AnnotationId,
#[serde(default, skip_serializing_if = "Option::is_none")]
at_text_hash: Option<Sha256>,
relation: GroundRelation,
#[serde(default, skip_serializing_if = "Option::is_none")]
reason: Option<String>,
},
Code {
file: String,
lines: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
code_text_hash: Option<Sha256>,
reason: String,
},
PriorStep {
path: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
reason: Option<String>,
},
Composition {
reason: String,
},
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "kebab-case")]
pub enum GroundRelation {
Instantiates,
Restricts,
Composes,
ExcludesCounterexample,
Promoted,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct CounterexampleBody {
pub violation: Violation,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct Violation {
pub description: String,
pub violated_step_path: String,
pub trigger_steps: Vec<ProofStep>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub refuted_grounds: Vec<Ground>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct InconclusiveBody {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub partial_proof: Option<Proof>,
pub gap: Gap,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct Gap {
pub description: String,
pub unfilled_path: String,
pub suggested_annotations: Vec<SuggestedAnnotation>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct SuggestedAnnotation {
pub kind: AnnotationKind,
pub suggested_text: String,
pub at_site: String,
pub rationale: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub would_close_path: Option<String>,
}
#[cfg(test)]
mod tests {
use super::*;
use crate::hash::text_hash;
fn sample_intent_id(s: &str) -> AnnotationId {
AnnotationId::parse(s).unwrap()
}
fn sample_hash(s: &str) -> Sha256 {
text_hash(s)
}
#[test]
fn verified_proof_round_trips_through_toml() {
let pf = ProofFile {
verdict: VerdictMeta {
r#type: VerdictType::Verified,
method: VerifyMethod::Neural,
produced_at_text_hash: sample_hash("text"),
produced_at_body_hash: sample_hash("body"),
produced_by: "aristo-neural-verifier@v0.0.5".to_string(),
verifier_model: Some("claude-sonnet-4-7".to_string()),
attempts: 1,
property_kind: PropertyKind::Invariant,
},
verified: Some(VerifiedBody {
proof: Proof {
conclusion: "the focal claim holds".to_string(),
steps: vec![ProofStep {
path: "0".to_string(),
claim: "by direct construction".to_string(),
relation_to_parent: RelationKind::Decomposes,
grounds: vec![Ground::Code {
file: "src/x.rs".to_string(),
lines: "10-20".to_string(),
code_text_hash: Some(sample_hash("code")),
reason: "constructive proof".to_string(),
}],
subgoal_paths: vec![],
proposed_promotion: false,
}],
},
}),
counterexample: None,
inconclusive: None,
};
let toml = pf.to_toml().unwrap();
let round = ProofFile::parse(&toml).unwrap();
assert_eq!(pf, round);
}
#[test]
fn body_accessor_returns_present_variant() {
let pf = sample_verified();
let body = pf.body().unwrap();
assert!(matches!(body, VerdictBody::Verified(_)));
}
#[test]
fn body_accessor_rejects_zero_bodies() {
let mut pf = sample_verified();
pf.verified = None;
assert!(matches!(pf.body(), Err(BodyShapeError::WrongBodyCount(0))));
}
#[test]
fn body_accessor_rejects_multiple_bodies() {
let mut pf = sample_verified();
pf.counterexample = Some(CounterexampleBody {
violation: Violation {
description: "x".to_string(),
violated_step_path: "0".to_string(),
trigger_steps: vec![],
refuted_grounds: vec![],
},
});
assert!(matches!(pf.body(), Err(BodyShapeError::WrongBodyCount(2))));
}
#[test]
fn body_accessor_rejects_tag_body_mismatch() {
let mut pf = sample_verified();
pf.verdict.r#type = VerdictType::Counterexample; assert!(matches!(
pf.body(),
Err(BodyShapeError::TagBodyMismatch { .. })
));
}
#[test]
fn intent_ground_serializes_with_kind_tag() {
let g = Ground::Intent {
id: sample_intent_id("foo"),
at_text_hash: Some(sample_hash("t")),
relation: GroundRelation::Instantiates,
reason: Some("because".to_string()),
};
let s = toml::to_string(&InlineHolder { g }).unwrap();
assert!(s.contains("kind = \"intent\""));
assert!(s.contains("relation = \"instantiates\""));
}
#[test]
fn ground_round_trips_all_variants() {
for g in [
Ground::Intent {
id: sample_intent_id("foo"),
at_text_hash: Some(sample_hash("t")),
relation: GroundRelation::Composes,
reason: None,
},
Ground::Assume {
id: sample_intent_id("os_zero_pages"),
at_text_hash: Some(sample_hash("t")),
relation: GroundRelation::ExcludesCounterexample,
reason: Some("rules out stale data".to_string()),
},
Ground::Code {
file: "src/x.rs".to_string(),
lines: "1-5".to_string(),
code_text_hash: Some(sample_hash("c")),
reason: "see comment".to_string(),
},
Ground::PriorStep {
path: "0.1".to_string(),
reason: None,
},
Ground::Composition {
reason: "subgoals combine via AND".to_string(),
},
] {
let s = toml::to_string(&InlineHolder { g: g.clone() }).unwrap();
let r: InlineHolder = toml::from_str(&s).unwrap();
assert_eq!(g, r.g);
}
}
#[test]
fn ground_omits_hash_when_none_and_round_trips() {
let g = Ground::Intent {
id: sample_intent_id("foo"),
at_text_hash: None,
relation: GroundRelation::Instantiates,
reason: Some("agent-written".to_string()),
};
let s = toml::to_string(&InlineHolder { g: g.clone() }).unwrap();
assert!(
!s.contains("at_text_hash"),
"absent at_text_hash must not serialize; got:\n{s}"
);
let r: InlineHolder = toml::from_str(&s).unwrap();
assert_eq!(g, r.g);
let g = Ground::Code {
file: "src/x.rs".to_string(),
lines: "1-5".to_string(),
code_text_hash: None,
reason: "see fn body".to_string(),
};
let s = toml::to_string(&InlineHolder { g: g.clone() }).unwrap();
assert!(
!s.contains("code_text_hash"),
"absent code_text_hash must not serialize; got:\n{s}"
);
let r: InlineHolder = toml::from_str(&s).unwrap();
assert_eq!(g, r.g);
}
#[test]
fn property_kind_serializes_kebab_case() {
#[derive(Serialize)]
struct Holder {
k: PropertyKind,
}
let s = toml::to_string(&Holder {
k: PropertyKind::Postcondition,
})
.unwrap();
assert!(s.contains("k = \"postcondition\""), "got: {s}");
}
#[derive(Serialize, Deserialize)]
struct InlineHolder {
g: Ground,
}
fn sample_verified() -> ProofFile {
ProofFile {
verdict: VerdictMeta {
r#type: VerdictType::Verified,
method: VerifyMethod::Neural,
produced_at_text_hash: sample_hash("text"),
produced_at_body_hash: sample_hash("body"),
produced_by: "test@0".to_string(),
verifier_model: None,
attempts: 1,
property_kind: PropertyKind::Invariant,
},
verified: Some(VerifiedBody {
proof: Proof {
conclusion: "ok".to_string(),
steps: vec![],
},
}),
counterexample: None,
inconclusive: None,
}
}
}