use std::collections::HashMap;
use aingle_graph::{Triple, Value};
use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use crate::error::{Error, Result};
use crate::rule::{Bindings, Rule, RuleKind};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct LogicProof {
pub id: String,
pub conclusion: ProofConclusion,
pub steps: Vec<ProofStep>,
pub timestamp: DateTime<Utc>,
pub hash: String,
pub metadata: HashMap<String, String>,
}
impl LogicProof {
pub fn new(conclusion: ProofConclusion) -> Self {
let id = generate_proof_id();
Self {
id: id.clone(),
conclusion,
steps: Vec::new(),
timestamp: Utc::now(),
hash: String::new(),
metadata: HashMap::new(),
}
}
pub fn add_step(&mut self, step: ProofStep) {
self.steps.push(step);
}
pub fn finalize(&mut self) {
self.hash = self.compute_hash();
}
fn compute_hash(&self) -> String {
use std::collections::hash_map::DefaultHasher;
use std::hash::{Hash, Hasher};
let mut hasher = DefaultHasher::new();
format!("{:?}", self.conclusion).hash(&mut hasher);
for step in &self.steps {
format!("{:?}", step).hash(&mut hasher);
}
self.timestamp.to_rfc3339().hash(&mut hasher);
format!("{:016x}", hasher.finish())
}
pub fn depth(&self) -> usize {
self.steps.iter().map(|s| s.depth).max().unwrap_or(0)
}
pub fn len(&self) -> usize {
self.steps.len()
}
pub fn is_empty(&self) -> bool {
self.steps.is_empty()
}
pub fn rules_used(&self) -> Vec<&str> {
let mut rules: Vec<_> = self.steps.iter().map(|s| s.rule_id.as_str()).collect();
rules.sort();
rules.dedup();
rules
}
pub fn to_json(&self) -> Result<String> {
serde_json::to_string_pretty(self).map_err(Error::from)
}
pub fn from_json(json: &str) -> Result<Self> {
serde_json::from_str(json).map_err(Error::from)
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum ProofConclusion {
Triple(TripleData),
Pattern(PatternData),
RuleApplication {
rule_id: String,
bindings: Vec<(String, String)>,
},
NoContradiction,
Consistent,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TripleData {
pub subject: String,
pub predicate: String,
pub object: String,
}
impl From<&Triple> for TripleData {
fn from(triple: &Triple) -> Self {
Self {
subject: node_id_to_string(&triple.subject),
predicate: triple.predicate.as_str().to_string(),
object: value_to_string(&triple.object),
}
}
}
fn node_id_to_string(node: &aingle_graph::NodeId) -> String {
match node {
aingle_graph::NodeId::Named(s) => s.clone(),
aingle_graph::NodeId::Hash(h) => format!("hash:{}", hex::encode(h)),
aingle_graph::NodeId::Blank(id) => format!("_:b{}", id),
}
}
impl From<Triple> for TripleData {
fn from(triple: Triple) -> Self {
Self::from(&triple)
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PatternData {
pub subject: Option<String>,
pub predicate: Option<String>,
pub object: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ProofStep {
pub step_num: usize,
pub rule_id: String,
pub step_type: StepType,
pub inputs: Vec<TripleData>,
pub output: Option<TripleData>,
pub bindings: Vec<(String, String)>,
pub depth: usize,
pub justification: String,
}
impl ProofStep {
pub fn fact(step_num: usize, triple: &Triple) -> Self {
Self {
step_num,
rule_id: "fact".to_string(),
step_type: StepType::Fact,
inputs: vec![],
output: Some(triple.into()),
bindings: vec![],
depth: 0,
justification: "Base fact from graph".to_string(),
}
}
pub fn inference(
step_num: usize,
rule_id: impl Into<String>,
inputs: Vec<&Triple>,
output: &Triple,
bindings: &Bindings,
depth: usize,
) -> Self {
Self {
step_num,
rule_id: rule_id.into(),
step_type: StepType::Inference,
inputs: inputs.into_iter().map(|t| t.into()).collect(),
output: Some(output.into()),
bindings: bindings_to_vec(bindings),
depth,
justification: "Derived by rule application".to_string(),
}
}
pub fn unification(
step_num: usize,
rule_id: impl Into<String>,
bindings: &Bindings,
depth: usize,
) -> Self {
Self {
step_num,
rule_id: rule_id.into(),
step_type: StepType::Unification,
inputs: vec![],
output: None,
bindings: bindings_to_vec(bindings),
depth,
justification: "Variable unification".to_string(),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum StepType {
Fact,
Inference,
Unification,
Assumption,
Contradiction,
SubProof,
}
pub struct ProofVerifier {
rules: HashMap<String, Rule>,
options: VerifyOptions,
}
impl ProofVerifier {
pub fn new() -> Self {
Self {
rules: HashMap::new(),
options: VerifyOptions::default(),
}
}
pub fn add_rule(&mut self, rule: Rule) {
self.rules.insert(rule.id.clone(), rule);
}
pub fn add_rules(&mut self, rules: &[Rule]) {
for rule in rules {
self.add_rule(rule.clone());
}
}
pub fn with_options(mut self, options: VerifyOptions) -> Self {
self.options = options;
self
}
pub fn verify(&self, proof: &LogicProof) -> VerifyResult {
let mut result = VerifyResult::new();
if self.options.check_hash {
let computed = proof.compute_hash();
if proof.hash != computed && !proof.hash.is_empty() {
result.add_error("Proof hash mismatch - proof may have been tampered");
return result;
}
}
for step in &proof.steps {
if !self.verify_step(step, proof) {
result.add_error(&format!(
"Invalid step {}: {}",
step.step_num, step.justification
));
}
}
if !self.verify_conclusion(proof) {
result.add_error("Conclusion does not follow from proof steps");
}
result.is_valid = result.errors.is_empty();
result
}
fn verify_step(&self, step: &ProofStep, _proof: &LogicProof) -> bool {
match step.step_type {
StepType::Fact => {
true
}
StepType::Inference => {
if step.rule_id == "fact" {
return true;
}
if self.options.check_rules {
if let Some(rule) = self.rules.get(&step.rule_id) {
rule.kind == RuleKind::Inference
} else {
!self.options.strict
}
} else {
true
}
}
StepType::Unification => {
let bindings: HashMap<_, _> = step.bindings.iter().cloned().collect();
bindings.len() == step.bindings.len() }
StepType::Assumption => true,
StepType::Contradiction => {
step.inputs.len() >= 2
}
StepType::SubProof => true,
}
}
fn verify_conclusion(&self, proof: &LogicProof) -> bool {
match &proof.conclusion {
ProofConclusion::Triple(triple) => {
proof.steps.iter().any(|s| {
s.output
.as_ref()
.map(|o| {
o.subject == triple.subject
&& o.predicate == triple.predicate
&& o.object == triple.object
})
.unwrap_or(false)
})
}
ProofConclusion::NoContradiction => {
!proof
.steps
.iter()
.any(|s| s.step_type == StepType::Contradiction)
}
ProofConclusion::Consistent => {
true
}
_ => true, }
}
}
impl Default for ProofVerifier {
fn default() -> Self {
Self::new()
}
}
#[derive(Debug, Clone)]
pub struct VerifyOptions {
pub check_hash: bool,
pub check_rules: bool,
pub strict: bool,
pub max_depth: Option<usize>,
}
impl Default for VerifyOptions {
fn default() -> Self {
Self {
check_hash: true,
check_rules: false,
strict: false,
max_depth: None,
}
}
}
#[derive(Debug, Clone, Default)]
pub struct VerifyResult {
pub is_valid: bool,
pub errors: Vec<String>,
pub warnings: Vec<String>,
}
impl VerifyResult {
pub fn new() -> Self {
Self {
is_valid: true,
errors: Vec::new(),
warnings: Vec::new(),
}
}
pub fn add_error(&mut self, msg: &str) {
self.is_valid = false;
self.errors.push(msg.to_string());
}
pub fn add_warning(&mut self, msg: &str) {
self.warnings.push(msg.to_string());
}
}
fn generate_proof_id() -> String {
use std::time::{SystemTime, UNIX_EPOCH};
let timestamp = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap()
.as_nanos();
format!("proof_{:016x}", timestamp)
}
fn value_to_string(value: &Value) -> String {
match value {
Value::Node(node) => node_id_to_string(node),
Value::String(s) => s.clone(),
Value::Integer(i) => i.to_string(),
Value::Float(f) => f.to_string(),
Value::Boolean(b) => b.to_string(),
Value::DateTime(dt) => dt.clone(),
Value::Bytes(b) => format!("0x{}", hex::encode(b)),
Value::Typed { value, .. } => value.clone(),
Value::LangString { value, .. } => value.clone(),
Value::Json(v) => v.to_string(),
Value::Null => "null".to_string(),
}
}
fn bindings_to_vec(_bindings: &Bindings) -> Vec<(String, String)> {
vec![]
}
#[cfg(test)]
mod tests {
use super::*;
use aingle_graph::{NodeId, Predicate};
#[test]
fn test_proof_creation() {
let triple = Triple::new(
NodeId::named("socrates"),
Predicate::named("is"),
Value::Node(NodeId::named("mortal")),
);
let mut proof = LogicProof::new(ProofConclusion::Triple((&triple).into()));
proof.add_step(ProofStep::fact(1, &triple));
proof.finalize();
assert!(!proof.hash.is_empty());
assert_eq!(proof.len(), 1);
}
#[test]
fn test_proof_serialization() {
let triple = Triple::new(
NodeId::named("socrates"),
Predicate::named("is"),
Value::Node(NodeId::named("human")),
);
let mut proof = LogicProof::new(ProofConclusion::Triple((&triple).into()));
proof.add_step(ProofStep::fact(1, &triple));
proof.finalize();
let json = proof.to_json().unwrap();
let restored = LogicProof::from_json(&json).unwrap();
assert_eq!(proof.id, restored.id);
assert_eq!(proof.len(), restored.len());
}
#[test]
fn test_proof_verification() {
let triple = Triple::new(
NodeId::named("a"),
Predicate::named("p"),
Value::literal("b"),
);
let mut proof = LogicProof::new(ProofConclusion::Triple((&triple).into()));
proof.add_step(ProofStep::fact(1, &triple));
proof.finalize();
let verifier = ProofVerifier::new();
let result = verifier.verify(&proof);
assert!(result.is_valid);
}
#[test]
fn test_proof_hash_tampering() {
let triple = Triple::new(
NodeId::named("x"),
Predicate::named("y"),
Value::literal("z"),
);
let mut proof = LogicProof::new(ProofConclusion::Triple((&triple).into()));
proof.add_step(ProofStep::fact(1, &triple));
proof.finalize();
proof.steps[0].justification = "Tampered!".to_string();
let verifier = ProofVerifier::new();
let result = verifier.verify(&proof);
assert!(!result.is_valid);
}
#[test]
fn test_proof_depth() {
let triple = Triple::new(
NodeId::named("a"),
Predicate::named("b"),
Value::literal("c"),
);
let mut proof = LogicProof::new(ProofConclusion::NoContradiction);
let mut step1 = ProofStep::fact(1, &triple);
step1.depth = 0;
let mut step2 = ProofStep::fact(2, &triple);
step2.depth = 1;
let mut step3 = ProofStep::fact(3, &triple);
step3.depth = 2;
proof.add_step(step1);
proof.add_step(step2);
proof.add_step(step3);
assert_eq!(proof.depth(), 2);
}
#[test]
fn test_step_types() {
let triple = Triple::new(
NodeId::named("test"),
Predicate::named("pred"),
Value::literal("val"),
);
let fact = ProofStep::fact(1, &triple);
assert_eq!(fact.step_type, StepType::Fact);
let bindings = Bindings::new();
let unif = ProofStep::unification(2, "rule1", &bindings, 1);
assert_eq!(unif.step_type, StepType::Unification);
}
}