editcodewithai 2.5.0 → 2.7.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.
- package/README.md +58 -12
- package/dist/applyEngine.d.ts +13 -0
- package/dist/applyEngine.js +123 -0
- package/dist/fileUtils.d.ts +27 -0
- package/dist/fileUtils.js +75 -2
- package/dist/index.d.ts +3 -3
- package/dist/index.js +35 -5
- package/dist/matching.d.ts +32 -0
- package/dist/matching.js +126 -0
- package/dist/prompt.d.ts +1 -1
- package/dist/prompt.js +4 -1
- package/dist/types.d.ts +39 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -171,6 +171,47 @@ The library handles several file operations automatically:
|
|
|
171
171
|
- **Creating new files**: When the AI includes a file name that doesn't exist in the original set.
|
|
172
172
|
- **Deleting files**: When the AI returns empty/whitespace-only content for a file (works with `whole` format; for `diff`/`udiff`, deletions happen naturally when all content is replaced).
|
|
173
173
|
|
|
174
|
+
### Best-effort application and warnings
|
|
175
|
+
|
|
176
|
+
`performAiEdit` applies edits independently and **never throws because the model
|
|
177
|
+
returned an edit that could not be matched**. If one edit references a missing
|
|
178
|
+
file, an anchor that cannot be found, or an ambiguous location, that edit is
|
|
179
|
+
skipped and reported — every other valid edit is still applied.
|
|
180
|
+
|
|
181
|
+
Skipped edits are returned in `result.warnings`:
|
|
182
|
+
|
|
183
|
+
```typescript
|
|
184
|
+
const result = await performAiEdit({
|
|
185
|
+
prompt,
|
|
186
|
+
files,
|
|
187
|
+
llmFunction,
|
|
188
|
+
editFormat: "diff",
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
for (const warning of result.warnings ?? []) {
|
|
192
|
+
console.warn(warning.code, warning.fileName, warning.message);
|
|
193
|
+
}
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
| Warning code | Meaning |
|
|
197
|
+
| ------------------ | ------------------------------------------------------------------- |
|
|
198
|
+
| `FILE_NOT_FOUND` | The edit targets a filename that isn't in the file set. |
|
|
199
|
+
| `SEARCH_NOT_FOUND` | The `SEARCH` text could not be located in the file. |
|
|
200
|
+
| `HUNK_NOT_FOUND` | A udiff hunk's original lines could not be located. |
|
|
201
|
+
| `AMBIGUOUS_SEARCH` | The anchor matched more than one location, so the edit was skipped. |
|
|
202
|
+
| `AMBIGUOUS_FILE` | More than one file shares the target name, so the edit was skipped. |
|
|
203
|
+
| `EMPTY_SEARCH` | The search block was empty, so the edit could not be applied. |
|
|
204
|
+
| `NO_EDITS_PARSED` | The model response contained no recognizable edits. |
|
|
205
|
+
|
|
206
|
+
Matching is deterministic: only harmless formatting drift is absorbed (CRLF vs
|
|
207
|
+
LF, a missing/extra trailing newline, trailing whitespace) plus filename drift —
|
|
208
|
+
model-emitted decoration (backticks, bold, headings, a `File:` label, a trailing
|
|
209
|
+
colon, a leading `./`) is stripped, and a target resolves by normalized name,
|
|
210
|
+
case-insensitively, or by a unique basename. Anything ambiguous or unmatched is
|
|
211
|
+
skipped rather than guessed. Successful normalized matches produce no warnings.
|
|
212
|
+
|
|
213
|
+
`warnings` is optional, so existing consumers continue to work unchanged.
|
|
214
|
+
|
|
174
215
|
## API Reference
|
|
175
216
|
|
|
176
217
|
### `performAiEdit(params)`
|
|
@@ -198,16 +239,17 @@ type LlmFunction = (prompt: string) => Promise<{
|
|
|
198
239
|
|
|
199
240
|
#### Output: `PerformAiEditResult`
|
|
200
241
|
|
|
201
|
-
| Property | Type
|
|
202
|
-
| ------------------------ |
|
|
203
|
-
| `changedFiles` | `VizFiles`
|
|
204
|
-
| `openRouterGenerationId` | `string`
|
|
205
|
-
| `upstreamCostCents` | `number`
|
|
206
|
-
| `provider` | `string`
|
|
207
|
-
| `inputTokens` | `number`
|
|
208
|
-
| `outputTokens` | `number`
|
|
209
|
-
| `promptTemplateVersion` | `number`
|
|
210
|
-
| `rawResponse` | `string`
|
|
242
|
+
| Property | Type | Description |
|
|
243
|
+
| ------------------------ | ---------------- | ----------------------------------------------------------------- |
|
|
244
|
+
| `changedFiles` | `VizFiles` | The updated file collection with all edits applied. |
|
|
245
|
+
| `openRouterGenerationId` | `string` | The generation ID from the LLM function response. |
|
|
246
|
+
| `upstreamCostCents` | `number` | Cost in cents (only populated if `apiKey` was provided). |
|
|
247
|
+
| `provider` | `string` | The OpenRouter provider name used. |
|
|
248
|
+
| `inputTokens` | `number` | Number of input (prompt) tokens billed. |
|
|
249
|
+
| `outputTokens` | `number` | Number of output (completion) tokens billed. |
|
|
250
|
+
| `promptTemplateVersion` | `number` | Version of the prompt template used (for tracking migrations). |
|
|
251
|
+
| `rawResponse` | `string` | The raw string response from the LLM, unmodified. |
|
|
252
|
+
| `warnings` | `ApplyWarning[]` | Edits that were skipped because they could not be applied safely. |
|
|
211
253
|
|
|
212
254
|
### Exported Utilities
|
|
213
255
|
|
|
@@ -225,11 +267,15 @@ import {
|
|
|
225
267
|
|
|
226
268
|
// --- Diff parsing (manual use) ---
|
|
227
269
|
parseDiffs, // Parse search/replace blocks (diff format)
|
|
228
|
-
|
|
270
|
+
normalizeDiffFileName, // Normalize a model-emitted filename (strip decoration/path noise)
|
|
271
|
+
applyDiffs, // Apply parsed diffs to a file set (throws on unmatched edits)
|
|
272
|
+
applyDiffsSafe, // Best-effort apply: returns { files, warnings } instead of throwing
|
|
229
273
|
parseDiffFenced, // Parse search/replace blocks (diff-fenced format)
|
|
230
274
|
parseUdiffs, // Parse unified diff hunks
|
|
231
|
-
applyUdiffs, // Apply parsed unified diff hunks
|
|
275
|
+
applyUdiffs, // Apply parsed unified diff hunks (throws on unmatched edits)
|
|
276
|
+
applyUdiffsSafe, // Best-effort apply: returns { files, warnings } instead of throwing
|
|
232
277
|
applyHybridEdits, // Apply mixed whole-file + diff edits from a single response
|
|
278
|
+
applyHybridEditsSafe, // Best-effort hybrid apply: returns { files, warnings }
|
|
233
279
|
|
|
234
280
|
// --- Cost metadata ---
|
|
235
281
|
getGenerationMetadata, // Fetch OpenRouter cost data for a generation ID
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { VizFiles } from "@vizhub/viz-types";
|
|
2
|
+
import type { ApplyResult, ParsedEdit } from "./types";
|
|
3
|
+
/**
|
|
4
|
+
* Applies parsed edits on a best-effort basis.
|
|
5
|
+
*
|
|
6
|
+
* Each edit is applied independently: an edit that cannot be applied safely is
|
|
7
|
+
* skipped and reported in `warnings`, and never prevents other edits from being
|
|
8
|
+
* applied. The input `files` is never mutated.
|
|
9
|
+
*
|
|
10
|
+
* This function only throws for programmer error; anticipated model-output
|
|
11
|
+
* failures are reported as warnings.
|
|
12
|
+
*/
|
|
13
|
+
export declare function applyEditsSafe(files: VizFiles, edits: ParsedEdit[]): ApplyResult;
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.applyEditsSafe = applyEditsSafe;
|
|
4
|
+
const matching_1 = require("./matching");
|
|
5
|
+
/**
|
|
6
|
+
* Normalizes a filename for comparison: trims, converts Windows separators to
|
|
7
|
+
* "/", collapses duplicate separators, and strips leading "./" and "/".
|
|
8
|
+
*
|
|
9
|
+
* Case is intentionally preserved here; callers that need to tolerate case
|
|
10
|
+
* drift fall back to case-insensitive and basename matching after this exact
|
|
11
|
+
* normalized comparison fails.
|
|
12
|
+
*/
|
|
13
|
+
function normalizeFileName(name) {
|
|
14
|
+
return name
|
|
15
|
+
.trim()
|
|
16
|
+
.replace(/\\/g, "/")
|
|
17
|
+
.replace(/\/+/g, "/")
|
|
18
|
+
.replace(/^(?:\.\/)+/, "")
|
|
19
|
+
.replace(/^\/+/, "");
|
|
20
|
+
}
|
|
21
|
+
function normalizeForBasename(name) {
|
|
22
|
+
const normalized = normalizeFileName(name).toLowerCase();
|
|
23
|
+
return normalized.substring(normalized.lastIndexOf("/") + 1);
|
|
24
|
+
}
|
|
25
|
+
function resolveFileId(files, rawName) {
|
|
26
|
+
const target = normalizeFileName(rawName);
|
|
27
|
+
const ids = Object.keys(files);
|
|
28
|
+
// 1. Exact match on the normalized name.
|
|
29
|
+
const exact = ids.filter((id) => normalizeFileName(files[id].name) === target);
|
|
30
|
+
if (exact.length === 1)
|
|
31
|
+
return { kind: "found", id: exact[0] };
|
|
32
|
+
if (exact.length > 1)
|
|
33
|
+
return { kind: "ambiguous" };
|
|
34
|
+
// 2. Case-insensitive match on the normalized name.
|
|
35
|
+
const lowerTarget = target.toLowerCase();
|
|
36
|
+
const caseInsensitive = ids.filter((id) => normalizeFileName(files[id].name).toLowerCase() === lowerTarget);
|
|
37
|
+
if (caseInsensitive.length === 1)
|
|
38
|
+
return { kind: "found", id: caseInsensitive[0] };
|
|
39
|
+
if (caseInsensitive.length > 1)
|
|
40
|
+
return { kind: "ambiguous" };
|
|
41
|
+
// 3. Basename match (substring after the last "/"), case-insensitive.
|
|
42
|
+
const basename = lowerTarget.substring(lowerTarget.lastIndexOf("/") + 1);
|
|
43
|
+
const byBasename = ids.filter((id) => normalizeForBasename(files[id].name) === basename);
|
|
44
|
+
if (byBasename.length === 1)
|
|
45
|
+
return { kind: "found", id: byBasename[0] };
|
|
46
|
+
if (byBasename.length > 1)
|
|
47
|
+
return { kind: "ambiguous" };
|
|
48
|
+
return { kind: "none" };
|
|
49
|
+
}
|
|
50
|
+
function anchorOf(edit) {
|
|
51
|
+
return edit.kind === "diff" ? edit.search : edit.original;
|
|
52
|
+
}
|
|
53
|
+
function replacementOf(edit) {
|
|
54
|
+
return edit.kind === "diff" ? edit.replace : edit.updated;
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Applies parsed edits on a best-effort basis.
|
|
58
|
+
*
|
|
59
|
+
* Each edit is applied independently: an edit that cannot be applied safely is
|
|
60
|
+
* skipped and reported in `warnings`, and never prevents other edits from being
|
|
61
|
+
* applied. The input `files` is never mutated.
|
|
62
|
+
*
|
|
63
|
+
* This function only throws for programmer error; anticipated model-output
|
|
64
|
+
* failures are reported as warnings.
|
|
65
|
+
*/
|
|
66
|
+
function applyEditsSafe(files, edits) {
|
|
67
|
+
const work = JSON.parse(JSON.stringify(files));
|
|
68
|
+
const warnings = [];
|
|
69
|
+
for (const edit of edits) {
|
|
70
|
+
const resolution = resolveFileId(work, edit.fileName);
|
|
71
|
+
if (resolution.kind === "ambiguous") {
|
|
72
|
+
warnings.push({
|
|
73
|
+
code: "AMBIGUOUS_FILE",
|
|
74
|
+
fileName: edit.fileName,
|
|
75
|
+
message: `Skipped ${edit.fileName}: multiple files share that name.`,
|
|
76
|
+
});
|
|
77
|
+
continue;
|
|
78
|
+
}
|
|
79
|
+
if (resolution.kind === "none") {
|
|
80
|
+
warnings.push({
|
|
81
|
+
code: "FILE_NOT_FOUND",
|
|
82
|
+
fileName: edit.fileName,
|
|
83
|
+
message: `Skipped changes to ${edit.fileName}: file not found.`,
|
|
84
|
+
});
|
|
85
|
+
continue;
|
|
86
|
+
}
|
|
87
|
+
const anchor = anchorOf(edit);
|
|
88
|
+
if (anchor.trim() === "") {
|
|
89
|
+
warnings.push({
|
|
90
|
+
code: "EMPTY_SEARCH",
|
|
91
|
+
fileName: edit.fileName,
|
|
92
|
+
message: `Skipped a change to ${edit.fileName}: the search block was empty.`,
|
|
93
|
+
});
|
|
94
|
+
continue;
|
|
95
|
+
}
|
|
96
|
+
const file = work[resolution.id];
|
|
97
|
+
const match = (0, matching_1.findAnchor)(file.text, anchor);
|
|
98
|
+
if (match === null) {
|
|
99
|
+
warnings.push({
|
|
100
|
+
code: edit.kind === "udiff" ? "HUNK_NOT_FOUND" : "SEARCH_NOT_FOUND",
|
|
101
|
+
fileName: edit.fileName,
|
|
102
|
+
message: edit.kind === "udiff"
|
|
103
|
+
? `Skipped part of a change to ${edit.fileName}: referenced lines were not found.`
|
|
104
|
+
: `Skipped a change to ${edit.fileName}: the referenced code was not found.`,
|
|
105
|
+
});
|
|
106
|
+
continue;
|
|
107
|
+
}
|
|
108
|
+
// Ambiguous anchors are never guessed: skip rather than replace the first.
|
|
109
|
+
if (match.matchCount > 1) {
|
|
110
|
+
warnings.push({
|
|
111
|
+
code: "AMBIGUOUS_SEARCH",
|
|
112
|
+
fileName: edit.fileName,
|
|
113
|
+
message: `Skipped a change to ${edit.fileName}: the referenced code matched ${match.matchCount} locations.`,
|
|
114
|
+
});
|
|
115
|
+
continue;
|
|
116
|
+
}
|
|
117
|
+
file.text =
|
|
118
|
+
file.text.slice(0, match.start) +
|
|
119
|
+
replacementOf(edit) +
|
|
120
|
+
file.text.slice(match.end);
|
|
121
|
+
}
|
|
122
|
+
return { files: work, warnings };
|
|
123
|
+
}
|
package/dist/fileUtils.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { VizFiles, VizFile, FileCollection } from "@vizhub/viz-types";
|
|
2
|
+
import type { ApplyResult } from "./types";
|
|
2
3
|
/**
|
|
3
4
|
* If the LLM outputs empty text for a file, we interpret this
|
|
4
5
|
* as a request to delete the file.
|
|
@@ -24,6 +25,15 @@ export interface Diff {
|
|
|
24
25
|
search: string;
|
|
25
26
|
replace: string;
|
|
26
27
|
}
|
|
28
|
+
/**
|
|
29
|
+
* Normalizes a model-emitted diff filename line, tolerating common
|
|
30
|
+
* decoration drift (markdown headings, list markers, labels, backticks,
|
|
31
|
+
* bold/italic asterisks, trailing colons) as well as path noise.
|
|
32
|
+
*
|
|
33
|
+
* Decoration-stripping rules are applied repeatedly until the string stops
|
|
34
|
+
* changing, so combinations (e.g. `- **`index.js`**:`) fully normalize.
|
|
35
|
+
*/
|
|
36
|
+
export declare function normalizeDiffFileName(raw: string): string;
|
|
27
37
|
export declare function parseDiffs(responseText: string): Diff[];
|
|
28
38
|
export declare function applyDiffs(originalFiles: VizFiles, diffs: Diff[]): VizFiles;
|
|
29
39
|
export declare function parseDiffFenced(responseText: string): Diff[];
|
|
@@ -43,4 +53,21 @@ export declare function parseUdiffs(responseText: string): UdiffHunk[];
|
|
|
43
53
|
* (If the same file appears in both forms, the whole-file content wins.)
|
|
44
54
|
*/
|
|
45
55
|
export declare function applyHybridEdits(responseText: string, originalFiles: VizFiles, parseWholeFiles: (text: string) => FileCollection): VizFiles;
|
|
56
|
+
/**
|
|
57
|
+
* Best-effort variant of `applyDiffs`. Never throws for unmatched files or
|
|
58
|
+
* anchors; skipped edits are reported in `warnings`.
|
|
59
|
+
*/
|
|
60
|
+
export declare function applyDiffsSafe(originalFiles: VizFiles, diffs: Diff[]): ApplyResult;
|
|
61
|
+
/**
|
|
62
|
+
* Best-effort variant of `applyUdiffs`. Never throws for unmatched files or
|
|
63
|
+
* hunks; skipped edits are reported in `warnings`.
|
|
64
|
+
*/
|
|
65
|
+
export declare function applyUdiffsSafe(originalFiles: VizFiles, hunks: UdiffHunk[]): ApplyResult;
|
|
66
|
+
/**
|
|
67
|
+
* Best-effort variant of `applyHybridEdits`. Applies search/replace diffs first,
|
|
68
|
+
* then whole-file replacements. Warnings for diffs that target a file which is
|
|
69
|
+
* also replaced wholesale are suppressed, since the whole-file content
|
|
70
|
+
* supersedes the failed diff.
|
|
71
|
+
*/
|
|
72
|
+
export declare function applyHybridEditsSafe(responseText: string, originalFiles: VizFiles, parseWholeFiles: (text: string) => FileCollection): ApplyResult;
|
|
46
73
|
export declare function applyUdiffs(originalFiles: VizFiles, hunks: UdiffHunk[]): VizFiles;
|
package/dist/fileUtils.js
CHANGED
|
@@ -4,13 +4,18 @@ exports.shouldDeleteFile = shouldDeleteFile;
|
|
|
4
4
|
exports.isImageFile = isImageFile;
|
|
5
5
|
exports.prepareFilesForPrompt = prepareFilesForPrompt;
|
|
6
6
|
exports.mergeFileChanges = mergeFileChanges;
|
|
7
|
+
exports.normalizeDiffFileName = normalizeDiffFileName;
|
|
7
8
|
exports.parseDiffs = parseDiffs;
|
|
8
9
|
exports.applyDiffs = applyDiffs;
|
|
9
10
|
exports.parseDiffFenced = parseDiffFenced;
|
|
10
11
|
exports.parseUdiffs = parseUdiffs;
|
|
11
12
|
exports.applyHybridEdits = applyHybridEdits;
|
|
13
|
+
exports.applyDiffsSafe = applyDiffsSafe;
|
|
14
|
+
exports.applyUdiffsSafe = applyUdiffsSafe;
|
|
15
|
+
exports.applyHybridEditsSafe = applyHybridEditsSafe;
|
|
12
16
|
exports.applyUdiffs = applyUdiffs;
|
|
13
17
|
const viz_utils_1 = require("@vizhub/viz-utils");
|
|
18
|
+
const applyEngine_1 = require("./applyEngine");
|
|
14
19
|
/**
|
|
15
20
|
* If the LLM outputs empty text for a file, we interpret this
|
|
16
21
|
* as a request to delete the file.
|
|
@@ -84,6 +89,35 @@ function mergeFileChanges(originalFiles, parsedFiles) {
|
|
|
84
89
|
});
|
|
85
90
|
return changedFiles;
|
|
86
91
|
}
|
|
92
|
+
/**
|
|
93
|
+
* Normalizes a model-emitted diff filename line, tolerating common
|
|
94
|
+
* decoration drift (markdown headings, list markers, labels, backticks,
|
|
95
|
+
* bold/italic asterisks, trailing colons) as well as path noise.
|
|
96
|
+
*
|
|
97
|
+
* Decoration-stripping rules are applied repeatedly until the string stops
|
|
98
|
+
* changing, so combinations (e.g. `- **`index.js`**:`) fully normalize.
|
|
99
|
+
*/
|
|
100
|
+
function normalizeDiffFileName(raw) {
|
|
101
|
+
let name = raw;
|
|
102
|
+
let previous;
|
|
103
|
+
do {
|
|
104
|
+
previous = name;
|
|
105
|
+
name = name.trim();
|
|
106
|
+
name = name.replace(/^#{1,6}\s+/, "");
|
|
107
|
+
name = name.replace(/^[-*+]\s+/, "");
|
|
108
|
+
name = name.replace(/^(?:file|path)\s*:\s*/i, "");
|
|
109
|
+
name = name.replace(/^`+|`+$/g, "");
|
|
110
|
+
name = name.replace(/^\*+|\*+$/g, "");
|
|
111
|
+
name = name.replace(/:\s*$/, "");
|
|
112
|
+
name = name.trim();
|
|
113
|
+
} while (name !== previous);
|
|
114
|
+
return name
|
|
115
|
+
.replace(/\\/g, "/")
|
|
116
|
+
.replace(/\/+/g, "/")
|
|
117
|
+
.replace(/^(?:\.\/)+/, "")
|
|
118
|
+
.replace(/^\/+/, "")
|
|
119
|
+
.trim();
|
|
120
|
+
}
|
|
87
121
|
function parseDiffs(responseText) {
|
|
88
122
|
const diffs = [];
|
|
89
123
|
// This regex captures the file path, and the content of the SEARCH and REPLACE blocks.
|
|
@@ -92,7 +126,7 @@ function parseDiffs(responseText) {
|
|
|
92
126
|
for (const match of matches) {
|
|
93
127
|
const [_, fileName, search, replace] = match;
|
|
94
128
|
diffs.push({
|
|
95
|
-
fileName: fileName
|
|
129
|
+
fileName: normalizeDiffFileName(fileName),
|
|
96
130
|
search,
|
|
97
131
|
replace,
|
|
98
132
|
});
|
|
@@ -123,7 +157,7 @@ function parseDiffFenced(responseText) {
|
|
|
123
157
|
for (const match of matches) {
|
|
124
158
|
const [_, fileName, search, replace] = match;
|
|
125
159
|
diffs.push({
|
|
126
|
-
fileName: fileName
|
|
160
|
+
fileName: normalizeDiffFileName(fileName),
|
|
127
161
|
search,
|
|
128
162
|
replace,
|
|
129
163
|
});
|
|
@@ -184,6 +218,45 @@ function applyHybridEdits(responseText, originalFiles, parseWholeFiles) {
|
|
|
184
218
|
changedFiles = mergeFileChanges(changedFiles, parsed);
|
|
185
219
|
return changedFiles;
|
|
186
220
|
}
|
|
221
|
+
/**
|
|
222
|
+
* Best-effort variant of `applyDiffs`. Never throws for unmatched files or
|
|
223
|
+
* anchors; skipped edits are reported in `warnings`.
|
|
224
|
+
*/
|
|
225
|
+
function applyDiffsSafe(originalFiles, diffs) {
|
|
226
|
+
return (0, applyEngine_1.applyEditsSafe)(originalFiles, diffs.map((diff) => ({
|
|
227
|
+
kind: "diff",
|
|
228
|
+
fileName: diff.fileName,
|
|
229
|
+
search: diff.search,
|
|
230
|
+
replace: diff.replace,
|
|
231
|
+
})));
|
|
232
|
+
}
|
|
233
|
+
/**
|
|
234
|
+
* Best-effort variant of `applyUdiffs`. Never throws for unmatched files or
|
|
235
|
+
* hunks; skipped edits are reported in `warnings`.
|
|
236
|
+
*/
|
|
237
|
+
function applyUdiffsSafe(originalFiles, hunks) {
|
|
238
|
+
return (0, applyEngine_1.applyEditsSafe)(originalFiles, hunks.map((hunk) => ({
|
|
239
|
+
kind: "udiff",
|
|
240
|
+
fileName: hunk.fileName,
|
|
241
|
+
original: hunk.original,
|
|
242
|
+
updated: hunk.updated,
|
|
243
|
+
})));
|
|
244
|
+
}
|
|
245
|
+
/**
|
|
246
|
+
* Best-effort variant of `applyHybridEdits`. Applies search/replace diffs first,
|
|
247
|
+
* then whole-file replacements. Warnings for diffs that target a file which is
|
|
248
|
+
* also replaced wholesale are suppressed, since the whole-file content
|
|
249
|
+
* supersedes the failed diff.
|
|
250
|
+
*/
|
|
251
|
+
function applyHybridEditsSafe(responseText, originalFiles, parseWholeFiles) {
|
|
252
|
+
const diffResult = applyDiffsSafe(originalFiles, parseDiffs(responseText));
|
|
253
|
+
const parsedWholeFiles = parseWholeFiles(responseText);
|
|
254
|
+
const changedFiles = mergeFileChanges(diffResult.files, parsedWholeFiles);
|
|
255
|
+
const wholeFileNames = new Set(Object.keys(parsedWholeFiles).map((name) => name.trim()));
|
|
256
|
+
const warnings = diffResult.warnings.filter((warning) => warning.fileName === undefined ||
|
|
257
|
+
!wholeFileNames.has(warning.fileName.trim()));
|
|
258
|
+
return { files: changedFiles, warnings };
|
|
259
|
+
}
|
|
187
260
|
function applyUdiffs(originalFiles, hunks) {
|
|
188
261
|
const changedFiles = JSON.parse(JSON.stringify(originalFiles));
|
|
189
262
|
for (const hunk of hunks) {
|
package/dist/index.d.ts
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import type { PerformAiEditParams, PerformAiEditResult } from "./types";
|
|
2
2
|
import { PROMPT_TEMPLATE_VERSION, assembleFullPrompt, FORMAT_INSTRUCTIONS } from "./prompt";
|
|
3
3
|
import { getGenerationMetadata } from "./metadata";
|
|
4
|
-
import { prepareFilesForPrompt, mergeFileChanges, parseDiffs, applyDiffs, parseDiffFenced, parseUdiffs, applyUdiffs, applyHybridEdits, isImageFile } from "./fileUtils";
|
|
5
|
-
export type { LlmFunction, PerformAiEditParams, PerformAiEditResult, EditFormat, } from "./types";
|
|
6
|
-
export { FORMAT_INSTRUCTIONS, mergeFileChanges, prepareFilesForPrompt, isImageFile, parseDiffs, applyDiffs, parseDiffFenced, parseUdiffs, applyUdiffs, applyHybridEdits, assembleFullPrompt, getGenerationMetadata, PROMPT_TEMPLATE_VERSION, };
|
|
4
|
+
import { prepareFilesForPrompt, mergeFileChanges, parseDiffs, applyDiffs, applyDiffsSafe, parseDiffFenced, normalizeDiffFileName, parseUdiffs, applyUdiffs, applyUdiffsSafe, applyHybridEdits, applyHybridEditsSafe, isImageFile } from "./fileUtils";
|
|
5
|
+
export type { LlmFunction, PerformAiEditParams, PerformAiEditResult, EditFormat, ApplyWarning, ApplyWarningCode, ApplyResult, ParsedEdit, } from "./types";
|
|
6
|
+
export { FORMAT_INSTRUCTIONS, mergeFileChanges, prepareFilesForPrompt, isImageFile, parseDiffs, applyDiffs, applyDiffsSafe, parseDiffFenced, normalizeDiffFileName, parseUdiffs, applyUdiffs, applyUdiffsSafe, applyHybridEdits, applyHybridEditsSafe, assembleFullPrompt, getGenerationMetadata, PROMPT_TEMPLATE_VERSION, };
|
|
7
7
|
/**
|
|
8
8
|
* Core AI logic for:
|
|
9
9
|
* - Building the prompt (including context)
|
package/dist/index.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.PROMPT_TEMPLATE_VERSION = exports.getGenerationMetadata = exports.assembleFullPrompt = exports.applyHybridEdits = exports.applyUdiffs = exports.parseUdiffs = exports.parseDiffFenced = exports.applyDiffs = exports.parseDiffs = exports.isImageFile = exports.prepareFilesForPrompt = exports.mergeFileChanges = exports.FORMAT_INSTRUCTIONS = void 0;
|
|
3
|
+
exports.PROMPT_TEMPLATE_VERSION = exports.getGenerationMetadata = exports.assembleFullPrompt = exports.applyHybridEditsSafe = exports.applyHybridEdits = exports.applyUdiffsSafe = exports.applyUdiffs = exports.parseUdiffs = exports.normalizeDiffFileName = exports.parseDiffFenced = exports.applyDiffsSafe = exports.applyDiffs = exports.parseDiffs = exports.isImageFile = exports.prepareFilesForPrompt = exports.mergeFileChanges = exports.FORMAT_INSTRUCTIONS = void 0;
|
|
4
4
|
exports.performAiEdit = performAiEdit;
|
|
5
5
|
const llm_code_format_1 = require("llm-code-format");
|
|
6
6
|
const prompt_1 = require("./prompt");
|
|
@@ -14,10 +14,14 @@ Object.defineProperty(exports, "prepareFilesForPrompt", { enumerable: true, get:
|
|
|
14
14
|
Object.defineProperty(exports, "mergeFileChanges", { enumerable: true, get: function () { return fileUtils_1.mergeFileChanges; } });
|
|
15
15
|
Object.defineProperty(exports, "parseDiffs", { enumerable: true, get: function () { return fileUtils_1.parseDiffs; } });
|
|
16
16
|
Object.defineProperty(exports, "applyDiffs", { enumerable: true, get: function () { return fileUtils_1.applyDiffs; } });
|
|
17
|
+
Object.defineProperty(exports, "applyDiffsSafe", { enumerable: true, get: function () { return fileUtils_1.applyDiffsSafe; } });
|
|
17
18
|
Object.defineProperty(exports, "parseDiffFenced", { enumerable: true, get: function () { return fileUtils_1.parseDiffFenced; } });
|
|
19
|
+
Object.defineProperty(exports, "normalizeDiffFileName", { enumerable: true, get: function () { return fileUtils_1.normalizeDiffFileName; } });
|
|
18
20
|
Object.defineProperty(exports, "parseUdiffs", { enumerable: true, get: function () { return fileUtils_1.parseUdiffs; } });
|
|
19
21
|
Object.defineProperty(exports, "applyUdiffs", { enumerable: true, get: function () { return fileUtils_1.applyUdiffs; } });
|
|
22
|
+
Object.defineProperty(exports, "applyUdiffsSafe", { enumerable: true, get: function () { return fileUtils_1.applyUdiffsSafe; } });
|
|
20
23
|
Object.defineProperty(exports, "applyHybridEdits", { enumerable: true, get: function () { return fileUtils_1.applyHybridEdits; } });
|
|
24
|
+
Object.defineProperty(exports, "applyHybridEditsSafe", { enumerable: true, get: function () { return fileUtils_1.applyHybridEditsSafe; } });
|
|
21
25
|
Object.defineProperty(exports, "isImageFile", { enumerable: true, get: function () { return fileUtils_1.isImageFile; } });
|
|
22
26
|
const debug = false;
|
|
23
27
|
/**
|
|
@@ -44,35 +48,60 @@ async function performAiEdit({ prompt, files, llmFunction, apiKey, editFormat =
|
|
|
44
48
|
// 4. We parse the output to figure out which files changed
|
|
45
49
|
const resultString = result.content;
|
|
46
50
|
let changedFiles;
|
|
51
|
+
const warnings = [];
|
|
52
|
+
let editsParsed = 0;
|
|
47
53
|
switch (editFormat) {
|
|
48
54
|
case "whole": {
|
|
49
55
|
const parsed = (0, llm_code_format_1.parseMarkdownFiles)(resultString, "bold");
|
|
56
|
+
editsParsed = Object.keys(parsed.files).length;
|
|
50
57
|
changedFiles = (0, fileUtils_1.mergeFileChanges)(files, parsed.files);
|
|
51
58
|
break;
|
|
52
59
|
}
|
|
53
60
|
case "diff": {
|
|
54
61
|
const diffs = (0, fileUtils_1.parseDiffs)(resultString);
|
|
55
|
-
|
|
62
|
+
editsParsed = diffs.length;
|
|
63
|
+
const applied = (0, fileUtils_1.applyDiffsSafe)(files, diffs);
|
|
64
|
+
changedFiles = applied.files;
|
|
65
|
+
warnings.push(...applied.warnings);
|
|
56
66
|
break;
|
|
57
67
|
}
|
|
58
68
|
case "diff-fenced": {
|
|
59
69
|
const diffs = (0, fileUtils_1.parseDiffFenced)(resultString);
|
|
60
|
-
|
|
70
|
+
editsParsed = diffs.length;
|
|
71
|
+
const applied = (0, fileUtils_1.applyDiffsSafe)(files, diffs);
|
|
72
|
+
changedFiles = applied.files;
|
|
73
|
+
warnings.push(...applied.warnings);
|
|
61
74
|
break;
|
|
62
75
|
}
|
|
63
76
|
case "udiff": {
|
|
64
77
|
const hunks = (0, fileUtils_1.parseUdiffs)(resultString);
|
|
65
|
-
|
|
78
|
+
editsParsed = hunks.length;
|
|
79
|
+
const applied = (0, fileUtils_1.applyUdiffsSafe)(files, hunks);
|
|
80
|
+
changedFiles = applied.files;
|
|
81
|
+
warnings.push(...applied.warnings);
|
|
66
82
|
break;
|
|
67
83
|
}
|
|
68
84
|
case "hybrid": {
|
|
69
|
-
|
|
85
|
+
const diffs = (0, fileUtils_1.parseDiffs)(resultString);
|
|
86
|
+
const wholeFiles = (0, llm_code_format_1.parseMarkdownFiles)(resultString, "bold").files;
|
|
87
|
+
editsParsed = diffs.length + Object.keys(wholeFiles).length;
|
|
88
|
+
const applied = (0, fileUtils_1.applyHybridEditsSafe)(resultString, files, (text) => (0, llm_code_format_1.parseMarkdownFiles)(text, "bold").files);
|
|
89
|
+
changedFiles = applied.files;
|
|
90
|
+
warnings.push(...applied.warnings);
|
|
70
91
|
break;
|
|
71
92
|
}
|
|
72
93
|
default:
|
|
73
94
|
// This will catch any unhandled or unknown edit formats.
|
|
74
95
|
throw new Error(`Unknown edit format: ${editFormat}`);
|
|
75
96
|
}
|
|
97
|
+
// Zero parsed edits is worth surfacing, but only when nothing else was
|
|
98
|
+
// reported — otherwise it just adds noise on top of real failures.
|
|
99
|
+
if (editsParsed === 0 && warnings.length === 0) {
|
|
100
|
+
warnings.push({
|
|
101
|
+
code: "NO_EDITS_PARSED",
|
|
102
|
+
message: "The model returned no applicable edits.",
|
|
103
|
+
});
|
|
104
|
+
}
|
|
76
105
|
// 6. Retrieve cost metadata for charging the user
|
|
77
106
|
const openRouterGenerationId = result.generationId || "";
|
|
78
107
|
let upstreamCostCents = 0;
|
|
@@ -98,5 +127,6 @@ async function performAiEdit({ prompt, files, llmFunction, apiKey, editFormat =
|
|
|
98
127
|
outputTokens,
|
|
99
128
|
promptTemplateVersion: prompt_1.PROMPT_TEMPLATE_VERSION,
|
|
100
129
|
rawResponse: resultString, // Include the raw response
|
|
130
|
+
warnings,
|
|
101
131
|
};
|
|
102
132
|
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Deterministic anchor matching for edit application.
|
|
3
|
+
*
|
|
4
|
+
* This module intentionally implements only deterministic strategies. It never
|
|
5
|
+
* guesses a location via similarity scoring; if a deterministic match cannot be
|
|
6
|
+
* found, callers skip the edit and report a warning.
|
|
7
|
+
*
|
|
8
|
+
* Strategies, in order:
|
|
9
|
+
* 1. `exact` — the anchor is a literal substring.
|
|
10
|
+
* 2. `line-normalized` — both sides split into lines and compared with
|
|
11
|
+
* trailing whitespace removed. This absorbs CRLF vs LF
|
|
12
|
+
* drift, a missing/extra trailing newline, and
|
|
13
|
+
* trailing-whitespace drift.
|
|
14
|
+
*
|
|
15
|
+
* Leading indentation drift is deliberately NOT handled (deferred).
|
|
16
|
+
*/
|
|
17
|
+
export type MatchStrategy = "exact" | "line-normalized";
|
|
18
|
+
export interface AnchorMatch {
|
|
19
|
+
start: number;
|
|
20
|
+
end: number;
|
|
21
|
+
strategy: MatchStrategy;
|
|
22
|
+
/** Number of equally-valid locations found. `> 1` means ambiguous. */
|
|
23
|
+
matchCount: number;
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Finds `anchor` within `source` using deterministic strategies only.
|
|
27
|
+
*
|
|
28
|
+
* Returns `null` when no deterministic match exists. When a match exists,
|
|
29
|
+
* `matchCount` reports how many equivalent locations were found so callers can
|
|
30
|
+
* skip ambiguous edits.
|
|
31
|
+
*/
|
|
32
|
+
export declare function findAnchor(source: string, anchor: string): AnchorMatch | null;
|
package/dist/matching.js
ADDED
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Deterministic anchor matching for edit application.
|
|
4
|
+
*
|
|
5
|
+
* This module intentionally implements only deterministic strategies. It never
|
|
6
|
+
* guesses a location via similarity scoring; if a deterministic match cannot be
|
|
7
|
+
* found, callers skip the edit and report a warning.
|
|
8
|
+
*
|
|
9
|
+
* Strategies, in order:
|
|
10
|
+
* 1. `exact` — the anchor is a literal substring.
|
|
11
|
+
* 2. `line-normalized` — both sides split into lines and compared with
|
|
12
|
+
* trailing whitespace removed. This absorbs CRLF vs LF
|
|
13
|
+
* drift, a missing/extra trailing newline, and
|
|
14
|
+
* trailing-whitespace drift.
|
|
15
|
+
*
|
|
16
|
+
* Leading indentation drift is deliberately NOT handled (deferred).
|
|
17
|
+
*/
|
|
18
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
19
|
+
exports.findAnchor = findAnchor;
|
|
20
|
+
/**
|
|
21
|
+
* Splits text into lines while retaining each line's character offsets.
|
|
22
|
+
* A trailing newline produces a final empty line, matching `String.split("\n")`.
|
|
23
|
+
*/
|
|
24
|
+
function splitLinesWithOffsets(text) {
|
|
25
|
+
const lines = [];
|
|
26
|
+
if (text.length === 0)
|
|
27
|
+
return lines;
|
|
28
|
+
let start = 0;
|
|
29
|
+
// Loop is bounded: `start` always advances past the newline.
|
|
30
|
+
for (;;) {
|
|
31
|
+
const newlineIndex = text.indexOf("\n", start);
|
|
32
|
+
if (newlineIndex === -1) {
|
|
33
|
+
lines.push({ text: text.slice(start), start, end: text.length });
|
|
34
|
+
break;
|
|
35
|
+
}
|
|
36
|
+
lines.push({
|
|
37
|
+
text: text.slice(start, newlineIndex),
|
|
38
|
+
start,
|
|
39
|
+
end: newlineIndex,
|
|
40
|
+
});
|
|
41
|
+
start = newlineIndex + 1;
|
|
42
|
+
if (start === text.length) {
|
|
43
|
+
lines.push({ text: "", start, end: start });
|
|
44
|
+
break;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
return lines;
|
|
48
|
+
}
|
|
49
|
+
function countOccurrences(haystack, needle) {
|
|
50
|
+
if (needle.length === 0)
|
|
51
|
+
return 0;
|
|
52
|
+
let count = 0;
|
|
53
|
+
let index = haystack.indexOf(needle);
|
|
54
|
+
while (index !== -1) {
|
|
55
|
+
count++;
|
|
56
|
+
index = haystack.indexOf(needle, index + needle.length);
|
|
57
|
+
}
|
|
58
|
+
return count;
|
|
59
|
+
}
|
|
60
|
+
function findLineNormalized(source, anchor) {
|
|
61
|
+
const sourceLines = splitLinesWithOffsets(source);
|
|
62
|
+
const anchorLines = splitLinesWithOffsets(anchor);
|
|
63
|
+
// Trailing/leading blank lines carry no anchoring information.
|
|
64
|
+
while (anchorLines.length > 0 &&
|
|
65
|
+
anchorLines[anchorLines.length - 1].text.trim() === "") {
|
|
66
|
+
anchorLines.pop();
|
|
67
|
+
}
|
|
68
|
+
while (anchorLines.length > 0 && anchorLines[0].text.trim() === "") {
|
|
69
|
+
anchorLines.shift();
|
|
70
|
+
}
|
|
71
|
+
if (anchorLines.length === 0)
|
|
72
|
+
return null;
|
|
73
|
+
if (anchorLines.length > sourceLines.length)
|
|
74
|
+
return null;
|
|
75
|
+
const normalizedAnchor = anchorLines.map((line) => line.text.trimEnd());
|
|
76
|
+
const normalizedSource = sourceLines.map((line) => line.text.trimEnd());
|
|
77
|
+
let matchCount = 0;
|
|
78
|
+
let first = null;
|
|
79
|
+
for (let i = 0; i + normalizedAnchor.length <= sourceLines.length; i++) {
|
|
80
|
+
let isMatch = true;
|
|
81
|
+
for (let j = 0; j < normalizedAnchor.length; j++) {
|
|
82
|
+
if (normalizedSource[i + j] !== normalizedAnchor[j]) {
|
|
83
|
+
isMatch = false;
|
|
84
|
+
break;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
if (isMatch) {
|
|
88
|
+
matchCount++;
|
|
89
|
+
if (first === null) {
|
|
90
|
+
first = {
|
|
91
|
+
start: sourceLines[i].start,
|
|
92
|
+
end: sourceLines[i + normalizedAnchor.length - 1].end,
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
if (first === null)
|
|
98
|
+
return null;
|
|
99
|
+
return {
|
|
100
|
+
start: first.start,
|
|
101
|
+
end: first.end,
|
|
102
|
+
strategy: "line-normalized",
|
|
103
|
+
matchCount,
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
107
|
+
* Finds `anchor` within `source` using deterministic strategies only.
|
|
108
|
+
*
|
|
109
|
+
* Returns `null` when no deterministic match exists. When a match exists,
|
|
110
|
+
* `matchCount` reports how many equivalent locations were found so callers can
|
|
111
|
+
* skip ambiguous edits.
|
|
112
|
+
*/
|
|
113
|
+
function findAnchor(source, anchor) {
|
|
114
|
+
if (anchor.length === 0 || source.length === 0)
|
|
115
|
+
return null;
|
|
116
|
+
const exactIndex = source.indexOf(anchor);
|
|
117
|
+
if (exactIndex !== -1) {
|
|
118
|
+
return {
|
|
119
|
+
start: exactIndex,
|
|
120
|
+
end: exactIndex + anchor.length,
|
|
121
|
+
strategy: "exact",
|
|
122
|
+
matchCount: countOccurrences(source, anchor),
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
return findLineNormalized(source, anchor);
|
|
126
|
+
}
|
package/dist/prompt.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { EditFormat } from "./types";
|
|
2
|
-
export declare const PROMPT_TEMPLATE_VERSION =
|
|
2
|
+
export declare const PROMPT_TEMPLATE_VERSION = 2;
|
|
3
3
|
export declare const FORMAT_INSTRUCTIONS: Record<EditFormat, string>;
|
|
4
4
|
/**
|
|
5
5
|
* Assembles the full prompt by combining task, files context, and formatting instructions
|
package/dist/prompt.js
CHANGED
|
@@ -3,7 +3,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
3
3
|
exports.FORMAT_INSTRUCTIONS = exports.PROMPT_TEMPLATE_VERSION = void 0;
|
|
4
4
|
exports.assembleFullPrompt = assembleFullPrompt;
|
|
5
5
|
// Versions of the prompt template.
|
|
6
|
-
exports.PROMPT_TEMPLATE_VERSION =
|
|
6
|
+
exports.PROMPT_TEMPLATE_VERSION = 2;
|
|
7
7
|
// Template pieces
|
|
8
8
|
const TASK = (prompt) => `## Your Task\n\n${prompt}`;
|
|
9
9
|
const FILES = (filesContext) => `## Original Files\n\n${filesContext}`;
|
|
@@ -32,6 +32,7 @@ exports.FORMAT_INSTRUCTIONS = {
|
|
|
32
32
|
"// new code\n",
|
|
33
33
|
">>>>>>> REPLACE\n",
|
|
34
34
|
"```\n",
|
|
35
|
+
"Use the exact filename listed in the Original Files section. Copy the SEARCH text verbatim, including whitespace and indentation. Do not invent filenames.",
|
|
35
36
|
].join(""),
|
|
36
37
|
"diff-fenced": [
|
|
37
38
|
"## Formatting Instructions\n\n",
|
|
@@ -44,6 +45,7 @@ exports.FORMAT_INSTRUCTIONS = {
|
|
|
44
45
|
"// new code\n",
|
|
45
46
|
">>>>>>> REPLACE\n",
|
|
46
47
|
"```\n",
|
|
48
|
+
"Use the exact filename listed in the Original Files section. Copy the SEARCH text verbatim, including whitespace and indentation. Do not invent filenames.",
|
|
47
49
|
].join(""),
|
|
48
50
|
udiff: [
|
|
49
51
|
"## Formatting Instructions\n\n",
|
|
@@ -55,6 +57,7 @@ exports.FORMAT_INSTRUCTIONS = {
|
|
|
55
57
|
"-// line to be removed\n",
|
|
56
58
|
"+// line to be added\n",
|
|
57
59
|
"```\n",
|
|
60
|
+
"Use the exact filename listed in the Original Files section. Copy all context lines verbatim, including whitespace. Do not invent filenames; use the whole-file format when creating a new file.",
|
|
58
61
|
].join(""),
|
|
59
62
|
hybrid: [
|
|
60
63
|
"## Formatting Instructions\n\n",
|
package/dist/types.d.ts
CHANGED
|
@@ -1,5 +1,42 @@
|
|
|
1
1
|
import { VizFiles } from "@vizhub/viz-types";
|
|
2
2
|
export type EditFormat = "whole" | "diff" | "diff-fenced" | "udiff" | "hybrid";
|
|
3
|
+
/**
|
|
4
|
+
* A parsed edit that can be applied independently to a file.
|
|
5
|
+
* `diff`/`diff-fenced` edits are search/replace pairs; `udiff` edits are
|
|
6
|
+
* original/updated blocks.
|
|
7
|
+
*/
|
|
8
|
+
export interface DiffEdit {
|
|
9
|
+
kind: "diff";
|
|
10
|
+
fileName: string;
|
|
11
|
+
search: string;
|
|
12
|
+
replace: string;
|
|
13
|
+
}
|
|
14
|
+
export interface UdiffEdit {
|
|
15
|
+
kind: "udiff";
|
|
16
|
+
fileName: string;
|
|
17
|
+
original: string;
|
|
18
|
+
updated: string;
|
|
19
|
+
}
|
|
20
|
+
export type ParsedEdit = DiffEdit | UdiffEdit;
|
|
21
|
+
/**
|
|
22
|
+
* Reason an edit was skipped. Warnings strictly mean "something went wrong"
|
|
23
|
+
* or "this edit could not be applied" — successful normalized matches are not
|
|
24
|
+
* reported here.
|
|
25
|
+
*/
|
|
26
|
+
export type ApplyWarningCode = "FILE_NOT_FOUND" | "SEARCH_NOT_FOUND" | "HUNK_NOT_FOUND" | "AMBIGUOUS_SEARCH" | "AMBIGUOUS_FILE" | "EMPTY_SEARCH" | "NO_EDITS_PARSED";
|
|
27
|
+
export interface ApplyWarning {
|
|
28
|
+
code: ApplyWarningCode;
|
|
29
|
+
fileName?: string;
|
|
30
|
+
message: string;
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Result of best-effort edit application. `files` always contains the changes
|
|
34
|
+
* that could be applied safely; `warnings` describes everything skipped.
|
|
35
|
+
*/
|
|
36
|
+
export interface ApplyResult {
|
|
37
|
+
files: VizFiles;
|
|
38
|
+
warnings: ApplyWarning[];
|
|
39
|
+
}
|
|
3
40
|
export type LlmFunction = (prompt: string) => Promise<{
|
|
4
41
|
content: string;
|
|
5
42
|
generationId?: string;
|
|
@@ -20,4 +57,6 @@ export interface PerformAiEditResult {
|
|
|
20
57
|
outputTokens?: number;
|
|
21
58
|
promptTemplateVersion?: number;
|
|
22
59
|
rawResponse?: string;
|
|
60
|
+
/** Edits that were skipped because they could not be applied safely. */
|
|
61
|
+
warnings?: ApplyWarning[];
|
|
23
62
|
}
|