use crate::sync::SyncError;
use ggen_graph::coherence::{CoherenceChecker, CoherenceReport, DriftKind, Pole, PoleState};
use std::collections::HashMap;
use std::path::Path;
#[derive(Debug, Clone)]
pub struct CoherenceGateConfig {
pub allow_count_discrepancy: bool,
pub check_event_log: bool,
pub expectations: Option<HashMap<Pole, String>>,
}
impl Default for CoherenceGateConfig {
fn default() -> Self {
Self {
allow_count_discrepancy: false,
check_event_log: true,
expectations: None,
}
}
}
pub struct CoherenceGate {
config: CoherenceGateConfig,
}
impl CoherenceGate {
pub fn new(config: CoherenceGateConfig) -> Self {
Self { config }
}
pub fn validate(
&self, ontology_bytes: &[u8], generated_files: &[(impl AsRef<Path>, String)],
event_log_events: &[&str],
) -> Result<CoherenceReport, SyncError> {
let ontology_str =
std::str::from_utf8(ontology_bytes).map_err(|e| SyncError::CoherenceViolation {
detail: format!("Cannot decode ontology as UTF-8: {}", e),
report: CoherenceChecker::check(&[]), })?;
let ontology_pole = CoherenceChecker::fingerprint_ontology(&[ontology_str]);
let artifact_pairs: Vec<(&str, u64)> = generated_files
.iter()
.map(|(p, content)| {
let path_str = p.as_ref().to_str().unwrap_or("<invalid-utf8-path>");
(path_str, content.len() as u64)
})
.collect();
let artifact_pole = CoherenceChecker::fingerprint_artifacts(&artifact_pairs);
let event_log_pole = if self.config.check_event_log {
CoherenceChecker::fingerprint_event_log(event_log_events)
} else {
CoherenceChecker::fingerprint_event_log(&[])
};
tracing::debug!(
target: "ggen_core",
event = "coherence.check_started",
ontology.item_count = ontology_pole.item_count,
artifact.item_count = artifact_pole.item_count,
event_log.item_count = event_log_pole.item_count,
"Starting coherence check for three poles"
);
let report = if let Some(expectations) = &self.config.expectations {
CoherenceChecker::check_with_expectations(
&[ontology_pole, artifact_pole, event_log_pole],
expectations,
)
} else {
CoherenceChecker::check(&[ontology_pole, artifact_pole, event_log_pole])
};
tracing::info!(
target: "ggen_core",
event = "coherence.check_completed",
coherence.admitted = report.admitted,
coherence.pole_count = report.poles.len(),
coherence.drift_count = report.drifts.len(),
"Coherence check completed"
);
let blocking_drifts: Vec<_> = report
.drifts
.iter()
.filter(|d| {
if !self.config.check_event_log
&& (d.source_pole == Pole::EventLog || d.target_pole == Pole::EventLog)
{
return false;
}
matches!(d.kind, DriftKind::Missing | DriftKind::HashMismatch)
|| (!self.config.allow_count_discrepancy
&& matches!(d.kind, DriftKind::CountDiscrepancy))
})
.collect();
if !blocking_drifts.is_empty() {
for drift in &blocking_drifts {
tracing::warn!(
target: "ggen_core",
event = "coherence.drift_detected",
drift.kind = ?drift.kind,
drift.source_pole = ?drift.source_pole,
drift.target_pole = ?drift.target_pole,
drift.detail = &drift.detail,
"Coherence drift detected (blocking)"
);
}
let detail = format!(
"Coherence check failed: {} blocking drift(s) detected",
blocking_drifts.len()
);
return Err(SyncError::CoherenceViolation { detail, report });
}
for drift in &report.drifts {
tracing::warn!(
target: "ggen_core",
event = "coherence.drift_detected",
drift.kind = ?drift.kind,
drift.source_pole = ?drift.source_pole,
drift.target_pole = ?drift.target_pole,
drift.detail = &drift.detail,
"Coherence drift detected (non-blocking)"
);
}
tracing::info!(
target: "ggen_core",
event = "coherence.admitted",
operation_id = &report.operation_id,
"Coherence check admitted — three poles are isomorphic"
);
Ok(report)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_gate_admits_full_coherence() {
let config = CoherenceGateConfig {
allow_count_discrepancy: true,
..Default::default()
};
let gate = CoherenceGate::new(config);
let ontology_bytes =
b"<https://example.org/s> <https://example.org/p> <https://example.org/o> .";
let generated = vec![("test.rs", "fn main() {}".to_string())];
let events = vec!["{ \"id\": \"test\" }"];
let report = gate.validate(ontology_bytes, &generated, &events);
assert!(report.is_ok());
let report = report.unwrap();
assert!(
report.admitted,
"expected full coherence with populated event log when check_event_log=true"
);
}
#[test]
fn test_gate_rejects_missing_pole() {
let config = CoherenceGateConfig {
check_event_log: false, ..Default::default()
};
let gate = CoherenceGate::new(config);
let ontology_bytes =
b"<https://example.org/s> <https://example.org/p> <https://example.org/o> .";
let generated = vec![("test.rs", "fn main() {}".to_string())];
let events = vec![];
let report = gate.validate(ontology_bytes, &generated, &events);
assert!(report.is_ok());
let report = report.unwrap();
assert!(
!report.admitted,
"missing event-log pole should prevent admission"
);
}
}