Skip to main content

scc_engine/
graph.rs

1//! Engine graph operations: search, listings, recompile.
2//!
3//! Returns VALUES (entities, pairs, packs); transports render them.
4//! The FTS+LIKE fallback contract moves with the code.
5
6use scc_api::QueryRequest;
7
8// trace:exempt reason=internal-detail
9pub struct QueryHit {
10    pub entities: Vec<scc_core::Entity>,
11    /// (name, signature, kind, file, start_line)
12    pub symbols: Vec<(String, String, String, String, u32)>,
13}
14
15// trace:exempt reason=internal-detail
16pub 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
31// trace:exempt reason=internal-detail
32pub fn components(store: &scc_store::Store) -> crate::Result<Vec<scc_core::Entity>> {
33    Ok(store.components()?)
34}
35
36// trace:exempt reason=internal-detail
37pub fn flows(store: &scc_store::Store) -> crate::Result<Vec<scc_core::Flow>> {
38    Ok(store.flows()?)
39}
40
41// trace:exempt reason=internal-detail
42pub 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
60/// Multi-step traversal (§16): resolve the start set (kind + LIKE name,
61/// union explicit ids), then walk each step's directed edges. Returns the
62/// final frontier entities plus every traversed relationship. Frontier is
63/// a set (visited ids never re-expand); per-entity, per-step edge fan-out
64/// is capped by `step.limit` (default 50); output capped by `limit`.
65/// Unknown directions fail loudly — never silently walk the wrong way.
66// trace:v1 id=impl.scc-engine-graph.traverse work=WORK-SI-MMMJA4G6 satisfies=REQ-SI-503JSBGP
67pub 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        // Deterministic: store queries order by id; frontier is a BTreeSet.
116        for id in &frontier {
117            // Trusted (default): the compiler's TrustedGraphView filters
118            // STALE / low-confidence INFERRED / disallowed provenance —
119            // the same authority every context consumer traverses (§§1.2,
120            // 125). Raw (trusted_only=false) walks store edges verbatim,
121            // explicitly — never silently.
122            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
183/// Overlay diagnostics (§97): why is `subject —predicate→ object` a trusted
184/// claim? Returns every stored assertion row (one per provider assertion —
185/// native EXTRACTED, pyright/tsserver RESOLVED, plugin contributions) with
186/// provenance, confidence, and evidence, plus the trusted verdict computed
187/// through the compiler view. Conflicts are preserved verbatim: disagreeing
188/// objects for the same subject+predicate appear side by side, never merged.
189// trace:v1 id=impl.scc-engine-graph.explain work=WORK-SI-MMMJA4G6 satisfies=REQ-SI-503JSBGP
190pub 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}