Skip to main content

lean_ctx/core/knowledge/
source_view.rs

1// SPDX-License-Identifier: Apache-2.0
2//! A source-authorized read view is not a replacement for the complete store.
3use std::collections::HashMap;
4
5use super::{FactOrigin, KnowledgeFact, ProjectKnowledge, WithheldKnowledge};
6
7#[cfg(test)]
8#[path = "source_view_tests.rs"]
9mod tests;
10
11impl ProjectKnowledge {
12    pub fn admit_sources(self) -> Self {
13        let root = self.project_root.clone();
14        let original = self.clone();
15        if let Ok(view) = crate::core::policy::runtime::with_project_source_view(&root, || {
16            crate::core::providers::provenance::with_reuse_deadline(|| self.admit_sources_inner())
17        }) {
18            view.with_pulled_values_wrapped()
19        } else {
20            original.withhold_all_content()
21        }
22    }
23
24    /// X5 invariant of every loaded store: the value of a fact pulled from
25    /// Hive (other members' text) is a Hive wrapper. Values stored before
26    /// the engine wrapped them at storage are wrapped here; a store written
27    /// back (`mutate_locked`) keeps them so.
28    fn with_pulled_values_wrapped(mut self) -> Self {
29        use crate::core::untrusted::{UntrustedKind, is_wrapped, wrap};
30        for fact in &mut self.facts {
31            if let Some(origin) = fact.hive_origin().map(str::to_string)
32                && !is_wrapped(&fact.value)
33            {
34                fact.value = wrap(UntrustedKind::Knowledge, &origin, &fact.value);
35            }
36        }
37        self
38    }
39
40    fn withhold_all_content(mut self) -> Self {
41        self.withheld
42            .extend(self.facts.drain(..).map(WithheldKnowledge::Fact));
43        self.withheld
44            .extend(self.patterns.drain(..).map(WithheldKnowledge::Pattern));
45        self.withheld
46            .extend(self.history.drain(..).map(WithheldKnowledge::History));
47        self.withheld.extend(
48            self.judged_pairs
49                .drain(..)
50                .map(WithheldKnowledge::JudgedPair),
51        );
52        self.rebuild_index();
53        self
54    }
55
56    fn admit_sources_inner(mut self) -> Self {
57        let protected = if let Ok(policy) = super::protection::current(&self.project_root) {
58            policy.is_some()
59        } else {
60            return self.withhold_all_content();
61        };
62        let mut seen: HashMap<String, Vec<(String, String, String)>> = HashMap::new();
63        let mut admitted = Vec::new();
64        for fact in std::mem::take(&mut self.facts) {
65            let allowed = match &fact.origin {
66                FactOrigin::Local => true,
67                FactOrigin::Unverified => !protected,
68                // A derived value needs its own verifiable derivation before
69                // source-backed inputs can authorize it, even outside policy mode.
70                FactOrigin::Derived(_) => false,
71                FactOrigin::Provider(origin) => {
72                    let Some(key) = crate::core::providers::provenance::digest(origin) else {
73                        self.withheld.push(WithheldKnowledge::Fact(fact));
74                        continue;
75                    };
76                    if !seen.contains_key(&key) && seen.len() >= 32 {
77                        self.withheld.push(WithheldKnowledge::Fact(fact));
78                        continue;
79                    }
80                    let current = seen.entry(key).or_insert_with(|| {
81                        origin
82                            .current_facts(&self.project_root)
83                            .unwrap_or_default()
84                            .into_iter()
85                            .filter_map(|f| {
86                                let value = crate::core::policy::content::protect_active(&f.value)
87                                    .ok()?
88                                    .into_owned();
89                                Some((f.category, f.key, value))
90                            })
91                            .collect()
92                    });
93                    current.iter().any(|(category, key, value)| {
94                        category == &fact.category && key == &fact.key && value == &fact.value
95                    })
96                }
97            };
98            if allowed {
99                admitted.push(fact);
100            } else {
101                self.withheld.push(WithheldKnowledge::Fact(fact));
102            }
103        }
104        self.facts = admitted;
105        self.rebuild_index();
106        self
107    }
108
109    /// Reconstitute the complete candidate only at the checked storage boundary.
110    /// A stale view cannot resurrect a removed or changed hidden record.
111    pub(super) fn complete_for_storage(&self, existing: &Self) -> Result<Self, String> {
112        fn unique_json_match<'a, T: serde::Serialize>(
113            items: &'a [T],
114            hidden: &T,
115        ) -> Result<&'a T, String> {
116            let hidden = serde_json::to_value(hidden)
117                .map_err(|_| "knowledge read view is not inspectable")?;
118            let mut matches = items
119                .iter()
120                .filter(|current| serde_json::to_value(current).is_ok_and(|value| value == hidden));
121            let Some(current) = matches.next() else {
122                return Err("knowledge read view is stale; reload before saving".into());
123            };
124            if matches.next().is_some() {
125                return Err("knowledge read view is ambiguous; reload before saving".into());
126            }
127            Ok(current)
128        }
129
130        let mut result = self.clone();
131        for hidden in std::mem::take(&mut result.withheld) {
132            match hidden {
133                WithheldKnowledge::Fact(fact) => {
134                    let mut matches = existing.facts.iter().filter(|current| {
135                        same_record(current, &fact)
136                            && current.created_at == fact.created_at
137                            && current.source_session == fact.source_session
138                    });
139                    let Some(current) = matches.next() else {
140                        return Err("knowledge read view is stale; reload before saving".into());
141                    };
142                    if matches.next().is_some() {
143                        return Err("knowledge read view is ambiguous; reload before saving".into());
144                    }
145                    // Keep the latest counters and validity state; never restore an old
146                    // hidden copy over a concurrent update.
147                    result.facts.push(current.clone());
148                }
149                WithheldKnowledge::Pattern(pattern) => {
150                    result
151                        .patterns
152                        .push(unique_json_match(&existing.patterns, &pattern)?.clone());
153                }
154                WithheldKnowledge::History(history) => {
155                    result
156                        .history
157                        .push(unique_json_match(&existing.history, &history)?.clone());
158                }
159                WithheldKnowledge::JudgedPair(pair) => {
160                    result
161                        .judged_pairs
162                        .push(unique_json_match(&existing.judged_pairs, &pair)?.clone());
163                }
164            }
165        }
166        result.rebuild_index();
167        Ok(result)
168    }
169}
170
171pub(crate) fn imported_origin(origin: &FactOrigin) -> FactOrigin {
172    match origin {
173        // Source dependencies survive import; the next read reacquires them.
174        FactOrigin::Provider(source) => FactOrigin::Provider(source.clone()),
175        FactOrigin::Local | FactOrigin::Unverified => FactOrigin::Unverified,
176        FactOrigin::Derived(sources) => {
177            FactOrigin::Derived(sources.iter().map(imported_origin).collect())
178        }
179    }
180}
181
182pub fn same_record(a: &KnowledgeFact, b: &KnowledgeFact) -> bool {
183    a.category == b.category && a.key == b.key && a.value == b.value && a.origin == b.origin
184}