1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
//! Backend-agnostic graph storage interface (#7).
//!
//! `GraphStore` is the surface the analyze (write) and query (read) paths use,
//! so the storage engine can change without touching command code. The sole
//! implementation today is the LadybugDB / `lbug` store ([`crate::LadybugStore`],
//! #8); the trait is kept so a future backend can be slotted in behind it.
use anyhow::Result;
use glyphtrail_core::{
Adjacency, ClassifiedItem, CommitMeta, Confidence, Edge, EdgeKind, Embedding, ImpactPolicy,
Node, NodeId, NodeKind, OperationKey, PendingLink,
};
/// Aggregate counts for an index, returned by [`GraphStore::stats`].
#[derive(Debug, Clone)]
pub struct Stats {
pub nodes: usize,
pub edges: usize,
pub files: usize,
/// Indexed file counts per language, descending by count.
pub languages: Vec<(String, usize)>,
}
/// The storage operations shared by every backend. `Adjacency` is a supertrait
/// so the impact engine can traverse any backend's graph.
pub trait GraphStore: Adjacency {
// --- write path (analyze) ---
fn clear(&mut self) -> Result<()>;
fn set_file(&mut self, path: &str, language: Option<&str>, hash: &str) -> Result<()>;
/// Stamp many file records at once. Backends override this to write them in a
/// single transaction; the default falls back to one [`Self::set_file`] per
/// row. Each tuple is `(path, language, content hash)`.
fn set_files(&mut self, files: &[(String, Option<String>, String)]) -> Result<()> {
for (path, language, hash) in files {
self.set_file(path, language.as_deref(), hash)?;
}
Ok(())
}
fn delete_file_data(&mut self, path: &str) -> Result<()>;
fn delete_nodes_by_kind(&mut self, kind: NodeKind) -> Result<()>;
fn insert_graph(&mut self, nodes: &[Node], edges: &[Edge]) -> Result<()>;
/// Insert nodes. `fresh` signals a full rebuild against a just-cleared store
/// with a primary-key-unique node set, letting a backend bulk-load (avoiding
/// a per-row MERGE that can't use the id index). The default merges.
fn insert_nodes(&mut self, nodes: &[Node], fresh: bool) -> Result<()> {
let _ = fresh;
self.insert_graph(nodes, &[])
}
/// Insert edges. `fresh` signals a full rebuild against a just-cleared store
/// with a de-duplicated edge set, letting a backend skip MERGE's per-edge
/// existence check (which goes quadratic on high-degree hub nodes). The
/// default merges, which is always safe; backends override for speed.
fn insert_edges(&mut self, edges: &[Edge], fresh: bool) -> Result<()> {
let _ = fresh;
self.insert_graph(&[], edges)
}
fn insert_operations(&mut self, ops: &[(NodeId, OperationKey)]) -> Result<()>;
/// Upsert atlas `Commit` side-table rows keyed by node id (#330).
fn set_commits(&mut self, commits: &[CommitMeta]) -> Result<()>;
/// Re-evaluate every stored atlas commit's `in_bounds` against a date window
/// (unix seconds, inclusive; `None` = unbounded), so narrowing the window
/// re-marks rows out of bounds rather than deleting them (#331).
fn remark_commit_bounds(&mut self, since: Option<i64>, until: Option<i64>) -> Result<()>;
/// Upsert atlas embeddings for one `(space, model)` namespace (#338).
/// Embeddings are namespaced by what was embedded + how (`space`: text/graph/
/// commit) and the producing model id, so several models coexist without mixing
/// in search and each is independently re-embeddable/exportable. Default no-op.
fn set_embeddings(&mut self, space: &str, model: &str, embeddings: &[Embedding]) -> Result<()> {
let _ = (space, model, embeddings);
Ok(())
}
/// Remove the embeddings of one `(space, model)` namespace, so re-embedding it
/// starts clean without touching other namespaces (#338). Default no-op.
fn clear_embeddings_for(&mut self, space: &str, model: &str) -> Result<()> {
let _ = (space, model);
Ok(())
}
fn insert_pending(&mut self, links: &[PendingLink]) -> Result<()>;
fn insert_imports(&mut self, imports: &[(String, String, String)]) -> Result<()>;
fn delete_edges_by_confidence(&mut self, confidence: Confidence) -> Result<usize>;
fn delete_edges_by_kind(&mut self, kind: EdgeKind) -> Result<usize>;
fn prune_dangling_edges(&mut self) -> Result<usize>;
fn set_meta(&mut self, key: &str, value: &str) -> Result<()>;
// --- read path (analyze re-resolution + query) ---
fn file_hash(&self, path: &str) -> Result<Option<String>>;
fn all_files(&self) -> Result<Vec<String>>;
fn files_with_hashes(&self) -> Result<Vec<(String, String)>>;
fn get_meta(&self, key: &str) -> Result<Option<String>>;
fn operations_by_kind(&self, kind: NodeKind) -> Result<Vec<(NodeId, OperationKey)>>;
fn all_operations(&self) -> Result<Vec<(NodeId, OperationKey)>>;
/// In-bounds atlas commits in `[since, until]` (unix seconds, inclusive; `None`
/// = unbounded), ordered by `committed_at` (#330).
fn commits_in_range(&self, since: Option<i64>, until: Option<i64>) -> Result<Vec<CommitMeta>>;
/// Total atlas commits in the side-table (cheap `COUNT`, no materialization).
fn commit_count(&self) -> Result<usize>;
/// Stored embeddings for one `(space, model)` namespace (#338). Default empty.
fn embeddings_for(&self, space: &str, model: &str) -> Result<Vec<Embedding>> {
let _ = (space, model);
Ok(Vec::new())
}
/// `(space, model, count, dim)` for every stored embedding namespace — for
/// `atlas status` and resolving which model a query should search (#338).
/// Default empty.
fn embedding_index(&self) -> Result<Vec<(String, String, usize, usize)>> {
Ok(Vec::new())
}
/// In-bounds atlas commits in `[since, until]` (unix seconds, inclusive;
/// `None` = unbounded), each joined to its repo name and touched-file count,
/// ordered by `committed_at` ascending (#333). `topic`, when set, keeps only
/// commits tagged with that topic (#334). Visibility/author filtering is the
/// caller's.
fn atlas_timeline(
&self,
since: Option<i64>,
until: Option<i64>,
topic: Option<&str>,
) -> Result<Vec<glyphtrail_core::AtlasTimelineRow>>;
/// Derived atlas topics with their commit counts, most-tagged first (#334).
fn atlas_topics(&self) -> Result<Vec<(String, usize)>>;
/// Just the atlas commit rows for the given node ids, joined to their repo
/// (touched-count omitted), so a commit similarity query maps its small ANN
/// hit set back to repo/date/subject without loading the whole timeline (#338).
/// Default empty for non-atlas backends.
fn atlas_commit_rows(
&self,
node_ids: &[String],
) -> Result<Vec<glyphtrail_core::AtlasTimelineRow>> {
let _ = node_ids;
Ok(Vec::new())
}
/// `(commit node id, touched file path)` over every atlas commit's `Touched`
/// edges, so `embed-commits` can fold a bounded digest of the changed paths into
/// each commit's document (meaning for sparse messages) (#338). Default empty.
fn commit_touched_paths(&self) -> Result<Vec<(String, String)>> {
Ok(Vec::new())
}
fn all_pending(&self) -> Result<Vec<PendingLink>>;
fn all_imports(&self) -> Result<Vec<(String, String, String)>>;
fn node_files(&self) -> Result<Vec<(String, String)>>;
/// `(node id, qualified name)` for every node, so cross-file resolution can
/// match a call's receiver against a candidate's enclosing scope (#5).
fn node_qualified_names(&self) -> Result<Vec<(String, String)>>;
/// `(node id, qualified name)` for every `Table` node already persisted, so an
/// incremental analyze can resolve a code↔table edge to a table whose `.sql`/
/// entity file wasn't re-parsed this pass (#435). Defaults to empty so a
/// backend that doesn't index tables stays source-compatible.
fn tables_by_name(&self) -> Result<Vec<(NodeId, String)>> {
Ok(Vec::new())
}
fn definition_index(&self) -> Result<Vec<(String, NodeId)>>;
fn get_node(&self, id: &str) -> Result<Option<Node>>;
fn nodes_in_file(&self, file: &str) -> Result<Vec<Node>>;
fn find_by_name(&self, name: &str) -> Result<Vec<Node>>;
/// Substring search over name/qualified-name/doc. Case-insensitive unless
/// `case_sensitive` (#367).
fn search(&self, query: &str, limit: usize, case_sensitive: bool) -> Result<Vec<Node>>;
fn neighbors(
&self,
id: &str,
kind: Option<EdgeKind>,
outgoing: bool,
) -> Result<Vec<(Node, EdgeKind, Confidence)>>;
fn reachable(
&self,
id: &str,
kind: EdgeKind,
outgoing: bool,
depth: usize,
) -> Result<Vec<Node>>;
fn subgraph(&self, ids: &[String]) -> Result<(Vec<Node>, Vec<Edge>)>;
fn classify_impact(
&self,
seeds: &[NodeId],
policy: &ImpactPolicy,
) -> Result<Vec<ClassifiedItem>>;
fn stats(&self) -> Result<Stats>;
/// `(node kind, count)` over all nodes, for the structural graph embedding
/// (#338). Default empty for backends that don't implement it.
fn node_kind_counts(&self) -> Result<Vec<(String, usize)>> {
Ok(Vec::new())
}
/// `(edge kind, count)` over all edges, for the structural graph embedding
/// (#338). Default empty.
fn edge_kind_counts(&self) -> Result<Vec<(String, usize)>> {
Ok(Vec::new())
}
fn export_graph(&self, limit: usize) -> Result<(Vec<Node>, Vec<Edge>)>;
/// Export nodes/edges with kind filters pushed into the query, so a trimmed
/// view doesn't transfer the whole graph (#194). `node_kinds`/`edge_kinds`
/// of `None` mean "all kinds"; `Some(&[])` means none. `limit` caps nodes.
/// Edges are filtered by their own kind and both endpoints' kinds; the
/// caller still prunes edges to any node dropped by `limit`.
fn export_filtered(
&self,
node_kinds: Option<&[String]>,
edge_kinds: Option<&[String]>,
limit: usize,
) -> Result<(Vec<Node>, Vec<Edge>)>;
}