Skip to main content

memstead_base/engine/
independence.rs

1//! The author≠checker independence reading, derived from provenance.
2//!
3//! A check record confirms an acceptance criterion only when the party
4//! that did the work did not record it. Until 2026-09-02 the reading
5//! compared a check's identity with the identity that CREATED the
6//! criterion entity — and the planning session authors criteria while the
7//! executing session checks them, so the executor's own checks read
8//! `confirmed_independent` (found twice by the evidence-engine bundle,
9//! once on a wrong check). The comparator now (decision basket line 9,
10//! option a): a check on a criterion reads `confirmed_independent` only
11//! when its identity differs from **every identity that mutated the
12//! verified plan, its criteria, or its session-log notes since the
13//! criterion was written**; a check under one of those identities reads
14//! `self_checked`; a check or a record without an identity stays
15//! `unconfirmable`. Identities are the only comparator; roles and the
16//! transport pair are recorded context. Nothing is stamped: the reading
17//! is computed at read time from the append-only provenance record, so
18//! every existing ledger keeps parsing and derives under the new rule.
19//!
20//! The `transition_requires_checks` gate consumes the same reading: a
21//! plan cannot complete on the executor's own checks.
22
23use std::collections::{BTreeSet, HashMap};
24
25use serde::Serialize;
26
27use super::Engine;
28use crate::check::{CheckRecord, CheckState};
29
30/// The independence half of a check's standing.
31#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
32#[serde(rename_all = "snake_case")]
33pub enum Independence {
34    /// The check's identity mutated nothing in the verified plan's set
35    /// since the criterion was written.
36    ConfirmedIndependent,
37    /// The check's identity is one of the executors.
38    SelfChecked,
39    /// The check, or every relevant provenance record, carries no
40    /// identity — absence is never promoted.
41    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/// One entity's standing before the gate: its derived check state, and
55/// for an ok-checked entity the independence of that check.
56#[derive(Debug, Clone, PartialEq, Eq)]
57pub struct CheckStanding {
58    pub state: CheckState,
59    /// `Some` only for `CheckedOk`.
60    pub independence: Option<Independence>,
61}
62
63impl CheckStanding {
64    /// A standing that confirms when the state is `checked_ok` — the
65    /// form store-only callers (and tests) build when no provenance is
66    /// in play.
67    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    /// Whether this standing satisfies the transition gate.
76    pub fn confirms(&self) -> bool {
77        self.state == CheckState::CheckedOk
78            && self.independence == Some(Independence::ConfirmedIndependent)
79    }
80
81    /// The label the gate reports for an entity that does not confirm:
82    /// the check state, or the independence reading when the state is
83    /// `checked_ok` but not independent.
84    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/// One mem's mutation touches by entity: `(timestamp, identity)` per
94/// touch, from the mem's provenance record (the git-branch note trailers,
95/// or the folder ledger). Built once per mem and shared by every
96/// derivation in one pass.
97#[derive(Debug, Default, Clone)]
98pub struct MemTouches {
99    by_entity: HashMap<String, Vec<(i64, Option<String>)>>,
100}
101
102impl MemTouches {
103    /// The oldest touch timestamp of `entity`, if any is recorded.
104    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    /// Identities that touched `entity` at or after `since`.
111    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    /// Whether any touch of `entity` carries an identity.
124    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/// The executors of one criterion: the identities the reading compares
132/// against, and the plans the set was drawn from.
133#[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    /// Gather one mem's touches from its provenance record. Git-branch
141    /// mems walk the branch's commit notes once; folder and in-memory
142    /// mems read their ledger; an archive records no history at the
143    /// engine seam and yields no touches (its checks stay
144    /// `unconfirmable` unless the ledger's own author identity decides).
145    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                        // A rename note names `old -> new`; both ids are the
166                        // same entity's story.
167                        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    /// The executors of `entity` (a criterion): every identity that
201    /// mutated the plan(s) it VERIFIES, those plans' other criteria, or the
202    /// notes PART_OF those plans, at or after the criterion's first touch.
203    /// `None` when the entity verifies no plan — the reading then falls
204    /// back to the entity's own author.
205    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    /// The independence reading of one ok check on `entity`.
242    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                    // Nothing in the plan's set carries an identity: no
260                    // comparator exists, the reading cannot be promoted.
261                    Independence::Unconfirmable
262                } else {
263                    Independence::ConfirmedIndependent
264                };
265                (reading, Some(executors))
266            }
267            None => {
268                // Not a criterion: today's rule, the entity's own author.
269                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    /// The gate's window into the ledger: derived state plus, for an
285    /// ok-checked entity, the independence of that check — with each
286    /// mem's touches gathered once per provider.
287    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}