dsh-hashline-edit 0.2.1 → 0.2.2

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 (109) hide show
  1. package/README.md +60 -56
  2. package/cordis.patch.yml +3 -3
  3. package/lib/index.js +7224 -6454
  4. package/lib/types/dsh/contracts.d.ts +3 -12
  5. package/lib/types/dsh/fs.d.ts +0 -3
  6. package/lib/types/dsh/guidance.d.ts +5 -5
  7. package/lib/types/dsh/host-compat.d.ts +78 -7
  8. package/lib/types/dsh/index.d.ts +9 -21
  9. package/lib/types/dsh/metrics/events.d.ts +1 -1
  10. package/lib/types/dsh/metrics/observer.d.ts +1 -1
  11. package/lib/types/dsh/sandbox.d.ts +2 -2
  12. package/lib/types/dsh/shell-hook.d.ts +1 -1
  13. package/lib/types/dsh/tools/edit.d.ts +10 -0
  14. package/lib/types/dsh/tools/read.d.ts +3 -3
  15. package/lib/types/src/domain-errors.d.ts +2 -16
  16. package/lib/types/src/edit-tool.d.ts +41 -0
  17. package/lib/types/src/edit-undo.d.ts +2 -11
  18. package/lib/types/src/file-content/index.d.ts +1 -1
  19. package/lib/types/src/file-content/preview.d.ts +1 -1
  20. package/lib/types/src/hash-store.d.ts +2 -2
  21. package/lib/types/src/hashline/alphabet.d.ts +16 -1
  22. package/lib/types/src/hashline/hash-identity.d.ts +10 -4
  23. package/lib/types/src/mutation-engine/engine.d.ts +31 -0
  24. package/lib/types/src/mutation-engine/pipeline.d.ts +1 -8
  25. package/lib/types/src/mutation-engine/types.d.ts +50 -40
  26. package/lib/types/src/payload-contract.d.ts +135 -0
  27. package/lib/types/src/served-session/serve-render.d.ts +0 -6
  28. package/lib/types/src/undo-store.d.ts +1 -17
  29. package/package.json +21 -18
  30. package/prompts/edit-guidelines.md +10 -0
  31. package/prompts/edit-snippet.md +1 -0
  32. package/prompts/edit.md +1 -0
  33. package/prompts/read-guidelines.md +4 -4
  34. package/prompts/read.md +1 -1
  35. package/prompts/undo-last-edit-guidelines.md +2 -2
  36. package/prompts/undo-last-edit-snippet.md +1 -1
  37. package/prompts/undo-last-edit.md +1 -1
  38. package/src/bash-classifier.ts +1626 -0
  39. package/src/bash-conversion/decide.ts +812 -0
  40. package/src/bash-display-transform.ts +50 -0
  41. package/src/bash-multifile-config.ts +239 -0
  42. package/src/bash-view-config.ts +158 -0
  43. package/src/cut-repair.ts +11 -77
  44. package/src/domain-errors.ts +3 -35
  45. package/src/edit-normalize.ts +8 -0
  46. package/src/edit-render.ts +161 -0
  47. package/src/edit-tool.ts +118 -0
  48. package/src/edit-undo.ts +25 -379
  49. package/src/edit.ts +136 -0
  50. package/src/file-content/index.ts +1 -1
  51. package/src/file-content/preview.ts +1 -1
  52. package/src/file-content/served-header.ts +1 -1
  53. package/src/hash-store.ts +1 -18
  54. package/src/hashline/alphabet.ts +42 -1
  55. package/src/hashline/hash-identity.ts +12 -3
  56. package/src/integrations/pi-lens/command.ts +712 -0
  57. package/src/integrations/pi-lens/config.ts +148 -0
  58. package/src/integrations/pi-lens/io-bridge-adapter.ts +234 -0
  59. package/src/integrations/pi-lens/io-bridge.ts +112 -0
  60. package/src/integrations/pi-lens/settings-overlay.ts +272 -0
  61. package/src/lifecycle-hooks/index.ts +1992 -0
  62. package/src/lifecycle-hooks/types.ts +64 -0
  63. package/src/mutated-files.ts +75 -0
  64. package/src/mutation-engine/engine.ts +239 -0
  65. package/src/mutation-engine/index.ts +17 -0
  66. package/src/mutation-engine/pipeline.ts +27 -198
  67. package/src/mutation-engine/types.ts +57 -39
  68. package/src/mutation-queue.ts +1 -1
  69. package/src/payload-contract.ts +685 -0
  70. package/src/preview-controller.ts +84 -0
  71. package/src/prompts.ts +43 -10
  72. package/src/read.ts +865 -0
  73. package/src/search-hook.ts +41 -0
  74. package/src/served-session/serve-render.ts +2 -2
  75. package/src/store-config.ts +73 -0
  76. package/src/store-janitor-warning.ts +169 -0
  77. package/src/store-janitor.ts +539 -0
  78. package/src/store-kernel/settings.ts +233 -0
  79. package/src/store.ts +154 -0
  80. package/src/tui-presenter.ts +134 -0
  81. package/src/undo-store.ts +5 -74
  82. package/src/write-hook.ts +130 -0
  83. package/lib/types/dsh/hide.d.ts +0 -79
  84. package/lib/types/dsh/tools/apply-patch.d.ts +0 -38
  85. package/lib/types/src/apply-patch/commit.d.ts +0 -49
  86. package/lib/types/src/apply-patch/grammar.d.ts +0 -29
  87. package/lib/types/src/apply-patch/lifecycle.d.ts +0 -32
  88. package/lib/types/src/apply-patch/plan.d.ts +0 -50
  89. package/lib/types/src/apply-patch/render.d.ts +0 -51
  90. package/lib/types/src/apply-patch/resolve.d.ts +0 -129
  91. package/lib/types/src/apply-patch/tool.d.ts +0 -54
  92. package/lib/types/src/apply-patch/types.d.ts +0 -362
  93. package/lib/types/src/fs-lifecycle.d.ts +0 -44
  94. package/lib/types/src/host-utils.d.ts +0 -63
  95. package/lib/types/src/mutation-engine/edit-request.d.ts +0 -34
  96. package/prompts/apply-patch-guidelines.md +0 -10
  97. package/prompts/apply-patch.md +0 -1
  98. package/src/apply-patch/commit.ts +0 -190
  99. package/src/apply-patch/grammar.ts +0 -620
  100. package/src/apply-patch/index.ts +0 -17
  101. package/src/apply-patch/lifecycle.ts +0 -195
  102. package/src/apply-patch/plan.ts +0 -800
  103. package/src/apply-patch/render.ts +0 -137
  104. package/src/apply-patch/resolve.ts +0 -470
  105. package/src/apply-patch/tool.ts +0 -257
  106. package/src/apply-patch/types.ts +0 -387
  107. package/src/fs-lifecycle.ts +0 -201
  108. package/src/host-utils.ts +0 -224
  109. package/src/mutation-engine/edit-request.ts +0 -39
