dsh-hashline-edit 0.1.0 → 0.2.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.
Files changed (131) hide show
  1. package/README.md +72 -70
  2. package/cordis.patch.yml +1 -1
  3. package/lib/index.js +4494 -4813
  4. package/lib/types/dsh/contracts.d.ts +69 -6
  5. package/lib/types/dsh/fs.d.ts +38 -3
  6. package/lib/types/dsh/guidance.d.ts +5 -5
  7. package/lib/types/dsh/hide.d.ts +78 -0
  8. package/lib/types/dsh/host-compat.d.ts +7 -78
  9. package/lib/types/dsh/index.d.ts +17 -7
  10. package/lib/types/dsh/metrics/events.d.ts +8 -1
  11. package/lib/types/dsh/metrics/instrument.d.ts +20 -14
  12. package/lib/types/dsh/metrics/observer.d.ts +28 -0
  13. package/lib/types/dsh/sandbox.d.ts +2 -2
  14. package/lib/types/dsh/shell-hook.d.ts +17 -8
  15. package/lib/types/dsh/shell-view.d.ts +65 -3
  16. package/lib/types/dsh/tools/apply-patch.d.ts +38 -0
  17. package/lib/types/dsh/tools/context.d.ts +19 -1
  18. package/lib/types/dsh/tools/output.d.ts +32 -4
  19. package/lib/types/dsh/tools/read.d.ts +12 -15
  20. package/lib/types/dsh/tools/undo.d.ts +5 -0
  21. package/lib/types/dsh/write-hook.d.ts +8 -22
  22. package/lib/types/src/apply-patch/commit.d.ts +49 -0
  23. package/lib/types/src/apply-patch/grammar.d.ts +29 -0
  24. package/lib/types/src/apply-patch/lifecycle.d.ts +32 -0
  25. package/lib/types/src/apply-patch/plan.d.ts +50 -0
  26. package/lib/types/src/apply-patch/render.d.ts +51 -0
  27. package/lib/types/src/apply-patch/resolve.d.ts +129 -0
  28. package/lib/types/src/apply-patch/tool.d.ts +54 -0
  29. package/lib/types/src/apply-patch/types.d.ts +362 -0
  30. package/lib/types/src/bash-view-banner.d.ts +20 -0
  31. package/lib/types/src/domain-errors.d.ts +16 -2
  32. package/lib/types/src/edit-undo.d.ts +11 -2
  33. package/lib/types/src/file-content/detection.d.ts +17 -0
  34. package/lib/types/src/file-content/index.d.ts +2 -2
  35. package/lib/types/src/file-content/preview.d.ts +1 -1
  36. package/lib/types/src/fs-lifecycle.d.ts +44 -0
  37. package/lib/types/src/hashline/resolve.d.ts +6 -0
  38. package/lib/types/src/host-utils.d.ts +63 -0
  39. package/lib/types/src/mutation-engine/batch-span-gate.d.ts +6 -0
  40. package/lib/types/src/mutation-engine/edit-request.d.ts +34 -0
  41. package/lib/types/src/mutation-engine/pipeline.d.ts +18 -3
  42. package/lib/types/src/mutation-engine/types.d.ts +50 -58
  43. package/lib/types/src/read-expansion/grammar-manifest.d.ts +4 -2
  44. package/lib/types/src/read-expansion/java-decoration.d.ts +21 -0
  45. package/lib/types/src/read-expansion/span-expansion.d.ts +6 -2
  46. package/lib/types/src/read-expansion/span-kind.d.ts +3 -2
  47. package/lib/types/src/read-expansion/vue-decoration.d.ts +14 -0
  48. package/lib/types/src/served-session/serve-render.d.ts +9 -3
  49. package/lib/types/src/undo-store.d.ts +17 -1
  50. package/package.json +10 -22
  51. package/prompts/apply-patch-guidelines.md +10 -0
  52. package/prompts/apply-patch.md +1 -0
  53. package/prompts/read-guidelines.md +5 -5
  54. package/prompts/read.md +1 -1
  55. package/prompts/undo-last-edit-guidelines.md +2 -2
  56. package/prompts/undo-last-edit-snippet.md +1 -1
  57. package/prompts/undo-last-edit.md +1 -1
  58. package/src/apply-patch/commit.ts +190 -0
  59. package/src/apply-patch/grammar.ts +620 -0
  60. package/src/apply-patch/index.ts +17 -0
  61. package/src/apply-patch/lifecycle.ts +195 -0
  62. package/src/apply-patch/plan.ts +800 -0
  63. package/src/apply-patch/render.ts +137 -0
  64. package/src/apply-patch/resolve.ts +470 -0
  65. package/src/apply-patch/tool.ts +257 -0
  66. package/src/apply-patch/types.ts +387 -0
  67. package/src/cut-repair.ts +77 -11
  68. package/src/domain-errors.ts +58 -10
  69. package/src/edit-undo.ts +379 -25
  70. package/src/file-content/detection.ts +34 -0
  71. package/src/file-content/index.ts +2 -2
  72. package/src/file-content/preview.ts +1 -1
  73. package/src/file-content/served-header.ts +1 -1
  74. package/src/fs-lifecycle.ts +201 -0
  75. package/src/hash-store.ts +18 -1
  76. package/src/hashline/lease-resolve.ts +14 -8
  77. package/src/hashline/resolve.ts +30 -27
  78. package/src/host-utils.ts +224 -0
  79. package/src/mutation-engine/batch-span-gate.ts +17 -4
  80. package/src/mutation-engine/edit-request.ts +39 -0
  81. package/src/mutation-engine/pipeline.ts +535 -63
  82. package/src/mutation-engine/types.ts +49 -65
  83. package/src/mutation-queue.ts +1 -1
  84. package/src/prompts.ts +0 -28
  85. package/src/read-expansion/ast-provider.ts +4 -0
  86. package/src/read-expansion/grammar-manifest.ts +21 -2
  87. package/src/read-expansion/java-decoration.ts +34 -0
  88. package/src/read-expansion/span-expansion.ts +6 -2
  89. package/src/read-expansion/span-kind.ts +3 -1
  90. package/src/read-expansion/vue-decoration.ts +43 -0
  91. package/src/served-session/serve-render.ts +5 -5
  92. package/src/undo-store.ts +74 -5
  93. package/lib/types/dsh/encoding-bridge.d.ts +0 -25
  94. package/lib/types/dsh/encoding.d.ts +0 -144
  95. package/lib/types/dsh/tools/edit.d.ts +0 -10
  96. package/lib/types/src/edit-tool.d.ts +0 -41
  97. package/lib/types/src/mutation-engine/engine.d.ts +0 -31
  98. package/lib/types/src/payload-contract.d.ts +0 -124
  99. package/prompts/edit-guidelines.md +0 -10
  100. package/prompts/edit-snippet.md +0 -1
  101. package/prompts/edit.md +0 -1
  102. package/src/bash-classifier.ts +0 -1626
  103. package/src/bash-conversion/decide.ts +0 -812
  104. package/src/bash-display-transform.ts +0 -50
  105. package/src/bash-multifile-config.ts +0 -239
  106. package/src/bash-view-config.ts +0 -158
  107. package/src/edit-normalize.ts +0 -8
  108. package/src/edit-render.ts +0 -161
  109. package/src/edit-tool.ts +0 -116
  110. package/src/edit.ts +0 -136
  111. package/src/integrations/pi-lens/command.ts +0 -712
  112. package/src/integrations/pi-lens/config.ts +0 -148
  113. package/src/integrations/pi-lens/io-bridge-adapter.ts +0 -234
  114. package/src/integrations/pi-lens/io-bridge.ts +0 -112
  115. package/src/integrations/pi-lens/settings-overlay.ts +0 -272
  116. package/src/lifecycle-hooks/index.ts +0 -1992
  117. package/src/lifecycle-hooks/types.ts +0 -64
  118. package/src/mutated-files.ts +0 -75
  119. package/src/mutation-engine/engine.ts +0 -173
  120. package/src/mutation-engine/index.ts +0 -17
  121. package/src/payload-contract.ts +0 -637
  122. package/src/preview-controller.ts +0 -84
  123. package/src/read.ts +0 -865
  124. package/src/search-hook.ts +0 -41
  125. package/src/store-config.ts +0 -73
  126. package/src/store-janitor-warning.ts +0 -169
  127. package/src/store-janitor.ts +0 -539
  128. package/src/store-kernel/settings.ts +0 -233
  129. package/src/store.ts +0 -154
  130. package/src/tui-presenter.ts +0 -134
  131. package/src/write-hook.ts +0 -130
