@hheei/pi-apply-patch 0.0.0-stage → 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (40) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +8 -2
  3. package/THIRD_PARTY_NOTICES.md +3 -0
  4. package/dist/apply-patch/executor.d.ts +15 -0
  5. package/dist/apply-patch/executor.js +407 -0
  6. package/dist/apply-patch/fs.d.ts +37 -0
  7. package/dist/apply-patch/fs.js +186 -0
  8. package/dist/apply-patch/glyph.d.ts +8 -0
  9. package/dist/apply-patch/glyph.js +23 -0
  10. package/dist/apply-patch/index.d.ts +6 -0
  11. package/dist/apply-patch/index.js +5 -0
  12. package/dist/apply-patch/jsdiff-matching.d.ts +13 -0
  13. package/dist/apply-patch/jsdiff-matching.js +613 -0
  14. package/dist/apply-patch/jsdiff-worker.d.ts +1 -0
  15. package/dist/apply-patch/jsdiff-worker.js +18 -0
  16. package/dist/apply-patch/jsdiff.d.ts +9 -0
  17. package/dist/apply-patch/jsdiff.js +219 -0
  18. package/dist/apply-patch/outcome.d.ts +84 -0
  19. package/dist/apply-patch/outcome.js +1 -0
  20. package/dist/apply-patch/parser.d.ts +85 -0
  21. package/dist/apply-patch/parser.js +486 -0
  22. package/dist/apply-patch/paths.d.ts +5 -0
  23. package/dist/apply-patch/paths.js +23 -0
  24. package/dist/apply-patch/policy.d.ts +11 -0
  25. package/dist/apply-patch/policy.js +44 -0
  26. package/dist/apply-patch/presentation.d.ts +7 -0
  27. package/dist/apply-patch/presentation.js +127 -0
  28. package/dist/apply-patch/settings.d.ts +5 -0
  29. package/dist/apply-patch/settings.js +33 -0
  30. package/dist/apply-patch/tool.d.ts +18 -0
  31. package/dist/apply-patch/tool.js +178 -0
  32. package/dist/apply-patch/view.d.ts +34 -0
  33. package/dist/apply-patch/view.js +256 -0
  34. package/dist/apply-patch-tool.d.ts +1 -0
  35. package/dist/apply-patch-tool.js +1 -0
  36. package/dist/extension.d.ts +2 -0
  37. package/dist/extension.js +22 -0
  38. package/dist/index.d.ts +1 -0
  39. package/dist/index.js +1 -0
  40. package/package.json +54 -3
