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| {
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::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, "expected full coherence with empty 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");
}
}