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}