@gitsense/gsc-utils 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (48) hide show
  1. package/LICENSE +21 -0
  2. package/dist/gitsense-chat-utils.cjs.js +10977 -0
  3. package/dist/gitsense-chat-utils.esm.js +10975 -0
  4. package/dist/gsc-utils.cjs.js +11043 -0
  5. package/dist/gsc-utils.esm.js +11041 -0
  6. package/package.json +37 -0
  7. package/src/AnalysisBlockUtils.js +151 -0
  8. package/src/ChatUtils.js +126 -0
  9. package/src/CodeBlockUtils/blockExtractor.js +277 -0
  10. package/src/CodeBlockUtils/blockProcessor.js +559 -0
  11. package/src/CodeBlockUtils/blockProcessor.js.rej +8 -0
  12. package/src/CodeBlockUtils/constants.js +62 -0
  13. package/src/CodeBlockUtils/continuationUtils.js +191 -0
  14. package/src/CodeBlockUtils/headerParser.js +175 -0
  15. package/src/CodeBlockUtils/headerUtils.js +236 -0
  16. package/src/CodeBlockUtils/index.js +83 -0
  17. package/src/CodeBlockUtils/lineNumberFormatter.js +117 -0
  18. package/src/CodeBlockUtils/markerRemover.js +89 -0
  19. package/src/CodeBlockUtils/patchIntegration.js +38 -0
  20. package/src/CodeBlockUtils/relationshipUtils.js +159 -0
  21. package/src/CodeBlockUtils/updateCodeBlock.js +372 -0
  22. package/src/CodeBlockUtils/uuidUtils.js +48 -0
  23. package/src/ContextUtils.js +180 -0
  24. package/src/GSToolBlockUtils.js +108 -0
  25. package/src/GitSenseChatUtils.js +386 -0
  26. package/src/JsonUtils.js +101 -0
  27. package/src/LLMUtils.js +31 -0
  28. package/src/MessageUtils.js +460 -0
  29. package/src/PatchUtils/constants.js +72 -0
  30. package/src/PatchUtils/diagnosticReporter.js +213 -0
  31. package/src/PatchUtils/enhancedPatchProcessor.js +390 -0
  32. package/src/PatchUtils/fuzzyMatcher.js +252 -0
  33. package/src/PatchUtils/hunkCorrector.js +204 -0
  34. package/src/PatchUtils/hunkValidator.js +305 -0
  35. package/src/PatchUtils/index.js +135 -0
  36. package/src/PatchUtils/patchExtractor.js +175 -0
  37. package/src/PatchUtils/patchHeaderFormatter.js +143 -0
  38. package/src/PatchUtils/patchParser.js +289 -0
  39. package/src/PatchUtils/patchProcessor.js +389 -0
  40. package/src/PatchUtils/patchVerifier/constants.js +23 -0
  41. package/src/PatchUtils/patchVerifier/detectAndFixOverlappingHunks.js +281 -0
  42. package/src/PatchUtils/patchVerifier/detectAndFixRedundantChanges.js +404 -0
  43. package/src/PatchUtils/patchVerifier/formatAndAddLineNumbers.js +165 -0
  44. package/src/PatchUtils/patchVerifier/index.js +25 -0
  45. package/src/PatchUtils/patchVerifier/verifyAndCorrectHunkHeaders.js +202 -0
  46. package/src/PatchUtils/patchVerifier/verifyAndCorrectLineNumbers.js +254 -0
  47. package/src/SharedUtils/timestampUtils.js +41 -0
  48. package/src/SharedUtils/versionUtils.js +58 -0