@@ -0,0 +1,486 @@
1
+ const BEGIN = "*** Begin Patch";
2
+ const END = "*** End Patch";
3
+ const ADD = "*** Add File: ";
4
+ const DELETE = "*** Delete File: ";
5
+ const UPDATE = "*** Update File: ";
6
+ const MOVE = "*** Move to: ";
7
+ const END_OF_FILE = "*** End of File";
8
+ export const MAX_V4A_PATCH_BYTES = 1_048_576;
9
+ export const MAX_V4A_OPERATIONS = 128;
10
+ export const MAX_V4A_HUNKS_PER_UPDATE = 128;
11
+ export const MAX_V4A_HUNK_LINES = 4_096;
12
+ export const MAX_V4A_PATH_BYTES = 4_096;
13
+ export const MAX_V4A_PATH_SEGMENT_BYTES = 255;
14
+ export function createV4aPreviewCursor() {
15
+ return { consumed: "", bytes: 0, operations: [], stopped: false };
16
+ }
17
+ export function previewV4aPatchPrefix(input, argsComplete = false, cursor) {
18
+ const state = cursor ?? createV4aPreviewCursor();
19
+ if (!input.startsWith(state.consumed)) {
20
+ state.consumed = "";
21
+ state.bytes = 0;
22
+ state.operations = [];
23
+ state.stopped = false;
24
+ }
25
+ if (state.stopped)
26
+ return freezePreview(state.operations);
27
+ const lines = splitLines(input.slice(state.consumed.length));
28
+ for (const [index, line] of lines.entries()) {
29
+ if (line === undefined)
30
+ break;
31
+ const encoded = `${line.text}${line.newline}`;
32
+ const size = Buffer.byteLength(encoded, "utf8");
33
+ if (state.bytes + size > MAX_V4A_PATCH_BYTES) {
34
+ state.stopped = true;
35
+ break;
36
+ }
37
+ const complete = line.newline !== "" || (argsComplete && index === lines.length - 1);
38
+ if (!complete)
39
+ break;
40
+ const applied = applyPreviewLine(state, line.text);
41
+ if (!applied) {
42
+ state.stopped = true;
43
+ break;
44
+ }
45
+ state.bytes += size;
46
+ state.consumed += encoded;
47
+ }
48
+ return freezePreview(state.operations);
49
+ }
50
+ /** Counts unique completed operation paths. Ignores payload lines and partial last lines. */
51
+ export function previewV4aPatchFileCount(input, cursor) {
52
+ if (cursor !== undefined)
53
+ return new Set(previewV4aPatchPrefix(input, false, cursor).map((operation) => operation.path))
54
+ .size;
55
+ const paths = new Set();
56
+ let last;
57
+ let offset = 0;
58
+ let bytes = 0;
59
+ while (offset < input.length) {
60
+ const newline = input.indexOf("\n", offset);
61
+ if (newline < 0)
62
+ break;
63
+ let text = input.slice(offset, newline);
64
+ if (text.endsWith("\r"))
65
+ text = text.slice(0, -1);
66
+ const size = newline + 1 - offset;
67
+ if (bytes + size > MAX_V4A_PATCH_BYTES || paths.size >= MAX_V4A_OPERATIONS)
68
+ break;
69
+ bytes += size;
70
+ offset = newline + 1;
71
+ if (text === END)
72
+ continue;
73
+ const header = parseHeader(text);
74
+ if (header !== undefined) {
75
+ if (!isPreviewablePath(header.path))
76
+ break;
77
+ last = header.path;
78
+ paths.add(header.path);
79
+ continue;
80
+ }
81
+ if (last !== undefined && text.startsWith(MOVE)) {
82
+ const moveTo = text.slice(MOVE.length);
83
+ if (!isPreviewablePath(moveTo))
84
+ break;
85
+ paths.delete(last);
86
+ paths.add(moveTo);
87
+ last = moveTo;
88
+ }
89
+ }
90
+ return paths.size;
91
+ }
92
+ function applyPreviewLine(state, text) {
93
+ if (text === BEGIN)
94
+ return true;
95
+ if (text === END)
96
+ return true;
97
+ const header = parseHeader(text);
98
+ if (header !== undefined) {
99
+ if (!isPreviewablePath(header.path) || state.operations.length >= MAX_V4A_OPERATIONS)
100
+ return false;
101
+ state.operations.push({ kind: header.kind, path: header.path, addedLines: 0, removedLines: 0 });
102
+ return true;
103
+ }
104
+ const current = state.operations.at(-1);
105
+ if (current === undefined)
106
+ return true;
107
+ if (current.kind === "update" && text.startsWith(MOVE)) {
108
+ const moveTo = text.slice(MOVE.length);
109
+ if (!isPreviewablePath(moveTo))
110
+ return false;
111
+ current.path = moveTo;
112
+ return true;
113
+ }
114
+ if (current.kind === "add") {
115
+ if (text === "")
116
+ return true;
117
+ if (!text.startsWith("+"))
118
+ return false;
119
+ if (text.slice(1).length > 0)
120
+ current.addedLines += 1;
121
+ return true;
122
+ }
123
+ if (current.kind === "delete")
124
+ return false;
125
+ if (text.startsWith("@@") || text.startsWith(" ") || text === "")
126
+ return true;
127
+ if (text.startsWith("+")) {
128
+ current.addedLines += 1;
129
+ return true;
130
+ }
131
+ if (text.startsWith("-")) {
132
+ current.removedLines += 1;
133
+ return true;
134
+ }
135
+ return false;
136
+ }
137
+ function freezePreview(operations) {
138
+ return Object.freeze(operations.map((operation) => Object.freeze({ ...operation })));
139
+ }
140
+ function isPreviewablePath(path) {
141
+ try {
142
+ assertPatchPath(path);
143
+ return true;
144
+ }
145
+ catch {
146
+ return false;
147
+ }
148
+ }
149
+ export function parseV4aPatch(input) {
150
+ return Object.freeze({ operations: Object.freeze([...parseV4aPatchOperations(input)]) });
151
+ }
152
+ /**
153
+ * Parses a V4A patch one complete operation at a time. Callers may present
154
+ * parsed operations before workspace validation or mutation begins.
155
+ */
156
+ export async function parseV4aPatchProgressively(input, onOperation) {
157
+ const operations = [];
158
+ for (const operation of parseV4aPatchOperations(input)) {
159
+ operations.push(operation);
160
+ await onOperation(operation, operations.length - 1);
161
+ }
162
+ return Object.freeze({ operations: Object.freeze(operations) });
163
+ }
164
+ function* parseV4aPatchOperations(input) {
165
+ if (Buffer.byteLength(input, "utf8") > MAX_V4A_PATCH_BYTES)
166
+ throw parseError(`patch exceeds ${MAX_V4A_PATCH_BYTES} byte limit`);
167
+ const lines = normalizeEnvelope(splitLines(input));
168
+ if (lines.length < 2 || lines[0]?.text !== BEGIN)
169
+ throw parseError("missing Begin Patch envelope", lines[0]?.number ?? 1);
170
+ let index = 1;
171
+ let operationCount = 0;
172
+ while (index < lines.length) {
173
+ const current = lines[index];
174
+ if (current === undefined)
175
+ throw parseError("unexpected end of patch", lines.at(-1)?.number);
176
+ if (current.text === END) {
177
+ if (operationCount === 0)
178
+ throw parseError("patch contains no actions", lines[0]?.number);
179
+ if (index + 1 !== lines.length)
180
+ throw parseError("content after End Patch envelope", lines[index + 1]?.number);
181
+ return;
182
+ }
183
+ if (operationCount >= MAX_V4A_OPERATIONS)
184
+ throw parseError(`patch exceeds ${MAX_V4A_OPERATIONS} operation limit`, current.number);
185
+ const header = parseHeader(current.text);
186
+ if (header === undefined)
187
+ throw parseError(`unknown patch header: ${current.text}`, current.number);
188
+ assertPatchPath(header.path, current.number);
189
+ if (header.kind === "add") {
190
+ const result = parseAdd(lines, index + 1, header.path);
191
+ operationCount += 1;
192
+ yield result.operation;
193
+ index = result.nextIndex;
194
+ continue;
195
+ }
196
+ if (header.kind === "delete") {
197
+ const nextIndex = index + 1;
198
+ if (nextIndex < lines.length && !isHeaderOrEnd(lines[nextIndex]?.text ?? ""))
199
+ throw parseError("Delete actions cannot contain body lines", lines[nextIndex]?.number);
200
+ operationCount += 1;
201
+ yield Object.freeze({ kind: "delete", path: header.path });
202
+ index = nextIndex;
203
+ continue;
204
+ }
205
+ const result = parseUpdate(lines, index + 1, header.path);
206
+ operationCount += 1;
207
+ yield result.operation;
208
+ index = result.nextIndex;
209
+ }
210
+ throw parseError("missing End Patch envelope", lines.at(-1)?.number);
211
+ }
212
+ export function operationTouchedPaths(operation) {
213
+ return operation.kind === "update" && operation.moveTo !== undefined
214
+ ? [operation.path, operation.moveTo]
215
+ : [operation.path];
216
+ }
217
+ /** Finds operation graph conflicts without reading or mutating the workspace. */
218
+ export function findV4aPatchConflicts(patch) {
219
+ const touched = new Map();
220
+ for (const [index, operation] of patch.operations.entries()) {
221
+ for (const path of operationTouchedPaths(operation)) {
222
+ const indices = touched.get(path) ?? [];
223
+ indices.push({ index, operation });
224
+ touched.set(path, indices);
225
+ }
226
+ }
227
+ const conflicts = [];
228
+ for (const [path, touches] of touched) {
229
+ const operationIndices = Object.freeze([...new Set(touches.map(({ index }) => index))]);
230
+ const repeatedWithinOperation = touches.length !== operationIndices.length;
231
+ const compatibleReplace = operationIndices.length === 2 &&
232
+ touches.every(({ operation }) => operation.kind === "add" || operation.kind === "delete") &&
233
+ new Set(touches.map(({ operation }) => operation.kind)).size === 2;
234
+ const compatibleUpdates = !repeatedWithinOperation &&
235
+ touches.every(({ operation }) => operation.kind === "update" && operation.moveTo === undefined);
236
+ if (!repeatedWithinOperation &&
237
+ (operationIndices.length < 2 || compatibleReplace || compatibleUpdates))
238
+ continue;
239
+ conflicts.push(Object.freeze({
240
+ path,
241
+ operationIndices,
242
+ message: `path touched more than once: ${path}`,
243
+ }));
244
+ }
245
+ return Object.freeze(conflicts);
246
+ }
247
+ export function compileV4aUpdateToUnifiedDiff(operation) {
248
+ if (operation.hunks.length === 0)
249
+ return "";
250
+ const oldPath = `a/${operation.path}`;
251
+ // Move is a separate Publish/unlink. Unified-diff headers must not turn
252
+ // an update into a rename; jsdiff never chooses the destination path.
253
+ const newPath = `b/${operation.path}`;
254
+ const out = [`--- ${oldPath}\n`, `+++ ${newPath}\n`];
255
+ for (const hunk of operation.hunks) {
256
+ const oldCount = hunk.lines.filter((line) => line.kind !== "add").length;
257
+ const newCount = hunk.lines.filter((line) => line.kind !== "remove").length;
258
+ const oldStart = oldCount === 0 ? 0 : 1;
259
+ const newStart = newCount === 0 ? 0 : 1;
260
+ out.push(`@@ -${oldStart},${oldCount} +${newStart},${newCount} @@${hunk.anchor ?? ""}\n`);
261
+ for (const line of hunk.lines) {
262
+ const prefix = line.kind === "add" ? "+" : line.kind === "remove" ? "-" : " ";
263
+ out.push(`${prefix}${line.text}`);
264
+ if (line.noNewline === true)
265
+ out.push("\\n");
266
+ }
267
+ }
268
+ return out.join("");
269
+ }
270
+ function parseAdd(lines, start, path) {
271
+ let index = start;
272
+ const content = [];
273
+ while (index < lines.length) {
274
+ const line = lines[index];
275
+ if (line === undefined)
276
+ throw parseError("unexpected end of add action");
277
+ if (line.text === BEGIN)
278
+ throw parseError("Begin Patch must appear only as the first line of the patch");
279
+ if (line.text === END && content.length === 0)
280
+ throw parseError("End Patch cannot appear before Add File content");
281
+ if (isHeaderOrEnd(line.text))
282
+ break;
283
+ if (line.text === "") {
284
+ content.push("", line.newline);
285
+ index += 1;
286
+ continue;
287
+ }
288
+ if (!line.text.startsWith("+"))
289
+ throw parseError("Add content lines must begin with +", line.number);
290
+ content.push(line.text.slice(1), line.newline);
291
+ index += 1;
292
+ }
293
+ if (content.length === 0)
294
+ throw parseError("Add action must contain content");
295
+ return {
296
+ operation: Object.freeze({ kind: "add", path, content: content.join("") }),
297
+ nextIndex: index,
298
+ };
299
+ }
300
+ function parseUpdate(lines, start, path) {
301
+ let index = start;
302
+ let moveTo;
303
+ const moveLine = lines[index];
304
+ if (moveLine?.text.startsWith(MOVE)) {
305
+ moveTo = moveLine.text.slice(MOVE.length);
306
+ assertPatchPath(moveTo, moveLine.number);
307
+ index += 1;
308
+ }
309
+ const hunks = [];
310
+ let current = {
311
+ anchors: [],
312
+ lines: [],
313
+ };
314
+ let hasChange = false;
315
+ while (index < lines.length) {
316
+ const line = lines[index];
317
+ if (line === undefined)
318
+ throw parseError("unexpected end of update action");
319
+ if (line.text === BEGIN)
320
+ throw parseError("Begin Patch must appear only as the first line of the patch");
321
+ if (line.text === END &&
322
+ current.lines.length === 0 &&
323
+ hunks.length === 0 &&
324
+ moveTo === undefined)
325
+ throw parseError("End Patch cannot appear before Update File body");
326
+ if (isHeaderOrEnd(line.text))
327
+ break;
328
+ if (line.text.startsWith(MOVE))
329
+ throw parseError("Move to must appear immediately after Update File", line.number);
330
+ if (current.endOfFile)
331
+ throw parseError("End of File must be the final update constraint", line.number);
332
+ if (line.text === "\") {
333
+ const previous = current.lines.at(-1);
334
+ if (previous === undefined)
335
+ throw parseError("No newline marker must follow update body lines", line.number);
336
+ current.lines[current.lines.length - 1] = Object.freeze({
337
+ ...previous,
338
+ noNewline: true,
339
+ });
340
+ index += 1;
341
+ continue;
342
+ }
343
+ if (line.text === END_OF_FILE) {
344
+ if (current.lines.length === 0)
345
+ throw parseError("End of File must follow update body lines", line.number);
346
+ current.endOfFile = true;
347
+ index += 1;
348
+ continue;
349
+ }
350
+ if (line.text.startsWith("@@")) {
351
+ const anchor = line.text.slice(2).trim();
352
+ if (current.lines.length > 0) {
353
+ hunks.push(freezeHunk(current));
354
+ current = { anchors: anchor.length === 0 ? [] : [anchor], lines: [] };
355
+ }
356
+ else if (anchor.length > 0)
357
+ current.anchors.push(anchor);
358
+ index += 1;
359
+ continue;
360
+ }
361
+ const parsedLine = parseUpdateLine(line);
362
+ if (parsedLine.kind !== "context")
363
+ hasChange = true;
364
+ current.lines.push(parsedLine);
365
+ index += 1;
366
+ }
367
+ if (current.lines.length > 0)
368
+ hunks.push(freezeHunk(current));
369
+ if (hunks.length > MAX_V4A_HUNKS_PER_UPDATE)
370
+ throw parseError(`Update action exceeds ${MAX_V4A_HUNKS_PER_UPDATE} hunk limit`);
371
+ if (hunks.some((hunk) => hunk.lines.length > MAX_V4A_HUNK_LINES))
372
+ throw parseError(`Update action exceeds ${MAX_V4A_HUNK_LINES} lines per hunk limit`);
373
+ if (hunks.length === 0 && moveTo === undefined)
374
+ throw parseError("Update action must contain body lines");
375
+ if (hunks.length > 0 && !hasChange)
376
+ throw parseError("Update action must change content");
377
+ const frozenHunks = Object.freeze([...hunks]);
378
+ return {
379
+ operation: Object.freeze(moveTo === undefined
380
+ ? { kind: "update", path, hunks: frozenHunks }
381
+ : { kind: "update", path, moveTo, hunks: frozenHunks }),
382
+ nextIndex: index,
383
+ };
384
+ }
385
+ function parseUpdateLine(line) {
386
+ if (line.text === "")
387
+ return Object.freeze({ kind: "context", text: line.newline });
388
+ const marker = line.text[0];
389
+ const text = `${line.text.slice(1)}${line.newline}`;
390
+ if (marker === " ")
391
+ return Object.freeze({ kind: "context", text });
392
+ if (marker === "+")
393
+ return Object.freeze({ kind: "add", text });
394
+ if (marker === "-")
395
+ return Object.freeze({ kind: "remove", text });
396
+ throw parseError("Update body lines must begin with space, +, -, or @@", line.number);
397
+ }
398
+ function freezeHunk(hunk) {
399
+ const lines = Object.freeze([...hunk.lines]);
400
+ const anchors = Object.freeze([...hunk.anchors]);
401
+ const anchor = anchors.at(-1);
402
+ return Object.freeze({
403
+ ...(anchor === undefined ? {} : { anchor }),
404
+ ...(anchors.length > 1 ? { anchors } : {}),
405
+ ...(hunk.endOfFile === undefined ? {} : { endOfFile: true }),
406
+ lines,
407
+ });
408
+ }
409
+ function parseHeader(text) {
410
+ if (text.startsWith(ADD))
411
+ return { kind: "add", path: text.slice(ADD.length) };
412
+ if (text.startsWith(DELETE))
413
+ return { kind: "delete", path: text.slice(DELETE.length) };
414
+ if (text.startsWith(UPDATE))
415
+ return { kind: "update", path: text.slice(UPDATE.length) };
416
+ return undefined;
417
+ }
418
+ function assertPatchPath(path, line) {
419
+ if (path.length === 0)
420
+ throw parseError("empty path in patch header", line);
421
+ if (path === BEGIN || path === END)
422
+ throw parseError("patch envelope markers cannot be used as file paths", line);
423
+ if (Buffer.byteLength(path, "utf8") > MAX_V4A_PATH_BYTES)
424
+ throw parseError(`path exceeds ${MAX_V4A_PATH_BYTES} byte limit`, line);
425
+ if (path
426
+ .split("/")
427
+ .some((segment) => Buffer.byteLength(segment, "utf8") > MAX_V4A_PATH_SEGMENT_BYTES))
428
+ throw parseError(`path segment exceeds ${MAX_V4A_PATH_SEGMENT_BYTES} byte limit`, line);
429
+ }
430
+ function isHeaderOrEnd(text) {
431
+ return text === END || text.startsWith(ADD) || text.startsWith(DELETE) || text.startsWith(UPDATE);
432
+ }
433
+ function splitLines(input) {
434
+ const matches = input.matchAll(/([^\r\n]*)(\r\n|\n|\r|$)/g);
435
+ const lines = [];
436
+ for (const match of matches) {
437
+ const rawText = match[1] ?? "";
438
+ const newline = match[2] ?? "";
439
+ const text = lines.length === 0 ? rawText.replace(/^\uFEFF/, "") : rawText;
440
+ if (text.length === 0 && newline.length === 0)
441
+ break;
442
+ lines.push({ text, newline, number: lines.length + 1 });
443
+ }
444
+ return lines;
445
+ }
446
+ function normalizeEnvelope(lines) {
447
+ let start = 0;
448
+ let end = lines.length;
449
+ while (start < end && lines[start]?.text === "")
450
+ start += 1;
451
+ while (end > start && lines[end - 1]?.text === "")
452
+ end -= 1;
453
+ let normalized = lines.slice(start, end);
454
+ const first = normalized[0]?.text;
455
+ const last = normalized[normalized.length - 1]?.text;
456
+ if ((first === "```" || first === "```patch" || first === "```diff") && last === "```") {
457
+ normalized = normalized.slice(1, -1);
458
+ start = 0;
459
+ end = normalized.length;
460
+ while (start < end && normalized[start]?.text === "")
461
+ start += 1;
462
+ while (end > start && normalized[end - 1]?.text === "")
463
+ end -= 1;
464
+ normalized = normalized.slice(start, end);
465
+ }
466
+ return keepOuterEnvelope(normalized);
467
+ }
468
+ function keepOuterEnvelope(lines) {
469
+ const start = lines.findIndex((line) => line.text === BEGIN);
470
+ let end = -1;
471
+ for (let index = lines.length - 1; index >= 0; index -= 1) {
472
+ if (lines[index]?.text === END) {
473
+ end = index;
474
+ break;
475
+ }
476
+ }
477
+ if (start < 0 || end <= start)
478
+ return lines;
479
+ const inner = lines
480
+ .slice(start + 1, end)
481
+ .filter((line) => line.text !== BEGIN && line.text !== END);
482
+ return [lines[start], ...inner, lines[end]];
483
+ }
484
+ function parseError(message, line) {
485
+ return new SyntaxError(`Invalid V4A patch: ${message}${line === undefined ? "" : ` at line ${line}`}`);
486
+ }
@@ -0,0 +1,5 @@
1
+ export declare function assertPatchPath(path: string): void;
2
+ /** Resolves relative paths from the workspace without following symlinks. */
3
+ export declare function resolvePatchPath(workspaceRoot: string, path: string): string;
4
+ /** Lexically detects confirmed local writes outside the workspace. */
5
+ export declare function isPatchPathOutsideWorkspace(workspaceRoot: string, path: string): boolean;
@@ -0,0 +1,23 @@
1
+ import { isAbsolute, relative, resolve, sep, win32 } from "node:path";
2
+ import { MAX_V4A_PATH_BYTES, MAX_V4A_PATH_SEGMENT_BYTES } from "./parser.js";
3
+ export function assertPatchPath(path) {
4
+ if (!path || path.includes("\0"))
5
+ throw new Error(`Invalid patch path: ${path}`);
6
+ if (Buffer.byteLength(path, "utf8") > MAX_V4A_PATH_BYTES)
7
+ throw new Error(`Patch path exceeds ${MAX_V4A_PATH_BYTES} byte limit: ${path}`);
8
+ const segments = path.split(/[\\/]/u);
9
+ if (segments.some((segment) => Buffer.byteLength(segment, "utf8") > MAX_V4A_PATH_SEGMENT_BYTES))
10
+ throw new Error(`Patch path segment exceeds ${MAX_V4A_PATH_SEGMENT_BYTES} byte limit: ${path}`);
11
+ }
12
+ /** Resolves relative paths from the workspace without following symlinks. */
13
+ export function resolvePatchPath(workspaceRoot, path) {
14
+ assertPatchPath(path);
15
+ return isAbsolute(path) || win32.isAbsolute(path) ? path : resolve(workspaceRoot, path);
16
+ }
17
+ /** Lexically detects confirmed local writes outside the workspace. */
18
+ export function isPatchPathOutsideWorkspace(workspaceRoot, path) {
19
+ const workspace = resolve(workspaceRoot);
20
+ const target = resolvePatchPath(workspace, path);
21
+ const fromWorkspace = relative(workspace, target);
22
+ return (fromWorkspace === ".." || fromWorkspace.startsWith(`..${sep}`) || isAbsolute(fromWorkspace));
23
+ }
@@ -0,0 +1,11 @@
1
+ import { type ExtensionSettingsPaths } from "@hheei/pi-ext-core";
2
+ export declare const APPLY_PATCH_SETTINGS_KEY = "applyPatch";
3
+ export interface ApplyPatchPolicy {
4
+ readonly fuzzFactor: number;
5
+ }
6
+ export interface LoadApplyPatchPolicyOptions {
7
+ readonly paths?: ExtensionSettingsPaths;
8
+ readonly signal?: AbortSignal;
9
+ }
10
+ export declare const DEFAULT_APPLY_PATCH_POLICY: ApplyPatchPolicy;
11
+ export declare function loadApplyPatchPolicy(options?: LoadApplyPatchPolicyOptions): Promise<ApplyPatchPolicy>;
@@ -0,0 +1,44 @@
1
+ import { defaultExtensionSettingsPaths, isRecord, readMergedJsonSettingsSection, } from "@hheei/pi-ext-core";
2
+ export const APPLY_PATCH_SETTINGS_KEY = "applyPatch";
3
+ const RETIRED_KEYS = ["minSimilarity", "maxConcurrentWorkers", "maxQueueDepth"];
4
+ export const DEFAULT_APPLY_PATCH_POLICY = {
5
+ fuzzFactor: 0,
6
+ };
7
+ function invalid(layer, key, reason) {
8
+ throw new Error(`${layer} setting ${APPLY_PATCH_SETTINGS_KEY}.${key} ${reason}`);
9
+ }
10
+ function readLayer(layer, value) {
11
+ if (value === undefined)
12
+ return {};
13
+ if (!isRecord(value))
14
+ invalid(layer, "<group>", "must be an object");
15
+ for (const key of Object.keys(value)) {
16
+ if (RETIRED_KEYS.includes(key))
17
+ invalid(layer, key, `is not supported; remove ${key} and set global ${APPLY_PATCH_SETTINGS_KEY}.fuzzFactor to 0 or 2`);
18
+ if (key !== "fuzzFactor" && key !== "enabled")
19
+ invalid(layer, key, "is not supported");
20
+ }
21
+ if (!Object.hasOwn(value, "fuzzFactor"))
22
+ return {};
23
+ const item = value.fuzzFactor;
24
+ if (typeof item !== "number" || !Number.isFinite(item))
25
+ invalid(layer, "fuzzFactor", "must be a finite number");
26
+ if (!Number.isInteger(item) || item < 0 || item > 2)
27
+ invalid(layer, "fuzzFactor", "must be an integer from 0 to 2");
28
+ return { fuzzFactor: item };
29
+ }
30
+ export async function loadApplyPatchPolicy(options = {}) {
31
+ const sections = await readMergedJsonSettingsSection({
32
+ paths: options.paths ?? defaultExtensionSettingsPaths(),
33
+ section: APPLY_PATCH_SETTINGS_KEY,
34
+ ...(options.signal === undefined ? {} : { signal: options.signal }),
35
+ });
36
+ const global = readLayer("global", sections.global);
37
+ const project = readLayer("project", sections.project);
38
+ const baseline = global.fuzzFactor ?? DEFAULT_APPLY_PATCH_POLICY.fuzzFactor;
39
+ if (project.fuzzFactor !== undefined && project.fuzzFactor > baseline)
40
+ invalid("project", "fuzzFactor", `must not exceed global value ${baseline}`);
41
+ return {
42
+ fuzzFactor: project.fuzzFactor ?? global.fuzzFactor ?? DEFAULT_APPLY_PATCH_POLICY.fuzzFactor,
43
+ };
44
+ }
@@ -0,0 +1,7 @@
1
+ import { type ToolPresentationProvider } from "@hheei/pi-ext-core";
2
+ /**
3
+ * The owned presentation of one apply_patch call or result. Facts come from the executor's
4
+ * persisted details, never from the current workspace; a result without those details keeps its
5
+ * model-visible text.
6
+ */
7
+ export declare const applyPatchPresentation: ToolPresentationProvider;