1use std::collections::{BTreeMap, BTreeSet};
4use std::fmt::Write as _;
5use std::io::Read as _;
6use std::path::{Path, PathBuf};
7use std::time::Instant;
8
9use fallow_engine::session::AnalysisSession;
10use fallow_engine::similar_code::{
11 FunctionVector, SimilarCodeCorpusSelection, SimilarCodeLimits as EngineLimits,
12 SimilarCodeSelectionInput, SimilarCodeSkipReason as EngineSkipReason,
13 evaluate_selected_similar_code, select_similar_code_corpus,
14};
15use fallow_engine::source::similar_code::{
16 ExtractedSimilarCodeFunction, SIMILAR_CODE_EXTRACTION_SEMANTICS_VERSION,
17 SimilarCodeExtractionLimits, SimilarCodeExtractionSkipReason,
18};
19use fallow_engine::test_paths::is_test_code_path_str;
20use fallow_engine::{
21 codeowners::CodeOwners,
22 project_analysis::ProjectAnalysisArtifactOptions,
23 trace::{trace_file, trace_impact_closure_without_search},
24};
25use fallow_output::{
26 SimilarCodeAction, SimilarCodeActionType, SimilarCodeCacheStatus, SimilarCodeCacheSummary,
27 SimilarCodeCandidate, SimilarCodeCandidateSnapshot, SimilarCodeCompletion,
28 SimilarCodeCompletionStatus, SimilarCodeDiagnostic, SimilarCodeDiagnosticDomain,
29 SimilarCodeDomainOutcome, SimilarCodeEnrichmentAvailability, SimilarCodeEnrichmentState,
30 SimilarCodeGeneration, SimilarCodeGenerationParameters, SimilarCodeInspectOutput,
31 SimilarCodeInspectPacket, SimilarCodeInspectSchemaVersion, SimilarCodeLimits,
32 SimilarCodeLocation, SimilarCodeModelProvenance, SimilarCodeNamedReference, SimilarCodeOutput,
33 SimilarCodePhase, SimilarCodePhaseCompletion, SimilarCodePhaseStatus, SimilarCodeProvider,
34 SimilarCodeProviderProvenance, SimilarCodeReviewOutput, SimilarCodeReviewProvenance,
35 SimilarCodeReviewSchemaVersion, SimilarCodeReviewedCandidate, SimilarCodeSchemaVersion,
36 SimilarCodeScopeProvenance, SimilarCodeSideEffectHint, SimilarCodeSideEvidence,
37 SimilarCodeSimilarityBand, SimilarCodeSkip, SimilarCodeSkipReason, SimilarCodeVerdictInput,
38 SimilarCodeVerdictMatch, SimilarCodeVerificationStatus,
39};
40use fallow_types::envelope::{ElapsedMs, ToolVersion};
41use globset::{GlobSet, GlobSetBuilder};
42use rustc_hash::{FxHashMap, FxHashSet};
43use serde_json::Value;
44use sha2::{Digest, Sha256};
45
46use crate::analysis_context::{
47 ProgrammaticAnalysisContext, changed_files_for_run, resolve_programmatic_analysis_context,
48 workspace_roots_for_session,
49};
50use crate::similar_code::{
51 self, EmbeddingInput, EmbeddingResult, ProviderError, ReadyProvider, SimilarCodeProviderStatus,
52};
53use crate::{ProgrammaticError, SimilarCodeInspectOptions, SimilarCodeOptions};
54
55use super::ProgrammaticResult;
56
57const MAX_FILES: usize = 20_000;
58const MAX_RUN_TIMEOUT_MS: u64 = 15 * 60 * 1_000;
59const HIGH_SIMILARITY: f64 = 0.88;
60const VERY_HIGH_SIMILARITY: f64 = 0.95;
61const MAX_REVIEW_INPUT_BYTES: usize = 16 * 1024 * 1024;
62const MAX_RATIONALE_CHARS: usize = 4_000;
63const MAX_SOURCE_WINDOW_CHARS: usize = 16_000;
64const MAX_INSPECT_SOURCE_BYTES: u64 = fallow_config::DEFAULT_MAX_FILE_SIZE_BYTES;
65const MAX_INSPECT_GRAPH_REFERENCES: usize = 50;
66const MAX_INSPECT_RELATED_TESTS: usize = 50;
67const MODULE_REFERENCE_NAME: &str = "<module>";
68const INSPECT_CHURN_WINDOW_MONTHS: u64 = 6;
69
70#[derive(Clone, Copy)]
71struct PhaseCompleteness {
72 discovery: bool,
73 extraction: bool,
74 embedding: bool,
75 comparison: bool,
76}
77
78trait RuntimeEmbedder {
79 fn embed(
80 &mut self,
81 project_root: &Path,
82 no_cache: bool,
83 inputs: &[EmbeddingInput<'_>],
84 ) -> Result<EmbeddingResult, ProviderError>;
85}
86
87struct VerifiedProviderEmbedder<'a> {
88 provider: &'a ReadyProvider,
89}
90
91impl RuntimeEmbedder for VerifiedProviderEmbedder<'_> {
92 fn embed(
93 &mut self,
94 project_root: &Path,
95 no_cache: bool,
96 inputs: &[EmbeddingInput<'_>],
97 ) -> Result<EmbeddingResult, ProviderError> {
98 similar_code::embed_selected(self.provider, project_root, no_cache, inputs)
99 }
100}
101
102impl PhaseCompleteness {
103 const fn all_complete(self) -> bool {
104 self.discovery && self.extraction && self.embedding && self.comparison
105 }
106}
107
108pub fn run_similar_code(options: &SimilarCodeOptions) -> ProgrammaticResult<SimilarCodeOutput> {
118 validate_options(options)?;
119 let resolved = resolve_programmatic_analysis_context(&options.analysis)?;
120 let provider = options
121 .adapter_provider_path
122 .as_deref()
123 .map_or_else(
124 similar_code::ready_provider,
125 similar_code::ready_provider_from_adapter_path,
126 )
127 .map_err(provider_error)?;
128 resolved.install(|| run_similar_code_inner(options, &resolved, &provider))
129}
130
131pub fn select_similar_code_candidate_snapshot(
138 candidate_json: &[u8],
139 candidate_id: &str,
140) -> ProgrammaticResult<SimilarCodeCandidateSnapshot> {
141 if candidate_json.len() > MAX_REVIEW_INPUT_BYTES {
142 return Err(candidate_input_error(
143 "similar-code candidate input exceeded the 16 MiB limit",
144 ));
145 }
146 if candidate_id.trim().is_empty() {
147 return Err(candidate_input_error("candidate_id must not be empty"));
148 }
149 let raw = parse_candidate_document(candidate_json).map_err(|error| {
150 candidate_input_error(format!(
151 "invalid similar-code candidate document: {}",
152 error.message
153 ))
154 })?;
155 let mut candidates = raw
156 .candidates
157 .into_iter()
158 .filter(|candidate| candidate.candidate_id == candidate_id);
159 let candidate = candidates.next().ok_or_else(|| {
160 candidate_input_error("candidate_id was not present in the discovery document")
161 })?;
162 if candidates.next().is_some() {
163 return Err(candidate_input_error(
164 "candidate document contains duplicate candidate_id values",
165 ));
166 }
167 Ok(SimilarCodeCandidateSnapshot {
168 schema_version: raw.schema_version,
169 generation: raw.generation,
170 candidate,
171 completion: raw.completion,
172 diagnostics: raw.diagnostics,
173 })
174}
175
176pub fn parse_similar_code_candidate_snapshot(
182 snapshot_json: &[u8],
183 candidate_id: &str,
184) -> ProgrammaticResult<SimilarCodeCandidateSnapshot> {
185 if snapshot_json.len() > MAX_REVIEW_INPUT_BYTES {
186 return Err(candidate_input_error(
187 "similar-code candidate snapshot exceeded the 16 MiB limit",
188 ));
189 }
190 let snapshot: SimilarCodeCandidateSnapshot = serde_json::from_slice(snapshot_json)
191 .map_err(|error| candidate_input_error(format!("invalid candidate snapshot: {error}")))?;
192 if candidate_id.trim().is_empty() || snapshot.candidate.candidate_id != candidate_id {
193 return Err(candidate_input_error(
194 "candidate snapshot identity does not match candidate_id",
195 ));
196 }
197 Ok(snapshot)
198}
199
200pub fn inspect_similar_code(
207 options: &SimilarCodeInspectOptions,
208) -> ProgrammaticResult<SimilarCodeInspectOutput> {
209 let started = Instant::now();
210 let candidate = options.snapshot.candidate.clone();
211 let resolved = resolve_programmatic_analysis_context(&options.analysis)?;
212 let root = resolved.root().to_path_buf();
213 let mut left = inspect_side(&root, &candidate.left)?;
214 let mut right = inspect_side(&root, &candidate.right)?;
215 let session = load_session(&resolved)?;
216 let enrichment = resolved.install(|| {
217 enrich_inspect(
218 &session,
219 &candidate.left,
220 &candidate.right,
221 &mut left,
222 &mut right,
223 )
224 });
225 let mut diagnostics = options.snapshot.diagnostics.clone();
226 diagnostics.extend(enrichment.diagnostics);
227 Ok(SimilarCodeInspectOutput {
228 schema_version: SimilarCodeInspectSchemaVersion::V1,
229 version: ToolVersion(env!("CARGO_PKG_VERSION").to_owned()),
230 elapsed_ms: ElapsedMs(duration_ms(started)),
231 generation: options.snapshot.generation.clone(),
232 packet: SimilarCodeInspectPacket {
233 candidate_id: candidate.candidate_id.clone(),
234 review_key: candidate.review_key.clone(),
235 availability: enrichment.availability,
236 graph_relationship: enrichment.graph_relationship,
237 left,
238 right,
239 },
240 candidate,
241 completion: options.snapshot.completion.clone(),
242 diagnostics,
243 })
244}
245
246#[expect(
253 clippy::too_many_lines,
254 reason = "the review join keeps fail-closed verdict validation in one auditable boundary"
255)]
256pub fn review_similar_code(
257 candidate_json: &[u8],
258 verdict_json: &[u8],
259 require_verdict_for_each_candidate: bool,
260) -> ProgrammaticResult<SimilarCodeReviewOutput> {
261 let started = Instant::now();
262 if candidate_json.len() > MAX_REVIEW_INPUT_BYTES || verdict_json.len() > MAX_REVIEW_INPUT_BYTES
263 {
264 return Err(review_error(
265 "similar-code review input exceeded the 16 MiB limit",
266 ));
267 }
268 let raw = parse_candidate_document(candidate_json)?;
269 let verdicts: SimilarCodeVerdictInput = serde_json::from_slice(verdict_json)
270 .map_err(|error| review_error(format!("invalid similar-code verdict document: {error}")))?;
271 let mut by_candidate_id = FxHashMap::default();
272 let mut by_review_key: FxHashMap<&str, Vec<usize>> = FxHashMap::default();
273 for (index, candidate) in raw.candidates.iter().enumerate() {
274 if by_candidate_id
275 .insert(candidate.candidate_id.as_str(), index)
276 .is_some()
277 {
278 return Err(review_error(
279 "candidate document contains duplicate candidate_id values",
280 ));
281 }
282 by_review_key
283 .entry(candidate.review_key.as_str())
284 .or_default()
285 .push(index);
286 }
287
288 let mut matched = vec![None; raw.candidates.len()];
289 let mut match_kind = vec![SimilarCodeVerdictMatch::Unverified; raw.candidates.len()];
290 let mut diagnostics = raw.diagnostics;
291 let mut seen_candidate_ids = FxHashSet::default();
292 let mut seen_review_keys = FxHashSet::default();
293 for verdict in verdicts.verdicts {
294 validate_verdict(&verdict)?;
295 if !seen_candidate_ids.insert(verdict.candidate_id.clone()) {
296 return Err(duplicate_verdict_identity_error());
297 }
298 if let Some(&index) = by_candidate_id.get(verdict.candidate_id.as_str()) {
299 if raw.candidates[index].review_key != verdict.review_key {
300 return Err(review_error(
301 "verdict review_key does not match its candidate_id",
302 ));
303 }
304 if matched[index].is_some() {
305 return Err(review_error(
306 "multiple verdicts resolve to the same candidate",
307 ));
308 }
309 matched[index] = Some(verdict);
310 match_kind[index] = SimilarCodeVerdictMatch::CandidateId;
311 continue;
312 }
313 if !seen_review_keys.insert(verdict.review_key.clone()) {
316 return Err(duplicate_verdict_identity_error());
317 }
318 let Some(indices) = by_review_key.get(verdict.review_key.as_str()) else {
319 return Err(review_error(
320 "verdict references a stale or unknown candidate",
321 ));
322 };
323 if indices.len() == 1 {
324 let index = indices[0];
325 if matched[index].is_some() {
326 return Err(review_error(
327 "multiple verdicts resolve to the same candidate",
328 ));
329 }
330 matched[index] = Some(verdict);
331 match_kind[index] = SimilarCodeVerdictMatch::ReviewKey;
332 } else {
333 for &index in indices {
334 if matched[index].is_none() {
335 match_kind[index] = SimilarCodeVerdictMatch::AmbiguousReviewKey;
336 }
337 }
338 diagnostics.push(SimilarCodeDiagnostic {
339 domain: SimilarCodeDiagnosticDomain::Review,
340 code: "FALLOW_SIMILAR_CODE_REVIEW_KEY_AMBIGUOUS".to_owned(),
341 message:
342 "a verdict review_key matched multiple current candidates and was not applied"
343 .to_owned(),
344 path: None,
345 });
346 }
347 }
348 if require_verdict_for_each_candidate && matched.iter().any(Option::is_none) {
349 return Err(review_error("a verdict is required for every candidate"));
350 }
351 let candidates = raw
352 .candidates
353 .into_iter()
354 .zip(matched)
355 .zip(match_kind)
356 .map(|((candidate, verdict), verdict_match)| {
357 let outcome = verdict
358 .as_ref()
359 .map_or(SimilarCodeDomainOutcome::NeedsHumanReview, |verdict| {
360 verdict.outcome
361 });
362 SimilarCodeReviewedCandidate {
363 candidate,
364 verdict,
365 verdict_match,
366 outcome,
367 }
368 })
369 .collect();
370 Ok(SimilarCodeReviewOutput {
371 schema_version: SimilarCodeReviewSchemaVersion::V1,
372 version: ToolVersion(env!("CARGO_PKG_VERSION").to_owned()),
373 elapsed_ms: ElapsedMs(duration_ms(started)),
374 generation: raw.generation,
375 review: SimilarCodeReviewProvenance {
376 candidates_sha256: sha256_hex(candidate_json),
377 verdicts_sha256: sha256_hex(verdict_json),
378 },
379 candidates,
380 completion: raw.completion,
381 diagnostics,
382 })
383}
384
385fn run_similar_code_inner(
386 options: &SimilarCodeOptions,
387 resolved: &ProgrammaticAnalysisContext,
388 provider: &ReadyProvider,
389) -> ProgrammaticResult<SimilarCodeOutput> {
390 let mut embedder = VerifiedProviderEmbedder { provider };
391 run_similar_code_inner_with_embedder(options, resolved, &provider.status, &mut embedder)
392}
393
394struct FileAdmission<'a> {
395 files: Vec<(&'a fallow_types::discover::DiscoveredFile, String)>,
396 total_eligible_files: usize,
397 scope: SimilarCodeScopeProvenance,
398}
399
400struct ExtractedCorpus {
401 functions: Vec<ExtractedSimilarCodeFunction>,
402 limits: SimilarCodeExtractionLimits,
403 skips: BTreeMap<SimilarCodeSkipReason, usize>,
404 source_read_failures: usize,
405}
406
407fn run_similar_code_inner_with_embedder(
408 options: &SimilarCodeOptions,
409 resolved: &ProgrammaticAnalysisContext,
410 provider: &SimilarCodeProviderStatus,
411 embedder: &mut dyn RuntimeEmbedder,
412) -> ProgrammaticResult<SimilarCodeOutput> {
413 let started = Instant::now();
414 let session = load_session(resolved)?;
415 let (threshold, min_lines) = detection_parameters(options, &session)?;
416 let changed_files = changed_files_for_run(resolved)?;
417 let workspace_roots = workspace_roots_for_session(resolved, session.workspaces())?;
418 let scope_active = similar_code_scope_active(
419 options,
420 resolved,
421 changed_files.as_ref(),
422 workspace_roots.as_deref(),
423 );
424 let in_scope = |path: &str| {
425 !scope_active
426 || similar_code_path_in_scope(
427 path,
428 options,
429 resolved,
430 changed_files.as_ref(),
431 workspace_roots.as_deref(),
432 )
433 };
434 let ignore = build_ignore_set(&session.config().similar_code.ignore)?;
435 let mut diagnostics = Vec::new();
436 let admission = admit_source_files(&session, &ignore, scope_active, in_scope);
437 let extraction = extract_corpus(&admission, min_lines, &mut diagnostics);
438 let engine_limits = EngineLimits::for_dimensions(provider.dimensions);
439 let selection = select_corpus(
440 &extraction.functions,
441 engine_limits,
442 scope_active,
443 in_scope,
444 &mut diagnostics,
445 )?;
446 let selected = selection
447 .selected_indices
448 .iter()
449 .map(|index| &extraction.functions[*index])
450 .collect::<Vec<_>>();
451 let mut embedding = embed_corpus(session.root(), resolved.no_cache(), &selected, embedder)?;
452 let (vectors, effective_selection) = comparison_inputs(&selected, &selection, &mut embedding)?;
453 let evaluation = evaluate_selected_similar_code(
454 &vectors,
455 &effective_selection,
456 threshold,
457 engine_limits,
458 SIMILAR_CODE_EXTRACTION_SEMANTICS_VERSION,
459 )
460 .map_err(engine_error)?;
461 let candidates = output_candidates(
462 evaluation.candidates,
463 &extraction.functions,
464 in_scope,
465 options.top,
466 );
467 let completion = build_completion(
468 &admission,
469 &extraction,
470 &selection,
471 &mut embedding,
472 &evaluation.completion,
473 &mut diagnostics,
474 );
475 Ok(SimilarCodeOutput {
476 schema_version: SimilarCodeSchemaVersion::V1,
477 version: ToolVersion(env!("CARGO_PKG_VERSION").to_owned()),
478 elapsed_ms: ElapsedMs(duration_ms(started)),
479 generation: generation(provider, threshold, min_lines, admission.scope),
480 candidates,
481 completion,
482 diagnostics,
483 })
484}
485
486fn detection_parameters(
487 options: &SimilarCodeOptions,
488 session: &AnalysisSession,
489) -> ProgrammaticResult<(f64, usize)> {
490 let threshold = options
491 .threshold
492 .unwrap_or_else(|| session.config().similar_code.threshold);
493 let min_lines = options
494 .min_lines
495 .unwrap_or_else(|| session.config().similar_code.min_lines);
496 validate_threshold(threshold)?;
497 if min_lines == 0 {
498 return Err(
499 ProgrammaticError::new("`similar_code.min_lines` must be at least 1", 2)
500 .with_code("FALLOW_INVALID_SIMILAR_CODE_MIN_LINES")
501 .with_context("similarCode.minLines"),
502 );
503 }
504 Ok((threshold, min_lines))
505}
506
507fn admit_source_files<'a>(
508 session: &'a AnalysisSession,
509 ignore: &GlobSet,
510 scope_active: bool,
511 in_scope: impl Fn(&str) -> bool,
512) -> FileAdmission<'a> {
513 let mut eligible_files = session
514 .files()
515 .iter()
516 .filter_map(|file| {
517 let relative = root_relative(session.root(), &file.path);
518 (!ignore.is_match(&relative)).then_some((file, relative))
519 })
520 .collect::<Vec<_>>();
521 eligible_files.sort_by_key(|(_, relative)| usize::from(!in_scope(relative)));
522 let total_eligible_files = eligible_files.len();
523 eligible_files.truncate(total_eligible_files.min(MAX_FILES));
524 let mut effective_scope_paths = if scope_active {
525 eligible_files
526 .iter()
527 .filter(|(_, relative)| in_scope(relative))
528 .map(|(_, relative)| relative.clone())
529 .collect::<Vec<_>>()
530 } else {
531 Vec::new()
532 };
533 effective_scope_paths.sort();
534 effective_scope_paths.dedup();
535 FileAdmission {
536 files: eligible_files,
537 total_eligible_files,
538 scope: SimilarCodeScopeProvenance {
539 active: scope_active,
540 paths: effective_scope_paths,
541 },
542 }
543}
544
545fn extract_corpus(
546 admission: &FileAdmission<'_>,
547 min_lines: usize,
548 diagnostics: &mut Vec<SimilarCodeDiagnostic>,
549) -> ExtractedCorpus {
550 let extraction_limits = SimilarCodeExtractionLimits::default();
551 let mut functions = Vec::new();
552 let mut extracted_source_bytes = 0usize;
553 let mut extraction_skips = BTreeMap::new();
554 let mut source_read_failures = 0usize;
555 for (file_index, (file, relative)) in admission.files.iter().enumerate() {
556 let remaining_functions = extraction_limits
557 .max_functions
558 .saturating_sub(functions.len());
559 let remaining_bytes = extraction_limits
560 .max_total_source_bytes
561 .saturating_sub(extracted_source_bytes);
562 if let Some(reason) = exhausted_extraction_limit(remaining_functions, remaining_bytes) {
563 add_skip(
564 &mut extraction_skips,
565 reason,
566 remaining_extraction_inputs(admission.files.len(), file_index),
567 );
568 break;
569 }
570 let source = match std::fs::read_to_string(&file.path) {
571 Ok(source) => source,
572 Err(error) => {
573 source_read_failures = source_read_failures.saturating_add(1);
574 diagnostics.push(SimilarCodeDiagnostic {
575 domain: SimilarCodeDiagnosticDomain::Extraction,
576 code: "FALLOW_SIMILAR_CODE_SOURCE_READ_FAILED".to_owned(),
577 message: format!("failed to read source: {error}"),
578 path: Some(relative.clone()),
579 });
580 continue;
581 }
582 };
583 let extracted = fallow_engine::source::similar_code::extract(
584 Path::new(relative),
585 &source,
586 SimilarCodeExtractionLimits {
587 max_functions: remaining_functions,
588 max_source_bytes_per_function: extraction_limits.max_source_bytes_per_function,
589 max_total_source_bytes: remaining_bytes,
590 },
591 );
592 for skip in extracted.skipped {
593 add_skip(
594 &mut extraction_skips,
595 map_extraction_skip(skip.reason),
596 skip.count,
597 );
598 }
599 for function in extracted.functions {
600 let lines = function
601 .location
602 .end_line
603 .saturating_sub(function.location.start_line)
604 .saturating_add(1) as usize;
605 if lines < min_lines {
606 add_skip(
607 &mut extraction_skips,
608 SimilarCodeSkipReason::BelowMinimumLines,
609 1,
610 );
611 } else {
612 extracted_source_bytes =
613 extracted_source_bytes.saturating_add(function.source.len());
614 functions.push(function);
615 }
616 }
617 }
618 ExtractedCorpus {
619 functions,
620 limits: extraction_limits,
621 skips: extraction_skips,
622 source_read_failures,
623 }
624}
625
626fn select_corpus(
627 functions: &[ExtractedSimilarCodeFunction],
628 engine_limits: EngineLimits,
629 scope_active: bool,
630 in_scope: impl Fn(&str) -> bool,
631 diagnostics: &mut Vec<SimilarCodeDiagnostic>,
632) -> ProgrammaticResult<SimilarCodeCorpusSelection> {
633 let selection_inputs = functions
634 .iter()
635 .map(|function| SimilarCodeSelectionInput {
636 location: &function.location,
637 source_sha256: function.source_sha256,
638 in_scope: in_scope(&function.location.file),
639 })
640 .collect::<Vec<_>>();
641 let selection =
642 select_similar_code_corpus(&selection_inputs, engine_limits).map_err(engine_error)?;
643 let scoped_functions = selection_inputs
644 .iter()
645 .filter(|function| function.in_scope)
646 .count();
647 let selected_scoped_functions = selection
648 .selected_in_scope
649 .iter()
650 .filter(|&&value| value)
651 .count();
652 if scope_active && selected_scoped_functions < scoped_functions {
653 diagnostics.push(SimilarCodeDiagnostic {
654 domain: SimilarCodeDiagnosticDomain::Workspace,
655 code: "FALLOW_SIMILAR_CODE_SCOPE_PARTIAL".to_owned(),
656 message: format!(
657 "scope limits admitted {selected_scoped_functions} of {scoped_functions} eligible scoped functions"
658 ),
659 path: None,
660 });
661 }
662 Ok(selection)
663}
664
665fn embed_corpus(
666 root: &Path,
667 no_cache: bool,
668 selected: &[&ExtractedSimilarCodeFunction],
669 embedder: &mut dyn RuntimeEmbedder,
670) -> ProgrammaticResult<EmbeddingResult> {
671 let embedding_inputs = selected
672 .iter()
673 .map(|function| EmbeddingInput {
674 source_sha256: function.source_sha256,
675 source: &function.source,
676 })
677 .collect::<Vec<_>>();
678 embedder
679 .embed(root, no_cache, &embedding_inputs)
680 .map_err(provider_error)
681}
682
683fn comparison_inputs(
684 selected: &[&ExtractedSimilarCodeFunction],
685 selection: &SimilarCodeCorpusSelection,
686 embedding: &mut EmbeddingResult,
687) -> ProgrammaticResult<(Vec<FunctionVector>, SimilarCodeCorpusSelection)> {
688 let mut vectors = Vec::new();
689 let mut effective_scope = Vec::new();
690 for (selected_index, values) in std::mem::take(&mut embedding.vectors)
691 .into_iter()
692 .enumerate()
693 {
694 if let Some(values) = values {
695 let function = selected[selected_index];
696 vectors.push(FunctionVector {
697 location: function.location.clone(),
698 source_sha256: function.source_sha256,
699 extraction_semantics_version: SIMILAR_CODE_EXTRACTION_SEMANTICS_VERSION,
700 values,
701 });
702 effective_scope.push(selection.selected_in_scope[selected_index]);
703 }
704 }
705 if vectors.len() < 2 && selected.len() >= 2 {
706 return Err(ProgrammaticError::new(
707 embedding.provider_problem.take().unwrap_or_else(|| {
708 "the local similar-code provider returned fewer than two usable vectors".to_owned()
709 }),
710 2,
711 )
712 .with_code("FALLOW_SIMILAR_CODE_PROVIDER_FAILED")
713 .with_context("similarCode.provider"));
714 }
715
716 let effective_selection = SimilarCodeCorpusSelection {
717 selected_indices: (0..vectors.len()).collect(),
718 selected_in_scope: effective_scope,
719 skipped: selection.skipped.clone(),
720 };
721 Ok((vectors, effective_selection))
722}
723
724fn output_candidates(
725 candidates: Vec<fallow_engine::similar_code::SimilarCodeCandidate>,
726 functions: &[ExtractedSimilarCodeFunction],
727 in_scope: impl Fn(&str) -> bool,
728 top: Option<usize>,
729) -> Vec<SimilarCodeCandidate> {
730 let metadata = functions
731 .iter()
732 .map(|function| (location_key(&function.location), function))
733 .collect::<FxHashMap<_, _>>();
734 let mut candidates = candidates
735 .into_iter()
736 .filter(|candidate| in_scope(&candidate.left.file) || in_scope(&candidate.right.file))
737 .filter_map(|candidate| map_candidate(candidate, &metadata))
738 .collect::<Vec<_>>();
739 if let Some(top) = top {
740 candidates.truncate(top);
741 }
742 candidates
743}
744
745fn completion_skips(
746 extraction_skips: &BTreeMap<SimilarCodeSkipReason, usize>,
747 omitted_files: usize,
748 comparison: &fallow_engine::similar_code::SimilarCodeCompletion,
749 embedding: &EmbeddingResult,
750 missing_vectors: usize,
751) -> Vec<SimilarCodeSkip> {
752 let mut skips = extraction_skips
753 .iter()
754 .map(|(reason, count)| SimilarCodeSkip {
755 phase: SimilarCodePhase::Extraction,
756 reason: *reason,
757 count: usize_to_u64(*count),
758 })
759 .collect::<Vec<_>>();
760 if omitted_files > 0 {
761 skips.push(SimilarCodeSkip {
762 phase: SimilarCodePhase::Discovery,
763 reason: SimilarCodeSkipReason::InputLimit,
764 count: usize_to_u64(omitted_files),
765 });
766 }
767 skips.extend(comparison.skipped.iter().map(|skip| SimilarCodeSkip {
768 phase: SimilarCodePhase::Comparison,
769 reason: map_engine_skip(skip.reason),
770 count: usize_to_u64(skip.count),
771 }));
772 if missing_vectors > 0 {
773 skips.push(SimilarCodeSkip {
774 phase: SimilarCodePhase::Embedding,
775 reason: if embedding
776 .provider_problem
777 .as_deref()
778 .is_some_and(|problem| problem.contains("timed out"))
779 {
780 SimilarCodeSkipReason::Timeout
781 } else {
782 SimilarCodeSkipReason::ProviderFailure
783 },
784 count: usize_to_u64(missing_vectors),
785 });
786 }
787 if embedding.truncated_functions > 0 {
788 skips.push(SimilarCodeSkip {
789 phase: SimilarCodePhase::Embedding,
790 reason: SimilarCodeSkipReason::TokenTruncation,
791 count: usize_to_u64(embedding.truncated_functions),
792 });
793 }
794 skips.sort_by_key(|skip| (skip.phase as u8, skip.reason as u8));
795 skips
796}
797
798fn build_completion(
799 admission: &FileAdmission<'_>,
800 extraction: &ExtractedCorpus,
801 selection: &SimilarCodeCorpusSelection,
802 embedding: &mut EmbeddingResult,
803 comparison: &fallow_engine::similar_code::SimilarCodeCompletion,
804 diagnostics: &mut Vec<SimilarCodeDiagnostic>,
805) -> SimilarCodeCompletion {
806 let omitted_files = admission
807 .total_eligible_files
808 .saturating_sub(admission.files.len());
809 let embedded_functions = comparison.functions_considered;
810 let missing_vectors = selection
811 .selected_indices
812 .len()
813 .saturating_sub(embedded_functions);
814 let skips = completion_skips(
815 &extraction.skips,
816 omitted_files,
817 comparison,
818 embedding,
819 missing_vectors,
820 );
821 if let Some(problem) = embedding.provider_problem.take() {
822 diagnostics.push(SimilarCodeDiagnostic {
823 domain: SimilarCodeDiagnosticDomain::Provider,
824 code: "FALLOW_SIMILAR_CODE_PROVIDER_PARTIAL".to_owned(),
825 message: problem,
826 path: None,
827 });
828 }
829 if let Some(problem) = embedding.cache_problem.take() {
830 diagnostics.push(SimilarCodeDiagnostic {
831 domain: SimilarCodeDiagnosticDomain::Cache,
832 code: "FALLOW_SIMILAR_CODE_CACHE_ADVISORY".to_owned(),
833 message: problem,
834 path: None,
835 });
836 }
837 let phase_completeness = PhaseCompleteness {
838 discovery: omitted_files == 0,
839 extraction: extraction_is_complete(&extraction.skips, extraction.source_read_failures),
840 embedding: missing_vectors == 0 && embedding.truncated_functions == 0,
841 comparison: comparison.status
842 == fallow_engine::similar_code::SimilarCodeCompletionStatus::Complete,
843 };
844 let complete = phase_completeness.all_complete()
845 && diagnostics
846 .iter()
847 .all(|diagnostic| diagnostic.domain == SimilarCodeDiagnosticDomain::Cache);
848 let cache_status = cache_status(
849 embedding.cache_disabled,
850 embedding.cache_hits,
851 embedding.cache_misses,
852 );
853 SimilarCodeCompletion {
854 status: if complete {
855 SimilarCodeCompletionStatus::Complete
856 } else {
857 SimilarCodeCompletionStatus::Partial
858 },
859 phases: phases(
860 admission.files.len(),
861 admission.total_eligible_files,
862 extraction.functions.len(),
863 selection.selected_indices.len(),
864 embedded_functions,
865 comparison.comparisons_performed,
866 phase_completeness,
867 missing_vectors,
868 embedding.truncated_functions,
869 extraction.source_read_failures,
870 ),
871 limits: output_limits(comparison.limits, extraction.limits),
872 skips,
873 cache: SimilarCodeCacheSummary {
874 status: cache_status,
875 hits: usize_to_u64(embedding.cache_hits),
876 misses: usize_to_u64(embedding.cache_misses),
877 writes: usize_to_u64(embedding.cache_writes),
878 invalid_entries: usize_to_u64(embedding.cache_invalid_entries),
879 },
880 provider_inference_ms: finite_f64_to_u64(embedding.inference_ms),
881 }
882}
883
884fn inspect_side(
885 root: &Path,
886 location: &SimilarCodeLocation,
887) -> ProgrammaticResult<SimilarCodeSideEvidence> {
888 let relative = Path::new(&location.path);
889 if location.path.trim().is_empty()
890 || relative.is_absolute()
891 || relative
892 .components()
893 .any(|component| !matches!(component, std::path::Component::Normal(_)))
894 {
895 return Err(candidate_input_error(
896 "candidate source paths must be normalized project-root-relative paths",
897 ));
898 }
899 let path = dunce::canonicalize(root.join(relative)).map_err(|error| {
900 ProgrammaticError::new(
901 format!(
902 "failed to resolve inspected source {}: {error}",
903 location.path
904 ),
905 2,
906 )
907 .with_code("FALLOW_SIMILAR_CODE_INSPECT_SOURCE_FAILED")
908 .with_context("similarCode.inspect")
909 })?;
910 if !path.starts_with(root) {
911 return Err(candidate_input_error(
912 "candidate source path resolves outside the project root",
913 ));
914 }
915 let source = read_inspect_source(&path, &location.path)?;
916 let extracted = fallow_engine::source::similar_code::extract(
917 Path::new(&location.path),
918 &source,
919 SimilarCodeExtractionLimits::default(),
920 );
921 let function = extracted
922 .functions
923 .into_iter()
924 .find(|function| function_matches_snapshot_location(function, location))
925 .ok_or_else(|| {
926 ProgrammaticError::new(
927 "inspected function no longer matches the candidate snapshot",
928 2,
929 )
930 .with_code("FALLOW_SIMILAR_CODE_CANDIDATE_STALE")
931 .with_context("similarCode.inspect")
932 })?;
933 Ok(SimilarCodeSideEvidence {
934 source_window: Some(bound_source_window(&function.source)),
935 parameter_count: Some(function.param_count),
936 is_async: Some(function.is_async),
937 is_generator: Some(function.is_generator),
938 has_await: Some(function.has_await),
939 has_throw: Some(function.has_throw),
940 side_effect_hint: Some(match function.side_effect_hint {
941 fallow_engine::source::similar_code::SimilarCodeSideEffectHint::PureLooking => {
942 SimilarCodeSideEffectHint::PureLooking
943 }
944 fallow_engine::source::similar_code::SimilarCodeSideEffectHint::MayHaveSideEffects => {
945 SimilarCodeSideEffectHint::MayHaveSideEffects
946 }
947 fallow_engine::source::similar_code::SimilarCodeSideEffectHint::Unknown => {
948 SimilarCodeSideEffectHint::Unknown
949 }
950 _ => SimilarCodeSideEffectHint::Unknown,
951 }),
952 entry_point_reachable: None,
953 callers: Vec::new(),
954 callees: Vec::new(),
955 owners: Vec::new(),
956 churn_commits: None,
957 tests: Vec::new(),
958 deterministic_clone_coverage: None,
959 runtime_observations: None,
960 })
961}
962
963fn function_matches_snapshot_location(
964 function: &ExtractedSimilarCodeFunction,
965 location: &SimilarCodeLocation,
966) -> bool {
967 function.location.file == location.path
968 && function.name == location.name
969 && function.location.start_line == location.start_line
970 && function.location.start_column_utf8.saturating_add(1) == location.start_column
971 && function.location.end_line == location.end_line
972 && function.location.end_column_utf8.saturating_add(1) == location.end_column
973 && hex(function.source_sha256.as_bytes()) == location.source_sha256
974}
975
976#[expect(
977 clippy::filetype_is_file,
978 reason = "exact inspect accepts only regular source files and rejects every special file"
979)]
980fn read_inspect_source(path: &Path, display_path: &str) -> ProgrammaticResult<String> {
981 let metadata = std::fs::symlink_metadata(path).map_err(|error| {
982 inspect_source_error(format!("failed to inspect source {display_path}: {error}"))
983 })?;
984 if !metadata.file_type().is_file() || metadata.len() > MAX_INSPECT_SOURCE_BYTES {
985 return Err(stale_candidate_error(format!(
986 "inspected source {display_path} exceeded the {} MiB per-file limit",
987 MAX_INSPECT_SOURCE_BYTES / (1024 * 1024)
988 )));
989 }
990
991 let file = std::fs::File::open(path).map_err(|error| {
992 inspect_source_error(format!(
993 "failed to read inspected source {display_path}: {error}"
994 ))
995 })?;
996 let mut bytes = Vec::new();
997 file.take(MAX_INSPECT_SOURCE_BYTES + 1)
998 .read_to_end(&mut bytes)
999 .map_err(|error| {
1000 inspect_source_error(format!(
1001 "failed to read inspected source {display_path}: {error}"
1002 ))
1003 })?;
1004 if bytes.len() as u64 > MAX_INSPECT_SOURCE_BYTES {
1005 return Err(stale_candidate_error(format!(
1006 "inspected source {display_path} exceeded the {} MiB per-file limit",
1007 MAX_INSPECT_SOURCE_BYTES / (1024 * 1024)
1008 )));
1009 }
1010 String::from_utf8(bytes).map_err(|error| {
1011 inspect_source_error(format!(
1012 "failed to read inspected source {display_path}: {error}"
1013 ))
1014 })
1015}
1016
1017fn inspect_source_error(message: impl Into<String>) -> ProgrammaticError {
1018 ProgrammaticError::new(message, 2)
1019 .with_code("FALLOW_SIMILAR_CODE_INSPECT_SOURCE_FAILED")
1020 .with_context("similarCode.inspect")
1021}
1022
1023fn stale_candidate_error(message: impl Into<String>) -> ProgrammaticError {
1024 ProgrammaticError::new(message, 2)
1025 .with_code("FALLOW_SIMILAR_CODE_CANDIDATE_STALE")
1026 .with_context("similarCode.inspect")
1027}
1028
1029struct InspectEnrichment {
1030 availability: SimilarCodeEnrichmentAvailability,
1031 graph_relationship: Option<String>,
1032 diagnostics: Vec<SimilarCodeDiagnostic>,
1033}
1034
1035fn enrich_inspect(
1036 session: &AnalysisSession,
1037 left_location: &SimilarCodeLocation,
1038 right_location: &SimilarCodeLocation,
1039 left: &mut SimilarCodeSideEvidence,
1040 right: &mut SimilarCodeSideEvidence,
1041) -> InspectEnrichment {
1042 let mut result = InspectEnrichment {
1043 availability: unavailable_inspect_enrichment(),
1044 graph_relationship: None,
1045 diagnostics: Vec::new(),
1046 };
1047
1048 if session.files().len() > MAX_FILES {
1049 result.diagnostics.push(enrichment_diagnostic(
1050 "FALLOW_SIMILAR_CODE_ENRICHMENT_INPUT_LIMIT",
1051 format!(
1052 "graph and deterministic clone enrichment require at most {MAX_FILES} discovered files"
1053 ),
1054 None,
1055 ));
1056 } else {
1057 enrich_graph_and_clones(
1058 session,
1059 left_location,
1060 right_location,
1061 left,
1062 right,
1063 &mut result,
1064 );
1065 }
1066
1067 enrich_ownership(
1068 session,
1069 left_location,
1070 right_location,
1071 left,
1072 right,
1073 &mut result,
1074 );
1075 enrich_churn(
1076 session,
1077 left_location,
1078 right_location,
1079 left,
1080 right,
1081 &mut result,
1082 );
1083 result
1084}
1085
1086fn enrich_graph_and_clones(
1087 session: &AnalysisSession,
1088 left_location: &SimilarCodeLocation,
1089 right_location: &SimilarCodeLocation,
1090 left: &mut SimilarCodeSideEvidence,
1091 right: &mut SimilarCodeSideEvidence,
1092 result: &mut InspectEnrichment,
1093) {
1094 let artifacts = match session.analyze_project_with_artifacts(
1095 &session.config().duplicates,
1096 ProjectAnalysisArtifactOptions {
1097 retain_graph: true,
1098 ..ProjectAnalysisArtifactOptions::default()
1099 },
1100 ) {
1101 Ok(artifacts) => artifacts,
1102 Err(error) => {
1103 result.diagnostics.push(enrichment_diagnostic(
1104 "FALLOW_SIMILAR_CODE_ANALYSIS_ENRICHMENT_UNAVAILABLE",
1105 format!("graph and deterministic clone enrichment failed: {error}"),
1106 None,
1107 ));
1108 return;
1109 }
1110 };
1111
1112 left.deterministic_clone_coverage = Some(deterministic_clone_coverage(
1113 &artifacts.duplication,
1114 session.root(),
1115 left_location,
1116 ));
1117 right.deterministic_clone_coverage = Some(deterministic_clone_coverage(
1118 &artifacts.duplication,
1119 session.root(),
1120 right_location,
1121 ));
1122 result.availability.deterministic_clone_coverage = SimilarCodeEnrichmentState::Available;
1123
1124 let Some(graph) = artifacts.dead_code.graph.as_ref() else {
1125 result.diagnostics.push(enrichment_diagnostic(
1126 "FALLOW_SIMILAR_CODE_GRAPH_ENRICHMENT_UNAVAILABLE",
1127 "retained module graph was unavailable",
1128 None,
1129 ));
1130 return;
1131 };
1132 let Some(left_trace) = trace_file(graph, session.root(), &left_location.path) else {
1133 result.diagnostics.push(enrichment_diagnostic(
1134 "FALLOW_SIMILAR_CODE_GRAPH_TARGET_UNAVAILABLE",
1135 "left candidate module was absent from the retained graph",
1136 Some(left_location.path.clone()),
1137 ));
1138 return;
1139 };
1140 let Some(right_trace) = trace_file(graph, session.root(), &right_location.path) else {
1141 result.diagnostics.push(enrichment_diagnostic(
1142 "FALLOW_SIMILAR_CODE_GRAPH_TARGET_UNAVAILABLE",
1143 "right candidate module was absent from the retained graph",
1144 Some(right_location.path.clone()),
1145 ));
1146 return;
1147 };
1148
1149 let left_impact =
1150 trace_impact_closure_without_search(graph, session.root(), &left_location.path);
1151 let right_impact =
1152 trace_impact_closure_without_search(graph, session.root(), &right_location.path);
1153 apply_graph_evidence(
1154 session.root(),
1155 &left_trace,
1156 &right_trace,
1157 left_location,
1158 right_location,
1159 left_impact
1160 .as_ref()
1161 .map_or(&[][..], |impact| impact.affected_not_shown.as_slice()),
1162 right_impact
1163 .as_ref()
1164 .map_or(&[][..], |impact| impact.affected_not_shown.as_slice()),
1165 left,
1166 right,
1167 result,
1168 );
1169}
1170
1171#[expect(
1172 clippy::too_many_arguments,
1173 reason = "the helper applies symmetric evidence for both immutable candidate sides"
1174)]
1175fn apply_graph_evidence(
1176 root: &Path,
1177 left_trace: &fallow_engine::trace::FileTrace,
1178 right_trace: &fallow_engine::trace::FileTrace,
1179 left_location: &SimilarCodeLocation,
1180 right_location: &SimilarCodeLocation,
1181 left_impact_paths: &[String],
1182 right_impact_paths: &[String],
1183 left: &mut SimilarCodeSideEvidence,
1184 right: &mut SimilarCodeSideEvidence,
1185 result: &mut InspectEnrichment,
1186) {
1187 left.entry_point_reachable = Some(left_trace.is_reachable);
1188 right.entry_point_reachable = Some(right_trace.is_reachable);
1189
1190 let (left_callers, left_callers_truncated) =
1191 bounded_module_references(&left_trace.imported_by, MAX_INSPECT_GRAPH_REFERENCES);
1192 let (left_callees, left_callees_truncated) =
1193 bounded_module_references(&left_trace.imports_from, MAX_INSPECT_GRAPH_REFERENCES);
1194 let (right_callers, right_callers_truncated) =
1195 bounded_module_references(&right_trace.imported_by, MAX_INSPECT_GRAPH_REFERENCES);
1196 let (right_callees, right_callees_truncated) =
1197 bounded_module_references(&right_trace.imports_from, MAX_INSPECT_GRAPH_REFERENCES);
1198 left.callers = left_callers;
1199 left.callees = left_callees;
1200 right.callers = right_callers;
1201 right.callees = right_callees;
1202
1203 let (left_tests, left_tests_truncated) =
1204 bounded_related_tests(root, left_impact_paths, MAX_INSPECT_RELATED_TESTS);
1205 let (right_tests, right_tests_truncated) =
1206 bounded_related_tests(root, right_impact_paths, MAX_INSPECT_RELATED_TESTS);
1207 left.tests = left_tests;
1208 right.tests = right_tests;
1209
1210 result.graph_relationship = Some(module_relationship(
1211 left_trace,
1212 right_trace,
1213 left_location,
1214 right_location,
1215 ));
1216 result.availability.graph_relationship = SimilarCodeEnrichmentState::Available;
1217 result.availability.entry_point_reachability = SimilarCodeEnrichmentState::Available;
1218 result.availability.callers = SimilarCodeEnrichmentState::Available;
1219 result.availability.callees = SimilarCodeEnrichmentState::Available;
1220 result.availability.tests = SimilarCodeEnrichmentState::Available;
1221 result.diagnostics.push(enrichment_diagnostic(
1222 "FALLOW_SIMILAR_CODE_GRAPH_REFERENCES_MODULE_LEVEL",
1223 "callers and callees are direct module import relationships; <module> at line 1 is a module anchor, not a function callsite",
1224 None,
1225 ));
1226 if left_callers_truncated
1227 || left_callees_truncated
1228 || right_callers_truncated
1229 || right_callees_truncated
1230 || left_tests_truncated
1231 || right_tests_truncated
1232 {
1233 result.diagnostics.push(enrichment_diagnostic(
1234 "FALLOW_SIMILAR_CODE_GRAPH_ENRICHMENT_TRUNCATED",
1235 "graph references or related tests exceeded inspect output limits",
1236 None,
1237 ));
1238 }
1239}
1240
1241fn bounded_module_references(
1242 paths: &[PathBuf],
1243 limit: usize,
1244) -> (Vec<SimilarCodeNamedReference>, bool) {
1245 let mut paths = paths
1246 .iter()
1247 .map(|path| normalize_path(path))
1248 .collect::<Vec<_>>();
1249 paths.sort();
1250 paths.dedup();
1251 let truncated = paths.len() > limit;
1252 paths.truncate(limit);
1253 (
1254 paths
1255 .into_iter()
1256 .map(|path| SimilarCodeNamedReference {
1257 path,
1258 name: MODULE_REFERENCE_NAME.to_owned(),
1259 line: 1,
1260 })
1261 .collect(),
1262 truncated,
1263 )
1264}
1265
1266fn bounded_related_tests(root: &Path, paths: &[String], limit: usize) -> (Vec<String>, bool) {
1267 let mut tests = paths
1268 .iter()
1269 .map(|path| path.replace('\\', "/"))
1270 .filter(|path| is_test_code_path_str(root, path))
1271 .collect::<Vec<_>>();
1272 tests.sort();
1273 tests.dedup();
1274 let truncated = tests.len() > limit;
1275 tests.truncate(limit);
1276 (tests, truncated)
1277}
1278
1279fn module_relationship(
1280 left_trace: &fallow_engine::trace::FileTrace,
1281 right_trace: &fallow_engine::trace::FileTrace,
1282 left_location: &SimilarCodeLocation,
1283 right_location: &SimilarCodeLocation,
1284) -> String {
1285 if left_location.path == right_location.path {
1286 return "same-module".to_owned();
1287 }
1288 let left_imports_right = trace_imports_path(left_trace, &right_location.path);
1289 let right_imports_left = trace_imports_path(right_trace, &left_location.path);
1290 if left_imports_right && right_imports_left {
1291 return "mutual-direct-module-import".to_owned();
1292 }
1293 if left_imports_right {
1294 return "left-directly-imports-right".to_owned();
1295 }
1296 if right_imports_left {
1297 return "right-directly-imports-left".to_owned();
1298 }
1299
1300 let left_callers = left_trace
1301 .imported_by
1302 .iter()
1303 .map(|path| normalize_path(path))
1304 .collect::<BTreeSet<_>>();
1305 if right_trace
1306 .imported_by
1307 .iter()
1308 .map(|path| normalize_path(path))
1309 .any(|path| left_callers.contains(&path))
1310 {
1311 "shared-direct-importer".to_owned()
1312 } else {
1313 "no-direct-module-relationship".to_owned()
1314 }
1315}
1316
1317fn trace_imports_path(trace: &fallow_engine::trace::FileTrace, target: &str) -> bool {
1318 trace
1319 .imports_from
1320 .iter()
1321 .any(|path| normalize_path(path) == target)
1322}
1323
1324fn enrich_ownership(
1325 session: &AnalysisSession,
1326 left_location: &SimilarCodeLocation,
1327 right_location: &SimilarCodeLocation,
1328 left: &mut SimilarCodeSideEvidence,
1329 right: &mut SimilarCodeSideEvidence,
1330 result: &mut InspectEnrichment,
1331) {
1332 match CodeOwners::load(session.root(), session.config().codeowners.as_deref()) {
1333 Ok(codeowners) => {
1334 left.owners = primary_owner(&codeowners, &left_location.path);
1335 right.owners = primary_owner(&codeowners, &right_location.path);
1336 result.availability.ownership = SimilarCodeEnrichmentState::Available;
1337 }
1338 Err(error) => result.diagnostics.push(enrichment_diagnostic(
1339 "FALLOW_SIMILAR_CODE_OWNERSHIP_UNAVAILABLE",
1340 error,
1341 None,
1342 )),
1343 }
1344}
1345
1346fn primary_owner(codeowners: &CodeOwners, path: &str) -> Vec<String> {
1347 codeowners
1348 .owner_of(Path::new(path))
1349 .map(|owner| vec![owner.to_owned()])
1350 .unwrap_or_default()
1351}
1352
1353fn enrich_churn(
1354 session: &AnalysisSession,
1355 left_location: &SimilarCodeLocation,
1356 right_location: &SimilarCodeLocation,
1357 left: &mut SimilarCodeSideEvidence,
1358 right: &mut SimilarCodeSideEvidence,
1359 result: &mut InspectEnrichment,
1360) {
1361 if !fallow_engine::churn::is_git_repo(session.root()) {
1362 result.diagnostics.push(enrichment_diagnostic(
1363 "FALLOW_SIMILAR_CODE_CHURN_UNAVAILABLE",
1364 "git repository unavailable at project root",
1365 None,
1366 ));
1367 return;
1368 }
1369 let since = fallow_engine::churn::SinceDuration::relative(
1370 INSPECT_CHURN_WINDOW_MONTHS,
1371 fallow_engine::churn::ChurnWindowUnit::Months,
1372 "6 months",
1373 );
1374 let Some((churn, _cache_hit)) = fallow_engine::churn::analyze_churn_cached(
1375 session.root(),
1376 &since,
1377 &session.config().cache_dir,
1378 session.config().no_cache,
1379 ) else {
1380 result.diagnostics.push(enrichment_diagnostic(
1381 "FALLOW_SIMILAR_CODE_CHURN_UNAVAILABLE",
1382 "git churn analysis failed",
1383 None,
1384 ));
1385 return;
1386 };
1387 left.churn_commits = Some(churn_commits_for(
1388 &churn,
1389 &session.root().join(&left_location.path),
1390 ));
1391 right.churn_commits = Some(churn_commits_for(
1392 &churn,
1393 &session.root().join(&right_location.path),
1394 ));
1395 result.availability.churn = SimilarCodeEnrichmentState::Available;
1396 if churn.shallow_clone {
1397 result.diagnostics.push(enrichment_diagnostic(
1398 "FALLOW_SIMILAR_CODE_CHURN_SHALLOW_HISTORY",
1399 "git churn counts may undercount history because the repository is shallow",
1400 None,
1401 ));
1402 }
1403}
1404
1405fn churn_commits_for(churn: &fallow_engine::churn::ChurnResult, path: &Path) -> u64 {
1406 if let Some(file) = churn.files.get(path) {
1407 return u64::from(file.commits);
1408 }
1409 let target = normalize_path(path);
1410 churn
1411 .files
1412 .iter()
1413 .find(|(candidate, _)| normalize_path(candidate) == target)
1414 .map_or(0, |(_, file)| u64::from(file.commits))
1415}
1416
1417fn deterministic_clone_coverage(
1418 report: &fallow_engine::duplicates::DuplicationReport,
1419 root: &Path,
1420 location: &SimilarCodeLocation,
1421) -> f64 {
1422 let start = usize::try_from(location.start_line).unwrap_or(usize::MAX);
1423 let end = usize::try_from(location.end_line).unwrap_or(0);
1424 if start > end {
1425 return 0.0;
1426 }
1427 let mut covered = BTreeSet::new();
1428 for group in &report.clone_groups {
1429 if !matches!(
1430 group.kind(),
1431 fallow_engine::duplicates::CloneGroupKind::Exact
1432 ) {
1433 continue;
1434 }
1435 for instance in &group.instances {
1436 if root_relative(root, &instance.file) != location.path {
1437 continue;
1438 }
1439 let overlap_start = start.max(instance.start_line);
1440 let overlap_end = end.min(instance.end_line);
1441 if overlap_start <= overlap_end {
1442 covered.extend(overlap_start..=overlap_end);
1443 }
1444 }
1445 }
1446 let total = end.saturating_sub(start).saturating_add(1);
1447 covered.len() as f64 / total as f64
1448}
1449
1450fn unavailable_inspect_enrichment() -> SimilarCodeEnrichmentAvailability {
1451 SimilarCodeEnrichmentAvailability {
1452 graph_relationship: SimilarCodeEnrichmentState::Unavailable,
1453 entry_point_reachability: SimilarCodeEnrichmentState::Unavailable,
1454 callers: SimilarCodeEnrichmentState::Unavailable,
1455 callees: SimilarCodeEnrichmentState::Unavailable,
1456 ownership: SimilarCodeEnrichmentState::Unavailable,
1457 churn: SimilarCodeEnrichmentState::Unavailable,
1458 tests: SimilarCodeEnrichmentState::Unavailable,
1459 deterministic_clone_coverage: SimilarCodeEnrichmentState::Unavailable,
1460 runtime: SimilarCodeEnrichmentState::NotRequested,
1461 }
1462}
1463
1464fn enrichment_diagnostic(
1465 code: &'static str,
1466 message: impl Into<String>,
1467 path: Option<String>,
1468) -> SimilarCodeDiagnostic {
1469 SimilarCodeDiagnostic {
1470 domain: SimilarCodeDiagnosticDomain::Enrichment,
1471 code: code.to_owned(),
1472 message: message.into(),
1473 path,
1474 }
1475}
1476
1477fn parse_candidate_document(bytes: &[u8]) -> ProgrammaticResult<SimilarCodeOutput> {
1478 let mut value: Value = serde_json::from_slice(bytes)
1479 .map_err(|error| review_error(format!("invalid similar-code candidate JSON: {error}")))?;
1480 let object = value
1481 .as_object_mut()
1482 .ok_or_else(|| review_error("similar-code candidate document must be a JSON object"))?;
1483 let kind = object
1484 .remove("kind")
1485 .and_then(|kind| kind.as_str().map(str::to_owned))
1486 .ok_or_else(|| review_error("similar-code candidate document is missing kind"))?;
1487 if kind != "similar-code" {
1488 return Err(review_error(
1489 "candidate document kind must be `similar-code`",
1490 ));
1491 }
1492 serde_json::from_value(value)
1493 .map_err(|error| review_error(format!("invalid similar-code candidate envelope: {error}")))
1494}
1495
1496fn validate_verdict(verdict: &fallow_output::SimilarCodeVerdict) -> ProgrammaticResult<()> {
1497 verdict
1498 .validate()
1499 .map_err(|error| review_error(format!("invalid verdict implication: {error}")))?;
1500 let rationale_chars = verdict.rationale.chars().count();
1501 if rationale_chars == 0 || rationale_chars > MAX_RATIONALE_CHARS {
1502 return Err(review_error(
1503 "verdict rationale must contain 1 through 4000 characters",
1504 ));
1505 }
1506 if verdict.rationale.chars().any(char::is_control) {
1507 return Err(review_error(
1508 "verdict rationale must not contain control characters",
1509 ));
1510 }
1511 Ok(())
1512}
1513
1514fn duplicate_verdict_identity_error() -> ProgrammaticError {
1515 review_error("verdict document contains duplicate candidate or review identities")
1516}
1517
1518fn review_error(message: impl Into<String>) -> ProgrammaticError {
1519 ProgrammaticError::new(message, 2)
1520 .with_code("FALLOW_SIMILAR_CODE_REVIEW_INVALID")
1521 .with_context("similarCode.review")
1522}
1523
1524fn candidate_input_error(message: impl Into<String>) -> ProgrammaticError {
1525 ProgrammaticError::new(message, 2)
1526 .with_code("FALLOW_SIMILAR_CODE_CANDIDATE_INPUT_INVALID")
1527 .with_context("similarCode.candidates")
1528}
1529
1530fn bound_source_window(source: &str) -> String {
1531 let mut chars = source.chars();
1532 let bounded = chars
1533 .by_ref()
1534 .take(MAX_SOURCE_WINDOW_CHARS)
1535 .collect::<String>();
1536 if chars.next().is_some() {
1537 format!("{bounded}\n/* source window truncated */")
1538 } else {
1539 bounded
1540 }
1541}
1542
1543fn sha256_hex(bytes: &[u8]) -> String {
1544 hex(&Sha256::digest(bytes))
1545}
1546
1547#[expect(
1548 clippy::cast_sign_loss,
1549 reason = "the guards above leave only finite positive values"
1550)]
1551fn finite_f64_to_u64(value: f64) -> u64 {
1552 if !value.is_finite() || value <= 0.0 {
1553 0
1554 } else if value >= u64::MAX as f64 {
1555 u64::MAX
1556 } else {
1557 value.round() as u64
1558 }
1559}
1560
1561fn load_session(resolved: &ProgrammaticAnalysisContext) -> ProgrammaticResult<AnalysisSession> {
1562 let mut project = fallow_engine::project_config::config_for_project_with_load_options(
1563 resolved.root(),
1564 resolved.config_path().as_deref(),
1565 fallow_config::ConfigLoadOptions {
1566 allow_remote_extends: resolved.allow_remote_extends(),
1567 },
1568 )
1569 .map_err(|error| {
1570 ProgrammaticError::new(format!("failed to load config: {error}"), 2)
1571 .with_code("FALLOW_CONFIG_LOAD_FAILED")
1572 .with_context("analysis.configPath")
1573 })?;
1574 project.config.no_cache = resolved.no_cache();
1575 project.config.threads = resolved.threads();
1576 Ok(AnalysisSession::from_config(project))
1577}
1578
1579fn build_ignore_set(patterns: &[String]) -> ProgrammaticResult<GlobSet> {
1580 let mut builder = GlobSetBuilder::new();
1581 for pattern in patterns {
1582 builder.add(fallow_config::compile_path_glob(pattern).map_err(|error| {
1583 ProgrammaticError::new(
1584 format!("invalid `similarCode.ignore` pattern `{pattern}`: {error}"),
1585 2,
1586 )
1587 .with_code("FALLOW_INVALID_SIMILAR_CODE_IGNORE")
1588 .with_context("similarCode.ignore")
1589 })?);
1590 }
1591 builder.build().map_err(|error| {
1592 ProgrammaticError::new(
1593 format!("failed to compile `similarCode.ignore`: {error}"),
1594 2,
1595 )
1596 .with_code("FALLOW_INVALID_SIMILAR_CODE_IGNORE")
1597 .with_context("similarCode.ignore")
1598 })
1599}
1600
1601fn map_candidate(
1602 candidate: fallow_engine::similar_code::SimilarCodeCandidate,
1603 metadata: &FxHashMap<(String, u32, u32), &ExtractedSimilarCodeFunction>,
1604) -> Option<SimilarCodeCandidate> {
1605 let left = metadata.get(&location_key(&candidate.left))?;
1606 let right = metadata.get(&location_key(&candidate.right))?;
1607 Some(SimilarCodeCandidate {
1608 candidate_id: candidate.candidate_id,
1609 review_key: candidate.review_key,
1610 left: output_location(left),
1611 right: output_location(right),
1612 similarity: candidate.similarity,
1613 similarity_band: similarity_band(candidate.similarity),
1614 verification_status: SimilarCodeVerificationStatus::Unverified,
1615 enrichment: raw_enrichment_availability(),
1616 actions: vec![
1617 SimilarCodeAction {
1618 action: SimilarCodeActionType::Inspect,
1619 description: "Inspect bounded source, graph, ownership, churn, test, and deterministic clone evidence".to_owned(),
1620 read_only: true,
1621 },
1622 SimilarCodeAction {
1623 action: SimilarCodeActionType::Review,
1624 description: "Join this immutable candidate with a separate evidence-grounded verdict".to_owned(),
1625 read_only: true,
1626 },
1627 ],
1628 })
1629}
1630
1631fn output_location(function: &ExtractedSimilarCodeFunction) -> SimilarCodeLocation {
1632 SimilarCodeLocation {
1633 path: function.location.file.clone(),
1634 name: function.name.clone(),
1635 start_line: function.location.start_line,
1636 start_column: function.location.start_column_utf8.saturating_add(1),
1637 end_line: function.location.end_line,
1638 end_column: function.location.end_column_utf8.saturating_add(1),
1639 source_sha256: hex(function.source_sha256.as_bytes()),
1640 }
1641}
1642
1643fn similar_code_scope_active(
1644 options: &SimilarCodeOptions,
1645 resolved: &ProgrammaticAnalysisContext,
1646 changed_files: Option<&FxHashSet<PathBuf>>,
1647 workspace_roots: Option<&[PathBuf]>,
1648) -> bool {
1649 !options.files.is_empty()
1650 || changed_files.is_some()
1651 || resolved.diff_index().is_some()
1652 || workspace_roots.is_some()
1653}
1654
1655fn similar_code_path_in_scope(
1656 path: &str,
1657 options: &SimilarCodeOptions,
1658 resolved: &ProgrammaticAnalysisContext,
1659 changed_files: Option<&FxHashSet<PathBuf>>,
1660 workspace_roots: Option<&[PathBuf]>,
1661) -> bool {
1662 if !options.files.is_empty()
1663 && !options
1664 .files
1665 .iter()
1666 .any(|filter| normalize_path(filter) == path)
1667 {
1668 return false;
1669 }
1670 if let Some(changed_files) = changed_files
1671 && !changed_files.contains(Path::new(path))
1672 && !changed_files.contains(&resolved.root().join(path))
1673 {
1674 return false;
1675 }
1676 if let Some(diff) = resolved.diff_index()
1677 && !diff.touches_file(&diff.key_for_root_relative(path))
1678 {
1679 return false;
1680 }
1681 if let Some(workspace_roots) = workspace_roots
1682 && !workspace_roots
1683 .iter()
1684 .any(|workspace| resolved.root().join(path).starts_with(workspace))
1685 {
1686 return false;
1687 }
1688 true
1689}
1690
1691#[expect(
1692 clippy::too_many_arguments,
1693 reason = "each argument maps directly to one public completion-accounting field"
1694)]
1695fn phases(
1696 admitted_files: usize,
1697 total_files: usize,
1698 extracted_functions: usize,
1699 selected_functions: usize,
1700 embedded_functions: usize,
1701 comparisons: usize,
1702 completeness: PhaseCompleteness,
1703 missing_vectors: usize,
1704 truncated_functions: usize,
1705 source_read_failures: usize,
1706) -> Vec<SimilarCodePhaseCompletion> {
1707 vec![
1708 phase(
1709 SimilarCodePhase::Discovery,
1710 phase_status(completeness.discovery),
1711 admitted_files,
1712 Some(total_files),
1713 (!completeness.discovery)
1714 .then(|| "the file admission limit omitted source files".to_owned()),
1715 ),
1716 phase(
1717 SimilarCodePhase::Extraction,
1718 phase_status(completeness.extraction),
1719 extracted_functions,
1720 None,
1721 (!completeness.extraction).then(|| {
1722 if source_read_failures > 0 {
1723 "one or more admitted source files could not be read".to_owned()
1724 } else {
1725 "one or more function forms or source fragments were outside extraction limits"
1726 .to_owned()
1727 }
1728 }),
1729 ),
1730 phase(
1731 SimilarCodePhase::Cache,
1732 SimilarCodePhaseStatus::Complete,
1733 selected_functions,
1734 Some(selected_functions),
1735 None,
1736 ),
1737 phase(
1738 SimilarCodePhase::Embedding,
1739 phase_status(completeness.embedding),
1740 embedded_functions,
1741 Some(selected_functions),
1742 (!completeness.embedding).then(|| {
1743 if missing_vectors > 0 {
1744 "the provider did not return every selected vector".to_owned()
1745 } else if truncated_functions > 0 {
1746 "the provider truncated one or more admitted functions".to_owned()
1747 } else {
1748 "embedding did not complete its admitted scope".to_owned()
1749 }
1750 }),
1751 ),
1752 phase(
1753 SimilarCodePhase::Validation,
1754 SimilarCodePhaseStatus::Complete,
1755 embedded_functions,
1756 Some(embedded_functions),
1757 None,
1758 ),
1759 phase(
1760 SimilarCodePhase::Comparison,
1761 phase_status(completeness.comparison),
1762 comparisons,
1763 None,
1764 (!completeness.comparison)
1765 .then(|| "comparison limits omitted candidate pairs".to_owned()),
1766 ),
1767 phase(
1768 SimilarCodePhase::Enrichment,
1769 SimilarCodePhaseStatus::Skipped,
1770 0,
1771 None,
1772 Some("raw discovery defers source-grounded enrichment to inspect".to_owned()),
1773 ),
1774 ]
1775}
1776
1777const fn phase_status(complete: bool) -> SimilarCodePhaseStatus {
1778 if complete {
1779 SimilarCodePhaseStatus::Complete
1780 } else {
1781 SimilarCodePhaseStatus::Partial
1782 }
1783}
1784
1785fn phase(
1786 phase: SimilarCodePhase,
1787 status: SimilarCodePhaseStatus,
1788 processed: usize,
1789 total: Option<usize>,
1790 reason: Option<String>,
1791) -> SimilarCodePhaseCompletion {
1792 SimilarCodePhaseCompletion {
1793 phase,
1794 status,
1795 processed: usize_to_u64(processed),
1796 total: total.map(usize_to_u64),
1797 reason,
1798 }
1799}
1800
1801fn output_limits(
1802 engine: EngineLimits,
1803 extraction: SimilarCodeExtractionLimits,
1804) -> SimilarCodeLimits {
1805 SimilarCodeLimits {
1806 max_files: usize_to_u64(MAX_FILES),
1807 max_functions: usize_to_u64(engine.max_functions),
1808 max_source_bytes: usize_to_u64(extraction.max_total_source_bytes),
1809 max_function_bytes: usize_to_u64(extraction.max_source_bytes_per_function),
1810 max_batch_size: usize_to_u64(similar_code::embedding_batch_size()),
1811 max_vector_bytes: usize_to_u64(engine.max_vector_bytes),
1812 max_comparisons: usize_to_u64(engine.max_comparisons),
1813 max_candidates: usize_to_u64(engine.max_candidates),
1814 max_neighbors_per_function: usize_to_u64(engine.max_neighbors_per_function),
1815 timeout_ms: MAX_RUN_TIMEOUT_MS,
1816 }
1817}
1818
1819fn generation(
1820 provider: &SimilarCodeProviderStatus,
1821 threshold: f64,
1822 min_lines: usize,
1823 scope: SimilarCodeScopeProvenance,
1824) -> SimilarCodeGeneration {
1825 SimilarCodeGeneration {
1826 extraction_semantics_version: SIMILAR_CODE_EXTRACTION_SEMANTICS_VERSION,
1827 embedding_semantics_version: similar_code::embedding_semantics_version(),
1828 provider: SimilarCodeProviderProvenance {
1829 provider: SimilarCodeProvider::OfficialLocalCompanion,
1830 companion_version: provider.sidecar_version.clone(),
1831 protocol_version: provider.protocol_version,
1832 source_left_machine: false,
1833 },
1834 model: SimilarCodeModelProvenance {
1835 model_id: provider.model_id.clone(),
1836 revision: provider.model_revision.clone(),
1837 artifact_sha256: similar_code::model_artifact_sha256().to_owned(),
1838 license: provider.license.clone(),
1839 dimensions: u32::try_from(provider.dimensions).unwrap_or(u32::MAX),
1840 },
1841 parameters: SimilarCodeGenerationParameters {
1842 dtype: "f32".to_owned(),
1843 pooling: "mean".to_owned(),
1844 normalized: true,
1845 batch_size: u32::try_from(similar_code::embedding_batch_size()).unwrap_or(u32::MAX),
1846 max_tokens: u32::try_from(provider.max_tokens).unwrap_or(u32::MAX),
1847 parameter_sha256: similar_code::parameter_sha256(),
1848 },
1849 scope,
1850 threshold,
1851 min_lines: usize_to_u64(min_lines),
1852 }
1853}
1854
1855fn raw_enrichment_availability() -> SimilarCodeEnrichmentAvailability {
1856 SimilarCodeEnrichmentAvailability {
1857 graph_relationship: SimilarCodeEnrichmentState::NotRequested,
1858 entry_point_reachability: SimilarCodeEnrichmentState::NotRequested,
1859 callers: SimilarCodeEnrichmentState::NotRequested,
1860 callees: SimilarCodeEnrichmentState::NotRequested,
1861 ownership: SimilarCodeEnrichmentState::NotRequested,
1862 churn: SimilarCodeEnrichmentState::NotRequested,
1863 tests: SimilarCodeEnrichmentState::NotRequested,
1864 deterministic_clone_coverage: SimilarCodeEnrichmentState::NotRequested,
1865 runtime: SimilarCodeEnrichmentState::NotRequested,
1866 }
1867}
1868
1869fn map_extraction_skip(reason: SimilarCodeExtractionSkipReason) -> SimilarCodeSkipReason {
1870 match reason {
1871 SimilarCodeExtractionSkipReason::GeneratedSource => SimilarCodeSkipReason::GeneratedSource,
1872 SimilarCodeExtractionSkipReason::SourceBytesPerFunctionLimit => {
1873 SimilarCodeSkipReason::FunctionTooLarge
1874 }
1875 SimilarCodeExtractionSkipReason::FunctionLimit => SimilarCodeSkipReason::InputLimit,
1876 SimilarCodeExtractionSkipReason::TotalSourceBytesLimit => {
1877 SimilarCodeSkipReason::SourceBytesLimit
1878 }
1879 _ => SimilarCodeSkipReason::UnsupportedFunction,
1880 }
1881}
1882
1883fn map_engine_skip(reason: EngineSkipReason) -> SimilarCodeSkipReason {
1884 match reason {
1885 EngineSkipReason::VectorMemoryLimit => SimilarCodeSkipReason::VectorMemoryLimit,
1886 EngineSkipReason::ComparisonLimit => SimilarCodeSkipReason::ComparisonLimit,
1887 EngineSkipReason::CandidateLimit => SimilarCodeSkipReason::CandidateLimit,
1888 EngineSkipReason::NeighborLimit => SimilarCodeSkipReason::NeighborLimit,
1889 _ => SimilarCodeSkipReason::InputLimit,
1890 }
1891}
1892
1893fn cache_status(disabled: bool, hits: usize, misses: usize) -> SimilarCodeCacheStatus {
1894 if disabled {
1895 SimilarCodeCacheStatus::Disabled
1896 } else if misses == 0 {
1897 SimilarCodeCacheStatus::Hit
1898 } else if hits == 0 {
1899 SimilarCodeCacheStatus::Miss
1900 } else {
1901 SimilarCodeCacheStatus::Mixed
1902 }
1903}
1904
1905fn similarity_band(similarity: f64) -> SimilarCodeSimilarityBand {
1906 if similarity >= VERY_HIGH_SIMILARITY {
1907 SimilarCodeSimilarityBand::VeryHigh
1908 } else if similarity >= HIGH_SIMILARITY {
1909 SimilarCodeSimilarityBand::High
1910 } else {
1911 SimilarCodeSimilarityBand::Moderate
1912 }
1913}
1914
1915fn validate_options(options: &SimilarCodeOptions) -> ProgrammaticResult<()> {
1916 if let Some(threshold) = options.threshold {
1917 validate_threshold(threshold)?;
1918 }
1919 if options.min_lines == Some(0) {
1920 return Err(ProgrammaticError::new("`min_lines` must be at least 1", 2)
1921 .with_code("FALLOW_INVALID_SIMILAR_CODE_MIN_LINES")
1922 .with_context("similarCode.minLines"));
1923 }
1924 if options.top == Some(0) {
1925 return Err(ProgrammaticError::new("`top` must be at least 1", 2)
1926 .with_code("FALLOW_INVALID_SIMILAR_CODE_TOP")
1927 .with_context("similarCode.top"));
1928 }
1929 for path in &options.files {
1930 if path.is_absolute()
1931 || path
1932 .components()
1933 .any(|part| matches!(part, std::path::Component::ParentDir))
1934 {
1935 return Err(ProgrammaticError::new(
1936 "`file` paths must be project-root-relative and must not contain `..`",
1937 2,
1938 )
1939 .with_code("FALLOW_INVALID_SIMILAR_CODE_FILE")
1940 .with_context("similarCode.files"));
1941 }
1942 }
1943 Ok(())
1944}
1945
1946fn validate_threshold(threshold: f64) -> ProgrammaticResult<()> {
1947 if !threshold.is_finite() || !(0.0..=1.0).contains(&threshold) {
1948 return Err(
1949 ProgrammaticError::new("`threshold` must be finite and between 0 and 1", 2)
1950 .with_code("FALLOW_INVALID_SIMILAR_CODE_THRESHOLD")
1951 .with_context("similarCode.threshold"),
1952 );
1953 }
1954 Ok(())
1955}
1956
1957#[expect(
1958 clippy::needless_pass_by_value,
1959 reason = "map_err supplies owned provider failures and the mapper consumes that boundary"
1960)]
1961fn provider_error(error: ProviderError) -> ProgrammaticError {
1962 let exit_code = if matches!(error, ProviderError::NotReady(_)) {
1963 3
1964 } else {
1965 2
1966 };
1967 let code = if exit_code == 3 {
1968 "FALLOW_SIMILAR_CODE_NOT_READY"
1969 } else {
1970 "FALLOW_SIMILAR_CODE_PROVIDER_FAILED"
1971 };
1972 ProgrammaticError::new(error.message(), exit_code)
1973 .with_code(code)
1974 .with_context("similarCode.provider")
1975}
1976
1977fn engine_error(error: impl std::fmt::Display) -> ProgrammaticError {
1978 ProgrammaticError::new(format!("invalid similar-code evaluation: {error}"), 2)
1979 .with_code("FALLOW_SIMILAR_CODE_EVALUATION_FAILED")
1980 .with_context("similarCode.evaluation")
1981}
1982
1983fn location_key(
1984 location: &fallow_engine::source::similar_code::SimilarCodeFunctionLocation,
1985) -> (String, u32, u32) {
1986 (
1987 location.file.clone(),
1988 location.start_byte,
1989 location.end_byte,
1990 )
1991}
1992
1993fn root_relative(root: &Path, path: &Path) -> String {
1994 normalize_path(path.strip_prefix(root).unwrap_or(path))
1995}
1996
1997fn normalize_path(path: &Path) -> String {
1998 path.to_string_lossy().replace('\\', "/")
1999}
2000
2001fn add_skip(
2002 skips: &mut BTreeMap<SimilarCodeSkipReason, usize>,
2003 reason: SimilarCodeSkipReason,
2004 count: usize,
2005) {
2006 *skips.entry(reason).or_default() += count;
2007}
2008
2009fn extraction_is_complete(
2010 skips: &BTreeMap<SimilarCodeSkipReason, usize>,
2011 source_read_failures: usize,
2012) -> bool {
2013 source_read_failures == 0
2014 && skips.iter().all(|(reason, count)| {
2015 *count == 0 || matches!(reason, SimilarCodeSkipReason::BelowMinimumLines)
2016 })
2017}
2018
2019const fn remaining_extraction_inputs(total: usize, current_index: usize) -> usize {
2020 total.saturating_sub(current_index)
2021}
2022
2023const fn exhausted_extraction_limit(
2024 remaining_functions: usize,
2025 remaining_source_bytes: usize,
2026) -> Option<SimilarCodeSkipReason> {
2027 if remaining_functions == 0 {
2028 Some(SimilarCodeSkipReason::InputLimit)
2029 } else if remaining_source_bytes == 0 {
2030 Some(SimilarCodeSkipReason::SourceBytesLimit)
2031 } else {
2032 None
2033 }
2034}
2035
2036fn hex(bytes: &[u8]) -> String {
2037 bytes.iter().fold(
2038 String::with_capacity(bytes.len().saturating_mul(2)),
2039 |mut output, byte| {
2040 let _ = write!(output, "{byte:02x}");
2041 output
2042 },
2043 )
2044}
2045
2046fn duration_ms(started: Instant) -> u64 {
2047 u64::try_from(started.elapsed().as_millis()).unwrap_or(u64::MAX)
2048}
2049
2050fn usize_to_u64(value: usize) -> u64 {
2051 u64::try_from(value).unwrap_or(u64::MAX)
2052}
2053
2054#[cfg(test)]
2055#[expect(
2056 clippy::unwrap_used,
2057 reason = "deterministic fixtures fail immediately"
2058)]
2059mod tests {
2060 use super::*;
2061 use std::sync::{Arc, Mutex};
2062 use std::time::Duration;
2063
2064 use crate::similar_code::{
2065 EmbeddingBatch, EmbeddingBatchVector, EmbeddingSession, EmbeddingSessionFactory,
2066 };
2067
2068 #[test]
2069 fn ignore_glob_single_star_stays_within_one_segment() {
2070 let set = build_ignore_set(&["src/*.ts".to_string()]).unwrap();
2071 assert!(set.is_match("src/a.ts"));
2072 assert!(!set.is_match("src/nested/a.ts"));
2073 }
2074
2075 #[derive(Default)]
2076 struct FakeProviderState {
2077 spawns: usize,
2078 batches: usize,
2079 complete_batches: Option<usize>,
2080 }
2081
2082 struct FakeEmbeddingSession {
2083 state: Arc<Mutex<FakeProviderState>>,
2084 dimensions: usize,
2085 }
2086
2087 impl EmbeddingSession for FakeEmbeddingSession {
2088 #[expect(
2089 clippy::unwrap_in_result,
2090 reason = "test helper: a panic fails the test"
2091 )]
2092 fn embed(&mut self, functions: &[(u32, &str)]) -> Result<EmbeddingBatch, String> {
2093 let should_return_partial = {
2094 let mut state = self.state.lock().unwrap();
2095 state.batches += 1;
2096 state
2097 .complete_batches
2098 .is_some_and(|limit| state.batches > limit)
2099 };
2100 if should_return_partial {
2101 return Ok(EmbeddingBatch {
2102 vectors: Vec::new(),
2103 inference_ms: 0.0,
2104 problem: Some("fixture provider returned a bounded partial batch".to_owned()),
2105 });
2106 }
2107 let vectors = functions
2108 .iter()
2109 .map(|(key, _)| {
2110 let mut values = vec![0.0; self.dimensions];
2111 values[0] = 1.0;
2112 EmbeddingBatchVector {
2113 key: *key,
2114 values,
2115 truncated: false,
2116 }
2117 })
2118 .collect();
2119 Ok(EmbeddingBatch {
2120 vectors,
2121 inference_ms: 0.25,
2122 problem: None,
2123 })
2124 }
2125 }
2126
2127 struct FakeEmbeddingFactory {
2128 state: Arc<Mutex<FakeProviderState>>,
2129 dimensions: usize,
2130 }
2131
2132 impl EmbeddingSessionFactory for FakeEmbeddingFactory {
2133 #[expect(
2134 clippy::unwrap_in_result,
2135 reason = "test helper: a panic fails the test"
2136 )]
2137 fn spawn(&mut self) -> Result<Box<dyn EmbeddingSession>, String> {
2138 self.state.lock().unwrap().spawns += 1;
2139 Ok(Box::new(FakeEmbeddingSession {
2140 state: Arc::clone(&self.state),
2141 dimensions: self.dimensions,
2142 }))
2143 }
2144 }
2145
2146 struct FixtureEmbedder {
2147 provider_cache_dir: PathBuf,
2148 run_timeout: Duration,
2149 factory: FakeEmbeddingFactory,
2150 }
2151
2152 impl RuntimeEmbedder for FixtureEmbedder {
2153 fn embed(
2154 &mut self,
2155 project_root: &Path,
2156 no_cache: bool,
2157 inputs: &[EmbeddingInput<'_>],
2158 ) -> Result<EmbeddingResult, ProviderError> {
2159 similar_code::embed_selected_with_factory(
2160 &self.provider_cache_dir,
2161 project_root,
2162 no_cache,
2163 inputs,
2164 self.run_timeout,
2165 &mut self.factory,
2166 )
2167 }
2168 }
2169
2170 fn similar_code_fixture() -> (tempfile::TempDir, PathBuf, SimilarCodeProviderStatus) {
2171 let temp = tempfile::tempdir().unwrap();
2172 let project = temp.path().join("project");
2173 let cache_root = temp.path().join("user-cache");
2174 let provider_cache_dir = cache_root.join("models").join("fixture-model");
2175 std::fs::create_dir_all(project.join("src")).unwrap();
2176 std::fs::create_dir_all(&cache_root).unwrap();
2177 std::fs::write(
2178 project.join("package.json"),
2179 r#"{"name":"similar-code-runtime-fixture","private":true}"#,
2180 )
2181 .unwrap();
2182 for (name, value) in [("a", 1), ("b", 2), ("c", 3)] {
2183 std::fs::write(
2184 project.join("src").join(format!("{name}.ts")),
2185 format!(
2186 "export function {name}(input: number) {{\n const adjusted = input + {value};\n return adjusted * 2;\n}}\n"
2187 ),
2188 )
2189 .unwrap();
2190 }
2191 let (model_id, model_revision, dimensions, license) = similar_code::provider_identity();
2192 let status = SimilarCodeProviderStatus {
2193 protocol_version: 2,
2194 embedding_semantics_version: similar_code::embedding_semantics_version(),
2195 sidecar_version: env!("CARGO_PKG_VERSION").to_owned(),
2196 model_ready: true,
2197 model_id: model_id.to_owned(),
2198 model_revision: model_revision.to_owned(),
2199 dimensions,
2200 max_tokens: 512,
2201 license: license.to_owned(),
2202 cache_dir: provider_cache_dir.to_string_lossy().into_owned(),
2203 download_bytes: similar_code::model_download_bytes(),
2204 analysis_offline: true,
2205 integrity_verified: true,
2206 problem: None,
2207 downloaded: None,
2208 };
2209 (temp, project, status)
2210 }
2211
2212 fn fixture_options(project: &Path) -> SimilarCodeOptions {
2213 SimilarCodeOptions {
2214 analysis: crate::AnalysisOptions {
2215 root: Some(project.to_path_buf()),
2216 ..crate::AnalysisOptions::default()
2217 },
2218 threshold: Some(0.9),
2219 min_lines: Some(2),
2220 ..SimilarCodeOptions::default()
2221 }
2222 }
2223
2224 fn run_with_fixture(
2225 options: &SimilarCodeOptions,
2226 status: &SimilarCodeProviderStatus,
2227 embedder: &mut FixtureEmbedder,
2228 ) -> ProgrammaticResult<SimilarCodeOutput> {
2229 let resolved = resolve_programmatic_analysis_context(&options.analysis)?;
2230 resolved
2231 .install(|| run_similar_code_inner_with_embedder(options, &resolved, status, embedder))
2232 }
2233
2234 fn find_cache_file(root: &Path) -> Option<PathBuf> {
2235 for entry in std::fs::read_dir(root).ok()? {
2236 let path = entry.ok()?.path();
2237 if path.file_name().is_some_and(|name| name == "vectors.bin") {
2238 return Some(path);
2239 }
2240 if path.is_dir()
2241 && let Some(found) = find_cache_file(&path)
2242 {
2243 return Some(found);
2244 }
2245 }
2246 None
2247 }
2248
2249 #[test]
2250 fn similar_code_runtime_covers_cold_warm_corrupt_cache_scope_and_output_contract() {
2251 let (_temp, project, status) = similar_code_fixture();
2252 let provider_cache_dir = PathBuf::from(&status.cache_dir);
2253 let state = Arc::new(Mutex::new(FakeProviderState::default()));
2254 let mut embedder = FixtureEmbedder {
2255 provider_cache_dir: provider_cache_dir.clone(),
2256 run_timeout: Duration::from_secs(5),
2257 factory: FakeEmbeddingFactory {
2258 state: Arc::clone(&state),
2259 dimensions: status.dimensions,
2260 },
2261 };
2262 let mut options = fixture_options(&project);
2263 options.files = vec![PathBuf::from("src/a.ts")];
2264
2265 let cold = run_with_fixture(&options, &status, &mut embedder).unwrap();
2266 assert!(!cold.candidates.is_empty());
2267 assert!(cold.candidates.iter().all(|candidate| {
2268 candidate.left.path == "src/a.ts" || candidate.right.path == "src/a.ts"
2269 }));
2270 assert_eq!(
2271 cold.completion.status,
2272 SimilarCodeCompletionStatus::Complete
2273 );
2274 assert!(cold.completion.cache.misses > 0);
2275 assert!(cold.completion.cache.writes > 0);
2276 let cold_spawns = state.lock().unwrap().spawns;
2277 let cold_ids = cold
2278 .candidates
2279 .iter()
2280 .map(|candidate| candidate.candidate_id.clone())
2281 .collect::<Vec<_>>();
2282 let json = serde_json::to_value(&cold).unwrap();
2283 assert_eq!(json["generation"]["embedding_semantics_version"], 1);
2284 assert_eq!(json["generation"]["provider"]["source_left_machine"], false);
2285 assert_eq!(json["generation"]["scope"]["active"], true);
2286 assert_eq!(
2287 json["generation"]["scope"]["paths"],
2288 serde_json::json!(["src/a.ts"])
2289 );
2290 assert!(json["completion"]["cache"].is_object());
2291
2292 let warm = run_with_fixture(&options, &status, &mut embedder).unwrap();
2293 assert_eq!(state.lock().unwrap().spawns, cold_spawns);
2294 assert!(warm.completion.cache.hits > 0);
2295 assert_eq!(warm.completion.cache.writes, 0);
2296 assert_eq!(
2297 warm.candidates
2298 .iter()
2299 .map(|candidate| candidate.candidate_id.clone())
2300 .collect::<Vec<_>>(),
2301 cold_ids
2302 );
2303
2304 let cache_root = provider_cache_dir.parent().and_then(Path::parent).unwrap();
2305 let cache_file = find_cache_file(cache_root).unwrap();
2306 std::fs::write(&cache_file, b"corrupt cache fixture").unwrap();
2307 let recovered = run_with_fixture(&options, &status, &mut embedder).unwrap();
2308 assert_eq!(recovered.completion.cache.invalid_entries, 1);
2309 assert!(recovered.completion.cache.writes > 0);
2310 assert!(state.lock().unwrap().spawns > cold_spawns);
2311 }
2312
2313 #[test]
2314 fn snapshot_inspect_survives_ranking_crowd_out_and_rejects_stale_source() {
2315 let (_temp, project, status) = similar_code_fixture();
2316 let state = Arc::new(Mutex::new(FakeProviderState::default()));
2317 let mut embedder = FixtureEmbedder {
2318 provider_cache_dir: PathBuf::from(&status.cache_dir),
2319 run_timeout: Duration::from_secs(5),
2320 factory: FakeEmbeddingFactory {
2321 state,
2322 dimensions: status.dimensions,
2323 },
2324 };
2325 let discovery =
2326 run_with_fixture(&fixture_options(&project), &status, &mut embedder).unwrap();
2327 let candidate_id = discovery.candidates.last().unwrap().candidate_id.clone();
2328 let tagged = fallow_output::serialize_similar_code_json_output(discovery).unwrap();
2329 let snapshot = select_similar_code_candidate_snapshot(
2330 &serde_json::to_vec(&tagged).unwrap(),
2331 &candidate_id,
2332 )
2333 .unwrap();
2334
2335 let mut legacy_options = fixture_options(&project);
2336 legacy_options.files = vec![
2337 PathBuf::from(&snapshot.candidate.left.path),
2338 PathBuf::from(&snapshot.candidate.right.path),
2339 ];
2340 legacy_options.top = Some(1);
2341 let endpoint_reranked = run_with_fixture(&legacy_options, &status, &mut embedder).unwrap();
2342 assert_eq!(endpoint_reranked.candidates.len(), 1);
2343 assert!(
2344 endpoint_reranked
2345 .candidates
2346 .iter()
2347 .all(|candidate| candidate.candidate_id != candidate_id),
2348 "the endpoint-only legacy rerank must reproduce the crowd-out condition"
2349 );
2350
2351 let inspect_options = SimilarCodeInspectOptions {
2352 analysis: crate::AnalysisOptions {
2353 root: Some(project.clone()),
2354 ..crate::AnalysisOptions::default()
2355 },
2356 snapshot: snapshot.clone(),
2357 };
2358 let inspected = inspect_similar_code(&inspect_options).unwrap();
2359 assert_eq!(inspected.candidate.candidate_id, candidate_id);
2360
2361 let stale_path = project.join(&snapshot.candidate.left.path);
2362 let stale_source = std::fs::read_to_string(&stale_path).unwrap();
2363 std::fs::write(
2364 &stale_path,
2365 stale_source.replace("return adjusted * 2", "return adjusted * 3"),
2366 )
2367 .unwrap();
2368 let error = inspect_similar_code(&inspect_options).unwrap_err();
2369 assert_eq!(
2370 error.code.as_deref(),
2371 Some("FALLOW_SIMILAR_CODE_CANDIDATE_STALE")
2372 );
2373 }
2374
2375 #[test]
2376 fn snapshot_inspect_rejects_oversized_endpoint_before_source_allocation() {
2377 let temp = tempfile::tempdir().unwrap();
2378 let project = dunce::canonicalize(temp.path()).unwrap();
2379 let source_path = project.join("src/endpoint.ts");
2380 std::fs::create_dir_all(source_path.parent().unwrap()).unwrap();
2381 std::fs::write(
2382 &source_path,
2383 "export function candidate() {\n return true;\n}\n",
2384 )
2385 .unwrap();
2386 std::fs::OpenOptions::new()
2387 .write(true)
2388 .open(&source_path)
2389 .unwrap()
2390 .set_len(MAX_INSPECT_SOURCE_BYTES + 1)
2391 .unwrap();
2392
2393 let error = inspect_side(&project, &location("src/endpoint.ts", 1, 3)).unwrap_err();
2394 assert_eq!(
2395 error.code.as_deref(),
2396 Some("FALLOW_SIMILAR_CODE_CANDIDATE_STALE")
2397 );
2398 assert!(error.message.contains("5 MiB per-file limit"));
2399 }
2400
2401 #[test]
2402 fn snapshot_inspect_does_not_rebind_an_identical_same_line_function() {
2403 let temp = tempfile::tempdir().unwrap();
2404 let project = dunce::canonicalize(temp.path()).unwrap();
2405 let relative = Path::new("src/duplicates.js");
2406 let source_path = project.join(relative);
2407 std::fs::create_dir_all(source_path.parent().unwrap()).unwrap();
2408 let function = "function duplicate() { return 1; }";
2409 let original = format!("{function} {function}\n");
2410 std::fs::write(&source_path, &original).unwrap();
2411
2412 let extracted = fallow_engine::source::similar_code::extract(
2413 relative,
2414 &original,
2415 SimilarCodeExtractionLimits::default(),
2416 );
2417 assert_eq!(extracted.functions.len(), 2);
2418 let snapshot = output_location(&extracted.functions[0]);
2419 assert_eq!(
2420 snapshot.source_sha256,
2421 output_location(&extracted.functions[1]).source_sha256
2422 );
2423 assert_ne!(
2424 snapshot.start_column,
2425 output_location(&extracted.functions[1]).start_column
2426 );
2427
2428 let second_start = function.len() + 1;
2429 std::fs::write(
2430 &source_path,
2431 format!("{}{function}\n", " ".repeat(second_start)),
2432 )
2433 .unwrap();
2434
2435 let error = inspect_side(&project, &snapshot).unwrap_err();
2436 assert_eq!(
2437 error.code.as_deref(),
2438 Some("FALLOW_SIMILAR_CODE_CANDIDATE_STALE")
2439 );
2440 }
2441
2442 #[test]
2443 fn similar_code_runtime_reports_partial_provider_output_and_bounded_timeout() {
2444 let (_temp, project, status) = similar_code_fixture();
2445 let provider_cache_dir = PathBuf::from(&status.cache_dir);
2446 let partial_state = Arc::new(Mutex::new(FakeProviderState {
2447 complete_batches: Some(2),
2448 ..FakeProviderState::default()
2449 }));
2450 let mut partial_embedder = FixtureEmbedder {
2451 provider_cache_dir: provider_cache_dir.clone(),
2452 run_timeout: Duration::from_secs(5),
2453 factory: FakeEmbeddingFactory {
2454 state: partial_state,
2455 dimensions: status.dimensions,
2456 },
2457 };
2458 let mut options = fixture_options(&project);
2459 options.analysis.no_cache = true;
2460
2461 let partial = run_with_fixture(&options, &status, &mut partial_embedder).unwrap();
2462 assert_eq!(
2463 partial.completion.status,
2464 SimilarCodeCompletionStatus::Partial
2465 );
2466 assert!(partial.completion.skips.iter().any(|skip| {
2467 skip.phase == SimilarCodePhase::Embedding
2468 && skip.reason == SimilarCodeSkipReason::ProviderFailure
2469 }));
2470 assert!(
2471 partial
2472 .diagnostics
2473 .iter()
2474 .any(|diagnostic| { diagnostic.code == "FALLOW_SIMILAR_CODE_PROVIDER_PARTIAL" })
2475 );
2476
2477 let timeout_state = Arc::new(Mutex::new(FakeProviderState::default()));
2478 let mut timeout_embedder = FixtureEmbedder {
2479 provider_cache_dir,
2480 run_timeout: Duration::ZERO,
2481 factory: FakeEmbeddingFactory {
2482 state: Arc::clone(&timeout_state),
2483 dimensions: status.dimensions,
2484 },
2485 };
2486 let error = run_with_fixture(&options, &status, &mut timeout_embedder).unwrap_err();
2487 assert_eq!(
2488 error.code.as_deref(),
2489 Some("FALLOW_SIMILAR_CODE_PROVIDER_FAILED")
2490 );
2491 assert_eq!(timeout_state.lock().unwrap().spawns, 0);
2492 }
2493
2494 fn location(path: &str, start_line: u32, end_line: u32) -> SimilarCodeLocation {
2495 SimilarCodeLocation {
2496 path: path.to_owned(),
2497 name: "candidate".to_owned(),
2498 start_line,
2499 start_column: 1,
2500 end_line,
2501 end_column: 1,
2502 source_sha256: "00".repeat(32),
2503 }
2504 }
2505
2506 fn file_trace(imports_from: &[&str], imported_by: &[&str]) -> fallow_engine::trace::FileTrace {
2507 fallow_engine::trace::FileTrace {
2508 file: PathBuf::from("src/current.ts"),
2509 is_reachable: true,
2510 is_entry_point: false,
2511 exports: Vec::new(),
2512 imports_from: imports_from.iter().map(PathBuf::from).collect(),
2513 imported_by: imported_by.iter().map(PathBuf::from).collect(),
2514 re_exports: Vec::new(),
2515 sources: Vec::new(),
2516 }
2517 }
2518
2519 fn side_evidence() -> SimilarCodeSideEvidence {
2520 SimilarCodeSideEvidence {
2521 source_window: None,
2522 parameter_count: None,
2523 is_async: None,
2524 is_generator: None,
2525 has_await: None,
2526 has_throw: None,
2527 side_effect_hint: None,
2528 entry_point_reachable: None,
2529 callers: Vec::new(),
2530 callees: Vec::new(),
2531 owners: Vec::new(),
2532 churn_commits: None,
2533 tests: Vec::new(),
2534 deterministic_clone_coverage: None,
2535 runtime_observations: None,
2536 }
2537 }
2538
2539 #[test]
2540 fn similar_code_phase_statuses_identify_only_the_incomplete_phase() {
2541 let phases = phases(
2542 5,
2543 5,
2544 3,
2545 3,
2546 3,
2547 3,
2548 PhaseCompleteness {
2549 discovery: true,
2550 extraction: false,
2551 embedding: true,
2552 comparison: true,
2553 },
2554 0,
2555 0,
2556 0,
2557 );
2558
2559 assert_eq!(phases[0].status, SimilarCodePhaseStatus::Complete);
2560 assert_eq!(phases[1].status, SimilarCodePhaseStatus::Partial);
2561 assert!(phases[1].reason.is_some());
2562 assert_eq!(phases[3].status, SimilarCodePhaseStatus::Complete);
2563 assert_eq!(phases[4].status, SimilarCodePhaseStatus::Complete);
2564 assert_eq!(phases[5].status, SimilarCodePhaseStatus::Complete);
2565 }
2566
2567 #[test]
2568 fn similar_code_extraction_completion_counts_limits_and_read_failures_honestly() {
2569 let mut skips = BTreeMap::from([(SimilarCodeSkipReason::BelowMinimumLines, 2)]);
2570 assert!(extraction_is_complete(&skips, 0));
2571 assert!(!extraction_is_complete(&skips, 1));
2572
2573 skips.insert(SimilarCodeSkipReason::InputLimit, 3);
2574 assert!(!extraction_is_complete(&skips, 0));
2575 assert_eq!(remaining_extraction_inputs(7, 3), 4);
2576 }
2577
2578 #[test]
2579 fn similar_code_exhausted_extraction_budget_uses_the_specific_skip_reason() {
2580 assert_eq!(
2581 exhausted_extraction_limit(0, 1),
2582 Some(SimilarCodeSkipReason::InputLimit)
2583 );
2584 assert_eq!(
2585 exhausted_extraction_limit(1, 0),
2586 Some(SimilarCodeSkipReason::SourceBytesLimit)
2587 );
2588 assert_eq!(exhausted_extraction_limit(1, 1), None);
2589 }
2590
2591 #[test]
2592 fn similar_code_scope_requires_one_endpoint_to_match_every_active_filter() {
2593 let root = tempfile::tempdir().unwrap();
2594 let resolved = resolve_programmatic_analysis_context(&crate::AnalysisOptions {
2595 root: Some(root.path().to_path_buf()),
2596 ..crate::AnalysisOptions::default()
2597 })
2598 .unwrap();
2599 let options = SimilarCodeOptions {
2600 files: vec![PathBuf::from("src/file-scoped.ts")],
2601 ..SimilarCodeOptions::default()
2602 };
2603 let changed = FxHashSet::from_iter([PathBuf::from("src/changed.ts")]);
2604
2605 assert!(similar_code_scope_active(
2606 &options,
2607 &resolved,
2608 Some(&changed),
2609 None,
2610 ));
2611 assert!(!similar_code_path_in_scope(
2612 "src/file-scoped.ts",
2613 &options,
2614 &resolved,
2615 Some(&changed),
2616 None,
2617 ));
2618 assert!(!similar_code_path_in_scope(
2619 "src/changed.ts",
2620 &options,
2621 &resolved,
2622 Some(&changed),
2623 None,
2624 ));
2625
2626 let changed = FxHashSet::from_iter([PathBuf::from("src/file-scoped.ts")]);
2627 assert!(similar_code_path_in_scope(
2628 "src/file-scoped.ts",
2629 &options,
2630 &resolved,
2631 Some(&changed),
2632 None,
2633 ));
2634 }
2635
2636 #[test]
2637 fn similar_code_module_references_are_sorted_deduplicated_and_bounded() {
2638 let paths = vec![
2639 PathBuf::from("src/z.ts"),
2640 PathBuf::from("src/a.ts"),
2641 PathBuf::from("src/a.ts"),
2642 ];
2643
2644 let (references, truncated) = bounded_module_references(&paths, 1);
2645
2646 assert!(truncated);
2647 assert_eq!(references.len(), 1);
2648 assert_eq!(references[0].path, "src/a.ts");
2649 assert_eq!(references[0].name, MODULE_REFERENCE_NAME);
2650 assert_eq!(references[0].line, 1);
2651 }
2652
2653 #[test]
2654 fn similar_code_related_tests_are_transitive_path_filtered_and_bounded() {
2655 let paths = vec![
2656 "src/helper.ts".to_owned(),
2657 "tests/z.spec.ts".to_owned(),
2658 "src/a.test.ts".to_owned(),
2659 "src/a.test.ts".to_owned(),
2660 ];
2661
2662 let (tests, truncated) = bounded_related_tests(Path::new("/project"), &paths, 1);
2663
2664 assert!(truncated);
2665 assert_eq!(tests, vec!["src/a.test.ts"]);
2666 }
2667
2668 #[test]
2669 fn similar_code_related_tests_leave_out_test_support_files() {
2670 let paths = vec![
2671 "src/__mocks__/api.ts".to_owned(),
2672 "src/__fixtures__/user.ts".to_owned(),
2673 "src/__snapshots__/api.ts.snap".to_owned(),
2674 "src/user.fixture.ts".to_owned(),
2675 "src/api.test.ts".to_owned(),
2676 ];
2677
2678 let (tests, truncated) = bounded_related_tests(Path::new("/project"), &paths, 10);
2679
2680 assert!(!truncated);
2681 assert_eq!(tests, vec!["src/api.test.ts"]);
2682 }
2683
2684 #[test]
2685 fn similar_code_module_relationship_uses_direct_edges_then_shared_importers() {
2686 let left_location = location("src/left.ts", 1, 3);
2687 let right_location = location("src/right.ts", 1, 3);
2688 let left = file_trace(&["src/right.ts"], &["src/shared.ts"]);
2689 let right = file_trace(&[], &["src/shared.ts"]);
2690
2691 assert_eq!(
2692 module_relationship(&left, &right, &left_location, &right_location),
2693 "left-directly-imports-right"
2694 );
2695
2696 let left = file_trace(&[], &["src/shared.ts"]);
2697 assert_eq!(
2698 module_relationship(&left, &right, &left_location, &right_location),
2699 "shared-direct-importer"
2700 );
2701 }
2702
2703 #[test]
2704 fn similar_code_primary_owner_uses_the_codeowners_winning_rule() {
2705 let codeowners = CodeOwners::parse("/src/* @team/base\n/src/special.ts @team/special")
2706 .expect("CODEOWNERS parses");
2707
2708 assert_eq!(
2709 primary_owner(&codeowners, "src/special.ts"),
2710 vec!["@team/special"]
2711 );
2712 assert!(primary_owner(&codeowners, "test/a.ts").is_empty());
2713 }
2714
2715 #[test]
2716 fn similar_code_churn_lookup_normalizes_path_separators_and_defaults_to_zero() {
2717 let mut files = FxHashMap::default();
2718 files.insert(
2719 PathBuf::from("C:/repo/src/a.ts"),
2720 fallow_engine::churn::FileChurn {
2721 path: PathBuf::from("C:/repo/src/a.ts"),
2722 commits: 7,
2723 weighted_commits: 0.0,
2724 lines_added: 0,
2725 lines_deleted: 0,
2726 trend: fallow_engine::churn::ChurnTrend::Stable,
2727 authors: FxHashMap::default(),
2728 },
2729 );
2730 let churn = fallow_engine::churn::ChurnResult {
2731 files,
2732 shallow_clone: false,
2733 author_pool: Vec::new(),
2734 clock: fallow_engine::clock::AnalysisClock::pinned(1_788_782_400),
2735 git_log_bytes: 0,
2736 commits: Vec::new(),
2737 };
2738
2739 assert_eq!(churn_commits_for(&churn, Path::new(r"C:\repo\src\a.ts")), 7);
2740 assert_eq!(churn_commits_for(&churn, Path::new("src/missing.ts")), 0);
2741 }
2742
2743 #[test]
2744 fn similar_code_clone_coverage_counts_unique_exact_lines_only() {
2745 let root = Path::new("/repo");
2746 let exact = fallow_engine::duplicates::CloneGroup {
2747 instances: vec![
2748 clone_instance("/repo/src/a.ts", 2, 4),
2749 clone_instance("/repo/src/a.ts", 4, 6),
2750 ],
2751 token_count: 10,
2752 line_count: 3,
2753 similarity: None,
2754 };
2755 let near = fallow_engine::duplicates::CloneGroup {
2756 instances: vec![clone_instance("/repo/src/a.ts", 7, 9)],
2757 token_count: 10,
2758 line_count: 3,
2759 similarity: Some(0.9),
2760 };
2761 let report = fallow_engine::duplicates::DuplicationReport {
2762 clone_groups: vec![exact, near],
2763 ..fallow_engine::duplicates::DuplicationReport::default()
2764 };
2765
2766 assert_eq!(
2767 deterministic_clone_coverage(&report, root, &location("src/a.ts", 1, 10)),
2768 0.5
2769 );
2770 }
2771
2772 #[test]
2773 fn similar_code_project_artifacts_enrich_both_sides_deterministically() {
2774 let project = tempfile::tempdir().expect("temporary project");
2775 let root = project.path();
2776 std::fs::create_dir_all(root.join("src")).expect("source directory");
2777 std::fs::create_dir_all(root.join("tests")).expect("test directory");
2778 std::fs::write(
2779 root.join("package.json"),
2780 r#"{"name":"inspect-enrichment","main":"src/index.ts"}"#,
2781 )
2782 .expect("package manifest");
2783 let repeated = "export function candidate(values: number[]) {\n const positive = values.filter((value) => value > 0);\n const doubled = positive.map((value) => value * 2);\n const total = doubled.reduce((sum, value) => sum + value, 0);\n return { total, count: doubled.length };\n}\n";
2784 std::fs::write(root.join("src/left.ts"), repeated).expect("left source");
2785 std::fs::write(root.join("src/right.ts"), repeated).expect("right source");
2786 std::fs::write(
2787 root.join("src/index.ts"),
2788 "import { candidate as left } from './left';\nimport { candidate as right } from './right';\nconsole.log(left([1]), right([2]));\n",
2789 )
2790 .expect("entry source");
2791 std::fs::write(
2792 root.join("tests/left.test.ts"),
2793 "import { candidate } from '../src/left';\ntest('candidate', () => expect(candidate([1])).toBeTruthy());\n",
2794 )
2795 .expect("test source");
2796
2797 let session = AnalysisSession::load_with_config(root, None, |config| {
2798 config.duplicates.min_tokens = 5;
2799 config.duplicates.min_lines = 2;
2800 })
2801 .expect("analysis session");
2802 let left_location = location("src/left.ts", 1, 6);
2803 let right_location = location("src/right.ts", 1, 6);
2804 let mut left = side_evidence();
2805 let mut right = side_evidence();
2806 let mut result = InspectEnrichment {
2807 availability: unavailable_inspect_enrichment(),
2808 graph_relationship: None,
2809 diagnostics: Vec::new(),
2810 };
2811
2812 enrich_graph_and_clones(
2813 &session,
2814 &left_location,
2815 &right_location,
2816 &mut left,
2817 &mut right,
2818 &mut result,
2819 );
2820
2821 assert_eq!(
2822 result.graph_relationship.as_deref(),
2823 Some("shared-direct-importer")
2824 );
2825 assert_eq!(
2826 result.availability.entry_point_reachability,
2827 SimilarCodeEnrichmentState::Available
2828 );
2829 assert_eq!(left.entry_point_reachable, Some(true));
2830 assert!(
2831 left.callers
2832 .iter()
2833 .any(|reference| reference.path == "src/index.ts")
2834 );
2835 assert_eq!(left.tests, vec!["tests/left.test.ts"]);
2836 assert!(
2837 left.deterministic_clone_coverage
2838 .is_some_and(|value| value > 0.0)
2839 );
2840 assert!(
2841 right
2842 .deterministic_clone_coverage
2843 .is_some_and(|value| value > 0.0)
2844 );
2845 }
2846
2847 fn clone_instance(
2848 file: &str,
2849 start_line: usize,
2850 end_line: usize,
2851 ) -> fallow_engine::duplicates::CloneInstance {
2852 fallow_engine::duplicates::CloneInstance {
2853 is_symlink: false,
2854 file: PathBuf::from(file),
2855 start_line,
2856 end_line,
2857 start_col: 0,
2858 end_col: 0,
2859 fragment: String::new(),
2860 }
2861 }
2862
2863 fn shared_review_key_candidates() -> (Vec<u8>, Vec<SimilarCodeCandidate>) {
2864 let (_temp, project, status) = similar_code_fixture();
2865 std::fs::copy(project.join("src/a.ts"), project.join("src/d.ts")).unwrap();
2866 let mut embedder = FixtureEmbedder {
2867 provider_cache_dir: PathBuf::from(&status.cache_dir),
2868 run_timeout: Duration::from_secs(5),
2869 factory: FakeEmbeddingFactory {
2870 state: Arc::new(Mutex::new(FakeProviderState::default())),
2871 dimensions: status.dimensions,
2872 },
2873 };
2874 let output = run_with_fixture(&fixture_options(&project), &status, &mut embedder).unwrap();
2875 let mut document = serde_json::to_value(&output).unwrap();
2876 document["kind"] = Value::from("similar-code");
2877 (serde_json::to_vec(&document).unwrap(), output.candidates)
2878 }
2879
2880 fn abstaining_verdict(candidate_id: &str, review_key: &str) -> Value {
2881 serde_json::json!({
2882 "candidate_id": candidate_id,
2883 "review_key": review_key,
2884 "candidate_worthy": null,
2885 "behaviorally_equivalent": null,
2886 "refactor_safe": null,
2887 "outcome": "needs-human-review",
2888 "rationale": "Evidence is incomplete."
2889 })
2890 }
2891
2892 fn verdict_document(verdicts: &[Value]) -> Vec<u8> {
2893 serde_json::to_vec(&serde_json::json!({
2894 "schema_version": "1",
2895 "verdicts": verdicts,
2896 }))
2897 .unwrap()
2898 }
2899
2900 fn shared_key_group(
2902 candidates: &[SimilarCodeCandidate],
2903 ) -> (String, Vec<&SimilarCodeCandidate>) {
2904 let mut groups: BTreeMap<&str, Vec<&SimilarCodeCandidate>> = BTreeMap::new();
2905 for candidate in candidates {
2906 groups
2907 .entry(candidate.review_key.as_str())
2908 .or_default()
2909 .push(candidate);
2910 }
2911 let (key, group) = groups
2912 .into_iter()
2913 .find(|(_, group)| group.len() > 1)
2914 .expect("a copied function gives two candidates with one review_key");
2915 (key.to_owned(), group)
2916 }
2917
2918 #[test]
2919 fn review_accepts_candidate_id_verdicts_that_share_a_review_key() {
2920 let (candidate_json, candidates) = shared_review_key_candidates();
2921 shared_key_group(&candidates);
2922 let verdicts = verdict_document(
2923 &candidates
2924 .iter()
2925 .map(|candidate| abstaining_verdict(&candidate.candidate_id, &candidate.review_key))
2926 .collect::<Vec<_>>(),
2927 );
2928
2929 let reviewed = review_similar_code(&candidate_json, &verdicts, true).unwrap();
2930
2931 assert_eq!(reviewed.candidates.len(), candidates.len());
2932 assert!(reviewed.candidates.iter().all(|reviewed| {
2933 reviewed.verdict.is_some()
2934 && reviewed.verdict_match == SimilarCodeVerdictMatch::CandidateId
2935 }));
2936 }
2937
2938 #[test]
2939 fn review_does_not_apply_a_review_key_verdict_that_matches_several_candidates() {
2940 let (candidate_json, candidates) = shared_review_key_candidates();
2941 let (key, group) = shared_key_group(&candidates);
2942 let verdicts = verdict_document(&[abstaining_verdict("sc_stale", &key)]);
2943
2944 let reviewed = review_similar_code(&candidate_json, &verdicts, false).unwrap();
2945 assert!(
2946 reviewed.diagnostics.iter().any(|diagnostic| {
2947 diagnostic.code == "FALLOW_SIMILAR_CODE_REVIEW_KEY_AMBIGUOUS"
2948 })
2949 );
2950 for candidate in &group {
2951 let joined = reviewed
2952 .candidates
2953 .iter()
2954 .find(|reviewed| reviewed.candidate.candidate_id == candidate.candidate_id)
2955 .unwrap();
2956 assert!(joined.verdict.is_none());
2957 assert_eq!(
2958 joined.verdict_match,
2959 SimilarCodeVerdictMatch::AmbiguousReviewKey
2960 );
2961 }
2962
2963 let error = review_similar_code(&candidate_json, &verdicts, true).unwrap_err();
2964 assert_eq!(error.message, "a verdict is required for every candidate");
2965 }
2966
2967 #[test]
2968 fn review_keeps_candidate_id_matches_next_to_an_ambiguous_review_key_verdict() {
2969 let (candidate_json, candidates) = shared_review_key_candidates();
2970 let (key, group) = shared_key_group(&candidates);
2971 let first = group[0];
2972 for verdicts in [
2973 vec![
2974 abstaining_verdict(&first.candidate_id, &key),
2975 abstaining_verdict("sc_stale", &key),
2976 ],
2977 vec![
2978 abstaining_verdict("sc_stale", &key),
2979 abstaining_verdict(&first.candidate_id, &key),
2980 ],
2981 ] {
2982 let reviewed =
2983 review_similar_code(&candidate_json, &verdict_document(&verdicts), false).unwrap();
2984 let joined = reviewed
2985 .candidates
2986 .iter()
2987 .find(|reviewed| reviewed.candidate.candidate_id == first.candidate_id)
2988 .unwrap();
2989 assert!(joined.verdict.is_some());
2990 assert_eq!(joined.verdict_match, SimilarCodeVerdictMatch::CandidateId);
2991 }
2992 }
2993
2994 #[test]
2995 fn review_rejects_repeated_identities_that_do_not_identify_one_candidate() {
2996 let (candidate_json, candidates) = shared_review_key_candidates();
2997 let (key, _) = shared_key_group(&candidates);
2998 let unique = candidates
2999 .iter()
3000 .find(|candidate| {
3001 candidates
3002 .iter()
3003 .filter(|other| other.review_key == candidate.review_key)
3004 .count()
3005 == 1
3006 })
3007 .expect("the fixture has a candidate with a unique review_key");
3008 let cases = [
3009 (
3010 vec![
3011 abstaining_verdict(&unique.candidate_id, &unique.review_key),
3012 abstaining_verdict(&unique.candidate_id, &unique.review_key),
3013 ],
3014 "verdict document contains duplicate candidate or review identities",
3015 ),
3016 (
3017 vec![
3018 abstaining_verdict("sc_stale_one", &key),
3019 abstaining_verdict("sc_stale_two", &key),
3020 ],
3021 "verdict document contains duplicate candidate or review identities",
3022 ),
3023 (
3024 vec![
3025 abstaining_verdict(&unique.candidate_id, &unique.review_key),
3026 abstaining_verdict("sc_stale", &unique.review_key),
3027 ],
3028 "multiple verdicts resolve to the same candidate",
3029 ),
3030 (
3031 vec![
3032 abstaining_verdict("sc_stale", &unique.review_key),
3033 abstaining_verdict(&unique.candidate_id, &unique.review_key),
3034 ],
3035 "multiple verdicts resolve to the same candidate",
3036 ),
3037 (
3038 vec![abstaining_verdict(&unique.candidate_id, &key)],
3039 "verdict review_key does not match its candidate_id",
3040 ),
3041 ];
3042 for (verdicts, message) in cases {
3043 let error = review_similar_code(&candidate_json, &verdict_document(&verdicts), false)
3044 .unwrap_err();
3045 assert_eq!(error.message, message);
3046 }
3047 }
3048}