@@ -0,0 +1,257 @@
1
+ /**
2
+ * apply_patch v4 — tool use case.
3
+ *
4
+ * The deep module the dsh host adapter calls. One `execute` owns the whole pipeline for one call:
5
+ *
6
+ * grammar.parseApplyPatch → plan.planApplyPatch → commit.commitPlan → render
7
+ *
8
+ * and returns the model-facing `{ content, details }` envelope. A rejection carries the rendered
9
+ * `[E_*]` envelope and is marked `isError`; nothing was written because plan/grammar own every
10
+ * pre-commit check and commit rolls back or reports its recovery report.
11
+ *
12
+ * The orchestration deps are injectable so the transaction lane can test the complete
13
+ * parse→plan→commit→render path before the grammar/resolve lanes merge into this branch.
14
+ *
15
+ * @module dsh-hashline-edit/apply-patch/tool
16
+ */
17
+ import { parseApplyPatch, type ParsePatchOptions } from "./grammar.js";
18
+ import { planApplyPatch, type PlanApplyPatchOptions } from "./plan.js";
19
+ import { commitPlan, type CommitPlanOptions, type CommitPlanResult } from "./commit.js";
20
+ import { renderFailure, renderSuccess } from "./render.js";
21
+ import type {
22
+ ApplyPatchContext,
23
+ ApplyPatchFailure,
24
+ ApplyPatchSuccess,
25
+ ApplyPatchToolResult,
26
+ AppliedFileSummary,
27
+ PatchPlan,
28
+ PatchProblem,
29
+ PlanPatchResult,
30
+ ServedFileBlock,
31
+ } from "./types.js";
32
+ import { attachEnvelope } from "../error-envelope.js";
33
+ import type { DomainErrorCode } from "../domain-errors.js";
34
+ import { DomainError } from "../domain-errors.js";
35
+ import { decodeNormText } from "../file-content/index.js";
36
+ import { defaultHashIdentity } from "../hashline/hash-identity.js";
37
+ import { loadHashStore } from "../hash-store.js";
38
+ import { reportDiagnostic } from "../diagnostics.js";
39
+ import { createSessionHandle } from "../served-session/session.js";
40
+ import { snapshotHashFor, snapshotIOFor } from "../snapshot-store/index.js";
41
+
42
+ /** The registered tool name; `apply_patch` is the only mutation entry point. */
43
+ export const APPLY_PATCH_TOOL_NAME = "apply_patch";
44
+
45
+ /** The use-case seam: one narrow interface over the pipeline. */
46
+ export interface ApplyPatchTool {
47
+ /**
48
+ * Execute one apply_patch call.
49
+ * @param input - the raw patch text.
50
+ * @param ctx - live cwd, session identity and cancellation.
51
+ * @returns the model-facing envelope; `isError` marks a rejection (nothing written).
52
+ */
53
+ execute(input: string, ctx: ApplyPatchContext): Promise<ApplyPatchToolResult>;
54
+
55
+ /**
56
+ * Parse + plan without writing, for previews and tests.
57
+ * @param input - the raw patch text.
58
+ * @param ctx - live cwd, session identity and cancellation.
59
+ * @returns the plan or every collected problem.
60
+ */
61
+ plan(input: string, ctx: ApplyPatchContext): Promise<PlanPatchResult>;
62
+ }
63
+
64
+ /** The injectable seams of the pipeline; production uses the real modules. */
65
+ export interface ApplyPatchToolDeps {
66
+ readonly parse?: (
67
+ input: string,
68
+ options?: ParsePatchOptions,
69
+ ) => ReturnType<typeof parseApplyPatch>;
70
+ readonly plan?: (
71
+ document: Parameters<typeof planApplyPatch>[0],
72
+ options: PlanApplyPatchOptions,
73
+ ) => ReturnType<typeof planApplyPatch>;
74
+ readonly commit?: (plan: PatchPlan, options: CommitPlanOptions) => Promise<CommitPlanResult>;
75
+ readonly renderSuccess?: typeof renderSuccess;
76
+ readonly renderFailure?: typeof renderFailure;
77
+ }
78
+
79
+ /** Aggregate a pre-commit rejection into the shared failure shape (no served rows at this seam). */
80
+ function failureFromProblems(
81
+ problems: readonly PatchProblem[],
82
+ served: readonly ServedFileBlock[] = [],
83
+ ): ApplyPatchFailure {
84
+ const first = problems[0];
85
+ return {
86
+ ok: false,
87
+ code: first?.code ?? "E_PATCH_SYNTAX",
88
+ message:
89
+ "nothing changed (" + problems.length + " problem" + (problems.length === 1 ? "" : "s") + ")",
90
+ problems,
91
+ served,
92
+ changedNothing: true,
93
+ };
94
+ }
95
+
96
+ /**
97
+ * Throw the shared rejection envelope: rendered text in the message, code/servedRows/servedBlock
98
+ * on the error object. This is the exact shape `dsh/tools/errors.ts` reads to build DshToolError —
99
+ * the host adapter has one failure path (throw), never an in-band isError branch.
100
+ */
101
+ function throwRejection(failure: ApplyPatchFailure, rendered: ApplyPatchToolResult): never {
102
+ const text = rendered.content.map((entry) => entry.text).join("\n");
103
+ const servedRows = failure.served.flatMap((block) => [...block.rows]);
104
+ const servedBlock = failure.served.map((block) => block.text).join("\n");
105
+ // WHY: the two apply_patch codes are registered in the shared domain registry, so the tool can
106
+ // WHY: throw the real DomainError (rendered [MODEL] [E_*] header + envelope fields) exactly like
107
+ // WHY: every other refusal. Other registry codes fall through to the envelope-only throw below
108
+ // WHY: because their payload shapes are owned by their producers.
109
+ if (failure.code === "E_PATCH_SYNTAX") {
110
+ throw new DomainError("E_PATCH_SYNTAX", {
111
+ message: text,
112
+ ...(servedRows.length > 0 ? { servedRows } : {}),
113
+ ...(servedBlock !== "" ? { servedBlock } : {}),
114
+ });
115
+ }
116
+ if (failure.code === "E_UNIQUE_MATCH") {
117
+ throw new DomainError("E_UNIQUE_MATCH", {
118
+ message: text,
119
+ ...(servedRows.length > 0 ? { servedRows } : {}),
120
+ ...(servedBlock !== "" ? { servedBlock } : {}),
121
+ });
122
+ }
123
+ if (failure.code === "E_STALE_ANCHOR") {
124
+ throw new DomainError("E_STALE_ANCHOR", {
125
+ headline: text,
126
+ cause: "anchor staleness",
127
+ ...(servedRows.length > 0 ? { servedRows } : {}),
128
+ ...(servedBlock !== "" ? { servedBlock } : {}),
129
+ });
130
+ }
131
+ const error = new Error(text);
132
+ attachEnvelope(error, {
133
+ code: failure.code as DomainErrorCode,
134
+ ...(servedRows.length > 0 ? { servedRows } : {}),
135
+ ...(servedBlock !== "" ? { servedBlock } : {}),
136
+ });
137
+ throw error;
138
+ }
139
+
140
+ /**
141
+ * Make a rejection's served rows real served rows: materialize the current file snapshot, then
142
+ * record the rows through the session handle so every returned HASH│content anchor carries a
143
+ * session+file lease (and the canon evidence derived from it). A retry can then use those anchors
144
+ * without a read; a failure here never replaces the original rejection.
145
+ */
146
+ async function recordRejectionServes(
147
+ blocks: readonly ServedFileBlock[],
148
+ ctx: ApplyPatchContext,
149
+ ): Promise<void> {
150
+ if (blocks.length === 0) return;
151
+ let store;
152
+ try {
153
+ store = await loadHashStore();
154
+ } catch (error) {
155
+ reportDiagnostic("apply-patch", "Failed to open the store for rejection serves:", error);
156
+ return;
157
+ }
158
+ for (const block of blocks) {
159
+ if (block.rows.length === 0) continue;
160
+ try {
161
+ const normalized = await decodeNormText(
162
+ block.path,
163
+ ctx.cwd,
164
+ ctx.signal === undefined ? undefined : { signal: ctx.signal },
165
+ );
166
+ const hashes = await defaultHashIdentity.hashesFor(normalized.normalized, {
167
+ path: normalized.absolutePath,
168
+ persist: true,
169
+ snapshotIO: snapshotIOFor(store),
170
+ });
171
+ const rows = block.rows.filter((row) => hashes[row.position] === row.hash);
172
+ if (rows.length === 0) continue;
173
+ const handle = createSessionHandle(ctx.sessionKey, normalized.absolutePath, store);
174
+ await handle.recordServeFeedback(
175
+ [...rows],
176
+ "live",
177
+ undefined,
178
+ snapshotHashFor(normalized.normalized),
179
+ );
180
+ } catch (error) {
181
+ reportDiagnostic("apply-patch", "Failed to record rejection served rows:", error);
182
+ }
183
+ }
184
+ }
185
+
186
+ /** Project the committed facts plus plan stats into the success result the renderer formats. */
187
+ function successFrom(
188
+ plan: PatchPlan,
189
+ committed: Extract<CommitPlanResult, { ok: true }>,
190
+ ): ApplyPatchSuccess {
191
+ const files: AppliedFileSummary[] = committed.files.map((file) => ({
192
+ status: file.status,
193
+ path: file.path,
194
+ ...(file.fromPath !== undefined ? { fromPath: file.fromPath } : {}),
195
+ addedLines: file.addedLines,
196
+ removedLines: file.removedLines,
197
+ }));
198
+ const addedLines = files.reduce((total, file) => total + file.addedLines, 0);
199
+ const removedLines = files.reduce((total, file) => total + file.removedLines, 0);
200
+ const hunks = plan.files.reduce((total, file) => total + file.mutations.length, 0);
201
+ return {
202
+ ok: true,
203
+ summary: "Updated " + files.length + " file" + (files.length === 1 ? "" : "s") + ":",
204
+ diff: committed.diff,
205
+ files,
206
+ served: committed.files.flatMap((file) => (file.served !== undefined ? [file.served] : [])),
207
+ warnings: committed.warnings,
208
+ metrics: {
209
+ files: files.length,
210
+ hunks,
211
+ addedLines,
212
+ removedLines,
213
+ },
214
+ };
215
+ }
216
+
217
+ /**
218
+ * Build the apply_patch use case.
219
+ * @param deps - optional seam overrides; production binds the real modules.
220
+ * @returns the tool seam bound to the real hashline engine and store.
221
+ */
222
+ export function createApplyPatchTool(deps: ApplyPatchToolDeps = {}): ApplyPatchTool {
223
+ const parse = deps.parse ?? parseApplyPatch;
224
+ const plan = deps.plan ?? planApplyPatch;
225
+ const commit = deps.commit ?? commitPlan;
226
+ const renderOk = deps.renderSuccess ?? renderSuccess;
227
+ const renderBad = deps.renderFailure ?? renderFailure;
228
+
229
+ const planOnly: ApplyPatchTool["plan"] = async (input, ctx) => {
230
+ const parsed = parse(input);
231
+ if (!parsed.ok) return { ok: false, problems: parsed.problems, served: [] };
232
+ return plan(parsed.document, {
233
+ cwd: ctx.cwd,
234
+ sessionKey: ctx.sessionKey,
235
+ ...(ctx.signal !== undefined ? { signal: ctx.signal } : {}),
236
+ });
237
+ };
238
+
239
+ return {
240
+ plan: planOnly,
241
+ async execute(input, ctx) {
242
+ const planned = await planOnly(input, ctx);
243
+ if (!planned.ok) {
244
+ await recordRejectionServes(planned.served ?? [], ctx);
245
+ const failure = failureFromProblems(planned.problems, planned.served ?? []);
246
+ throwRejection(failure, renderBad(failure));
247
+ }
248
+ const committed = await commit(planned.plan, {
249
+ cwd: ctx.cwd,
250
+ sessionKey: ctx.sessionKey,
251
+ ...(ctx.signal !== undefined ? { signal: ctx.signal } : {}),
252
+ });
253
+ if (!committed.ok) throwRejection(committed.failure, renderBad(committed.failure));
254
+ return renderOk(successFrom(planned.plan, committed));
255
+ },
256
+ };
257
+ }
@@ -0,0 +1,387 @@
1
+ /**
2
+ * apply_patch v4 — shared contract types.
3
+ *
4
+ * This module is the frozen vocabulary every apply-patch lane compiles against:
5
+ *
6
+ * - the **patch AST** the grammar layer produces (`PatchDocument`),
7
+ * - the **plan IR** the planner produces and the transaction layer commits
8
+ * (`PatchPlan`, `PlannedFile`, `PlannedMutation`),
9
+ * - the **result / error** types the renderer and the host adapter consume.
10
+ *
11
+ * The engine underneath is the existing hashline kernel: content-addressed 4-char
12
+ * anchors, served leases, MVCC snapshots, correlated multi-file transactions and
13
+ * the undo store. Nothing here re-declares those primitives; the plan IR only
14
+ * adds the addressing and lifecycle vocabulary apply_patch needs.
15
+ *
16
+ * Vocabulary (see GLOSSARY.md): anchor, served row, lease, fragment hunk,
17
+ * identity hunk, reference hunk, patch line, plan.
18
+ *
19
+ * @module dsh-hashline-edit/apply-patch/types
20
+ */
21
+ import type { DomainErrorCode, ServedRow } from "../domain-errors.js";
22
+ import type { LineEnding } from "../edit-diff.js";
23
+ import type { HashStore } from "../hash-store.js";
24
+ import type { TransactionRecoveryReport } from "../mutation-engine/types.js";
25
+
26
+ /** Patch AST (grammar layer output). */
27
+
28
+ /** A 4-character anchor: `[A-Za-z0-9]{4}` (the hashline alphabet). */
29
+ export type Anchor = string;
30
+
31
+ /** A 1-based line number in the submitted patch text, used by every diagnostic. */
32
+ export type PatchLineNumber = number;
33
+
34
+ /** A path exactly as written in the patch: workspace-relative or workspace-absolute. */
35
+ export type PatchPath = string;
36
+
37
+ /** A closed anchor span; both anchors must have been served for the same file. */
38
+ export interface AnchorSpan {
39
+ readonly from: Anchor;
40
+ readonly to: Anchor;
41
+ }
42
+
43
+ /** One parsed `*** Begin Patch` … `*** End Patch` document. */
44
+ export interface PatchDocument {
45
+ /** File operations in submission order. */
46
+ readonly files: readonly PatchFileOp[];
47
+ /** Total number of lines in the input (1-based diagnostics may point at EOF). */
48
+ readonly lineCount: number;
49
+ }
50
+
51
+ export type PatchFileOp = AddFileOp | DeleteFileOp | UpdateFileOp;
52
+
53
+ /** `*** Add File: <path>` — every following line is a '+' content line. */
54
+ export interface AddFileOp {
55
+ readonly kind: "add";
56
+ readonly path: PatchPath;
57
+ readonly headerLine: PatchLineNumber;
58
+ /** Content lines WITHOUT the leading '+', in file order. Empty means an empty file. */
59
+ readonly contentLines: readonly string[];
60
+ }
61
+
62
+ /** `*** Delete File: <path>` */
63
+ export interface DeleteFileOp {
64
+ readonly kind: "delete";
65
+ readonly path: PatchPath;
66
+ readonly headerLine: PatchLineNumber;
67
+ }
68
+
69
+ /** `*** Update File: <path>` with its optional `*** Move to:` and zero or more hunks. */
70
+ export interface UpdateFileOp {
71
+ readonly kind: "update";
72
+ readonly path: PatchPath;
73
+ readonly headerLine: PatchLineNumber;
74
+ /** `*** Move to: <path>`; the parser only accepts it directly after the update header. */
75
+ readonly moveTo?: MoveDirective;
76
+ /** Hunks in submission order; each hunk's window starts after the previous hunk's end. */
77
+ readonly hunks: readonly PatchHunk[];
78
+ }
79
+
80
+ /** `*** Move to: <path>` */
81
+ export interface MoveDirective {
82
+ readonly path: PatchPath;
83
+ readonly line: PatchLineNumber;
84
+ }
85
+
86
+ export type PatchHunk = FragmentHunk | IdentityHunk | ReferenceHunk;
87
+
88
+ /**
89
+ * The addressing a hunk opens with.
90
+ *
91
+ * `start` is the bare `@@` (fragment mode, search from the file start);
92
+ * `context` is `@@ <line>` (fragment mode, search window starts after that
93
+ * line's unique hit); `anchors` is `@@<a>` / `@@<a>-<b>` (identity mode).
94
+ */
95
+ export type HunkAddress =
96
+ | { readonly kind: "start" }
97
+ | { readonly kind: "context"; readonly text: string }
98
+ | { readonly kind: "anchors"; readonly span: AnchorSpan };
99
+
100
+ /** A context/line matcher hunk: `@@` or `@@ <context>` followed by ' '/'+'/'-' lines. */
101
+ export interface FragmentHunk {
102
+ readonly kind: "fragment";
103
+ /** Grammar guarantees this is `start` or `context`; `anchors` never appears here. */
104
+ readonly address: HunkAddress;
105
+ readonly headerLine: PatchLineNumber;
106
+ /** Body lines, in submission order; at least one is required. */
107
+ readonly lines: readonly FragmentLine[];
108
+ /** `*** End of File` line number when the hunk is tail-pinned, absent otherwise. */
109
+ readonly endOfFile?: PatchLineNumber;
110
+ }
111
+
112
+ /** One body line of a fragment hunk. */
113
+ export interface FragmentLine {
114
+ readonly op: "context" | "remove" | "add";
115
+ /** Line content without the leading marker (' ', '-', '+'). */
116
+ readonly text: string;
117
+ readonly line: PatchLineNumber;
118
+ }
119
+
120
+ /** An identity hunk: `@@<a>` / `@@<a>-<b>` followed by '+' lines only (empty body = delete). */
121
+ export interface IdentityHunk {
122
+ readonly kind: "identity";
123
+ readonly span: AnchorSpan;
124
+ readonly headerLine: PatchLineNumber;
125
+ /** '+' lines without the marker; empty means the span is deleted. */
126
+ readonly addedLines: readonly string[];
127
+ }
128
+
129
+ /** A copy/cut hunk: the content comes from another file's served span; no body lines allowed. */
130
+ export interface ReferenceHunk {
131
+ readonly kind: "reference";
132
+ /**
133
+ * The hunk's own addressing (`@@`, `@@ <context>`, `@@<a>` or `@@<a>-<b>`),
134
+ * which determines the destination position (see docs/apply-patch-v4-internals.md §4).
135
+ */
136
+ readonly address: HunkAddress;
137
+ readonly source: ReferenceSource;
138
+ readonly headerLine: PatchLineNumber;
139
+ }
140
+
141
+ /** `*** Copy From:` / `*** Cut From:` source span. */
142
+ export interface ReferenceSource {
143
+ readonly mode: "copy" | "cut";
144
+ readonly path: PatchPath;
145
+ readonly span: AnchorSpan;
146
+ readonly line: PatchLineNumber;
147
+ }
148
+
149
+ /** Plan IR (planner output / transaction input). */
150
+
151
+ /**
152
+ * An inclusive 1-based line range in a file's pre-call baseline.
153
+ *
154
+ * A zero-width insertion uses the engine's inverted convention:
155
+ * `startLine = point + 1` and `endLine = point` (insert after line `point`;
156
+ * point 0 inserts at the start of the file).
157
+ */
158
+ export interface LineRange {
159
+ readonly startLine: number;
160
+ readonly endLine: number;
161
+ }
162
+
163
+ /** The file-lifecycle action planned for one target. */
164
+ export type LifecyclePlan =
165
+ | { readonly kind: "keep" }
166
+ | { readonly kind: "add" }
167
+ | { readonly kind: "delete" }
168
+ | { readonly kind: "move"; readonly to: PatchPath; readonly toAbsolutePath: string };
169
+
170
+ /** One file's state at call time — the single baseline every mutation resolves against. */
171
+ export interface FileBaseline {
172
+ /** The path as written in the patch. */
173
+ readonly path: PatchPath;
174
+ /** The resolved absolute path the engine reads/writes. */
175
+ readonly absolutePath: string;
176
+ /** LF-normalized text; `""` for an Add File target. */
177
+ readonly text: string;
178
+ /** 4-char anchors parallel to the file's lines; empty for an Add File target. */
179
+ readonly hashes: readonly string[];
180
+ /** Byte-order mark the file carried ("" when none). */
181
+ readonly bom: string;
182
+ /** The file's original line ending. */
183
+ readonly ending: LineEnding;
184
+ /** False for `*** Add File:` targets, true for every other existing target. */
185
+ readonly exists: boolean;
186
+ }
187
+
188
+ /** One resolved content mutation against `FileBaseline.text`, in baseline coordinates. */
189
+ export interface PlannedMutation {
190
+ readonly kind: "replace" | "insert" | "delete";
191
+ readonly range: LineRange;
192
+ /** Replacement lines (LF-joined at commit); empty for `delete` and for an empty insertion. */
193
+ readonly lines: readonly string[];
194
+ /** The patch line that produced this mutation (diagnostics and ordering evidence). */
195
+ readonly patchLine: PatchLineNumber;
196
+ }
197
+
198
+ /** One Copy/Cut source span materialized from a baseline. */
199
+ export interface PlannedReferenceUse {
200
+ readonly mode: "copy" | "cut";
201
+ readonly path: PatchPath;
202
+ readonly absolutePath: string;
203
+ readonly range: LineRange;
204
+ /** The source bytes, line-split and LF-normalized. */
205
+ readonly lines: readonly string[];
206
+ readonly patchLine: PatchLineNumber;
207
+ }
208
+
209
+ /** One file in the plan, with every mutation expressed against its own pre-call baseline. */
210
+ export interface PlannedFile {
211
+ readonly path: PatchPath;
212
+ readonly absolutePath: string;
213
+ readonly lifecycle: LifecyclePlan;
214
+ readonly baseline: FileBaseline;
215
+ /** Non-overlapping mutations in baseline coordinates, ordered by `range.startLine`. */
216
+ readonly mutations: readonly PlannedMutation[];
217
+ /** Copy/Cut sources this target consumes. */
218
+ readonly references: readonly PlannedReferenceUse[];
219
+ /** True when committing this member changes bytes or the namespace. */
220
+ readonly writes: boolean;
221
+ }
222
+
223
+ /** The full plan one apply_patch call commits as one correlated transaction. */
224
+ export interface PatchPlan {
225
+ /** Written members in commit order: the primary file leads, the rest follow deterministically. */
226
+ readonly files: readonly PlannedFile[];
227
+ /** Files read only as Copy/Cut sources (never written). */
228
+ readonly referenceFiles: readonly FileBaseline[];
229
+ readonly stats: PatchStats;
230
+ }
231
+
232
+ /** Aggregate line/file counts the success summary reports. */
233
+ export interface PatchStats {
234
+ readonly filesChanged: number;
235
+ readonly addedLines: number;
236
+ readonly removedLines: number;
237
+ }
238
+
239
+ /** Result and error types. */
240
+
241
+ /**
242
+ * The two error codes apply_patch adds to the shared registry.
243
+ *
244
+ * `E_STALE_ANCHOR` is NOT new: `src/domain-errors.ts` already registers it with
245
+ * the served-rows payload this feature reuses. The registry lane must add these
246
+ * two codes (payload + producer) before the surface lane registers the tool.
247
+ */
248
+ export type ApplyPatchNewErrorCode = "E_PATCH_SYNTAX" | "E_UNIQUE_MATCH";
249
+
250
+ /** Every code a patch problem may carry. */
251
+ export type ApplyPatchErrorCode = DomainErrorCode | ApplyPatchNewErrorCode;
252
+
253
+ /** One candidate row for an ambiguous fragment match, served back to the model. */
254
+ export interface PatchCandidate {
255
+ /** 1-based line number in the baseline. */
256
+ readonly line: number;
257
+ readonly hash: Anchor;
258
+ readonly text: string;
259
+ }
260
+
261
+ /** One collectable problem in a rejection; every problem carries its patch line and code. */
262
+ export interface PatchProblem {
263
+ readonly code: ApplyPatchErrorCode;
264
+ readonly patchLine: PatchLineNumber;
265
+ readonly path?: PatchPath;
266
+ readonly message: string;
267
+ /** Candidate rows when the failure is an ambiguous fragment match. */
268
+ readonly candidates?: readonly PatchCandidate[];
269
+ /** 1-based target line used to center the rejection serve window when known. */
270
+ readonly line?: number;
271
+ }
272
+
273
+ /** One file's current rows rendered `HASH│content` for reject-and-serve. */
274
+ export interface ServedFileBlock {
275
+ readonly path: PatchPath;
276
+ readonly rows: readonly ServedRow[];
277
+ /** Rendered block, one `HASH│content` row per line. */
278
+ readonly text: string;
279
+ }
280
+
281
+ /** One file's lifecycle status and line counts in the success summary. */
282
+ export interface AppliedFileSummary {
283
+ /** A = added, M = modified, R = renamed, D = deleted. */
284
+ readonly status: "A" | "M" | "R" | "D";
285
+ readonly path: PatchPath;
286
+ /** Original path for a rename (`R` only). */
287
+ readonly fromPath?: PatchPath;
288
+ readonly addedLines: number;
289
+ readonly removedLines: number;
290
+ }
291
+
292
+ /** Metrics one committed call reports (mirrors the existing RMetrics intent). */
293
+ export interface PatchMetrics {
294
+ readonly files: number;
295
+ readonly hunks: number;
296
+ readonly addedLines: number;
297
+ readonly removedLines: number;
298
+ readonly durationMs?: number;
299
+ }
300
+
301
+ /** Successful apply_patch outcome. */
302
+ export interface ApplyPatchSuccess {
303
+ readonly ok: true;
304
+ /** File-level summary, e.g. `Updated 2 files:\nA src/new.ts\nM src/a.ts`. */
305
+ readonly summary: string;
306
+ /** Annotated diff for the UI/details (never the model's primary text). */
307
+ readonly diff: string;
308
+ readonly files: readonly AppliedFileSummary[];
309
+ /** New anchors for every changed line, per file — the second call needs no read. */
310
+ readonly served: readonly ServedFileBlock[];
311
+ readonly warnings: readonly string[];
312
+ readonly metrics: PatchMetrics;
313
+ }
314
+
315
+ /** Rejected apply_patch outcome: one transaction, zero disk writes. */
316
+ export interface ApplyPatchFailure {
317
+ readonly ok: false;
318
+ /** Primary code for the envelope; every problem still carries its own code. */
319
+ readonly code: ApplyPatchErrorCode;
320
+ readonly message: string;
321
+ /** Every independently diagnosable problem, in patch order. */
322
+ readonly problems: readonly PatchProblem[];
323
+ /** Current rows for every file named by a problem (reject-and-serve). */
324
+ readonly served: readonly ServedFileBlock[];
325
+ /** A rejection is pre-commit: the disk is unchanged by construction. */
326
+ /**
327
+ * True when the commit left the disk byte-identical (every written member rolled back). A
328
+ * mid-commit failure that could not be rolled back sets this false and reports exactly which
329
+ * members were restored, remain, or are uncertain.
330
+ */
331
+ readonly changedNothing: boolean;
332
+ /** The rollback's three-way outcome, when the failure happened after a member was written. */
333
+ readonly recovery?: TransactionRecoveryReport;
334
+ }
335
+
336
+ export type ApplyPatchResult = ApplyPatchSuccess | ApplyPatchFailure;
337
+
338
+ /** The grammar seam's total result: a document or every syntax problem found. */
339
+ export type ParsePatchResult =
340
+ | { readonly ok: true; readonly document: PatchDocument }
341
+ | { readonly ok: false; readonly problems: readonly PatchProblem[] };
342
+
343
+ /** The planner seam's total result. */
344
+ export type PlanPatchResult =
345
+ | { readonly ok: true; readonly plan: PatchPlan }
346
+ | {
347
+ readonly ok: false;
348
+ readonly problems: readonly PatchProblem[];
349
+ /** Current rows for every problem file, so the rejection needs no read to retry. */
350
+ readonly served: readonly ServedFileBlock[];
351
+ };
352
+
353
+ /** The model-facing tool envelope returned by the renderer / tool use case. */
354
+ export interface ApplyPatchToolResult {
355
+ readonly content: ReadonlyArray<{ readonly type: "text"; readonly text: string }>;
356
+ readonly details: ApplyPatchDetails;
357
+ /** True when this envelope renders a rejection (nothing was written). */
358
+ readonly isError?: boolean;
359
+ }
360
+
361
+ /** The structured details half of {@link ApplyPatchToolResult}. */
362
+ export interface ApplyPatchDetails {
363
+ readonly summary: string;
364
+ readonly diff: string;
365
+ readonly files: readonly AppliedFileSummary[];
366
+ readonly servedByPath: readonly ServedFileBlock[];
367
+ readonly warnings: readonly string[];
368
+ readonly metrics: PatchMetrics;
369
+ }
370
+
371
+ /** Execution facts every apply_patch seam shares. */
372
+ export interface ApplyPatchContext {
373
+ /** Absolute directory relative request paths resolve against (the live session directory). */
374
+ readonly cwd: string;
375
+ /** Session identity that keys served anchors and leases; never empty. */
376
+ readonly sessionKey: string;
377
+ /** Caller cancellation forwarded into every async file operation. */
378
+ readonly signal?: AbortSignal;
379
+ }
380
+
381
+ /** Options for the planner/commit seams over the existing hash store. */
382
+ export interface ApplyPatchEngineOptions extends ApplyPatchContext {
383
+ /** Hash store override for tests; the default opens the configured store. */
384
+ readonly store?: HashStore;
385
+ /** POSIX access-mode bitmask; commit defaults to read+write, plan to read-only. */
386
+ readonly accessMode?: number;
387
+ }