@jsonstudio/llms 0.6.753 → 0.6.802
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/conversion/compat/actions/apply-patch-fixer.d.ts +1 -0
- package/dist/conversion/compat/actions/apply-patch-fixer.js +30 -0
- package/dist/conversion/compat/actions/apply-patch-format-fixer.d.ts +1 -0
- package/dist/conversion/compat/actions/apply-patch-format-fixer.js +233 -0
- package/dist/conversion/compat/actions/index.d.ts +2 -0
- package/dist/conversion/compat/actions/index.js +2 -0
- package/dist/conversion/compat/profiles/chat-gemini.json +15 -15
- package/dist/conversion/compat/profiles/chat-glm.json +194 -194
- package/dist/conversion/compat/profiles/chat-iflow.json +199 -199
- package/dist/conversion/compat/profiles/chat-lmstudio.json +43 -43
- package/dist/conversion/compat/profiles/chat-qwen.json +20 -20
- package/dist/conversion/compat/profiles/responses-c4m.json +42 -42
- package/dist/conversion/compat/profiles/responses-output2choices-test.json +10 -9
- package/dist/conversion/hub/pipeline/context-limit.d.ts +13 -0
- package/dist/conversion/hub/pipeline/context-limit.js +55 -0
- package/dist/conversion/hub/pipeline/hub-pipeline.d.ts +6 -0
- package/dist/conversion/hub/pipeline/hub-pipeline.js +35 -0
- package/dist/conversion/shared/bridge-message-utils.d.ts +1 -0
- package/dist/conversion/shared/bridge-message-utils.js +7 -0
- package/dist/conversion/shared/bridge-policies.js +8 -8
- package/dist/conversion/shared/snapshot-hooks.js +54 -1
- package/dist/conversion/shared/tool-governor.js +18 -23
- package/dist/filters/special/response-tool-arguments-stringify.js +3 -22
- package/dist/router/virtual-router/engine-selection.js +49 -4
- package/dist/router/virtual-router/engine.d.ts +5 -0
- package/dist/router/virtual-router/engine.js +21 -0
- package/dist/tools/apply-patch/regression-capturer.d.ts +12 -0
- package/dist/tools/apply-patch/regression-capturer.js +112 -0
- package/dist/tools/apply-patch/structured.d.ts +20 -0
- package/dist/tools/apply-patch/structured.js +441 -0
- package/dist/tools/apply-patch/validator.d.ts +8 -0
- package/dist/tools/apply-patch/validator.js +616 -0
- package/dist/tools/apply-patch-structured.d.ts +1 -20
- package/dist/tools/apply-patch-structured.js +1 -277
- package/dist/tools/args-json.d.ts +1 -0
- package/dist/tools/args-json.js +175 -0
- package/dist/tools/exec-command/normalize.d.ts +17 -0
- package/dist/tools/exec-command/normalize.js +112 -0
- package/dist/tools/exec-command/regression-capturer.d.ts +11 -0
- package/dist/tools/exec-command/regression-capturer.js +144 -0
- package/dist/tools/exec-command/validator.d.ts +6 -0
- package/dist/tools/exec-command/validator.js +22 -0
- package/dist/tools/patch-args-normalizer.d.ts +15 -0
- package/dist/tools/patch-args-normalizer.js +472 -0
- package/dist/tools/patch-regression-capturer.d.ts +1 -0
- package/dist/tools/patch-regression-capturer.js +1 -0
- package/dist/tools/tool-registry.js +36 -541
- package/package.json +1 -1
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import * as fs from 'fs';
|
|
2
|
+
import * as path from 'path';
|
|
3
|
+
import * as os from 'os';
|
|
4
|
+
import { createHash } from 'crypto';
|
|
5
|
+
/**
|
|
6
|
+
* Captures apply_patch payload regressions into a golden samples structure.
|
|
7
|
+
*
|
|
8
|
+
* Trigger: apply_patch validation failure OR fixer failure
|
|
9
|
+
* Condition: non-context errors only (format/json/prefix/etc.)
|
|
10
|
+
*
|
|
11
|
+
* Default destination:
|
|
12
|
+
* ~/.routecodex/golden_samples/ci-regression/apply_patch/<error-type>/
|
|
13
|
+
*
|
|
14
|
+
* Optional repo destination (explicit opt-in):
|
|
15
|
+
* ROUTECODEX_APPLY_PATCH_REGRESSION_TO_REPO=1
|
|
16
|
+
* → <repoRoot>/samples/ci-goldens/_regressions/apply_patch/<error-type>/
|
|
17
|
+
*
|
|
18
|
+
* Explicit override:
|
|
19
|
+
* ROUTECODEX_APPLY_PATCH_REGRESSION_DIR=/abs/path
|
|
20
|
+
*/
|
|
21
|
+
const MAX_SAMPLES_PER_TYPE = 50;
|
|
22
|
+
function detectRepoRootFromCwd() {
|
|
23
|
+
try {
|
|
24
|
+
let dir = process.cwd();
|
|
25
|
+
for (let i = 0; i < 12; i += 1) {
|
|
26
|
+
const marker = path.join(dir, 'samples', 'ci-goldens');
|
|
27
|
+
const pkg = path.join(dir, 'package.json');
|
|
28
|
+
if (fs.existsSync(marker) && fs.existsSync(pkg)) {
|
|
29
|
+
try {
|
|
30
|
+
const raw = fs.readFileSync(pkg, 'utf-8');
|
|
31
|
+
const json = JSON.parse(raw);
|
|
32
|
+
if (json && typeof json === 'object' && String(json.name || '') === 'routecodex') {
|
|
33
|
+
return dir;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
catch {
|
|
37
|
+
// ignore parse errors
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
const parent = path.dirname(dir);
|
|
41
|
+
if (parent === dir)
|
|
42
|
+
break;
|
|
43
|
+
dir = parent;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
catch {
|
|
47
|
+
// ignore
|
|
48
|
+
}
|
|
49
|
+
return null;
|
|
50
|
+
}
|
|
51
|
+
function resolveSamplesRoot() {
|
|
52
|
+
const explicit = String(process?.env?.ROUTECODEX_APPLY_PATCH_REGRESSION_DIR || '').trim();
|
|
53
|
+
if (explicit) {
|
|
54
|
+
return path.resolve(explicit);
|
|
55
|
+
}
|
|
56
|
+
const toRepo = String(process?.env?.ROUTECODEX_APPLY_PATCH_REGRESSION_TO_REPO || '').trim() === '1';
|
|
57
|
+
if (toRepo) {
|
|
58
|
+
const repoRoot = detectRepoRootFromCwd();
|
|
59
|
+
if (repoRoot) {
|
|
60
|
+
return path.join(repoRoot, 'samples', 'ci-goldens', '_regressions', 'apply_patch');
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
return path.join(os.homedir(), '.routecodex', 'golden_samples', 'ci-regression', 'apply_patch');
|
|
64
|
+
}
|
|
65
|
+
function isContextError(error) {
|
|
66
|
+
if (!error)
|
|
67
|
+
return false;
|
|
68
|
+
const lower = error.toLowerCase();
|
|
69
|
+
return (lower.includes('failed to find context') ||
|
|
70
|
+
lower.includes('failed to find expected lines') ||
|
|
71
|
+
lower.includes('file not found') ||
|
|
72
|
+
lower.includes('no such file'));
|
|
73
|
+
}
|
|
74
|
+
function stableSampleId(sample) {
|
|
75
|
+
const key = `${String(sample.errorType || 'unknown')}:${String(sample.originalArgs ?? '')}:` +
|
|
76
|
+
`${String(sample.normalizedArgs ?? '')}:${String(sample.fixerResult ?? '')}:${String(sample.validationError ?? '')}`;
|
|
77
|
+
return createHash('sha1').update(key).digest('hex').slice(0, 16);
|
|
78
|
+
}
|
|
79
|
+
export function captureApplyPatchRegression(sample) {
|
|
80
|
+
try {
|
|
81
|
+
if (isContextError(sample.validationError))
|
|
82
|
+
return;
|
|
83
|
+
const root = resolveSamplesRoot();
|
|
84
|
+
const safeType = String(sample.errorType || 'unknown').replace(/[^a-z0-9-]/gi, '_');
|
|
85
|
+
const typeDir = path.join(root, safeType);
|
|
86
|
+
if (!fs.existsSync(typeDir)) {
|
|
87
|
+
fs.mkdirSync(typeDir, { recursive: true });
|
|
88
|
+
}
|
|
89
|
+
// Check limit (best-effort; keep runtime safe)
|
|
90
|
+
try {
|
|
91
|
+
const existing = fs.readdirSync(typeDir).filter((f) => f.endsWith('.json'));
|
|
92
|
+
if (existing.length >= MAX_SAMPLES_PER_TYPE)
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
catch {
|
|
96
|
+
// ignore
|
|
97
|
+
}
|
|
98
|
+
const id = `sample_${stableSampleId(sample)}`;
|
|
99
|
+
const file = path.join(typeDir, `${id}.json`);
|
|
100
|
+
if (fs.existsSync(file))
|
|
101
|
+
return;
|
|
102
|
+
const fullSample = {
|
|
103
|
+
id,
|
|
104
|
+
timestamp: new Date().toISOString(),
|
|
105
|
+
...sample
|
|
106
|
+
};
|
|
107
|
+
fs.writeFileSync(file, JSON.stringify(fullSample, null, 2), 'utf-8');
|
|
108
|
+
}
|
|
109
|
+
catch {
|
|
110
|
+
// Silently fail to avoid disrupting runtime
|
|
111
|
+
}
|
|
112
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
export type StructuredApplyPatchKind = 'insert_after' | 'insert_before' | 'replace' | 'delete' | 'create_file' | 'delete_file';
|
|
2
|
+
export interface StructuredApplyPatchChange {
|
|
3
|
+
file?: string;
|
|
4
|
+
kind: StructuredApplyPatchKind | string;
|
|
5
|
+
anchor?: string;
|
|
6
|
+
target?: string;
|
|
7
|
+
lines?: string[] | string;
|
|
8
|
+
use_anchor_indent?: boolean;
|
|
9
|
+
}
|
|
10
|
+
export interface StructuredApplyPatchPayload extends Record<string, unknown> {
|
|
11
|
+
instructions?: string;
|
|
12
|
+
file?: string;
|
|
13
|
+
changes: StructuredApplyPatchChange[];
|
|
14
|
+
}
|
|
15
|
+
export declare class StructuredApplyPatchError extends Error {
|
|
16
|
+
reason: string;
|
|
17
|
+
constructor(reason: string, message: string);
|
|
18
|
+
}
|
|
19
|
+
export declare function buildStructuredPatch(payload: StructuredApplyPatchPayload): string;
|
|
20
|
+
export declare function isStructuredApplyPatchPayload(candidate: unknown): candidate is StructuredApplyPatchPayload;
|
|
@@ -0,0 +1,441 @@
|
|
|
1
|
+
export class StructuredApplyPatchError extends Error {
|
|
2
|
+
reason;
|
|
3
|
+
constructor(reason, message) {
|
|
4
|
+
super(message);
|
|
5
|
+
this.reason = reason;
|
|
6
|
+
}
|
|
7
|
+
}
|
|
8
|
+
const SUPPORTED_KINDS = [
|
|
9
|
+
'insert_after',
|
|
10
|
+
'insert_before',
|
|
11
|
+
'replace',
|
|
12
|
+
'delete',
|
|
13
|
+
'create_file',
|
|
14
|
+
'delete_file'
|
|
15
|
+
];
|
|
16
|
+
const FILE_PATH_INVALID_RE = /[\r\n]/;
|
|
17
|
+
const decodeEscapedNewlinesIfObvious = (value) => {
|
|
18
|
+
if (!value)
|
|
19
|
+
return value;
|
|
20
|
+
if (value.includes('\n'))
|
|
21
|
+
return value;
|
|
22
|
+
const lower = value.toLowerCase();
|
|
23
|
+
const looksEscaped = value.includes('\\r\\n') ||
|
|
24
|
+
(value.includes('\\n') && /\\n[ \t]/.test(value)) ||
|
|
25
|
+
lower.includes('\\u000a') ||
|
|
26
|
+
lower.includes('\\u000d');
|
|
27
|
+
if (!looksEscaped) {
|
|
28
|
+
return value;
|
|
29
|
+
}
|
|
30
|
+
let out = value;
|
|
31
|
+
out = out.replace(/\\r\\n/g, '\n');
|
|
32
|
+
out = out.replace(/\\n/g, '\n');
|
|
33
|
+
out = out.replace(/\\r/g, '\n');
|
|
34
|
+
out = out.replace(/\\u000a/gi, '\n');
|
|
35
|
+
out = out.replace(/\\u000d/gi, '\n');
|
|
36
|
+
return out;
|
|
37
|
+
};
|
|
38
|
+
const toSafeString = (value, label) => {
|
|
39
|
+
const str = typeof value === 'string' ? value : '';
|
|
40
|
+
if (!str.trim()) {
|
|
41
|
+
throw new StructuredApplyPatchError('missing_field', `${label} is required`);
|
|
42
|
+
}
|
|
43
|
+
return str;
|
|
44
|
+
};
|
|
45
|
+
const normalizeFilePath = (raw, label) => {
|
|
46
|
+
let trimmed = raw.trim();
|
|
47
|
+
if (!trimmed) {
|
|
48
|
+
throw new StructuredApplyPatchError('invalid_file', `${label} must not be empty`);
|
|
49
|
+
}
|
|
50
|
+
if (FILE_PATH_INVALID_RE.test(trimmed)) {
|
|
51
|
+
const firstLine = trimmed.split(/[\r\n]/)[0]?.trim() ?? '';
|
|
52
|
+
if (!firstLine) {
|
|
53
|
+
throw new StructuredApplyPatchError('invalid_file', `${label} must be a single-line path`);
|
|
54
|
+
}
|
|
55
|
+
trimmed = firstLine;
|
|
56
|
+
}
|
|
57
|
+
return trimmed.replace(/\\/g, '/');
|
|
58
|
+
};
|
|
59
|
+
const readStringish = (value) => {
|
|
60
|
+
if (typeof value === 'string')
|
|
61
|
+
return value;
|
|
62
|
+
if (Array.isArray(value) && value.length === 1 && typeof value[0] === 'string')
|
|
63
|
+
return String(value[0]);
|
|
64
|
+
if (value && typeof value === 'object') {
|
|
65
|
+
const rec = value;
|
|
66
|
+
const nested = readStringish(rec.path ?? rec.file ?? rec.filename ?? rec.filepath);
|
|
67
|
+
if (nested)
|
|
68
|
+
return nested;
|
|
69
|
+
}
|
|
70
|
+
return undefined;
|
|
71
|
+
};
|
|
72
|
+
const splitTextIntoLines = (input) => {
|
|
73
|
+
const decoded = decodeEscapedNewlinesIfObvious(input);
|
|
74
|
+
const normalized = decoded.replace(/\r/g, '');
|
|
75
|
+
const parts = normalized.split('\n');
|
|
76
|
+
if (parts.length && parts[parts.length - 1] === '') {
|
|
77
|
+
parts.pop();
|
|
78
|
+
}
|
|
79
|
+
return parts.length ? parts : [''];
|
|
80
|
+
};
|
|
81
|
+
const normalizeLines = (value, label) => {
|
|
82
|
+
if (Array.isArray(value)) {
|
|
83
|
+
if (!value.length) {
|
|
84
|
+
return [];
|
|
85
|
+
}
|
|
86
|
+
const out = [];
|
|
87
|
+
for (const [idx, entry] of value.entries()) {
|
|
88
|
+
if (typeof entry !== 'string') {
|
|
89
|
+
if (entry === null || entry === undefined) {
|
|
90
|
+
out.push('');
|
|
91
|
+
continue;
|
|
92
|
+
}
|
|
93
|
+
out.push(String(entry));
|
|
94
|
+
continue;
|
|
95
|
+
}
|
|
96
|
+
// Preserve intentional whitespace
|
|
97
|
+
const normalized = entry.replace(/\r/g, '');
|
|
98
|
+
const decoded = decodeEscapedNewlinesIfObvious(normalized);
|
|
99
|
+
if (decoded.includes('\n')) {
|
|
100
|
+
out.push(...splitTextIntoLines(decoded));
|
|
101
|
+
}
|
|
102
|
+
else {
|
|
103
|
+
out.push(decoded);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
return out;
|
|
107
|
+
}
|
|
108
|
+
if (typeof value === 'string') {
|
|
109
|
+
return splitTextIntoLines(value);
|
|
110
|
+
}
|
|
111
|
+
if (value === null || value === undefined) {
|
|
112
|
+
throw new StructuredApplyPatchError('invalid_lines', `${label} must be an array of strings or a multi-line string`);
|
|
113
|
+
}
|
|
114
|
+
return [String(value)];
|
|
115
|
+
};
|
|
116
|
+
const buildContextLines = (raw) => splitTextIntoLines(raw).map((line) => ` ${line}`);
|
|
117
|
+
const buildPrefixedLines = (lines, prefix) => lines.map((line) => `${prefix}${line}`);
|
|
118
|
+
const findSubsequence = (haystack, needle) => {
|
|
119
|
+
if (!needle.length)
|
|
120
|
+
return -1;
|
|
121
|
+
outer: for (let i = 0; i + needle.length <= haystack.length; i += 1) {
|
|
122
|
+
for (let j = 0; j < needle.length; j += 1) {
|
|
123
|
+
if ((haystack[i + j] ?? '') !== (needle[j] ?? ''))
|
|
124
|
+
continue outer;
|
|
125
|
+
}
|
|
126
|
+
return i;
|
|
127
|
+
}
|
|
128
|
+
return -1;
|
|
129
|
+
};
|
|
130
|
+
const tryApplyToFileLines = (section, kindRaw, change, changeRec, index) => {
|
|
131
|
+
const lines = section.lines;
|
|
132
|
+
if (kindRaw === 'insert_after' || kindRaw === 'insert_before') {
|
|
133
|
+
const anchorSource = change.anchor ??
|
|
134
|
+
change.target ??
|
|
135
|
+
change.context ??
|
|
136
|
+
change.from ??
|
|
137
|
+
change.old;
|
|
138
|
+
const anchor = toSafeString(anchorSource, `changes[${index}].anchor`);
|
|
139
|
+
const anchorLines = splitTextIntoLines(anchor);
|
|
140
|
+
const anchorIndex = findSubsequence(lines, anchorLines);
|
|
141
|
+
if (anchorIndex < 0)
|
|
142
|
+
return false;
|
|
143
|
+
const linesSource = changeRec.lines ?? changeRec.text ?? changeRec.content ?? changeRec.body ?? changeRec.replacement;
|
|
144
|
+
const additions = normalizeLines(linesSource, `changes[${index}].lines`);
|
|
145
|
+
if (!additions.length) {
|
|
146
|
+
throw new StructuredApplyPatchError('invalid_lines', `changes[${index}].lines must include at least one line`);
|
|
147
|
+
}
|
|
148
|
+
const prepared = kindRaw === 'insert_after'
|
|
149
|
+
? applyAnchorIndent(additions, anchorLines, 'last', change.use_anchor_indent)
|
|
150
|
+
: applyAnchorIndent(additions, anchorLines, 'first', change.use_anchor_indent);
|
|
151
|
+
const insertAt = kindRaw === 'insert_after' ? anchorIndex + anchorLines.length : anchorIndex;
|
|
152
|
+
lines.splice(insertAt, 0, ...prepared);
|
|
153
|
+
return true;
|
|
154
|
+
}
|
|
155
|
+
if (kindRaw === 'replace' || kindRaw === 'delete') {
|
|
156
|
+
const targetSource = change.target ??
|
|
157
|
+
change.anchor ??
|
|
158
|
+
change.context ??
|
|
159
|
+
change.from ??
|
|
160
|
+
change.old;
|
|
161
|
+
const target = toSafeString(targetSource, `changes[${index}].target`);
|
|
162
|
+
const targetLines = splitTextIntoLines(target);
|
|
163
|
+
const targetIndex = findSubsequence(lines, targetLines);
|
|
164
|
+
if (targetIndex < 0)
|
|
165
|
+
return false;
|
|
166
|
+
if (kindRaw === 'delete') {
|
|
167
|
+
lines.splice(targetIndex, targetLines.length);
|
|
168
|
+
return true;
|
|
169
|
+
}
|
|
170
|
+
const linesSource = changeRec.lines ?? changeRec.text ?? changeRec.content ?? changeRec.body ?? changeRec.replacement;
|
|
171
|
+
let replacements;
|
|
172
|
+
if ((linesSource === null || linesSource === undefined) && typeof change.anchor === 'string' && typeof change.target === 'string') {
|
|
173
|
+
replacements = splitTextIntoLines(String(change.anchor));
|
|
174
|
+
}
|
|
175
|
+
else {
|
|
176
|
+
replacements = normalizeLines(linesSource, `changes[${index}].lines`);
|
|
177
|
+
}
|
|
178
|
+
lines.splice(targetIndex, targetLines.length, ...replacements);
|
|
179
|
+
return true;
|
|
180
|
+
}
|
|
181
|
+
return false;
|
|
182
|
+
};
|
|
183
|
+
const detectIndentFromAnchor = (anchorLines, mode) => {
|
|
184
|
+
const source = mode === 'first' ? anchorLines[0] ?? '' : anchorLines[anchorLines.length - 1] ?? '';
|
|
185
|
+
const match = source.match(/^(\s*)/);
|
|
186
|
+
return match ? match[1] ?? '' : '';
|
|
187
|
+
};
|
|
188
|
+
const applyAnchorIndent = (lines, anchorLines, position, enabled) => {
|
|
189
|
+
if (!enabled) {
|
|
190
|
+
return lines;
|
|
191
|
+
}
|
|
192
|
+
const indent = detectIndentFromAnchor(anchorLines, position);
|
|
193
|
+
if (!indent) {
|
|
194
|
+
return lines;
|
|
195
|
+
}
|
|
196
|
+
return lines.map((line) => {
|
|
197
|
+
if (!line.trim()) {
|
|
198
|
+
return line;
|
|
199
|
+
}
|
|
200
|
+
if (/^\s/.test(line)) {
|
|
201
|
+
return line;
|
|
202
|
+
}
|
|
203
|
+
return `${indent}${line}`;
|
|
204
|
+
});
|
|
205
|
+
};
|
|
206
|
+
export function buildStructuredPatch(payload) {
|
|
207
|
+
if (!payload || typeof payload !== 'object') {
|
|
208
|
+
throw new StructuredApplyPatchError('missing_payload', 'apply_patch arguments must be a JSON object');
|
|
209
|
+
}
|
|
210
|
+
if (!Array.isArray(payload.changes) || payload.changes.length === 0) {
|
|
211
|
+
throw new StructuredApplyPatchError('missing_changes', 'apply_patch requires a non-empty "changes" array');
|
|
212
|
+
}
|
|
213
|
+
const topLevelFile = typeof payload.file === 'string' && payload.file.trim()
|
|
214
|
+
? normalizeFilePath(payload.file, 'file')
|
|
215
|
+
: typeof payload.path === 'string' && String(payload.path).trim()
|
|
216
|
+
? normalizeFilePath(String(payload.path), 'path')
|
|
217
|
+
: typeof payload.filepath === 'string' && String(payload.filepath).trim()
|
|
218
|
+
? normalizeFilePath(String(payload.filepath), 'filepath')
|
|
219
|
+
: typeof payload.filename === 'string' && String(payload.filename).trim()
|
|
220
|
+
? normalizeFilePath(String(payload.filename), 'filename')
|
|
221
|
+
: undefined;
|
|
222
|
+
const sectionOrder = [];
|
|
223
|
+
const fileSections = new Map();
|
|
224
|
+
const ensureUpdateSection = (file) => {
|
|
225
|
+
const existing = fileSections.get(file);
|
|
226
|
+
if (existing) {
|
|
227
|
+
if (existing.type !== 'update') {
|
|
228
|
+
throw new StructuredApplyPatchError('invalid_change_sequence', `File "${file}" already marked as ${existing.type}`);
|
|
229
|
+
}
|
|
230
|
+
return existing;
|
|
231
|
+
}
|
|
232
|
+
const created = { type: 'update', hunks: [] };
|
|
233
|
+
sectionOrder.push(file);
|
|
234
|
+
fileSections.set(file, created);
|
|
235
|
+
return created;
|
|
236
|
+
};
|
|
237
|
+
for (const [index, change] of payload.changes.entries()) {
|
|
238
|
+
if (!change || typeof change !== 'object') {
|
|
239
|
+
throw new StructuredApplyPatchError('invalid_change', `Change at index ${index} must be an object`);
|
|
240
|
+
}
|
|
241
|
+
const changeRec = change;
|
|
242
|
+
const kindRaw = typeof change.kind === 'string' ? change.kind.trim().toLowerCase() : '';
|
|
243
|
+
if (!kindRaw) {
|
|
244
|
+
throw new StructuredApplyPatchError('invalid_change_kind', `Change at index ${index} is missing "kind"`);
|
|
245
|
+
}
|
|
246
|
+
if (!SUPPORTED_KINDS.includes(kindRaw)) {
|
|
247
|
+
throw new StructuredApplyPatchError('invalid_change_kind', `Unsupported change kind "${change.kind}" at index ${index}`);
|
|
248
|
+
}
|
|
249
|
+
const fileSource = readStringish(changeRec.file) ??
|
|
250
|
+
readStringish(changeRec.path) ??
|
|
251
|
+
readStringish(changeRec.filepath) ??
|
|
252
|
+
readStringish(changeRec.filename) ??
|
|
253
|
+
readStringish(changeRec.file_path);
|
|
254
|
+
const file = fileSource ? normalizeFilePath(fileSource, `changes[${index}].file`) : topLevelFile;
|
|
255
|
+
if (!file) {
|
|
256
|
+
throw new StructuredApplyPatchError('invalid_file', `Change at index ${index} is missing "file"`);
|
|
257
|
+
}
|
|
258
|
+
if (kindRaw === 'create_file') {
|
|
259
|
+
const linesSource = changeRec.lines ?? changeRec.text ?? changeRec.content ?? changeRec.body ?? changeRec.replacement;
|
|
260
|
+
const lines = normalizeLines(linesSource, `changes[${index}].lines`);
|
|
261
|
+
const existing = fileSections.get(file);
|
|
262
|
+
if (!existing) {
|
|
263
|
+
sectionOrder.push(file);
|
|
264
|
+
fileSections.set(file, { type: 'add', lines });
|
|
265
|
+
}
|
|
266
|
+
else if (existing.type === 'delete') {
|
|
267
|
+
// Common model behavior: delete_file + create_file for same path → treat as replace.
|
|
268
|
+
fileSections.set(file, { type: 'replace', lines });
|
|
269
|
+
}
|
|
270
|
+
else if (existing.type === 'add') {
|
|
271
|
+
// Idempotent: last create wins.
|
|
272
|
+
fileSections.set(file, { type: 'add', lines });
|
|
273
|
+
}
|
|
274
|
+
else if (existing.type === 'replace') {
|
|
275
|
+
fileSections.set(file, { type: 'replace', lines });
|
|
276
|
+
}
|
|
277
|
+
else {
|
|
278
|
+
// Existing updates imply file existed; represent as delete+add to keep a single executable path.
|
|
279
|
+
fileSections.set(file, { type: 'replace', lines });
|
|
280
|
+
}
|
|
281
|
+
continue;
|
|
282
|
+
}
|
|
283
|
+
if (kindRaw === 'delete_file') {
|
|
284
|
+
const existing = fileSections.get(file);
|
|
285
|
+
if (!existing) {
|
|
286
|
+
sectionOrder.push(file);
|
|
287
|
+
fileSections.set(file, { type: 'delete' });
|
|
288
|
+
continue;
|
|
289
|
+
}
|
|
290
|
+
if (existing.type === 'add') {
|
|
291
|
+
// create_file then delete_file → net no-op; drop the section entirely.
|
|
292
|
+
fileSections.delete(file);
|
|
293
|
+
const idx = sectionOrder.indexOf(file);
|
|
294
|
+
if (idx >= 0)
|
|
295
|
+
sectionOrder.splice(idx, 1);
|
|
296
|
+
continue;
|
|
297
|
+
}
|
|
298
|
+
// update/replace/delete then delete_file → net delete.
|
|
299
|
+
fileSections.set(file, { type: 'delete' });
|
|
300
|
+
continue;
|
|
301
|
+
}
|
|
302
|
+
// Common shape: replace with lines but no target → treat as full-file replacement.
|
|
303
|
+
if (kindRaw === 'replace') {
|
|
304
|
+
const targetSource = change.target ?? change.anchor ?? change.context ?? change.from ?? change.old;
|
|
305
|
+
const hasTarget = typeof targetSource === 'string' && targetSource.trim().length > 0;
|
|
306
|
+
const linesSource = changeRec.lines ?? changeRec.text ?? changeRec.content ?? changeRec.body ?? changeRec.replacement;
|
|
307
|
+
const hasReplacementBody = !(linesSource === null || linesSource === undefined);
|
|
308
|
+
if (!hasTarget && hasReplacementBody) {
|
|
309
|
+
const lines = normalizeLines(linesSource, `changes[${index}].lines`);
|
|
310
|
+
const existing = fileSections.get(file);
|
|
311
|
+
if (!existing) {
|
|
312
|
+
sectionOrder.push(file);
|
|
313
|
+
}
|
|
314
|
+
// Prefer a deterministic executable output: delete+add (replace).
|
|
315
|
+
fileSections.set(file, { type: 'replace', lines });
|
|
316
|
+
continue;
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
const existing = fileSections.get(file);
|
|
320
|
+
// Shape fix: allow create_file + subsequent structured edits by applying edits to the created content in-memory.
|
|
321
|
+
if (existing && (existing.type === 'add' || existing.type === 'replace')) {
|
|
322
|
+
const applied = tryApplyToFileLines(existing, kindRaw, change, changeRec, index);
|
|
323
|
+
if (applied) {
|
|
324
|
+
continue;
|
|
325
|
+
}
|
|
326
|
+
throw new StructuredApplyPatchError('invalid_change_sequence', `File "${file}" already marked as ${existing.type}`);
|
|
327
|
+
}
|
|
328
|
+
const section = ensureUpdateSection(file);
|
|
329
|
+
switch (kindRaw) {
|
|
330
|
+
case 'insert_after': {
|
|
331
|
+
const anchorSource = change.anchor ?? change.target ?? change.context ?? change.from ?? change.old;
|
|
332
|
+
const anchor = toSafeString(anchorSource, `changes[${index}].anchor`);
|
|
333
|
+
const anchorLines = splitTextIntoLines(anchor);
|
|
334
|
+
const linesSource = changeRec.lines ?? changeRec.text ?? changeRec.content ?? changeRec.body ?? changeRec.replacement;
|
|
335
|
+
const additions = normalizeLines(linesSource, `changes[${index}].lines`);
|
|
336
|
+
if (!additions.length) {
|
|
337
|
+
throw new StructuredApplyPatchError('invalid_lines', `changes[${index}].lines must include at least one line`);
|
|
338
|
+
}
|
|
339
|
+
const prepared = applyAnchorIndent(additions, anchorLines, 'last', change.use_anchor_indent);
|
|
340
|
+
const hunkBody = [...buildContextLines(anchor), ...buildPrefixedLines(prepared, '+')];
|
|
341
|
+
section.hunks.push(hunkBody);
|
|
342
|
+
break;
|
|
343
|
+
}
|
|
344
|
+
case 'insert_before': {
|
|
345
|
+
const anchorSource = change.anchor ?? change.target ?? change.context ?? change.from ?? change.old;
|
|
346
|
+
const anchor = toSafeString(anchorSource, `changes[${index}].anchor`);
|
|
347
|
+
const anchorLines = splitTextIntoLines(anchor);
|
|
348
|
+
const linesSource = changeRec.lines ?? changeRec.text ?? changeRec.content ?? changeRec.body ?? changeRec.replacement;
|
|
349
|
+
const additions = normalizeLines(linesSource, `changes[${index}].lines`);
|
|
350
|
+
if (!additions.length) {
|
|
351
|
+
throw new StructuredApplyPatchError('invalid_lines', `changes[${index}].lines must include at least one line`);
|
|
352
|
+
}
|
|
353
|
+
const prepared = applyAnchorIndent(additions, anchorLines, 'first', change.use_anchor_indent);
|
|
354
|
+
const hunkBody = [...buildPrefixedLines(prepared, '+'), ...buildContextLines(anchor)];
|
|
355
|
+
section.hunks.push(hunkBody);
|
|
356
|
+
break;
|
|
357
|
+
}
|
|
358
|
+
case 'replace': {
|
|
359
|
+
// 兼容仅提供 anchor 的 replace 形态:将 anchor 视为 target 以尽可能保留用户意图。
|
|
360
|
+
const targetSource = change.target ?? change.anchor ?? change.context ?? change.from ?? change.old;
|
|
361
|
+
const target = toSafeString(targetSource, `changes[${index}].target`);
|
|
362
|
+
const linesSource = changeRec.lines ?? changeRec.text ?? changeRec.content ?? changeRec.body ?? changeRec.replacement;
|
|
363
|
+
let replacements;
|
|
364
|
+
if ((linesSource === null || linesSource === undefined) && typeof change.anchor === 'string' && typeof change.target === 'string') {
|
|
365
|
+
// Common model mistake: provide { anchor: <new>, target: <old> } but omit lines.
|
|
366
|
+
// Treat anchor as replacement body (shape fix only).
|
|
367
|
+
replacements = splitTextIntoLines(String(change.anchor));
|
|
368
|
+
}
|
|
369
|
+
else {
|
|
370
|
+
replacements = normalizeLines(linesSource, `changes[${index}].lines`);
|
|
371
|
+
}
|
|
372
|
+
const hunkBody = [
|
|
373
|
+
...buildPrefixedLines(splitTextIntoLines(target), '-'),
|
|
374
|
+
...buildPrefixedLines(replacements, '+')
|
|
375
|
+
];
|
|
376
|
+
section.hunks.push(hunkBody);
|
|
377
|
+
break;
|
|
378
|
+
}
|
|
379
|
+
case 'delete': {
|
|
380
|
+
const targetSource = change.target ?? change.anchor ?? change.context ?? change.from ?? change.old;
|
|
381
|
+
const target = toSafeString(targetSource, `changes[${index}].target`);
|
|
382
|
+
const hunkBody = buildPrefixedLines(splitTextIntoLines(target), '-');
|
|
383
|
+
section.hunks.push(hunkBody);
|
|
384
|
+
break;
|
|
385
|
+
}
|
|
386
|
+
default: {
|
|
387
|
+
throw new StructuredApplyPatchError('invalid_change_kind', `Unsupported change kind "${change.kind}" at index ${index}`);
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
if (!sectionOrder.length) {
|
|
392
|
+
throw new StructuredApplyPatchError('missing_changes', 'apply_patch payload produced no file operations');
|
|
393
|
+
}
|
|
394
|
+
const lines = ['*** Begin Patch'];
|
|
395
|
+
for (const file of sectionOrder) {
|
|
396
|
+
const section = fileSections.get(file);
|
|
397
|
+
if (!section)
|
|
398
|
+
continue;
|
|
399
|
+
if (section.type === 'add') {
|
|
400
|
+
lines.push(`*** Add File: ${file}`);
|
|
401
|
+
for (const line of section.lines) {
|
|
402
|
+
lines.push(`+${line}`);
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
else if (section.type === 'delete') {
|
|
406
|
+
lines.push(`*** Delete File: ${file}`);
|
|
407
|
+
}
|
|
408
|
+
else if (section.type === 'replace') {
|
|
409
|
+
lines.push(`*** Delete File: ${file}`);
|
|
410
|
+
lines.push(`*** Add File: ${file}`);
|
|
411
|
+
for (const line of section.lines) {
|
|
412
|
+
lines.push(`+${line}`);
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
else {
|
|
416
|
+
lines.push(`*** Update File: ${file}`);
|
|
417
|
+
const hunks = section.hunks || [];
|
|
418
|
+
for (const hunk of hunks) {
|
|
419
|
+
// 结构化补丁仅负责生成统一 diff 形态,不对多段 hunk 做逻辑裁剪;
|
|
420
|
+
// 具体哪些 hunk 能成功应用由 apply_patch 客户端自行校验并返回错误信息。
|
|
421
|
+
for (const entry of hunk) {
|
|
422
|
+
if (!entry.startsWith('@@')) {
|
|
423
|
+
lines.push(entry);
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
lines.push('*** End Patch');
|
|
430
|
+
return lines.join('\n');
|
|
431
|
+
}
|
|
432
|
+
export function isStructuredApplyPatchPayload(candidate) {
|
|
433
|
+
if (!candidate || typeof candidate !== 'object') {
|
|
434
|
+
return false;
|
|
435
|
+
}
|
|
436
|
+
const record = candidate;
|
|
437
|
+
if (!Array.isArray(record.changes)) {
|
|
438
|
+
return false;
|
|
439
|
+
}
|
|
440
|
+
return true;
|
|
441
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export declare const looksLikePatch: (text?: string) => boolean;
|
|
2
|
+
export declare const normalizeApplyPatchText: (raw: string) => string;
|
|
3
|
+
export type ApplyPatchValidationResult = {
|
|
4
|
+
ok: boolean;
|
|
5
|
+
reason?: string;
|
|
6
|
+
normalizedArgs?: string;
|
|
7
|
+
};
|
|
8
|
+
export declare function validateApplyPatchArgs(argsString: string, rawArgs: unknown): ApplyPatchValidationResult;
|