Skip to main content

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}