@@ -1,620 +0,0 @@
1
- /**
2
- * apply_patch v4 — grammar layer.
3
- *
4
- * Owns the Codex patch syntax plus the two extensions:
5
- *
6
- * - fragment mode: `@@` / `@@ <context>` + ' '/'+'/'-' body lines (+ optional
7
- * `*** End of File`),
8
- * - identity mode: `@@<a>` / `@@<a>-<b>` + '+' lines only (empty body = delete),
9
- * - reference hunks: `*** Copy From: <path> <a>-<b>` / `*** Cut From: ...`,
10
- * - file lifecycle: `*** Add File:`, `*** Delete File:`, `*** Move to:`.
11
- *
12
- * The layer is pure: it never touches the filesystem and never resolves anchors.
13
- * Every problem it reports carries the 1-based patch line, so the renderer can
14
- * print one rejection listing every syntax problem in the submission.
15
- *
16
- * @module dsh-hashline-edit/apply-patch/grammar
17
- */
18
- import { HASH_RE } from "../hashline/alphabet.js";
19
- import type {
20
- AddFileOp,
21
- AnchorSpan,
22
- DeleteFileOp,
23
- FragmentHunk,
24
- FragmentLine,
25
- HunkAddress,
26
- IdentityHunk,
27
- MoveDirective,
28
- ParsePatchResult,
29
- PatchDocument,
30
- PatchFileOp,
31
- PatchHunk,
32
- PatchProblem,
33
- ReferenceHunk,
34
- ReferenceSource,
35
- UpdateFileOp,
36
- } from "./types.js";
37
-
38
- /** Hard cap on one patch input (bytes), mirroring the host request budget. */
39
- export const PATCH_MAX_BYTES = 512 * 1024;
40
-
41
- /** Options for {@link parseApplyPatch}. */
42
- export interface ParsePatchOptions {
43
- /** Maximum accepted input size in bytes; defaults to {@link PATCH_MAX_BYTES}. */
44
- readonly maxBytes?: number;
45
- }
46
-
47
- const BEGIN_PATCH = "*** Begin Patch";
48
- const END_PATCH = "*** End Patch";
49
- const ADD_FILE = "*** Add File:";
50
- const DELETE_FILE = "*** Delete File:";
51
- const UPDATE_FILE = "*** Update File:";
52
- const MOVE_TO = "*** Move to:";
53
- const COPY_FROM = "*** Copy From:";
54
- const CUT_FROM = "*** Cut From:";
55
- const END_OF_FILE = "*** End of File";
56
-
57
- type ReferenceMode = "copy" | "cut";
58
-
59
- function syntaxProblem(patchLine: number, message: string, path?: string): PatchProblem {
60
- if (path === undefined) return { code: "E_PATCH_SYNTAX", patchLine, message };
61
- return { code: "E_PATCH_SYNTAX", patchLine, path, message };
62
- }
63
-
64
- function quote(line: string): string {
65
- return JSON.stringify(line);
66
- }
67
-
68
- /**
69
- * Split a patch into physical lines while keeping the 1-based line numbers the
70
- * diagnostics report. A UTF-8 BOM and CRLF endings are accepted; the final
71
- * newline does not create a phantom line.
72
- */
73
- function splitPatchLines(input: string): string[] {
74
- const text = input.charCodeAt(0) === 0xfeff ? input.slice(1) : input;
75
- const lines = text.split("\n");
76
- if (lines.length > 0 && lines[lines.length - 1] === "") lines.pop();
77
- return lines.map((line) => (line.endsWith("\r") ? line.slice(0, -1) : line));
78
- }
79
-
80
- /**
81
- * Parse one patch document.
82
- *
83
- * Success returns the AST defined in `types.ts`; failure returns every syntax
84
- * problem the parser could diagnose in one pass (never a throw for malformed
85
- * user input). Rejections use `E_PATCH_SYNTAX` with the offending patch line.
86
- *
87
- * @param input - the raw `*** Begin Patch` … `*** End Patch` text.
88
- * @param options - size guard knobs.
89
- * @returns the parsed document or the collected problems.
90
- */
91
- export function parseApplyPatch(input: string, options?: ParsePatchOptions): ParsePatchResult {
92
- const maxBytes = options?.maxBytes ?? PATCH_MAX_BYTES;
93
- if (new TextEncoder().encode(input).byteLength > maxBytes) {
94
- return {
95
- ok: false,
96
- problems: [syntaxProblem(1, `patch is larger than the ${maxBytes}-byte input limit`)],
97
- };
98
- }
99
- return new PatchParser(splitPatchLines(input)).parse();
100
- }
101
-
102
- /**
103
- * Parse an anchor token: `<a>` (single line) or `<a>-<b>` (closed span).
104
- * Returns `undefined` when the token is not a valid four-character anchor pair; the
105
- * grammar layer turns that into an `E_PATCH_SYNTAX` problem at the patch line.
106
- *
107
- * @param token - the text after `@@` (no leading space).
108
- * @returns the closed span, or undefined for a malformed token.
109
- */
110
- export function parseAnchorSpan(token: string): AnchorSpan | undefined {
111
- const parts = token.split("-");
112
- if (parts.length === 1) {
113
- const anchor = parts[0] ?? "";
114
- if (!HASH_RE.test(anchor)) return undefined;
115
- return { from: anchor, to: anchor };
116
- }
117
- if (parts.length === 2) {
118
- const from = parts[0] ?? "";
119
- const to = parts[1] ?? "";
120
- if (!HASH_RE.test(from) || !HASH_RE.test(to)) return undefined;
121
- return { from, to };
122
- }
123
- return undefined;
124
- }
125
-
126
- class PatchParser {
127
- private index = 0;
128
- private readonly problems: PatchProblem[] = [];
129
- private readonly files: PatchFileOp[] = [];
130
-
131
- constructor(private readonly lines: readonly string[]) {}
132
-
133
- parse(): ParsePatchResult {
134
- this.skipBlankLines();
135
- if (this.atEnd()) {
136
- return {
137
- ok: false,
138
- problems: [syntaxProblem(1, `patch is empty; expected '${BEGIN_PATCH}'`)],
139
- };
140
- }
141
- const first = this.lines[this.index] ?? "";
142
- if (first.trim() !== BEGIN_PATCH) {
143
- return {
144
- ok: false,
145
- problems: [
146
- syntaxProblem(
147
- this.index + 1,
148
- `the first non-empty line must be '${BEGIN_PATCH}', got ${quote(first)}`,
149
- ),
150
- ],
151
- };
152
- }
153
- this.index += 1;
154
-
155
- let sawEnd = false;
156
- while (!this.atEnd()) {
157
- const raw = this.lines[this.index] ?? "";
158
- const trimmed = raw.trim();
159
- if (trimmed === "") {
160
- this.index += 1;
161
- continue;
162
- }
163
- if (trimmed === END_PATCH) {
164
- sawEnd = true;
165
- this.index += 1;
166
- break;
167
- }
168
- if (trimmed.startsWith("*** ")) {
169
- const file = this.parseFileHeader(trimmed);
170
- if (file !== undefined) this.files.push(file);
171
- continue;
172
- }
173
- this.problem(
174
- this.index,
175
- `expected a file header ('*** Add File:', '*** Delete File:' or '*** Update File:'), got ${quote(raw)}`,
176
- );
177
- this.index += 1;
178
- }
179
-
180
- if (!sawEnd) {
181
- this.problem(this.lines.length, `missing '${END_PATCH}'`);
182
- } else {
183
- while (!this.atEnd()) {
184
- const raw = this.lines[this.index] ?? "";
185
- if (raw.trim() !== "") {
186
- this.problem(this.index, `unexpected content after '${END_PATCH}': ${quote(raw)}`);
187
- }
188
- this.index += 1;
189
- }
190
- }
191
-
192
- if (this.problems.length > 0) return { ok: false, problems: this.problems };
193
- const document: PatchDocument = { files: this.files, lineCount: this.lines.length };
194
- return { ok: true, document };
195
- }
196
-
197
- private atEnd(): boolean {
198
- return this.index >= this.lines.length;
199
- }
200
-
201
- private skipBlankLines(): void {
202
- while (!this.atEnd() && (this.lines[this.index] ?? "").trim() === "") this.index += 1;
203
- }
204
-
205
- private problem(zeroBasedIndex: number, message: string): void {
206
- this.problems.push(syntaxProblem(zeroBasedIndex + 1, message));
207
- }
208
-
209
- private parseFileHeader(trimmed: string): PatchFileOp | undefined {
210
- const line = this.index + 1;
211
- if (trimmed.startsWith(ADD_FILE)) return this.parseAddFile(line);
212
- if (trimmed.startsWith(DELETE_FILE)) return this.parseDeleteFile(line);
213
- if (trimmed.startsWith(UPDATE_FILE)) return this.parseUpdateFile(line);
214
- if (trimmed === BEGIN_PATCH) {
215
- this.problem(this.index, `duplicate '${BEGIN_PATCH}'`);
216
- this.index += 1;
217
- return undefined;
218
- }
219
- if (trimmed.startsWith(MOVE_TO) || trimmed === MOVE_TO) {
220
- this.problem(
221
- this.index,
222
- `'${MOVE_TO}' is only valid directly after '${UPDATE_FILE}' and before the first hunk`,
223
- );
224
- this.index += 1;
225
- return undefined;
226
- }
227
- if (trimmed.startsWith(COPY_FROM) || trimmed.startsWith(CUT_FROM)) {
228
- this.problem(
229
- this.index,
230
- `'${COPY_FROM}' / '${CUT_FROM}' must be preceded by an identity hunk header ('@@<a>' or '@@<a>-<b>')`,
231
- );
232
- this.index += 1;
233
- return undefined;
234
- }
235
- if (trimmed === END_OF_FILE) {
236
- this.problem(this.index, `'${END_OF_FILE}' is only valid at the end of a fragment hunk`);
237
- this.index += 1;
238
- return undefined;
239
- }
240
- this.problem(this.index, `unknown patch directive ${quote(trimmed)}`);
241
- this.index += 1;
242
- return undefined;
243
- }
244
-
245
- private headerPath(trimmedLine: string, marker: string, patchLine: number): string | undefined {
246
- const rest = trimmedLine.slice(marker.length);
247
- if (rest === "") {
248
- this.problem(patchLine - 1, `'${marker}' requires a non-empty path`);
249
- return undefined;
250
- }
251
- if (!rest.startsWith(" ")) {
252
- this.problem(patchLine - 1, `expected '${marker} <path>', got ${quote(trimmedLine)}`);
253
- return undefined;
254
- }
255
- const path = rest.trim();
256
- if (path === "") {
257
- this.problem(patchLine - 1, `'${marker}' requires a non-empty path`);
258
- return undefined;
259
- }
260
- return path;
261
- }
262
-
263
- private parseAddFile(headerLine: number): AddFileOp | undefined {
264
- const path = this.headerPath(this.lines[this.index]?.trim() ?? "", ADD_FILE, headerLine);
265
- this.index += 1;
266
- const contentLines: string[] = [];
267
- while (!this.atEnd()) {
268
- const raw = this.lines[this.index] ?? "";
269
- if (raw.trim().startsWith("*** ")) break;
270
- if (raw.startsWith("+")) {
271
- contentLines.push(raw.slice(1));
272
- this.index += 1;
273
- continue;
274
- }
275
- this.problem(this.index, `Add File content lines must start with '+', got ${quote(raw)}`);
276
- this.index += 1;
277
- }
278
- if (path === undefined) return undefined;
279
- return { kind: "add", path, headerLine, contentLines };
280
- }
281
-
282
- private parseDeleteFile(headerLine: number): DeleteFileOp | undefined {
283
- const path = this.headerPath(this.lines[this.index]?.trim() ?? "", DELETE_FILE, headerLine);
284
- this.index += 1;
285
- while (!this.atEnd()) {
286
- const raw = this.lines[this.index] ?? "";
287
- const trimmed = raw.trim();
288
- if (trimmed === "") {
289
- this.index += 1;
290
- continue;
291
- }
292
- if (trimmed.startsWith("*** ")) break;
293
- this.problem(this.index, `Delete File takes no body lines, got ${quote(raw)}`);
294
- this.index += 1;
295
- }
296
- if (path === undefined) return undefined;
297
- return { kind: "delete", path, headerLine };
298
- }
299
-
300
- private parseUpdateFile(headerLine: number): UpdateFileOp | undefined {
301
- const path = this.headerPath(this.lines[this.index]?.trim() ?? "", UPDATE_FILE, headerLine);
302
- this.index += 1;
303
- let moveTo: MoveDirective | undefined;
304
- let firstHunkSeen = false;
305
- const hunks: PatchHunk[] = [];
306
-
307
- while (!this.atEnd()) {
308
- const raw = this.lines[this.index] ?? "";
309
- const trimmed = raw.trim();
310
- if (trimmed === "") {
311
- this.index += 1;
312
- continue;
313
- }
314
- if (trimmed.startsWith("*** ")) {
315
- if (trimmed.startsWith(MOVE_TO) || trimmed === MOVE_TO) {
316
- const moveLine = this.index + 1;
317
- const to = this.headerPath(trimmed, MOVE_TO, moveLine);
318
- if (firstHunkSeen) {
319
- this.problem(
320
- this.index,
321
- `'${MOVE_TO}' must appear directly after '${UPDATE_FILE}' and before the first hunk`,
322
- );
323
- } else if (moveTo !== undefined) {
324
- this.problem(this.index, `duplicate '${MOVE_TO}' directive`);
325
- } else if (to !== undefined) {
326
- moveTo = { path: to, line: moveLine };
327
- }
328
- this.index += 1;
329
- continue;
330
- }
331
- if (trimmed === END_OF_FILE) {
332
- this.problem(this.index, `'${END_OF_FILE}' is only valid at the end of a fragment hunk`);
333
- this.index += 1;
334
- continue;
335
- }
336
- if (trimmed.startsWith(COPY_FROM) || trimmed.startsWith(CUT_FROM)) {
337
- this.problem(
338
- this.index,
339
- `'${COPY_FROM}' / '${CUT_FROM}' must be preceded by an identity hunk header ('@@<a>' or '@@<a>-<b>')`,
340
- );
341
- this.index += 1;
342
- continue;
343
- }
344
- break;
345
- }
346
- if (trimmed.startsWith("@@")) {
347
- const hunk = this.parseHunk();
348
- if (hunk !== undefined) hunks.push(hunk);
349
- firstHunkSeen = true;
350
- continue;
351
- }
352
- this.problem(
353
- this.index,
354
- `expected a hunk header '@@', '@@ <context>' or '@@<anchor>' before body lines, got ${quote(raw)}`,
355
- );
356
- this.index += 1;
357
- }
358
-
359
- if (path === undefined) return undefined;
360
- const op: UpdateFileOp = {
361
- kind: "update",
362
- path,
363
- headerLine,
364
- hunks,
365
- ...(moveTo === undefined ? {} : { moveTo }),
366
- };
367
- return op;
368
- }
369
-
370
- private parseHunk(): PatchHunk | undefined {
371
- const headerLine = this.index + 1;
372
- const header = this.lines[this.index] ?? "";
373
- if (header === "@@" || header.startsWith("@@ ")) {
374
- if (this.nextLineIsReference()) {
375
- this.rejectFragmentReference(headerLine);
376
- return undefined;
377
- }
378
- const address: HunkAddress =
379
- header === "@@" ? { kind: "start" } : { kind: "context", text: header.slice(3) };
380
- this.index += 1;
381
- return this.parseFragmentBody(address, headerLine);
382
- }
383
-
384
- const token = header.slice(2).trimEnd();
385
- const span = parseAnchorSpan(token);
386
- if (span === undefined) {
387
- this.problem(
388
- this.index,
389
- `invalid hunk header ${quote(header)}: expected '@@', '@@ <context>', '@@<anchor>' or '@@<anchor>-<anchor>'`,
390
- );
391
- this.index += 1;
392
- this.skipLooseBodyLines();
393
- return undefined;
394
- }
395
- const isReference = this.nextLineIsReference();
396
- this.index += 1;
397
- if (isReference) {
398
- return this.parseReferenceBody({ kind: "anchors", span }, headerLine);
399
- }
400
- return this.parseIdentityBody(span, headerLine);
401
- }
402
-
403
- private nextLineIsReference(): boolean {
404
- const next = this.lines[this.index + 1];
405
- if (next === undefined) return false;
406
- const trimmed = next.trim();
407
- return (
408
- trimmed === COPY_FROM ||
409
- trimmed.startsWith(`${COPY_FROM} `) ||
410
- trimmed === CUT_FROM ||
411
- trimmed.startsWith(`${CUT_FROM} `)
412
- );
413
- }
414
-
415
- private rejectFragmentReference(headerLine: number): void {
416
- this.problem(
417
- headerLine - 1,
418
- `reference hunks require identity addressing ('@@<a>' or '@@<a>-<b>'); fragment addressing is not allowed before '${COPY_FROM}' / '${CUT_FROM}'`,
419
- );
420
- this.index += 2;
421
- this.skipLooseBodyLines();
422
- }
423
-
424
- private skipLooseBodyLines(): void {
425
- while (!this.atEnd()) {
426
- const raw = this.lines[this.index] ?? "";
427
- if (raw.trim() === "") {
428
- this.index += 1;
429
- continue;
430
- }
431
- if (raw.startsWith("@@") || raw.startsWith("*** ")) return;
432
- this.index += 1;
433
- }
434
- }
435
-
436
- private parseFragmentBody(address: HunkAddress, headerLine: number): FragmentHunk {
437
- const lines: FragmentLine[] = [];
438
- let endOfFile: number | undefined;
439
- let ended = false;
440
- while (!this.atEnd()) {
441
- const raw = this.lines[this.index] ?? "";
442
- if (ended) {
443
- const trimmed = raw.trim();
444
- if (trimmed === "") {
445
- this.index += 1;
446
- continue;
447
- }
448
- if (raw.startsWith("@@") || raw.startsWith("*** ")) break;
449
- this.problem(this.index, `unexpected line after '${END_OF_FILE}': ${quote(raw)}`);
450
- this.index += 1;
451
- continue;
452
- }
453
- if (raw === "") {
454
- this.problem(
455
- this.index,
456
- `fragment hunk body lines must start with ' ', '-' or '+', got ${quote(raw)}`,
457
- );
458
- this.index += 1;
459
- continue;
460
- }
461
- if (raw.startsWith(" ")) {
462
- lines.push({ op: "context", text: raw.slice(1), line: this.index + 1 });
463
- this.index += 1;
464
- continue;
465
- }
466
- if (raw.startsWith("-")) {
467
- lines.push({ op: "remove", text: raw.slice(1), line: this.index + 1 });
468
- this.index += 1;
469
- continue;
470
- }
471
- if (raw.startsWith("+")) {
472
- lines.push({ op: "add", text: raw.slice(1), line: this.index + 1 });
473
- this.index += 1;
474
- continue;
475
- }
476
- if (raw.trimEnd() === END_OF_FILE) {
477
- endOfFile = this.index + 1;
478
- this.problemIfEmptyFragment(lines, endOfFile);
479
- ended = true;
480
- this.index += 1;
481
- continue;
482
- }
483
- if (raw.startsWith("@@") || raw.startsWith("*** ")) break;
484
- this.problem(
485
- this.index,
486
- `invalid fragment body line ${quote(raw)}: every body line must start with ' ', '-' or '+'`,
487
- );
488
- this.index += 1;
489
- }
490
- if (lines.length === 0 && endOfFile === undefined) {
491
- this.problem(headerLine - 1, "fragment hunk requires at least one body line");
492
- }
493
- return {
494
- kind: "fragment",
495
- address,
496
- headerLine,
497
- lines,
498
- ...(endOfFile === undefined ? {} : { endOfFile }),
499
- };
500
- }
501
-
502
- private problemIfEmptyFragment(lines: readonly FragmentLine[], markerLine: number): void {
503
- if (lines.length === 0) {
504
- this.problem(markerLine - 1, "fragment hunk requires at least one body line");
505
- }
506
- }
507
-
508
- private parseIdentityBody(span: AnchorSpan, headerLine: number): IdentityHunk {
509
- const addedLines: string[] = [];
510
- while (!this.atEnd()) {
511
- const raw = this.lines[this.index] ?? "";
512
- if (raw === "") break;
513
- if (raw.startsWith(" ")) {
514
- this.problem(
515
- this.index,
516
- `identity hunk only allows '+' body lines; found a context line ${quote(raw)}`,
517
- );
518
- this.index += 1;
519
- continue;
520
- }
521
- if (raw.startsWith("-")) {
522
- this.problem(
523
- this.index,
524
- `identity hunk only allows '+' body lines; found a '-' line ${quote(raw)} (an empty body deletes the span)`,
525
- );
526
- this.index += 1;
527
- continue;
528
- }
529
- if (raw.startsWith("+")) {
530
- addedLines.push(raw.slice(1));
531
- this.index += 1;
532
- continue;
533
- }
534
- if (raw.trimEnd() === END_OF_FILE) {
535
- this.problem(this.index, `'${END_OF_FILE}' is only valid in a fragment hunk`);
536
- this.index += 1;
537
- continue;
538
- }
539
- if (raw.startsWith("@@") || raw.startsWith("*** ")) break;
540
- this.problem(
541
- this.index,
542
- `invalid identity body line ${quote(raw)}: identity hunks allow only '+' lines`,
543
- );
544
- this.index += 1;
545
- }
546
- return { kind: "identity", span, headerLine, addedLines };
547
- }
548
-
549
- private parseReferenceBody(address: HunkAddress, headerLine: number): ReferenceHunk | undefined {
550
- if (address.kind !== "anchors") {
551
- this.problem(headerLine - 1, "reference hunks require identity addressing");
552
- return undefined;
553
- }
554
- const sourceLine = this.lines[this.index] ?? "";
555
- const sourceLineNumber = this.index + 1;
556
- const trimmed = sourceLine.trim();
557
- const mode: ReferenceMode = trimmed.startsWith(CUT_FROM) ? "cut" : "copy";
558
- const source = this.parseReferenceSource(trimmed, sourceLineNumber, mode);
559
- this.index += 1;
560
- while (!this.atEnd()) {
561
- const raw = this.lines[this.index] ?? "";
562
- if (raw.trim() === "") {
563
- this.index += 1;
564
- continue;
565
- }
566
- if (raw.startsWith("@@") || raw.startsWith("*** ")) break;
567
- this.problem(this.index, `reference hunk takes no body lines, got ${quote(raw)}`);
568
- this.index += 1;
569
- }
570
- if (source === undefined) return undefined;
571
- return { kind: "reference", address, source, headerLine };
572
- }
573
-
574
- private parseReferenceSource(
575
- trimmedLine: string,
576
- patchLine: number,
577
- mode: ReferenceMode,
578
- ): ReferenceSource | undefined {
579
- const marker = mode === "copy" ? COPY_FROM : CUT_FROM;
580
- const rest = trimmedLine.slice(marker.length);
581
- if (rest === "") {
582
- this.problem(
583
- patchLine - 1,
584
- `'${marker}' requires a source path and an '<anchor>-<anchor>' span`,
585
- );
586
- return undefined;
587
- }
588
- if (!rest.startsWith(" ")) {
589
- this.problem(
590
- patchLine - 1,
591
- `expected '${marker} <path> <anchor>-<anchor>', got ${quote(trimmedLine)}`,
592
- );
593
- return undefined;
594
- }
595
- const body = rest.trim();
596
- const split = body.lastIndexOf(" ");
597
- if (split <= 0) {
598
- this.problem(
599
- patchLine - 1,
600
- `expected '<path> <anchor>-<anchor>' after '${marker}' in ${quote(trimmedLine)}`,
601
- );
602
- return undefined;
603
- }
604
- const path = body.slice(0, split).trim();
605
- const token = body.slice(split + 1);
606
- if (path === "") {
607
- this.problem(patchLine - 1, `'${marker}' requires a non-empty source path`);
608
- return undefined;
609
- }
610
- const span = parseAnchorSpan(token);
611
- if (span === undefined || !token.includes("-")) {
612
- this.problem(
613
- patchLine - 1,
614
- `invalid reference span ${quote(token)}: expected '<anchor>-<anchor>'`,
615
- );
616
- return undefined;
617
- }
618
- return { mode, path, span, line: patchLine };
619
- }
620
- }
@@ -1,17 +0,0 @@
1
- /**
2
- * apply_patch v4 — public module surface.
3
- *
4
- * This barrel is the stable import point for the apply-patch lanes and the dsh
5
- * adapter. The individual modules keep their own boundaries; re-exporting them
6
- * here lets downstream code depend on one specifier and keeps the knip entry
7
- * graph honest while the feature is still being wired.
8
- *
9
- * @module dsh-hashline-edit/apply-patch
10
- */
11
- export * from "./types.js";
12
- export * from "./grammar.js";
13
- export * from "./resolve.js";
14
- export * from "./plan.js";
15
- export * from "./commit.js";
16
- export * from "./render.js";
17
- export * from "./tool.js";