lean_ctx/core/knowledge/
source_view.rs1use 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 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 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 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 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 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}