Skip to main content

cosh_tools/fs/
types.rs

1use std::path::PathBuf;
2
3use schemars::JsonSchema;
4use serde::{Deserialize, Serialize};
5
6use crate::util::path_guard::PathGuard;
7
8/// One passive LSP finding attached to an fs operation result.
9#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
10pub struct LspNote {
11    /// Workspace path of the file the finding refers to.
12    pub path: String,
13    /// 1-based line where the finding starts.
14    pub line: u32,
15    /// Human-readable diagnostic message.
16    pub message: String,
17    /// Emitting tool (e.g. `rustc`, `tsc`), when the server reports one.
18    pub source: Option<String>,
19}
20
21/// Structured LSP feedback for one fs operation, split by severity so the
22/// TUI can color errors and warnings differently and the model gets precise
23/// anchors. `None`/empty means the operation produced no findings.
24#[derive(Debug, Clone, Default, Serialize, Deserialize, JsonSchema)]
25pub struct LspNotes {
26    #[serde(default, skip_serializing_if = "Vec::is_empty")]
27    pub errors: Vec<LspNote>,
28    #[serde(default, skip_serializing_if = "Vec::is_empty")]
29    pub warnings: Vec<LspNote>,
30}
31
32/// A single read specification.
33#[derive(Debug, Deserialize, JsonSchema)]
34pub struct Target {
35    pub path: String,
36    /// Legacy form of `offset` (reads the syntactic block containing that
37    /// line). Kept as an alias; the advertised schema uses `offset`.
38    pub line: Option<usize>,
39    pub symbol: Option<String>,
40    /// Legacy 1-based inclusive line range(s) to read exactly, e.g.
41    /// `"50-100"` or `"10-20,200-220"` (comma-separated for multiple
42    /// disjoint ranges). Performs a plain line slice — no AST block
43    /// resolution. Accepted but NOT advertised: the schema uses
44    /// `offset`/`limit` (the CC-trained shape). `offset`+`limit` is
45    /// normalized into a single range before this is consulted, and wins
46    /// when both are provided.
47    pub line_range: Option<String>,
48    /// 1-based line number to start reading from (the CC `Read` shape).
49    /// With `limit`, reads exactly `offset..offset+limit-1` (plain slice);
50    /// without `limit`, reads the syntactic block containing that line
51    /// (same behavior as the legacy `line`).
52    pub offset: Option<usize>,
53    /// Number of lines to read; only meaningful together with `offset`.
54    pub limit: Option<usize>,
55}
56
57/// Configuration for file read operations.
58#[derive(Default, Debug, Deserialize, JsonSchema)] // #[derive(Debug, Deserialize, JsonSchema)]
59pub struct FsRead {
60    pub targets: Vec<Target>,
61}
62#[derive(Clone, Default, Debug, Deserialize, JsonSchema)]
63pub struct TargetFile {
64    /// Content to write. The advertised schema uses `content` (the field
65    /// name every mainstream write tool trains on); `text` is the legacy
66    /// batch-form name, kept as the primary field with `content` as a serde
67    /// alias. NOT `#[serde(default)]`: a missing `content` must fail at
68    /// parse time (surfacing the schema hint), not fall through to an
69    /// empty-write warning.
70    #[serde(alias = "content")]
71    pub text: String,
72    pub path: String,
73    /// Optional file hash from a previous `read`. When the target file
74    /// already exists on disk the hash is **required** — the write is
75    /// rejected if it is missing or stale.  For brand-new files the
76    /// field is ignored.
77    #[serde(default)]
78    pub file_hash: Option<String>,
79}
80
81/// Configuration for file write operations.
82#[derive(Default, Debug, Deserialize, JsonSchema)]
83pub struct FsWrite {
84    pub targets: Vec<TargetFile>,
85}
86
87#[derive(Debug, Clone, Deserialize, JsonSchema)]
88pub struct FsMetadata {
89    pub root: PathBuf,
90    pub allowlist: Option<Vec<PathBuf>>,
91    pub blocklist: Option<Vec<PathBuf>>,
92}
93
94// ___
95#[derive(Debug, Clone, Default, Deserialize, JsonSchema)]
96pub struct EditTarget {
97    pub path: String,
98    pub file_hash: String,
99    pub ops: String,
100}
101
102/// Configuration for file edit operations (hashline replace engine).
103#[derive(Debug, Clone, Default, Deserialize, JsonSchema)]
104pub struct FsEdit {
105    /// Edit targets. Multi-file batching is intentionally NOT advertised by
106    /// the tool schema: single-path calls proved far more reliable in agent
107    /// sessions (batched calls raised schema-error rates). The batch form is
108    /// kept — not removed — for future studies and benchmarks.
109    pub targets: Vec<EditTarget>,
110    /// Preview mode: apply in memory only, returning the diff and the
111    /// syntax-probe verdict without writing anything. Re-issue without
112    /// `dry_run` to apply for real.
113    #[serde(default)]
114    pub dry_run: bool,
115}
116
117/// One content-anchored replacement.
118///
119/// Public call shape is flat: `fs_edit` unwraps the single advertised
120/// `path/old_string/new_string` object into this struct (batch form kept for
121/// benchmarks).
122///
123/// `old_string` is a content address: it must match the file exactly (the
124/// match must be unique unless [`Self::replace_all`] is set). The edit is
125/// still bound to a hashline snapshot tag, so drift between read and edit
126/// keeps being detected — the match runs against the tagged snapshot and the
127/// result flows through the same hashline apply pipeline as `targets`.
128#[derive(Debug, Clone, Deserialize, JsonSchema)]
129pub struct ReplaceEdit {
130    pub path: String,
131    /// 4-hex content hash tag: the `¶path#TAG` anchor from your last read
132    /// (or a previous edit result). Required for the first edit of each file
133    /// in the call; follow-up edits to the same file in the same call may
134    /// omit it — they chain on the fresh tag produced by the previous edit.
135    #[serde(default)]
136    pub file_hash: Option<String>,
137    /// Exact text to replace, copied verbatim including whitespace and
138    /// newlines. Must occur exactly once unless [`Self::replace_all`] is set;
139    /// use the smallest snippet that is unique.
140    pub old_string: String,
141    /// Replacement text. Empty deletes the matched text.
142    pub new_string: String,
143    /// Replace every occurrence instead of requiring a unique match.
144    #[serde(default)]
145    pub replace_all: bool,
146}
147
148/// Arguments for the content edit engine.
149///
150/// The tool schema advertises one edit (one path) per call; the `Vec` batch
151/// form is kept — not removed — for future studies and benchmarks (batched
152/// calls raised schema-error rates in agent sessions). Same-file edits in a
153/// batch still chain: follow-ups may omit `file_hash`.
154#[derive(Debug, Clone, Default, Deserialize, JsonSchema)]
155pub struct FsContentEdit {
156    pub edits: Vec<ReplaceEdit>,
157    /// Preview mode: apply in memory only, returning the diff and the
158    /// syntax-probe verdict without writing anything. Re-issue without
159    /// `dry_run` to apply for real.
160    #[serde(default)]
161    pub dry_run: bool,
162}
163
164// ---------------------------------------------------------------------------
165// AST engine
166// ---------------------------------------------------------------------------
167
168/// A single structural rewrite op for the AST engine.
169///
170/// `pat` is an AST-aware pattern (ast-grep style) that may use metavariables
171/// such as `$NAME` (matches exactly one node) and `$$$NAME` (matches zero or
172/// more nodes, e.g. an argument list). `out` is the replacement template;
173/// metavariables referenced there are substituted with the text captured when
174/// `pat` matched.
175#[derive(Debug, Clone, Deserialize, JsonSchema)]
176pub struct AstEditOp {
177    /// AST pattern to match against the file's syntax tree.
178    pub pat: String,
179    /// Replacement template. Metavariables from `pat` may be referenced.
180    pub out: String,
181}
182
183/// The AST engine argument.
184#[derive(Debug, Clone, Default, Deserialize, JsonSchema)]
185pub struct FsAstEdit {
186    /// Structural rewrite operations; applied in order over each matched file.
187    pub ops: Vec<AstEditOp>,
188    /// Files, directories, or globs to rewrite. The advertised schema is one
189    /// path per call; multi-path glob sweeps are kept for codemods and
190    /// future benchmarks.
191    pub paths: Vec<String>,
192    /// Hard cap on the number of files edited in one call (defaults to
193    /// [`crate::fs::ast_edit::DEFAULT_MAX_FILES`]).
194    #[serde(default)]
195    pub max_files: Option<usize>,
196}
197
198/// Parameters for file rollback operations.
199#[derive(Debug, Default, Deserialize, JsonSchema)]
200pub struct FsRollbackInput {
201    pub path: String,
202    pub hash: String,
203}
204
205/// Configuration for file rollback operations.
206#[derive(Default)]
207pub struct FsRollback;
208
209impl FsMetadata {
210    /// Validate `path` against the project root, allowlist, and blocklist.
211    ///
212    /// Returns the canonicalized safe path on success, or a descriptive error
213    /// string explaining why the path was denied.
214    ///
215    /// Delegates to [`PathGuard`] for all validation and canonicalization logic.
216    pub(crate) fn fs_guard(&self, path: &str) -> Result<PathBuf, String> {
217        let guard = PathGuard::new(
218            &self.root,
219            self.allowlist.as_deref(),
220            self.blocklist.as_deref(),
221        );
222        guard.resolve(path)
223    }
224}