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