All files / src/llm-orchestration/action-handlers/matchers hybrid.matcher.ts

96.66% Statements 29/30
80% Branches 16/20
100% Functions 5/5
96.55% Lines 28/29

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107  8x 8x 8x           8x 32x 32x 32x                           19x 19x 12x       7x 7x         7x 7x 1x       6x                               6x             6x     6x     6x       6x                             6x   6x 24x 24x   24x 8x       6x      
import { MatchResult, MatchCandidate } from './base.matcher';
import { ExactMatcher } from './exact.matcher';
import { NormalizedMatcher } from './normalized.matcher';
import { ContextAwareMatcher } from './context-aware.matcher';
 
/**
 * Hybrid matcher that combines multiple matching strategies.
 * Tries exact match first, then falls back to more sophisticated methods.
 */
export class HybridMatcher {
  private readonly exactMatcher = new ExactMatcher();
  private readonly normalizedMatcher = new NormalizedMatcher();
  private readonly contextAwareMatcher = new ContextAwareMatcher();
 
  /**
   * Attempt to find the best match using all available strategies.
   *
   * Strategy:
   * 1. Try exact match (fastest, highest confidence)
   * 2. Try normalized match (handles whitespace variations)
   * 3. Try context-aware match (disambiguates using surrounding context)
   *
   * Returns the first successful match, or aggregates all candidates on failure.
   */
  findBestMatch(searchBlock: string, content: string): MatchResult {
    // Phase 1: Exact match (fastest)
    const exactResult = this.exactMatcher.match(searchBlock, content);
    if (exactResult.found && exactResult.unique) {
      return exactResult;
    }
 
    // Phase 2: Normalized match (handles whitespace)
    const normalizedResult = this.normalizedMatcher.match(searchBlock, content);
    Iif (normalizedResult.found && normalizedResult.unique) {
      return normalizedResult;
    }
 
    // Phase 3: Context-aware match (disambiguates)
    const contextResult = this.contextAwareMatcher.match(searchBlock, content);
    if (contextResult.found && contextResult.unique) {
      return contextResult;
    }
 
    // Phase 4: Failure - aggregate all candidates
    return this.aggregateFailureCandidates(
      exactResult,
      normalizedResult,
      contextResult,
    );
  }
 
  /**
   * Aggregate candidates from all matchers on failure.
   * Deduplicates similar candidates and sorts by confidence.
   */
  private aggregateFailureCandidates(
    exactResult: MatchResult,
    normalizedResult: MatchResult,
    contextResult: MatchResult,
  ): MatchResult {
    const allCandidates = [
      ...(exactResult.candidates || []),
      ...(normalizedResult.candidates || []),
      ...(contextResult.candidates || []),
    ];
 
    // Deduplicate candidates with same startLine
    const deduplicated = this.deduplicateCandidates(allCandidates);
 
    // Sort by confidence descending
    deduplicated.sort((a, b) => b.confidence - a.confidence);
 
    const bestConfidence =
      deduplicated.length > 0 ? deduplicated[0].confidence : 0;
 
    // Return found: true if we have any candidates, even if not unique
    // This is important for showing options to the user
    return {
      found: deduplicated.length > 0,
      unique: false,
      confidence: bestConfidence,
      candidates: deduplicated,
    };
  }
 
  /**
   * Remove duplicate candidates (same startLine and endLine).
   * Keep the one with highest confidence.
   */
  private deduplicateCandidates(
    candidates: MatchCandidate[],
  ): MatchCandidate[] {
    const map = new Map<string, MatchCandidate>();
 
    for (const candidate of candidates) {
      const key = `${candidate.startLine}-${candidate.endLine}`;
      const existing = map.get(key);
 
      if (!existing || candidate.confidence > existing.confidence) {
        map.set(key, candidate);
      }
    }
 
    return Array.from(map.values());
  }
}