vtcode_core/tools/tool_intent/
mutation.rs1use std::path::{Component, Path, PathBuf};
4
5pub fn tmp_plans_dir() -> PathBuf {
10 std::env::temp_dir().join("vtcode-plans")
11}
12
13fn tmp_plans_root_normalized() -> String {
14 tmp_plans_dir().to_string_lossy().replace('\\', "/")
15}
16
17fn path_targets_plan_artifact(path: &str) -> bool {
18 let normalized = path.trim().replace('\\', "/");
19 if Path::new(&normalized).components().any(|part| part == Component::ParentDir) {
22 return false;
23 }
24 if normalized == ".vtcode/plans"
25 || normalized.starts_with(".vtcode/plans/")
26 || normalized.contains("/.vtcode/plans/")
27 {
28 return true;
29 }
30 let tmp_root = tmp_plans_root_normalized();
31 normalized == tmp_root
32 || normalized.starts_with(&format!("{tmp_root}/"))
33 || normalized.contains(&format!("{tmp_root}/"))
34}
35
36fn path_is_docs_only(path: &str) -> bool {
37 let normalized = path.trim().replace('\\', "/");
38 let lower = normalized.to_ascii_lowercase();
39 let file_name = lower.rsplit('/').next().unwrap_or_default();
40 if matches!(file_name, "cmakelists.txt" | "requirements.txt") {
41 return false;
42 }
43 let stem = file_name.split('.').next().unwrap_or_default();
46 if matches!(stem, "readme" | "changelog" | "license") {
47 let ext = file_name.rsplit('.').next().unwrap_or_default();
48 if ext == stem || matches!(ext, "md" | "mdx" | "txt" | "rst") {
49 return true;
50 }
51 }
52 matches!(file_name.rsplit('.').next(), Some("md" | "mdx" | "txt" | "rst"))
53}
54
55fn file_mutation_paths(name: &str, args: &serde_json::Value) -> Vec<PathBuf> {
56 let mut paths = crate::tools::apply_patch::mutation_target_paths(name, args);
57 if let Some(extra) = args.get("filePath").and_then(|value| value.as_str()) {
59 let trimmed = extra.trim();
60 if !trimmed.is_empty() {
61 paths.push(PathBuf::from(trimmed));
62 }
63 }
64 paths
65}
66
67pub fn is_docs_only_write(name: &str, args: &serde_json::Value) -> bool {
72 use crate::config::constants::tools as tool_names;
73 use crate::tools::names::canonical_tool_name;
74 use crate::tools::tool_intent::file_operation_action;
75
76 let canonical = canonical_tool_name(name);
77 let is_file_write = matches!(
78 canonical,
79 tool_names::APPLY_PATCH
80 | tool_names::WRITE_FILE
81 | tool_names::EDIT_FILE
82 | tool_names::CREATE_FILE
83 | tool_names::SEARCH_REPLACE
84 | tool_names::DELETE_FILE
85 | tool_names::MOVE_FILE
86 | tool_names::COPY_FILE
87 | tool_names::UNIFIED_FILE
88 );
89 if !is_file_write {
90 return false;
91 }
92 if canonical == tool_names::UNIFIED_FILE
93 && file_operation_action(args)
94 .map(|action| action.eq_ignore_ascii_case("read"))
95 .unwrap_or(false)
96 {
97 return false;
98 }
99 let paths = file_mutation_paths(canonical, args);
100 if paths.is_empty() {
101 return false;
102 }
103 paths.iter().all(|path| path.to_str().is_some_and(path_is_docs_only))
104}
105
106pub fn is_plan_artifact_write(name: &str, args: &serde_json::Value) -> bool {
107 use crate::config::constants::tools as tool_names;
108 use crate::tools::names::canonical_tool_name;
109 use crate::tools::tool_intent::file_operation_action;
110
111 let canonical = canonical_tool_name(name);
112 if canonical == tool_names::TASK_TRACKER {
113 return true;
114 }
115 if !matches!(
116 canonical,
117 tool_names::UNIFIED_FILE
118 | tool_names::WRITE_FILE
119 | tool_names::EDIT_FILE
120 | tool_names::CREATE_FILE
121 | tool_names::SEARCH_REPLACE
122 ) || (canonical == tool_names::UNIFIED_FILE
123 && file_operation_action(args).is_some_and(|action| action.eq_ignore_ascii_case("read")))
124 {
125 return false;
126 }
127 let paths = file_mutation_paths(canonical, args);
128 !paths.is_empty() && paths.iter().all(|path| path.to_str().is_some_and(path_targets_plan_artifact))
129}