use serde_json::{json, Value};
use super::binding::effective_for_capture;
use super::binding::BindingGuard;
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
pub enum EditSchemaArm {
Legacy,
Hashline,
}
impl EditSchemaArm {
pub const fn as_str(self) -> &'static str {
match self {
Self::Legacy => "legacy",
Self::Hashline => "hashline",
}
}
pub const fn is_hashline(self) -> bool {
matches!(self, Self::Hashline)
}
}
pub fn select_edit_schema(effective: bool) -> EditSchemaArm {
if effective {
EditSchemaArm::Hashline
} else {
EditSchemaArm::Legacy
}
}
pub fn select_edit_schema_for_capture(guard: Option<&BindingGuard>) -> EditSchemaArm {
select_edit_schema(effective_for_capture(guard))
}
pub 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.";
pub 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.";
pub fn legacy_edit_schema() -> Value {
json!({
"$schema": "https://json-schema.org/draft/2020-12/schema",
"type": "object",
"properties": {
"filePath": {
"description": "Path to the file to edit (absolute or relative to project root)",
"type": "string"
},
"symbol": {
"description": "Named symbol to replace (function, class, type)",
"type": "string"
},
"content": {
"description": "Replacement content for symbol mode. For whole-file writes, use the `write` tool.",
"type": "string"
},
"appendContent": {
"description": "Text to append to the end of path; creates the file if needed",
"type": "string"
},
"edits": {
"description": "Batch edits — non-empty array of { oldString, newString }, { oldString, newString, replaceAll: true }, or { startLine, endLine, content } objects",
"minItems": 1,
"type": "array",
"items": {
"type": "object",
"properties": {
"oldString": {
"description": "Text to find for a batch find/replace edit",
"type": "string"
},
"newString": {
"description": "Replacement text for a batch find/replace edit",
"type": "string"
},
"replaceAll": {
"description": "Replace every occurrence for this batch item",
"type": "boolean"
},
"occurrence": {
"description": "1-based occurrence for this batch item (1 = first match)",
"type": "integer",
"minimum": 1
},
"startLine": {
"description": "1-based start line for a batch line-range edit",
"type": "integer",
"minimum": 1
},
"endLine": {
"description": "1-based end line for a batch line-range edit",
"type": "integer",
"minimum": 1
},
"content": {
"description": "Replacement text for a batch line-range edit",
"type": "string"
}
}
}
}
},
"required": ["filePath"],
"description": LEGACY_EDIT_DESCRIPTION
})
}
pub fn hashline_edit_schema() -> Value {
json!({
"$schema": "https://json-schema.org/draft/2020-12/schema",
"type": "object",
"additionalProperties": false,
"properties": {
"patch": {
"type": "string",
"minLength": 1,
"description": "Hashline patch text with one or more [path#TAG] sections and PUT/CUT/REM/MV operations"
}
},
"required": ["patch"],
"description": HASHLINE_EDIT_DESCRIPTION
})
}
pub fn edit_schema_for(arm: EditSchemaArm) -> Value {
match arm {
EditSchemaArm::Legacy => legacy_edit_schema(),
EditSchemaArm::Hashline => hashline_edit_schema(),
}
}
pub fn edit_description_for(arm: EditSchemaArm) -> &'static str {
match arm {
EditSchemaArm::Legacy => LEGACY_EDIT_DESCRIPTION,
EditSchemaArm::Hashline => HASHLINE_EDIT_DESCRIPTION,
}
}
pub const HASHLINE_EDIT_COMMAND: &str = "hashline_edit";
pub const HASHLINE_PREFLIGHT_COMMAND: &str = "hashline_preflight";
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct GovernedEditManifestEntry {
pub name: &'static str,
pub arm: EditSchemaArm,
pub description: &'static str,
pub schema: Value,
pub supports_tool: bool,
pub hoisted: bool,
pub lane: &'static str,
}
pub fn governed_edit_manifest_entry(arm: EditSchemaArm) -> GovernedEditManifestEntry {
GovernedEditManifestEntry {
name: "edit",
arm,
description: edit_description_for(arm),
schema: edit_schema_for(arm),
supports_tool: true,
hoisted: true,
lane: "mutation",
}
}
pub fn regenerate_governed_edit_artifacts() -> Value {
let legacy = governed_edit_manifest_entry(EditSchemaArm::Legacy);
let hashline = governed_edit_manifest_entry(EditSchemaArm::Hashline);
json!({
"tool": "edit",
"dual_mode": true,
"selection": "session_effective_hashline",
"arms": {
"legacy": {
"arm": legacy.arm.as_str(),
"description": legacy.description,
"schema": legacy.schema,
"supports_tool": legacy.supports_tool,
"hoisted": legacy.hoisted,
"lane": legacy.lane,
"command": "edit",
},
"hashline": {
"arm": hashline.arm.as_str(),
"description": hashline.description,
"schema": hashline.schema,
"supports_tool": hashline.supports_tool,
"hoisted": hashline.hoisted,
"lane": hashline.lane,
"command": HASHLINE_EDIT_COMMAND,
"preflight_command": HASHLINE_PREFLIGHT_COMMAND,
}
},
"invariant": "a session never exposes both edit schemas",
})
}
pub fn translate_gate_on_edit(
arguments: &Value,
) -> Result<GateOnTranslation, crate::hashline::syntax::HashlineRejection> {
let request = crate::hashline::syntax::validate_raw_arguments(arguments)?;
Ok(GateOnTranslation {
command: HASHLINE_EDIT_COMMAND,
patch: request.patch,
})
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct GateOnTranslation {
pub command: &'static str,
pub patch: String,
}
impl GateOnTranslation {
pub fn to_native_args(&self) -> Value {
json!({ "patch": self.patch })
}
}
pub fn translate_edit_for_session(
guard: Option<&BindingGuard>,
arguments: &Value,
) -> Result<Option<GateOnTranslation>, crate::hashline::syntax::HashlineRejection> {
if !effective_for_capture(guard) {
return Ok(None);
}
Ok(Some(translate_gate_on_edit(arguments)?))
}