use std::collections::{BTreeSet, HashMap};
use serde::Serialize;
use super::Engine;
use crate::check::{CheckRecord, CheckState};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
#[serde(rename_all = "snake_case")]
pub enum Independence {
ConfirmedIndependent,
SelfChecked,
Unconfirmable,
}
impl Independence {
pub fn as_str(self) -> &'static str {
match self {
Self::ConfirmedIndependent => "confirmed_independent",
Self::SelfChecked => "self_checked",
Self::Unconfirmable => "unconfirmable",
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct CheckStanding {
pub state: CheckState,
pub independence: Option<Independence>,
}
impl CheckStanding {
pub fn assumed_independent(state: CheckState) -> Self {
Self {
state,
independence: (state == CheckState::CheckedOk)
.then_some(Independence::ConfirmedIndependent),
}
}
pub fn confirms(&self) -> bool {
self.state == CheckState::CheckedOk
&& self.independence == Some(Independence::ConfirmedIndependent)
}
pub fn label(&self) -> &'static str {
match (self.state, self.independence) {
(CheckState::CheckedOk, Some(i)) => i.as_str(),
(CheckState::CheckedOk, None) => Independence::Unconfirmable.as_str(),
(s, _) => s.as_str(),
}
}
}
#[derive(Debug, Default, Clone)]
pub struct MemTouches {
by_entity: HashMap<String, Vec<(i64, Option<String>)>>,
}
impl MemTouches {
fn written_at(&self, entity: &str) -> Option<i64> {
self.by_entity
.get(entity)
.and_then(|t| t.iter().map(|(ts, _)| *ts).min())
}
fn identities_since(&self, entity: &str, since: i64, into: &mut BTreeSet<String>) {
if let Some(touches) = self.by_entity.get(entity) {
for (ts, id) in touches {
if *ts >= since
&& let Some(id) = id
{
into.insert(id.clone());
}
}
}
}
fn any_identity(&self, entity: &str) -> bool {
self.by_entity
.get(entity)
.is_some_and(|t| t.iter().any(|(_, id)| id.is_some()))
}
}
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize)]
pub struct Executors {
pub identities: Vec<String>,
pub plans: Vec<String>,
}
impl Engine {
pub fn mem_touches(&self, mem: &str) -> MemTouches {
let mut out = MemTouches::default();
let Some(m) = self.mounts.iter().find(|m| m.mount.mem == mem) else {
return out;
};
match &m.mount.storage {
crate::workspace::MountStorage::GitBranch { gitdir, branch } => {
if let Some(hook) = self.git_branch_ops.as_ref()
&& let Ok(changes) = (hook.changes_since)(
gitdir,
branch,
mem,
crate::ops::EMPTY_TREE_SHA,
crate::ops::RENAME_SIMILARITY_DEFAULT,
)
{
for n in &changes.notes {
let Some(entity) = n.entity_id.as_deref() else {
continue;
};
for id in entity.split("->").map(str::trim).filter(|s| !s.is_empty()) {
out.by_entity
.entry(id.to_string())
.or_default()
.push((n.timestamp, n.identity.clone()));
}
}
}
}
crate::workspace::MountStorage::Folder { .. }
| crate::workspace::MountStorage::InMemory => {
if let Ok(records) = m.backend.read_provenance(None) {
for r in records {
let Some(entity) = r.entity.as_deref() else {
continue;
};
let ts = r
.timestamp
.duration_since(std::time::UNIX_EPOCH)
.map(|d| d.as_secs() as i64)
.unwrap_or(0);
out.by_entity
.entry(entity.to_string())
.or_default()
.push((ts, r.identity.clone()));
}
}
}
crate::workspace::MountStorage::Archive { .. } => {}
}
out
}
pub fn executors_of(
&self,
entity: &crate::entity::Entity,
touches: &MemTouches,
) -> Option<Executors> {
let plans: Vec<crate::entity::EntityId> = entity
.relationships
.iter()
.filter(|r| r.rel_type == "VERIFIES")
.map(|r| r.target.clone())
.collect();
if plans.is_empty() {
return None;
}
let since = touches.written_at(&entity.id.0).unwrap_or(0);
let mut set: BTreeSet<String> = BTreeSet::new();
let mut members: BTreeSet<String> = BTreeSet::new();
for plan in &plans {
members.insert(plan.0.clone());
for other in self.store.all_entities().filter(|o| o.mem == entity.mem) {
if other.relationships.iter().any(|r| {
&r.target == plan && (r.rel_type == "VERIFIES" || r.rel_type == "PART_OF")
}) {
members.insert(other.id.0.clone());
}
}
}
for member in &members {
touches.identities_since(member, since, &mut set);
}
Some(Executors {
identities: set.into_iter().collect(),
plans: plans.into_iter().map(|p| p.0).collect(),
})
}
pub fn independence_of(
&self,
entity: &crate::entity::Entity,
check: &CheckRecord,
touches: &MemTouches,
) -> (Independence, Option<Executors>) {
let Some(checker) = check.identity.as_deref() else {
return (Independence::Unconfirmable, None);
};
match self.executors_of(entity, touches) {
Some(executors) => {
let reading = if executors.identities.iter().any(|i| i == checker) {
Independence::SelfChecked
} else if executors.identities.is_empty()
&& !executors.plans.iter().any(|p| touches.any_identity(p))
&& !touches.any_identity(&entity.id.0)
{
Independence::Unconfirmable
} else {
Independence::ConfirmedIndependent
};
(reading, Some(executors))
}
None => {
let author = touches
.by_entity
.get(&entity.id.0)
.and_then(|t| t.iter().min_by_key(|(ts, _)| *ts))
.and_then(|(_, id)| id.clone());
let reading = match author {
Some(a) if a == checker => Independence::SelfChecked,
Some(_) => Independence::ConfirmedIndependent,
None => Independence::Unconfirmable,
};
(reading, None)
}
}
}
pub(crate) fn check_standing_provider(
&self,
) -> impl Fn(&crate::entity::Entity) -> CheckStanding + '_ {
let ledger = self
.workspace_root()
.map(crate::check::CheckLedger::for_workspace);
let touches: std::cell::RefCell<HashMap<String, MemTouches>> =
std::cell::RefCell::new(HashMap::new());
move |entity: &crate::entity::Entity| {
let Some(ledger) = &ledger else {
return CheckStanding {
state: CheckState::NeverChecked,
independence: None,
};
};
let latest =
ledger.latest_for_kind(&entity.id.0, crate::check::CheckKind::Verification);
let state = crate::check::derive_state(latest.as_ref(), &entity.content_hash);
if state != CheckState::CheckedOk {
return CheckStanding {
state,
independence: None,
};
}
let check = latest.expect("checked_ok implies a record");
let mut cache = touches.borrow_mut();
let mem_touches = cache
.entry(entity.mem.clone())
.or_insert_with(|| self.mem_touches(&entity.mem));
let (independence, _) = self.independence_of(entity, &check, mem_touches);
CheckStanding {
state,
independence: Some(independence),
}
}
}
}