1use std::fmt::Write;
2use std::path::{Path, PathBuf};
3use std::sync::Arc;
4
5use rayon::prelude::*;
6use rustc_hash::{FxHashMap, FxHashSet};
7use similar::{ChangeTag, TextDiff};
8
9use crate::config::budget::BUDGET;
10use crate::config::limits::LIMITS;
11use crate::config::tokenization::TOKENIZATION;
12use crate::core::{compute_seed_weights, identify_core_fragments};
13use crate::edges;
14use crate::mode::{PipelineConfig, ScoringMode};
15use crate::parsers::fragment_file;
16use crate::render::{DiffContextOutput, build_diff_context_output};
17use crate::scoring::create_scoring_strategy;
18use crate::signatures::generate_signature_variants;
19use crate::tokenizer::count_tokens;
20use crate::types::{DiffHunk, Fragment, FragmentId};
21
22pub struct MemoryRepo {
23 pub name: String,
24 pub initial_files: FxHashMap<String, String>,
25 pub changed_files: FxHashMap<String, String>,
26}
27
28pub fn build_diff_context_in_memory(
29 repo: &MemoryRepo,
30 budget_tokens: Option<u32>,
31 _alpha: f64,
32 tau: f64,
33 no_content: bool,
34 scoring_mode: ScoringMode,
35) -> DiffContextOutput {
36 let hunks = compute_memory_hunks(&repo.initial_files, &repo.changed_files);
37 if hunks.is_empty() {
38 return empty_output(&repo.name);
39 }
40
41 let diff_text = compute_memory_diff_text(&repo.initial_files, &repo.changed_files);
42 let all_files = merge_file_contents(&repo.initial_files, &repo.changed_files);
43
44 let changed_paths: FxHashSet<String> =
45 hunks.iter().map(|h| h.path.as_ref().to_string()).collect();
46
47 let changed_file_paths: Vec<PathBuf> = changed_paths.iter().map(PathBuf::from).collect();
48 let all_file_paths: Vec<PathBuf> = all_files.keys().map(PathBuf::from).collect();
49 let file_cache: FxHashMap<PathBuf, String> = all_files
50 .iter()
51 .map(|(k, v)| (PathBuf::from(k), v.clone()))
52 .collect();
53
54 let discovered = edges::discover_all_related_files(
55 &changed_file_paths,
56 &all_file_paths,
57 None,
58 Some(&file_cache),
59 );
60 let discovered_paths: FxHashSet<String> = discovered
61 .iter()
62 .map(|p| p.to_string_lossy().to_string())
63 .collect();
64
65 let allowed_paths: FxHashSet<&str> = changed_paths
66 .iter()
67 .chain(discovered_paths.iter())
68 .map(|s| s.as_str())
69 .collect();
70
71 let mut all_fragments: Vec<Fragment> = Vec::new();
72 let mut seen: FxHashSet<FragmentId> = FxHashSet::default();
73 for (path, content) in &all_files {
74 if !allowed_paths.contains(path.as_str()) {
75 continue;
76 }
77 let path_arc: Arc<str> = Arc::from(path.as_str());
78 let frags = fragment_file(path_arc, content);
79 for f in frags {
80 if seen.insert(f.id.clone()) {
81 all_fragments.push(f);
82 }
83 }
84 }
85
86 all_fragments.par_iter_mut().for_each(|f| {
87 f.token_count = count_tokens(&f.content) + LIMITS.overhead_per_fragment;
88 });
89
90 let core_ids = identify_core_fragments(&hunks, &all_fragments);
91
92 let mut core_excerpts =
99 crate::excerpt::generate_core_excerpts(&all_fragments, &core_ids, &hunks);
100 core_excerpts.par_iter_mut().for_each(|(_, f)| {
101 f.token_count = count_tokens(&f.content) + LIMITS.overhead_per_fragment;
102 });
103
104 let mut sig_frags = generate_signature_variants(&all_fragments);
105 sig_frags.par_iter_mut().for_each(|f| {
106 f.token_count = count_tokens(&f.content) + LIMITS.overhead_per_fragment;
107 });
108 all_fragments.extend(sig_frags);
109
110 let effective_budget = budget_tokens.unwrap_or(BUDGET.unlimited);
111 let config = PipelineConfig::from_mode(scoring_mode);
112 let seed_weights = compute_seed_weights(&hunks, &core_ids, &all_fragments);
113
114 let discovered_arc: FxHashSet<Arc<str>> = discovered_paths
115 .iter()
116 .map(|s| Arc::from(s.as_str()))
117 .collect();
118
119 let strategy = create_scoring_strategy(&config);
120
121 let scoring_result = strategy.score_and_filter(
122 &all_fragments,
123 &core_ids,
124 &hunks,
125 None,
126 Some(&seed_weights),
127 Some(&discovered_arc),
128 crate::deadline::Deadline::none(),
131 );
132
133 let needs = crate::utility::needs::needs_from_diff(&all_fragments, &core_ids, &diff_text);
134
135 let file_importance =
136 crate::utility::compute_file_importance(&scoring_result.filtered_fragments);
137 let selection = crate::select::lazy_greedy_select(
138 scoring_result.filtered_fragments.clone(),
139 &core_ids,
140 &scoring_result.rel_scores,
141 &needs,
142 effective_budget,
143 tau,
144 Some(&file_importance),
145 Some(&core_excerpts),
146 scoring_result.admissible_files.as_ref(),
147 );
148
149 let mut selected = selection.selected;
150
151 crate::postpass::coherence_post_pass(
152 &mut selected,
153 &scoring_result.filtered_fragments,
154 &scoring_result.graph,
155 effective_budget,
156 );
157
158 crate::postpass::rescue_nontrivial_context(
159 &mut selected,
160 &all_fragments,
161 &scoring_result.rel_scores,
162 &core_ids,
163 effective_budget,
164 );
165
166 let used: u32 = selected.iter().map(|f| f.token_count).sum();
167 let remaining = effective_budget.saturating_sub(used);
168 let changed_files: Vec<PathBuf> = changed_paths.iter().map(PathBuf::from).collect();
169 crate::postpass::ensure_changed_files_represented(
170 &mut selected,
171 &all_fragments,
172 &changed_files,
173 remaining,
174 Path::new("."),
175 &[],
176 None,
177 &core_ids,
178 &FxHashMap::default(),
179 );
180
181 let dummy_root = Path::new(".");
182 let mut changed_list: Vec<String> = changed_paths.iter().cloned().collect();
183 changed_list.sort();
184 let change = crate::render::ChangeSummary {
185 lockfile_changes: Vec::new(),
186 ignored_changes: Vec::new(),
187 policy_excluded_count: 0,
188 commit_message: None,
189 changed_files: changed_list,
190 deleted_files: Vec::new(),
191 renamed_files: Vec::new(),
192 };
193 build_diff_context_output(
194 dummy_root,
195 &selected,
196 no_content,
197 &core_ids,
198 &scoring_result.rel_scores,
199 change,
200 )
201}
202
203fn compute_memory_hunks(
204 initial: &FxHashMap<String, String>,
205 changed: &FxHashMap<String, String>,
206) -> Vec<DiffHunk> {
207 let mut hunks = Vec::new();
208
209 for (path, new_content) in changed {
210 let old_content = initial.get(path).map(|s| s.as_str()).unwrap_or("");
211 if old_content == new_content {
212 continue;
213 }
214 let path_arc: Arc<str> = Arc::from(path.as_str());
215 let file_hunks = diff_to_hunks(&path_arc, old_content, new_content);
216 hunks.extend(file_hunks);
217 }
218
219 for (path, _old_content) in initial {
220 if !changed.contains_key(path) {
221 let path_arc: Arc<str> = Arc::from(path.as_str());
222 let old_line_count = initial[path].lines().count() as u32;
223 if old_line_count > 0 {
224 hunks.push(DiffHunk {
225 path: path_arc,
226 new_start: 1,
227 new_len: 0,
228 old_start: 1,
229 old_len: old_line_count,
230 });
231 }
232 }
233 }
234
235 hunks
236}
237
238fn diff_to_hunks(path: &Arc<str>, old: &str, new: &str) -> Vec<DiffHunk> {
239 let diff = TextDiff::from_lines(old, new);
240 let mut hunks = Vec::new();
241
242 let mut new_line: u32 = 0;
243 let mut old_line: u32 = 0;
244
245 let mut hunk_new_start: Option<u32> = None;
246 let mut hunk_new_len: u32 = 0;
247 let mut hunk_old_start: u32 = 0;
248 let mut hunk_old_len: u32 = 0;
249
250 for change in diff.iter_all_changes() {
251 match change.tag() {
252 ChangeTag::Equal => {
253 if let Some(start) = hunk_new_start.take() {
254 hunks.push(DiffHunk {
255 path: Arc::clone(path),
256 new_start: start,
257 new_len: hunk_new_len,
258 old_start: hunk_old_start,
259 old_len: hunk_old_len,
260 });
261 hunk_new_len = 0;
262 hunk_old_len = 0;
263 }
264 new_line += 1;
265 old_line += 1;
266 }
267 ChangeTag::Delete => {
268 if hunk_new_start.is_none() {
269 hunk_new_start = Some(new_line + 1);
270 hunk_old_start = old_line + 1;
271 }
272 hunk_old_len += 1;
273 old_line += 1;
274 }
275 ChangeTag::Insert => {
276 if hunk_new_start.is_none() {
277 hunk_new_start = Some(new_line + 1);
278 hunk_old_start = old_line + 1;
279 }
280 hunk_new_len += 1;
281 new_line += 1;
282 }
283 }
284 }
285
286 if let Some(start) = hunk_new_start {
287 hunks.push(DiffHunk {
288 path: Arc::clone(path),
289 new_start: start,
290 new_len: hunk_new_len,
291 old_start: hunk_old_start,
292 old_len: hunk_old_len,
293 });
294 }
295
296 hunks
297}
298
299fn compute_memory_diff_text(
300 initial: &FxHashMap<String, String>,
301 changed: &FxHashMap<String, String>,
302) -> String {
303 let mut result = String::new();
304
305 let mut paths: Vec<&String> = changed.keys().collect();
306 paths.sort();
307
308 for path in paths {
309 let new_content = &changed[path];
310 let old_content = initial.get(path).map(|s| s.as_str()).unwrap_or("");
311 if old_content == new_content {
312 continue;
313 }
314
315 let diff = TextDiff::from_lines(old_content, new_content);
316 let mut udiff = diff.unified_diff();
317 let formatted = udiff
318 .context_radius(TOKENIZATION.diff_context_radius)
319 .header(&format!("a/{path}"), &format!("b/{path}"));
320 let _ = write!(result, "{formatted}");
321 }
322
323 let mut deleted_paths: Vec<&String> = initial
324 .keys()
325 .filter(|p| !changed.contains_key(*p))
326 .collect();
327 deleted_paths.sort();
328
329 for path in deleted_paths {
330 let old_content = &initial[path];
331 let empty = String::new();
332 let diff = TextDiff::from_lines(old_content, &empty);
333 let mut udiff = diff.unified_diff();
334 let formatted = udiff
335 .context_radius(TOKENIZATION.diff_context_radius)
336 .header(&format!("a/{path}"), "/dev/null");
337 let _ = write!(result, "{formatted}");
338 }
339
340 result
341}
342
343fn merge_file_contents(
344 initial: &FxHashMap<String, String>,
345 changed: &FxHashMap<String, String>,
346) -> FxHashMap<String, String> {
347 let mut merged = initial.clone();
348 for (path, content) in changed {
349 merged.insert(path.clone(), content.clone());
350 }
351 merged
352}
353
354fn empty_output(name: &str) -> DiffContextOutput {
355 DiffContextOutput {
356 lockfile_changes: Vec::new(),
357 ignored_changes: Vec::new(),
358 policy_excluded_count: 0,
359 name: name.to_string(),
360 output_type: "diff_context".to_string(),
361 commit_message: None,
362 changed_files: Vec::new(),
363 deleted_files: Vec::new(),
364 renamed_files: Vec::new(),
365 fragment_count: 0,
366 fragments: Vec::new(),
367 latency: None,
368 }
369}