1use std::collections::{BTreeSet, HashMap};
24
25use serde::Serialize;
26
27use super::Engine;
28use crate::check::{CheckRecord, CheckState};
29
30#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
32#[serde(rename_all = "snake_case")]
33pub enum Independence {
34 ConfirmedIndependent,
37 SelfChecked,
39 Unconfirmable,
42}
43
44impl Independence {
45 pub fn as_str(self) -> &'static str {
46 match self {
47 Self::ConfirmedIndependent => "confirmed_independent",
48 Self::SelfChecked => "self_checked",
49 Self::Unconfirmable => "unconfirmable",
50 }
51 }
52}
53
54#[derive(Debug, Clone, PartialEq, Eq)]
57pub struct CheckStanding {
58 pub state: CheckState,
59 pub independence: Option<Independence>,
61}
62
63impl CheckStanding {
64 pub fn assumed_independent(state: CheckState) -> Self {
68 Self {
69 state,
70 independence: (state == CheckState::CheckedOk)
71 .then_some(Independence::ConfirmedIndependent),
72 }
73 }
74
75 pub fn confirms(&self) -> bool {
77 self.state == CheckState::CheckedOk
78 && self.independence == Some(Independence::ConfirmedIndependent)
79 }
80
81 pub fn label(&self) -> &'static str {
85 match (self.state, self.independence) {
86 (CheckState::CheckedOk, Some(i)) => i.as_str(),
87 (CheckState::CheckedOk, None) => Independence::Unconfirmable.as_str(),
88 (s, _) => s.as_str(),
89 }
90 }
91}
92
93#[derive(Debug, Default, Clone)]
98pub struct MemTouches {
99 by_entity: HashMap<String, Vec<(i64, Option<String>)>>,
100}
101
102impl MemTouches {
103 fn written_at(&self, entity: &str) -> Option<i64> {
105 self.by_entity
106 .get(entity)
107 .and_then(|t| t.iter().map(|(ts, _)| *ts).min())
108 }
109
110 fn identities_since(&self, entity: &str, since: i64, into: &mut BTreeSet<String>) {
112 if let Some(touches) = self.by_entity.get(entity) {
113 for (ts, id) in touches {
114 if *ts >= since
115 && let Some(id) = id
116 {
117 into.insert(id.clone());
118 }
119 }
120 }
121 }
122
123 fn any_identity(&self, entity: &str) -> bool {
125 self.by_entity
126 .get(entity)
127 .is_some_and(|t| t.iter().any(|(_, id)| id.is_some()))
128 }
129}
130
131#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize)]
134pub struct Executors {
135 pub identities: Vec<String>,
136 pub plans: Vec<String>,
137}
138
139impl Engine {
140 pub fn mem_touches(&self, mem: &str) -> MemTouches {
146 let mut out = MemTouches::default();
147 let Some(m) = self.mounts.iter().find(|m| m.mount.mem == mem) else {
148 return out;
149 };
150 match &m.mount.storage {
151 crate::workspace::MountStorage::GitBranch { gitdir, branch } => {
152 if let Some(hook) = self.git_branch_ops.as_ref()
153 && let Ok(changes) = (hook.changes_since)(
154 gitdir,
155 branch,
156 mem,
157 crate::ops::EMPTY_TREE_SHA,
158 crate::ops::RENAME_SIMILARITY_DEFAULT,
159 )
160 {
161 for n in &changes.notes {
162 let Some(entity) = n.entity_id.as_deref() else {
163 continue;
164 };
165 for id in entity.split("->").map(str::trim).filter(|s| !s.is_empty()) {
168 out.by_entity
169 .entry(id.to_string())
170 .or_default()
171 .push((n.timestamp, n.identity.clone()));
172 }
173 }
174 }
175 }
176 crate::workspace::MountStorage::Folder { .. }
177 | crate::workspace::MountStorage::InMemory => {
178 if let Ok(records) = m.backend.read_provenance(None) {
179 for r in records {
180 let Some(entity) = r.entity.as_deref() else {
181 continue;
182 };
183 let ts = r
184 .timestamp
185 .duration_since(std::time::UNIX_EPOCH)
186 .map(|d| d.as_secs() as i64)
187 .unwrap_or(0);
188 out.by_entity
189 .entry(entity.to_string())
190 .or_default()
191 .push((ts, r.identity.clone()));
192 }
193 }
194 }
195 crate::workspace::MountStorage::Archive { .. } => {}
196 }
197 out
198 }
199
200 pub fn executors_of(
206 &self,
207 entity: &crate::entity::Entity,
208 touches: &MemTouches,
209 ) -> Option<Executors> {
210 let plans: Vec<crate::entity::EntityId> = entity
211 .relationships
212 .iter()
213 .filter(|r| r.rel_type == "VERIFIES")
214 .map(|r| r.target.clone())
215 .collect();
216 if plans.is_empty() {
217 return None;
218 }
219 let since = touches.written_at(&entity.id.0).unwrap_or(0);
220 let mut set: BTreeSet<String> = BTreeSet::new();
221 let mut members: BTreeSet<String> = BTreeSet::new();
222 for plan in &plans {
223 members.insert(plan.0.clone());
224 for other in self.store.all_entities().filter(|o| o.mem == entity.mem) {
225 if other.relationships.iter().any(|r| {
226 &r.target == plan && (r.rel_type == "VERIFIES" || r.rel_type == "PART_OF")
227 }) {
228 members.insert(other.id.0.clone());
229 }
230 }
231 }
232 for member in &members {
233 touches.identities_since(member, since, &mut set);
234 }
235 Some(Executors {
236 identities: set.into_iter().collect(),
237 plans: plans.into_iter().map(|p| p.0).collect(),
238 })
239 }
240
241 pub fn independence_of(
243 &self,
244 entity: &crate::entity::Entity,
245 check: &CheckRecord,
246 touches: &MemTouches,
247 ) -> (Independence, Option<Executors>) {
248 let Some(checker) = check.identity.as_deref() else {
249 return (Independence::Unconfirmable, None);
250 };
251 match self.executors_of(entity, touches) {
252 Some(executors) => {
253 let reading = if executors.identities.iter().any(|i| i == checker) {
254 Independence::SelfChecked
255 } else if executors.identities.is_empty()
256 && !executors.plans.iter().any(|p| touches.any_identity(p))
257 && !touches.any_identity(&entity.id.0)
258 {
259 Independence::Unconfirmable
262 } else {
263 Independence::ConfirmedIndependent
264 };
265 (reading, Some(executors))
266 }
267 None => {
268 let author = touches
270 .by_entity
271 .get(&entity.id.0)
272 .and_then(|t| t.iter().min_by_key(|(ts, _)| *ts))
273 .and_then(|(_, id)| id.clone());
274 let reading = match author {
275 Some(a) if a == checker => Independence::SelfChecked,
276 Some(_) => Independence::ConfirmedIndependent,
277 None => Independence::Unconfirmable,
278 };
279 (reading, None)
280 }
281 }
282 }
283
284 pub(crate) fn check_standing_provider(
288 &self,
289 ) -> impl Fn(&crate::entity::Entity) -> CheckStanding + '_ {
290 let ledger = self
291 .workspace_root()
292 .map(crate::check::CheckLedger::for_workspace);
293 let touches: std::cell::RefCell<HashMap<String, MemTouches>> =
294 std::cell::RefCell::new(HashMap::new());
295 move |entity: &crate::entity::Entity| {
296 let Some(ledger) = &ledger else {
297 return CheckStanding {
298 state: CheckState::NeverChecked,
299 independence: None,
300 };
301 };
302 let latest =
303 ledger.latest_for_kind(&entity.id.0, crate::check::CheckKind::Verification);
304 let state = crate::check::derive_state(latest.as_ref(), &entity.content_hash);
305 if state != CheckState::CheckedOk {
306 return CheckStanding {
307 state,
308 independence: None,
309 };
310 }
311 let check = latest.expect("checked_ok implies a record");
312 let mut cache = touches.borrow_mut();
313 let mem_touches = cache
314 .entry(entity.mem.clone())
315 .or_insert_with(|| self.mem_touches(&entity.mem));
316 let (independence, _) = self.independence_of(entity, &check, mem_touches);
317 CheckStanding {
318 state,
319 independence: Some(independence),
320 }
321 }
322 }
323}