newt_core/
scope_grounding.rs1const DEF_KEYWORDS: &[&str] = &[
17 "function", "fn", "struct", "trait", "enum", "method", "type", "module", "mod", "macro",
18];
19
20fn looks_like_identifier(t: &str) -> bool {
23 let t = t.trim_matches('`');
24 !t.is_empty()
25 && t.chars().all(|c| c.is_ascii_alphanumeric() || c == '_')
26 && t.chars()
27 .next()
28 .is_some_and(|c| c.is_ascii_alphabetic() || c == '_')
29 && (t.contains('_')
30 || t.contains(|c: char| c.is_ascii_digit())
31 || (t.chars().any(|c| c.is_ascii_uppercase())
32 && t.chars().any(|c| c.is_ascii_lowercase())))
33}
34
35pub fn grep_terms(text: &str) -> Vec<String> {
41 let mut terms: Vec<String> = Vec::new();
42 let mut rest = text;
44 while let Some(open) = rest.find('`') {
45 rest = &rest[open + 1..];
46 let Some(close) = rest.find('`') else { break };
47 let span = rest[..close].trim();
48 if (3..=40).contains(&span.chars().count())
49 && span.split_whitespace().count() == 1
50 && span.chars().any(|c| c.is_ascii_alphanumeric())
51 {
52 terms.push(span.to_string());
53 }
54 rest = &rest[close + 1..];
55 }
56 for tok in text.split(|c: char| c.is_whitespace() || "()[]{}<>,;:\"'".contains(c)) {
58 if let Some(after) = tok.strip_prefix('/') {
59 let cmd: String = after
60 .chars()
61 .take_while(|c| c.is_ascii_alphanumeric() || *c == '_' || *c == '-')
62 .collect();
63 if cmd.len() >= 2 {
64 terms.push(format!("/{cmd}"));
65 }
66 }
67 }
68 let words: Vec<&str> = text
73 .split(|c: char| c.is_whitespace() || "()[]{}<>,;:\"'`".contains(c))
74 .collect();
75 for (i, w) in words.iter().enumerate() {
76 if DEF_KEYWORDS.contains(&w.to_ascii_lowercase().as_str()) {
77 let prev = i.checked_sub(1).and_then(|j| words.get(j));
78 let next = words.get(i + 1);
79 for cand in [prev, next].into_iter().flatten() {
80 if looks_like_identifier(cand) {
81 terms.push((*cand).to_string());
82 }
83 }
84 }
85 }
86 terms.sort();
87 terms.dedup();
88 terms.truncate(12);
89 terms
90}
91
92pub fn ere_escape(s: &str) -> String {
95 let mut out = String::with_capacity(s.len());
96 for c in s.chars() {
97 if "\\.^$*+?()[]{}|".contains(c) {
98 out.push('\\');
99 }
100 out.push(c);
101 }
102 out
103}
104
105pub fn definition_grep_pattern(term: &str) -> String {
110 format!(
111 "^[[:space:]]*(pub[[:space:]]+)?(async[[:space:]]+)?(unsafe[[:space:]]+)?\
112 (fn|struct|trait|enum|type|const|static|union|mod)[[:space:]]+{}([^A-Za-z0-9_]|$)",
113 ere_escape(term)
114 )
115}
116
117pub const AMBIGUOUS_DEF_FILES: usize = 3;
121
122pub const SCOPE_CAP: usize = 6;
124
125pub fn derive_subtask_scope(
133 instruction: &str,
134 def_sites: &impl Fn(&str) -> Vec<String>,
135) -> Vec<String> {
136 let mut paths: Vec<String> = Vec::new();
137 for term in grep_terms(instruction) {
138 let mut term_files: Vec<String> = Vec::new();
139 for hit in def_sites(&term) {
140 if let Some(p) = hit.split(':').next() {
141 if !p.is_empty() && !p.starts_with('"') && !term_files.iter().any(|q| q == p) {
142 term_files.push(p.to_string());
143 }
144 }
145 }
146 if term_files.is_empty() || term_files.len() > AMBIGUOUS_DEF_FILES {
147 continue;
148 }
149 for p in term_files {
150 if !paths.iter().any(|q| q == &p) {
151 paths.push(p);
152 }
153 }
154 }
155 paths
156}
157
158pub fn ground_scope(
168 instruction: &str,
169 declared: &[String],
170 def_sites: &impl Fn(&str) -> Vec<String>,
171) -> Vec<String> {
172 let mut scope = derive_subtask_scope(instruction, def_sites);
173 for d in declared {
174 let d = d.trim();
175 if !d.is_empty() && !scope.iter().any(|q| q == d) {
176 scope.push(d.to_string());
177 }
178 }
179 scope.truncate(SCOPE_CAP);
180 scope
181}
182
183#[cfg(test)]
184mod tests {
185 use super::*;
186
187 #[test]
188 fn grep_terms_extracts_slash_commands_and_backtick_tokens_only() {
189 let terms =
190 grep_terms("roll up `/dgx` help; refactor `help_lines` and /models — but not help");
191 assert!(terms.contains(&"/dgx".to_string()), "{terms:?}");
192 assert!(terms.contains(&"/models".to_string()), "{terms:?}");
193 assert!(terms.contains(&"help_lines".to_string()), "{terms:?}");
194 assert!(!terms.iter().any(|t| t == "help"), "{terms:?}");
196 }
197
198 #[test]
199 fn grep_terms_extracts_unquoted_identifiers_next_to_def_keywords() {
200 let t1 = grep_terms("Refactor the help_lines function in newt-cli/src/crew.rs");
202 assert!(t1.contains(&"help_lines".to_string()), "{t1:?}");
203 let t2 = grep_terms("add a new struct ConfigLoader to hold settings");
204 assert!(t2.contains(&"ConfigLoader".to_string()), "{t2:?}");
205 let t3 = grep_terms("this function works well and the parser is fine");
207 assert!(
208 !t3.iter()
209 .any(|x| x == "works" || x == "parser" || x == "the"),
210 "{t3:?}"
211 );
212 }
213
214 #[test]
215 fn definition_grep_pattern_matches_defs_not_mentions() {
216 let re = regex::Regex::new(&definition_grep_pattern("help_lines")).unwrap();
217 assert!(re.is_match("fn help_lines() -> &'static [&'static str] {"));
218 assert!(re.is_match(" pub async fn help_lines() {"));
219 assert!(re.is_match("pub fn help_lines<T>(x: T) {"));
220 assert!(!re.is_match(" // help_lines is the seam"));
221 assert!(!re.is_match(" let v = self.help_lines();"));
222 assert!(!re.is_match("fn help_lines_other() {")); }
224
225 #[test]
226 fn derive_subtask_scope_extracts_def_files_and_skips_ambiguous_terms() {
227 let def_sites = |sym: &str| -> Vec<String> {
233 match sym {
234 "humanize_duration" => vec![
235 "src/util.rs:2:pub fn humanize_duration(...)".to_string(),
236 "src/util.rs:9:fn humanize_duration_impl(...)".to_string(),
237 "src/lib.rs:11:fn humanizes()".to_string(),
238 "\"src/gr\\303\\266\\303\\237e.rs\":1:fn humanize_x()".to_string(),
239 ],
240 "main" => (0..14).map(|i| format!("file{i}.rs:1:fn main()")).collect(),
241 _ => Vec::new(),
242 }
243 };
244 let scope = derive_subtask_scope(
245 "fix `humanize_duration` in `main` so 90s renders as 1m 30s",
246 &def_sites,
247 );
248 assert_eq!(
249 scope,
250 vec!["src/util.rs".to_string(), "src/lib.rs".to_string()],
251 "defining files kept; ambiguous `main` skipped; quoted path dropped"
252 );
253 assert!(derive_subtask_scope("tidy the docs", &def_sites).is_empty());
255 }
256
257 #[test]
258 fn ground_scope_unions_def_sites_with_declared_and_caps() {
259 let def_sites = |sym: &str| -> Vec<String> {
266 if sym == "humanize_duration" {
267 vec!["src/util.rs:2:pub fn humanize_duration".to_string()]
268 } else {
269 Vec::new()
270 }
271 };
272 let scope = ground_scope(
273 "Fix `humanize_duration` in the util module",
274 &["tests/util_test.rs".to_string(), "src/util.rs".to_string()],
275 &def_sites,
276 );
277 assert_eq!(
278 scope,
279 vec!["src/util.rs".to_string(), "tests/util_test.rs".to_string()],
280 "derived seam leads; declared companion appended; dup deduped"
281 );
282 let scope2 = ground_scope(
283 "Create the brand-new reporting module",
284 &["src/report.rs".to_string()],
285 &def_sites,
286 );
287 assert_eq!(
288 scope2,
289 vec!["src/report.rs".to_string()],
290 "no def-site found → the declared file survives alone"
291 );
292 }
293}