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