use corpora_core::{Diagnostic, DocPath, Graph, OracleStatus, RevisionOracle};
use crate::Rule;
pub struct GateB {
oracle: Box<dyn RevisionOracle>,
}
impl GateB {
pub fn new(oracle: Box<dyn RevisionOracle>) -> Self {
GateB { oracle }
}
}
impl Rule for GateB {
fn code(&self) -> &'static str {
"GATE_B"
}
fn check(&self, g: &Graph, out: &mut Vec<Diagnostic>) {
match self.oracle.status() {
OracleStatus::Disabled => return,
OracleStatus::Unavailable => {
out.push(Diagnostic::warn(
"GATE_B",
&DocPath("(repository)".into()),
"git is unavailable; code_revision pins are unverified".to_string(),
));
return;
}
OracleStatus::NoRepo => {
let pins = g.records().filter(|r| r.facet.code_revision().is_some()).count();
if pins > 0 {
out.push(Diagnostic::warn(
"GATE_B",
&DocPath("(repository)".into()),
format!("no git repository found; {pins} code_revision pin(s) unverified"),
));
}
return;
}
OracleStatus::Ready => {}
}
for r in g.records() {
if let Some(rev) = r.facet.code_revision() {
if self.oracle.resolve(rev).is_none() {
out.push(Diagnostic::error(
"GATE_B",
&r.path,
format!("code_revision {} does not resolve in the repository", rev.0),
));
}
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use corpora_core::*;
use std::sync::Arc;
struct FakeOracle {
status: OracleStatus,
resolves: bool,
}
impl RevisionOracle for FakeOracle {
fn status(&self) -> OracleStatus {
self.status
}
fn resolve(&self, _r: &Rev) -> Option<Rev> {
self.resolves.then(|| Rev("resolved".into()))
}
}
fn current_record(rev: &str) -> Record {
Record::minimal(
Some(Id("C1".into())),
DocPath("c.md".into()),
Kind::Current,
Lifecycle::Current,
Authority::Descriptive,
Facet::Current {
implementation: Impl::Implemented,
code_revision: Rev(rev.into()),
source_revision: None,
},
)
}
fn run(oracle: Box<dyn RevisionOracle>, records: Vec<Record>) -> Vec<Diagnostic> {
let g = Graph::build(records.into_iter().map(Arc::new).collect()).0;
let mut out = Vec::new();
GateB::new(oracle).check(&g, &mut out);
out
}
#[test]
fn unresolvable_revision_flagged() {
let oracle = Box::new(FakeOracle { status: OracleStatus::Ready, resolves: false });
let out = run(oracle, vec![current_record("deadbeef")]);
assert_eq!(out.len(), 1);
assert_eq!(out[0].code, "GATE_B");
assert_eq!(out[0].severity, Severity::Error);
}
#[test]
fn resolvable_revision_passes() {
let oracle = Box::new(FakeOracle { status: OracleStatus::Ready, resolves: true });
let out = run(oracle, vec![current_record("cafef00d")]);
assert!(out.is_empty(), "{out:?}");
}
#[test]
fn disabled_skips_silently_even_with_pins() {
assert!(run(Box::new(NullOracle), vec![current_record("deadbeef")]).is_empty());
}
#[test]
fn no_repo_warns_when_there_are_pins() {
let oracle = Box::new(FakeOracle { status: OracleStatus::NoRepo, resolves: false });
let out = run(oracle, vec![current_record("deadbeef")]);
assert_eq!(out.len(), 1);
assert_eq!(out[0].severity, Severity::Warning);
}
#[test]
fn no_repo_silent_without_pins() {
let narrative = Record::minimal(
Some(Id("D1".into())),
DocPath("d1.md".into()),
Kind::Decision,
Lifecycle::Current,
Authority::Normative,
Facet::Narrative,
);
let oracle = Box::new(FakeOracle { status: OracleStatus::NoRepo, resolves: false });
assert!(run(oracle, vec![narrative]).is_empty());
}
#[test]
fn unavailable_git_warns() {
let oracle = Box::new(FakeOracle { status: OracleStatus::Unavailable, resolves: false });
let out = run(oracle, vec![current_record("deadbeef")]);
assert_eq!(out.len(), 1);
assert_eq!(out[0].severity, Severity::Warning);
}
}