basant307/AI_Governance_Project
048
1/**2 * @license3 * Copyright 2025 Google LLC4 * SPDX-License-Identifier: Apache-2.05 */6 7import * as Diff from 'diff';8import type { DiffStat } from './tools.js';9 10export const DEFAULT_DIFF_OPTIONS: Diff.PatchOptions = {11 context: 3,12 ignoreWhitespace: true,13};14 15/**16 * Returns true when the unified diff patch string contains at least one hunk.17 */18export function hasHunks(patch: string): boolean {19 return patch.includes('\n@@ ');20}21 22/**23 * Creates a unified diff patch with smart whitespace handling.24 *25 * Uses ignoreWhitespace:true first to produce clean diffs when content and26 * whitespace change together. Falls back to ignoreWhitespace:false when no27 * hunks are found, so that whitespace-only edits (e.g. re-indentation) still28 * produce a visible diff instead of "No changes detected".29 */30export function createPatchSmart(31 filename: string,32 oldStr: string,33 newStr: string,34 oldHeader?: string,35 newHeader?: string,36): string {37 const cleanPatch = Diff.createPatch(38 filename,39 oldStr,40 newStr,41 oldHeader,42 newHeader,43 DEFAULT_DIFF_OPTIONS,44 );45 46 if (hasHunks(cleanPatch)) {47 return cleanPatch;48 }49 50 return Diff.createPatch(filename, oldStr, newStr, oldHeader, newHeader, {51 ...DEFAULT_DIFF_OPTIONS,52 ignoreWhitespace: false,53 });54}55 56function structuredPatchSmart(57 filename: string,58 oldStr: string,59 newStr: string,60 oldHeader?: string,61 newHeader?: string,62): Diff.ParsedDiff {63 const result = Diff.structuredPatch(64 filename,65 filename,66 oldStr,67 newStr,68 oldHeader,69 newHeader,70 DEFAULT_DIFF_OPTIONS,71 );72 73 if (result.hunks.length > 0) {74 return result;75 }76 77 return Diff.structuredPatch(78 filename,79 filename,80 oldStr,81 newStr,82 oldHeader,83 newHeader,84 {85 ...DEFAULT_DIFF_OPTIONS,86 ignoreWhitespace: false,87 },88 );89}90 91export function getDiffStat(92 fileName: string,93 oldStr: string,94 aiStr: string,95 userStr: string,96): DiffStat {97 const getStats = (patch: Diff.ParsedDiff) => {98 let addedLines = 0;99 let removedLines = 0;100 let addedChars = 0;101 let removedChars = 0;102 103 patch.hunks.forEach((hunk: Diff.Hunk) => {104 hunk.lines.forEach((line: string) => {105 if (line.startsWith('+')) {106 addedLines++;107 addedChars += line.length - 1;108 } else if (line.startsWith('-')) {109 removedLines++;110 removedChars += line.length - 1;111 }112 });113 });114 return { addedLines, removedLines, addedChars, removedChars };115 };116 117 const modelPatch = structuredPatchSmart(118 fileName,119 oldStr,120 aiStr,121 'Current',122 'Proposed',123 );124 const modelStats = getStats(modelPatch);125 126 const userStats =127 aiStr === userStr128 ? {129 addedLines: 0,130 removedLines: 0,131 addedChars: 0,132 removedChars: 0,133 }134 : getStats(135 structuredPatchSmart(fileName, aiStr, userStr, 'Proposed', 'User'),136 );137 138 return {139 model_added_lines: modelStats.addedLines,140 model_removed_lines: modelStats.removedLines,141 model_added_chars: modelStats.addedChars,142 model_removed_chars: modelStats.removedChars,143 user_added_lines: userStats.addedLines,144 user_removed_lines: userStats.removedLines,145 user_added_chars: userStats.addedChars,146 user_removed_chars: userStats.removedChars,147 };148}149 