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}