use crate::identity::{ScopedElementRef, SourceSpan};
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
pub enum EvidenceTier {
E0,
E1,
E2,
E3,
E4,
E5,
}
impl EvidenceTier {
pub fn as_str(self) -> &'static str {
match self {
Self::E0 => "E0",
Self::E1 => "E1",
Self::E2 => "E2",
Self::E3 => "E3",
Self::E4 => "E4",
Self::E5 => "E5",
}
}
pub fn meets_verified_gate(self) -> bool {
self >= Self::E4
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct EvidenceRecord {
pub tier: EvidenceTier,
pub summary: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub fixture_id: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub span: Option<SourceSpan>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub non_claims: Vec<String>,
}
impl EvidenceRecord {
pub fn research_stub(summary: impl Into<String>) -> Self {
Self {
tier: EvidenceTier::E0,
summary: summary.into(),
fixture_id: None,
span: None,
non_claims: vec![
"Does not claim ES ElementId remapping works".into(),
"Does not claim converter-grade IFC".into(),
],
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum EdgeKind {
EsElementIdRef,
EsValueTree,
BimRelation,
ElemTableOwnership,
Other(String),
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct TypedEdge {
pub kind: EdgeKind,
pub from: ScopedElementRef,
pub to: ScopedElementRef,
pub tier: EvidenceTier,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub span: Option<SourceSpan>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub notes: Vec<String>,
}
#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
pub struct EvidenceLedger {
pub records: Vec<EvidenceRecord>,
}
impl EvidenceLedger {
pub fn push(&mut self, record: EvidenceRecord) {
self.records.push(record);
}
pub fn max_tier(&self) -> Option<EvidenceTier> {
self.records.iter().map(|r| r.tier).max()
}
pub fn to_json_string(&self) -> Result<String, serde_json::Error> {
serde_json::to_string_pretty(self)
}
pub fn from_json_str(s: &str) -> Result<Self, serde_json::Error> {
serde_json::from_str(s)
}
}
#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
pub struct EdgeLedger {
pub edges: Vec<TypedEdge>,
}
impl EdgeLedger {
pub fn push(&mut self, edge: TypedEdge) {
self.edges.push(edge);
}
pub fn es_edges(&self) -> impl Iterator<Item = &TypedEdge> {
self.edges
.iter()
.filter(|e| matches!(e.kind, EdgeKind::EsElementIdRef | EdgeKind::EsValueTree))
}
pub fn to_json_string(&self) -> Result<String, serde_json::Error> {
serde_json::to_string_pretty(self)
}
pub fn from_json_str(s: &str) -> Result<Self, serde_json::Error> {
serde_json::from_str(s)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::identity::DocumentIdentity;
#[test]
fn verified_gate_requires_e4() {
assert!(!EvidenceTier::E3.meets_verified_gate());
assert!(EvidenceTier::E4.meets_verified_gate());
}
#[test]
fn ledger_round_trips_and_separates_es_edges() {
let doc = DocumentIdentity::from_key("doc");
let mut edges = EdgeLedger::default();
edges.push(TypedEdge {
kind: EdgeKind::EsElementIdRef,
from: ScopedElementRef::from_element_id(doc.clone(), 1),
to: ScopedElementRef::from_element_id(doc.clone(), 2),
tier: EvidenceTier::E0,
span: None,
notes: vec!["stub".into()],
});
edges.push(TypedEdge {
kind: EdgeKind::ElemTableOwnership,
from: ScopedElementRef::from_element_id(doc.clone(), 3),
to: ScopedElementRef::from_element_id(doc, 4),
tier: EvidenceTier::E1,
span: None,
notes: vec![],
});
assert_eq!(edges.es_edges().count(), 1);
let json = serde_json::to_value(&edges).expect("ser");
let back: EdgeLedger = serde_json::from_value(json).expect("de");
assert_eq!(back.edges.len(), 2);
let mut evidence = EvidenceLedger::default();
evidence.push(EvidenceRecord::research_stub("phase1 leftover"));
let e_json = evidence.to_json_string().expect("evidence ser");
let e_back = EvidenceLedger::from_json_str(&e_json).expect("evidence de");
assert_eq!(e_back.records.len(), 1);
assert!(e_back.records[0].non_claims.len() >= 2);
let edge_json = edges.to_json_string().expect("edge ser");
let edge_back = EdgeLedger::from_json_str(&edge_json).expect("edge de");
assert_eq!(edge_back.es_edges().count(), 1);
}
}