@aexol/spectral 0.9.126 → 0.9.128
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.
- package/dist/agent/index.d.ts.map +1 -1
- package/dist/agent/index.js +5 -0
- package/dist/extensions/seo/analyzers/content.d.ts +29 -6
- package/dist/extensions/seo/analyzers/content.d.ts.map +1 -1
- package/dist/extensions/seo/analyzers/content.js +243 -8
- package/dist/extensions/seo/analyzers/crawler.d.ts.map +1 -1
- package/dist/extensions/seo/analyzers/crawler.js +133 -7
- package/dist/extensions/seo/analyzers/drift.d.ts.map +1 -1
- package/dist/extensions/seo/analyzers/drift.js +86 -29
- package/dist/extensions/seo/analyzers/images.d.ts +10 -4
- package/dist/extensions/seo/analyzers/images.d.ts.map +1 -1
- package/dist/extensions/seo/analyzers/images.js +153 -5
- package/dist/extensions/seo/analyzers/performance.d.ts +3 -3
- package/dist/extensions/seo/analyzers/performance.d.ts.map +1 -1
- package/dist/extensions/seo/analyzers/performance.js +77 -6
- package/dist/extensions/seo/analyzers/readability.d.ts.map +1 -1
- package/dist/extensions/seo/analyzers/readability.js +17 -9
- package/dist/extensions/seo/analyzers/schema.d.ts.map +1 -1
- package/dist/extensions/seo/analyzers/schema.js +99 -10
- package/dist/extensions/seo/analyzers/sitemap.d.ts.map +1 -1
- package/dist/extensions/seo/analyzers/sitemap.js +57 -16
- package/dist/extensions/seo/analyzers/technical.d.ts +0 -7
- package/dist/extensions/seo/analyzers/technical.d.ts.map +1 -1
- package/dist/extensions/seo/analyzers/technical.js +408 -37
- package/dist/extensions/seo/config.d.ts +2 -0
- package/dist/extensions/seo/config.d.ts.map +1 -1
- package/dist/extensions/seo/config.js +2 -0
- package/dist/extensions/seo/tools/batch.js +1 -1
- package/dist/extensions/seo/tools/content.d.ts.map +1 -1
- package/dist/extensions/seo/tools/content.js +5 -3
- package/dist/extensions/seo/tools/full-audit.d.ts +1 -1
- package/dist/extensions/seo/tools/full-audit.d.ts.map +1 -1
- package/dist/extensions/seo/tools/full-audit.js +391 -66
- package/dist/extensions/seo/tools/helpers.d.ts +15 -4
- package/dist/extensions/seo/tools/helpers.d.ts.map +1 -1
- package/dist/extensions/seo/tools/helpers.js +57 -6
- package/dist/extensions/seo/tools/performance.d.ts.map +1 -1
- package/dist/extensions/seo/tools/performance.js +17 -11
- package/dist/extensions/seo/tools/report.d.ts.map +1 -1
- package/dist/extensions/seo/tools/report.js +513 -47
- package/dist/extensions/seo/types.d.ts +295 -3
- package/dist/extensions/seo/types.d.ts.map +1 -1
- package/dist/extensions/seo/utils/fetcher.d.ts +4 -0
- package/dist/extensions/seo/utils/fetcher.d.ts.map +1 -1
- package/dist/extensions/seo/utils/fetcher.js +77 -11
- package/dist/extensions/seo/utils/parser.d.ts +3 -1
- package/dist/extensions/seo/utils/parser.d.ts.map +1 -1
- package/dist/extensions/seo/utils/parser.js +105 -25
- package/dist/extensions/seo/utils/renderer.d.ts +6 -1
- package/dist/extensions/seo/utils/renderer.d.ts.map +1 -1
- package/dist/extensions/seo/utils/renderer.js +207 -1
- package/dist/extensions/seo/utils/url-safety.d.ts.map +1 -1
- package/dist/extensions/seo/utils/url-safety.js +19 -3
- package/dist/extensions/web/tools/fetch.js +2 -2
- package/dist/memory/tools/compact-context.d.ts.map +1 -1
- package/dist/memory/tools/compact-context.js +10 -3
- package/dist/sdk/agent-core/agent-loop.d.ts.map +1 -1
- package/dist/sdk/agent-core/agent-loop.js +87 -4
- package/dist/sdk/agent-core/types.d.ts +1 -0
- package/dist/sdk/agent-core/types.d.ts.map +1 -1
- package/dist/sdk/ai/utils/validation.d.ts +0 -7
- package/dist/sdk/ai/utils/validation.d.ts.map +1 -1
- package/dist/sdk/ai/utils/validation.js +21 -1
- package/dist/sdk/coding-agent/core/extensions/runner.d.ts.map +1 -1
- package/dist/sdk/coding-agent/core/extensions/runner.js +15 -1
- package/dist/sdk/coding-agent/core/tools/apply-patch.d.ts +7 -2
- package/dist/sdk/coding-agent/core/tools/apply-patch.d.ts.map +1 -1
- package/dist/sdk/coding-agent/core/tools/apply-patch.js +219 -41
- package/dist/sdk/coding-agent/core/tools/atomic-write.d.ts +30 -0
- package/dist/sdk/coding-agent/core/tools/atomic-write.d.ts.map +1 -0
- package/dist/sdk/coding-agent/core/tools/atomic-write.js +42 -0
- package/dist/sdk/coding-agent/core/tools/edit-diff.d.ts +10 -4
- package/dist/sdk/coding-agent/core/tools/edit-diff.d.ts.map +1 -1
- package/dist/sdk/coding-agent/core/tools/edit-diff.js +44 -24
- package/dist/sdk/coding-agent/core/tools/edit.d.ts +6 -0
- package/dist/sdk/coding-agent/core/tools/edit.d.ts.map +1 -1
- package/dist/sdk/coding-agent/core/tools/edit.js +31 -5
- package/dist/sdk/coding-agent/core/tools/file-mutation-queue.d.ts +9 -3
- package/dist/sdk/coding-agent/core/tools/file-mutation-queue.d.ts.map +1 -1
- package/dist/sdk/coding-agent/core/tools/file-mutation-queue.js +42 -3
- package/dist/sdk/coding-agent/core/tools/read.d.ts.map +1 -1
- package/dist/sdk/coding-agent/core/tools/read.js +79 -47
- package/dist/sdk/coding-agent/core/tools/truncate.d.ts +4 -2
- package/dist/sdk/coding-agent/core/tools/truncate.d.ts.map +1 -1
- package/dist/sdk/coding-agent/core/tools/truncate.js +43 -23
- package/dist/sdk/coding-agent/core/tools/write.d.ts +5 -8
- package/dist/sdk/coding-agent/core/tools/write.d.ts.map +1 -1
- package/dist/sdk/coding-agent/core/tools/write.js +29 -11
- package/package.json +1 -1
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
import { constants } from "fs";
|
|
2
|
-
import { access as fsAccess, mkdir as fsMkdir, readFile as fsReadFile, rm as fsRm, writeFile as fsWriteFile } from "fs/promises";
|
|
2
|
+
import { access as fsAccess, mkdir as fsMkdir, readFile as fsReadFile, rm as fsRm, stat as fsStat, writeFile as fsWriteFile, } from "fs/promises";
|
|
3
3
|
import { dirname } from "path";
|
|
4
4
|
import { Type } from "typebox";
|
|
5
5
|
import { detectLineEnding, generateDiffString, generateUnifiedPatch, normalizeForFuzzyMatch, normalizeToLF, restoreLineEndings, stripBom, } from "./edit-diff.js";
|
|
6
6
|
import { withFileMutationQueue } from "./file-mutation-queue.js";
|
|
7
7
|
import { resolveToCwd } from "./path-utils.js";
|
|
8
8
|
import { wrapToolDefinition } from "./tool-definition-wrapper.js";
|
|
9
|
+
import { atomicWriteFile, defaultAtomicWriteOperations } from "./atomic-write.js";
|
|
9
10
|
const applyPatchSchema = Type.Object({
|
|
10
11
|
patchText: Type.String({ description: "The full patch text that describes all changes to be made." }),
|
|
11
12
|
}, { additionalProperties: false });
|
|
@@ -15,11 +16,9 @@ const defaultApplyPatchOperations = {
|
|
|
15
16
|
removeFile: (filePath) => fsRm(filePath, { force: false }),
|
|
16
17
|
mkdir: (filePath) => fsMkdir(filePath, { recursive: true }).then(() => { }),
|
|
17
18
|
access: (filePath) => fsAccess(filePath, constants.R_OK | constants.W_OK),
|
|
19
|
+
stat: (filePath) => fsStat(filePath).then((s) => ({ isDirectory: s.isDirectory() })),
|
|
18
20
|
};
|
|
19
|
-
|
|
20
|
-
const heredocMatch = input.match(/^(?:cat\s+)?<<['"]?(\w+)['"]?\s*\n([\s\S]*?)\n\1\s*$/);
|
|
21
|
-
return heredocMatch ? heredocMatch[2] : input;
|
|
22
|
-
}
|
|
21
|
+
export { defaultApplyPatchOperations };
|
|
23
22
|
function parsePatchHeader(lines, startIndex) {
|
|
24
23
|
const line = lines[startIndex];
|
|
25
24
|
if (line.startsWith("*** Add File:")) {
|
|
@@ -46,8 +45,11 @@ function parseAddFileContent(lines, startIndex) {
|
|
|
46
45
|
let content = "";
|
|
47
46
|
let index = startIndex;
|
|
48
47
|
while (index < lines.length && !lines[index].startsWith("***")) {
|
|
49
|
-
|
|
50
|
-
|
|
48
|
+
const line = lines[index];
|
|
49
|
+
if (!line.startsWith("+")) {
|
|
50
|
+
throw new Error(`Add File content line missing '+' prefix: ${line}`);
|
|
51
|
+
}
|
|
52
|
+
content += `${line.slice(1)}\n`;
|
|
51
53
|
index++;
|
|
52
54
|
}
|
|
53
55
|
return { content: content.endsWith("\n") ? content.slice(0, -1) : content, nextIndex: index };
|
|
@@ -107,7 +109,7 @@ function parseUpdateHeader(line) {
|
|
|
107
109
|
}
|
|
108
110
|
function parsePatch(patchText) {
|
|
109
111
|
const normalizedPatchText = patchText.replace(/\r\n/g, "\n").replace(/\r/g, "\n");
|
|
110
|
-
const lines =
|
|
112
|
+
const lines = normalizedPatchText.trim().split("\n");
|
|
111
113
|
const beginIndex = lines.findIndex((line) => line.trim() === "*** Begin Patch");
|
|
112
114
|
const endIndex = lines.findIndex((line) => line.trim() === "*** End Patch");
|
|
113
115
|
if (beginIndex === -1 || endIndex === -1 || beginIndex >= endIndex) {
|
|
@@ -161,7 +163,38 @@ function seekSequence(lines, pattern, startIndex, eof = false) {
|
|
|
161
163
|
(a, b) => a.trimEnd() === b.trimEnd(),
|
|
162
164
|
(a, b) => a.trim() === b.trim(),
|
|
163
165
|
compareNormalized,
|
|
164
|
-
].reduce((found, compare) => found !== -1 ? found : tryMatch(lines, pattern, startIndex, compare, eof), -1);
|
|
166
|
+
].reduce((found, compare) => (found !== -1 ? found : tryMatch(lines, pattern, startIndex, compare, eof)), -1);
|
|
167
|
+
}
|
|
168
|
+
/**
|
|
169
|
+
* Searches for a pattern starting from `startIndex` (forward), and if not found,
|
|
170
|
+
* falls back to a whole-file search preferring the match nearest to `startIndex`.
|
|
171
|
+
* This allows context that appears before the cursor to still resolve (A7).
|
|
172
|
+
*/
|
|
173
|
+
function seekSequenceBidirectional(lines, pattern, startIndex, eof = false) {
|
|
174
|
+
const forward = seekSequence(lines, pattern, startIndex, eof);
|
|
175
|
+
if (forward !== -1)
|
|
176
|
+
return forward;
|
|
177
|
+
if (pattern.length === 0)
|
|
178
|
+
return -1;
|
|
179
|
+
const comparators = [
|
|
180
|
+
(a, b) => a === b,
|
|
181
|
+
(a, b) => a.trimEnd() === b.trimEnd(),
|
|
182
|
+
(a, b) => a.trim() === b.trim(),
|
|
183
|
+
compareNormalized,
|
|
184
|
+
];
|
|
185
|
+
for (const compare of comparators) {
|
|
186
|
+
let bestIndex = -1;
|
|
187
|
+
for (let index = 0; index <= lines.length - pattern.length; index++) {
|
|
188
|
+
if (pattern.every((line, offset) => compare(lines[index + offset], line))) {
|
|
189
|
+
if (bestIndex === -1 || Math.abs(index - startIndex) < Math.abs(bestIndex - startIndex)) {
|
|
190
|
+
bestIndex = index;
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
if (bestIndex !== -1)
|
|
195
|
+
return bestIndex;
|
|
196
|
+
}
|
|
197
|
+
return -1;
|
|
165
198
|
}
|
|
166
199
|
function seekSequenceNearHint(lines, pattern, startIndex, hintLine, eof = false) {
|
|
167
200
|
if (!hintLine || hintLine < 1 || pattern.length === 0)
|
|
@@ -216,7 +249,7 @@ function computeReplacements(originalLines, filePath, chunks) {
|
|
|
216
249
|
let lineIndex = 0;
|
|
217
250
|
for (const chunk of chunks) {
|
|
218
251
|
if (chunk.changeContext) {
|
|
219
|
-
const contextIndex =
|
|
252
|
+
const contextIndex = seekSequenceBidirectional(originalLines, [chunk.changeContext], lineIndex);
|
|
220
253
|
if (contextIndex === -1)
|
|
221
254
|
throw new Error(`Failed to find context '${chunk.changeContext}' in ${filePath}.`);
|
|
222
255
|
lineIndex = contextIndex + 1;
|
|
@@ -263,8 +296,112 @@ function deriveUpdatedContent(filePath, rawContent, chunks) {
|
|
|
263
296
|
newLines.push("");
|
|
264
297
|
return { bom, oldContent: normalized, newContent: newLines.join("\n"), lineEnding };
|
|
265
298
|
}
|
|
299
|
+
/**
|
|
300
|
+
* Groups update hunks by target path (A2). When multiple hunks target the
|
|
301
|
+
* same file, their chunks are applied sequentially to a single evolving buffer
|
|
302
|
+
* so earlier hunks' changes are not lost.
|
|
303
|
+
*/
|
|
304
|
+
function groupUpdateHunksByPath(hunks) {
|
|
305
|
+
const groups = new Map();
|
|
306
|
+
for (const hunk of hunks) {
|
|
307
|
+
if (hunk.type !== "update")
|
|
308
|
+
continue;
|
|
309
|
+
let group = groups.get(hunk.path);
|
|
310
|
+
if (!group) {
|
|
311
|
+
group = { path: hunk.path, hunks: [] };
|
|
312
|
+
groups.set(hunk.path, group);
|
|
313
|
+
}
|
|
314
|
+
if (hunk.movePath && !group.movePath) {
|
|
315
|
+
group.movePath = hunk.movePath;
|
|
316
|
+
}
|
|
317
|
+
group.hunks.push(hunk.chunks);
|
|
318
|
+
}
|
|
319
|
+
return groups;
|
|
320
|
+
}
|
|
321
|
+
/**
|
|
322
|
+
* Builds backup entries for all changes BEFORE any mutation. On rollback,
|
|
323
|
+
* entries are restored in reverse order.
|
|
324
|
+
*/
|
|
325
|
+
function buildBackups(changes) {
|
|
326
|
+
const backups = [];
|
|
327
|
+
for (const change of changes) {
|
|
328
|
+
switch (change.type) {
|
|
329
|
+
case "add":
|
|
330
|
+
backups.push({ absolutePath: change.absolutePath, existed: false });
|
|
331
|
+
break;
|
|
332
|
+
case "update":
|
|
333
|
+
if (change.movePath && change.absoluteMovePath) {
|
|
334
|
+
backups.push({ absolutePath: change.absolutePath, existed: true, content: change.rawOriginalContent ?? undefined });
|
|
335
|
+
backups.push({ absolutePath: change.absoluteMovePath, existed: false });
|
|
336
|
+
}
|
|
337
|
+
else {
|
|
338
|
+
backups.push({ absolutePath: change.absolutePath, existed: true, content: change.rawOriginalContent ?? undefined });
|
|
339
|
+
}
|
|
340
|
+
break;
|
|
341
|
+
case "delete":
|
|
342
|
+
backups.push({ absolutePath: change.absolutePath, existed: true, content: change.rawOriginalContent ?? undefined });
|
|
343
|
+
break;
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
return backups;
|
|
347
|
+
}
|
|
348
|
+
async function rollbackChanges(backups, ops) {
|
|
349
|
+
for (let i = backups.length - 1; i >= 0; i--) {
|
|
350
|
+
const backup = backups[i];
|
|
351
|
+
try {
|
|
352
|
+
if (backup.existed && backup.content !== undefined) {
|
|
353
|
+
await ops.writeFile(backup.absolutePath, backup.content);
|
|
354
|
+
}
|
|
355
|
+
else if (!backup.existed) {
|
|
356
|
+
await ops.removeFile(backup.absolutePath);
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
catch {
|
|
360
|
+
// Best-effort rollback; individual failures are tolerated
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
/**
|
|
365
|
+
* Applies all changes transactionally (A1, A4). If any change fails, all
|
|
366
|
+
* prior mutations are rolled back so the repository returns to its original
|
|
367
|
+
* state.
|
|
368
|
+
*/
|
|
369
|
+
async function applyChangesTransactionally(changes, ops, atomicOps, signal) {
|
|
370
|
+
const backups = buildBackups(changes);
|
|
371
|
+
try {
|
|
372
|
+
for (const change of changes) {
|
|
373
|
+
if (signal?.aborted)
|
|
374
|
+
throw new Error("Operation aborted");
|
|
375
|
+
if (change.type === "delete") {
|
|
376
|
+
try {
|
|
377
|
+
await ops.removeFile(change.absolutePath);
|
|
378
|
+
}
|
|
379
|
+
catch (error) {
|
|
380
|
+
// A8: handle ENOENT gracefully (file already gone — no-op)
|
|
381
|
+
if (error instanceof Error && "code" in error && error.code === "ENOENT") {
|
|
382
|
+
continue;
|
|
383
|
+
}
|
|
384
|
+
throw error;
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
else {
|
|
388
|
+
const targetPath = change.absoluteMovePath ?? change.absolutePath;
|
|
389
|
+
await ops.mkdir(dirname(targetPath));
|
|
390
|
+
const writeFn = ops.atomicWrite ?? ((p, c) => atomicWriteFile(p, c, "utf-8", atomicOps));
|
|
391
|
+
await writeFn(targetPath, change.bom + restoreLineEndings(change.newContent, change.lineEnding));
|
|
392
|
+
if (change.movePath)
|
|
393
|
+
await ops.removeFile(change.absolutePath);
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
catch (error) {
|
|
398
|
+
await rollbackChanges(backups, ops);
|
|
399
|
+
throw error;
|
|
400
|
+
}
|
|
401
|
+
}
|
|
266
402
|
export function createApplyPatchToolDefinition(cwd, options) {
|
|
267
403
|
const ops = options?.operations ?? defaultApplyPatchOperations;
|
|
404
|
+
const atomicOps = defaultAtomicWriteOperations;
|
|
268
405
|
return {
|
|
269
406
|
name: "apply_patch",
|
|
270
407
|
label: "Apply Patch",
|
|
@@ -284,6 +421,10 @@ export function createApplyPatchToolDefinition(cwd, options) {
|
|
|
284
421
|
const hunks = parsePatch(input.patchText);
|
|
285
422
|
if (hunks.length === 0)
|
|
286
423
|
throw new Error("apply_patch verification failed: no hunks found.");
|
|
424
|
+
// A2: group update hunks by path to avoid data loss from duplicate-file patches
|
|
425
|
+
const updateGroups = groupUpdateHunksByPath(hunks);
|
|
426
|
+
const processedUpdatePaths = new Set();
|
|
427
|
+
// Parse / derive phase — no mutations; failures here never partially mutate the repo
|
|
287
428
|
const changes = [];
|
|
288
429
|
for (const hunk of hunks) {
|
|
289
430
|
if (signal?.aborted)
|
|
@@ -302,62 +443,99 @@ export function createApplyPatchToolDefinition(cwd, options) {
|
|
|
302
443
|
type: "add",
|
|
303
444
|
bom,
|
|
304
445
|
lineEnding: "\n",
|
|
446
|
+
rawOriginalContent: null,
|
|
305
447
|
});
|
|
306
448
|
continue;
|
|
307
449
|
}
|
|
308
|
-
await ops.access(absolutePath).catch((error) => {
|
|
309
|
-
const errorMessage = error instanceof Error && "code" in error ? `Error code: ${error.code}` : String(error);
|
|
310
|
-
throw new Error(`Could not patch file: ${hunk.path}. ${errorMessage}.`);
|
|
311
|
-
});
|
|
312
|
-
const rawContent = (await ops.readFile(absolutePath)).toString("utf-8");
|
|
313
450
|
if (hunk.type === "delete") {
|
|
314
|
-
|
|
451
|
+
// A8: directory check
|
|
452
|
+
if (ops.stat) {
|
|
453
|
+
const statResult = await ops.stat(absolutePath).catch(() => null);
|
|
454
|
+
if (statResult?.isDirectory) {
|
|
455
|
+
throw new Error(`Cannot delete directory: ${hunk.path}`);
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
// A8: no pre-access TOCTOU check; read for backup, handle missing gracefully
|
|
459
|
+
let rawContent = null;
|
|
460
|
+
try {
|
|
461
|
+
rawContent = (await ops.readFile(absolutePath)).toString("utf-8");
|
|
462
|
+
}
|
|
463
|
+
catch {
|
|
464
|
+
// File doesn't exist — delete is a no-op
|
|
465
|
+
}
|
|
466
|
+
const { bom, text } = rawContent !== null ? stripBom(rawContent) : { bom: "", text: "" };
|
|
315
467
|
changes.push({
|
|
316
468
|
path: hunk.path,
|
|
317
469
|
absolutePath,
|
|
318
|
-
oldContent: normalizeToLF(text),
|
|
470
|
+
oldContent: rawContent !== null ? normalizeToLF(text) : "",
|
|
319
471
|
newContent: "",
|
|
320
472
|
type: "delete",
|
|
321
473
|
bom,
|
|
322
|
-
lineEnding: detectLineEnding(text),
|
|
474
|
+
lineEnding: rawContent !== null ? detectLineEnding(text) : "\n",
|
|
475
|
+
rawOriginalContent: rawContent,
|
|
323
476
|
});
|
|
324
477
|
continue;
|
|
325
478
|
}
|
|
326
|
-
|
|
327
|
-
|
|
479
|
+
// hunk.type === "update"
|
|
480
|
+
// A2: process grouped updates only once per path
|
|
481
|
+
if (processedUpdatePaths.has(hunk.path))
|
|
482
|
+
continue;
|
|
483
|
+
processedUpdatePaths.add(hunk.path);
|
|
484
|
+
const group = updateGroups.get(hunk.path);
|
|
485
|
+
if (!group)
|
|
486
|
+
continue;
|
|
487
|
+
let rawContent;
|
|
488
|
+
try {
|
|
489
|
+
rawContent = (await ops.readFile(absolutePath)).toString("utf-8");
|
|
490
|
+
}
|
|
491
|
+
catch (error) {
|
|
492
|
+
const errorMessage = error instanceof Error && "code" in error ? `Error code: ${error.code}` : String(error);
|
|
493
|
+
throw new Error(`Could not patch file: ${hunk.path}. ${errorMessage}.`);
|
|
494
|
+
}
|
|
495
|
+
// A2: apply each hunk's chunks sequentially to the evolving buffer
|
|
496
|
+
let currentContent = rawContent;
|
|
497
|
+
let firstBom = "";
|
|
498
|
+
let firstLineEnding = "\n";
|
|
499
|
+
let firstOldContent = "";
|
|
500
|
+
for (let i = 0; i < group.hunks.length; i++) {
|
|
501
|
+
const result = deriveUpdatedContent(hunk.path, currentContent, group.hunks[i]);
|
|
502
|
+
if (i === 0) {
|
|
503
|
+
firstBom = result.bom;
|
|
504
|
+
firstLineEnding = result.lineEnding;
|
|
505
|
+
firstOldContent = result.oldContent;
|
|
506
|
+
}
|
|
507
|
+
currentContent = result.newContent;
|
|
508
|
+
}
|
|
509
|
+
const absoluteMovePath = group.movePath ? resolveToCwd(group.movePath, cwd) : undefined;
|
|
328
510
|
changes.push({
|
|
329
511
|
path: hunk.path,
|
|
330
512
|
absolutePath,
|
|
331
|
-
oldContent:
|
|
332
|
-
newContent:
|
|
513
|
+
oldContent: firstOldContent,
|
|
514
|
+
newContent: currentContent,
|
|
333
515
|
type: "update",
|
|
334
|
-
movePath:
|
|
516
|
+
movePath: group.movePath,
|
|
335
517
|
absoluteMovePath,
|
|
336
|
-
bom:
|
|
337
|
-
lineEnding:
|
|
518
|
+
bom: firstBom,
|
|
519
|
+
lineEnding: firstLineEnding,
|
|
520
|
+
rawOriginalContent: rawContent,
|
|
338
521
|
});
|
|
339
522
|
}
|
|
523
|
+
// Validate before any mutations
|
|
524
|
+
for (const change of changes) {
|
|
525
|
+
if (change.oldContent === change.newContent && !change.movePath && change.type !== "delete") {
|
|
526
|
+
throw new Error(`No changes made to ${change.path}. The patch produced identical content.`);
|
|
527
|
+
}
|
|
528
|
+
}
|
|
529
|
+
// A3: pass mutated files as array for per-file serialization
|
|
340
530
|
const mutatedFiles = changes.map((change) => change.absoluteMovePath ?? change.absolutePath);
|
|
341
|
-
return withFileMutationQueue(mutatedFiles
|
|
531
|
+
return withFileMutationQueue(mutatedFiles, async () => {
|
|
532
|
+
// A1: transactional apply with rollback
|
|
533
|
+
await applyChangesTransactionally(changes, ops, atomicOps, signal);
|
|
534
|
+
// Build diff output (only after successful apply)
|
|
342
535
|
let combinedDiff = "";
|
|
343
536
|
let combinedPatch = "";
|
|
344
537
|
let firstChangedLine;
|
|
345
538
|
for (const change of changes) {
|
|
346
|
-
if (signal?.aborted)
|
|
347
|
-
throw new Error("Operation aborted");
|
|
348
|
-
if (change.oldContent === change.newContent && !change.movePath) {
|
|
349
|
-
throw new Error(`No changes made to ${change.path}. The patch produced identical content.`);
|
|
350
|
-
}
|
|
351
|
-
if (change.type === "delete") {
|
|
352
|
-
await ops.removeFile(change.absolutePath);
|
|
353
|
-
}
|
|
354
|
-
else {
|
|
355
|
-
const targetPath = change.absoluteMovePath ?? change.absolutePath;
|
|
356
|
-
await ops.mkdir(dirname(targetPath));
|
|
357
|
-
await ops.writeFile(targetPath, change.bom + restoreLineEndings(change.newContent, change.lineEnding));
|
|
358
|
-
if (change.movePath)
|
|
359
|
-
await ops.removeFile(change.absolutePath);
|
|
360
|
-
}
|
|
361
539
|
const displayPath = change.movePath ? `${change.path} => ${change.movePath}` : change.path;
|
|
362
540
|
const diffResult = generateDiffString(change.oldContent, change.newContent);
|
|
363
541
|
combinedDiff += `${displayPath}\n${diffResult.diff}\n`;
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
export type BufferEncoding = "utf-8" | "utf8" | "ascii" | "latin1" | "binary" | "ucs2" | "ucs-2" | "utf16le" | "utf-16le";
|
|
2
|
+
/**
|
|
3
|
+
* Operations surface for atomic writes. Mirrors the subset of
|
|
4
|
+
* {@link WriteOperations} needed so that tests can stub the filesystem.
|
|
5
|
+
*/
|
|
6
|
+
export interface AtomicWriteOperations {
|
|
7
|
+
writeFile: (absolutePath: string, data: string) => Promise<void>;
|
|
8
|
+
rename: (oldPath: string, newPath: string) => Promise<void>;
|
|
9
|
+
unlink: (absolutePath: string) => Promise<void>;
|
|
10
|
+
}
|
|
11
|
+
declare const defaultAtomicWriteOperations: AtomicWriteOperations;
|
|
12
|
+
/**
|
|
13
|
+
* Write `content` to `absolutePath` atomically by first writing to a sibling
|
|
14
|
+
* temp file in the same directory and then renaming it into place. `rename`
|
|
15
|
+
* is atomic when source and destination live on the same filesystem, which is
|
|
16
|
+
* guaranteed because the temp file is a sibling of the destination.
|
|
17
|
+
*
|
|
18
|
+
* If the temp write or rename fails, the temp file is best-effort removed so
|
|
19
|
+
* no stale partial temp file is left behind. The original file (if any) is
|
|
20
|
+
* never touched until the rename succeeds, so a failed write leaves the
|
|
21
|
+
* existing file intact.
|
|
22
|
+
*
|
|
23
|
+
* @param absolutePath Final destination file path.
|
|
24
|
+
* @param content Text content to write.
|
|
25
|
+
* @param encoding Encoding for the temp write (defaults to "utf-8").
|
|
26
|
+
* @param operations Optional stub for tests; defaults to real `fs/promises`.
|
|
27
|
+
*/
|
|
28
|
+
export declare function atomicWriteFile(absolutePath: string, content: string, encoding?: BufferEncoding, operations?: AtomicWriteOperations): Promise<void>;
|
|
29
|
+
export { defaultAtomicWriteOperations };
|
|
30
|
+
//# sourceMappingURL=atomic-write.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"atomic-write.d.ts","sourceRoot":"","sources":["../../../../../src/sdk/coding-agent/core/tools/atomic-write.ts"],"names":[],"mappings":"AAQA,MAAM,MAAM,cAAc,GACvB,OAAO,GACP,MAAM,GACN,OAAO,GACP,QAAQ,GACR,QAAQ,GACR,MAAM,GACN,OAAO,GACP,SAAS,GACT,UAAU,CAAC;AAEd;;;GAGG;AACH,MAAM,WAAW,qBAAqB;IACrC,SAAS,EAAE,CAAC,YAAY,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IACjE,MAAM,EAAE,CAAC,OAAO,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IAC5D,MAAM,EAAE,CAAC,YAAY,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;CAChD;AAED,QAAA,MAAM,4BAA4B,EAAE,qBAInC,CAAC;AAEF;;;;;;;;;;;;;;;GAeG;AACH,wBAAsB,eAAe,CACpC,YAAY,EAAE,MAAM,EACpB,OAAO,EAAE,MAAM,EACf,QAAQ,GAAE,cAAwB,EAClC,UAAU,GAAE,qBAAoD,GAC9D,OAAO,CAAC,IAAI,CAAC,CAef;AAED,OAAO,EAAE,4BAA4B,EAAE,CAAC"}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { randomBytes } from "node:crypto";
|
|
2
|
+
import { rename as fsRename, unlink as fsUnlink, writeFile as fsWriteFile, } from "node:fs/promises";
|
|
3
|
+
import { dirname, join } from "node:path";
|
|
4
|
+
const defaultAtomicWriteOperations = {
|
|
5
|
+
writeFile: (path, data) => fsWriteFile(path, data, "utf-8"),
|
|
6
|
+
rename: (oldPath, newPath) => fsRename(oldPath, newPath),
|
|
7
|
+
unlink: (path) => fsUnlink(path),
|
|
8
|
+
};
|
|
9
|
+
/**
|
|
10
|
+
* Write `content` to `absolutePath` atomically by first writing to a sibling
|
|
11
|
+
* temp file in the same directory and then renaming it into place. `rename`
|
|
12
|
+
* is atomic when source and destination live on the same filesystem, which is
|
|
13
|
+
* guaranteed because the temp file is a sibling of the destination.
|
|
14
|
+
*
|
|
15
|
+
* If the temp write or rename fails, the temp file is best-effort removed so
|
|
16
|
+
* no stale partial temp file is left behind. The original file (if any) is
|
|
17
|
+
* never touched until the rename succeeds, so a failed write leaves the
|
|
18
|
+
* existing file intact.
|
|
19
|
+
*
|
|
20
|
+
* @param absolutePath Final destination file path.
|
|
21
|
+
* @param content Text content to write.
|
|
22
|
+
* @param encoding Encoding for the temp write (defaults to "utf-8").
|
|
23
|
+
* @param operations Optional stub for tests; defaults to real `fs/promises`.
|
|
24
|
+
*/
|
|
25
|
+
export async function atomicWriteFile(absolutePath, content, encoding = "utf-8", operations = defaultAtomicWriteOperations) {
|
|
26
|
+
const dir = dirname(absolutePath);
|
|
27
|
+
const base = absolutePath.split(/[\\/]/).pop() ?? absolutePath;
|
|
28
|
+
// Sibling temp file: same dir so rename is atomic on the same filesystem.
|
|
29
|
+
// The leading dot + random suffix keeps it out of the way and unique.
|
|
30
|
+
const tempPath = join(dir, `.${base}.tmp-${randomBytes(6).toString("hex")}`);
|
|
31
|
+
try {
|
|
32
|
+
await operations.writeFile(tempPath, content);
|
|
33
|
+
await operations.rename(tempPath, absolutePath);
|
|
34
|
+
}
|
|
35
|
+
catch (error) {
|
|
36
|
+
// Best-effort cleanup of the temp file on failure; never let cleanup
|
|
37
|
+
// errors mask the original write/rename failure.
|
|
38
|
+
await operations.unlink(tempPath).catch(() => { });
|
|
39
|
+
throw error;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
export { defaultAtomicWriteOperations };
|
|
@@ -6,11 +6,15 @@ export declare function detectLineEnding(content: string): "\r\n" | "\n";
|
|
|
6
6
|
export declare function normalizeToLF(text: string): string;
|
|
7
7
|
export declare function restoreLineEndings(text: string, ending: "\r\n" | "\n"): string;
|
|
8
8
|
/**
|
|
9
|
-
* Normalize text for fuzzy matching. Applies
|
|
9
|
+
* Normalize text for fuzzy matching. Applies only semantically-safe
|
|
10
|
+
* whitespace/visibility transformations:
|
|
10
11
|
* - Strip trailing whitespace from each line
|
|
11
|
-
* - Normalize
|
|
12
|
-
*
|
|
13
|
-
*
|
|
12
|
+
* - Normalize special Unicode spaces to a regular space
|
|
13
|
+
*
|
|
14
|
+
* Intentionally does NOT perform NFKC compatibility decomposition, smart-quote
|
|
15
|
+
* → straight-quote, or dash/hyphen substitutions: those transformations make
|
|
16
|
+
* semantically different code match silently. Matching across those characters
|
|
17
|
+
* requires the oldText to use the exact same characters as the file.
|
|
14
18
|
*/
|
|
15
19
|
export declare function normalizeForFuzzyMatch(text: string): string;
|
|
16
20
|
export interface FuzzyMatchResult {
|
|
@@ -32,6 +36,8 @@ export interface Edit {
|
|
|
32
36
|
export interface AppliedEditsResult {
|
|
33
37
|
baseContent: string;
|
|
34
38
|
newContent: string;
|
|
39
|
+
/** True if any edit was matched via a non-exact (line-trimmed or normalized) strategy. */
|
|
40
|
+
usedFuzzyMatch: boolean;
|
|
35
41
|
}
|
|
36
42
|
export declare function fuzzyFindText(content: string, oldText: string): FuzzyMatchResult;
|
|
37
43
|
/** Strip UTF-8 BOM if present, return both the BOM (if any) and the text without it */
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"edit-diff.d.ts","sourceRoot":"","sources":["../../../../../src/sdk/coding-agent/core/tools/edit-diff.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAOH,wBAAgB,gBAAgB,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,
|
|
1
|
+
{"version":3,"file":"edit-diff.d.ts","sourceRoot":"","sources":["../../../../../src/sdk/coding-agent/core/tools/edit-diff.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAOH,wBAAgB,gBAAgB,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAa/D;AAED,wBAAgB,aAAa,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAElD;AAED,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,IAAI,GAAG,MAAM,CAE9E;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,sBAAsB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAU3D;AAED,MAAM,WAAW,gBAAgB;IAChC,gCAAgC;IAChC,KAAK,EAAE,OAAO,CAAC;IACf,+DAA+D;IAC/D,KAAK,EAAE,MAAM,CAAC;IACd,yDAAyD;IACzD,WAAW,EAAE,MAAM,CAAC;IACpB,4DAA4D;IAC5D,cAAc,EAAE,OAAO,CAAC;IACxB,8DAA8D;IAC9D,qBAAqB,EAAE,MAAM,CAAC;CAC9B;AAED,MAAM,WAAW,IAAI;IACpB,OAAO,EAAE,MAAM,CAAC;IAChB,OAAO,EAAE,MAAM,CAAC;CAChB;AAwBD,MAAM,WAAW,kBAAkB;IAClC,WAAW,EAAE,MAAM,CAAC;IACpB,UAAU,EAAE,MAAM,CAAC;IACnB,0FAA0F;IAC1F,cAAc,EAAE,OAAO,CAAC;CACxB;AAsLD,wBAAgB,aAAa,CAAC,OAAO,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,gBAAgB,CAkBhF;AAkDD,uFAAuF;AACvF,wBAAgB,QAAQ,CAAC,OAAO,EAAE,MAAM,GAAG;IAAE,GAAG,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,CAEvE;AA0CD;;;;;;;GAOG;AACH,wBAAgB,6BAA6B,CAC5C,iBAAiB,EAAE,MAAM,EACzB,KAAK,EAAE,IAAI,EAAE,EACb,IAAI,EAAE,MAAM,GACV,kBAAkB,CA4DpB;AAED,yCAAyC;AACzC,wBAAgB,oBAAoB,CAAC,IAAI,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,EAAE,YAAY,SAAI,GAAG,MAAM,CAKnH;AAED;;;GAGG;AACH,wBAAgB,kBAAkB,CACjC,UAAU,EAAE,MAAM,EAClB,UAAU,EAAE,MAAM,EAClB,YAAY,SAAI,GACd;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,gBAAgB,EAAE,MAAM,GAAG,SAAS,CAAA;CAAE,CAuHxD;AAED,MAAM,WAAW,cAAc;IAC9B,IAAI,EAAE,MAAM,CAAC;IACb,gBAAgB,EAAE,MAAM,GAAG,SAAS,CAAC;CACrC;AAED,MAAM,WAAW,aAAa;IAC7B,KAAK,EAAE,MAAM,CAAC;CACd;AAED;;;GAGG;AACH,wBAAsB,gBAAgB,CACrC,IAAI,EAAE,MAAM,EACZ,KAAK,EAAE,IAAI,EAAE,EACb,GAAG,EAAE,MAAM,GACT,OAAO,CAAC,cAAc,GAAG,aAAa,CAAC,CAyBzC;AAED;;;GAGG;AACH,wBAAsB,eAAe,CACpC,IAAI,EAAE,MAAM,EACZ,OAAO,EAAE,MAAM,EACf,OAAO,EAAE,MAAM,EACf,GAAG,EAAE,MAAM,GACT,OAAO,CAAC,cAAc,GAAG,aAAa,CAAC,CAEzC"}
|
|
@@ -7,13 +7,21 @@ import { constants } from "fs";
|
|
|
7
7
|
import { access, readFile } from "fs/promises";
|
|
8
8
|
import { resolveToCwd } from "./path-utils.js";
|
|
9
9
|
export function detectLineEnding(content) {
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
10
|
+
let crlfCount = 0;
|
|
11
|
+
let lfCount = 0;
|
|
12
|
+
for (let i = 0; i < content.length; i++) {
|
|
13
|
+
if (content[i] !== "\n")
|
|
14
|
+
continue;
|
|
15
|
+
if (i > 0 && content[i - 1] === "\r") {
|
|
16
|
+
crlfCount++;
|
|
17
|
+
}
|
|
18
|
+
else {
|
|
19
|
+
lfCount++;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
if (crlfCount === 0 && lfCount === 0)
|
|
15
23
|
return "\n";
|
|
16
|
-
return
|
|
24
|
+
return crlfCount > lfCount ? "\r\n" : "\n";
|
|
17
25
|
}
|
|
18
26
|
export function normalizeToLF(text) {
|
|
19
27
|
return text.replace(/\r\n/g, "\n").replace(/\r/g, "\n");
|
|
@@ -22,31 +30,26 @@ export function restoreLineEndings(text, ending) {
|
|
|
22
30
|
return ending === "\r\n" ? text.replace(/\n/g, "\r\n") : text;
|
|
23
31
|
}
|
|
24
32
|
/**
|
|
25
|
-
* Normalize text for fuzzy matching. Applies
|
|
33
|
+
* Normalize text for fuzzy matching. Applies only semantically-safe
|
|
34
|
+
* whitespace/visibility transformations:
|
|
26
35
|
* - Strip trailing whitespace from each line
|
|
27
|
-
* - Normalize
|
|
28
|
-
*
|
|
29
|
-
*
|
|
36
|
+
* - Normalize special Unicode spaces to a regular space
|
|
37
|
+
*
|
|
38
|
+
* Intentionally does NOT perform NFKC compatibility decomposition, smart-quote
|
|
39
|
+
* → straight-quote, or dash/hyphen substitutions: those transformations make
|
|
40
|
+
* semantically different code match silently. Matching across those characters
|
|
41
|
+
* requires the oldText to use the exact same characters as the file.
|
|
30
42
|
*/
|
|
31
43
|
export function normalizeForFuzzyMatch(text) {
|
|
32
|
-
return
|
|
33
|
-
|
|
34
|
-
// Strip trailing whitespace per line
|
|
44
|
+
return text
|
|
45
|
+
// Strip trailing whitespace per line (tab/space visibility tolerance)
|
|
35
46
|
.split("\n")
|
|
36
47
|
.map((line) => line.trimEnd())
|
|
37
48
|
.join("\n")
|
|
38
|
-
//
|
|
39
|
-
.replace(/[\u2018\u2019\u201A\u201B]/g, "'")
|
|
40
|
-
// Smart double quotes → "
|
|
41
|
-
.replace(/[\u201C\u201D\u201E\u201F]/g, '"')
|
|
42
|
-
// Various dashes/hyphens → -
|
|
43
|
-
// U+2010 hyphen, U+2011 non-breaking hyphen, U+2012 figure dash,
|
|
44
|
-
// U+2013 en-dash, U+2014 em-dash, U+2015 horizontal bar, U+2212 minus
|
|
45
|
-
.replace(/[\u2010\u2011\u2012\u2013\u2014\u2015\u2212]/g, "-")
|
|
46
|
-
// Special spaces → regular space
|
|
49
|
+
// Special spaces → regular space (whitespace-equivalence only)
|
|
47
50
|
// U+00A0 NBSP, U+2002-U+200A various spaces, U+202F narrow NBSP,
|
|
48
51
|
// U+205F medium math space, U+3000 ideographic space
|
|
49
|
-
.replace(/[\u00A0\u2002-\u200A\u202F\u205F\u3000]/g, " ")
|
|
52
|
+
.replace(/[\u00A0\u2002-\u200A\u202F\u205F\u3000]/g, " ");
|
|
50
53
|
}
|
|
51
54
|
function findExactMatches(content, oldText) {
|
|
52
55
|
const matches = [];
|
|
@@ -194,6 +197,12 @@ function findLineTrimmedMatches(content, oldText) {
|
|
|
194
197
|
}
|
|
195
198
|
return dedupeMatches(matches);
|
|
196
199
|
}
|
|
200
|
+
/**
|
|
201
|
+
* Files at or above this size skip the O(n) normalized index map. Line-trimmed
|
|
202
|
+
* matching (which already covers tab/space/CRLF tolerance) is still attempted;
|
|
203
|
+
* only the costly normalized pass is skipped for large files.
|
|
204
|
+
*/
|
|
205
|
+
const NORMALIZED_MATCH_MAX_BYTES = 256 * 1024; // 256KB
|
|
197
206
|
function findTextMatches(content, oldText) {
|
|
198
207
|
const exactMatches = findExactMatches(content, oldText);
|
|
199
208
|
if (exactMatches.length > 0)
|
|
@@ -201,6 +210,8 @@ function findTextMatches(content, oldText) {
|
|
|
201
210
|
const lineTrimmedMatches = findLineTrimmedMatches(content, oldText);
|
|
202
211
|
if (lineTrimmedMatches.length > 0)
|
|
203
212
|
return lineTrimmedMatches;
|
|
213
|
+
if (content.length >= NORMALIZED_MATCH_MAX_BYTES)
|
|
214
|
+
return [];
|
|
204
215
|
return findNormalizedTextMatches(content, oldText);
|
|
205
216
|
}
|
|
206
217
|
export function fuzzyFindText(content, oldText) {
|
|
@@ -240,8 +251,14 @@ function adaptIndentationFlexibleReplacement(oldText, newText, matchedText) {
|
|
|
240
251
|
const matchedBaseIndent = leadingWhitespace(matchedBaseLine);
|
|
241
252
|
if (oldBaseIndent === matchedBaseIndent)
|
|
242
253
|
return newText;
|
|
254
|
+
// When oldText/matchedText/newText have differing line counts, only adapt the
|
|
255
|
+
// common-length prefix; leave the remaining newText lines untouched so a
|
|
256
|
+
// divergent tail is not corrupted by fabricated base-indent prefixes.
|
|
257
|
+
const commonLength = Math.min(oldLines.length, matchedLines.length, newLines.length);
|
|
243
258
|
return newLines
|
|
244
259
|
.map((line, index) => {
|
|
260
|
+
if (index >= commonLength)
|
|
261
|
+
return line;
|
|
245
262
|
if (line.trim().length === 0)
|
|
246
263
|
return line;
|
|
247
264
|
const oldLine = oldLines[index];
|
|
@@ -312,6 +329,7 @@ export function applyEditsToNormalizedContent(normalizedContent, edits, path) {
|
|
|
312
329
|
}
|
|
313
330
|
const baseContent = normalizedContent;
|
|
314
331
|
const matchedEdits = [];
|
|
332
|
+
let usedFuzzyMatch = false;
|
|
315
333
|
for (let i = 0; i < normalizedEdits.length; i++) {
|
|
316
334
|
const edit = normalizedEdits[i];
|
|
317
335
|
const matches = findTextMatches(baseContent, edit.oldText);
|
|
@@ -322,6 +340,8 @@ export function applyEditsToNormalizedContent(normalizedContent, edits, path) {
|
|
|
322
340
|
throw getDuplicateError(path, i, normalizedEdits.length, matches.length);
|
|
323
341
|
}
|
|
324
342
|
const match = matches[0];
|
|
343
|
+
if (match.strategy !== "exact")
|
|
344
|
+
usedFuzzyMatch = true;
|
|
325
345
|
matchedEdits.push({
|
|
326
346
|
editIndex: i,
|
|
327
347
|
matchIndex: match.index,
|
|
@@ -348,7 +368,7 @@ export function applyEditsToNormalizedContent(normalizedContent, edits, path) {
|
|
|
348
368
|
if (baseContent === newContent) {
|
|
349
369
|
throw getNoChangeError(path, normalizedEdits.length);
|
|
350
370
|
}
|
|
351
|
-
return { baseContent, newContent };
|
|
371
|
+
return { baseContent, newContent, usedFuzzyMatch };
|
|
352
372
|
}
|
|
353
373
|
/** Generate a standard unified patch. */
|
|
354
374
|
export function generateUnifiedPatch(path, oldContent, newContent, contextLines = 4) {
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { AgentTool } from "../../../agent-core/index.js";
|
|
2
|
+
import type { Stats } from "fs";
|
|
2
3
|
import { type Static, Type } from "typebox";
|
|
3
4
|
import type { ToolDefinition } from "../extensions/types.js";
|
|
4
5
|
declare const editSchema: Type.TObject<{
|
|
@@ -28,6 +29,11 @@ export interface EditOperations {
|
|
|
28
29
|
writeFile: (absolutePath: string, content: string) => Promise<void>;
|
|
29
30
|
/** Check if file is readable and writable (throw if not) */
|
|
30
31
|
access: (absolutePath: string) => Promise<void>;
|
|
32
|
+
/**
|
|
33
|
+
* Stat a file to obtain a freshness token (mtime). Used to detect stale reads
|
|
34
|
+
* between read and write. Optional — defaults to local fs.stat.
|
|
35
|
+
*/
|
|
36
|
+
stat?: (absolutePath: string) => Promise<Stats>;
|
|
31
37
|
}
|
|
32
38
|
export interface EditToolOptions {
|
|
33
39
|
/** Custom operations for file editing. Default: local filesystem */
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"edit.d.ts","sourceRoot":"","sources":["../../../../../src/sdk/coding-agent/core/tools/edit.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,8BAA8B,CAAC;AAG9D,OAAO,EAAE,KAAK,MAAM,EAAE,IAAI,EAAE,MAAM,SAAS,CAAC;AAE5C,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,wBAAwB,CAAC;
|
|
1
|
+
{"version":3,"file":"edit.d.ts","sourceRoot":"","sources":["../../../../../src/sdk/coding-agent/core/tools/edit.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,8BAA8B,CAAC;AAG9D,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,IAAI,CAAC;AAChC,OAAO,EAAE,KAAK,MAAM,EAAE,IAAI,EAAE,MAAM,SAAS,CAAC;AAE5C,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,wBAAwB,CAAC;AA8B7D,QAAA,MAAM,UAAU;;;;;;EASf,CAAC;AAEF,MAAM,MAAM,aAAa,GAAG,MAAM,CAAC,OAAO,UAAU,CAAC,CAAC;AAMtD,MAAM,WAAW,eAAe;IAC/B,gDAAgD;IAChD,IAAI,EAAE,MAAM,CAAC;IACb,iDAAiD;IACjD,KAAK,EAAE,MAAM,CAAC;IACd,8EAA8E;IAC9E,gBAAgB,CAAC,EAAE,MAAM,CAAC;CAC1B;AAED;;;GAGG;AACH,MAAM,WAAW,cAAc;IAC9B,qCAAqC;IACrC,QAAQ,EAAE,CAAC,YAAY,EAAE,MAAM,KAAK,OAAO,CAAC,MAAM,CAAC,CAAC;IACpD,8BAA8B;IAC9B,SAAS,EAAE,CAAC,YAAY,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IACpE,4DAA4D;IAC5D,MAAM,EAAE,CAAC,YAAY,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IAChD;;;OAGG;IACH,IAAI,CAAC,EAAE,CAAC,YAAY,EAAE,MAAM,KAAK,OAAO,CAAC,KAAK,CAAC,CAAC;CAChD;AASD,MAAM,WAAW,eAAe;IAC/B,oEAAoE;IACpE,UAAU,CAAC,EAAE,cAAc,CAAC;CAC5B;AAqGD,wBAAgB,wBAAwB,CACvC,GAAG,EAAE,MAAM,EACX,OAAO,CAAC,EAAE,eAAe,GACvB,cAAc,CAAC,OAAO,UAAU,EAAE,eAAe,GAAG,SAAS,CAAC,CAyJhE;AAED,wBAAgB,cAAc,CAAC,GAAG,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,eAAe,GAAG,SAAS,CAAC,OAAO,UAAU,CAAC,CAEnG"}
|