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 = concat!(
54 "Apply a hashline patch. Arguments are exactly `{patch}` where `patch` is a non-empty string. ",
55 "Server-owned preview control is outside this schema.\n\n",
56 "Quick reference:\n",
57 "- Header: `[path#TAG]`; TAG is exactly four hexadecimal digits from a current tagged read. ",
58 "Read every addressed row and gap boundary; REM and MV require a whole-file tagged read. ",
59 "Re-read after an edit before chaining: an edit-response tag can retain only changed context.\n",
60 "- Same canonical path: multiple sections compose in patch order against pre-request coordinates.\n",
61 "- Addresses: `0` (BOF), `N` (one line), `N.=M` (range; `N..=M`/`N..M` also work), ",
62 "`<N`/`>N` (gap before/after), `N*`/`<N*`/`>N*` (block), and `$`/`$-K` (EOF-relative). ",
63 "A plain `N` PUT replaces; use `<N` or `>N` to insert.\n",
64 "- PUT text: `PUT <address>:` followed by one or more `+` body rows (`+` alone is blank). ",
65 "A final patch newline is allowed. PUT without `:` copies `@name` (or the anonymous register) and takes no body; names use `@` plus ASCII letters, digits, `_`, or `-`.\n",
66 "- CUT: `CUT <address> [@name]`. REM: bare `REM` only, removing the whole file. ",
67 "MV: `MV <destination>` (one whitespace-free path, optional matching quotes), once and after any line operations. ",
68 "`*** Begin Patch`/`*** End Patch` is an optional envelope.\n",
69 "- Only `read` (and accepted AFT `cat`/`head`/`tail` rewrites) mint hashline tags. ",
70 "`aft_zoom`, `aft_outline`, `grep`, `aft_search`, and conflict snippets do not. ",
71 "After navigation, call `read` on every file and range the patch addresses."
72);
73
74pub fn legacy_edit_schema() -> Value {
76 json!({
77 "$schema": "https://json-schema.org/draft/2020-12/schema",
78 "type": "object",
79 "properties": {
80 "filePath": {
81 "description": "Path to the file to edit (absolute or relative to project root)",
82 "type": "string"
83 },
84 "symbol": {
85 "description": "Named symbol to replace (function, class, type)",
86 "type": "string"
87 },
88 "content": {
89 "description": "Replacement content for symbol mode. For whole-file writes, use the `write` tool.",
90 "type": "string"
91 },
92 "appendContent": {
93 "description": "Text to append to the end of path; creates the file if needed",
94 "type": "string"
95 },
96 "edits": {
97 "description": "Batch edits — non-empty array of { oldString, newString }, { oldString, newString, replaceAll: true }, or { startLine, endLine, content } objects",
98 "minItems": 1,
99 "type": "array",
100 "items": {
101 "type": "object",
102 "properties": {
103 "oldString": {
104 "description": "Text to find for a batch find/replace edit",
105 "type": "string"
106 },
107 "newString": {
108 "description": "Replacement text for a batch find/replace edit",
109 "type": "string"
110 },
111 "replaceAll": {
112 "description": "Replace every occurrence for this batch item",
113 "type": "boolean"
114 },
115 "occurrence": {
116 "description": "1-based occurrence for this batch item (1 = first match)",
117 "type": "integer",
118 "minimum": 1
119 },
120 "startLine": {
121 "description": "1-based start line for a batch line-range edit",
122 "type": "integer",
123 "minimum": 1
124 },
125 "endLine": {
126 "description": "1-based end line for a batch line-range edit",
127 "type": "integer",
128 "minimum": 1
129 },
130 "content": {
131 "description": "Replacement text for a batch line-range edit",
132 "type": "string"
133 }
134 }
135 }
136 }
137 },
138 "required": ["filePath"],
139 "description": LEGACY_EDIT_DESCRIPTION
140 })
141}
142
143pub fn hashline_edit_schema() -> Value {
145 json!({
146 "$schema": "https://json-schema.org/draft/2020-12/schema",
147 "type": "object",
148 "additionalProperties": false,
149 "properties": {
150 "patch": {
151 "type": "string",
152 "minLength": 1,
153 "description": "Hashline patch text with one or more [path#TAG] sections and PUT/CUT/REM/MV operations"
154 }
155 },
156 "required": ["patch"],
157 "description": HASHLINE_EDIT_DESCRIPTION
158 })
159}
160
161pub fn edit_schema_for(arm: EditSchemaArm) -> Value {
163 match arm {
164 EditSchemaArm::Legacy => legacy_edit_schema(),
165 EditSchemaArm::Hashline => hashline_edit_schema(),
166 }
167}
168
169pub fn edit_description_for(arm: EditSchemaArm) -> &'static str {
171 match arm {
172 EditSchemaArm::Legacy => LEGACY_EDIT_DESCRIPTION,
173 EditSchemaArm::Hashline => HASHLINE_EDIT_DESCRIPTION,
174 }
175}
176
177pub const HASHLINE_EDIT_COMMAND: &str = "hashline_edit";
179
180pub const HASHLINE_PREFLIGHT_COMMAND: &str = "hashline_preflight";
182
183#[derive(Clone, Debug, Eq, PartialEq)]
185pub struct GovernedEditManifestEntry {
186 pub name: &'static str,
187 pub arm: EditSchemaArm,
188 pub description: &'static str,
189 pub schema: Value,
190 pub supports_tool: bool,
191 pub hoisted: bool,
192 pub lane: &'static str,
193}
194
195pub fn governed_edit_manifest_entry(arm: EditSchemaArm) -> GovernedEditManifestEntry {
197 GovernedEditManifestEntry {
198 name: "edit",
199 arm,
200 description: edit_description_for(arm),
201 schema: edit_schema_for(arm),
202 supports_tool: true,
203 hoisted: true,
204 lane: "mutation",
205 }
206}
207
208pub fn regenerate_governed_edit_artifacts() -> Value {
214 let legacy = governed_edit_manifest_entry(EditSchemaArm::Legacy);
215 let hashline = governed_edit_manifest_entry(EditSchemaArm::Hashline);
216 json!({
217 "tool": "edit",
218 "dual_mode": true,
219 "selection": "session_effective_hashline",
220 "arms": {
221 "legacy": {
222 "arm": legacy.arm.as_str(),
223 "description": legacy.description,
224 "schema": legacy.schema,
225 "supports_tool": legacy.supports_tool,
226 "hoisted": legacy.hoisted,
227 "lane": legacy.lane,
228 "command": "edit",
229 },
230 "hashline": {
231 "arm": hashline.arm.as_str(),
232 "description": hashline.description,
233 "schema": hashline.schema,
234 "supports_tool": hashline.supports_tool,
235 "hoisted": hashline.hoisted,
236 "lane": hashline.lane,
237 "command": HASHLINE_EDIT_COMMAND,
238 "preflight_command": HASHLINE_PREFLIGHT_COMMAND,
239 }
240 },
241 "invariant": "a session never exposes both edit schemas",
242 })
243}
244
245pub fn translate_gate_on_edit(
250 arguments: &Value,
251) -> Result<GateOnTranslation, crate::hashline::syntax::HashlineRejection> {
252 let request = crate::hashline::syntax::validate_raw_arguments(arguments)?;
253 Ok(GateOnTranslation {
254 command: HASHLINE_EDIT_COMMAND,
255 patch: request.patch,
256 })
257}
258
259#[derive(Clone, Debug, Eq, PartialEq)]
261pub struct GateOnTranslation {
262 pub command: &'static str,
263 pub patch: String,
264}
265
266impl GateOnTranslation {
267 pub fn to_native_args(&self) -> Value {
268 json!({ "patch": self.patch })
269 }
270}
271
272pub fn translate_edit_for_session(
278 guard: Option<&BindingGuard>,
279 arguments: &Value,
280) -> Result<Option<GateOnTranslation>, crate::hashline::syntax::HashlineRejection> {
281 if !effective_for_capture(guard) {
282 return Ok(None);
283 }
284 Ok(Some(translate_gate_on_edit(arguments)?))
285}