Skip to main content

pathfinder_lib/server/
types.rs

1//! Tool parameter and response types for Pathfinder MCP tools.
2//!
3//! These structs are deserialized by the rmcp framework from MCP tool call
4//! payloads. The `dead_code` lint fires for param struct fields that are read
5//! by serde (via `Deserialize`) but never accessed by name in production code.
6//! A module-level `#![allow]` is used here so that each newly implemented tool
7//! can remove its struct's allow without touching unrelated items.
8#![allow(dead_code)] // Fields are read by serde deserialization, not by name
9
10use pathfinder_common::types::{ActionableGuidance, DegradedReason, FilterMode};
11use rmcp::schemars;
12use rmcp::serde::{self, Deserialize, Serialize};
13
14/// Response for `symbol_overview`.
15#[derive(Debug, Serialize, serde::Deserialize, schemars::JsonSchema)]
16pub struct SymbolOverviewResponse {
17    /// Source code and location of the symbol.
18    #[serde(skip_serializing_if = "Option::is_none")]
19    pub source: Option<SymbolSource>,
20    /// Impact analysis (incoming callers + outgoing callees).
21    #[serde(skip_serializing_if = "Option::is_none")]
22    pub impact: Option<ImpactSummary>,
23    /// Reference locations across the codebase.
24    /// Absent (omitted from JSON) when LSP was unavailable — references are unknown.
25    /// Present as `[]` when LSP confirmed zero references.
26    #[serde(skip_serializing_if = "Option::is_none")]
27    pub references: Option<Vec<SymbolOverviewReference>>,
28    /// Total number of files containing references.
29    pub files_referenced: usize,
30    /// Whether any component was degraded.
31    pub degraded: bool,
32    /// Whether the impact analysis (callers/callees) was degraded.
33    #[serde(skip_serializing_if = "std::ops::Not::not", default)]
34    pub impact_degraded: bool,
35    /// Whether the references lookup was degraded.
36    #[serde(skip_serializing_if = "std::ops::Not::not", default)]
37    pub references_degraded: bool,
38    /// Reason for degradation, if any.
39    #[serde(skip_serializing_if = "Option::is_none")]
40    pub degraded_reason: Option<DegradedReason>,
41    /// Machine-readable guidance when `degraded` is `true`.
42    #[serde(skip_serializing_if = "Option::is_none")]
43    pub actionable_guidance: Option<ActionableGuidance>,
44    /// LSP readiness at the time of the call.
45    #[serde(skip_serializing_if = "Option::is_none")]
46    pub lsp_readiness: Option<String>,
47    /// Whether warm start is in progress (set on timeout only).
48    #[serde(skip_serializing_if = "Option::is_none")]
49    pub warm_start_in_progress: Option<bool>,
50}
51
52/// Source code block for `symbol_overview`.
53#[derive(Debug, Clone, Serialize, Deserialize, schemars::JsonSchema)]
54pub struct SymbolSource {
55    /// Content of the symbol (source code).
56    pub content: String,
57    /// Starting line number (1-indexed).
58    pub start_line: usize,
59    /// Ending line number (1-indexed).
60    pub end_line: usize,
61    /// Programming language.
62    pub language: String,
63}
64
65/// Impact summary for `symbol_overview`.
66#[derive(Debug, Clone, Serialize, Deserialize, schemars::JsonSchema)]
67pub struct ImpactSummary {
68    /// Direct callers.
69    /// Absent (omitted from JSON) when LSP was unavailable — callers are unknown.
70    /// Present as `[]` when LSP confirmed zero callers.
71    #[serde(skip_serializing_if = "Option::is_none")]
72    pub incoming: Option<Vec<SymbolOverviewImpactEntry>>,
73    /// Direct callees.
74    /// Absent (omitted from JSON) when LSP was unavailable — callees are unknown.
75    /// Present as `[]` when LSP confirmed zero callees.
76    #[serde(skip_serializing_if = "Option::is_none")]
77    pub outgoing: Option<Vec<SymbolOverviewImpactEntry>>,
78    /// Whether the impact analysis was degraded.
79    pub degraded: bool,
80}
81
82/// A single entry in the impact summary.
83#[derive(Debug, Clone, Serialize, Deserialize, schemars::JsonSchema)]
84pub struct SymbolOverviewImpactEntry {
85    pub semantic_path: String,
86    pub file: String,
87    pub line: usize,
88    pub snippet: String,
89    pub direction: String,
90}
91
92/// A reference location in `symbol_overview`.
93#[derive(Debug, Clone, Serialize, Deserialize, schemars::JsonSchema)]
94pub struct SymbolOverviewReference {
95    pub file: String,
96    pub line: u32,
97    pub column: u32,
98    pub snippet: String,
99}
100
101// ── Legacy Response Types ───────────────────────────────────────────
102// These response structs are returned by the underlying `_impl` methods
103// and serialized into MCP responses by the `#[tool_router]` handlers in `server.rs`.
104
105/// The response for `search_codebase`.
106#[derive(Debug, Serialize, schemars::JsonSchema)]
107pub struct SearchCodebaseResponse {
108    /// List of search matches.
109    pub matches: Vec<pathfinder_search::SearchMatch>,
110    /// Raw match count from ripgrep **before** `filter_mode` filtering, **after** ripgrep truncation.
111    ///
112    /// When `truncated = true`, this equals `max_results` and ripgrep stopped searching early.
113    /// When `filter_mode` is `"comments_only"` or `"code_only"`, matches that do not
114    /// pass the filter are excluded from `matches` but still counted here.
115    /// Compare with `total_matches` to see how many matches were removed by filtering.
116    pub raw_match_count: usize,
117    /// Total matches in this response (after `filter_mode` filtering).
118    ///
119    /// This always equals `matches.len()` and `returned_count`. Provided for consistency
120    /// with agent expectations: "total" means what you actually get, not ripgrep's pre-filter count.
121    /// Use `raw_match_count` to see ripgrep's count before filtering.
122    /// Use `filtered_count` to see how many matches were removed by `filter_mode`.
123    pub total_matches: usize,
124    /// Number of matches actually returned in this response (after `filter_mode` filtering).
125    ///
126    /// `returned_count == total_matches == matches.len()`. Provided as a convenience field
127    /// and for backward compatibility.
128    pub returned_count: usize,
129    /// Number of matches removed by `filter_mode` filtering.
130    ///
131    /// `filtered_count = raw_match_count - total_matches`.
132    /// When `filter_mode = "All"`, this is always 0.
133    pub filtered_count: usize,
134    /// Number of files that were actually searched.
135    pub files_searched: usize,
136    /// Number of searchable files matching the `path_glob` (excludes binary and gitignored).
137    /// When `files_searched < files_in_scope`, some files were skipped
138    /// due to permission issues or I/O errors.
139    pub files_in_scope: usize,
140    /// Percentage of in-scope files that were actually searched.
141    /// 100% means exhaustive search; lower values indicate skipped files.
142    pub coverage_percent: u8,
143    /// Indicates if the match list was truncated by `max_results`.
144    pub truncated: bool,
145    /// Grouped output — populated when `group_by_file: true`.
146    ///
147    /// Each group represents one file and contains either full matches (for
148    /// unknown files) or minimal matches (for files in `known_files`).
149    #[serde(skip_serializing_if = "Option::is_none")]
150    pub file_groups: Option<Vec<SearchResultGroup>>,
151    /// Whether the search response is degraded.
152    #[serde(default)]
153    pub degraded: bool,
154    /// Reason for degradation, if any.
155    #[serde(skip_serializing_if = "Option::is_none")]
156    pub degraded_reason: Option<DegradedReason>,
157    /// When results are truncated, this field provides the `offset` value
158    /// to use for the next page of results. Absent when not truncated.
159    #[serde(skip_serializing_if = "Option::is_none")]
160    pub next_offset: Option<u32>,
161    /// Actionable hint when `filter_mode` removes all results.
162    /// Suggests retrying with `filter_mode=all` when matches exist but were filtered out.
163    #[serde(skip_serializing_if = "Option::is_none")]
164    pub hint: Option<String>,
165    /// Machine-readable guidance when `degraded` is `true`.
166    /// Tells the agent whether to retry, what fallback tool to use, and whether
167    /// results are trustworthy.
168    #[serde(skip_serializing_if = "Option::is_none")]
169    pub actionable_guidance: Option<ActionableGuidance>,
170    /// Wall-clock time in milliseconds that this tool call took to complete.
171    #[serde(skip_serializing_if = "Option::is_none")]
172    pub duration_ms: Option<u64>,
173    /// Files skipped because they matched known binary extensions.
174    pub binary_skipped: usize,
175    /// Files skipped because they were excluded by `.gitignore` rules.
176    pub gitignored_skipped: usize,
177    /// Files skipped for other reasons (permission denied, I/O error, etc.).
178    pub other_skipped: usize,
179}
180
181/// A minimal match entry for files already in the agent's context (`known_files`)
182/// when grouped by file.
183///
184/// Omits `file`, `version_hash` (deduplicated at group level), and `content`,
185/// `context_before`, and `context_after` to save tokens.
186#[derive(Debug, Serialize, schemars::JsonSchema)]
187pub struct GroupedKnownMatch {
188    /// 1-indexed line number.
189    pub line: u64,
190    /// 1-indexed column number.
191    pub column: u64,
192    /// AST symbol enclosing this match (if available).
193    #[serde(skip_serializing_if = "Option::is_none")]
194    pub enclosing_semantic_path: Option<String>,
195    /// Whether this match is at a definition position (fn, struct, class, etc.).
196    #[serde(skip_serializing_if = "Option::is_none")]
197    pub is_definition: Option<bool>,
198    /// Always `true` — signals this match was suppressed because the file is known.
199    pub known: bool,
200}
201
202/// A group of matches belonging to one file, returned when `group_by_file: true`.
203#[derive(Debug, Serialize, schemars::JsonSchema)]
204pub struct SearchResultGroup {
205    /// File path relative to workspace root.
206    pub file: String,
207    /// Short content fingerprint of the file (7-char hex, derived from SHA-256).
208    /// Shared by all matches in this group.
209    pub version_hash: String,
210    /// Total number of matches in this group (both full and known).
211    ///
212    /// Provided so agents can quickly assess match density without counting sub-arrays.
213    /// Always present regardless of whether `matches` or `known_matches` are serialized.
214    pub total_matches: usize,
215    /// Full matches (for files NOT in `known_files`).
216    ///
217    /// Per-match objects contain only `{ line, column, content, context_before,
218    /// context_after, enclosing_semantic_path }` — `file` and `version_hash` are
219    /// deduplicated at group level to avoid repeating them for every match.
220    ///
221    /// Absent (not just empty) when all matches in this group are for known files.
222    /// Check `total_matches` for the match count regardless of which array is populated.
223    #[serde(skip_serializing_if = "Vec::is_empty")]
224    #[schemars(skip)]
225    pub matches: Vec<GroupedMatch>,
226    /// Minimal matches (for files in `known_files`).
227    ///
228    /// Absent when no matches in this group are for known files.
229    #[serde(skip_serializing_if = "Vec::is_empty")]
230    #[schemars(skip)]
231    pub known_matches: Vec<GroupedKnownMatch>,
232}
233/// A single match within a `SearchResultGroup`.
234///
235/// Omits `file` and `version_hash` (deduplicated at group level) to reduce
236/// token usage when many matches belong to the same file.
237#[derive(Debug, Serialize, schemars::JsonSchema)]
238pub struct GroupedMatch {
239    /// 1-indexed line number of the match.
240    pub line: u64,
241    /// 1-indexed column number of the match start.
242    pub column: u64,
243    /// The full content of the matching line.
244    pub content: String,
245    /// Lines immediately before the match.
246    #[serde(skip_serializing_if = "Vec::is_empty")]
247    pub context_before: Vec<String>,
248    /// Lines immediately after the match.
249    #[serde(skip_serializing_if = "Vec::is_empty")]
250    pub context_after: Vec<String>,
251    /// AST symbol enclosing this match (if available).
252    #[serde(skip_serializing_if = "Option::is_none")]
253    pub enclosing_semantic_path: Option<String>,
254    /// Whether this match is at a definition position (fn, struct, class, etc.).
255    #[serde(skip_serializing_if = "Option::is_none")]
256    pub is_definition: Option<bool>,
257}
258
259/// The metadata embedded in `structured_content` for `get_repo_map`.
260#[derive(Debug, Serialize, serde::Deserialize, schemars::JsonSchema)]
261pub struct GetRepoMapMetadata {
262    /// Technology stack of the repository.
263    pub tech_stack: Vec<String>,
264    /// The detail mode used for this response: `"structure"`, `"files"`, or `"symbols"`.
265    #[serde(skip_serializing_if = "Option::is_none")]
266    pub mode: Option<String>,
267    /// Number of directories scanned during repository mapping (structure mode only).
268    #[serde(skip_serializing_if = "Option::is_none")]
269    pub dirs_scanned: Option<usize>,
270    /// Number of files scanned.
271    pub files_scanned: usize,
272    /// Number of files truncated.
273    pub files_truncated: usize,
274    /// File paths that were truncated due to token budget.
275    #[serde(default, skip_serializing_if = "Vec::is_empty")]
276    pub truncated_paths: Vec<String>,
277    /// Number of files within the configured scope.
278    pub files_in_scope: usize,
279    /// Percentage of files covered by the search.
280    pub coverage_percent: u8,
281    /// Map of file paths to their version hashes.
282    pub version_hashes: std::collections::HashMap<String, String>,
283    /// Absent when visibility filtering is working correctly.
284    /// Present as `true` if the visibility filter could not be applied
285    /// (agents should then treat all symbols as public).
286    #[serde(skip_serializing_if = "Option::is_none")]
287    pub visibility_degraded: Option<bool>,
288    /// `true` when filtering by `changed_since` fails (e.g., git is unavailable).
289    #[serde(default)]
290    pub degraded: bool,
291    /// Reason for degradation.
292    #[serde(skip_serializing_if = "Option::is_none")]
293    pub degraded_reason: Option<DegradedReason>,
294    /// Machine-readable guidance when `degraded` is `true`.
295    #[serde(skip_serializing_if = "Option::is_none")]
296    pub actionable_guidance: Option<ActionableGuidance>,
297    /// System capabilities available for this repository.
298    pub capabilities: RepoCapabilities,
299    /// Actual `max_tokens` used (may differ from requested due to auto-scaling).
300    pub max_tokens_used: u32,
301    /// Flat map of language ID to status string (`"ready"`, `"warming_up"`, `"unavailable"`).
302    #[serde(skip_serializing_if = "Option::is_none")]
303    pub lsp_status: Option<std::collections::HashMap<String, String>>,
304    /// Wall-clock time in milliseconds that this tool call took to complete.
305    #[serde(skip_serializing_if = "Option::is_none")]
306    pub duration_ms: Option<u64>,
307    /// Optional hint for the agent (e.g. suggesting to increase `max_tokens` if coverage < 100).
308    #[serde(skip_serializing_if = "Option::is_none")]
309    pub hint: Option<String>,
310    /// Structured recommendation for `max_tokens` to achieve full coverage.
311    #[serde(skip_serializing_if = "Option::is_none")]
312    pub suggested_max_tokens: Option<u32>,
313}
314
315/// The overall capabilities of the Pathfinder system.
316#[derive(Debug, Serialize, serde::Deserialize, schemars::JsonSchema)]
317pub struct RepoCapabilities {
318    /// Whether the search engine is supported.
319    pub search: bool,
320    /// LSP-specific capabilities and status.
321    pub lsp: LspCapabilities,
322}
323
324/// LSP status and capabilities.
325#[derive(Debug, Serialize, serde::Deserialize, schemars::JsonSchema)]
326pub struct LspCapabilities {
327    /// `true` if LSP is generally supported by the system.
328    pub supported: bool,
329    /// Map of language ID to its specific LSP process status.
330    pub per_language: std::collections::HashMap<String, pathfinder_lsp::types::LspLanguageStatus>,
331}
332
333/// The metadata embedded in `structured_content` for `read_symbol_scope`.
334#[derive(Debug, Serialize, serde::Deserialize, schemars::JsonSchema)]
335pub struct ReadSymbolScopeMetadata {
336    /// The extracted symbol source code.
337    ///
338    /// Mirrors `content[0].text` in the MCP response. Provided here so that
339    /// agents consuming `structured_content` directly have the full source
340    /// without needing to inspect the main content array.
341    pub content: String,
342    /// Starting line number of the symbol in the source.
343    pub start_line: usize,
344    /// Ending line number of the symbol in the source.
345    pub end_line: usize,
346    /// Programming language of the source symbol.
347    pub language: String,
348    /// Wall-clock time in milliseconds that this tool call took to complete.
349    #[serde(skip_serializing_if = "Option::is_none")]
350    pub duration_ms: Option<u64>,
351}
352
353/// A symbol output for `read_source_file`.
354#[derive(Debug, Clone, PartialEq, Serialize, serde::Deserialize, schemars::JsonSchema)]
355pub struct SourceSymbol {
356    /// Name of the symbol.
357    pub name: String,
358    /// Semantic path of the symbol.
359    pub semantic_path: String,
360    /// Kind of the symbol (e.g., function, struct).
361    pub kind: String,
362    /// Starting line number of the symbol in the source.
363    pub start_line: usize,
364    /// Ending line number of the symbol in the source.
365    pub end_line: usize,
366    /// Child symbols nested within this symbol.
367    #[serde(default, skip_serializing_if = "Vec::is_empty")]
368    pub children: Vec<Self>,
369}
370
371/// The metadata embedded in `structured_content` for `read_source_file`.
372#[derive(Debug, Clone, PartialEq, Default, Serialize, serde::Deserialize, schemars::JsonSchema)]
373pub struct ReadSourceFileMetadata {
374    /// Programming language of the source file.
375    pub language: String,
376    /// Clean source content without timing metadata appended.
377    /// Provided so consumers like `read_files` get uncontaminated content.
378    #[serde(default, skip_serializing_if = "Option::is_none")]
379    pub content: Option<String>,
380    /// Symbols extracted from the source file.
381    #[serde(default, skip_serializing_if = "Vec::is_empty")]
382    pub symbols: Vec<SourceSymbol>,
383    /// Wall-clock time in milliseconds that this tool call took to complete.
384    #[serde(skip_serializing_if = "Option::is_none")]
385    pub duration_ms: Option<u64>,
386    /// Whether this file's language is not supported for AST parsing.
387    /// When true, content is raw file content and symbols is empty.
388    #[serde(default, skip_serializing_if = "Option::is_none")]
389    pub unsupported_language: Option<bool>,
390}
391
392/// The metadata embedded in `structured_content` for `read_file`.
393#[derive(Debug, Serialize, serde::Deserialize, schemars::JsonSchema)]
394pub struct ReadFileMetadata {
395    /// First line number returned from the file.
396    pub start_line: u32,
397    /// Number of lines returned.
398    pub lines_returned: u32,
399    /// Total number of lines in the file.
400    pub total_lines: u32,
401    /// Total size of the file in bytes.
402    pub file_size_bytes: u64,
403    /// Whether the output was truncated.
404    pub truncated: bool,
405    /// Detected language of the file.
406    pub language: String,
407    /// Wall-clock time in milliseconds that this tool call took to complete.
408    #[serde(skip_serializing_if = "Option::is_none")]
409    pub duration_ms: Option<u64>,
410}
411
412// ── Navigation Tool Response Types ─────────────────────────────────
413
414/// A dependency signature extracted for `read_with_deep_context`.
415#[derive(Debug, Clone, Serialize, serde::Deserialize, schemars::JsonSchema)]
416pub struct DeepContextDependency {
417    /// Semantic path of the called symbol.
418    pub semantic_path: String,
419    /// Extracted signature (declaration line only, no body).
420    pub signature: String,
421    /// File path relative to workspace root.
422    pub file: String,
423    /// 1-indexed line of the definition.
424    pub line: usize,
425}
426
427/// The metadata embedded in `structured_content` for `read_with_deep_context`.
428#[derive(Debug, Serialize, serde::Deserialize, schemars::JsonSchema)]
429pub struct ReadWithDeepContextMetadata {
430    /// Start line of the symbol (1-indexed).
431    pub start_line: usize,
432    /// End line of the symbol (1-indexed).
433    pub end_line: usize,
434    /// Detected language.
435    pub language: String,
436    /// Signatures of all symbols called by this one.
437    #[serde(skip_serializing_if = "Vec::is_empty", default)]
438    pub dependencies: Vec<DeepContextDependency>,
439    /// `true` when LSP dependency resolution was unavailable (Tree-sitter only).
440    #[serde(default)]
441    pub degraded: bool,
442    /// Reason for degradation (e.g., `"no_lsp"`).
443    #[serde(skip_serializing_if = "Option::is_none")]
444    pub degraded_reason: Option<DegradedReason>,
445    /// Machine-readable guidance when `degraded` is `true`.
446    #[serde(skip_serializing_if = "Option::is_none")]
447    pub actionable_guidance: Option<ActionableGuidance>,
448    /// IW-2: LSP readiness signal at the time of the call.
449    ///
450    /// - `"ready"`: LSP is fully operational — results are authoritative.
451    /// - `"warming_up"`: LSP is still indexing — results may be partial.
452    /// - `"unavailable"`: No LSP; Tree-sitter fallback used.
453    #[serde(skip_serializing_if = "Option::is_none")]
454    pub lsp_readiness: Option<String>,
455    /// Whether the LSP warm-start is still in progress at the time of the call.
456    #[serde(skip_serializing_if = "Option::is_none")]
457    pub warm_start_in_progress: Option<bool>,
458    /// `true` when the `max_dependencies` limit was reached and results were truncated.
459    pub dependencies_truncated: bool,
460    /// Spec 5.2: How the deep context was resolved.
461    /// One of: `lsp_call_hierarchy`, `treesitter_direct`, `treesitter_fallback`, `grep_fallback`.
462    /// Note: `grep_fallback` corresponds to `GrepFallbackDependencies` reason — the finer-grained
463    /// locate/trace values (`grep_file_scoped`, `grep_impl_scoped`, etc.) are NOT emitted here.
464    #[serde(skip_serializing_if = "Option::is_none")]
465    pub resolution_strategy: Option<String>,
466    /// Spec 5.1: Wall-clock duration of the tool call in milliseconds.
467    #[serde(skip_serializing_if = "Option::is_none")]
468    pub duration_ms: Option<u64>,
469    /// File-level import/using statements (populated when `include_imports=true`).
470    ///
471    /// Contains the raw import lines from the file. Useful for Java, C#, Kotlin,
472    /// and other OOP languages where imports clarify what types are in scope.
473    #[serde(skip_serializing_if = "Vec::is_empty", default)]
474    pub imports: Vec<String>,
475    /// Source code of the symbol. Populated so agents parsing `structured_content`
476    /// have access to source alongside dependencies without parsing the text channel.
477    #[serde(skip_serializing_if = "Option::is_none")]
478    pub content: Option<String>,
479}
480
481/// The response for `get_definition`.
482#[derive(Debug, Serialize, serde::Deserialize, schemars::JsonSchema)]
483pub struct GetDefinitionResponse {
484    /// Relative file path of the definition site.
485    pub file: String,
486    /// 1-indexed line number of the definition.
487    pub line: u32,
488    /// 1-indexed column number.
489    pub column: u32,
490    /// First line of the definition (code preview).
491    pub preview: String,
492    /// `true` when LSP was unavailable and no fallback was possible.
493    #[serde(default)]
494    pub degraded: bool,
495    /// Reason for degradation.
496    #[serde(skip_serializing_if = "Option::is_none")]
497    pub degraded_reason: Option<DegradedReason>,
498    /// Machine-readable guidance when `degraded` is `true`.
499    #[serde(skip_serializing_if = "Option::is_none")]
500    pub actionable_guidance: Option<ActionableGuidance>,
501    /// IW-2: LSP readiness at the time of the call.
502    ///
503    /// - `"ready"`: LSP operational — definition is authoritative.
504    /// - `"warming_up"`: LSP still indexing — result may be from Tree-sitter fallback.
505    /// - `"unavailable"`: No LSP; result is from ripgrep heuristics.
506    #[serde(skip_serializing_if = "Option::is_none")]
507    pub lsp_readiness: Option<String>,
508    /// Whether the LSP warm-start is still in progress at the time of the call.
509    /// When `true`, retrying after 15-30s may yield better results.
510    #[serde(skip_serializing_if = "Option::is_none")]
511    pub warm_start_in_progress: Option<bool>,
512    /// Spec 5.1: Wall-clock duration of the tool call in milliseconds.
513    #[serde(skip_serializing_if = "Option::is_none")]
514    pub duration_ms: Option<u64>,
515    /// Spec 5.2: How the definition was resolved.
516    /// One of: `lsp`, `lsp_retry`, `grep_file`, `grep_impl`, `grep_global`, `grep_broad`.
517    #[serde(skip_serializing_if = "Option::is_none")]
518    pub resolution_strategy: Option<String>,
519}
520
521/// A single reference in an impact analysis.
522#[derive(Debug, Serialize, serde::Deserialize, schemars::JsonSchema)]
523pub struct ImpactReference {
524    /// Semantic path of the referencing/referenced symbol.
525    pub semantic_path: String,
526    /// File path relative to workspace root.
527    pub file: String,
528    /// 1-indexed line of the call site or definition.
529    pub line: usize,
530    /// A short code snippet showing the call site or declaration.
531    pub snippet: String,
532    /// Direction of the reference relative to the target symbol.
533    ///
534    /// - `"incoming"` — this symbol calls or references the target (a caller).
535    /// - `"outgoing"` — the target calls or references this symbol (a callee).
536    /// - `"incoming_heuristic"` — inferred by grep fallback when LSP is unavailable;
537    ///   treat as a candidate, not a confirmed call.
538    pub direction: String,
539    /// BFS traversal depth (0 = direct caller/callee, 1 = one hop away, etc.).
540    pub depth: usize,
541    /// Confidence level of this reference.
542    ///
543    /// - `"lsp"` — confirmed by LSP call hierarchy (authoritative).
544    /// - `"heuristic"` — inferred by grep or AST fallback when LSP is unavailable or degraded.
545    ///   Treat as a candidate; may include false positives from dynamic dispatch or
546    ///   same-named symbols in different scopes.
547    ///
548    /// `null` (absent) when the confidence is unknown or the caller pre-dates this field.
549    #[serde(skip_serializing_if = "Option::is_none", default)]
550    pub confidence: Option<String>,
551}
552
553/// The metadata embedded in `structured_content` for `find_callers_callees`.
554#[derive(Debug, Default, Serialize, serde::Deserialize, schemars::JsonSchema)]
555pub struct FindCallersCalleesMetadata {
556    /// Symbols that call the target (caller graph).
557    /// `null` when `incoming_verified` is `Some(false)` AND the result is
558    /// genuinely unknown (LSP unavailable, no heuristic fallback matched) —
559    /// callers are **unknown**, do NOT treat as zero.
560    /// An empty array `[]` means callers were verified (either by LSP, or by
561    /// heuristic grep fallback) and the list is complete.
562    /// Check `incoming_verified` to disambiguate "UNKNOWN (null)" from
563    /// "verified empty (Some([]))" — null is a footgun when agents coalesce
564    /// it to an empty array.
565    pub incoming: Option<Vec<ImpactReference>>,
566    /// Symbols the target calls (callee graph).
567    /// Same semantics as `incoming` but for the callee list.
568    /// Check `outgoing_verified` to disambiguate `null` (UNKNOWN) from `[]`
569    /// (verified zero).
570    pub outgoing: Option<Vec<ImpactReference>>,
571    /// Whether `incoming` was verified by LSP (vs unknown due to degradation).
572    ///
573    /// - `Some(true)`: callers list is verified (either LSP-confirmed, possibly
574    ///   empty, or heuristic grep results that successfully completed).
575    ///   `incoming` will be `Some(vec)` — agents may use `incoming.len()` to
576    ///   count, but treat the list as a candidate set when the global
577    ///   `degraded` flag is `true` (heuristic, may include false positives).
578    /// - `Some(false)`: callers are UNKNOWN. `incoming` is `null`.
579    ///   Do NOT treat as zero. Use `search` to verify manually.
580    /// - `None`: field not applicable (e.g., scope did not request callers).
581    #[serde(skip_serializing_if = "Option::is_none")]
582    pub incoming_verified: Option<bool>,
583    /// Whether `outgoing` was verified by LSP (vs unknown due to degradation).
584    ///
585    /// Same semantics as `incoming_verified` but for the callee list.
586    #[serde(skip_serializing_if = "Option::is_none")]
587    pub outgoing_verified: Option<bool>,
588    /// Number of transitive levels traversed.
589    pub depth_reached: u32,
590    /// Total files referenced across all incoming and outgoing references.
591    pub files_referenced: usize,
592    /// Whether the call hierarchy analysis was degraded (LSP unavailable or crashed).
593    /// Always present. When `true`, `incoming` and `outgoing` may be `null` (unknown)
594    /// or contain heuristic grep-based results. Check `resolution_strategy` and
595    /// `confidence` on each reference to assess reliability.
596    pub degraded: bool,
597    /// Machine-readable reason for degradation (e.g., `no_lsp`, `lsp_crash`, `lsp_timeout`).
598    /// Absent when `degraded` is `false`.
599    #[serde(skip_serializing_if = "Option::is_none")]
600    pub degraded_reason: Option<DegradedReason>,
601    /// Machine-readable guidance when `degraded` is `true`.
602    #[serde(skip_serializing_if = "Option::is_none")]
603    pub actionable_guidance: Option<ActionableGuidance>,
604    /// LSP readiness at the time of the call.
605    ///
606    /// - `"ready"`: LSP is fully operational — results are authoritative.
607    /// - `"warming_up"`: LSP is still indexing — results may be partial.
608    /// - `"unavailable"`: No LSP; results degraded.
609    #[serde(skip_serializing_if = "Option::is_none")]
610    pub lsp_readiness: Option<String>,
611    /// Whether the LSP warm-start is still in progress at the time of the call.
612    #[serde(skip_serializing_if = "Option::is_none")]
613    pub warm_start_in_progress: Option<bool>,
614    /// `true` when the `max_references` limit was reached and results were truncated.
615    pub references_truncated: bool,
616    /// Spec 5.1: Wall-clock duration of the tool call in milliseconds.
617    #[serde(skip_serializing_if = "Option::is_none")]
618    pub duration_ms: Option<u64>,
619    /// Spec 5.2: How the call hierarchy was resolved.
620    /// One of: `lsp_call_hierarchy`, `lsp_call_hierarchy_with_impl_expansion`,
621    /// `grep_file_scoped`, `treesitter_direct`, `treesitter_fallback`.
622    /// Note: `lsp_call_hierarchy_with_impl_expansion` indicates the target was a
623    /// trait/interface method; callers were merged across all concrete
624    /// implementations found via `goto_implementation`.
625    /// Note: `grep_file_scoped` covers all LSP-unavailable grep fallback paths; the finer-grained
626    /// `grep_impl_scoped`/`grep_global`/`grep_broad` values are NOT emitted for this tool.
627    #[serde(skip_serializing_if = "Option::is_none")]
628    pub resolution_strategy: Option<String>,
629    /// Spec 4.2: Test functions that reference or test this symbol.
630    /// Populated when `include_test_coverage=true` was passed.
631    /// Absent (omitted from JSON) when unavailable/not requested.
632    /// Present as `[]` when requested and confirmed to have zero test callers.
633    #[serde(skip_serializing_if = "Option::is_none")]
634    pub test_callers: Option<Vec<ImpactReference>>,
635    /// Spec 4.2: Status of test coverage search.
636    /// One of: `"found"`, `"not_found"`, `"unknown_degraded"`.
637    #[serde(skip_serializing_if = "Option::is_none")]
638    pub test_coverage_status: Option<String>,
639    /// P2-7: Actionable hint when zero callers/callees are found and the result
640    /// is not degraded. Helps agents distinguish "genuinely unused" from common
641    /// false-negative scenarios.
642    #[serde(skip_serializing_if = "Option::is_none")]
643    pub hint: Option<String>,
644}
645
646// ── Get Semantic Path Tool Types ────────────────────────────────────────
647
648/// Result for `get_semantic_path`.
649#[derive(Debug, serde::Serialize, serde::Deserialize, schemars::JsonSchema)]
650pub struct GetSemanticPathResult {
651    /// The full semantic path (`file::symbol`) of the innermost enclosing symbol.
652    ///
653    /// Absent (omitted from JSON) when the line is not inside any named symbol
654    /// (e.g., it is a module-level attribute, blank line, or the file uses an
655    /// unsupported language).
656    #[serde(skip_serializing_if = "Option::is_none")]
657    pub semantic_path: Option<String>,
658    /// The symbol portion only (without the file prefix).
659    ///
660    /// Absent (omitted from JSON) when `semantic_path` is absent.
661    #[serde(skip_serializing_if = "Option::is_none")]
662    pub symbol: Option<String>,
663    /// The file portion of the semantic path (same as the `file` parameter).
664    pub file: String,
665    /// The queried line number (1-indexed, echoed back for confirmation).
666    pub line: u32,
667}
668
669// ── Find All References Tool Types ─────────────────────────────────────
670
671/// The metadata embedded in `structured_content` for `find_all_references`.
672#[derive(Debug, Default, Serialize, serde::Deserialize, schemars::JsonSchema)]
673pub struct FindAllReferencesMetadata {
674    /// All reference locations for the target symbol.
675    /// Empty array `[]` means LSP confirmed zero references.
676    /// `null` when `degraded` is `true` — LSP was unavailable.
677    pub references: Option<Vec<ReferenceLocation>>,
678    /// Total number of references found (before pagination).
679    #[serde(skip_serializing_if = "Option::is_none")]
680    pub total_references: Option<usize>,
681    /// Whether the results were truncated due to `max_results`.
682    #[serde(skip_serializing_if = "std::ops::Not::not", default)]
683    pub truncated: bool,
684    /// Total number of files containing references.
685    pub files_referenced: usize,
686    /// Whether the reference lookup was degraded (LSP unavailable or crashed).
687    pub degraded: bool,
688    /// Machine-readable reason for degradation (e.g., `no_lsp`, `lsp_crash`, `lsp_timeout`).
689    #[serde(skip_serializing_if = "Option::is_none")]
690    pub degraded_reason: Option<DegradedReason>,
691    /// Machine-readable guidance when `degraded` is `true`.
692    #[serde(skip_serializing_if = "Option::is_none")]
693    pub actionable_guidance: Option<ActionableGuidance>,
694    /// LSP readiness at the time of the call.
695    ///
696    /// - `"ready"`: LSP is fully operational — results are authoritative.
697    /// - `"warming_up"`: LSP is still indexing — results may be partial.
698    /// - `"unavailable"`: No LSP; results degraded.
699    #[serde(skip_serializing_if = "Option::is_none")]
700    pub lsp_readiness: Option<String>,
701    /// Whether the LSP warm-start is still in progress at the time of the call.
702    #[serde(skip_serializing_if = "Option::is_none")]
703    pub warm_start_in_progress: Option<bool>,
704    /// Wall-clock time in milliseconds that this tool call took to complete.
705    #[serde(skip_serializing_if = "Option::is_none")]
706    pub duration_ms: Option<u64>,
707    /// Spec 5.2: How the references were resolved.
708    /// One of: `lsp_references`, `grep_file_scoped`, `grep_impl_scoped`, `grep_global`, `grep_broad`, `treesitter_fallback`.
709    #[serde(skip_serializing_if = "Option::is_none")]
710    pub resolution_strategy: Option<String>,
711    /// P2-7: Actionable hint when zero references are found and the result
712    /// is not degraded. Helps agents distinguish "genuinely unused" from common
713    /// false-negative scenarios like reflection or dynamic dispatch.
714    #[serde(skip_serializing_if = "Option::is_none")]
715    pub hint: Option<String>,
716}
717
718/// A single reference location for `find_all_references`.
719#[derive(Debug, Clone, Serialize, Deserialize, schemars::JsonSchema)]
720pub struct ReferenceLocation {
721    /// File path relative to workspace root.
722    pub file: String,
723    /// 1-indexed line number where the reference occurs.
724    pub line: u32,
725    /// 1-indexed column number where the reference occurs.
726    pub column: u32,
727    /// A short code snippet showing the reference (e.g., function call or variable access).
728    pub snippet: String,
729}
730
731// ── LSP Health Tool Types ────────────────────────────────────────────
732
733/// Structured information about a degraded tool.
734///
735/// Provides detailed information about which tools are degraded and what
736/// fallback behavior agents can expect. Replaces the old `Vec<String>`
737/// format with machine-readable severity and human-readable descriptions.
738#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, schemars::JsonSchema)]
739pub struct DegradedToolInfo {
740    /// Tool name (e.g., `"trace"`, `"locate"`, `"inspect"`).
741    pub tool: String,
742    /// Severity of degradation:
743    ///
744    /// - `"unavailable"` — tool will error, use alternatives
745    /// - `"grep_fallback"` — tool returns heuristic results, verify manually
746    /// - `"warmup_pending"` — retry after indexing completes
747    /// - `"partial"` — some features work (e.g., definition works but not call hierarchy)
748    pub severity: String,
749    /// Human-readable description of the fallback behavior and limitations.
750    pub description: String,
751}
752
753/// The response for `lsp_health`.
754#[derive(Debug, Serialize, Deserialize, schemars::JsonSchema)]
755pub struct LspHealthResponse {
756    /// Overall LSP readiness: `"ready"`, `"warming_up"`, or `"unavailable"`.
757    pub status: String,
758    /// Per-language status details.
759    pub languages: Vec<LspLanguageHealth>,
760    /// PATCH-004: Whether `warm_start` has completed.
761    ///
762    /// When `true`, all `warm_start` background tasks have finished (even if
763    /// some languages failed). When `false`, `warm_start` is still in progress.
764    /// This allows distinguishing "still warming up" from "`warm_start` finished
765    /// but LSP didn't report readiness".
766    pub warm_start_complete: bool,
767    /// P2-6: Whether ALL detected languages have finished background indexing.
768    ///
769    /// `true` when every language reports `indexing_status == "complete"` or has
770    /// no indexing information (language unavailable). `false` when any language
771    /// is still indexing. Agents can poll this single field instead of iterating
772    /// the `languages` array.
773    pub indexing_complete: bool,
774    /// Spec 1.3: Known limitations of the current LSP setup.
775    ///
776    /// Populated with actionable limitations that agents should be aware of,
777    /// such as missing capabilities or languages that require manual setup.
778    #[serde(skip_serializing_if = "Vec::is_empty", default)]
779    pub known_limitations: Vec<String>,
780}
781
782/// Per-language LSP health status.
783#[derive(Debug, Serialize, Deserialize, schemars::JsonSchema)]
784pub struct LspLanguageHealth {
785    /// Language ID (e.g., "rust", "typescript").
786    pub language: String,
787    /// Status of the language server process.
788    ///
789    /// Lifecycle: `unavailable` → `starting` → `warming_up` → `ready`
790    /// A `ready` LSP may be downgraded to `degraded` if a live probe fails.
791    ///
792    /// - `"unavailable"`: No LSP process running or detected
793    /// - `"starting"`: Process exists, no capability info yet (lazy start)
794    /// - `"warming_up"`: Process running, `navigation_ready` not yet confirmed
795    /// - `"ready"`: Initialize handshake complete, `navigation_ready=true`,
796    ///   and live probe succeeded (`navigation_verified=Some(true)`)
797    /// - `"degraded"`: Initialize handshake completed (`navigation_ready=true`)
798    ///   but live probe failed (`navigation_verified=Some(false)`).
799    ///   Navigation MAY still work — retry or use with caution.
800    pub status: String,
801    /// Time since LSP process started, formatted as a human-readable string (e.g., "45s").
802    #[serde(skip_serializing_if = "Option::is_none")]
803    pub uptime: Option<String>,
804    /// How diagnostics work for this language.
805    #[serde(skip_serializing_if = "Option::is_none")]
806    pub diagnostics_strategy: Option<String>,
807    /// Whether call hierarchy is supported (affects `trace`, `inspect` with dependencies).
808    #[serde(skip_serializing_if = "Option::is_none")]
809    pub supports_call_hierarchy: Option<bool>,
810    /// Whether diagnostics are supported (affects LSP health quality).
811    #[serde(skip_serializing_if = "Option::is_none")]
812    pub supports_diagnostics: Option<bool>,
813    /// Whether definition is supported (affects `locate`).
814    #[serde(skip_serializing_if = "Option::is_none")]
815    pub supports_definition: Option<bool>,
816    /// Background indexing status: `"complete"`, `"in_progress"`, or None.
817    ///
818    /// Independent of overall status — an LSP can be "ready" for navigation
819    /// while still indexing in the background.
820    #[serde(skip_serializing_if = "Option::is_none")]
821    pub indexing_status: Option<String>,
822    #[serde(skip_serializing_if = "Option::is_none")]
823    pub indexing_source: Option<String>,
824    #[serde(skip_serializing_if = "Option::is_none")]
825    pub indexing_duration_secs: Option<u64>,
826    /// Whether the LSP advertised navigation capabilities during initialize.
827    ///
828    /// This reflects capability negotiation only — it does NOT guarantee
829    /// navigation actually works. For live verification, check `navigation_verified`.
830    ///
831    /// `true` once the LSP initialize handshake completes with `definitionProvider: true`.
832    /// Independent of `indexing_status` — navigation works during indexing but
833    /// results may be partial until indexing completes.
834    #[serde(skip_serializing_if = "Option::is_none")]
835    pub navigation_ready: Option<bool>,
836    /// Whether navigation was verified by a live probe (not just capability
837    /// advertisement). Distinct from `navigation_ready` which reflects
838    /// capability negotiation only.
839    ///
840    /// - `Some(true)`: live probe succeeded — navigation is operational
841    /// - `Some(false)`: live probe failed — navigation may be broken despite
842    ///   `navigation_ready: true`
843    /// - `None`: probe not yet run (freshness unknown)
844    #[serde(skip_serializing_if = "Option::is_none")]
845    pub navigation_verified: Option<bool>,
846    /// Whether the status was verified by a live probe (rather than just progress notifications).
847    /// When true, the agent can trust the status.
848    #[serde(skip_serializing_if = "crate::server::types::is_false", default)]
849    pub probe_verified: bool,
850    /// Whether navigation (`locate`, `trace`) was confirmed by a live probe.
851    ///
852    /// `true` only when a live `goto_definition` probe request succeeded — meaning the LSP
853    /// returned a real location, not just that it advertised the capability in the initialize
854    /// handshake. Stronger signal than `navigation_ready` alone.
855    ///
856    /// Note: This is an independent signal from `status: "ready"`. An LSP can be in the `"ready"` status
857    /// (handshake completed) even if a live probe has not yet been executed or has failed (in which case
858    /// `navigation_tested` will be `None` or `Some(false)` respectively).
859    ///
860    /// Agents should prefer this over `probe_verified` — it has the same meaning but
861    /// communicates intent more clearly.
862    #[serde(skip_serializing_if = "Option::is_none", default)]
863    pub navigation_tested: Option<bool>,
864    /// Whether the call hierarchy capability was verified by a live probe.
865    #[serde(skip_serializing_if = "crate::server::types::is_false", default)]
866    pub call_hierarchy_verified: bool,
867    /// Install guidance when LSP is unavailable.
868    ///
869    /// Provides actionable commands users can run to install their LSP servers.
870    /// `None` when LSP is running or language not detected at all.
871    #[serde(skip_serializing_if = "Option::is_none", default)]
872    pub install_hint: Option<String>,
873    /// The LSP server identity (e.g., "rust-analyzer", "Pyright", "gopls").
874    /// Useful for distinguishing which Python LSP is running.
875    /// `None` when the process is not running or server omitted serverInfo.
876    #[serde(skip_serializing_if = "Option::is_none", default)]
877    pub server_name: Option<String>,
878    /// Number of dynamic capability registrations received from the LSP server.
879    /// Useful for diagnosing dynamic registration delays (e.g., jdtls).
880    #[serde(skip_serializing_if = "Option::is_none", default)]
881    pub registrations_received: Option<u32>,
882    /// Indexing progress percentage (0-100) if the LSP reports it via workDoneProgress.
883    /// `None` when the LSP does not report progress or indexing is complete.
884    #[serde(skip_serializing_if = "Option::is_none")]
885    pub indexing_progress_percent: Option<u8>,
886    /// Tools that are degraded (using fallback) for this language.
887    ///
888    /// Empty when LSP is fully operational. Lists which tools lose LSP support
889    /// with detailed severity and description for each.
890    #[serde(skip_serializing_if = "Vec::is_empty", default)]
891    pub degraded_tools: Vec<DegradedToolInfo>,
892    /// Seconds since last liveness probe for this language.
893    #[serde(skip_serializing_if = "Option::is_none")]
894    pub last_probe_age_secs: Option<u32>,
895}
896
897/// A single symbol found by `find_symbol`.
898#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, schemars::JsonSchema)]
899pub struct FoundSymbol {
900    /// Full semantic path (<file::symbol>).
901    pub semantic_path: String,
902    /// Symbol kind (e.g., "class", "function", "struct", "interface", "enum").
903    pub kind: String,
904    /// File path relative to workspace root.
905    pub file: String,
906    /// 1-indexed line where the symbol is defined.
907    pub line: u64,
908    /// First 100 characters of the definition line.
909    pub preview: String,
910}
911
912/// Response from `find_symbol`.
913#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, schemars::JsonSchema)]
914pub struct FindSymbolResponse {
915    /// Matching symbols found.
916    pub symbols: Vec<FoundSymbol>,
917    /// Total number of matches found (before truncation).
918    pub total_found: u32,
919    /// Search strategy used: "ripgrep+treesitter", "ripgrep+fallback".
920    pub search_strategy: String,
921    /// Time taken in milliseconds.
922    #[serde(skip_serializing_if = "Option::is_none")]
923    pub duration_ms: Option<u64>,
924    /// Suggested alternative symbol paths when `symbols` is empty.
925    ///
926    /// Populated using fuzzy matching and cross-file search when the
927    /// query matches no exact symbol. Each entry is a full semantic path
928    /// (`file::symbol`) that can be used directly with `inspect` or
929    /// `trace`.
930    ///
931    /// Absent when `symbols` is non-empty (exact match found).
932    /// Absent when no suggestions could be found.
933    #[serde(skip_serializing_if = "Option::is_none")]
934    pub did_you_mean: Option<Vec<String>>,
935    /// Human-readable hint when no exact match found but suggestions exist.
936    #[serde(skip_serializing_if = "Option::is_none")]
937    pub hint: Option<String>,
938}
939
940/// Result for a single file in `read_files`.
941#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, schemars::JsonSchema)]
942pub struct FileResult {
943    /// File path.
944    pub path: String,
945    /// File content (None if error).
946    #[serde(skip_serializing_if = "Option::is_none")]
947    pub content: Option<String>,
948    /// Language of the file (e.g., "rust", "typescript", "toml").
949    #[serde(skip_serializing_if = "Option::is_none")]
950    pub language: Option<String>,
951    /// Total number of lines in the file (before truncation).
952    #[serde(skip_serializing_if = "Option::is_none")]
953    pub total_lines: Option<u32>,
954    /// Short content fingerprint of the file (7-char hex, derived from SHA-256).
955    ///
956    /// Use for change detection: if the hash differs from a previous call, the file
957    /// has been modified. Not a full SHA-256 digest — compare as opaque strings only.
958    #[serde(skip_serializing_if = "Option::is_none")]
959    pub version_hash: Option<String>,
960    /// Error message if file could not be read (e.g., "file not found", "sandbox denied").
961    #[serde(skip_serializing_if = "Option::is_none")]
962    pub error: Option<String>,
963}
964
965/// Response from `read_files`.
966#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, schemars::JsonSchema)]
967pub struct ReadFilesResponse {
968    /// Results for each requested file.
969    pub files: Vec<FileResult>,
970    /// Number of files successfully read.
971    pub succeeded: u32,
972    /// Number of files that failed to read.
973    pub failed: u32,
974    /// Time taken in milliseconds.
975    #[serde(skip_serializing_if = "Option::is_none")]
976    pub duration_ms: Option<u64>,
977}
978
979/// Helper to skip serializing false values for `probe_verified`.
980#[allow(clippy::trivially_copy_pass_by_ref)] // Required by serde's skip_serializing_if signature
981pub(crate) fn is_false(b: &bool) -> bool {
982    !b
983}
984
985// ── Consolidated Tool Parameter Types ───────────────────────────────
986//
987// These param structs are used by the 7 consolidated tool handlers
988// registered in the `#[tool_router]` impl block in `server.rs`.
989
990/// Detail level for the `explore` tool.
991#[derive(Debug, Clone, Default, serde::Deserialize, schemars::JsonSchema)]
992#[serde(rename_all = "snake_case")]
993pub enum Detail {
994    /// Directory tree + package manager files only.
995    Structure,
996    /// Directory tree + all filenames (no symbols).
997    Files,
998    /// Full AST symbol hierarchy (current behavior).
999    #[default]
1000    Symbols,
1001}
1002
1003/// Parameters for the `explore` tool.
1004#[derive(Debug, Default, serde::Deserialize, schemars::JsonSchema)]
1005pub struct ExploreParams {
1006    /// Directory to map.
1007    #[serde(default = "default_repo_map_path")]
1008    pub path: String,
1009    /// Level of detail to include in the output.
1010    #[serde(default)]
1011    pub detail: Detail,
1012    /// Total token budget for the entire skeleton output. Default: 16000.
1013    ///
1014    /// When `coverage_percent` in the response is low, increase this value
1015    /// to include more files in the map.
1016    #[serde(default = "default_max_tokens")]
1017    pub max_tokens: u32,
1018    /// Max directory traversal depth (default: 3).
1019    ///
1020    /// Increase when `coverage_percent` is low or the project has deeply-nested
1021    /// source files. The walker stops early on shallow repos, so over-provisioning
1022    /// is safe and nearly free.
1023    #[serde(default = "default_explore_depth")]
1024    pub depth: u32,
1025    /// Visibility filter: `public` or `all`.
1026    #[serde(default)]
1027    pub visibility: pathfinder_common::types::Visibility,
1028    /// Per-file token cap before a file skeleton is collapsed to a summary stub.
1029    ///
1030    /// When the rendered skeleton of an individual file exceeds this limit, the
1031    /// file is replaced with a truncated stub showing only class/struct names and
1032    /// method counts. Default: 2000.
1033    #[serde(default = "default_max_tokens_per_file")]
1034    pub max_tokens_per_file: u32,
1035    /// Git ref or duration to show only recently modified files (e.g., `HEAD~5`, `3h`, `2024-01-01`).
1036    ///
1037    /// Defaults to an empty string, which disables the git filter and maps the entire repository.
1038    #[serde(default)]
1039    pub changed_since: String,
1040    /// Only include files with these extensions. Mutually exclusive with `exclude_extensions`.
1041    ///
1042    /// Format: raw extension string without a leading dot or wildcard (e.g. `"rs"` or `"ts"`,
1043    /// NOT `".rs"` or `"*.rs"`).
1044    ///
1045    /// If both `include_extensions` and `exclude_extensions` are non-empty, the tool returns
1046    /// an invalid params error.
1047    #[serde(default)]
1048    pub include_extensions: Vec<String>,
1049    /// Exclude files with these extensions. Mutually exclusive with `include_extensions`.
1050    ///
1051    /// Format: raw extension string without a leading dot or wildcard (e.g. `"rs"` or `"ts"`,
1052    /// NOT `".rs"` or `"*.rs"`).
1053    ///
1054    /// If both `include_extensions` and `exclude_extensions` are non-empty, the tool returns
1055    /// an invalid params error.
1056    #[serde(default)]
1057    pub exclude_extensions: Vec<String>,
1058}
1059
1060/// Search mode for the `search` tool.
1061#[derive(Debug, Clone, Default, serde::Deserialize, schemars::JsonSchema)]
1062#[serde(rename_all = "snake_case")]
1063pub enum SearchMode {
1064    /// Text search (default).
1065    #[default]
1066    Text,
1067    /// Symbol name resolution.
1068    Symbol,
1069    /// Regex search.
1070    Regex,
1071}
1072
1073/// Glob pattern representation that accepts either a single string or an array of strings.
1074#[derive(
1075    Debug, Clone, serde::Serialize, serde::Deserialize, schemars::JsonSchema, PartialEq, Eq,
1076)]
1077#[serde(untagged)]
1078pub enum ExcludeGlob {
1079    /// A single glob pattern.
1080    Single(String),
1081    /// Multiple glob patterns.
1082    Multiple(Vec<String>),
1083}
1084
1085impl Default for ExcludeGlob {
1086    fn default() -> Self {
1087        Self::Single(String::new())
1088    }
1089}
1090
1091impl std::fmt::Display for ExcludeGlob {
1092    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1093        match self {
1094            Self::Single(s) => write!(f, "{s}"),
1095            Self::Multiple(v) => write!(f, "[{}]", v.join(", ")),
1096        }
1097    }
1098}
1099
1100impl ExcludeGlob {
1101    /// Convert the glob pattern(s) to a list of string patterns.
1102    #[must_use]
1103    pub fn to_vec(&self) -> Vec<String> {
1104        match self {
1105            Self::Single(s) => {
1106                if s.is_empty() {
1107                    Vec::new()
1108                } else {
1109                    vec![s.clone()]
1110                }
1111            }
1112            Self::Multiple(v) => v.clone(),
1113        }
1114    }
1115}
1116
1117/// Parameters for the `search` tool.
1118#[derive(Debug, serde::Deserialize, schemars::JsonSchema)]
1119pub struct SearchParams {
1120    /// Search pattern (literal text, regex, or symbol name depending on `mode`).
1121    pub query: String,
1122    /// Search mode: `text` (default), `symbol`, or `regex`.
1123    #[serde(default)]
1124    pub mode: SearchMode,
1125    /// Limit search scope (e.g., `src/**/*.ts`).
1126    ///
1127    /// Supports brace expansion to search multiple patterns in one call:
1128    /// `{src/**/*.ts,tests/**/*.ts}` matches files in both directories.
1129    ///
1130    /// Brace expansion is handled by the `globset` crate and works out of the
1131    /// box — no extra configuration needed.
1132    #[serde(default = "default_path_glob")]
1133    pub path_glob: String,
1134    /// Maximum matches returned. Default: 50.
1135    ///
1136    /// Applies to all modes including `symbol`. When `mode="symbol"`, this caps
1137    /// the number of resolved symbol matches returned.
1138    #[serde(default = "default_max_results")]
1139    pub max_results: u32,
1140    /// Lines of context above/below each match (text/regex modes only).
1141    #[serde(default = "default_context_lines")]
1142    pub context_lines: u32,
1143    /// File paths already in the agent's context.
1144    ///
1145    /// For matches in these files, only minimal metadata is returned.
1146    /// Full content and context lines are omitted to save tokens.
1147    #[serde(default)]
1148    pub known_files: Vec<String>,
1149    /// Glob patterns for files to exclude from search. Accepts either a single
1150    /// string or an array of strings.
1151    ///
1152    /// Supports brace expansion to exclude multiple patterns in one call:
1153    /// `{**/*.test.*,**/*.spec.*}` excludes both test and spec files.
1154    /// `{**/*.test.ts,**/*.spec.ts,**/*.e2e.ts}` — three patterns, one string.
1155    ///
1156    /// Brace expansion is handled by the `globset` crate and works out of the
1157    /// box — no extra configuration needed.
1158    #[serde(default)]
1159    pub exclude_glob: ExcludeGlob,
1160    /// Number of matches to skip before returning results (for pagination).
1161    /// Use with `max_results` to page through large result sets.
1162    #[serde(default)]
1163    pub offset: u32,
1164    /// Optional filter by symbol kind. Only used when `mode` is `symbol`.
1165    ///
1166    /// **Canonical values:** `function`, `class`, `struct`, `interface`, `enum`,
1167    /// `constant`, `module`, `impl`, `type`.
1168    ///
1169    /// **Accepted aliases (all case-insensitive):**
1170    /// - `method`, `fn` → treated as `function`
1171    /// - `trait` → treated as `interface` (Rust traits, Go interfaces)
1172    /// - `const`, `static`, `let` → treated as `constant`
1173    /// - `mod`, `namespace` → treated as `module`
1174    /// - `type` matches class, struct, interface, trait, and enum (broadest type-level search).
1175    /// - `class` matches class, struct, and interface (broad OOP-style search, but NOT enums).
1176    /// - `struct` matches only struct.
1177    ///
1178    /// Example: `kind="trait"` finds Rust traits and Go interfaces.
1179    #[serde(default)]
1180    pub kind: Option<String>,
1181    /// Group matches by file path. Default: true.
1182    #[serde(default = "default_group_by_file")]
1183    pub group_by_file: bool,
1184    /// Filter results: `all`, `code_only`, `comments_only`, or `non_code` (matches in comments AND string literals (non-code content)). Default: `code_only`.
1185    #[serde(default = "default_filter_mode")]
1186    pub filter_mode: FilterMode,
1187}
1188
1189impl Default for SearchParams {
1190    fn default() -> Self {
1191        Self {
1192            query: String::new(),
1193            mode: SearchMode::Text,
1194            path_glob: default_path_glob(),
1195            max_results: default_max_results(),
1196            context_lines: default_context_lines(),
1197            known_files: Vec::new(),
1198            exclude_glob: ExcludeGlob::default(),
1199            offset: 0,
1200            kind: None,
1201            group_by_file: default_group_by_file(),
1202            filter_mode: default_filter_mode(),
1203        }
1204    }
1205}
1206
1207/// Parameters for the `read` tool.
1208///
1209/// Accepts either a single file via `filepath` or multiple files via `paths`.
1210/// Exactly one of the two must be provided; the handler validates this at runtime.
1211/// File type (source vs config) is auto-detected from the extension.
1212#[derive(Debug, Clone, serde::Deserialize, schemars::JsonSchema)]
1213pub struct ReadParams {
1214    /// Single file path. Use this for reading one file.
1215    /// Mutually exclusive with `paths`.
1216    #[serde(default, alias = "path")]
1217    pub filepath: Option<String>,
1218    /// Multiple file paths (max 10). Use this for batch reading.
1219    /// Mutually exclusive with `filepath`.
1220    #[serde(default)]
1221    pub paths: Option<Vec<String>>,
1222    /// Detail level for source files: `source_only`, `compact`, `symbols`, or `full`.
1223    /// Ignored for config/non-source files (always raw content).
1224    #[serde(default = "default_detail_level")]
1225    pub detail_level: String,
1226    /// First line to return (1-indexed). Single-file mode only.
1227    #[serde(default = "default_start_line")]
1228    pub start_line: u32,
1229    /// Last line to return (1-indexed, inclusive). Single-file mode only.
1230    #[serde(default)]
1231    pub end_line: Option<u32>,
1232    /// Maximum lines returned per file (defaults to 500). Applies to batch mode and config/raw files.
1233    #[serde(default = "default_max_lines")]
1234    pub max_lines_per_file: u32,
1235}
1236
1237impl Default for ReadParams {
1238    fn default() -> Self {
1239        Self {
1240            filepath: None,
1241            paths: None,
1242            detail_level: default_detail_level(),
1243            start_line: default_start_line(),
1244            end_line: None,
1245            max_lines_per_file: default_max_lines(),
1246        }
1247    }
1248}
1249
1250/// Parameters for the `inspect` tool.
1251#[derive(Debug, serde::Deserialize, schemars::JsonSchema)]
1252pub struct InspectParams {
1253    /// Semantic path (e.g., `src/auth.ts::AuthService.login`). MUST include file path and '::'.
1254    #[serde(default)]
1255    pub semantic_path: Option<String>,
1256    /// Multiple semantic paths (max 10) for batch inspection.
1257    #[serde(default, skip_serializing_if = "Option::is_none")]
1258    pub semantic_paths: Option<Vec<String>>,
1259    /// When `true`, also fetch callee signatures (dependency context).
1260    /// Default: `false` (equivalent to old `read_symbol_scope` behavior).
1261    #[serde(default)]
1262    pub include_dependencies: bool,
1263    /// Maximum number of dependencies (callee signatures) to return.
1264    /// Only used when `include_dependencies` is `true`. Default: 50.
1265    #[serde(default = "default_max_dependencies")]
1266    pub max_dependencies: u32,
1267    /// Include file-level import statements in the response.
1268    ///
1269    /// Useful for languages with verbose package paths (Java, C#, Kotlin)
1270    /// where imports clarify what types are in scope.
1271    /// Only used when `include_dependencies` is `true`. Default: `false`.
1272    #[serde(default)]
1273    pub include_imports: bool,
1274}
1275
1276impl Default for InspectParams {
1277    fn default() -> Self {
1278        Self {
1279            semantic_path: None,
1280            semantic_paths: None,
1281            include_dependencies: false,
1282            max_dependencies: default_max_dependencies(),
1283            include_imports: false,
1284        }
1285    }
1286}
1287
1288/// A single entry in a batch locate request.
1289#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, schemars::JsonSchema)]
1290pub struct LocateEntry {
1291    /// Semantic path to resolve (e.g., `src/auth.ts::AuthService.login`).
1292    #[serde(default, skip_serializing_if = "Option::is_none")]
1293    pub semantic_path: Option<String>,
1294    /// File path (e.g., `src/auth.ts`).
1295    #[serde(default, skip_serializing_if = "Option::is_none")]
1296    pub file: Option<String>,
1297    /// 1-indexed line number to resolve.
1298    #[serde(default, skip_serializing_if = "Option::is_none")]
1299    pub line: Option<u32>,
1300}
1301
1302/// Parameters for the `locate` tool.
1303///
1304/// Auto-detects mode from input:
1305/// - If `semantic_path` is provided → jump to definition.
1306/// - If `file` + `line` are provided → resolve to semantic path.
1307///
1308/// Exactly one mode must be specified; the handler validates this at runtime.
1309#[derive(Debug, Default, serde::Deserialize, schemars::JsonSchema)]
1310pub struct LocateParams {
1311    /// Semantic path to the reference (e.g., `src/auth.ts::AuthService.login`).
1312    /// Provide this to jump to a symbol's definition.
1313    #[serde(default)]
1314    pub semantic_path: Option<String>,
1315    /// Relative path to the file (e.g., `src/auth.ts`).
1316    /// Provide together with `line` to resolve a location to its semantic path.
1317    #[serde(default, alias = "path")]
1318    pub file: Option<String>,
1319    /// 1-indexed line number to resolve.
1320    /// Provide together with `file` to resolve a location to its semantic path.
1321    #[serde(default)]
1322    pub line: Option<u32>,
1323    /// Multiple locations (max 10) for batch location/resolution.
1324    #[serde(default, skip_serializing_if = "Option::is_none")]
1325    pub locations: Option<Vec<LocateEntry>>,
1326}
1327
1328/// Result entry for a single inspect operation in a batch.
1329#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, schemars::JsonSchema)]
1330pub struct InspectResultEntry {
1331    pub semantic_path: String,
1332    pub status: String, // "ok" or "error"
1333    #[serde(skip_serializing_if = "Option::is_none")]
1334    pub source: Option<String>,
1335    #[serde(skip_serializing_if = "Option::is_none")]
1336    pub start_line: Option<usize>,
1337    #[serde(skip_serializing_if = "Option::is_none")]
1338    pub end_line: Option<usize>,
1339    #[serde(skip_serializing_if = "Option::is_none")]
1340    pub language: Option<String>,
1341    #[serde(skip_serializing_if = "Option::is_none")]
1342    pub dependencies: Option<Vec<DeepContextDependency>>,
1343    #[serde(skip_serializing_if = "Option::is_none")]
1344    pub error: Option<String>,
1345}
1346
1347/// Response from a batch `inspect` tool call.
1348#[derive(Debug, serde::Serialize, serde::Deserialize, schemars::JsonSchema)]
1349pub struct BatchInspectResult {
1350    pub results: Vec<InspectResultEntry>,
1351    pub succeeded: usize,
1352    pub failed: usize,
1353    pub total_duration_ms: u64,
1354}
1355
1356/// Result entry for a single locate operation in a batch.
1357#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, schemars::JsonSchema)]
1358pub struct LocateResultEntry {
1359    pub input: LocateEntry, // echo back for correlation
1360    pub status: String,     // "ok" or "error"
1361    #[serde(skip_serializing_if = "Option::is_none")]
1362    pub file: Option<String>,
1363    #[serde(skip_serializing_if = "Option::is_none")]
1364    pub line: Option<u32>,
1365    #[serde(skip_serializing_if = "Option::is_none")]
1366    pub column: Option<u32>,
1367    #[serde(skip_serializing_if = "Option::is_none")]
1368    pub semantic_path: Option<String>,
1369    #[serde(skip_serializing_if = "Option::is_none")]
1370    pub preview: Option<String>,
1371    #[serde(skip_serializing_if = "Option::is_none")]
1372    pub resolution_strategy: Option<String>,
1373    #[serde(skip_serializing_if = "Option::is_none")]
1374    pub error: Option<String>,
1375}
1376
1377/// Response from a batch `locate` tool call.
1378#[derive(Debug, serde::Serialize, serde::Deserialize, schemars::JsonSchema)]
1379pub struct BatchLocateResult {
1380    pub results: Vec<LocateResultEntry>,
1381    pub succeeded: usize,
1382    pub failed: usize,
1383    pub total_duration_ms: u64,
1384}
1385
1386/// Scope for the `trace` tool.
1387#[derive(Debug, Clone, Default, serde::Deserialize, schemars::JsonSchema)]
1388#[serde(rename_all = "snake_case")]
1389pub enum TraceScope {
1390    /// Call hierarchy — callers and callees (default).
1391    #[default]
1392    Callers,
1393    /// All references (calls, imports, type annotations, etc.).
1394    References,
1395    /// Combined overview: source + callers + callees + references.
1396    Overview,
1397}
1398
1399/// Parameters for the `trace` tool.
1400#[derive(Debug, serde::Deserialize, schemars::JsonSchema)]
1401pub struct TraceParams {
1402    /// Semantic path to the target symbol (e.g., `src/mod.rs::func`). MUST include file path and '::'.
1403    pub semantic_path: String,
1404    /// What to trace: `callers` (default), `references`, or `overview`.
1405    #[serde(default)]
1406    pub scope: TraceScope,
1407    /// Traversal depth for call hierarchy (max: 5). Only used with `scope=callers`.
1408    ///
1409    /// Each depth level requires additional LSP round-trips, so deeper traversal is slower.
1410    /// Use `max_depth=1` for a fast, shallow result (direct callers/callees only).
1411    /// Use `max_depth=2` or `max_depth=3` for moderate transitive analysis.
1412    /// Reserve `max_depth=4` or `max_depth=5` for large-scale architectural analysis.
1413    #[serde(default = "default_max_depth")]
1414    pub max_depth: u32,
1415    /// Maximum total references to return (clamped to 1..=500).
1416    /// Prevents context overflow on large codebases. Default: 50.
1417    ///
1418    /// In `overview` scope, this single parameter controls both the
1419    /// callers/callees cap and the references cap (both set to this value).
1420    #[serde(default = "default_max_references")]
1421    pub max_references: u32,
1422    /// Number of results to skip before returning (for pagination).
1423    #[serde(default)]
1424    pub offset: u32,
1425}
1426
1427impl Default for TraceParams {
1428    fn default() -> Self {
1429        Self {
1430            semantic_path: String::default(),
1431            scope: TraceScope::default(),
1432            max_depth: default_max_depth(),
1433            max_references: default_max_references(),
1434            offset: 0,
1435        }
1436    }
1437}
1438
1439/// Parameters for the `health` tool.
1440#[derive(Debug, Default, serde::Deserialize, schemars::JsonSchema)]
1441pub struct HealthParams {
1442    /// Optional language to check (e.g., "rust", "typescript").
1443    /// If omitted, checks all available languages.
1444    #[serde(default)]
1445    pub language: Option<String>,
1446    /// Optional action to perform.
1447    ///
1448    /// - `"restart"`: Force-restart the LSP process for the specified language.
1449    ///   `language` must be set when using `"restart"`.
1450    ///   Returns updated health status after the restart attempt.
1451    #[serde(default)]
1452    pub action: Option<String>,
1453    /// Optional parameter to force a live liveness probe regardless of cache age.
1454    #[serde(default)]
1455    pub force_probe: Option<bool>,
1456}
1457
1458// ── Default Value Functions ─────────────────────────────────────────
1459
1460#[must_use]
1461pub fn default_path_glob() -> String {
1462    "**/*".to_owned()
1463}
1464#[must_use]
1465pub const fn default_max_results() -> u32 {
1466    50
1467}
1468#[must_use]
1469pub const fn default_context_lines() -> u32 {
1470    2
1471}
1472#[must_use]
1473pub fn default_repo_map_path() -> String {
1474    ".".to_owned()
1475}
1476#[must_use]
1477pub const fn default_max_tokens() -> u32 {
1478    16_000
1479}
1480#[must_use]
1481pub const fn default_max_tokens_per_file() -> u32 {
1482    2_000
1483}
1484#[must_use]
1485pub const fn default_max_depth() -> u32 {
1486    3
1487}
1488#[must_use]
1489pub const fn default_max_references() -> u32 {
1490    50
1491}
1492#[must_use]
1493pub const fn default_max_dependencies() -> u32 {
1494    50
1495}
1496#[must_use]
1497pub const fn default_start_line() -> u32 {
1498    1
1499}
1500#[must_use]
1501pub const fn default_max_lines() -> u32 {
1502    500
1503}
1504#[must_use]
1505pub fn default_detail_level() -> String {
1506    "compact".to_string()
1507}
1508#[must_use]
1509pub const fn default_explore_depth() -> u32 {
1510    3
1511}
1512#[must_use]
1513pub const fn default_group_by_file() -> bool {
1514    true
1515}
1516#[must_use]
1517pub const fn default_filter_mode() -> FilterMode {
1518    FilterMode::CodeOnly
1519}
1520
1521#[cfg(test)]
1522#[path = "types_test.rs"]
1523mod tests;