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