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