1use scc_api::QueryRequest;
7
8pub struct QueryHit {
10 pub entities: Vec<scc_core::Entity>,
11 pub symbols: Vec<(String, String, String, String, u32)>,
13}
14
15pub fn query(store: &scc_store::Store, req: &QueryRequest) -> crate::Result<QueryHit> {
17 let limit = req.limit.max(1);
18 let entities = store.search_entities(&req.query, limit)?;
19 let symbols = store.search_symbols(&req.query, limit)?;
20 let (entities, symbols) = if entities.is_empty() && symbols.is_empty() {
21 (
22 store.search_entities_like(&req.query, limit)?,
23 store.search_symbols_like(&req.query, limit)?,
24 )
25 } else {
26 (entities, symbols)
27 };
28 Ok(QueryHit { entities, symbols })
29}
30
31pub fn components(store: &scc_store::Store) -> crate::Result<Vec<scc_core::Entity>> {
33 Ok(store.components()?)
34}
35
36pub fn flows(store: &scc_store::Store) -> crate::Result<Vec<scc_core::Flow>> {
38 Ok(store.flows()?)
39}
40
41pub fn relationships(
43 store: &scc_store::Store,
44 subject: Option<&str>,
45 predicate: Option<&str>,
46 limit: usize,
47) -> crate::Result<Vec<scc_core::Relationship>> {
48 let mut out = if let Some(s) = subject {
49 store.relationships_for(s)?
50 } else {
51 store.all_relationships()?
52 };
53 if let Some(p) = predicate {
54 out.retain(|r| r.predicate == p);
55 }
56 out.truncate(limit.max(1));
57 Ok(out)
58}
59
60pub fn traverse(
68 compiler: &scc_context::ContextCompiler<'_>,
69 req: &scc_api::TraverseRequest,
70) -> crate::Result<(Vec<scc_core::Entity>, Vec<scc_core::Relationship>)> {
71 let store = compiler.store;
72 use std::collections::{BTreeMap, BTreeSet};
73 let mut start_ids: BTreeSet<String> = BTreeSet::new();
74 if let Some(kind) = req.kind.as_deref().filter(|k| !k.is_empty()) {
75 let needle = req.name.clone().unwrap_or_default();
76 for e in store.search_entities_like_kind(kind, &needle, 200)? {
77 start_ids.insert(e.id);
78 }
79 } else if let Some(name) = req.name.as_deref().filter(|n| !n.is_empty()) {
80 for e in store.search_entities_like(name, 200)? {
81 start_ids.insert(e.id);
82 }
83 for (n, _, _, _, _) in store.search_symbols_like(name, 200)? {
84 for e in store.search_entities_like(&n, 20)? {
85 start_ids.insert(e.id);
86 }
87 }
88 }
89 for id in &req.from_ids {
90 start_ids.insert(id.clone());
91 }
92 for step in &req.steps {
93 if !["out", "in", "both"].contains(&step.dir.as_str()) {
94 return Err(crate::EngineError::Other(format!(
95 "unknown traverse direction '{}' (use out|in|both)",
96 step.dir
97 )));
98 }
99 }
100 if start_ids.is_empty() {
101 return Ok((Vec::new(), Vec::new()));
102 }
103 let entities: BTreeMap<String, scc_core::Entity> = store
104 .all_entities()?
105 .into_iter()
106 .map(|e| (e.id.clone(), e))
107 .collect();
108 let mut frontier: BTreeSet<String> = start_ids;
109 let mut visited: BTreeSet<String> = frontier.clone();
110 let mut rels: Vec<scc_core::Relationship> = Vec::new();
111 for step in &req.steps {
112 let cap = if step.limit == 0 { 50 } else { step.limit };
113 let mut next: BTreeSet<String> = BTreeSet::new();
114 let mut step_rels: Vec<scc_core::Relationship> = Vec::new();
115 for id in &frontier {
117 let mut edges: Vec<scc_core::Relationship> = if req.trusted_only {
123 match step.dir.as_str() {
124 "out" => compiler.view.out_edges(id),
125 "in" => compiler.view.in_edges(id),
126 "both" => {
127 let mut e = compiler.view.out_edges(id);
128 e.extend(compiler.view.in_edges(id));
129 e.sort_by(|a, b| a.id.cmp(&b.id));
130 e
131 }
132 _ => unreachable!("direction validated above"),
133 }
134 .into_iter()
135 .cloned()
136 .collect()
137 } else {
138 match step.dir.as_str() {
139 "out" => store.relationships_for(id)?,
140 "in" => store.relationships_to(id)?,
141 "both" => {
142 let mut e = store.relationships_for(id)?;
143 e.extend(store.relationships_to(id)?);
144 e.sort_by(|a, b| a.id.cmp(&b.id));
145 e
146 }
147 _ => unreachable!("direction validated above"),
148 }
149 };
150 if let Some(p) = step.predicate.as_deref().filter(|s| !s.is_empty()) {
151 edges.retain(|r| r.predicate == p);
152 }
153 edges.truncate(cap);
154 for r in edges {
155 let land = (if r.subject == *id { &r.object } else { &r.subject }).clone();
156 if let Some(k) = step.where_kind.as_deref().filter(|s| !s.is_empty()) {
157 match entities.get(land.as_str()) {
158 Some(e) if e.kind == k => {}
159 _ => continue,
160 }
161 }
162 step_rels.push(r);
163 if visited.insert(land.clone()) {
164 next.insert(land);
165 }
166 }
167 }
168 rels.extend(step_rels);
169 frontier = next;
170 if frontier.is_empty() {
171 break;
172 }
173 }
174 let mut out_entities: Vec<scc_core::Entity> = frontier
175 .iter()
176 .filter_map(|id| entities.get(id).cloned())
177 .collect();
178 let limit = if req.limit == 0 { 100 } else { req.limit };
179 out_entities.truncate(limit);
180 Ok((out_entities, rels))
181}
182
183pub fn explain(
191 compiler: &scc_context::ContextCompiler<'_>,
192 subject: &str,
193 predicate: &str,
194 object: &str,
195) -> serde_json::Value {
196 let store = compiler.store;
197 let direct = store.relationships_between(subject, predicate, object).unwrap_or_default();
198 let rivals: Vec<scc_core::Relationship> = store
199 .relationships_for(subject)
200 .unwrap_or_default()
201 .into_iter()
202 .filter(|r| r.predicate == predicate && r.object != object)
203 .collect();
204 let trusted = compiler
205 .view
206 .out_pred(subject, predicate)
207 .iter()
208 .any(|r| r.object == object);
209 let why = if trusted {
210 "visible through TrustedGraphView"
211 } else if direct.is_empty() {
212 "no stored assertion for this triple"
213 } else {
214 "hidden by TrustedGraphView (stale evidence or below trust floor)"
215 };
216 serde_json::json!({
217 "subject": subject, "predicate": predicate, "object": object,
218 "trusted": trusted, "why": why,
219 "assertions": direct,
220 "conflicting_objects": rivals.into_iter().map(|r| r.object).collect::<Vec<_>>(),
221 })
222}