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 rmcp::schemars;
11use rmcp::serde::{self, Deserialize, Serialize};
12
13// ── Tool Parameter Types ────────────────────────────────────────────
14
15/// Parameters for `search_codebase`.
16#[derive(Debug, Default, serde::Deserialize, schemars::JsonSchema)]
17pub struct SearchCodebaseParams {
18    /// Search pattern (literal or regex).
19    pub query: String,
20    /// Treat query as regex.
21    #[serde(default)]
22    pub is_regex: bool,
23    /// Limit search scope (e.g., `src/**/*.ts`).
24    #[serde(default = "default_path_glob")]
25    pub path_glob: String,
26    /// Filter mode: `code_only`, `comments_only`, or `all`.
27    ///
28    /// Uses Tree-sitter node classification to filter matches by context.
29    /// Defaults to `code_only` (exclude comments and string literals).
30    #[serde(default)]
31    pub filter_mode: pathfinder_common::types::FilterMode,
32    /// Maximum matches returned.
33    #[serde(default = "default_max_results")]
34    pub max_results: u32,
35    /// Lines of context above/below each match.
36    #[serde(default = "default_context_lines")]
37    pub context_lines: u32,
38    /// File paths already in the agent's context.
39    ///
40    /// For matches in these files, only minimal metadata is returned
41    /// (`file`, `line`, `column`, `enclosing_semantic_path`, `version_hash`).
42    /// The full `content` and context lines are omitted to save tokens.
43    #[serde(default)]
44    pub known_files: Vec<String>,
45    /// Group matches by file in the response.
46    ///
47    /// When `true`, the response includes `file_groups` instead of (or in addition to)
48    /// the flat `matches` list. Each group contains all matches for one file with a
49    /// single `version_hash` at group level.
50    #[serde(default)]
51    pub group_by_file: bool,
52    /// Glob pattern for files to exclude from search (e.g., `**/*.test.*`).
53    ///
54    /// Applied before search — not as a post-filter — so excluded files are
55    /// never read. Can be combined with `path_glob` include patterns.
56    #[serde(default)]
57    pub exclude_glob: String,
58    /// Number of matches to skip before returning results (for pagination).
59    /// Use with `max_results` to page through large result sets.
60    #[serde(default)]
61    pub offset: u32,
62}
63
64/// Parameters for `get_repo_map`.
65#[derive(Debug, Default, serde::Deserialize, schemars::JsonSchema)]
66pub struct GetRepoMapParams {
67    /// Directory to map.
68    #[serde(default = "default_repo_map_path")]
69    pub path: String,
70    /// Total token budget for the entire skeleton output. Default: 16000.
71    ///
72    /// When `coverage_percent` in the response is low, increase this value
73    /// to include more files in the map.
74    #[serde(default = "default_max_tokens")]
75    pub max_tokens: u32,
76    /// Max directory traversal depth (default: 5).
77    ///
78    /// Increase this value when `coverage_percent` in the response is low
79    /// or when your project has deeply-nested source files (e.g. a depth 6+
80    /// monorepo). The walker stops early on shallow repos, so over-provisioning
81    /// is safe and nearly free.
82    #[serde(default = "default_depth")]
83    pub depth: u32,
84    /// Visibility filter: `public` or `all`.
85    #[serde(default)]
86    pub visibility: pathfinder_common::types::Visibility,
87    /// Import inclusion: `none`, `third_party`, or `all`.
88    #[serde(default)]
89    pub include_imports: pathfinder_common::types::IncludeImports,
90    /// Per-file token cap before a file skeleton is collapsed to a summary stub.
91    ///
92    /// When the rendered skeleton of an individual file exceeds this limit, the
93    /// file is replaced with a truncated stub showing only class/struct names and
94    /// method counts. Increase this value when files show `[TRUNCATED DUE TO SIZE]`
95    /// in the output. Default: 2000.
96    #[serde(default = "default_max_tokens_per_file")]
97    pub max_tokens_per_file: u32,
98    /// Git ref or duration to show only recently modified files (e.g., `HEAD~5`, `3h`, `2024-01-01`).
99    #[serde(default)]
100    pub changed_since: String,
101    /// Only include files with these extensions. Mutually exclusive with `exclude_extensions`.
102    #[serde(default)]
103    pub include_extensions: Vec<String>,
104    /// Exclude files with these extensions. Mutually exclusive with `include_extensions`.
105    #[serde(default)]
106    pub exclude_extensions: Vec<String>,
107}
108
109/// Parameters for `read_symbol_scope`.
110#[derive(Debug, Default, serde::Deserialize, schemars::JsonSchema)]
111pub struct ReadSymbolScopeParams {
112    /// Semantic path (e.g., `src/auth.ts::AuthService.login`).
113    pub semantic_path: String,
114}
115
116/// Parameters for `read_with_deep_context`.
117#[derive(Debug, Default, serde::Deserialize, schemars::JsonSchema)]
118pub struct ReadWithDeepContextParams {
119    /// Semantic path (e.g., `src/auth.ts::AuthService.login`). MUST include file path and '::'.
120    pub semantic_path: String,
121}
122
123/// Parameters for `get_definition`.
124#[derive(Debug, Default, serde::Deserialize, schemars::JsonSchema)]
125pub struct GetDefinitionParams {
126    /// Semantic path to the reference (e.g., `src/auth.ts::AuthService.login`).
127    pub semantic_path: String,
128}
129
130/// Parameters for `analyze_impact`.
131#[derive(Debug, Default, serde::Deserialize, schemars::JsonSchema)]
132pub struct AnalyzeImpactParams {
133    /// Semantic path to the target (e.g., `src/mod.rs::func`).
134    pub semantic_path: String,
135    /// Traversal depth (max: 5).
136    #[serde(default = "default_max_depth")]
137    pub max_depth: u32,
138}
139
140/// Parameters for `replace_body`.
141#[derive(Debug, Default, serde::Deserialize, schemars::JsonSchema)]
142pub struct ReplaceBodyParams {
143    /// Full semantic path to the target (e.g., `src/mod.rs::func`).
144    pub semantic_path: String,
145    /// SHA-256 hash from previous read (OCC).
146    pub base_version: String,
147    /// Replacement body content (without outer braces).
148    pub new_code: String,
149    /// Write to disk even if validation fails.
150    #[serde(default)]
151    pub ignore_validation_failures: bool,
152}
153
154/// Parameters for `replace_full`.
155#[derive(Debug, Default, serde::Deserialize, schemars::JsonSchema)]
156pub struct ReplaceFullParams {
157    /// Full semantic path to the target (e.g., `src/mod.rs::func`).
158    pub semantic_path: String,
159    /// SHA-256 hash from previous read (OCC).
160    pub base_version: String,
161    /// Complete replacement declaration.
162    pub new_code: String,
163    /// Write to disk even if validation fails.
164    #[serde(default)]
165    pub ignore_validation_failures: bool,
166}
167
168/// Parameters for `insert_before`.
169#[derive(Debug, Default, serde::Deserialize, schemars::JsonSchema)]
170pub struct InsertBeforeParams {
171    /// Full semantic path or bare file path for BOF (e.g., `src/mod.rs::func` or `src/mod.rs`).
172    pub semantic_path: String,
173    /// SHA-256 hash from previous read (OCC).
174    pub base_version: String,
175    /// Code block to insert.
176    pub new_code: String,
177    /// Write to disk even if validation fails.
178    #[serde(default)]
179    pub ignore_validation_failures: bool,
180}
181
182/// Parameters for `insert_after`.
183#[derive(Debug, Default, serde::Deserialize, schemars::JsonSchema)]
184pub struct InsertAfterParams {
185    /// Full semantic path or bare file path for EOF (e.g., `src/mod.rs::func` or `src/mod.rs`).
186    pub semantic_path: String,
187    /// SHA-256 hash from previous read (OCC).
188    pub base_version: String,
189    /// Code block to insert.
190    pub new_code: String,
191    /// Write to disk even if validation fails.
192    #[serde(default)]
193    pub ignore_validation_failures: bool,
194}
195
196/// Parameters for `insert_into`.
197#[derive(Debug, Default, serde::Deserialize, schemars::JsonSchema)]
198pub struct InsertIntoParams {
199    /// Full semantic path to the container target (e.g., `src/lib.rs::tests`).
200    pub semantic_path: String,
201    /// SHA-256 hash from previous read (OCC).
202    pub base_version: String,
203    /// Code block to insert.
204    pub new_code: String,
205    /// Write to disk even if validation fails.
206    #[serde(default)]
207    pub ignore_validation_failures: bool,
208}
209
210/// Parameters for `delete_symbol`.
211#[derive(Debug, Default, serde::Deserialize, schemars::JsonSchema)]
212pub struct DeleteSymbolParams {
213    /// Full semantic path to the target (e.g., `src/auth.ts::AuthService.login`).
214    pub semantic_path: String,
215    /// SHA-256 hash from previous read (OCC).
216    pub base_version: String,
217    /// Write to disk even if validation fails.
218    #[serde(default)]
219    pub ignore_validation_failures: bool,
220}
221
222/// Parameters for `validate_only`.
223#[derive(Debug, Default, serde::Deserialize, schemars::JsonSchema)]
224pub struct ValidateOnlyParams {
225    /// Full semantic path to the target (e.g., `src/mod.rs::func`).
226    pub semantic_path: String,
227    /// Edit type: `replace_body`, `replace_full`, `insert_before`, `insert_after`, `insert_into`, or `delete`.
228    pub edit_type: String,
229    /// Replacement code (required for all types except `delete`).
230    pub new_code: Option<String>,
231    /// SHA-256 hash from previous read (OCC).
232    pub base_version: String,
233}
234
235/// Parameters for `create_file`.
236#[derive(Debug, Default, Clone, serde::Deserialize, schemars::JsonSchema)]
237pub struct CreateFileParams {
238    /// Relative file path.
239    pub filepath: String,
240    /// Initial file content.
241    pub content: String,
242}
243
244/// Parameters for `delete_file`.
245#[derive(Debug, Default, serde::Deserialize, schemars::JsonSchema)]
246pub struct DeleteFileParams {
247    /// Relative file path.
248    pub filepath: String,
249    /// SHA-256 hash from previous read (OCC).
250    pub base_version: String,
251}
252
253/// Parameters for `read_file`.
254#[derive(Debug, Default, serde::Deserialize, schemars::JsonSchema)]
255pub struct ReadFileParams {
256    /// Relative file path.
257    pub filepath: String,
258    /// First line to return (1-indexed).
259    #[serde(default = "default_start_line")]
260    pub start_line: u32,
261    /// Maximum lines to return.
262    #[serde(default = "default_max_lines")]
263    pub max_lines: u32,
264}
265
266/// Parameters for `write_file`.
267#[derive(Debug, Default, serde::Deserialize, schemars::JsonSchema)]
268pub struct WriteFileParams {
269    /// Relative file path.
270    pub filepath: String,
271    /// SHA-256 hash from previous read (OCC).
272    pub base_version: String,
273    /// Full replacement content. Mutually exclusive with `replacements`.
274    pub content: Option<String>,
275    /// Search-and-replace operations. Mutually exclusive with `content`.
276    pub replacements: Option<Vec<Replacement>>,
277}
278
279/// A search-and-replace operation for `write_file`.
280#[derive(Debug, Default, Clone, serde::Deserialize, schemars::JsonSchema)]
281pub struct Replacement {
282    /// Exact text to find.
283    pub old_text: String,
284    /// Replacement text.
285    pub new_text: String,
286}
287
288/// Parameters for `read_source_file`.
289#[derive(Debug, Default, serde::Deserialize, schemars::JsonSchema)]
290pub struct ReadSourceFileParams {
291    /// Relative file path.
292    pub filepath: String,
293    /// Detail level: "compact", "symbols", "full".
294    #[serde(default = "default_detail_level")]
295    pub detail_level: String,
296    /// First line to return (1-indexed).
297    #[serde(default = "default_start_line")]
298    pub start_line: u32,
299    /// Last line to return (1-indexed, inclusive).
300    #[serde(default)]
301    pub end_line: Option<u32>,
302}
303
304/// A single edit in a `replace_batch` call.
305///
306/// Each edit specifies **either** semantic targeting (Option A) OR text targeting (Option B):
307///
308/// **Option A — Semantic targeting:** Set `semantic_path`, `edit_type`, and optionally `new_code`.
309/// Use for source-code constructs that have a parseable AST symbol.
310///
311/// **Option B — Text targeting:** Set `old_text`, `context_line`, and optionally `replacement_text`.
312/// Use for Vue `<template>`/`<style>` zones or any region with no usable semantic path.
313/// The search scans ±25 lines around `context_line` for an exact match of `old_text`.
314#[derive(Debug, Default, serde::Deserialize, schemars::JsonSchema)]
315pub struct BatchEdit {
316    // ── Semantic targeting (Option A) ─────────────────────────────────────
317    /// Full semantic path to the target (e.g., `file.vue::script::check`).
318    /// Required when using semantic targeting.
319    #[serde(default)]
320    pub semantic_path: String,
321    /// Edit type: `replace_body`, `replace_full`, `insert_before`, `insert_after`, or `delete`.
322    /// Required when using semantic targeting.
323    #[serde(default)]
324    pub edit_type: String,
325    /// Replacement code (required for all semantic types except `delete`).
326    pub new_code: Option<String>,
327
328    // ── Text targeting (Option B) ──────────────────────────────────────────
329    /// Exact text to find and replace. Set this for template/style edits that have no
330    /// semantic path (e.g., Vue `<template>`, `<style>` zones, embedded SQL).
331    /// When set, `semantic_path` and `edit_type` are ignored.
332    /// The search scans ±25 lines around `context_line` for an exact match.
333    pub old_text: Option<String>,
334    /// Line number (1-indexed) to anchor the `old_text` search window.
335    /// Required when `old_text` is set. The search scans ±25 lines around this line.
336    pub context_line: Option<u32>,
337    /// Replacement text when using text targeting. Required when `old_text` is set.
338    pub replacement_text: Option<String>,
339    // ── Shared options ─────────────────────────────────────────────────────
340    /// When `true`, collapses `\s+` to a single space before matching `old_text`.
341    /// Useful for HTML/template contexts where indentation may be inconsistent.
342    /// Do NOT use for Python, YAML, or other whitespace-significant languages.
343    #[serde(default)]
344    pub normalize_whitespace: bool,
345}
346
347/// Parameters for `replace_batch`.
348#[derive(Debug, Default, serde::Deserialize, schemars::JsonSchema)]
349pub struct ReplaceBatchParams {
350    /// Relative file path.
351    pub filepath: String,
352    /// SHA-256 hash from previous read (OCC) for the entire file.
353    pub base_version: String,
354    /// List of edits to apply atomically.
355    pub edits: Vec<BatchEdit>,
356    /// Write to disk even if validation fails.
357    #[serde(default)]
358    pub ignore_validation_failures: bool,
359}
360
361// ── Response Types ──────────────────────────────────────────────────
362
363/// The response for `search_codebase`.
364#[derive(Debug, Serialize, schemars::JsonSchema)]
365pub struct SearchCodebaseResponse {
366    /// List of search matches.
367    pub matches: Vec<pathfinder_search::SearchMatch>,
368    /// Raw match count from ripgrep **before** `filter_mode` filtering, **after** ripgrep truncation.
369    ///
370    /// When `truncated = true`, this equals `max_results` and ripgrep stopped searching early.
371    /// When `filter_mode` is `"comments_only"` or `"code_only"`, matches that do not
372    /// pass the filter are excluded from `matches` but still counted here.
373    /// Compare with `returned_count` to understand how many matches were filtered.
374    pub total_matches: usize,
375    /// Number of matches actually returned in this response (after `filter_mode` filtering).
376    ///
377    /// `returned_count == matches.len()`. Provided as a convenience field so agents
378    /// do not need to count `matches` themselves.
379    pub returned_count: usize,
380    /// Indicates if the match list was truncated by `max_results`.
381    pub truncated: bool,
382    /// Grouped output — populated when `group_by_file: true`.
383    ///
384    /// Each group represents one file and contains either full matches (for
385    /// unknown files) or minimal matches (for files in `known_files`).
386    #[serde(skip_serializing_if = "Option::is_none")]
387    pub file_groups: Option<Vec<SearchResultGroup>>,
388    /// Whether the search response is degraded.
389    #[serde(default)]
390    pub degraded: bool,
391    /// Reason for degradation, if any.
392    #[serde(skip_serializing_if = "Option::is_none")]
393    pub degraded_reason: Option<String>,
394    /// When results are truncated, this field provides the `offset` value
395    /// to use for the next page of results. Absent when not truncated.
396    #[serde(skip_serializing_if = "Option::is_none")]
397    pub next_offset: Option<u32>,
398}
399
400/// A minimal match entry for files already in the agent's context (`known_files`)
401/// when grouped by file.
402///
403/// Omits `file`, `version_hash` (deduplicated at group level), and `content`,
404/// `context_before`, and `context_after` to save tokens.
405#[derive(Debug, Serialize, schemars::JsonSchema)]
406pub struct GroupedKnownMatch {
407    /// 1-indexed line number.
408    pub line: u64,
409    /// 1-indexed column number.
410    pub column: u64,
411    /// AST symbol enclosing this match (if available).
412    #[serde(skip_serializing_if = "Option::is_none")]
413    pub enclosing_semantic_path: Option<String>,
414    /// Always `true` — signals this match was suppressed because the file is known.
415    pub known: bool,
416}
417
418/// A group of matches belonging to one file, returned when `group_by_file: true`.
419#[derive(Debug, Serialize, schemars::JsonSchema)]
420pub struct SearchResultGroup {
421    /// File path relative to workspace root.
422    pub file: String,
423    /// SHA-256 hash of the file (shared by all matches in this group).
424    pub version_hash: String,
425    /// Total number of matches in this group (both full and known).
426    ///
427    /// Provided so agents can quickly assess match density without counting sub-arrays.
428    /// Always present regardless of whether `matches` or `known_matches` are serialized.
429    pub total_matches: usize,
430    /// Full matches (for files NOT in `known_files`).
431    ///
432    /// Per-match objects contain only `{ line, column, content, context_before,
433    /// context_after, enclosing_semantic_path }` — `file` and `version_hash` are
434    /// deduplicated at group level to avoid repeating them for every match.
435    ///
436    /// Absent (not just empty) when all matches in this group are for known files.
437    /// Check `total_matches` for the match count regardless of which array is populated.
438    #[serde(skip_serializing_if = "Vec::is_empty")]
439    #[schemars(skip)]
440    pub matches: Vec<GroupedMatch>,
441    /// Minimal matches (for files in `known_files`).
442    ///
443    /// Absent when no matches in this group are for known files.
444    #[serde(skip_serializing_if = "Vec::is_empty")]
445    #[schemars(skip)]
446    pub known_matches: Vec<GroupedKnownMatch>,
447}
448/// A single match within a `SearchResultGroup`.
449///
450/// Omits `file` and `version_hash` (deduplicated at group level) to reduce
451/// token usage when many matches belong to the same file.
452#[derive(Debug, Serialize, schemars::JsonSchema)]
453pub struct GroupedMatch {
454    /// 1-indexed line number of the match.
455    pub line: u64,
456    /// 1-indexed column number of the match start.
457    pub column: u64,
458    /// The full content of the matching line.
459    pub content: String,
460    /// Lines immediately before the match.
461    #[serde(skip_serializing_if = "Vec::is_empty")]
462    pub context_before: Vec<String>,
463    /// Lines immediately after the match.
464    #[serde(skip_serializing_if = "Vec::is_empty")]
465    pub context_after: Vec<String>,
466    /// AST symbol enclosing this match (if available).
467    #[serde(skip_serializing_if = "Option::is_none")]
468    pub enclosing_semantic_path: Option<String>,
469}
470
471/// The metadata embedded in `structured_content` for `get_repo_map`.
472#[derive(Debug, Serialize, serde::Deserialize, schemars::JsonSchema)]
473pub struct GetRepoMapMetadata {
474    /// Technology stack of the repository.
475    pub tech_stack: Vec<String>,
476    /// Number of files scanned.
477    pub files_scanned: usize,
478    /// Number of files truncated.
479    pub files_truncated: usize,
480    /// Number of files within the configured scope.
481    pub files_in_scope: usize,
482    /// Percentage of files covered by the search.
483    pub coverage_percent: u8,
484    /// Map of file paths to their version hashes.
485    pub version_hashes: std::collections::HashMap<String, String>,
486    /// Always `true` while visibility filtering is not yet implemented.
487    /// Agents should treat all symbols as public regardless of `visibility` param.
488    #[serde(skip_serializing_if = "Option::is_none")]
489    pub visibility_degraded: Option<bool>,
490    /// `true` when filtering by `changed_since` fails (e.g., git is unavailable).
491    #[serde(default)]
492    pub degraded: bool,
493    /// Reason for degradation.
494    #[serde(skip_serializing_if = "Option::is_none")]
495    pub degraded_reason: Option<String>,
496    /// System capabilities available for this repository.
497    pub capabilities: RepoCapabilities,
498}
499
500/// The overall capabilities of the Pathfinder system.
501#[derive(Debug, Serialize, serde::Deserialize, schemars::JsonSchema)]
502pub struct RepoCapabilities {
503    /// Whether AST-aware edit tools are supported.
504    pub edit: bool,
505    /// Whether the search engine is supported.
506    pub search: bool,
507    /// LSP-specific capabilities and status.
508    pub lsp: LspCapabilities,
509}
510
511/// LSP status and capabilities.
512#[derive(Debug, Serialize, serde::Deserialize, schemars::JsonSchema)]
513pub struct LspCapabilities {
514    /// `true` if LSP is generally supported by the system.
515    pub supported: bool,
516    /// Map of language ID to its specific LSP process status.
517    pub per_language: std::collections::HashMap<String, pathfinder_lsp::types::LspLanguageStatus>,
518}
519
520/// The metadata embedded in `structured_content` for `read_symbol_scope`.
521#[derive(Debug, Serialize, serde::Deserialize, schemars::JsonSchema)]
522pub struct ReadSymbolScopeMetadata {
523    /// The extracted symbol source code.
524    ///
525    /// Mirrors `content[0].text` in the MCP response. Provided here so that
526    /// agents consuming `structured_content` directly have the full source
527    /// without needing to inspect the main content array.
528    pub content: String,
529    /// Starting line number of the symbol in the source.
530    pub start_line: usize,
531    /// Ending line number of the symbol in the source.
532    pub end_line: usize,
533    /// Version hash of the file containing the symbol.
534    pub version_hash: String,
535    /// Programming language of the source symbol.
536    pub language: String,
537}
538
539/// The response for `read_symbol_scope`.
540#[derive(Debug, Serialize, schemars::JsonSchema)]
541pub struct ReadSymbolScopeResponse {
542    /// The extracted symbol source code.
543    pub content: String,
544    /// Starting line number of the symbol in the source.
545    pub start_line: usize,
546    /// Ending line number of the symbol in the source.
547    pub end_line: usize,
548    /// Version hash of the file containing the symbol.
549    pub version_hash: String,
550    /// Programming language of the source symbol.
551    pub language: String,
552}
553
554/// A symbol output for `read_source_file`.
555#[derive(Debug, Clone, PartialEq, Serialize, serde::Deserialize, schemars::JsonSchema)]
556pub struct SourceSymbol {
557    /// Name of the symbol.
558    pub name: String,
559    /// Semantic path of the symbol.
560    pub semantic_path: String,
561    /// Kind of the symbol (e.g., function, struct).
562    pub kind: String,
563    /// Starting line number of the symbol in the source.
564    pub start_line: usize,
565    /// Ending line number of the symbol in the source.
566    pub end_line: usize,
567    /// Child symbols nested within this symbol.
568    #[serde(skip_serializing_if = "Vec::is_empty")]
569    pub children: Vec<Self>,
570}
571
572/// The metadata embedded in `structured_content` for `read_source_file`.
573#[derive(Debug, Clone, PartialEq, Default, Serialize, serde::Deserialize, schemars::JsonSchema)]
574pub struct ReadSourceFileMetadata {
575    /// Version hash of the source file.
576    pub version_hash: String,
577    /// Programming language of the source file.
578    pub language: String,
579    /// Symbols extracted from the source file.
580    #[serde(skip_serializing_if = "Vec::is_empty")]
581    pub symbols: Vec<SourceSymbol>,
582}
583
584/// The response for all AST-aware edit tools:
585/// `replace_body`, `replace_full`, `insert_before`, `insert_after`,
586/// `delete_symbol`, and `validate_only`.
587#[derive(Debug, Serialize, schemars::JsonSchema)]
588pub struct EditResponse {
589    /// Whether the edit succeeded (always `true` for non-`validate_only` tools).
590    pub success: bool,
591    /// SHA-256 hash of the file after the edit. `None` for `validate_only`.
592    #[serde(skip_serializing_if = "Option::is_none")]
593    pub new_version_hash: Option<String>,
594    /// Whether the code was reformatted (always `false` until LSP formatting is wired).
595    pub formatted: bool,
596    /// LSP validation result.
597    pub validation: EditValidation,
598    /// `true` when LSP validation was skipped (no language server available).
599    #[serde(default)]
600    pub validation_skipped: bool,
601    /// Machine-readable reason why validation was skipped.
602    #[serde(skip_serializing_if = "Option::is_none")]
603    pub validation_skipped_reason: Option<String>,
604    /// Warning about the edit operation (e.g., targeting a Rust struct instead of impl block).
605    #[serde(skip_serializing_if = "Option::is_none")]
606    pub warning: Option<String>,
607}
608
609/// The result of LSP validation for an edit operation.
610#[derive(Debug, Clone, Serialize, schemars::JsonSchema)]
611pub struct EditValidation {
612    /// `"passed"`, `"failed"`, `"skipped"`, or `"uncertain"`.
613    ///
614    /// - `"passed"`: validation ran and detected no new errors
615    /// - `"failed"`: validation ran and detected new errors
616    /// - `"skipped"`: validation was not performed (no LSP available)
617    /// - `"uncertain"`: validation ran but results are unreliable (LSP warmup)
618    pub status: String,
619    /// Errors introduced by the edit.
620    pub introduced_errors: Vec<pathfinder_common::error::DiagnosticError>,
621    /// Errors resolved by the edit.
622    pub resolved_errors: Vec<pathfinder_common::error::DiagnosticError>,
623    /// IW-2: Confidence in the validation result.
624    ///
625    /// - `"high"`: LSP pull diagnostics used (definitive result).
626    /// - `"medium"`: LSP push diagnostics used (may miss errors during indexing).
627    /// - `"low"`: Both pre/post diagnostic sets were empty (could be clean or LSP not ready).
628    /// - `"none"`: Validation was skipped entirely.
629    ///
630    /// Agents should use this to decide whether to re-validate or proceed:
631    /// `"high"` → trust the result; `"low"`/`"none"` → consider calling `lsp_health` first.
632    #[serde(skip_serializing_if = "Option::is_none")]
633    pub validation_confidence: Option<String>,
634    /// MT-4: Actionable recovery instruction for the agent.
635    ///
636    /// Present when validation was skipped or degraded due to an LSP error.
637    /// Tells the agent *what to do next* to recover or validate externally.
638    ///
639    /// Examples:
640    /// - `"Call lsp_health to check server status"`
641    /// - `"LSP request timed out. Call lsp_health(action='restart')."`
642    /// - `"No LSP configured. Install a language server or run cargo check externally."`
643    #[serde(skip_serializing_if = "Option::is_none")]
644    pub recovery_action: Option<String>,
645}
646
647impl EditValidation {
648    /// Return a skipped validation result (no LSP available).
649    #[must_use]
650    pub fn skipped() -> Self {
651        Self {
652            status: "skipped".to_owned(),
653            introduced_errors: vec![],
654            resolved_errors: vec![],
655            validation_confidence: Some("none".to_owned()),
656            recovery_action: None,
657        }
658    }
659
660    /// Return a skipped validation result enriched with a recovery hint.
661    ///
662    /// Use this instead of `skipped()` whenever an `LspError` is the cause —
663    /// pass `lsp_error.recovery_hint()` as the `recovery_action` argument.
664    /// This is the MT-4 telemetry path.
665    #[must_use]
666    pub fn skipped_with_recovery(recovery_action: Option<String>) -> Self {
667        Self {
668            status: "skipped".to_owned(),
669            introduced_errors: vec![],
670            resolved_errors: vec![],
671            validation_confidence: Some("none".to_owned()),
672            recovery_action,
673        }
674    }
675
676    /// Return an uncertain validation result (LSP ran but results are unreliable).
677    ///
678    /// Use when both pre- and post-edit diagnostics are empty, which could mean
679    /// either (a) the code is genuinely clean, or (b) the LSP hasn't finished
680    /// indexing. Agents should treat "uncertain" as "possibly correct but unverified".
681    #[must_use]
682    pub fn uncertain() -> Self {
683        Self {
684            status: "uncertain".to_owned(),
685            introduced_errors: vec![],
686            resolved_errors: vec![],
687            validation_confidence: Some("low".to_owned()),
688            recovery_action: None,
689        }
690    }
691}
692
693/// The response for `create_file`.
694#[derive(Debug, Serialize, schemars::JsonSchema)]
695pub struct CreateFileResponse {
696    /// Whether the file creation succeeded.
697    pub success: bool,
698    /// Hash of the file version after creation.
699    pub version_hash: String,
700    /// Results of validation after file creation.
701    pub validation: ValidationResult,
702}
703
704/// The response for `delete_file`.
705#[derive(Debug, Serialize, schemars::JsonSchema)]
706pub struct DeleteFileResponse {
707    /// Whether the file deletion succeeded.
708    pub success: bool,
709    /// Short hash of the file version that was deleted (for audit/OCC chain).
710    pub version_hash: String,
711}
712
713/// The metadata embedded in `structured_content` for `read_file`.
714#[derive(Debug, Serialize, serde::Deserialize, schemars::JsonSchema)]
715pub struct ReadFileMetadata {
716    /// First line number returned from the file.
717    pub start_line: u32,
718    /// Number of lines returned.
719    pub lines_returned: u32,
720    /// Total number of lines in the file.
721    pub total_lines: u32,
722    /// Whether the output was truncated.
723    pub truncated: bool,
724    /// Version hash of the file.
725    pub version_hash: String,
726    /// Detected language of the file.
727    pub language: String,
728}
729
730/// The metadata embedded in `structured_content` for `write_file`.
731#[derive(Debug, Serialize, serde::Deserialize, schemars::JsonSchema)]
732pub struct WriteFileMetadata {
733    /// Whether the write operation succeeded.
734    pub success: bool,
735    /// New version hash after writing.
736    pub new_version_hash: String,
737}
738
739/// Validation result for edits.
740#[derive(Debug, Serialize, schemars::JsonSchema)]
741pub struct ValidationResult {
742    /// Status of the validation (`"passed"`, `"failed"`, or `"skipped"`).
743    pub status: String,
744    /// Errors that were introduced in the edits.
745    pub introduced_errors: Vec<pathfinder_common::error::DiagnosticError>,
746}
747
748// ── Navigation Tool Response Types ─────────────────────────────────
749
750/// A dependency signature extracted for `read_with_deep_context`.
751#[derive(Debug, Serialize, serde::Deserialize, schemars::JsonSchema)]
752pub struct DeepContextDependency {
753    /// Semantic path of the called symbol.
754    pub semantic_path: String,
755    /// Extracted signature (declaration line only, no body).
756    pub signature: String,
757    /// File path relative to workspace root.
758    pub file: String,
759    /// 1-indexed line of the definition.
760    pub line: usize,
761}
762
763/// The metadata embedded in `structured_content` for `read_with_deep_context`.
764#[derive(Debug, Serialize, serde::Deserialize, schemars::JsonSchema)]
765pub struct ReadWithDeepContextMetadata {
766    /// Start line of the symbol (1-indexed).
767    pub start_line: usize,
768    /// End line of the symbol (1-indexed).
769    pub end_line: usize,
770    /// OCC version hash for the symbol's file.
771    pub version_hash: String,
772    /// Detected language.
773    pub language: String,
774    /// Signatures of all symbols called by this one.
775    #[serde(skip_serializing_if = "Vec::is_empty", default)]
776    pub dependencies: Vec<DeepContextDependency>,
777    /// `true` when LSP dependency resolution was unavailable (Tree-sitter only).
778    #[serde(default)]
779    pub degraded: bool,
780    /// Reason for degradation (e.g., `"no_lsp"`).
781    #[serde(skip_serializing_if = "Option::is_none")]
782    pub degraded_reason: Option<String>,
783    /// IW-2: LSP readiness signal at the time of the call.
784    ///
785    /// - `"ready"`: LSP is fully operational — results are authoritative.
786    /// - `"warming_up"`: LSP is still indexing — results may be partial.
787    /// - `"unavailable"`: No LSP; Tree-sitter fallback used.
788    #[serde(skip_serializing_if = "Option::is_none")]
789    pub lsp_readiness: Option<String>,
790}
791
792/// The response for `get_definition`.
793#[derive(Debug, Serialize, schemars::JsonSchema)]
794pub struct GetDefinitionResponse {
795    /// Relative file path of the definition site.
796    pub file: String,
797    /// 1-indexed line number of the definition.
798    pub line: u32,
799    /// 1-indexed column number.
800    pub column: u32,
801    /// First line of the definition (code preview).
802    pub preview: String,
803    /// `true` when LSP was unavailable and no fallback was possible.
804    #[serde(default)]
805    pub degraded: bool,
806    /// Reason for degradation.
807    #[serde(skip_serializing_if = "Option::is_none")]
808    pub degraded_reason: Option<String>,
809    /// IW-2: LSP readiness at the time of the call.
810    ///
811    /// - `"ready"`: LSP operational — definition is authoritative.
812    /// - `"warming_up"`: LSP still indexing — result may be from Tree-sitter fallback.
813    /// - `"unavailable"`: No LSP; result is from ripgrep heuristics.
814    #[serde(skip_serializing_if = "Option::is_none")]
815    pub lsp_readiness: Option<String>,
816}
817
818/// A single reference in an impact analysis.
819#[derive(Debug, Serialize, serde::Deserialize, schemars::JsonSchema)]
820pub struct ImpactReference {
821    /// Semantic path of the referencing/referenced symbol.
822    pub semantic_path: String,
823    /// File path relative to workspace root.
824    pub file: String,
825    /// 1-indexed line of the call site or definition.
826    pub line: usize,
827    /// A short code snippet showing the call site or declaration.
828    pub snippet: String,
829    /// OCC version hash for this file.
830    pub version_hash: String,
831    /// Direction of the reference relative to the target symbol.
832    ///
833    /// - `"incoming"` — this symbol calls or references the target (a caller).
834    /// - `"outgoing"` — the target calls or references this symbol (a callee).
835    /// - `"incoming_heuristic"` — inferred by grep fallback when LSP is unavailable;
836    ///   treat as a candidate, not a confirmed call.
837    pub direction: String,
838    /// BFS traversal depth (0 = direct caller/callee, 1 = one hop away, etc.).
839    pub depth: usize,
840}
841
842/// The metadata embedded in `structured_content` for `analyze_impact`.
843#[derive(Debug, Serialize, serde::Deserialize, schemars::JsonSchema)]
844pub struct AnalyzeImpactMetadata {
845    /// Symbols that call the target (caller graph).
846    /// `null` when `degraded` is `true` — LSP was unavailable so callers are **unknown**.
847    /// An empty array `[]` means LSP confirmed zero callers.
848    pub incoming: Option<Vec<ImpactReference>>,
849    /// Symbols the target calls (callee graph).
850    /// `null` when `degraded` is `true` — LSP was unavailable so callees are **unknown**.
851    /// An empty array `[]` means LSP confirmed zero callees.
852    pub outgoing: Option<Vec<ImpactReference>>,
853    /// Number of transitive levels traversed.
854    pub depth_reached: u32,
855    /// Total files referenced across all incoming and outgoing references.
856    pub files_referenced: usize,
857    /// Whether the call hierarchy analysis was degraded (LSP unavailable or crashed).
858    /// Always present. When `true`, `incoming` and `outgoing` are `null` (not empty arrays).
859    pub degraded: bool,
860    /// Machine-readable reason for degradation (e.g., `no_lsp`, `lsp_crash`, `lsp_timeout`).
861    /// Absent when `degraded` is `false`.
862    #[serde(skip_serializing_if = "Option::is_none")]
863    pub degraded_reason: Option<String>,
864    /// SHA-256 version hashes for all referenced files (including the target file itself),
865    /// keyed by relative file path. Agents can use these as `base_version` for immediate
866    /// editing without a separate read call.
867    #[serde(skip_serializing_if = "std::collections::HashMap::is_empty", default)]
868    pub version_hashes: std::collections::HashMap<String, String>,
869}
870
871// ── LSP Health Tool Types ────────────────────────────────────────────
872
873/// Parameters for `lsp_health`.
874#[derive(Debug, Default, serde::Deserialize, schemars::JsonSchema)]
875pub struct LspHealthParams {
876    /// Optional language to check (e.g., "rust", "typescript").
877    /// If omitted, checks all available languages.
878    #[serde(default)]
879    pub language: Option<String>,
880    /// IW-4: Optional action to perform.
881    ///
882    /// - `"restart"`: Force-restart the LSP process for the specified language.
883    ///   `language` must be set when using `"restart"`.
884    ///   Returns updated health status after the restart attempt.
885    #[serde(default)]
886    pub action: Option<String>,
887}
888
889/// The response for `lsp_health`.
890#[derive(Debug, Serialize, Deserialize, schemars::JsonSchema)]
891pub struct LspHealthResponse {
892    /// Overall LSP readiness: `"ready"`, `"warming_up"`, or `"unavailable"`.
893    pub status: String,
894    /// Per-language status details.
895    pub languages: Vec<LspLanguageHealth>,
896}
897
898/// Per-language LSP health status.
899#[derive(Debug, Serialize, Deserialize, schemars::JsonSchema)]
900pub struct LspLanguageHealth {
901    /// Language ID (e.g., "rust", "typescript").
902    pub language: String,
903    /// Status: `"ready"`, `"warming_up"`, `"starting"`, or `"unavailable"`.
904    pub status: String,
905    /// Time since LSP process started, formatted as a human-readable string (e.g., "45s").
906    #[serde(skip_serializing_if = "Option::is_none")]
907    pub uptime: Option<String>,
908    /// How diagnostics work for this language.
909    #[serde(skip_serializing_if = "Option::is_none")]
910    pub diagnostics_strategy: Option<String>,
911    /// Whether call hierarchy is supported (affects `analyze_impact`, `read_with_deep_context`).
912    #[serde(skip_serializing_if = "Option::is_none")]
913    pub supports_call_hierarchy: Option<bool>,
914    /// Whether validation is supported (affects `validate_only`, edit tools).
915    #[serde(skip_serializing_if = "Option::is_none")]
916    pub supports_diagnostics: Option<bool>,
917    /// Whether definition is supported (affects `get_definition`).
918    #[serde(skip_serializing_if = "Option::is_none")]
919    pub supports_definition: Option<bool>,
920    /// Whether formatting is supported (affects edit tools).
921    #[serde(skip_serializing_if = "Option::is_none")]
922    pub supports_formatting: Option<bool>,
923    /// Background indexing status: `"complete"`, `"in_progress"`, or None.
924    ///
925    /// Independent of overall status — an LSP can be "ready" for navigation
926    /// while still indexing in the background.
927    #[serde(skip_serializing_if = "Option::is_none")]
928    pub indexing_status: Option<String>,
929    /// Whether navigation (`get_definition`, `analyze_impact`) is functional.
930    ///
931    /// `true` once the LSP initialize handshake completes with `definitionProvider: true`.
932    /// Independent of `indexing_status` — navigation works during indexing but
933    /// results may be partial until indexing completes.
934    ///
935    /// Agents should use this signal to decide:
936    /// - `navigation_ready = true` + `indexing_status = "complete"` → full confidence
937    /// - `navigation_ready = true` + `indexing_status = "in_progress"` → results may be partial
938    /// - `navigation_ready = false` or `None` → fall back to Tree-sitter
939    #[serde(skip_serializing_if = "Option::is_none")]
940    pub navigation_ready: Option<bool>,
941    /// Whether the status was verified by a live probe (rather than just progress notifications).
942    /// When true, the agent can trust the status.
943    #[serde(skip_serializing_if = "crate::server::types::is_false")]
944    pub probe_verified: bool,
945    /// Install guidance when LSP is unavailable.
946    ///
947    /// Provides actionable commands users can run to install their LSP servers.
948    /// `None` when LSP is running or language not detected at all.
949    #[serde(skip_serializing_if = "Option::is_none")]
950    pub install_hint: Option<String>,
951    /// Tools that are degraded (using fallback) for this language.
952    ///
953    /// Empty when LSP is fully operational. Lists which tools lose LSP support.
954    /// Example: `["analyze_impact", "read_with_deep_context"]` when call hierarchy unsupported.
955    #[serde(skip_serializing_if = "Vec::is_empty")]
956    pub degraded_tools: Vec<String>,
957    /// Approximate validation latency in milliseconds for this language.
958    ///
959    /// `None` when unknown or not applicable. Helps agents decide whether to validate.
960    #[serde(skip_serializing_if = "Option::is_none")]
961    pub validation_latency_ms: Option<u64>,
962}
963
964/// Helper to skip serializing false values for `probe_verified`.
965#[allow(clippy::trivially_copy_pass_by_ref)] // Required by serde's skip_serializing_if signature
966pub(crate) fn is_false(b: &bool) -> bool {
967    !b
968}
969
970// ── Default Value Functions ─────────────────────────────────────────
971
972#[must_use]
973pub fn default_path_glob() -> String {
974    "**/*".to_owned()
975}
976#[must_use]
977pub const fn default_max_results() -> u32 {
978    50
979}
980#[must_use]
981pub const fn default_context_lines() -> u32 {
982    2
983}
984#[must_use]
985pub fn default_repo_map_path() -> String {
986    ".".to_owned()
987}
988#[must_use]
989pub const fn default_max_tokens() -> u32 {
990    16_000
991}
992#[must_use]
993pub const fn default_max_tokens_per_file() -> u32 {
994    2_000
995}
996#[must_use]
997pub const fn default_depth() -> u32 {
998    5
999}
1000#[must_use]
1001pub const fn default_max_depth() -> u32 {
1002    2
1003}
1004#[must_use]
1005pub const fn default_start_line() -> u32 {
1006    1
1007}
1008#[must_use]
1009pub const fn default_max_lines() -> u32 {
1010    500
1011}
1012#[must_use]
1013pub fn default_detail_level() -> String {
1014    "compact".to_string()
1015}