gonzalo_graph/model.rs
1//! The code-graph data model. Serializable so a graph can be persisted as a
2//! gonzalo record and shared/synced like any other data.
3
4use serde::{Deserialize, Serialize};
5use std::collections::BTreeMap;
6
7/// What kind of Rust item a symbol is.
8#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
9#[serde(rename_all = "snake_case")]
10pub enum SymbolKind {
11 Function,
12 Struct,
13 Enum,
14 Trait,
15 Impl,
16 Module,
17 Const,
18 Static,
19 TypeAlias,
20 /// A class (Python `class`, and other languages that have classes).
21 Class,
22 /// An interface (TypeScript `interface`, and similar constructs).
23 Interface,
24}
25
26impl SymbolKind {
27 /// Lowercase name, used as a stable key when bucketing symbols by kind.
28 /// Matches the `snake_case` serde representation.
29 pub fn as_str(self) -> &'static str {
30 match self {
31 Self::Function => "function",
32 Self::Struct => "struct",
33 Self::Enum => "enum",
34 Self::Trait => "trait",
35 Self::Impl => "impl",
36 Self::Module => "module",
37 Self::Const => "const",
38 Self::Static => "static",
39 Self::TypeAlias => "type_alias",
40 Self::Class => "class",
41 Self::Interface => "interface",
42 }
43 }
44}
45
46/// A defined symbol with its in-file location (1-based line numbers).
47///
48/// **Path-agnostic** (ADR 0012): a symbol carries no file path, so the same
49/// file content produces byte-identical slices regardless of where it lives,
50/// and content-addressed storage dedups them across paths/worktrees. The path
51/// is supplied at assembly from the manifest — see [`Located`].
52#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
53pub struct Symbol {
54 pub name: String,
55 pub kind: SymbolKind,
56 pub start_line: usize,
57 pub end_line: usize,
58}
59
60/// A name-based reference (e.g. a call) from within `from` (the enclosing
61/// function symbol, if any) to `name`. References are unresolved: they match
62/// by name, not by a resolved definition. This is a heuristic call graph,
63/// suitable for navigation; true name resolution is a later milestone.
64///
65/// Path-agnostic like [`Symbol`]; the path comes from assembly.
66#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
67pub struct Reference {
68 pub name: String,
69 pub from: Option<String>,
70 pub line: usize,
71 /// How the callee was written at the call site. Defaults to
72 /// [`RefKind::Free`] and is omitted from the serialized slice when free, so
73 /// a file of plain calls keeps the byte-identical slice — and therefore the
74 /// same content hash — it had before this field existed.
75 #[serde(default, skip_serializing_if = "RefKind::is_free")]
76 pub kind: RefKind,
77}
78
79/// The syntactic shape of a call site.
80///
81/// A name alone cannot distinguish `chain()` from `x.chain()`, and conflating
82/// them makes the resolver attribute a std or dependency method to a same-named
83/// free function that happens to be the only one in the view (#223). Recording
84/// the shape keeps that judgement possible at resolution time.
85#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
86#[serde(rename_all = "snake_case")]
87pub enum RefKind {
88 /// A plain call — `foo()` — or a path call such as `a::b::foo()`.
89 #[default]
90 Free,
91 /// A call through a receiver whose type is unknown — `x.foo()`. The callee
92 /// belongs to whatever `x` is, which the graph does not know, so it may well
93 /// be defined outside the view entirely.
94 Method,
95}
96
97impl RefKind {
98 /// Whether this is the default, [`Free`](RefKind::Free) shape.
99 pub fn is_free(&self) -> bool {
100 matches!(self, Self::Free)
101 }
102
103 /// Lowercase name, matching the `snake_case` serde representation. Used as
104 /// the stored value in the persistent graph.
105 pub fn as_str(self) -> &'static str {
106 match self {
107 Self::Free => "free",
108 Self::Method => "method",
109 }
110 }
111
112 /// Parse from [`as_str`](RefKind::as_str). Anything unrecognized — including
113 /// a row written before the column existed — reads as `Free`, the
114 /// pre-existing behaviour.
115 pub fn from_str_or_free(raw: &str) -> Self {
116 match raw {
117 "method" => Self::Method,
118 _ => Self::Free,
119 }
120 }
121}
122
123/// A query result carried with the assembly path it was found under. The path
124/// is not stored in the slice ([`Symbol`]/[`Reference`] are path-agnostic); it
125/// is re-attached at assembly from the manifest, so navigation still resolves
126/// to a concrete file.
127#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
128pub struct Located<T> {
129 pub path: String,
130 pub item: T,
131}
132
133/// A code graph: the symbols defined and references found in a single file's
134/// slice. Path-agnostic; a whole view is assembled from many of these keyed by
135/// path in a [`GraphStore`](crate::GraphStore).
136#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
137pub struct CodeGraph {
138 pub symbols: Vec<Symbol>,
139 pub references: Vec<Reference>,
140}
141
142impl CodeGraph {
143 /// Serialize this slice to its content-addressed blob bytes (ADR 0012).
144 /// Byte-stable for equal content, since the model carries no path.
145 pub fn to_slice_bytes(&self) -> Vec<u8> {
146 serde_json::to_vec(self).expect("CodeGraph serializes")
147 }
148
149 /// Deserialize a slice from its blob bytes.
150 pub fn from_slice_bytes(bytes: &[u8]) -> Result<Self, serde_json::Error> {
151 serde_json::from_slice(bytes)
152 }
153}
154
155/// A file and the number of symbols defined in it.
156#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
157pub struct FileSummary {
158 pub path: String,
159 pub symbols: usize,
160}
161
162/// The aggregate shape of a whole view — what is here, rather than facts about
163/// one symbol. `by_kind` and `by_language` are keyed by the lowercase names from
164/// [`SymbolKind::as_str`] and [`Language::as_str`](crate::Language::as_str);
165/// symbols in files with an unrecognized extension bucket under `"unknown"`, so
166/// `by_language` always sums to `symbols`.
167#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
168pub struct ViewOverview {
169 /// Distinct paths contributing symbols or references.
170 pub files: usize,
171 pub symbols: usize,
172 pub references: usize,
173 pub by_kind: BTreeMap<String, usize>,
174 pub by_language: BTreeMap<String, usize>,
175 /// Files with the most symbols, descending. Bounded by the caller's limit;
176 /// `files` above is the untruncated count.
177 pub largest_files: Vec<FileSummary>,
178}
179
180/// A symbol name ranked by some score, with the paths that define it. `paths`
181/// is empty when the name is referenced but never defined in this view (a call
182/// into a dependency, or a name the parser saw but no slice declares).
183#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
184pub struct RankedSymbol {
185 pub name: String,
186 pub score: usize,
187 pub paths: Vec<String>,
188}
189
190/// What [`GraphStore::top`](crate::GraphStore::top) ranks by.
191#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
192#[serde(rename_all = "snake_case")]
193pub enum Ranking {
194 /// Number of references to the name — how heavily it is called.
195 FanIn,
196 /// Number of distinct names called from within it.
197 FanOut,
198 /// Number of definitions of the name. A score above 1 means the name is
199 /// ambiguous, which is what makes name-matched traversal unreliable.
200 Definitions,
201}
202
203/// A conjunctive filter for [`GraphStore::list`](crate::GraphStore::list) — every
204/// set field must match. All fields unset matches every symbol.
205#[derive(Debug, Clone, Default, PartialEq, Eq)]
206pub struct SymbolFilter {
207 pub path_prefix: Option<String>,
208 pub kind: Option<SymbolKind>,
209 pub name_contains: Option<String>,
210}
211
212impl SymbolFilter {
213 /// Restrict to symbols whose path starts with `prefix` (scopes to a crate
214 /// or directory).
215 #[must_use]
216 pub fn path_prefix(mut self, prefix: impl Into<String>) -> Self {
217 self.path_prefix = Some(prefix.into());
218 self
219 }
220
221 /// Restrict to one [`SymbolKind`].
222 #[must_use]
223 pub fn kind(mut self, kind: SymbolKind) -> Self {
224 self.kind = Some(kind);
225 self
226 }
227
228 /// Restrict to symbols whose name contains `needle`.
229 #[must_use]
230 pub fn name_contains(mut self, needle: impl Into<String>) -> Self {
231 self.name_contains = Some(needle.into());
232 self
233 }
234
235 /// Whether `located` satisfies every set field.
236 pub fn matches(&self, located: &Located<Symbol>) -> bool {
237 self.path_prefix
238 .as_ref()
239 .is_none_or(|p| located.path.starts_with(p.as_str()))
240 && self.kind.is_none_or(|k| located.item.kind == k)
241 && self
242 .name_contains
243 .as_ref()
244 .is_none_or(|n| located.item.name.contains(n.as_str()))
245 }
246}
247
248/// A bounded slice of a larger result set. `total` is the untruncated match
249/// count, so a caller can always tell what it did not see.
250#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
251pub struct Page<T> {
252 pub items: Vec<T>,
253 pub total: usize,
254 pub truncated: bool,
255}
256
257impl<T> Page<T> {
258 /// Take at most `limit` of `items`, recording the pre-truncation total.
259 pub fn new(items: Vec<T>, limit: usize) -> Self {
260 let total = items.len();
261 let mut items = items;
262 items.truncate(limit);
263 Self {
264 truncated: items.len() < total,
265 items,
266 total,
267 }
268 }
269}