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