@@ -0,0 +1,389 @@
1
+ /**
2
+ * Component: PatchUtils Processor
3
+ * Block-UUID: 96ec8bca-9fc3-441b-b81f-6fefc840c626
4
+ * Parent-UUID: 9e5b8ae2-e978-4a20-b1dd-c044d9a5277f
5
+ * Version: 1.3.0
6
+ * Description: Handles applying, creating, and converting patches. Applies traditional unified diffs, cleaning line number prefixes from diff content before using jsdiff.
7
+ * Language: JavaScript
8
+ * Created-at: 2025-07-20T17:24:16.836Z
9
+ * Authors: Gemini 2.5 Pro (v1.0.0), Gemini 2.5 Pro (v1.1.0), Claude 3.7 Sonnet (v1.2.0), Gemini 2.5 Flash (v1.2.0), Gemini 2.5 Flash Thinking (v1.3.0)
10
+ */
11
+
12
+
13
+ const parser = require('./patchParser');
14
+ const verifier = require('./patchVerifier');
15
+ const jsdiff = require('diff');
16
+ const { removeLineNumbers } = require('../CodeBlockUtils/lineNumberFormatter');
17
+
18
+ /**
19
+ * Cleans line number prefixes ('NNN: ') from relevant lines in unified diff content.
20
+ * @param {string} diffContent - The raw unified diff content string.
21
+ * @returns {string} The cleaned diff content string, ready for jsdiff.
22
+ * @private
23
+ */
24
+ function _cleanDiffContentLineNumbers(diffContent) {
25
+ if (!diffContent) {
26
+ return "";
27
+ }
28
+
29
+ const lines = diffContent.split('\n');
30
+ const cleanedLines = lines.map(line => {
31
+ // Only clean lines starting with '+', '-', or ' ' (context)
32
+ // Leave @@ headers and other markers alone.
33
+ if (
34
+ !line.startsWith('+++') &&
35
+ !line.startsWith('---') &&
36
+ (line.startsWith('+') || line.startsWith('-') || line.startsWith(' '))
37
+ ) {
38
+ const prefix = line[0]; // +, -, or space
39
+ // Preserve the prefix character in its own column
40
+ // The content starts after the prefix character
41
+ const contentWithoutPrefix = line.substring(1);
42
+
43
+ // Check if the content has a line number pattern (e.g., "12: ")
44
+ const lineNumberMatch = contentWithoutPrefix.match(/^\s*\d+:\s/);
45
+ if (lineNumberMatch) {
46
+ return prefix + contentWithoutPrefix.substring(lineNumberMatch[0].length);
47
+ }
48
+
49
+ return prefix + contentWithoutPrefix;
50
+ }
51
+ return line; // Return unchanged if not a diff content line
52
+ });
53
+ return cleanedLines.join('\n');
54
+ }
55
+
56
+ /**
57
+ * Apply patch against source code. Expects traditional unified diff format.
58
+ * Cleans line number prefixes from patch content before applying with jsdiff.
59
+ * @param {string} sourceText - Original source code (MUST NOT contain line numbers).
60
+ * @param {string} patchText - Full patch block content (metadata + unified diff with line numbers on diff lines).
61
+ * @returns {{
62
+ * valid: boolean,
63
+ * patchedText: string | null,
64
+ * errors: string[]
65
+ * }} - Patch result with valid flag, patchedText (only if valid, without line numbers), and errors if any.
66
+ */
67
+ function applyPatch(sourceText, patchText) {
68
+ if (sourceText === null || sourceText === undefined) { // Allow empty string source
69
+ return {
70
+ valid: false,
71
+ patchedText: null,
72
+ errors: ["Source code is null or undefined."]
73
+ };
74
+ }
75
+
76
+ if (!patchText) {
77
+ return {
78
+ valid: false,
79
+ patchedText: null,
80
+ errors: ["Patch text is empty or null."]
81
+ };
82
+ }
83
+
84
+ let patchApplied = null;
85
+
86
+ try {
87
+ // 1. Determine format (should be traditional)
88
+ const format = parser.determinePatchFormat(patchText);
89
+ if (format !== 'traditional') {
90
+ // Also handle 'context' explicitly as an error now
91
+ const errorMsg = format === 'context'
92
+ ? "Unsupported patch format: 'context'. Only 'traditional' unified diff is supported."
93
+ : `Unknown or invalid patch format detected: ${format}. Expected 'traditional' unified diff.`;
94
+ return { valid: false, patchedText: null, errors: [errorMsg] };
95
+ }
96
+
97
+ // 1.1. Play it safe and clean sourceText by removing potential line numbers
98
+ sourceText = removeLineNumbers(sourceText);
99
+
100
+ // 2. Extract metadata (optional for applying, but good practice)
101
+ const metadata = parser.extractPatchMetadata(patchText);
102
+ // Could add metadata validation here if needed, but applyPatch focuses on application
103
+
104
+ // 3. Extract the raw diff content
105
+ let rawDiffContent = parser.extractPatchContent(patchText, 'traditional');
106
+
107
+ if (!rawDiffContent && patchText.includes('@@')) {
108
+ // Check if content is missing but markers were present
109
+ console.warn("applyPatch: Patch text contains '@@' but no content was extracted between markers.");
110
+ // Proceed, jsdiff might handle empty diffs, or fail gracefully.
111
+ } else if (!rawDiffContent) {
112
+ return { valid: false, patchedText: null, errors: ["Could not extract diff content from patch text."] };
113
+ }
114
+
115
+ // 4. Apply the patch
116
+ const patchedResult = jsdiff.applyPatch(sourceText, rawDiffContent, { fuzzFactor: 0 });
117
+
118
+ if (patchedResult === false) {
119
+ // jsdiff.applyPatch returns false on failure
120
+ return {
121
+ valid: false,
122
+ patchedText: null,
123
+ errors: ["Patch could not be applied. Hunk mismatch or invalid patch format likely."]
124
+ };
125
+ }
126
+
127
+ // Success! patchedResult is the patched string
128
+ return {
129
+ valid: true,
130
+ patchedText: patchedResult,
131
+ errors: []
132
+ };
133
+ } catch (error) {
134
+ // Check if it's a jsdiff specific error if possible, otherwise generic
135
+ const errorMessage = error instanceof Error ? error.message : "Unexpected error during patch application.";
136
+ return {
137
+ valid: false,
138
+ patchedText: null,
139
+ errors: [errorMessage]
140
+ };
141
+ }
142
+ }
143
+
144
+ /**
145
+ * Creates a patch between two code versions
146
+ * @param {string} sourceText - Original source code (WITH line numbers NNN:)
147
+ * @param {string} targetCode - Modified source code (WITH line numbers NNN:)
148
+ * @param {Object} metadata - Patch metadata
149
+ * @param {string} filename - Optional filename for the diff header (defaults to 'Original')
150
+ * @returns {string} Patch in unified diff format with metadata and line numbers in diff lines
151
+ * @throws {Error} If patch creation fails.
152
+ */
153
+ function createPatch(sourceText, targetCode, metadata = {}, filename = 'Original') {
154
+ if (sourceText === null || sourceText === undefined) {
155
+ throw new Error("createPatch: Source code is null or undefined");
156
+ }
157
+ if (targetCode === null || targetCode === undefined) {
158
+ throw new Error("createPatch: Target code is null or undefined");
159
+ }
160
+
161
+ try {
162
+ // --- CRITICAL: jsdiff needs CLEAN text ---
163
+ const cleanSource = removeLineNumbers(sourceText);
164
+ const cleanTarget = removeLineNumbers(targetCode);
165
+
166
+ // --- Generate standard diff ---
167
+ // Provide filenames for standard diff headers (--- filename \n +++ filename)
168
+ const diffResult = jsdiff.createPatch(filename, cleanSource, cleanTarget, '', '', { context: 3 });
169
+
170
+ // --- Post-process the diff to add line numbers back ---
171
+ const diffLines = diffResult.split('\n');
172
+ const outputLines = [];
173
+ let sourceLineCounter = 0; // 0-based index for original source lines array
174
+ const originalSourceLines = sourceText.split('\n'); // Keep original with numbers
175
+
176
+ // Skip the header lines generated by jsdiff (---, +++, @@) initially
177
+ let diffLineIndex = 0;
178
+ while (diffLineIndex < diffLines.length && !diffLines[diffLineIndex].startsWith('@@ ')) {
179
+ // Keep the standard --- and +++ headers, but potentially adjust filename if needed
180
+ if (diffLines[diffLineIndex].startsWith('--- ') || diffLines[diffLineIndex].startsWith('+++ ')) {
181
+ // Use conceptual filenames as per system prompt format
182
+ if (diffLines[diffLineIndex].startsWith('--- ')) outputLines.push('--- Original');
183
+ if (diffLines[diffLineIndex].startsWith('+++ ')) outputLines.push('+++ Modified');
184
+ } else {
185
+ // Keep other potential header lines if any (e.g., index) - unlikely with createPatch defaults
186
+ outputLines.push(diffLines[diffLineIndex]);
187
+ }
188
+ diffLineIndex++;
189
+ }
190
+
191
+ // Process hunks
192
+ while (diffLineIndex < diffLines.length) {
193
+ const line = diffLines[diffLineIndex];
194
+
195
+ if (line.startsWith('@@ ')) {
196
+ // Hunk header - Keep it as is, jsdiff generated it based on clean source,
197
+ // but the LLM expects it to reference the *numbered* source.
198
+ // This assumes the LLM correctly interprets these numbers against its numbered view.
199
+ outputLines.push(line);
200
+ // Extract starting line number from hunk to sync our sourceLineCounter
201
+ const match = line.match(/@@ -(\d+)/);
202
+ if (match && match[1]) {
203
+ // jsdiff line numbers are 1-based, adjust to 0-based index
204
+ sourceLineCounter = parseInt(match[1], 10) - 1;
205
+ } else {
206
+ // This shouldn't happen with valid diffs, but handle defensively
207
+ console.warn("Could not parse starting line number from hunk header:", line);
208
+ // Attempt to continue, but line numbers might be wrong
209
+ }
210
+ diffLineIndex++;
211
+ } else if (line.startsWith(' ')) { // Context line
212
+ // Find the corresponding original line with number prefix
213
+ const originalLine = originalSourceLines[sourceLineCounter] ?? line.substring(1); // Fallback if out of sync
214
+ outputLines.push(` ${originalLine}`); // Add space prefix back
215
+ sourceLineCounter++;
216
+ diffLineIndex++;
217
+ } else if (line.startsWith('-')) { // Deletion line
218
+ const originalLine = originalSourceLines[sourceLineCounter] ?? line.substring(1);
219
+ outputLines.push(`-${originalLine}`); // Add dash prefix back
220
+ sourceLineCounter++; // Consume a line from the source
221
+ diffLineIndex++;
222
+ } else if (line.startsWith('+')) { // Addition line
223
+ // Additions don't have a direct corresponding *original* line number prefix.
224
+ // We need to add the prefix based on the *target* line number.
225
+ // This is tricky. For simplicity matching the LLM example, let's find the *next*
226
+ // non-deleted line's number in the original source OR increment if at end.
227
+ // A truly robust solution might need the target lines array.
228
+ // Let's use the *current* source line number for the prefix, as seen in the example patch.
229
+ const currentLineNumStr = String(sourceLineCounter + 1).padStart(3, ' '); // Rough padding
230
+ outputLines.push(`+${currentLineNumStr}: ${line.substring(1)}`); // Add plus prefix and generated number
231
+ // Do NOT increment sourceLineCounter for additions
232
+ diffLineIndex++;
233
+ } else if (line.trim() === '' || line === '\') {
234
+ // Keep blank lines and the "No newline" marker
235
+ outputLines.push(line);
236
+ diffLineIndex++;
237
+ } else {
238
+ // Unexpected line format in diff
239
+ console.warn("Unexpected line format in diff output:", line);
240
+ outputLines.push(line); // Keep it anyway?
241
+ diffLineIndex++;
242
+ }
243
+ }
244
+
245
+ const processedDiff = outputLines.join('\n');
246
+
247
+ // Format metadata
248
+ const formattedMetadata = _formatPatchMetadata(metadata); // Use internal helper
249
+
250
+ // Combine metadata, markers, and processed patch
251
+ return `${formattedMetadata}\n\n# --- PATCH START MARKER ---\n${processedDiff}\n# --- PATCH END MARKER ---`;
252
+
253
+ } catch (error) {
254
+ console.error("Error creating patch:", error);
255
+ throw new Error(`Failed to create patch: ${error.message}`);
256
+ }
257
+ }
258
+
259
+ /**
260
+ * Creates a patch between two full code block strings (including their metadata headers).
261
+ * The generated patch will contain the custom metadata header and adjusted hunk line numbers
262
+ * that account for the original code block's header.
263
+ *
264
+ * @param {object} CodeBlockUtils - Instance of CodeBlockUtils to work around circular dependencies
265
+ * @param {string} sourceCodeBlockText - The complete string of the original code block (including its metadata header).
266
+ * @param {string} targetCodeBlockText - The complete string of the modified code block (including its metadata header).
267
+ * @param {object} patchMetadata - An object containing the necessary metadata for the patch header (e.g., Source-Block-UUID, Target-Block-UUID, etc.).
268
+ * @returns {object} Patch in unified diff format, patch with custom metadata and adjusted line numbers.
269
+ * @throws {Error} If inputs are invalid or patch creation fails.
270
+ */
271
+ function createPatchFromCodeBlocks(CodeBlockUtils, sourceCodeBlockText, targetCodeBlockText, patchMetadata = {}) {
272
+ if (typeof sourceCodeBlockText !== 'string' || typeof targetCodeBlockText !== 'string') {
273
+ throw new Error("createPatchFromCodeBlocks: Source and target code block texts must be strings.");
274
+ }
275
+
276
+ const [ sourceHeaderText ] = sourceCodeBlockText.split('\n\n\n');
277
+ const [ targetHeaderText ] = targetCodeBlockText.split('\n\n\n');
278
+
279
+ const sourceContentTemp = sourceCodeBlockText.split('\n\n\n'); sourceContentTemp.shift();
280
+ const sourceContent = sourceContentTemp.join('\n\n');
281
+
282
+ const targetContentTemp = targetCodeBlockText.split('\n\n\n'); targetContentTemp.shift();
283
+ const targetContent = targetContentTemp.join('\n\n');
284
+
285
+ // Get the number of lines in the header + two blank lines
286
+ const sourceHeaderLineCount = sourceHeaderText.split('\n').length;
287
+ const targetHeaderLineCount = targetHeaderText.split('\n').length;
288
+
289
+ // To keep things simple we are going to require the source and target header line count to be the same.
290
+ // By doing so, we can can just add the line count to the hunks
291
+ if (sourceHeaderLineCount !== targetHeaderLineCount) {
292
+ throw new Error('Source and target header line count must be the same');
293
+ }
294
+
295
+ const cleanSourceContent = sourceContent.trimEnd();
296
+ const cleanTargetContent = targetContent.trimEnd();
297
+
298
+ // 2. Generate raw diff on content only
299
+ const unadjustedDiffPatch = jsdiff.createTwoFilesPatch('Original', 'Modified', cleanSourceContent, cleanTargetContent, '', '', { context: 3, stripTrailingCr: true });
300
+
301
+ // 3. Post-process Diff to Adjust Line Numbers and Add NNN: Prefixes
302
+ const diffLines = unadjustedDiffPatch.split('\n');
303
+ const outputLines = [];
304
+ const outputDiffLines = [];
305
+
306
+ for (let i = 0; i < diffLines.length; i++) {
307
+ const line = diffLines[i];
308
+
309
+ if (!line.startsWith('@@ '))
310
+ continue;
311
+
312
+ // Hunk header: @@ -oldStart,oldCount +newStart,newCount @@
313
+ const match = line.match(/@@ -(\d+)(?:,(\d+))? \+(\d+)(?:,(\d+))? @@/);
314
+ if (match) {
315
+ let oldStart = parseInt(match[1], 10);
316
+ let oldCount = match[2] ? parseInt(match[2], 10) : 1;
317
+ let newStart = parseInt(match[3], 10);
318
+ let newCount = match[4] ? parseInt(match[4], 10) : 1;
319
+
320
+ // Adjust line numbers to account for the header and 2 blank lines
321
+ oldStart += sourceHeaderLineCount + 2;
322
+ newStart += targetHeaderLineCount + 2;
323
+
324
+ const adjustedHeader =
325
+ `@@ `+
326
+ `-${oldStart}${oldCount !== 1 ? ',' + oldCount : ''} `+
327
+ `+${newStart}${newCount !== 1 ? ',' + newCount : ''} `+
328
+ `@@`;
329
+
330
+ diffLines[i] = adjustedHeader;
331
+ }
332
+ }
333
+
334
+ const adjustedDiffPatch = diffLines.join('\n');
335
+
336
+ // 4. Assemble Final Patch
337
+ const formattedMetadata = _formatPatchMetadata(patchMetadata);
338
+ const formatted = `${formattedMetadata}\n\n\n`+
339
+ `# --- PATCH START MARKER ---\n`+
340
+ `${adjustedDiffPatch}\n`+
341
+ `# --- PATCH END MARKER ---`;
342
+ return {
343
+ formatted,
344
+ formattedMetadata,
345
+ unadjustedDiffPatch,
346
+ adjustedDiffPatch
347
+ };
348
+ }
349
+
350
+ /**
351
+ * Formats patch metadata as string
352
+ * @param {Object} metadata - Patch metadata
353
+ * @returns {string} Formatted metadata string
354
+ * @private
355
+ */
356
+ function _formatPatchMetadata(metadata) {
357
+ // --- Function body from original PatchUtils._formatPatchMetadata ---
358
+ // --- (Copied Verbatim) ---
359
+ const lines = ['# Patch Metadata'];
360
+ const metadataOrder = [
361
+ 'Source-Block-UUID', 'Target-Block-UUID', 'Source-Version', 'Target-Version',
362
+ 'Description', 'Authors'
363
+ ];
364
+
365
+ // Add ordered metadata
366
+ metadataOrder.forEach(key => {
367
+ if (metadata[key]) {
368
+ lines.push(`# ${key}: ${metadata[key]}`);
369
+ }
370
+ });
371
+
372
+ // Add any other metadata not in the preferred order
373
+ for (const [key, value] of Object.entries(metadata)) {
374
+ if (!metadataOrder.includes(key)) {
375
+ lines.push(`# ${key}: ${value}`);
376
+ }
377
+ }
378
+
379
+ return lines.join('\n');
380
+ // --- End of Function Body ---
381
+ }
382
+
383
+ module.exports = {
384
+ applyPatch,
385
+ createPatch,
386
+ createPatchFromCodeBlocks,
387
+ };
388
+
389
+
@@ -0,0 +1,23 @@
1
+ /**
2
+ * Component: PatchUtils Constants
3
+ * Block-UUID: 32adc00e-7509-4219-8e40-6c1319371db9
4
+ * Parent-UUID: N/A
5
+ * Version: 1.0.0
6
+ * Description: Contains shared constants and regular expressions used by the patch verification utilities.
7
+ * Language: JavaScript
8
+ * Created-at: 2025-05-14T03:45:32.087Z
9
+ * Authors: Gemini 2.5 Flash Thinking (v1.0.0)
10
+ */
11
+
12
+
13
+ // Regex to parse context/deletion/addition lines with line numbers
14
+ // Captures: 1: diff prefix (' ', '-', or '+'), 2: line number, 3: content after 'NNN: '
15
+ const CONTENT_LINE_REGEX = /^([ +-])\s*[-+]*(\d+):\s?(.*)$/;
16
+ // Regex to parse hunk headers
17
+ // Captures: 1: old_start, 2: old_count (optional), 3: new_start, 4: new_count (optional)
18
+ const HUNK_HEADER_REGEX = /^@@ -(\d+)(?:,(\d+))? \+(\d+)(?:,(\d+))? @@/;
19
+
20
+ module.exports = {
21
+ CONTENT_LINE_REGEX,
22
+ HUNK_HEADER_REGEX,
23
+ };
@@ -0,0 +1,281 @@
1
+ /**
2
+ * Component: PatchUtils Detect and Fix Overlapping Hunks
3
+ * Block-UUID: 0e1b2c3d-4e5f-6a7b-8c9d-0e1f2a3b4c5d
4
+ * Parent-UUID: 2308ed72-91ff-48ba-bc80-310c23c01ff1
5
+ * Version: 1.0.0
6
+ * Description: Detects and optionally fixes overlapping hunks in a patch file by merging them.
7
+ * Language: JavaScript
8
+ * Created-at: 2025-05-14T03:48:50.244Z
9
+ * Authors: Gemini 2.5 Flash Thinking (v1.0.0)
10
+ */
11
+
12
+
13
+ const { CONTENT_LINE_REGEX, HUNK_HEADER_REGEX } = require('./constants');
14
+
15
+
16
+ /**
17
+ * Represents an error found during patch verification.
18
+ * @typedef {object} VerificationError
19
+ * @property {number} [originalLineNumber] - The line number reported in the patch file (for line errors).
20
+ * @property {number} [hunkIndex] - The index (0-based) of the hunk where the error occurred (for header errors).
21
+ * @property {string} lineContent - The full content of the problematic line or header in the patch.
22
+ * @property {string} message - Description of the error.
23
+ * @property {number} [searchStart] - The starting line number (1-based) of the search window in the source.
24
+ * @property {number} [searchEnd] - The ending line number (1-based) of the search window in the source.
25
+ * @property {string} [errorType] - Type of error (e.g., 'redundant-change', 'line-mismatch', 'header-mismatch').
26
+ * @property {string[]} [suggestedFix] - Suggested lines to replace the problematic section.
27
+ */
28
+
29
+
30
+ /**
31
+ * Detects and fixes overlapping hunks in a patch file. Overlapping hunks occur when
32
+ * multiple hunks try to modify the same section of code, which can cause patch application
33
+ * failures. This function identifies such overlaps and merges the affected hunks.
34
+ *
35
+ * @param {string} patchText - The full text of the patch (including metadata and markers).
36
+ * @param {boolean} [autoFix=false] - Whether to automatically fix detected overlapping hunks.
37
+ * @returns {{
38
+ * correctedPatchText: string,
39
+ * correctionsMade: boolean,
40
+ * errors: VerificationError[]
41
+ * }} - An object containing the potentially corrected patch text, a flag indicating if corrections were made, and an array of errors encountered.
42
+ * @throws {Error} If patchText is not a string.
43
+ */
44
+ function detectAndFixOverlappingHunks(patchText, autoFix = false) {
45
+ if (typeof patchText !== 'string') {
46
+ throw new Error("Invalid input: patchText must be a string.");
47
+ }
48
+
49
+ const lines = patchText.split('\n');
50
+ const errors = [];
51
+ let correctionsMade = false;
52
+
53
+ // Extract all hunks from the patch
54
+ const hunks = [];
55
+ let currentHunk = null;
56
+ let inMetadata = true;
57
+ let metadataLines = [];
58
+
59
+ for (let i = 0; i < lines.length; i++) {
60
+ const line = lines[i];
61
+
62
+ // Collect metadata until we hit the patch start marker
63
+ if (inMetadata) {
64
+ metadataLines.push(line);
65
+ if (line === '# --- PATCH START MARKER ---') {
66
+ inMetadata = false;
67
+ }
68
+ continue;
69
+ }
70
+
71
+ // End of patch
72
+ if (line === '# --- PATCH END MARKER ---') {
73
+ if (currentHunk) {
74
+ hunks.push(currentHunk);
75
+ currentHunk = null;
76
+ }
77
+ break;
78
+ }
79
+
80
+ // Start of a new hunk
81
+ if (line.startsWith('@@ ')) {
82
+ if (currentHunk) {
83
+ hunks.push(currentHunk);
84
+ }
85
+
86
+ const headerMatch = line.match(HUNK_HEADER_REGEX);
87
+ if (!headerMatch) {
88
+ errors.push({
89
+ lineContent: line,
90
+ message: "Failed to parse hunk header.",
91
+ errorType: 'header-parse-error'
92
+ });
93
+ continue;
94
+ }
95
+
96
+ currentHunk = {
97
+ header: line,
98
+ headerMatch,
99
+ oldStart: parseInt(headerMatch[1], 10),
100
+ oldCount: headerMatch[2] ? parseInt(headerMatch[2], 10) : 1,
101
+ newStart: parseInt(headerMatch[3], 10),
102
+ newCount: headerMatch[4] ? parseInt(headerMatch[4], 10) : 1,
103
+ lines: [line],
104
+ contentLines: []
105
+ };
106
+ } else if (currentHunk) {
107
+ // Add line to current hunk
108
+ currentHunk.lines.push(line);
109
+ currentHunk.contentLines.push(line);
110
+ }
111
+ }
112
+
113
+ // Add the last hunk if there is one
114
+ if (currentHunk) {
115
+ hunks.push(currentHunk);
116
+ }
117
+
118
+ // Check for overlapping hunks
119
+ const overlappingPairs = [];
120
+ for (let i = 0; i < hunks.length; i++) {
121
+ for (let j = i + 1; j < hunks.length; j++) {
122
+ const hunk1 = hunks[i];
123
+ const hunk2 = hunks[j];
124
+
125
+ // Check if hunks overlap in the original file
126
+ const hunk1OldEnd = hunk1.oldStart + hunk1.oldCount - 1;
127
+ const hunk2OldEnd = hunk2.oldStart + hunk2.oldCount - 1;
128
+
129
+ // Check if hunks overlap in the new file
130
+ const hunk1NewEnd = hunk1.newStart + hunk1.newCount - 1;
131
+ const hunk2NewEnd = hunk2.newStart + hunk2.newCount - 1;
132
+
133
+
134
+ if ((hunk1.oldStart <= hunk2OldEnd && hunk2.oldStart <= hunk1OldEnd) ||
135
+ (hunk1.newStart <= hunk2NewEnd && hunk2.newStart <= hunk1NewEnd)) {
136
+
137
+ overlappingPairs.push({ hunk1Index: i, hunk2Index: j });
138
+
139
+ errors.push({
140
+ lineContent: hunk1.header,
141
+ message: `Overlapping hunks detected: Hunk starting at old line ${hunk1.oldStart} overlaps with hunk starting at old line ${hunk2.oldStart}.`,
142
+ errorType: 'overlapping-hunks',
143
+ hunkIndex: i
144
+ });
145
+ }
146
+ }
147
+ }
148
+
149
+ // If no overlaps or autoFix is disabled, return original
150
+ if (overlappingPairs.length === 0 || !autoFix) {
151
+ return {
152
+ correctedPatchText: patchText,
153
+ correctionsMade: false,
154
+ errors
155
+ };
156
+ }
157
+
158
+ // Fix overlapping hunks by merging them
159
+ const mergedHunks = [...hunks];
160
+ const removedIndices = new Set();
161
+
162
+ for (const { hunk1Index, hunk2Index } of overlappingPairs) {
163
+ // Skip if either hunk has already been merged
164
+ if (removedIndices.has(hunk1Index) || removedIndices.has(hunk2Index)) {
165
+ continue;
166
+ }
167
+
168
+ const hunk1 = mergedHunks[hunk1Index];
169
+ const hunk2 = mergedHunks[hunk2Index];
170
+
171
+ // Determine the range of the merged hunk
172
+ const oldStart = Math.min(hunk1.oldStart, hunk2.oldStart);
173
+ const oldEnd = Math.max(hunk1.oldStart + hunk1.oldCount - 1, hunk2.oldStart + hunk2.oldCount - 1);
174
+ const oldCount = oldEnd - oldStart + 1;
175
+
176
+ const newStart = Math.min(hunk1.newStart, hunk2.newStart);
177
+ const newEnd = Math.max(hunk1.newStart + hunk1.newCount - 1, hunk2.newStart + hunk2.newCount - 1);
178
+ const newCount = newEnd - newStart + 1;
179
+
180
+ // Create a new merged hunk header
181
+ const oldPart = oldCount === 1 ? `${oldStart}` : `${oldStart},${oldCount}`;
182
+ const newPart = newCount === 1 ? `${newStart}` : `${newStart},${newCount}`;
183
+ const mergedHeader = `@@ -${oldPart} +${newPart} @@`;
184
+
185
+ // Merge the content lines (this is a simplified approach)
186
+ // A more sophisticated implementation would need to handle line conflicts
187
+ const mergedContentLines = [];
188
+ const processedLines = new Set();
189
+
190
+ // Process hunk1 content
191
+ for (const line of hunk1.contentLines) {
192
+ const match = line.match(CONTENT_LINE_REGEX);
193
+ if (match) {
194
+ const lineNum = parseInt(match[2], 10);
195
+ const key = `${match[1]}-${lineNum}-${match[3]}`;
196
+ if (!processedLines.has(key)) {
197
+ mergedContentLines.push(line);
198
+ processedLines.add(key);
199
+ }
200
+ } else {
201
+ // Handle lines that don't match the content regex (e.g., comments within hunk)
202
+ if (!processedLines.has(line)) {
203
+ mergedContentLines.push(line);
204
+ processedLines.add(line);
205
+ }
206
+ }
207
+ }
208
+
209
+ // Process hunk2 content
210
+ for (const line of hunk2.contentLines) {
211
+ const match = line.match(CONTENT_LINE_REGEX);
212
+ if (match) {
213
+ const lineNum = parseInt(match[2], 10);
214
+ const key = `${match[1]}-${lineNum}-${match[3]}`;
215
+ if (!processedLines.has(key)) {
216
+ mergedContentLines.push(line);
217
+ processedLines.add(key);
218
+ }
219
+ } else {
220
+ // Handle lines that don't match the content regex (e.g., comments within hunk)
221
+ if (!processedLines.has(line)) {
222
+ mergedContentLines.push(line);
223
+ processedLines.add(line);
224
+ }
225
+ }
226
+ }
227
+
228
+ // Sort merged content lines by line number for better readability (optional but good practice)
229
+ mergedContentLines.sort((a, b) => {
230
+ const matchA = a.match(CONTENT_LINE_REGEX);
231
+ const matchB = b.match(CONTENT_LINE_REGEX);
232
+
233
+ if (!matchA || !matchB) {
234
+ // If either doesn't match, keep original order relative to each other
235
+ return 0;
236
+ }
237
+
238
+ const lineNumA = parseInt(matchA[2], 10);
239
+ const lineNumB = parseInt(matchB[2], 10);
240
+
241
+ return lineNumA - lineNumB;
242
+ });
243
+
244
+
245
+ // Create the merged hunk
246
+ const mergedHunk = {
247
+ header: mergedHeader,
248
+ oldStart,
249
+ oldCount,
250
+ newStart,
251
+ newCount,
252
+ lines: [mergedHeader, ...mergedContentLines],
253
+ contentLines: mergedContentLines
254
+ };
255
+
256
+ // Replace hunk1 with the merged hunk and mark hunk2 for removal
257
+ mergedHunks[hunk1Index] = mergedHunk;
258
+ removedIndices.add(hunk2Index);
259
+
260
+ correctionsMade = true;
261
+ }
262
+
263
+ // Rebuild the patch text with merged hunks
264
+ const outputLines = [...metadataLines];
265
+ for (let i = 0; i < mergedHunks.length; i++) {
266
+ if (!removedIndices.has(i)) {
267
+ outputLines.push(...mergedHunks[i].lines);
268
+ }
269
+ }
270
+ outputLines.push('# --- PATCH END MARKER ---');
271
+
272
+ return {
273
+ correctedPatchText: outputLines.join('\n'),
274
+ correctionsMade,
275
+ errors
276
+ };
277
+ }
278
+
279
+ module.exports = {
280
+ detectAndFixOverlappingHunks,
281
+ };