Skip to main content

fallow_api/runtime/
similar_code.rs

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