aft/hashline/integration/
schema.rs1use serde_json::{json, Value};
9
10use super::binding::effective_for_capture;
11use super::binding::BindingGuard;
12
13#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
15pub enum EditSchemaArm {
16 Legacy,
18 Hashline,
20}
21
22impl EditSchemaArm {
23 pub const fn as_str(self) -> &'static str {
24 match self {
25 Self::Legacy => "legacy",
26 Self::Hashline => "hashline",
27 }
28 }
29
30 pub const fn is_hashline(self) -> bool {
31 matches!(self, Self::Hashline)
32 }
33}
34
35pub fn select_edit_schema(effective: bool) -> EditSchemaArm {
37 if effective {
38 EditSchemaArm::Hashline
39 } else {
40 EditSchemaArm::Legacy
41 }
42}
43
44pub fn select_edit_schema_for_capture(guard: Option<&BindingGuard>) -> EditSchemaArm {
46 select_edit_schema(effective_for_capture(guard))
47}
48
49pub const LEGACY_EDIT_DESCRIPTION: &str = "Edit a file by finding and replacing text, or by targeting named symbols. To write or overwrite a whole file, use the `write` tool — `edit` requires an explicit edit mode and will not silently overwrite a file from `content` alone.";
51
52pub const HASHLINE_EDIT_DESCRIPTION: &str = "Apply a hashline patch. Arguments are exactly `{patch}` where `patch` is a non-empty string of one or more `[path#TAG]` sections with PUT/CUT/REM/MV operations. Paths and tags come from section headers; obtain tags from tagged reads. Server-owned preview control is outside this schema.";
54
55pub fn legacy_edit_schema() -> Value {
57 json!({
58 "$schema": "https://json-schema.org/draft/2020-12/schema",
59 "type": "object",
60 "properties": {
61 "filePath": {
62 "description": "Path to the file to edit (absolute or relative to project root)",
63 "type": "string"
64 },
65 "symbol": {
66 "description": "Named symbol to replace (function, class, type)",
67 "type": "string"
68 },
69 "content": {
70 "description": "Replacement content for symbol mode. For whole-file writes, use the `write` tool.",
71 "type": "string"
72 },
73 "appendContent": {
74 "description": "Text to append to the end of path; creates the file if needed",
75 "type": "string"
76 },
77 "edits": {
78 "description": "Batch edits — non-empty array of { oldString, newString }, { oldString, newString, replaceAll: true }, or { startLine, endLine, content } objects",
79 "minItems": 1,
80 "type": "array",
81 "items": {
82 "type": "object",
83 "properties": {
84 "oldString": {
85 "description": "Text to find for a batch find/replace edit",
86 "type": "string"
87 },
88 "newString": {
89 "description": "Replacement text for a batch find/replace edit",
90 "type": "string"
91 },
92 "replaceAll": {
93 "description": "Replace every occurrence for this batch item",
94 "type": "boolean"
95 },
96 "occurrence": {
97 "description": "1-based occurrence for this batch item (1 = first match)",
98 "type": "integer",
99 "minimum": 1
100 },
101 "startLine": {
102 "description": "1-based start line for a batch line-range edit",
103 "type": "integer",
104 "minimum": 1
105 },
106 "endLine": {
107 "description": "1-based end line for a batch line-range edit",
108 "type": "integer",
109 "minimum": 1
110 },
111 "content": {
112 "description": "Replacement text for a batch line-range edit",
113 "type": "string"
114 }
115 }
116 }
117 }
118 },
119 "required": ["filePath"],
120 "description": LEGACY_EDIT_DESCRIPTION
121 })
122}
123
124pub fn hashline_edit_schema() -> Value {
126 json!({
127 "$schema": "https://json-schema.org/draft/2020-12/schema",
128 "type": "object",
129 "additionalProperties": false,
130 "properties": {
131 "patch": {
132 "type": "string",
133 "minLength": 1,
134 "description": "Hashline patch text with one or more [path#TAG] sections and PUT/CUT/REM/MV operations"
135 }
136 },
137 "required": ["patch"],
138 "description": HASHLINE_EDIT_DESCRIPTION
139 })
140}
141
142pub fn edit_schema_for(arm: EditSchemaArm) -> Value {
144 match arm {
145 EditSchemaArm::Legacy => legacy_edit_schema(),
146 EditSchemaArm::Hashline => hashline_edit_schema(),
147 }
148}
149
150pub fn edit_description_for(arm: EditSchemaArm) -> &'static str {
152 match arm {
153 EditSchemaArm::Legacy => LEGACY_EDIT_DESCRIPTION,
154 EditSchemaArm::Hashline => HASHLINE_EDIT_DESCRIPTION,
155 }
156}
157
158pub const HASHLINE_EDIT_COMMAND: &str = "hashline_edit";
160
161pub const HASHLINE_PREFLIGHT_COMMAND: &str = "hashline_preflight";
163
164#[derive(Clone, Debug, Eq, PartialEq)]
166pub struct GovernedEditManifestEntry {
167 pub name: &'static str,
168 pub arm: EditSchemaArm,
169 pub description: &'static str,
170 pub schema: Value,
171 pub supports_tool: bool,
172 pub hoisted: bool,
173 pub lane: &'static str,
174}
175
176pub fn governed_edit_manifest_entry(arm: EditSchemaArm) -> GovernedEditManifestEntry {
178 GovernedEditManifestEntry {
179 name: "edit",
180 arm,
181 description: edit_description_for(arm),
182 schema: edit_schema_for(arm),
183 supports_tool: true,
184 hoisted: true,
185 lane: "mutation",
186 }
187}
188
189pub fn regenerate_governed_edit_artifacts() -> Value {
195 let legacy = governed_edit_manifest_entry(EditSchemaArm::Legacy);
196 let hashline = governed_edit_manifest_entry(EditSchemaArm::Hashline);
197 json!({
198 "tool": "edit",
199 "dual_mode": true,
200 "selection": "session_effective_hashline",
201 "arms": {
202 "legacy": {
203 "arm": legacy.arm.as_str(),
204 "description": legacy.description,
205 "schema": legacy.schema,
206 "supports_tool": legacy.supports_tool,
207 "hoisted": legacy.hoisted,
208 "lane": legacy.lane,
209 "command": "edit",
210 },
211 "hashline": {
212 "arm": hashline.arm.as_str(),
213 "description": hashline.description,
214 "schema": hashline.schema,
215 "supports_tool": hashline.supports_tool,
216 "hoisted": hashline.hoisted,
217 "lane": hashline.lane,
218 "command": HASHLINE_EDIT_COMMAND,
219 "preflight_command": HASHLINE_PREFLIGHT_COMMAND,
220 }
221 },
222 "invariant": "a session never exposes both edit schemas",
223 })
224}
225
226pub fn translate_gate_on_edit(
231 arguments: &Value,
232) -> Result<GateOnTranslation, crate::hashline::syntax::HashlineRejection> {
233 let request = crate::hashline::syntax::validate_raw_arguments(arguments)?;
234 Ok(GateOnTranslation {
235 command: HASHLINE_EDIT_COMMAND,
236 patch: request.patch,
237 })
238}
239
240#[derive(Clone, Debug, Eq, PartialEq)]
242pub struct GateOnTranslation {
243 pub command: &'static str,
244 pub patch: String,
245}
246
247impl GateOnTranslation {
248 pub fn to_native_args(&self) -> Value {
249 json!({ "patch": self.patch })
250 }
251}
252
253pub fn translate_edit_for_session(
259 guard: Option<&BindingGuard>,
260 arguments: &Value,
261) -> Result<Option<GateOnTranslation>, crate::hashline::syntax::HashlineRejection> {
262 if !effective_for_capture(guard) {
263 return Ok(None);
264 }
265 Ok(Some(translate_gate_on_edit(arguments)?))
266}