Skip to main content

fallow_api/runtime/
similar_code.rs

1//! Programmatic orchestration for local advisory similar-code discovery.
2
3use 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,
48    resolve_programmatic_analysis_context_deferred_workspace, 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
108/// Run opt-in semantic similar-code discovery through the verified local provider.
109///
110/// Candidate presence never changes success status. Missing local setup uses
111/// exit code 3, while invalid inputs and provider failures use exit code 2.
112///
113/// # Errors
114///
115/// Returns a structured error for invalid analysis options, unavailable local
116/// setup, source discovery failures, or unusable provider output.
117pub fn run_similar_code(options: &SimilarCodeOptions) -> ProgrammaticResult<SimilarCodeOutput> {
118    validate_options(options)?;
119    let resolved = resolve_programmatic_analysis_context_deferred_workspace(&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
131/// Select one bounded candidate snapshot from raw discovery JSON.
132///
133/// # Errors
134///
135/// Returns an error for oversized or malformed discovery JSON, duplicate
136/// candidate identities, or an unknown requested candidate.
137pub 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
176/// Validate one inline bounded candidate snapshot.
177///
178/// # Errors
179///
180/// Returns an error for oversized, malformed, or identity-mismatched input.
181pub 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
200/// Validate and inspect one exact candidate snapshot without rerunning global
201/// provider retrieval or ranking.
202///
203/// # Errors
204///
205/// Returns an error when the candidate is stale or source cannot be reproduced.
206pub 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_deferred_workspace(&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/// Join immutable candidate JSON with a separately authored verdict document.
247///
248/// # Errors
249///
250/// Returns a fail-closed error for malformed envelopes, invalid judgment
251/// implications, duplicate or stale verdicts, or missing required verdicts.
252#[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        // Two candidates can share a review_key when one function is copied
314        // verbatim, so the key is unique only among verdicts that match by it.
315        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
1457fn finite_f64_to_u64(value: f64) -> u64 {
1458    if !value.is_finite() || value <= 0.0 {
1459        0
1460    } else if value >= u64::MAX as f64 {
1461        u64::MAX
1462    } else {
1463        value.round() as u64
1464    }
1465}
1466
1467fn load_session(resolved: &ProgrammaticAnalysisContext) -> ProgrammaticResult<AnalysisSession> {
1468    let mut project = fallow_engine::project_config::config_for_project_with_load_options(
1469        resolved.root(),
1470        resolved.config_path().as_deref(),
1471        fallow_config::ConfigLoadOptions {
1472            allow_remote_extends: resolved.allow_remote_extends(),
1473        },
1474    )
1475    .map_err(|error| {
1476        ProgrammaticError::new(format!("failed to load config: {error}"), 2)
1477            .with_code("FALLOW_CONFIG_LOAD_FAILED")
1478            .with_context("analysis.configPath")
1479    })?;
1480    project.config.no_cache = resolved.no_cache();
1481    project.config.threads = resolved.threads();
1482    Ok(AnalysisSession::from_config(project))
1483}
1484
1485fn build_ignore_set(patterns: &[String]) -> ProgrammaticResult<GlobSet> {
1486    let mut builder = GlobSetBuilder::new();
1487    for pattern in patterns {
1488        builder.add(Glob::new(pattern).map_err(|error| {
1489            ProgrammaticError::new(
1490                format!("invalid `similarCode.ignore` pattern `{pattern}`: {error}"),
1491                2,
1492            )
1493            .with_code("FALLOW_INVALID_SIMILAR_CODE_IGNORE")
1494            .with_context("similarCode.ignore")
1495        })?);
1496    }
1497    builder.build().map_err(|error| {
1498        ProgrammaticError::new(
1499            format!("failed to compile `similarCode.ignore`: {error}"),
1500            2,
1501        )
1502        .with_code("FALLOW_INVALID_SIMILAR_CODE_IGNORE")
1503        .with_context("similarCode.ignore")
1504    })
1505}
1506
1507fn map_candidate(
1508    candidate: fallow_engine::similar_code::SimilarCodeCandidate,
1509    metadata: &FxHashMap<(String, u32, u32), &ExtractedSimilarCodeFunction>,
1510) -> Option<SimilarCodeCandidate> {
1511    let left = metadata.get(&location_key(&candidate.left))?;
1512    let right = metadata.get(&location_key(&candidate.right))?;
1513    Some(SimilarCodeCandidate {
1514        candidate_id: candidate.candidate_id,
1515        review_key: candidate.review_key,
1516        left: output_location(left),
1517        right: output_location(right),
1518        similarity: candidate.similarity,
1519        similarity_band: similarity_band(candidate.similarity),
1520        verification_status: SimilarCodeVerificationStatus::Unverified,
1521        enrichment: raw_enrichment_availability(),
1522        actions: vec![
1523            SimilarCodeAction {
1524                action: SimilarCodeActionType::Inspect,
1525                description: "Inspect bounded source, graph, ownership, churn, test, and deterministic clone evidence".to_owned(),
1526                read_only: true,
1527            },
1528            SimilarCodeAction {
1529                action: SimilarCodeActionType::Review,
1530                description: "Join this immutable candidate with a separate evidence-grounded verdict".to_owned(),
1531                read_only: true,
1532            },
1533        ],
1534    })
1535}
1536
1537fn output_location(function: &ExtractedSimilarCodeFunction) -> SimilarCodeLocation {
1538    SimilarCodeLocation {
1539        path: function.location.file.clone(),
1540        name: function.name.clone(),
1541        start_line: function.location.start_line,
1542        start_column: function.location.start_column_utf8.saturating_add(1),
1543        end_line: function.location.end_line,
1544        end_column: function.location.end_column_utf8.saturating_add(1),
1545        source_sha256: hex(function.source_sha256.as_bytes()),
1546    }
1547}
1548
1549fn similar_code_scope_active(
1550    options: &SimilarCodeOptions,
1551    resolved: &ProgrammaticAnalysisContext,
1552    changed_files: Option<&FxHashSet<PathBuf>>,
1553    workspace_roots: Option<&[PathBuf]>,
1554) -> bool {
1555    !options.files.is_empty()
1556        || changed_files.is_some()
1557        || resolved.diff_index().is_some()
1558        || workspace_roots.is_some()
1559}
1560
1561fn similar_code_path_in_scope(
1562    path: &str,
1563    options: &SimilarCodeOptions,
1564    resolved: &ProgrammaticAnalysisContext,
1565    changed_files: Option<&FxHashSet<PathBuf>>,
1566    workspace_roots: Option<&[PathBuf]>,
1567) -> bool {
1568    if !options.files.is_empty()
1569        && !options
1570            .files
1571            .iter()
1572            .any(|filter| normalize_path(filter) == path)
1573    {
1574        return false;
1575    }
1576    if let Some(changed_files) = changed_files
1577        && !changed_files.contains(Path::new(path))
1578        && !changed_files.contains(&resolved.root().join(path))
1579    {
1580        return false;
1581    }
1582    if let Some(diff) = resolved.diff_index()
1583        && !diff.touches_file(&diff.key_for_root_relative(path))
1584    {
1585        return false;
1586    }
1587    if let Some(workspace_roots) = workspace_roots
1588        && !workspace_roots
1589            .iter()
1590            .any(|workspace| resolved.root().join(path).starts_with(workspace))
1591    {
1592        return false;
1593    }
1594    true
1595}
1596
1597#[expect(
1598    clippy::too_many_arguments,
1599    reason = "each argument maps directly to one public completion-accounting field"
1600)]
1601fn phases(
1602    admitted_files: usize,
1603    total_files: usize,
1604    extracted_functions: usize,
1605    selected_functions: usize,
1606    embedded_functions: usize,
1607    comparisons: usize,
1608    completeness: PhaseCompleteness,
1609    missing_vectors: usize,
1610    truncated_functions: usize,
1611    source_read_failures: usize,
1612) -> Vec<SimilarCodePhaseCompletion> {
1613    vec![
1614        phase(
1615            SimilarCodePhase::Discovery,
1616            phase_status(completeness.discovery),
1617            admitted_files,
1618            Some(total_files),
1619            (!completeness.discovery)
1620                .then(|| "the file admission limit omitted source files".to_owned()),
1621        ),
1622        phase(
1623            SimilarCodePhase::Extraction,
1624            phase_status(completeness.extraction),
1625            extracted_functions,
1626            None,
1627            (!completeness.extraction).then(|| {
1628                if source_read_failures > 0 {
1629                    "one or more admitted source files could not be read".to_owned()
1630                } else {
1631                    "one or more function forms or source fragments were outside extraction limits"
1632                        .to_owned()
1633                }
1634            }),
1635        ),
1636        phase(
1637            SimilarCodePhase::Cache,
1638            SimilarCodePhaseStatus::Complete,
1639            selected_functions,
1640            Some(selected_functions),
1641            None,
1642        ),
1643        phase(
1644            SimilarCodePhase::Embedding,
1645            phase_status(completeness.embedding),
1646            embedded_functions,
1647            Some(selected_functions),
1648            (!completeness.embedding).then(|| {
1649                if missing_vectors > 0 {
1650                    "the provider did not return every selected vector".to_owned()
1651                } else if truncated_functions > 0 {
1652                    "the provider truncated one or more admitted functions".to_owned()
1653                } else {
1654                    "embedding did not complete its admitted scope".to_owned()
1655                }
1656            }),
1657        ),
1658        phase(
1659            SimilarCodePhase::Validation,
1660            SimilarCodePhaseStatus::Complete,
1661            embedded_functions,
1662            Some(embedded_functions),
1663            None,
1664        ),
1665        phase(
1666            SimilarCodePhase::Comparison,
1667            phase_status(completeness.comparison),
1668            comparisons,
1669            None,
1670            (!completeness.comparison)
1671                .then(|| "comparison limits omitted candidate pairs".to_owned()),
1672        ),
1673        phase(
1674            SimilarCodePhase::Enrichment,
1675            SimilarCodePhaseStatus::Skipped,
1676            0,
1677            None,
1678            Some("raw discovery defers source-grounded enrichment to inspect".to_owned()),
1679        ),
1680    ]
1681}
1682
1683const fn phase_status(complete: bool) -> SimilarCodePhaseStatus {
1684    if complete {
1685        SimilarCodePhaseStatus::Complete
1686    } else {
1687        SimilarCodePhaseStatus::Partial
1688    }
1689}
1690
1691fn phase(
1692    phase: SimilarCodePhase,
1693    status: SimilarCodePhaseStatus,
1694    processed: usize,
1695    total: Option<usize>,
1696    reason: Option<String>,
1697) -> SimilarCodePhaseCompletion {
1698    SimilarCodePhaseCompletion {
1699        phase,
1700        status,
1701        processed: usize_to_u64(processed),
1702        total: total.map(usize_to_u64),
1703        reason,
1704    }
1705}
1706
1707fn output_limits(
1708    engine: EngineLimits,
1709    extraction: SimilarCodeExtractionLimits,
1710) -> SimilarCodeLimits {
1711    SimilarCodeLimits {
1712        max_files: usize_to_u64(MAX_FILES),
1713        max_functions: usize_to_u64(engine.max_functions),
1714        max_source_bytes: usize_to_u64(extraction.max_total_source_bytes),
1715        max_function_bytes: usize_to_u64(extraction.max_source_bytes_per_function),
1716        max_batch_size: usize_to_u64(similar_code::embedding_batch_size()),
1717        max_vector_bytes: usize_to_u64(engine.max_vector_bytes),
1718        max_comparisons: usize_to_u64(engine.max_comparisons),
1719        max_candidates: usize_to_u64(engine.max_candidates),
1720        max_neighbors_per_function: usize_to_u64(engine.max_neighbors_per_function),
1721        timeout_ms: MAX_RUN_TIMEOUT_MS,
1722    }
1723}
1724
1725fn generation(
1726    provider: &SimilarCodeProviderStatus,
1727    threshold: f64,
1728    min_lines: usize,
1729    scope: SimilarCodeScopeProvenance,
1730) -> SimilarCodeGeneration {
1731    SimilarCodeGeneration {
1732        extraction_semantics_version: SIMILAR_CODE_EXTRACTION_SEMANTICS_VERSION,
1733        embedding_semantics_version: similar_code::embedding_semantics_version(),
1734        provider: SimilarCodeProviderProvenance {
1735            provider: SimilarCodeProvider::OfficialLocalCompanion,
1736            companion_version: provider.sidecar_version.clone(),
1737            protocol_version: provider.protocol_version,
1738            source_left_machine: false,
1739        },
1740        model: SimilarCodeModelProvenance {
1741            model_id: provider.model_id.clone(),
1742            revision: provider.model_revision.clone(),
1743            artifact_sha256: similar_code::model_artifact_sha256().to_owned(),
1744            license: provider.license.clone(),
1745            dimensions: u32::try_from(provider.dimensions).unwrap_or(u32::MAX),
1746        },
1747        parameters: SimilarCodeGenerationParameters {
1748            dtype: "f32".to_owned(),
1749            pooling: "mean".to_owned(),
1750            normalized: true,
1751            batch_size: u32::try_from(similar_code::embedding_batch_size()).unwrap_or(u32::MAX),
1752            max_tokens: u32::try_from(provider.max_tokens).unwrap_or(u32::MAX),
1753            parameter_sha256: similar_code::parameter_sha256(),
1754        },
1755        scope,
1756        threshold,
1757        min_lines: usize_to_u64(min_lines),
1758    }
1759}
1760
1761fn raw_enrichment_availability() -> SimilarCodeEnrichmentAvailability {
1762    SimilarCodeEnrichmentAvailability {
1763        graph_relationship: SimilarCodeEnrichmentState::NotRequested,
1764        entry_point_reachability: SimilarCodeEnrichmentState::NotRequested,
1765        callers: SimilarCodeEnrichmentState::NotRequested,
1766        callees: SimilarCodeEnrichmentState::NotRequested,
1767        ownership: SimilarCodeEnrichmentState::NotRequested,
1768        churn: SimilarCodeEnrichmentState::NotRequested,
1769        tests: SimilarCodeEnrichmentState::NotRequested,
1770        deterministic_clone_coverage: SimilarCodeEnrichmentState::NotRequested,
1771        runtime: SimilarCodeEnrichmentState::NotRequested,
1772    }
1773}
1774
1775fn map_extraction_skip(reason: SimilarCodeExtractionSkipReason) -> SimilarCodeSkipReason {
1776    match reason {
1777        SimilarCodeExtractionSkipReason::GeneratedSource => SimilarCodeSkipReason::GeneratedSource,
1778        SimilarCodeExtractionSkipReason::SourceBytesPerFunctionLimit => {
1779            SimilarCodeSkipReason::FunctionTooLarge
1780        }
1781        SimilarCodeExtractionSkipReason::FunctionLimit => SimilarCodeSkipReason::InputLimit,
1782        SimilarCodeExtractionSkipReason::TotalSourceBytesLimit => {
1783            SimilarCodeSkipReason::SourceBytesLimit
1784        }
1785        _ => SimilarCodeSkipReason::UnsupportedFunction,
1786    }
1787}
1788
1789fn map_engine_skip(reason: EngineSkipReason) -> SimilarCodeSkipReason {
1790    match reason {
1791        EngineSkipReason::VectorMemoryLimit => SimilarCodeSkipReason::VectorMemoryLimit,
1792        EngineSkipReason::ComparisonLimit => SimilarCodeSkipReason::ComparisonLimit,
1793        EngineSkipReason::CandidateLimit => SimilarCodeSkipReason::CandidateLimit,
1794        EngineSkipReason::NeighborLimit => SimilarCodeSkipReason::NeighborLimit,
1795        _ => SimilarCodeSkipReason::InputLimit,
1796    }
1797}
1798
1799fn cache_status(disabled: bool, hits: usize, misses: usize) -> SimilarCodeCacheStatus {
1800    if disabled {
1801        SimilarCodeCacheStatus::Disabled
1802    } else if misses == 0 {
1803        SimilarCodeCacheStatus::Hit
1804    } else if hits == 0 {
1805        SimilarCodeCacheStatus::Miss
1806    } else {
1807        SimilarCodeCacheStatus::Mixed
1808    }
1809}
1810
1811fn similarity_band(similarity: f64) -> SimilarCodeSimilarityBand {
1812    if similarity >= VERY_HIGH_SIMILARITY {
1813        SimilarCodeSimilarityBand::VeryHigh
1814    } else if similarity >= HIGH_SIMILARITY {
1815        SimilarCodeSimilarityBand::High
1816    } else {
1817        SimilarCodeSimilarityBand::Moderate
1818    }
1819}
1820
1821fn validate_options(options: &SimilarCodeOptions) -> ProgrammaticResult<()> {
1822    if let Some(threshold) = options.threshold {
1823        validate_threshold(threshold)?;
1824    }
1825    if options.min_lines == Some(0) {
1826        return Err(ProgrammaticError::new("`min_lines` must be at least 1", 2)
1827            .with_code("FALLOW_INVALID_SIMILAR_CODE_MIN_LINES")
1828            .with_context("similarCode.minLines"));
1829    }
1830    if options.top == Some(0) {
1831        return Err(ProgrammaticError::new("`top` must be at least 1", 2)
1832            .with_code("FALLOW_INVALID_SIMILAR_CODE_TOP")
1833            .with_context("similarCode.top"));
1834    }
1835    for path in &options.files {
1836        if path.is_absolute()
1837            || path
1838                .components()
1839                .any(|part| matches!(part, std::path::Component::ParentDir))
1840        {
1841            return Err(ProgrammaticError::new(
1842                "`file` paths must be project-root-relative and must not contain `..`",
1843                2,
1844            )
1845            .with_code("FALLOW_INVALID_SIMILAR_CODE_FILE")
1846            .with_context("similarCode.files"));
1847        }
1848    }
1849    Ok(())
1850}
1851
1852fn validate_threshold(threshold: f64) -> ProgrammaticResult<()> {
1853    if !threshold.is_finite() || !(0.0..=1.0).contains(&threshold) {
1854        return Err(
1855            ProgrammaticError::new("`threshold` must be finite and between 0 and 1", 2)
1856                .with_code("FALLOW_INVALID_SIMILAR_CODE_THRESHOLD")
1857                .with_context("similarCode.threshold"),
1858        );
1859    }
1860    Ok(())
1861}
1862
1863#[expect(
1864    clippy::needless_pass_by_value,
1865    reason = "map_err supplies owned provider failures and the mapper consumes that boundary"
1866)]
1867fn provider_error(error: ProviderError) -> ProgrammaticError {
1868    let exit_code = if matches!(error, ProviderError::NotReady(_)) {
1869        3
1870    } else {
1871        2
1872    };
1873    let code = if exit_code == 3 {
1874        "FALLOW_SIMILAR_CODE_NOT_READY"
1875    } else {
1876        "FALLOW_SIMILAR_CODE_PROVIDER_FAILED"
1877    };
1878    ProgrammaticError::new(error.message(), exit_code)
1879        .with_code(code)
1880        .with_context("similarCode.provider")
1881}
1882
1883fn engine_error(error: impl std::fmt::Display) -> ProgrammaticError {
1884    ProgrammaticError::new(format!("invalid similar-code evaluation: {error}"), 2)
1885        .with_code("FALLOW_SIMILAR_CODE_EVALUATION_FAILED")
1886        .with_context("similarCode.evaluation")
1887}
1888
1889fn location_key(
1890    location: &fallow_engine::source::similar_code::SimilarCodeFunctionLocation,
1891) -> (String, u32, u32) {
1892    (
1893        location.file.clone(),
1894        location.start_byte,
1895        location.end_byte,
1896    )
1897}
1898
1899fn root_relative(root: &Path, path: &Path) -> String {
1900    normalize_path(path.strip_prefix(root).unwrap_or(path))
1901}
1902
1903fn normalize_path(path: &Path) -> String {
1904    path.to_string_lossy().replace('\\', "/")
1905}
1906
1907fn add_skip(
1908    skips: &mut BTreeMap<SimilarCodeSkipReason, usize>,
1909    reason: SimilarCodeSkipReason,
1910    count: usize,
1911) {
1912    *skips.entry(reason).or_default() += count;
1913}
1914
1915fn extraction_is_complete(
1916    skips: &BTreeMap<SimilarCodeSkipReason, usize>,
1917    source_read_failures: usize,
1918) -> bool {
1919    source_read_failures == 0
1920        && skips.iter().all(|(reason, count)| {
1921            *count == 0 || matches!(reason, SimilarCodeSkipReason::BelowMinimumLines)
1922        })
1923}
1924
1925const fn remaining_extraction_inputs(total: usize, current_index: usize) -> usize {
1926    total.saturating_sub(current_index)
1927}
1928
1929const fn exhausted_extraction_limit(
1930    remaining_functions: usize,
1931    remaining_source_bytes: usize,
1932) -> Option<SimilarCodeSkipReason> {
1933    if remaining_functions == 0 {
1934        Some(SimilarCodeSkipReason::InputLimit)
1935    } else if remaining_source_bytes == 0 {
1936        Some(SimilarCodeSkipReason::SourceBytesLimit)
1937    } else {
1938        None
1939    }
1940}
1941
1942fn hex(bytes: &[u8]) -> String {
1943    bytes.iter().fold(
1944        String::with_capacity(bytes.len().saturating_mul(2)),
1945        |mut output, byte| {
1946            let _ = write!(output, "{byte:02x}");
1947            output
1948        },
1949    )
1950}
1951
1952fn duration_ms(started: Instant) -> u64 {
1953    u64::try_from(started.elapsed().as_millis()).unwrap_or(u64::MAX)
1954}
1955
1956fn usize_to_u64(value: usize) -> u64 {
1957    u64::try_from(value).unwrap_or(u64::MAX)
1958}
1959
1960#[cfg(test)]
1961#[expect(
1962    clippy::float_cmp,
1963    clippy::unwrap_used,
1964    reason = "deterministic fixtures fail immediately and ratios have exact binary representations"
1965)]
1966mod tests {
1967    use super::*;
1968    use std::sync::{Arc, Mutex};
1969    use std::time::Duration;
1970
1971    use crate::similar_code::{
1972        EmbeddingBatch, EmbeddingBatchVector, EmbeddingSession, EmbeddingSessionFactory,
1973    };
1974
1975    #[derive(Default)]
1976    struct FakeProviderState {
1977        spawns: usize,
1978        batches: usize,
1979        complete_batches: Option<usize>,
1980    }
1981
1982    struct FakeEmbeddingSession {
1983        state: Arc<Mutex<FakeProviderState>>,
1984        dimensions: usize,
1985    }
1986
1987    impl EmbeddingSession for FakeEmbeddingSession {
1988        fn embed(&mut self, functions: &[(u32, &str)]) -> Result<EmbeddingBatch, String> {
1989            let should_return_partial = {
1990                let mut state = self.state.lock().unwrap();
1991                state.batches += 1;
1992                state
1993                    .complete_batches
1994                    .is_some_and(|limit| state.batches > limit)
1995            };
1996            if should_return_partial {
1997                return Ok(EmbeddingBatch {
1998                    vectors: Vec::new(),
1999                    inference_ms: 0.0,
2000                    problem: Some("fixture provider returned a bounded partial batch".to_owned()),
2001                });
2002            }
2003            let vectors = functions
2004                .iter()
2005                .map(|(key, _)| {
2006                    let mut values = vec![0.0; self.dimensions];
2007                    values[0] = 1.0;
2008                    EmbeddingBatchVector {
2009                        key: *key,
2010                        values,
2011                        truncated: false,
2012                    }
2013                })
2014                .collect();
2015            Ok(EmbeddingBatch {
2016                vectors,
2017                inference_ms: 0.25,
2018                problem: None,
2019            })
2020        }
2021    }
2022
2023    struct FakeEmbeddingFactory {
2024        state: Arc<Mutex<FakeProviderState>>,
2025        dimensions: usize,
2026    }
2027
2028    impl EmbeddingSessionFactory for FakeEmbeddingFactory {
2029        fn spawn(&mut self) -> Result<Box<dyn EmbeddingSession>, String> {
2030            self.state.lock().unwrap().spawns += 1;
2031            Ok(Box::new(FakeEmbeddingSession {
2032                state: Arc::clone(&self.state),
2033                dimensions: self.dimensions,
2034            }))
2035        }
2036    }
2037
2038    struct FixtureEmbedder {
2039        provider_cache_dir: PathBuf,
2040        run_timeout: Duration,
2041        factory: FakeEmbeddingFactory,
2042    }
2043
2044    impl RuntimeEmbedder for FixtureEmbedder {
2045        fn embed(
2046            &mut self,
2047            project_root: &Path,
2048            no_cache: bool,
2049            inputs: &[EmbeddingInput<'_>],
2050        ) -> Result<EmbeddingResult, ProviderError> {
2051            similar_code::embed_selected_with_factory(
2052                &self.provider_cache_dir,
2053                project_root,
2054                no_cache,
2055                inputs,
2056                self.run_timeout,
2057                &mut self.factory,
2058            )
2059        }
2060    }
2061
2062    fn similar_code_fixture() -> (tempfile::TempDir, PathBuf, SimilarCodeProviderStatus) {
2063        let temp = tempfile::tempdir().unwrap();
2064        let project = temp.path().join("project");
2065        let cache_root = temp.path().join("user-cache");
2066        let provider_cache_dir = cache_root.join("models").join("fixture-model");
2067        std::fs::create_dir_all(project.join("src")).unwrap();
2068        std::fs::create_dir_all(&cache_root).unwrap();
2069        std::fs::write(
2070            project.join("package.json"),
2071            r#"{"name":"similar-code-runtime-fixture","private":true}"#,
2072        )
2073        .unwrap();
2074        for (name, value) in [("a", 1), ("b", 2), ("c", 3)] {
2075            std::fs::write(
2076                project.join("src").join(format!("{name}.ts")),
2077                format!(
2078                    "export function {name}(input: number) {{\n  const adjusted = input + {value};\n  return adjusted * 2;\n}}\n"
2079                ),
2080            )
2081            .unwrap();
2082        }
2083        let (model_id, model_revision, dimensions, license) = similar_code::provider_identity();
2084        let status = SimilarCodeProviderStatus {
2085            protocol_version: 2,
2086            embedding_semantics_version: similar_code::embedding_semantics_version(),
2087            sidecar_version: env!("CARGO_PKG_VERSION").to_owned(),
2088            model_ready: true,
2089            model_id: model_id.to_owned(),
2090            model_revision: model_revision.to_owned(),
2091            dimensions,
2092            max_tokens: 512,
2093            license: license.to_owned(),
2094            cache_dir: provider_cache_dir.to_string_lossy().into_owned(),
2095            download_bytes: similar_code::model_download_bytes(),
2096            analysis_offline: true,
2097            integrity_verified: true,
2098            problem: None,
2099            downloaded: None,
2100        };
2101        (temp, project, status)
2102    }
2103
2104    fn fixture_options(project: &Path) -> SimilarCodeOptions {
2105        SimilarCodeOptions {
2106            analysis: crate::AnalysisOptions {
2107                root: Some(project.to_path_buf()),
2108                ..crate::AnalysisOptions::default()
2109            },
2110            threshold: Some(0.9),
2111            min_lines: Some(2),
2112            ..SimilarCodeOptions::default()
2113        }
2114    }
2115
2116    fn run_with_fixture(
2117        options: &SimilarCodeOptions,
2118        status: &SimilarCodeProviderStatus,
2119        embedder: &mut FixtureEmbedder,
2120    ) -> ProgrammaticResult<SimilarCodeOutput> {
2121        let resolved = resolve_programmatic_analysis_context_deferred_workspace(&options.analysis)?;
2122        resolved
2123            .install(|| run_similar_code_inner_with_embedder(options, &resolved, status, embedder))
2124    }
2125
2126    fn find_cache_file(root: &Path) -> Option<PathBuf> {
2127        for entry in std::fs::read_dir(root).ok()? {
2128            let path = entry.ok()?.path();
2129            if path.file_name().is_some_and(|name| name == "vectors.bin") {
2130                return Some(path);
2131            }
2132            if path.is_dir()
2133                && let Some(found) = find_cache_file(&path)
2134            {
2135                return Some(found);
2136            }
2137        }
2138        None
2139    }
2140
2141    #[test]
2142    fn similar_code_runtime_covers_cold_warm_corrupt_cache_scope_and_output_contract() {
2143        let (_temp, project, status) = similar_code_fixture();
2144        let provider_cache_dir = PathBuf::from(&status.cache_dir);
2145        let state = Arc::new(Mutex::new(FakeProviderState::default()));
2146        let mut embedder = FixtureEmbedder {
2147            provider_cache_dir: provider_cache_dir.clone(),
2148            run_timeout: Duration::from_secs(5),
2149            factory: FakeEmbeddingFactory {
2150                state: Arc::clone(&state),
2151                dimensions: status.dimensions,
2152            },
2153        };
2154        let mut options = fixture_options(&project);
2155        options.files = vec![PathBuf::from("src/a.ts")];
2156
2157        let cold = run_with_fixture(&options, &status, &mut embedder).unwrap();
2158        assert!(!cold.candidates.is_empty());
2159        assert!(cold.candidates.iter().all(|candidate| {
2160            candidate.left.path == "src/a.ts" || candidate.right.path == "src/a.ts"
2161        }));
2162        assert_eq!(
2163            cold.completion.status,
2164            SimilarCodeCompletionStatus::Complete
2165        );
2166        assert!(cold.completion.cache.misses > 0);
2167        assert!(cold.completion.cache.writes > 0);
2168        let cold_spawns = state.lock().unwrap().spawns;
2169        let cold_ids = cold
2170            .candidates
2171            .iter()
2172            .map(|candidate| candidate.candidate_id.clone())
2173            .collect::<Vec<_>>();
2174        let json = serde_json::to_value(&cold).unwrap();
2175        assert_eq!(json["generation"]["embedding_semantics_version"], 1);
2176        assert_eq!(json["generation"]["provider"]["source_left_machine"], false);
2177        assert_eq!(json["generation"]["scope"]["active"], true);
2178        assert_eq!(
2179            json["generation"]["scope"]["paths"],
2180            serde_json::json!(["src/a.ts"])
2181        );
2182        assert!(json["completion"]["cache"].is_object());
2183
2184        let warm = run_with_fixture(&options, &status, &mut embedder).unwrap();
2185        assert_eq!(state.lock().unwrap().spawns, cold_spawns);
2186        assert!(warm.completion.cache.hits > 0);
2187        assert_eq!(warm.completion.cache.writes, 0);
2188        assert_eq!(
2189            warm.candidates
2190                .iter()
2191                .map(|candidate| candidate.candidate_id.clone())
2192                .collect::<Vec<_>>(),
2193            cold_ids
2194        );
2195
2196        let cache_root = provider_cache_dir.parent().and_then(Path::parent).unwrap();
2197        let cache_file = find_cache_file(cache_root).unwrap();
2198        std::fs::write(&cache_file, b"corrupt cache fixture").unwrap();
2199        let recovered = run_with_fixture(&options, &status, &mut embedder).unwrap();
2200        assert_eq!(recovered.completion.cache.invalid_entries, 1);
2201        assert!(recovered.completion.cache.writes > 0);
2202        assert!(state.lock().unwrap().spawns > cold_spawns);
2203    }
2204
2205    #[test]
2206    fn snapshot_inspect_survives_ranking_crowd_out_and_rejects_stale_source() {
2207        let (_temp, project, status) = similar_code_fixture();
2208        let state = Arc::new(Mutex::new(FakeProviderState::default()));
2209        let mut embedder = FixtureEmbedder {
2210            provider_cache_dir: PathBuf::from(&status.cache_dir),
2211            run_timeout: Duration::from_secs(5),
2212            factory: FakeEmbeddingFactory {
2213                state,
2214                dimensions: status.dimensions,
2215            },
2216        };
2217        let discovery =
2218            run_with_fixture(&fixture_options(&project), &status, &mut embedder).unwrap();
2219        let candidate_id = discovery.candidates.last().unwrap().candidate_id.clone();
2220        let tagged = fallow_output::serialize_similar_code_json_output(discovery).unwrap();
2221        let snapshot = select_similar_code_candidate_snapshot(
2222            &serde_json::to_vec(&tagged).unwrap(),
2223            &candidate_id,
2224        )
2225        .unwrap();
2226
2227        let mut legacy_options = fixture_options(&project);
2228        legacy_options.files = vec![
2229            PathBuf::from(&snapshot.candidate.left.path),
2230            PathBuf::from(&snapshot.candidate.right.path),
2231        ];
2232        legacy_options.top = Some(1);
2233        let endpoint_reranked = run_with_fixture(&legacy_options, &status, &mut embedder).unwrap();
2234        assert_eq!(endpoint_reranked.candidates.len(), 1);
2235        assert!(
2236            endpoint_reranked
2237                .candidates
2238                .iter()
2239                .all(|candidate| candidate.candidate_id != candidate_id),
2240            "the endpoint-only legacy rerank must reproduce the crowd-out condition"
2241        );
2242
2243        let inspect_options = SimilarCodeInspectOptions {
2244            analysis: crate::AnalysisOptions {
2245                root: Some(project.clone()),
2246                ..crate::AnalysisOptions::default()
2247            },
2248            snapshot: snapshot.clone(),
2249        };
2250        let inspected = inspect_similar_code(&inspect_options).unwrap();
2251        assert_eq!(inspected.candidate.candidate_id, candidate_id);
2252
2253        let stale_path = project.join(&snapshot.candidate.left.path);
2254        let stale_source = std::fs::read_to_string(&stale_path).unwrap();
2255        std::fs::write(
2256            &stale_path,
2257            stale_source.replace("return adjusted * 2", "return adjusted * 3"),
2258        )
2259        .unwrap();
2260        let error = inspect_similar_code(&inspect_options).unwrap_err();
2261        assert_eq!(
2262            error.code.as_deref(),
2263            Some("FALLOW_SIMILAR_CODE_CANDIDATE_STALE")
2264        );
2265    }
2266
2267    #[test]
2268    fn snapshot_inspect_rejects_oversized_endpoint_before_source_allocation() {
2269        let temp = tempfile::tempdir().unwrap();
2270        let project = dunce::canonicalize(temp.path()).unwrap();
2271        let source_path = project.join("src/endpoint.ts");
2272        std::fs::create_dir_all(source_path.parent().unwrap()).unwrap();
2273        std::fs::write(
2274            &source_path,
2275            "export function candidate() {\n  return true;\n}\n",
2276        )
2277        .unwrap();
2278        std::fs::OpenOptions::new()
2279            .write(true)
2280            .open(&source_path)
2281            .unwrap()
2282            .set_len(MAX_INSPECT_SOURCE_BYTES + 1)
2283            .unwrap();
2284
2285        let error = inspect_side(&project, &location("src/endpoint.ts", 1, 3)).unwrap_err();
2286        assert_eq!(
2287            error.code.as_deref(),
2288            Some("FALLOW_SIMILAR_CODE_CANDIDATE_STALE")
2289        );
2290        assert!(error.message.contains("5 MiB per-file limit"));
2291    }
2292
2293    #[test]
2294    fn snapshot_inspect_does_not_rebind_an_identical_same_line_function() {
2295        let temp = tempfile::tempdir().unwrap();
2296        let project = dunce::canonicalize(temp.path()).unwrap();
2297        let relative = Path::new("src/duplicates.js");
2298        let source_path = project.join(relative);
2299        std::fs::create_dir_all(source_path.parent().unwrap()).unwrap();
2300        let function = "function duplicate() { return 1; }";
2301        let original = format!("{function} {function}\n");
2302        std::fs::write(&source_path, &original).unwrap();
2303
2304        let extracted = fallow_engine::source::similar_code::extract(
2305            relative,
2306            &original,
2307            SimilarCodeExtractionLimits::default(),
2308        );
2309        assert_eq!(extracted.functions.len(), 2);
2310        let snapshot = output_location(&extracted.functions[0]);
2311        assert_eq!(
2312            snapshot.source_sha256,
2313            output_location(&extracted.functions[1]).source_sha256
2314        );
2315        assert_ne!(
2316            snapshot.start_column,
2317            output_location(&extracted.functions[1]).start_column
2318        );
2319
2320        let second_start = function.len() + 1;
2321        std::fs::write(
2322            &source_path,
2323            format!("{}{function}\n", " ".repeat(second_start)),
2324        )
2325        .unwrap();
2326
2327        let error = inspect_side(&project, &snapshot).unwrap_err();
2328        assert_eq!(
2329            error.code.as_deref(),
2330            Some("FALLOW_SIMILAR_CODE_CANDIDATE_STALE")
2331        );
2332    }
2333
2334    #[test]
2335    fn similar_code_runtime_reports_partial_provider_output_and_bounded_timeout() {
2336        let (_temp, project, status) = similar_code_fixture();
2337        let provider_cache_dir = PathBuf::from(&status.cache_dir);
2338        let partial_state = Arc::new(Mutex::new(FakeProviderState {
2339            complete_batches: Some(2),
2340            ..FakeProviderState::default()
2341        }));
2342        let mut partial_embedder = FixtureEmbedder {
2343            provider_cache_dir: provider_cache_dir.clone(),
2344            run_timeout: Duration::from_secs(5),
2345            factory: FakeEmbeddingFactory {
2346                state: partial_state,
2347                dimensions: status.dimensions,
2348            },
2349        };
2350        let mut options = fixture_options(&project);
2351        options.analysis.no_cache = true;
2352
2353        let partial = run_with_fixture(&options, &status, &mut partial_embedder).unwrap();
2354        assert_eq!(
2355            partial.completion.status,
2356            SimilarCodeCompletionStatus::Partial
2357        );
2358        assert!(partial.completion.skips.iter().any(|skip| {
2359            skip.phase == SimilarCodePhase::Embedding
2360                && skip.reason == SimilarCodeSkipReason::ProviderFailure
2361        }));
2362        assert!(
2363            partial
2364                .diagnostics
2365                .iter()
2366                .any(|diagnostic| { diagnostic.code == "FALLOW_SIMILAR_CODE_PROVIDER_PARTIAL" })
2367        );
2368
2369        let timeout_state = Arc::new(Mutex::new(FakeProviderState::default()));
2370        let mut timeout_embedder = FixtureEmbedder {
2371            provider_cache_dir,
2372            run_timeout: Duration::ZERO,
2373            factory: FakeEmbeddingFactory {
2374                state: Arc::clone(&timeout_state),
2375                dimensions: status.dimensions,
2376            },
2377        };
2378        let error = run_with_fixture(&options, &status, &mut timeout_embedder).unwrap_err();
2379        assert_eq!(
2380            error.code.as_deref(),
2381            Some("FALLOW_SIMILAR_CODE_PROVIDER_FAILED")
2382        );
2383        assert_eq!(timeout_state.lock().unwrap().spawns, 0);
2384    }
2385
2386    fn location(path: &str, start_line: u32, end_line: u32) -> SimilarCodeLocation {
2387        SimilarCodeLocation {
2388            path: path.to_owned(),
2389            name: "candidate".to_owned(),
2390            start_line,
2391            start_column: 1,
2392            end_line,
2393            end_column: 1,
2394            source_sha256: "00".repeat(32),
2395        }
2396    }
2397
2398    fn file_trace(imports_from: &[&str], imported_by: &[&str]) -> fallow_engine::trace::FileTrace {
2399        fallow_engine::trace::FileTrace {
2400            file: PathBuf::from("src/current.ts"),
2401            is_reachable: true,
2402            is_entry_point: false,
2403            exports: Vec::new(),
2404            imports_from: imports_from.iter().map(PathBuf::from).collect(),
2405            imported_by: imported_by.iter().map(PathBuf::from).collect(),
2406            re_exports: Vec::new(),
2407            sources: Vec::new(),
2408        }
2409    }
2410
2411    fn side_evidence() -> SimilarCodeSideEvidence {
2412        SimilarCodeSideEvidence {
2413            source_window: None,
2414            parameter_count: None,
2415            is_async: None,
2416            is_generator: None,
2417            has_await: None,
2418            has_throw: None,
2419            side_effect_hint: None,
2420            entry_point_reachable: None,
2421            callers: Vec::new(),
2422            callees: Vec::new(),
2423            owners: Vec::new(),
2424            churn_commits: None,
2425            tests: Vec::new(),
2426            deterministic_clone_coverage: None,
2427            runtime_observations: None,
2428        }
2429    }
2430
2431    #[test]
2432    fn similar_code_phase_statuses_identify_only_the_incomplete_phase() {
2433        let phases = phases(
2434            5,
2435            5,
2436            3,
2437            3,
2438            3,
2439            3,
2440            PhaseCompleteness {
2441                discovery: true,
2442                extraction: false,
2443                embedding: true,
2444                comparison: true,
2445            },
2446            0,
2447            0,
2448            0,
2449        );
2450
2451        assert_eq!(phases[0].status, SimilarCodePhaseStatus::Complete);
2452        assert_eq!(phases[1].status, SimilarCodePhaseStatus::Partial);
2453        assert!(phases[1].reason.is_some());
2454        assert_eq!(phases[3].status, SimilarCodePhaseStatus::Complete);
2455        assert_eq!(phases[4].status, SimilarCodePhaseStatus::Complete);
2456        assert_eq!(phases[5].status, SimilarCodePhaseStatus::Complete);
2457    }
2458
2459    #[test]
2460    fn similar_code_extraction_completion_counts_limits_and_read_failures_honestly() {
2461        let mut skips = BTreeMap::from([(SimilarCodeSkipReason::BelowMinimumLines, 2)]);
2462        assert!(extraction_is_complete(&skips, 0));
2463        assert!(!extraction_is_complete(&skips, 1));
2464
2465        skips.insert(SimilarCodeSkipReason::InputLimit, 3);
2466        assert!(!extraction_is_complete(&skips, 0));
2467        assert_eq!(remaining_extraction_inputs(7, 3), 4);
2468    }
2469
2470    #[test]
2471    fn similar_code_exhausted_extraction_budget_uses_the_specific_skip_reason() {
2472        assert_eq!(
2473            exhausted_extraction_limit(0, 1),
2474            Some(SimilarCodeSkipReason::InputLimit)
2475        );
2476        assert_eq!(
2477            exhausted_extraction_limit(1, 0),
2478            Some(SimilarCodeSkipReason::SourceBytesLimit)
2479        );
2480        assert_eq!(exhausted_extraction_limit(1, 1), None);
2481    }
2482
2483    #[test]
2484    fn similar_code_scope_requires_one_endpoint_to_match_every_active_filter() {
2485        let root = tempfile::tempdir().unwrap();
2486        let resolved =
2487            resolve_programmatic_analysis_context_deferred_workspace(&crate::AnalysisOptions {
2488                root: Some(root.path().to_path_buf()),
2489                ..crate::AnalysisOptions::default()
2490            })
2491            .unwrap();
2492        let options = SimilarCodeOptions {
2493            files: vec![PathBuf::from("src/file-scoped.ts")],
2494            ..SimilarCodeOptions::default()
2495        };
2496        let changed = FxHashSet::from_iter([PathBuf::from("src/changed.ts")]);
2497
2498        assert!(similar_code_scope_active(
2499            &options,
2500            &resolved,
2501            Some(&changed),
2502            None,
2503        ));
2504        assert!(!similar_code_path_in_scope(
2505            "src/file-scoped.ts",
2506            &options,
2507            &resolved,
2508            Some(&changed),
2509            None,
2510        ));
2511        assert!(!similar_code_path_in_scope(
2512            "src/changed.ts",
2513            &options,
2514            &resolved,
2515            Some(&changed),
2516            None,
2517        ));
2518
2519        let changed = FxHashSet::from_iter([PathBuf::from("src/file-scoped.ts")]);
2520        assert!(similar_code_path_in_scope(
2521            "src/file-scoped.ts",
2522            &options,
2523            &resolved,
2524            Some(&changed),
2525            None,
2526        ));
2527    }
2528
2529    #[test]
2530    fn similar_code_module_references_are_sorted_deduplicated_and_bounded() {
2531        let paths = vec![
2532            PathBuf::from("src/z.ts"),
2533            PathBuf::from("src/a.ts"),
2534            PathBuf::from("src/a.ts"),
2535        ];
2536
2537        let (references, truncated) = bounded_module_references(&paths, 1);
2538
2539        assert!(truncated);
2540        assert_eq!(references.len(), 1);
2541        assert_eq!(references[0].path, "src/a.ts");
2542        assert_eq!(references[0].name, MODULE_REFERENCE_NAME);
2543        assert_eq!(references[0].line, 1);
2544    }
2545
2546    #[test]
2547    fn similar_code_related_tests_are_transitive_path_filtered_and_bounded() {
2548        let paths = vec![
2549            "src/helper.ts".to_owned(),
2550            "tests/z.spec.ts".to_owned(),
2551            "src/a.test.ts".to_owned(),
2552            "src/a.test.ts".to_owned(),
2553        ];
2554
2555        let (tests, truncated) = bounded_related_tests(Path::new("/project"), &paths, 1);
2556
2557        assert!(truncated);
2558        assert_eq!(tests, vec!["src/a.test.ts"]);
2559    }
2560
2561    #[test]
2562    fn similar_code_related_tests_leave_out_test_support_files() {
2563        let paths = vec![
2564            "src/__mocks__/api.ts".to_owned(),
2565            "src/__fixtures__/user.ts".to_owned(),
2566            "src/__snapshots__/api.ts.snap".to_owned(),
2567            "src/user.fixture.ts".to_owned(),
2568            "src/api.test.ts".to_owned(),
2569        ];
2570
2571        let (tests, truncated) = bounded_related_tests(Path::new("/project"), &paths, 10);
2572
2573        assert!(!truncated);
2574        assert_eq!(tests, vec!["src/api.test.ts"]);
2575    }
2576
2577    #[test]
2578    fn similar_code_module_relationship_uses_direct_edges_then_shared_importers() {
2579        let left_location = location("src/left.ts", 1, 3);
2580        let right_location = location("src/right.ts", 1, 3);
2581        let left = file_trace(&["src/right.ts"], &["src/shared.ts"]);
2582        let right = file_trace(&[], &["src/shared.ts"]);
2583
2584        assert_eq!(
2585            module_relationship(&left, &right, &left_location, &right_location),
2586            "left-directly-imports-right"
2587        );
2588
2589        let left = file_trace(&[], &["src/shared.ts"]);
2590        assert_eq!(
2591            module_relationship(&left, &right, &left_location, &right_location),
2592            "shared-direct-importer"
2593        );
2594    }
2595
2596    #[test]
2597    fn similar_code_primary_owner_uses_the_codeowners_winning_rule() {
2598        let codeowners = CodeOwners::parse("/src/* @team/base\n/src/special.ts @team/special")
2599            .expect("CODEOWNERS parses");
2600
2601        assert_eq!(
2602            primary_owner(&codeowners, "src/special.ts"),
2603            vec!["@team/special"]
2604        );
2605        assert!(primary_owner(&codeowners, "test/a.ts").is_empty());
2606    }
2607
2608    #[test]
2609    fn similar_code_churn_lookup_normalizes_path_separators_and_defaults_to_zero() {
2610        let mut files = FxHashMap::default();
2611        files.insert(
2612            PathBuf::from("C:/repo/src/a.ts"),
2613            fallow_engine::churn::FileChurn {
2614                path: PathBuf::from("C:/repo/src/a.ts"),
2615                commits: 7,
2616                weighted_commits: 0.0,
2617                lines_added: 0,
2618                lines_deleted: 0,
2619                trend: fallow_engine::churn::ChurnTrend::Stable,
2620                authors: FxHashMap::default(),
2621            },
2622        );
2623        let churn = fallow_engine::churn::ChurnResult {
2624            files,
2625            shallow_clone: false,
2626            author_pool: Vec::new(),
2627            clock: fallow_engine::clock::AnalysisClock::pinned(1_788_782_400),
2628            git_log_bytes: 0,
2629        };
2630
2631        assert_eq!(churn_commits_for(&churn, Path::new(r"C:\repo\src\a.ts")), 7);
2632        assert_eq!(churn_commits_for(&churn, Path::new("src/missing.ts")), 0);
2633    }
2634
2635    #[test]
2636    fn similar_code_clone_coverage_counts_unique_exact_lines_only() {
2637        let root = Path::new("/repo");
2638        let exact = fallow_engine::duplicates::CloneGroup {
2639            instances: vec![
2640                clone_instance("/repo/src/a.ts", 2, 4),
2641                clone_instance("/repo/src/a.ts", 4, 6),
2642            ],
2643            token_count: 10,
2644            line_count: 3,
2645            similarity: None,
2646        };
2647        let near = fallow_engine::duplicates::CloneGroup {
2648            instances: vec![clone_instance("/repo/src/a.ts", 7, 9)],
2649            token_count: 10,
2650            line_count: 3,
2651            similarity: Some(0.9),
2652        };
2653        let report = fallow_engine::duplicates::DuplicationReport {
2654            clone_groups: vec![exact, near],
2655            ..fallow_engine::duplicates::DuplicationReport::default()
2656        };
2657
2658        assert_eq!(
2659            deterministic_clone_coverage(&report, root, &location("src/a.ts", 1, 10)),
2660            0.5
2661        );
2662    }
2663
2664    #[test]
2665    fn similar_code_project_artifacts_enrich_both_sides_deterministically() {
2666        let project = tempfile::tempdir().expect("temporary project");
2667        let root = project.path();
2668        std::fs::create_dir_all(root.join("src")).expect("source directory");
2669        std::fs::create_dir_all(root.join("tests")).expect("test directory");
2670        std::fs::write(
2671            root.join("package.json"),
2672            r#"{"name":"inspect-enrichment","main":"src/index.ts"}"#,
2673        )
2674        .expect("package manifest");
2675        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";
2676        std::fs::write(root.join("src/left.ts"), repeated).expect("left source");
2677        std::fs::write(root.join("src/right.ts"), repeated).expect("right source");
2678        std::fs::write(
2679            root.join("src/index.ts"),
2680            "import { candidate as left } from './left';\nimport { candidate as right } from './right';\nconsole.log(left([1]), right([2]));\n",
2681        )
2682        .expect("entry source");
2683        std::fs::write(
2684            root.join("tests/left.test.ts"),
2685            "import { candidate } from '../src/left';\ntest('candidate', () => expect(candidate([1])).toBeTruthy());\n",
2686        )
2687        .expect("test source");
2688
2689        let session = AnalysisSession::load_with_config(root, None, |config| {
2690            config.duplicates.min_tokens = 5;
2691            config.duplicates.min_lines = 2;
2692        })
2693        .expect("analysis session");
2694        let left_location = location("src/left.ts", 1, 6);
2695        let right_location = location("src/right.ts", 1, 6);
2696        let mut left = side_evidence();
2697        let mut right = side_evidence();
2698        let mut result = InspectEnrichment {
2699            availability: unavailable_inspect_enrichment(),
2700            graph_relationship: None,
2701            diagnostics: Vec::new(),
2702        };
2703
2704        enrich_graph_and_clones(
2705            &session,
2706            &left_location,
2707            &right_location,
2708            &mut left,
2709            &mut right,
2710            &mut result,
2711        );
2712
2713        assert_eq!(
2714            result.graph_relationship.as_deref(),
2715            Some("shared-direct-importer")
2716        );
2717        assert_eq!(
2718            result.availability.entry_point_reachability,
2719            SimilarCodeEnrichmentState::Available
2720        );
2721        assert_eq!(left.entry_point_reachable, Some(true));
2722        assert!(
2723            left.callers
2724                .iter()
2725                .any(|reference| reference.path == "src/index.ts")
2726        );
2727        assert_eq!(left.tests, vec!["tests/left.test.ts"]);
2728        assert!(
2729            left.deterministic_clone_coverage
2730                .is_some_and(|value| value > 0.0)
2731        );
2732        assert!(
2733            right
2734                .deterministic_clone_coverage
2735                .is_some_and(|value| value > 0.0)
2736        );
2737    }
2738
2739    fn clone_instance(
2740        file: &str,
2741        start_line: usize,
2742        end_line: usize,
2743    ) -> fallow_engine::duplicates::CloneInstance {
2744        fallow_engine::duplicates::CloneInstance {
2745            file: PathBuf::from(file),
2746            start_line,
2747            end_line,
2748            start_col: 0,
2749            end_col: 0,
2750            fragment: String::new(),
2751        }
2752    }
2753
2754    fn shared_review_key_candidates() -> (Vec<u8>, Vec<SimilarCodeCandidate>) {
2755        let (_temp, project, status) = similar_code_fixture();
2756        std::fs::copy(project.join("src/a.ts"), project.join("src/d.ts")).unwrap();
2757        let mut embedder = FixtureEmbedder {
2758            provider_cache_dir: PathBuf::from(&status.cache_dir),
2759            run_timeout: Duration::from_secs(5),
2760            factory: FakeEmbeddingFactory {
2761                state: Arc::new(Mutex::new(FakeProviderState::default())),
2762                dimensions: status.dimensions,
2763            },
2764        };
2765        let output = run_with_fixture(&fixture_options(&project), &status, &mut embedder).unwrap();
2766        let mut document = serde_json::to_value(&output).unwrap();
2767        document["kind"] = Value::from("similar-code");
2768        (serde_json::to_vec(&document).unwrap(), output.candidates)
2769    }
2770
2771    fn abstaining_verdict(candidate_id: &str, review_key: &str) -> Value {
2772        serde_json::json!({
2773            "candidate_id": candidate_id,
2774            "review_key": review_key,
2775            "candidate_worthy": null,
2776            "behaviorally_equivalent": null,
2777            "refactor_safe": null,
2778            "outcome": "needs-human-review",
2779            "rationale": "Evidence is incomplete."
2780        })
2781    }
2782
2783    fn verdict_document(verdicts: &[Value]) -> Vec<u8> {
2784        serde_json::to_vec(&serde_json::json!({
2785            "schema_version": "1",
2786            "verdicts": verdicts,
2787        }))
2788        .unwrap()
2789    }
2790
2791    /// Returns the shared review key and the candidates that carry it.
2792    fn shared_key_group(
2793        candidates: &[SimilarCodeCandidate],
2794    ) -> (String, Vec<&SimilarCodeCandidate>) {
2795        let mut groups: BTreeMap<&str, Vec<&SimilarCodeCandidate>> = BTreeMap::new();
2796        for candidate in candidates {
2797            groups
2798                .entry(candidate.review_key.as_str())
2799                .or_default()
2800                .push(candidate);
2801        }
2802        let (key, group) = groups
2803            .into_iter()
2804            .find(|(_, group)| group.len() > 1)
2805            .expect("a copied function gives two candidates with one review_key");
2806        (key.to_owned(), group)
2807    }
2808
2809    #[test]
2810    fn review_accepts_candidate_id_verdicts_that_share_a_review_key() {
2811        let (candidate_json, candidates) = shared_review_key_candidates();
2812        shared_key_group(&candidates);
2813        let verdicts = verdict_document(
2814            &candidates
2815                .iter()
2816                .map(|candidate| abstaining_verdict(&candidate.candidate_id, &candidate.review_key))
2817                .collect::<Vec<_>>(),
2818        );
2819
2820        let reviewed = review_similar_code(&candidate_json, &verdicts, true).unwrap();
2821
2822        assert_eq!(reviewed.candidates.len(), candidates.len());
2823        assert!(reviewed.candidates.iter().all(|reviewed| {
2824            reviewed.verdict.is_some()
2825                && reviewed.verdict_match == SimilarCodeVerdictMatch::CandidateId
2826        }));
2827    }
2828
2829    #[test]
2830    fn review_does_not_apply_a_review_key_verdict_that_matches_several_candidates() {
2831        let (candidate_json, candidates) = shared_review_key_candidates();
2832        let (key, group) = shared_key_group(&candidates);
2833        let verdicts = verdict_document(&[abstaining_verdict("sc_stale", &key)]);
2834
2835        let reviewed = review_similar_code(&candidate_json, &verdicts, false).unwrap();
2836        assert!(
2837            reviewed.diagnostics.iter().any(|diagnostic| {
2838                diagnostic.code == "FALLOW_SIMILAR_CODE_REVIEW_KEY_AMBIGUOUS"
2839            })
2840        );
2841        for candidate in &group {
2842            let joined = reviewed
2843                .candidates
2844                .iter()
2845                .find(|reviewed| reviewed.candidate.candidate_id == candidate.candidate_id)
2846                .unwrap();
2847            assert!(joined.verdict.is_none());
2848            assert_eq!(
2849                joined.verdict_match,
2850                SimilarCodeVerdictMatch::AmbiguousReviewKey
2851            );
2852        }
2853
2854        let error = review_similar_code(&candidate_json, &verdicts, true).unwrap_err();
2855        assert_eq!(error.message, "a verdict is required for every candidate");
2856    }
2857
2858    #[test]
2859    fn review_keeps_candidate_id_matches_next_to_an_ambiguous_review_key_verdict() {
2860        let (candidate_json, candidates) = shared_review_key_candidates();
2861        let (key, group) = shared_key_group(&candidates);
2862        let first = group[0];
2863        for verdicts in [
2864            vec![
2865                abstaining_verdict(&first.candidate_id, &key),
2866                abstaining_verdict("sc_stale", &key),
2867            ],
2868            vec![
2869                abstaining_verdict("sc_stale", &key),
2870                abstaining_verdict(&first.candidate_id, &key),
2871            ],
2872        ] {
2873            let reviewed =
2874                review_similar_code(&candidate_json, &verdict_document(&verdicts), false).unwrap();
2875            let joined = reviewed
2876                .candidates
2877                .iter()
2878                .find(|reviewed| reviewed.candidate.candidate_id == first.candidate_id)
2879                .unwrap();
2880            assert!(joined.verdict.is_some());
2881            assert_eq!(joined.verdict_match, SimilarCodeVerdictMatch::CandidateId);
2882        }
2883    }
2884
2885    #[test]
2886    fn review_rejects_repeated_identities_that_do_not_identify_one_candidate() {
2887        let (candidate_json, candidates) = shared_review_key_candidates();
2888        let (key, _) = shared_key_group(&candidates);
2889        let unique = candidates
2890            .iter()
2891            .find(|candidate| {
2892                candidates
2893                    .iter()
2894                    .filter(|other| other.review_key == candidate.review_key)
2895                    .count()
2896                    == 1
2897            })
2898            .expect("the fixture has a candidate with a unique review_key");
2899        let cases = [
2900            (
2901                vec![
2902                    abstaining_verdict(&unique.candidate_id, &unique.review_key),
2903                    abstaining_verdict(&unique.candidate_id, &unique.review_key),
2904                ],
2905                "verdict document contains duplicate candidate or review identities",
2906            ),
2907            (
2908                vec![
2909                    abstaining_verdict("sc_stale_one", &key),
2910                    abstaining_verdict("sc_stale_two", &key),
2911                ],
2912                "verdict document contains duplicate candidate or review identities",
2913            ),
2914            (
2915                vec![
2916                    abstaining_verdict(&unique.candidate_id, &unique.review_key),
2917                    abstaining_verdict("sc_stale", &unique.review_key),
2918                ],
2919                "multiple verdicts resolve to the same candidate",
2920            ),
2921            (
2922                vec![
2923                    abstaining_verdict("sc_stale", &unique.review_key),
2924                    abstaining_verdict(&unique.candidate_id, &unique.review_key),
2925                ],
2926                "multiple verdicts resolve to the same candidate",
2927            ),
2928            (
2929                vec![abstaining_verdict(&unique.candidate_id, &key)],
2930                "verdict review_key does not match its candidate_id",
2931            ),
2932        ];
2933        for (verdicts, message) in cases {
2934            let error = review_similar_code(&candidate_json, &verdict_document(&verdicts), false)
2935                .unwrap_err();
2936            assert_eq!(error.message, message);
2937        }
2938    }
2939}