Skip to main content

ucp_codegraph/
model.rs

1use serde::{Deserialize, Serialize};
2use std::collections::{BTreeMap, BTreeSet};
3use std::path::PathBuf;
4use ucm_core::Document;
5
6pub const CODEGRAPH_PROFILE: &str = "codegraph";
7pub const CODEGRAPH_PROFILE_VERSION: &str = "v1";
8pub const CODEGRAPH_PROFILE_MARKER: &str = "codegraph.v1";
9pub const CODEGRAPH_EXTRACTOR_VERSION: &str = "ucp-codegraph-extractor.v1";
10
11pub(crate) const META_NODE_CLASS: &str = "node_class";
12pub(crate) const META_LOGICAL_KEY: &str = "logical_key";
13pub(crate) const META_CODEREF: &str = "coderef";
14pub(crate) const META_LANGUAGE: &str = "language";
15pub(crate) const META_SYMBOL_KIND: &str = "symbol_kind";
16pub(crate) const META_SYMBOL_NAME: &str = "name";
17pub(crate) const META_EXPORTED: &str = "exported";
18
19#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
20#[serde(rename_all = "snake_case")]
21pub enum CodeGraphSeverity {
22    Error,
23    Warning,
24    Info,
25}
26
27#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
28pub struct CodeGraphDiagnostic {
29    pub severity: CodeGraphSeverity,
30    pub code: String,
31    pub message: String,
32    #[serde(skip_serializing_if = "Option::is_none")]
33    pub path: Option<String>,
34    #[serde(skip_serializing_if = "Option::is_none")]
35    pub logical_key: Option<String>,
36}
37
38impl CodeGraphDiagnostic {
39    pub(crate) fn error(code: &str, message: impl Into<String>) -> Self {
40        Self {
41            severity: CodeGraphSeverity::Error,
42            code: code.to_string(),
43            message: message.into(),
44            path: None,
45            logical_key: None,
46        }
47    }
48
49    pub(crate) fn warning(code: &str, message: impl Into<String>) -> Self {
50        Self {
51            severity: CodeGraphSeverity::Warning,
52            code: code.to_string(),
53            message: message.into(),
54            path: None,
55            logical_key: None,
56        }
57    }
58
59    pub(crate) fn info(code: &str, message: impl Into<String>) -> Self {
60        Self {
61            severity: CodeGraphSeverity::Info,
62            code: code.to_string(),
63            message: message.into(),
64            path: None,
65            logical_key: None,
66        }
67    }
68
69    pub(crate) fn with_path(mut self, path: impl Into<String>) -> Self {
70        self.path = Some(path.into());
71        self
72    }
73
74    pub(crate) fn with_logical_key(mut self, logical_key: impl Into<String>) -> Self {
75        self.logical_key = Some(logical_key.into());
76        self
77    }
78}
79
80#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
81pub struct CodeGraphValidationResult {
82    pub valid: bool,
83    pub diagnostics: Vec<CodeGraphDiagnostic>,
84}
85
86#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
87pub struct CodeGraphStats {
88    pub total_nodes: usize,
89    pub repository_nodes: usize,
90    pub directory_nodes: usize,
91    pub file_nodes: usize,
92    pub symbol_nodes: usize,
93    pub total_edges: usize,
94    pub reference_edges: usize,
95    pub export_edges: usize,
96    #[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
97    pub languages: BTreeMap<String, usize>,
98}
99
100#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
101#[serde(rename_all = "snake_case")]
102pub enum CodeGraphBuildStatus {
103    Success,
104    PartialSuccess,
105    FailedValidation,
106}
107
108#[derive(Debug, Clone)]
109pub struct CodeGraphBuildResult {
110    pub document: Document,
111    pub diagnostics: Vec<CodeGraphDiagnostic>,
112    pub stats: CodeGraphStats,
113    pub profile_version: String,
114    pub canonical_fingerprint: String,
115    pub status: CodeGraphBuildStatus,
116    pub incremental: Option<CodeGraphIncrementalStats>,
117}
118
119impl CodeGraphBuildResult {
120    pub fn has_errors(&self) -> bool {
121        self.diagnostics
122            .iter()
123            .any(|d| d.severity == CodeGraphSeverity::Error)
124    }
125}
126
127#[derive(Debug, Clone, Serialize, Deserialize)]
128pub struct CodeGraphBuildInput {
129    pub repository_path: PathBuf,
130    pub commit_hash: String,
131    #[serde(default)]
132    pub config: CodeGraphExtractorConfig,
133}
134
135#[derive(Debug, Clone, Serialize, Deserialize)]
136pub struct CodeGraphIncrementalBuildInput {
137    pub build: CodeGraphBuildInput,
138    pub state_file: PathBuf,
139}
140
141#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
142pub struct CodeGraphIncrementalStats {
143    #[serde(default)]
144    pub requested: bool,
145    #[serde(default)]
146    pub scanned_files: usize,
147    #[serde(default)]
148    pub state_entries: usize,
149    #[serde(default)]
150    pub direct_invalidated_files: usize,
151    #[serde(default)]
152    pub surface_changed_files: usize,
153    #[serde(default)]
154    pub reused_files: usize,
155    #[serde(default)]
156    pub rebuilt_files: usize,
157    #[serde(default)]
158    pub added_files: usize,
159    #[serde(default)]
160    pub changed_files: usize,
161    #[serde(default)]
162    pub deleted_files: usize,
163    #[serde(default)]
164    pub invalidated_files: usize,
165    #[serde(default, skip_serializing_if = "Option::is_none")]
166    pub full_rebuild_reason: Option<String>,
167}
168
169#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
170pub struct CodeGraphExtractorConfig {
171    #[serde(default = "default_include_extensions")]
172    pub include_extensions: Vec<String>,
173    #[serde(default = "default_exclude_dirs")]
174    pub exclude_dirs: Vec<String>,
175    #[serde(default = "default_continue_on_parse_error")]
176    pub continue_on_parse_error: bool,
177    #[serde(default)]
178    pub include_hidden: bool,
179    #[serde(default = "default_max_file_bytes")]
180    pub max_file_bytes: usize,
181    #[serde(default = "default_emit_export_edges")]
182    pub emit_export_edges: bool,
183}
184
185impl Default for CodeGraphExtractorConfig {
186    fn default() -> Self {
187        Self {
188            include_extensions: default_include_extensions(),
189            exclude_dirs: default_exclude_dirs(),
190            continue_on_parse_error: default_continue_on_parse_error(),
191            include_hidden: false,
192            max_file_bytes: default_max_file_bytes(),
193            emit_export_edges: default_emit_export_edges(),
194        }
195    }
196}
197
198fn default_include_extensions() -> Vec<String> {
199    vec!["rs", "py", "ts", "tsx", "js", "jsx"]
200        .into_iter()
201        .map(|s| s.to_string())
202        .collect()
203}
204
205fn default_exclude_dirs() -> Vec<String> {
206    vec![".git", "target", "node_modules", "dist", "build"]
207        .into_iter()
208        .map(|s| s.to_string())
209        .collect()
210}
211
212fn default_continue_on_parse_error() -> bool {
213    true
214}
215
216fn default_max_file_bytes() -> usize {
217    2 * 1024 * 1024
218}
219
220fn default_emit_export_edges() -> bool {
221    true
222}
223
224#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
225#[serde(rename_all = "snake_case")]
226pub(crate) enum CodeLanguage {
227    Rust,
228    Python,
229    TypeScript,
230    JavaScript,
231}
232
233impl CodeLanguage {
234    pub(crate) fn as_str(self) -> &'static str {
235        match self {
236            Self::Rust => "rust",
237            Self::Python => "python",
238            Self::TypeScript => "typescript",
239            Self::JavaScript => "javascript",
240        }
241    }
242}
243
244#[derive(Debug, Clone, Serialize, Deserialize)]
245pub(crate) struct RepoFile {
246    pub(crate) absolute_path: PathBuf,
247    pub(crate) relative_path: String,
248    pub(crate) language: CodeLanguage,
249}
250
251#[derive(Debug, Clone, Serialize, Deserialize)]
252pub(crate) struct ExtractedSymbol {
253    pub(crate) name: String,
254    pub(crate) qualified_name: String,
255    pub(crate) identity: String,
256    pub(crate) parent_identity: Option<String>,
257    pub(crate) kind: String,
258    pub(crate) description: Option<String>,
259    pub(crate) modifiers: ExtractedModifiers,
260    pub(crate) inputs: Vec<ExtractedInput>,
261    pub(crate) output: Option<String>,
262    pub(crate) type_info: Option<String>,
263    pub(crate) exported: bool,
264    pub(crate) start_line: usize,
265    pub(crate) start_col: usize,
266    pub(crate) end_line: usize,
267    pub(crate) end_col: usize,
268}
269
270#[derive(Debug, Clone, Serialize, Deserialize)]
271pub(crate) struct ExtractedInput {
272    pub(crate) name: String,
273    #[serde(rename = "type", skip_serializing_if = "Option::is_none")]
274    pub(crate) type_name: Option<String>,
275}
276
277#[derive(Debug, Clone, Default)]
278pub(crate) struct ExtractedSignature {
279    pub(crate) inputs: Vec<ExtractedInput>,
280    pub(crate) output: Option<String>,
281    pub(crate) type_info: Option<String>,
282}
283
284#[derive(Debug, Clone, Default, Serialize, Deserialize)]
285#[serde(default)]
286pub(crate) struct ExtractedModifiers {
287    #[serde(rename = "async", skip_serializing_if = "is_false")]
288    pub(crate) is_async: bool,
289    #[serde(skip_serializing_if = "is_false")]
290    pub(crate) generator: bool,
291    #[serde(rename = "static", skip_serializing_if = "is_false")]
292    pub(crate) is_static: bool,
293    #[serde(skip_serializing_if = "Option::is_none")]
294    pub(crate) visibility: Option<String>,
295}
296
297impl ExtractedModifiers {
298    pub(crate) fn is_empty(&self) -> bool {
299        !self.is_async && !self.generator && !self.is_static && self.visibility.is_none()
300    }
301}
302
303fn is_false(value: &bool) -> bool {
304    !*value
305}
306
307#[derive(Debug, Clone, Serialize, Deserialize)]
308pub(crate) struct ExtractedImport {
309    pub(crate) module: String,
310    pub(crate) symbols: Vec<String>,
311    pub(crate) bindings: Vec<ImportBinding>,
312    pub(crate) module_aliases: Vec<String>,
313    pub(crate) reexported: bool,
314    pub(crate) wildcard: bool,
315}
316
317#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
318pub(crate) struct ImportBinding {
319    pub(crate) source_name: String,
320    pub(crate) local_name: String,
321}
322
323#[derive(Debug, Clone, Serialize, Deserialize)]
324pub(crate) struct ExtractedRelationship {
325    pub(crate) source_identity: String,
326    pub(crate) relation: String,
327    pub(crate) target_expr: String,
328    pub(crate) target_name: String,
329}
330
331#[derive(Debug, Clone, Serialize, Deserialize)]
332pub(crate) struct ExtractedUsage {
333    pub(crate) source_identity: String,
334    pub(crate) target_expr: String,
335    pub(crate) target_name: String,
336}
337
338#[derive(Debug, Clone, Serialize, Deserialize)]
339pub(crate) struct ExtractedAlias {
340    pub(crate) name: String,
341    pub(crate) target_expr: String,
342    pub(crate) target_name: String,
343    pub(crate) owner_identity: Option<String>,
344}
345
346#[derive(Debug, Clone, PartialEq, Eq)]
347pub(crate) enum ImportResolution {
348    Resolved(String),
349    External,
350    Unresolved,
351}
352
353#[derive(Debug, Clone, Default, Serialize, Deserialize)]
354pub(crate) struct FileAnalysis {
355    pub(crate) file_description: Option<String>,
356    pub(crate) symbols: Vec<ExtractedSymbol>,
357    pub(crate) imports: Vec<ExtractedImport>,
358    pub(crate) relationships: Vec<ExtractedRelationship>,
359    pub(crate) usages: Vec<ExtractedUsage>,
360    pub(crate) aliases: Vec<ExtractedAlias>,
361    pub(crate) export_bindings: Vec<ImportBinding>,
362    pub(crate) exported_symbol_names: BTreeSet<String>,
363    pub(crate) default_exported_symbol_names: BTreeSet<String>,
364    pub(crate) diagnostics: Vec<CodeGraphDiagnostic>,
365}
366
367#[derive(Debug, Clone)]
368pub(crate) struct FileAnalysisRecord {
369    pub(crate) file: String,
370    pub(crate) language: CodeLanguage,
371    pub(crate) imports: Vec<ExtractedImport>,
372    pub(crate) relationships: Vec<ExtractedRelationship>,
373    pub(crate) usages: Vec<ExtractedUsage>,
374    pub(crate) aliases: Vec<ExtractedAlias>,
375    pub(crate) export_bindings: Vec<ImportBinding>,
376}
377
378impl ExtractedImport {
379    pub(crate) fn module(module: impl Into<String>) -> Self {
380        Self {
381            module: module.into(),
382            symbols: Vec::new(),
383            bindings: Vec::new(),
384            module_aliases: Vec::new(),
385            reexported: false,
386            wildcard: false,
387        }
388    }
389
390    pub(crate) fn symbol(module: impl Into<String>, symbol: impl Into<String>) -> Self {
391        let symbol = symbol.into();
392        Self {
393            module: module.into(),
394            symbols: vec![symbol.clone()],
395            bindings: vec![ImportBinding::same(symbol)],
396            module_aliases: Vec::new(),
397            reexported: false,
398            wildcard: false,
399        }
400    }
401
402    pub(crate) fn bindings(module: impl Into<String>, bindings: Vec<ImportBinding>) -> Self {
403        let mut symbols = bindings
404            .iter()
405            .map(|binding| binding.source_name.clone())
406            .collect::<Vec<_>>();
407        symbols.sort();
408        symbols.dedup();
409
410        Self {
411            module: module.into(),
412            symbols,
413            bindings,
414            module_aliases: Vec::new(),
415            reexported: false,
416            wildcard: false,
417        }
418    }
419
420    pub(crate) fn with_module_alias(mut self, alias: impl Into<String>) -> Self {
421        let alias = alias.into();
422        if !alias.is_empty() && !self.module_aliases.contains(&alias) {
423            self.module_aliases.push(alias);
424        }
425        self
426    }
427
428    pub(crate) fn reexported(mut self) -> Self {
429        self.reexported = true;
430        self
431    }
432
433    pub(crate) fn wildcard(mut self) -> Self {
434        self.wildcard = true;
435        self
436    }
437}
438
439impl ImportBinding {
440    pub(crate) fn new(source_name: impl Into<String>, local_name: impl Into<String>) -> Self {
441        Self {
442            source_name: source_name.into(),
443            local_name: local_name.into(),
444        }
445    }
446
447    pub(crate) fn same(name: impl Into<String>) -> Self {
448        let name = name.into();
449        Self::new(name.clone(), name)
450    }
451}
452
453impl ExtractedRelationship {
454    pub(crate) fn new(
455        source_identity: impl Into<String>,
456        relation: impl Into<String>,
457        target_expr: impl Into<String>,
458        target_name: impl Into<String>,
459    ) -> Self {
460        Self {
461            source_identity: source_identity.into(),
462            relation: relation.into(),
463            target_expr: target_expr.into(),
464            target_name: target_name.into(),
465        }
466    }
467}
468
469impl ExtractedUsage {
470    pub(crate) fn new(
471        source_identity: impl Into<String>,
472        target_expr: impl Into<String>,
473        target_name: impl Into<String>,
474    ) -> Self {
475        Self {
476            source_identity: source_identity.into(),
477            target_expr: target_expr.into(),
478            target_name: target_name.into(),
479        }
480    }
481}
482
483impl ExtractedAlias {
484    pub(crate) fn new(
485        name: impl Into<String>,
486        target_expr: impl Into<String>,
487        target_name: impl Into<String>,
488        owner_identity: Option<&str>,
489    ) -> Self {
490        Self {
491            name: name.into(),
492            target_expr: target_expr.into(),
493            target_name: target_name.into(),
494            owner_identity: owner_identity.map(str::to_string),
495        }
496    }
497}