Skip to main content

tessera_codegraph/
types.rs

1use std::fmt::{self, Display};
2
3use serde::{Deserialize, Serialize};
4
5#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
6#[serde(rename_all = "snake_case")]
7pub enum Language {
8    JavaScript,
9    TypeScript,
10    /// TypeScript with JSX — parsed by the TSX grammar so `<Component />`
11    /// elements get extracted as references. Stored as "typescript" in the DB.
12    Tsx,
13    Python,
14    Go,
15    Rust,
16    Java,
17    C,
18    /// C++. `.h` headers are parsed by the C++ grammar too — it is a strict
19    /// superset of C, so it extracts both C and C++ headers correctly.
20    Cpp,
21    CSharp,
22    Ruby,
23    Php,
24}
25
26impl Language {
27    pub fn from_extension(ext: &str) -> Option<Self> {
28        match ext {
29            "js" | "jsx" | "mjs" | "cjs" => Some(Self::JavaScript),
30            "ts" | "mts" | "cts" => Some(Self::TypeScript),
31            "tsx" => Some(Self::Tsx),
32            "py" | "pyw" => Some(Self::Python),
33            "go" => Some(Self::Go),
34            "rs" => Some(Self::Rust),
35            "java" => Some(Self::Java),
36            "c" => Some(Self::C),
37            // `.h` is parsed with the C++ grammar (a C superset) so headers
38            // from both C and C++ projects extract cleanly.
39            "cpp" | "cc" | "cxx" | "hpp" | "hh" | "hxx" | "h" => Some(Self::Cpp),
40            "cs" => Some(Self::CSharp),
41            "rb" => Some(Self::Ruby),
42            "php" | "phtml" => Some(Self::Php),
43            _ => None,
44        }
45    }
46
47    pub fn from_name(name: &str) -> Option<Self> {
48        match name.to_lowercase().as_str() {
49            "javascript" | "js" | "jsx" => Some(Self::JavaScript),
50            "typescript" | "ts" => Some(Self::TypeScript),
51            "tsx" => Some(Self::Tsx),
52            "python" | "py" => Some(Self::Python),
53            "go" => Some(Self::Go),
54            "rust" | "rs" => Some(Self::Rust),
55            "java" => Some(Self::Java),
56            "c" => Some(Self::C),
57            "cpp" | "c++" | "cxx" | "cc" => Some(Self::Cpp),
58            "csharp" | "c#" | "cs" => Some(Self::CSharp),
59            "ruby" | "rb" => Some(Self::Ruby),
60            "php" => Some(Self::Php),
61            _ => None,
62        }
63    }
64
65    /// Logical family — TSX and TS share the "typescript" label in the DB
66    /// because callers/agents don't care which sub-grammar parsed the file.
67    pub fn family(&self) -> &'static str {
68        match self {
69            Self::JavaScript => "javascript",
70            Self::TypeScript | Self::Tsx => "typescript",
71            Self::Python => "python",
72            Self::Go => "go",
73            Self::Rust => "rust",
74            Self::Java => "java",
75            Self::C => "c",
76            Self::Cpp => "cpp",
77            Self::CSharp => "csharp",
78            Self::Ruby => "ruby",
79            Self::Php => "php",
80        }
81    }
82}
83
84impl Display for Language {
85    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
86        f.write_str(self.family())
87    }
88}
89
90#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
91#[serde(rename_all = "snake_case")]
92pub enum GraphEngineKind {
93    Sqlite,
94    Cozo,
95}
96
97impl Display for GraphEngineKind {
98    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
99        match self {
100            Self::Sqlite => f.write_str("sqlite"),
101            Self::Cozo => f.write_str("cozo"),
102        }
103    }
104}
105
106#[derive(Debug, Clone, Serialize, Deserialize)]
107pub struct SymbolRecord {
108    pub id: i64,
109    pub name: String,
110    pub qualified_name: String,
111    pub kind: String,
112    pub file_id: i64,
113    pub path: String,
114    pub language: String,
115    pub start_line: usize,
116    pub end_line: usize,
117    pub signature: String,
118    pub exported: bool,
119}
120
121#[derive(Debug, Clone, Serialize, Deserialize)]
122pub struct ReferenceRecord {
123    pub id: i64,
124    pub symbol_name: String,
125    pub from_symbol_id: Option<i64>,
126    pub from_symbol: Option<String>,
127    pub path: String,
128    pub line: usize,
129    pub column: usize,
130    pub context: String,
131    pub kind: String,
132}
133
134#[derive(Debug, Clone, Serialize, Deserialize)]
135pub struct IndexedSymbol {
136    pub name: String,
137    pub qualified_name: String,
138    pub kind: String,
139    pub start_line: usize,
140    pub end_line: usize,
141    pub signature: String,
142    pub exported: bool,
143}
144
145#[derive(Debug, Clone, Serialize, Deserialize)]
146pub struct IndexedImport {
147    pub source: String,
148    pub line: usize,
149    pub kind: String,
150}
151
152#[derive(Debug, Clone, Serialize, Deserialize)]
153pub struct IndexedReference {
154    pub symbol_name: String,
155    pub from_qualified_name: Option<String>,
156    pub line: usize,
157    pub column: usize,
158    pub context: String,
159    pub kind: String,
160}
161
162#[derive(Debug, Clone, Serialize, Deserialize)]
163pub struct DefinitionResult {
164    pub matches: Vec<SymbolRecord>,
165    pub meta: QueryMeta,
166}
167
168impl Display for DefinitionResult {
169    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
170        if self.matches.is_empty() {
171            writeln!(f, "No definitions found.")?;
172            return writeln!(
173                f,
174                "  hint: run `tessera search <name>` for fuzzy lookup, `tessera validate <name>` for near-misses, or re-index with `tessera index . --full` if the DB is stale."
175            );
176        }
177        for symbol in &self.matches {
178            writeln!(
179                f,
180                "{} {} at {}:{}",
181                symbol.kind, symbol.qualified_name, symbol.path, symbol.start_line
182            )?;
183            if !symbol.signature.is_empty() {
184                writeln!(f, "  {}", symbol.signature)?;
185            }
186        }
187        write_meta(f, &self.meta)
188    }
189}
190
191#[derive(Debug, Clone, Serialize, Deserialize)]
192pub struct ReferencesResult {
193    pub references: Vec<ReferenceRecord>,
194    pub meta: QueryMeta,
195}
196
197impl Display for ReferencesResult {
198    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
199        if self.references.is_empty() {
200            writeln!(f, "No references found.")?;
201            return writeln!(
202                f,
203                "  hint: run `tessera impact <symbol>` for transitive callers, or `tessera search <symbol>` to confirm the indexed name."
204            );
205        }
206        for reference in &self.references {
207            writeln!(
208                f,
209                "{}:{}:{} {}",
210                reference.path, reference.line, reference.column, reference.context
211            )?;
212        }
213        write_meta(f, &self.meta)
214    }
215}
216
217#[derive(Debug, Clone, Serialize, Deserialize)]
218pub struct OutlineResult {
219    pub path: String,
220    pub symbols: Vec<SymbolRecord>,
221    pub meta: QueryMeta,
222}
223
224impl Display for OutlineResult {
225    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
226        if self.symbols.is_empty() {
227            writeln!(f, "No indexed symbols under {}.", self.path)?;
228            writeln!(
229                f,
230                "  hint: check the path prefix, run `tessera stats`, or re-index with `tessera index . --full`."
231            )?;
232        } else {
233            writeln!(f, "Outline for {}", self.path)?;
234            for symbol in &self.symbols {
235                writeln!(
236                    f,
237                    "  {:<9} {:<36} {}:{}",
238                    symbol.kind, symbol.qualified_name, symbol.path, symbol.start_line
239                )?;
240            }
241        }
242        write_meta(f, &self.meta)
243    }
244}
245
246#[derive(Debug, Clone, Serialize, Deserialize)]
247pub struct ExpandResult {
248    pub symbol: Option<SymbolRecord>,
249    pub body: Option<String>,
250    pub dependencies: Vec<ReferenceRecord>,
251    pub meta: QueryMeta,
252}
253
254impl Display for ExpandResult {
255    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
256        let Some(symbol) = &self.symbol else {
257            writeln!(f, "No symbol found.")?;
258            return writeln!(
259                f,
260                "  hint: run `tessera find-definition <symbol>` or `tessera search <symbol>` to find the indexed name."
261            );
262        };
263        writeln!(
264            f,
265            "{} {} at {}:{}-{}",
266            symbol.kind, symbol.qualified_name, symbol.path, symbol.start_line, symbol.end_line
267        )?;
268        if let Some(body) = &self.body {
269            writeln!(f, "\n{body}")?;
270        }
271        if !self.dependencies.is_empty() {
272            writeln!(f, "\nImmediate dependencies:")?;
273            for dep in &self.dependencies {
274                writeln!(f, "  {} at line {}", dep.symbol_name, dep.line)?;
275            }
276        }
277        write_meta(f, &self.meta)
278    }
279}
280
281#[derive(Debug, Clone, Serialize, Deserialize)]
282pub struct ImpactResult {
283    pub symbol: String,
284    pub callers: Vec<ImpactCaller>,
285    pub meta: QueryMeta,
286}
287
288impl Display for ImpactResult {
289    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
290        if self.callers.is_empty() {
291            writeln!(f, "No callers found for {}.", self.symbol)?;
292            writeln!(
293                f,
294                "  hint: run `tessera find-references {}` for direct refs, `tessera validate {}` for near-misses, or re-index if the call graph is stale.",
295                self.symbol, self.symbol
296            )?;
297        } else {
298            writeln!(f, "Impact for {}", self.symbol)?;
299            for caller in &self.callers {
300                writeln!(
301                    f,
302                    "  score {:>5.1} depth {} fanout {:>3} {} at {}:{}",
303                    caller.criticality,
304                    caller.depth,
305                    caller.breakdown.fanout_in,
306                    caller.symbol.qualified_name,
307                    caller.symbol.path,
308                    caller.symbol.start_line
309                )?;
310            }
311        }
312        write_meta(f, &self.meta)
313    }
314}
315
316#[derive(Debug, Clone, Serialize, Deserialize)]
317pub struct ImpactCaller {
318    pub symbol: SymbolRecord,
319    pub depth: usize,
320    pub criticality: f32,
321    pub breakdown: CriticalityBreakdown,
322}
323
324#[derive(Debug, Clone, Serialize, Deserialize)]
325pub struct CriticalityBreakdown {
326    pub pagerank: f32,
327    pub fanout_in: usize,
328    pub fanout_out: usize,
329    pub exported: bool,
330    pub test_coverage: usize,
331    pub depth_decay: f32,
332}
333
334#[derive(Debug, Clone, Serialize, Deserialize)]
335pub struct QueryMeta {
336    pub tokens_returned_estimate: usize,
337    pub alternative_queries: Vec<AlternativeQuery>,
338}
339
340#[derive(Debug, Clone, Serialize, Deserialize)]
341pub struct AlternativeQuery {
342    pub tool: String,
343    pub tokens_estimate: usize,
344    pub fidelity: f32,
345}
346
347#[derive(Debug, Clone, Serialize, Deserialize)]
348pub struct SymbolSuggestion {
349    pub qualified_name: String,
350    pub name: String,
351    pub path: String,
352    pub line: usize,
353    pub confidence: f32,
354}
355
356#[derive(Debug, Clone, Serialize, Deserialize)]
357pub struct ValidateResult {
358    pub query: String,
359    pub exists: bool,
360    pub bloom_hit: bool,
361    pub candidates: Vec<SymbolSuggestion>,
362    pub meta: QueryMeta,
363}
364
365impl Display for ValidateResult {
366    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
367        if self.exists {
368            writeln!(f, "✓ {} exists in the graph.", self.query)?;
369        } else {
370            writeln!(f, "✗ {} not found (bloom={}).", self.query, self.bloom_hit)?;
371        }
372        if !self.candidates.is_empty() {
373            writeln!(f, "Nearest candidates:")?;
374            for candidate in &self.candidates {
375                writeln!(
376                    f,
377                    "  {:>4.2}  {} at {}:{}",
378                    candidate.confidence, candidate.qualified_name, candidate.path, candidate.line
379                )?;
380            }
381        } else if !self.exists {
382            writeln!(
383                f,
384                "  hint: run `tessera search {}` for broader fuzzy matching, or re-index with `tessera index . --full` if this symbol should exist.",
385                self.query
386            )?;
387        }
388        write_meta(f, &self.meta)
389    }
390}
391
392#[derive(Debug, Clone, Serialize, Deserialize)]
393pub struct SnippetReferenceCheck {
394    pub symbol_name: String,
395    pub line: usize,
396    pub column: usize,
397    pub exists: bool,
398    pub candidates: Vec<SymbolSuggestion>,
399}
400
401#[derive(Debug, Clone, Serialize, Deserialize)]
402pub struct ValidateSnippetResult {
403    pub language: String,
404    pub total_calls: usize,
405    pub unresolved_calls: usize,
406    pub checks: Vec<SnippetReferenceCheck>,
407    pub meta: QueryMeta,
408}
409
410impl Display for ValidateSnippetResult {
411    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
412        writeln!(
413            f,
414            "Validated {} calls ({} unresolved) in {} snippet.",
415            self.total_calls, self.unresolved_calls, self.language
416        )?;
417        for check in &self.checks {
418            let mark = if check.exists { "✓" } else { "✗" };
419            writeln!(
420                f,
421                "  {} {} at line {} col {}",
422                mark, check.symbol_name, check.line, check.column
423            )?;
424            if !check.exists {
425                for candidate in check.candidates.iter().take(3) {
426                    writeln!(
427                        f,
428                        "      -> maybe {} ({:.2})",
429                        candidate.qualified_name, candidate.confidence
430                    )?;
431                }
432            }
433        }
434        write_meta(f, &self.meta)
435    }
436}
437
438#[derive(Debug, Clone, Serialize, Deserialize)]
439pub struct ContextPack {
440    pub symbol: Option<SymbolRecord>,
441    pub body: Option<String>,
442    pub dependency_signatures: Vec<SignatureLine>,
443    pub caller_signatures: Vec<SignatureLine>,
444    pub tests: Vec<SymbolRecord>,
445    pub budget_tokens: usize,
446    pub meta: QueryMeta,
447}
448
449#[derive(Debug, Clone, Serialize, Deserialize)]
450pub struct SignatureLine {
451    pub qualified_name: String,
452    pub kind: String,
453    pub path: String,
454    pub line: usize,
455    pub signature: String,
456}
457
458impl Display for ContextPack {
459    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
460        let Some(symbol) = &self.symbol else {
461            return writeln!(f, "No symbol found.");
462        };
463        writeln!(
464            f,
465            "{} {} at {}:{} (budget ~{} tokens)",
466            symbol.kind, symbol.qualified_name, symbol.path, symbol.start_line, self.budget_tokens
467        )?;
468        if let Some(body) = &self.body {
469            writeln!(f, "\n# body\n{body}")?;
470        }
471        if !self.dependency_signatures.is_empty() {
472            writeln!(f, "\n# dependencies")?;
473            for sig in &self.dependency_signatures {
474                writeln!(f, "  {} @ {}:{}", sig.qualified_name, sig.path, sig.line)?;
475                if !sig.signature.is_empty() {
476                    writeln!(f, "    {}", sig.signature)?;
477                }
478            }
479        }
480        if !self.caller_signatures.is_empty() {
481            writeln!(f, "\n# callers")?;
482            for sig in &self.caller_signatures {
483                writeln!(f, "  {} @ {}:{}", sig.qualified_name, sig.path, sig.line)?;
484                if !sig.signature.is_empty() {
485                    writeln!(f, "    {}", sig.signature)?;
486                }
487            }
488        }
489        if !self.tests.is_empty() {
490            writeln!(f, "\n# tests")?;
491            for test in &self.tests {
492                writeln!(
493                    f,
494                    "  {} @ {}:{}",
495                    test.qualified_name, test.path, test.start_line
496                )?;
497            }
498        }
499        write_meta(f, &self.meta)
500    }
501}
502
503#[derive(Debug, Clone, Serialize, Deserialize)]
504pub struct PlanQueryResult {
505    pub query: String,
506    pub inferred_intent: String,
507    pub steps: Vec<PlanStep>,
508    pub meta: QueryMeta,
509}
510
511#[derive(Debug, Clone, Serialize, Deserialize)]
512pub struct PlanStep {
513    pub order: usize,
514    pub tool: String,
515    pub command: String,
516    pub reason: String,
517    pub expected_tokens: usize,
518}
519
520impl Display for PlanQueryResult {
521    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
522        writeln!(f, "Plan for {:?} ({})", self.query, self.inferred_intent)?;
523        for step in &self.steps {
524            writeln!(
525                f,
526                "  {}. {}  ~{} tokens",
527                step.order, step.command, step.expected_tokens
528            )?;
529            writeln!(f, "     {}", step.reason)?;
530        }
531        write_meta(f, &self.meta)
532    }
533}
534
535#[derive(Debug, Clone, Serialize, Deserialize)]
536pub struct EditPrepResult {
537    pub symbol: String,
538    pub validate: ValidateResult,
539    pub signature: SignatureResult,
540    pub siblings: SiblingsResult,
541    pub context: ContextPack,
542    pub tests: TestsForResult,
543    pub next_steps: Vec<PlanStep>,
544    pub meta: QueryMeta,
545}
546
547impl Display for EditPrepResult {
548    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
549        writeln!(f, "Edit prep for {}", self.symbol)?;
550        writeln!(f, "  exists: {}", self.validate.exists)?;
551        if let Some(symbol) = &self.signature.symbol {
552            writeln!(
553                f,
554                "  target: {} {} @ {}:{}",
555                symbol.kind, symbol.qualified_name, symbol.path, symbol.start_line
556            )?;
557        }
558        writeln!(
559            f,
560            "  context: body={} deps={} callers={} tests={}",
561            self.context.body.is_some(),
562            self.context.dependency_signatures.len(),
563            self.context.caller_signatures.len(),
564            self.tests.tests.len()
565        )?;
566        if !self.siblings.siblings.is_empty() {
567            writeln!(f, "  siblings:")?;
568            for sibling in self.siblings.siblings.iter().take(5) {
569                writeln!(
570                    f,
571                    "    {} ({} shared callers)",
572                    sibling.qualified_name, sibling.shared_callers
573                )?;
574            }
575        }
576        if !self.next_steps.is_empty() {
577            writeln!(f, "\nNext steps:")?;
578            for step in &self.next_steps {
579                writeln!(f, "  {}. {}", step.order, step.command)?;
580            }
581        }
582        write_meta(f, &self.meta)
583    }
584}
585
586#[derive(Debug, Clone, Serialize, Deserialize)]
587pub struct DiffImpactResult {
588    pub from_ref: String,
589    pub to_ref: String,
590    pub changed_files: usize,
591    pub changed_symbols: Vec<DiffChangedSymbol>,
592    pub impacted: Vec<DiffImpactedSymbol>,
593    pub meta: QueryMeta,
594}
595
596#[derive(Debug, Clone, Serialize, Deserialize)]
597pub struct DiffChangedSymbol {
598    pub symbol: SymbolRecord,
599    pub added_lines: usize,
600    pub removed_lines: usize,
601}
602
603#[derive(Debug, Clone, Serialize, Deserialize)]
604pub struct DiffImpactedSymbol {
605    pub symbol: SymbolRecord,
606    pub via: String,
607    pub criticality: f32,
608}
609
610impl Display for DiffImpactResult {
611    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
612        writeln!(
613            f,
614            "Diff impact {}..{}  ·  {} changed files, {} changed symbols, {} impacted",
615            self.from_ref,
616            self.to_ref,
617            self.changed_files,
618            self.changed_symbols.len(),
619            self.impacted.len()
620        )?;
621        if !self.changed_symbols.is_empty() {
622            writeln!(f, "\n# changed")?;
623            for c in &self.changed_symbols {
624                writeln!(
625                    f,
626                    "  +{:>3} -{:<3} {} @ {}:{}",
627                    c.added_lines,
628                    c.removed_lines,
629                    c.symbol.qualified_name,
630                    c.symbol.path,
631                    c.symbol.start_line
632                )?;
633            }
634        }
635        if !self.impacted.is_empty() {
636            writeln!(f, "\n# impacted callers (top {})", self.impacted.len())?;
637            for i in &self.impacted {
638                writeln!(
639                    f,
640                    "  score {:>5.1}  {} (via {}) @ {}:{}",
641                    i.criticality,
642                    i.symbol.qualified_name,
643                    i.via,
644                    i.symbol.path,
645                    i.symbol.start_line
646                )?;
647            }
648        }
649        write_meta(f, &self.meta)
650    }
651}
652
653#[derive(Debug, Clone, Serialize, Deserialize)]
654pub struct ImportRecord {
655    pub source: String,
656    pub from_path: String,
657    pub line: usize,
658    pub kind: String,
659}
660
661#[derive(Debug, Clone, Serialize, Deserialize)]
662pub struct ImportsResult {
663    pub path: String,
664    pub imports: Vec<ImportRecord>,
665    pub meta: QueryMeta,
666}
667
668impl Display for ImportsResult {
669    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
670        if self.imports.is_empty() {
671            writeln!(f, "No imports under {}.", self.path)?;
672        } else {
673            writeln!(f, "Imports for {}", self.path)?;
674            for imp in &self.imports {
675                writeln!(f, "  {} ({}) @ line {}", imp.source, imp.kind, imp.line)?;
676            }
677        }
678        write_meta(f, &self.meta)
679    }
680}
681
682#[derive(Debug, Clone, Serialize, Deserialize)]
683pub struct ImportedByResult {
684    pub source: String,
685    pub importers: Vec<ImportRecord>,
686    pub meta: QueryMeta,
687}
688
689impl Display for ImportedByResult {
690    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
691        if self.importers.is_empty() {
692            writeln!(f, "Nothing imports {}.", self.source)?;
693        } else {
694            writeln!(
695                f,
696                "{} is imported by ({}):",
697                self.source,
698                self.importers.len()
699            )?;
700            for imp in &self.importers {
701                writeln!(f, "  {} @ line {}", imp.from_path, imp.line)?;
702            }
703        }
704        write_meta(f, &self.meta)
705    }
706}
707
708#[derive(Debug, Clone, Serialize, Deserialize)]
709pub struct SignatureResult {
710    pub symbol: Option<SymbolRecord>,
711    pub members: Vec<SignatureLine>,
712    pub meta: QueryMeta,
713}
714
715impl Display for SignatureResult {
716    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
717        let Some(symbol) = &self.symbol else {
718            return writeln!(f, "No symbol found.");
719        };
720        writeln!(
721            f,
722            "{} {} @ {}:{}",
723            symbol.kind, symbol.qualified_name, symbol.path, symbol.start_line
724        )?;
725        if !symbol.signature.is_empty() {
726            writeln!(f, "  {}", symbol.signature)?;
727        }
728        if !self.members.is_empty() {
729            writeln!(f, "  members:")?;
730            for m in &self.members {
731                writeln!(f, "    {:<9} {}", m.kind, m.signature)?;
732            }
733        }
734        write_meta(f, &self.meta)
735    }
736}
737
738#[derive(Debug, Clone, Serialize, Deserialize)]
739pub struct SiblingsResult {
740    pub symbol: String,
741    pub siblings: Vec<Sibling>,
742    pub meta: QueryMeta,
743}
744
745#[derive(Debug, Clone, Serialize, Deserialize)]
746pub struct Sibling {
747    pub qualified_name: String,
748    pub shared_callers: usize,
749    pub path: Option<String>,
750    pub line: Option<usize>,
751}
752
753impl Display for SiblingsResult {
754    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
755        if self.siblings.is_empty() {
756            writeln!(f, "No siblings found for {}.", self.symbol)?;
757        } else {
758            writeln!(f, "Siblings of {} (by shared callers)", self.symbol)?;
759            for s in &self.siblings {
760                let where_ = match (&s.path, s.line) {
761                    (Some(p), Some(l)) => format!("{p}:{l}"),
762                    _ => "—".to_string(),
763                };
764                writeln!(
765                    f,
766                    "  {:>3}  {} @ {}",
767                    s.shared_callers, s.qualified_name, where_
768                )?;
769            }
770        }
771        write_meta(f, &self.meta)
772    }
773}
774
775#[derive(Debug, Clone, Default, Serialize, Deserialize)]
776pub struct SearchOptions {
777    /// Restrict to these symbol kinds (function, method, class, struct, …).
778    /// Empty means "any".
779    pub kinds: Vec<String>,
780    /// Restrict to these languages (typescript, java, …). Empty means "any".
781    pub languages: Vec<String>,
782    /// `Some(true)` = only exported; `Some(false)` = only non-exported.
783    pub exported: Option<bool>,
784    /// Match symbols whose file path starts with this prefix.
785    pub path_prefix: Option<String>,
786    /// Maximum number of hits to return.
787    pub limit: usize,
788}
789
790#[derive(Debug, Clone, Default, Serialize, Deserialize)]
791pub struct UnusedOptions {
792    /// Restrict to these symbol kinds (function, method, class, struct, ...).
793    /// Empty means "any".
794    pub kinds: Vec<String>,
795    /// Restrict to these languages (typescript, java, ...). Empty means "any".
796    pub languages: Vec<String>,
797    /// `Some(true)` = only exported; `Some(false)` = only non-exported.
798    pub exported: Option<bool>,
799    /// Match symbols whose file path starts with this prefix.
800    pub path_prefix: Option<String>,
801    /// Maximum number of hits to return.
802    pub limit: usize,
803}
804
805#[derive(Debug, Clone, Serialize, Deserialize)]
806pub struct UnusedSymbol {
807    pub symbol: SymbolRecord,
808    pub inbound_refs: usize,
809    pub inbound_edges: usize,
810}
811
812#[derive(Debug, Clone, Serialize, Deserialize)]
813pub struct UnusedResult {
814    pub symbols: Vec<UnusedSymbol>,
815    pub meta: QueryMeta,
816}
817
818impl Display for UnusedResult {
819    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
820        if self.symbols.is_empty() {
821            writeln!(f, "No unused symbols found.")?;
822            writeln!(
823                f,
824                "  hint: relax filters such as `--kind`, `--language`, `--path`, or `--exported=false`."
825            )?;
826        } else {
827            writeln!(f, "Unused symbols ({}):", self.symbols.len())?;
828            for unused in &self.symbols {
829                let symbol = &unused.symbol;
830                writeln!(
831                    f,
832                    "  {:<9} {} at {}:{}",
833                    symbol.kind, symbol.qualified_name, symbol.path, symbol.start_line
834                )?;
835            }
836        }
837        write_meta(f, &self.meta)
838    }
839}
840
841#[derive(Debug, Clone, Serialize, Deserialize)]
842pub struct SearchHit {
843    pub symbol: SymbolRecord,
844    pub score: f32,
845}
846
847#[derive(Debug, Clone, Serialize, Deserialize)]
848pub struct SearchResult {
849    pub query: String,
850    pub hits: Vec<SearchHit>,
851    pub meta: QueryMeta,
852}
853
854impl Display for SearchResult {
855    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
856        if self.hits.is_empty() {
857            writeln!(f, "No symbols matched {:?}.", self.query)?;
858            writeln!(
859                f,
860                "  hint: try a wider pattern such as `*{}*`, remove filters, or run `tessera validate {}` for near-misses.",
861                self.query, self.query
862            )?;
863        } else {
864            writeln!(f, "Search for {:?} ({} hits)", self.query, self.hits.len())?;
865            for hit in &self.hits {
866                writeln!(
867                    f,
868                    "  {:>4.2}  {:<9} {:<40} {}:{}",
869                    hit.score,
870                    hit.symbol.kind,
871                    hit.symbol.qualified_name,
872                    hit.symbol.path,
873                    hit.symbol.start_line
874                )?;
875            }
876        }
877        write_meta(f, &self.meta)
878    }
879}
880
881#[derive(Debug, Clone, Serialize, Deserialize)]
882pub struct StatsResult {
883    pub files: usize,
884    pub symbols: usize,
885    pub references: usize,
886    pub edges: usize,
887    pub languages: Vec<LanguageCount>,
888    pub kinds: Vec<KindCount>,
889    pub top_fanout: Vec<TopFanout>,
890    pub db_path: String,
891    pub snapshot_present: bool,
892    pub meta: QueryMeta,
893}
894
895impl Display for StatsResult {
896    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
897        writeln!(
898            f,
899            "Tessera index at {} (snapshot={})",
900            self.db_path, self.snapshot_present
901        )?;
902        writeln!(
903            f,
904            "  files={}  symbols={}  references={}  edges={}",
905            self.files, self.symbols, self.references, self.edges
906        )?;
907        if !self.languages.is_empty() {
908            write!(f, "  languages: ")?;
909            let parts: Vec<String> = self
910                .languages
911                .iter()
912                .map(|lc| format!("{}={}", lc.language, lc.count))
913                .collect();
914            writeln!(f, "{}", parts.join(", "))?;
915        }
916        if !self.kinds.is_empty() {
917            write!(f, "  kinds: ")?;
918            let parts: Vec<String> = self
919                .kinds
920                .iter()
921                .map(|kc| format!("{}={}", kc.kind, kc.count))
922                .collect();
923            writeln!(f, "{}", parts.join(", "))?;
924        }
925        if !self.top_fanout.is_empty() {
926            writeln!(f, "  top fanout:")?;
927            for tf in &self.top_fanout {
928                writeln!(f, "    {:>4} {}", tf.callers, tf.qualified_name)?;
929            }
930        }
931        write_meta(f, &self.meta)
932    }
933}
934
935#[derive(Debug, Clone, Serialize, Deserialize)]
936pub struct LanguageCount {
937    pub language: String,
938    pub count: usize,
939}
940
941#[derive(Debug, Clone, Serialize, Deserialize)]
942pub struct KindCount {
943    pub kind: String,
944    pub count: usize,
945}
946
947#[derive(Debug, Clone, Serialize, Deserialize)]
948pub struct TopFanout {
949    pub qualified_name: String,
950    pub callers: usize,
951}
952
953#[derive(Debug, Clone, Serialize, Deserialize)]
954pub struct TestsForResult {
955    pub symbol: String,
956    pub tests: Vec<SymbolRecord>,
957    pub meta: QueryMeta,
958}
959
960impl Display for TestsForResult {
961    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
962        if self.tests.is_empty() {
963            writeln!(f, "No tests transitively touch {}.", self.symbol)?;
964        } else {
965            writeln!(f, "Tests touching {} ({}):", self.symbol, self.tests.len())?;
966            for test in &self.tests {
967                writeln!(
968                    f,
969                    "  {} at {}:{}",
970                    test.qualified_name, test.path, test.start_line
971                )?;
972            }
973        }
974        write_meta(f, &self.meta)
975    }
976}
977
978#[derive(Debug, Clone, Serialize, Deserialize)]
979pub struct BenchResult {
980    pub path: String,
981    pub files: usize,
982    pub symbols: usize,
983    pub references: usize,
984    pub index_full_ms: u128,
985    pub index_incremental_ms: u128,
986    pub queries: Vec<BenchQuery>,
987    pub savings: Vec<BenchSavings>,
988    pub chart: String,
989}
990
991impl Display for BenchResult {
992    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
993        writeln!(f, "{}", self.chart)
994    }
995}
996
997#[derive(Debug, Clone, Serialize, Deserialize)]
998pub struct BenchQuery {
999    pub name: String,
1000    pub ms: u128,
1001    pub tokens: usize,
1002}
1003
1004#[derive(Debug, Clone, Serialize, Deserialize)]
1005pub struct BenchSavings {
1006    pub label: String,
1007    pub raw_tokens: usize,
1008    pub tessera_tokens: usize,
1009    pub ratio: f32,
1010}
1011
1012#[derive(Debug, Clone, Serialize, Deserialize)]
1013pub struct PathNode {
1014    pub qualified_name: String,
1015    pub kind: String,
1016    pub path: String,
1017    pub line: usize,
1018}
1019
1020#[derive(Debug, Clone, Serialize, Deserialize)]
1021pub struct ConnectResult {
1022    pub from: String,
1023    pub to: String,
1024    pub found: bool,
1025    /// Ordered call path from `from` to `to` (inclusive of both endpoints).
1026    pub path: Vec<PathNode>,
1027    pub meta: QueryMeta,
1028}
1029
1030impl Display for ConnectResult {
1031    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1032        if !self.found {
1033            writeln!(
1034                f,
1035                "No call path from {} to {} (within search depth).",
1036                self.from, self.to
1037            )?;
1038            return write_meta(f, &self.meta);
1039        }
1040        writeln!(
1041            f,
1042            "Call path {} → {} ({} hops)",
1043            self.from,
1044            self.to,
1045            self.path.len().saturating_sub(1)
1046        )?;
1047        for (i, node) in self.path.iter().enumerate() {
1048            let arrow = if i == 0 { "  " } else { "  ↳ " };
1049            writeln!(
1050                f,
1051                "{}{} @ {}:{}",
1052                arrow, node.qualified_name, node.path, node.line
1053            )?;
1054        }
1055        write_meta(f, &self.meta)
1056    }
1057}
1058
1059#[derive(Debug, Clone, Serialize, Deserialize)]
1060pub struct ExportResult {
1061    pub format: String,
1062    pub scope: String,
1063    pub group_by: String,
1064    pub nodes: usize,
1065    pub edges: usize,
1066    pub truncated: bool,
1067    /// The rendered graph in the requested format (DOT or Mermaid).
1068    pub diagram: String,
1069    #[serde(skip_serializing_if = "Option::is_none")]
1070    pub html_path: Option<String>,
1071    pub meta: QueryMeta,
1072}
1073
1074impl Display for ExportResult {
1075    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1076        // The diagram itself IS the output so it pipes cleanly into
1077        // `dot`/mermaid. Counts + truncation go to the meta line as a comment.
1078        write!(f, "{}", self.diagram)
1079    }
1080}
1081
1082fn write_meta(f: &mut fmt::Formatter<'_>, meta: &QueryMeta) -> fmt::Result {
1083    writeln!(
1084        f,
1085        "\n_meta: ~{} tokens returned; alternatives: {}",
1086        meta.tokens_returned_estimate,
1087        meta.alternative_queries
1088            .iter()
1089            .map(|query| format!(
1090                "{} (~{} tokens, {:.2} fidelity)",
1091                query.tool, query.tokens_estimate, query.fidelity
1092            ))
1093            .collect::<Vec<_>>()
1094            .join(", ")
1095    )
1096}