All files / src/sdk prepare.ts

91.17% Statements 31/34
80% Branches 16/20
100% Functions 4/4
93.54% Lines 29/31

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                              11x   11x 12x 12x 4x   8x       11x                     12x   12x 1x     11x 11x 11x   11x 10x     10x 10x               10x                 10x   10x     10x 2x 2x       8x         10x 10x             10x       10x     11x          
import type { EventContext, SkippedFile } from '../types/index.js';
import {
  parseFileDiff,
  expandDiffContext,
  classifyFile,
  coalesceHunks,
  splitLargeHunks,
  type HunkWithContext,
} from '../diff/index.js';
import type { PreparedFile, PrepareFilesOptions, PrepareFilesResult } from './types.js';
 
/**
 * Group hunks by filename into PreparedFile entries.
 */
export function groupHunksByFile(hunks: HunkWithContext[]): PreparedFile[] {
  const fileMap = new Map<string, HunkWithContext[]>();
 
  for (const hunk of hunks) {
    const existing = fileMap.get(hunk.filename);
    if (existing) {
      existing.push(hunk);
    } else {
      fileMap.set(hunk.filename, [hunk]);
    }
  }
 
  return Array.from(fileMap, ([filename, fileHunks]) => ({ filename, hunks: fileHunks }));
}
 
/**
 * Prepare files for analysis by parsing patches into hunks with context.
 * Returns files that have changes to analyze and files that were skipped.
 */
export function prepareFiles(
  context: EventContext,
  options: PrepareFilesOptions = {}
): PrepareFilesResult {
  const { contextLines = 20, chunking } = options;
 
  if (!context.pullRequest) {
    return { files: [], skippedFiles: [] };
  }
 
  const pr = context.pullRequest;
  const allHunks: HunkWithContext[] = [];
  const skippedFiles: SkippedFile[] = [];
 
  for (const file of pr.files) {
    Iif (!file.patch) continue;
 
    // Check if this file should be skipped based on chunking patterns
    const mode = classifyFile(file.filename, chunking?.filePatterns);
    Iif (mode === 'skip') {
      skippedFiles.push({
        filename: file.filename,
        reason: 'builtin', // Could be enhanced to track which pattern matched
      });
      continue;
    }
 
    const statusMap: Record<string, 'added' | 'removed' | 'modified' | 'renamed'> = {
      added: 'added',
      removed: 'removed',
      modified: 'modified',
      renamed: 'renamed',
      copied: 'added',
      changed: 'modified',
      unchanged: 'modified',
    };
    const status = statusMap[file.status] ?? 'modified';
 
    const diff = parseFileDiff(file.filename, file.patch, status);
 
    // Skip files with no meaningful diff content (e.g., empty files)
    if (diff.hunks.length === 0 || diff.hunks.every((h) => h.newCount === 0 && h.oldCount === 0)) {
      skippedFiles.push({ filename: file.filename, reason: 'builtin' });
      continue;
    }
 
    // Split large hunks first (handles large files becoming single hunks)
    const splitHunks = splitLargeHunks(diff.hunks, {
      maxChunkSize: chunking?.coalesce?.maxChunkSize,
    });
 
    // Then coalesce nearby small ones if enabled (default: enabled)
    const coalesceEnabled = chunking?.coalesce?.enabled !== false;
    const hunks = coalesceEnabled
      ? coalesceHunks(splitHunks, {
          maxGapLines: chunking?.coalesce?.maxGapLines,
          maxChunkSize: chunking?.coalesce?.maxChunkSize,
        })
      : splitHunks;
 
    const hunksWithContext = expandDiffContext(context.repoPath, { ...diff, hunks }, {
      contextLines,
      contentSource: context.diffContextSource,
    });
    allHunks.push(...hunksWithContext);
  }
 
  return {
    files: groupHunksByFile(allHunks),
    skippedFiles,
  };
}