Skip to main content

_diffctx/
memory_pipeline.rs

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    // The same two inputs the shipped pipeline gives the selector. Passing
93    // `None` for both made this harness score a different system: no
94    // excerpt-downshift (#149), so an oversized core was skipped rather than
95    // narrowed, and no I(f) prior, so per-file importance did not shape
96    // admission. Iterating on either of those against this harness measured
97    // something nobody runs.
98    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        // The corpus harness has no timeout contract; before #210 it
129        // inherited whatever ceiling the last in-process run left behind.
130        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}