dsh-hashline-edit 0.1.1 → 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 (130) hide show
  1. package/README.md +70 -69
  2. package/cordis.patch.yml +1 -1
  3. package/lib/index.js +4143 -4518
  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 +52 -0
  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/domain-errors.d.ts +16 -2
  31. package/lib/types/src/edit-undo.d.ts +11 -2
  32. package/lib/types/src/file-content/detection.d.ts +17 -0
  33. package/lib/types/src/file-content/index.d.ts +2 -2
  34. package/lib/types/src/file-content/preview.d.ts +1 -1
  35. package/lib/types/src/fs-lifecycle.d.ts +44 -0
  36. package/lib/types/src/hashline/resolve.d.ts +6 -0
  37. package/lib/types/src/host-utils.d.ts +63 -0
  38. package/lib/types/src/mutation-engine/batch-span-gate.d.ts +6 -0
  39. package/lib/types/src/mutation-engine/edit-request.d.ts +34 -0
  40. package/lib/types/src/mutation-engine/pipeline.d.ts +18 -3
  41. package/lib/types/src/mutation-engine/types.d.ts +50 -58
  42. package/lib/types/src/read-expansion/grammar-manifest.d.ts +4 -2
  43. package/lib/types/src/read-expansion/java-decoration.d.ts +21 -0
  44. package/lib/types/src/read-expansion/span-expansion.d.ts +6 -2
  45. package/lib/types/src/read-expansion/span-kind.d.ts +3 -2
  46. package/lib/types/src/read-expansion/vue-decoration.d.ts +14 -0
  47. package/lib/types/src/served-session/serve-render.d.ts +9 -3
  48. package/lib/types/src/undo-store.d.ts +17 -1
  49. package/package.json +10 -22
  50. package/prompts/apply-patch-guidelines.md +10 -0
  51. package/prompts/apply-patch.md +1 -0
  52. package/prompts/read-guidelines.md +5 -5
  53. package/prompts/read.md +1 -1
  54. package/prompts/undo-last-edit-guidelines.md +2 -2
  55. package/prompts/undo-last-edit-snippet.md +1 -1
  56. package/prompts/undo-last-edit.md +1 -1
  57. package/src/apply-patch/commit.ts +190 -0
  58. package/src/apply-patch/grammar.ts +620 -0
  59. package/src/apply-patch/index.ts +17 -0
  60. package/src/apply-patch/lifecycle.ts +195 -0
  61. package/src/apply-patch/plan.ts +800 -0
  62. package/src/apply-patch/render.ts +137 -0
  63. package/src/apply-patch/resolve.ts +470 -0
  64. package/src/apply-patch/tool.ts +257 -0
  65. package/src/apply-patch/types.ts +387 -0
  66. package/src/cut-repair.ts +77 -11
  67. package/src/domain-errors.ts +58 -10
  68. package/src/edit-undo.ts +379 -25
  69. package/src/file-content/detection.ts +34 -0
  70. package/src/file-content/index.ts +2 -2
  71. package/src/file-content/preview.ts +1 -1
  72. package/src/file-content/served-header.ts +1 -1
  73. package/src/fs-lifecycle.ts +201 -0
  74. package/src/hash-store.ts +18 -1
  75. package/src/hashline/lease-resolve.ts +14 -8
  76. package/src/hashline/resolve.ts +30 -27
  77. package/src/host-utils.ts +224 -0
  78. package/src/mutation-engine/batch-span-gate.ts +17 -4
  79. package/src/mutation-engine/edit-request.ts +39 -0
  80. package/src/mutation-engine/pipeline.ts +535 -63
  81. package/src/mutation-engine/types.ts +49 -65
  82. package/src/mutation-queue.ts +1 -1
  83. package/src/prompts.ts +0 -28
  84. package/src/read-expansion/ast-provider.ts +4 -0
  85. package/src/read-expansion/grammar-manifest.ts +21 -2
  86. package/src/read-expansion/java-decoration.ts +34 -0
  87. package/src/read-expansion/span-expansion.ts +6 -2
  88. package/src/read-expansion/span-kind.ts +3 -1
  89. package/src/read-expansion/vue-decoration.ts +43 -0
  90. package/src/served-session/serve-render.ts +5 -5
  91. package/src/undo-store.ts +74 -5
  92. package/lib/types/dsh/encoding-bridge.d.ts +0 -25
  93. package/lib/types/dsh/encoding.d.ts +0 -144
  94. package/lib/types/dsh/tools/edit.d.ts +0 -10
  95. package/lib/types/src/edit-tool.d.ts +0 -41
  96. package/lib/types/src/mutation-engine/engine.d.ts +0 -31
  97. package/lib/types/src/payload-contract.d.ts +0 -124
  98. package/prompts/edit-guidelines.md +0 -10
  99. package/prompts/edit-snippet.md +0 -1
  100. package/prompts/edit.md +0 -1
  101. package/src/bash-classifier.ts +0 -1626
  102. package/src/bash-conversion/decide.ts +0 -812
  103. package/src/bash-display-transform.ts +0 -50
  104. package/src/bash-multifile-config.ts +0 -239
  105. package/src/bash-view-config.ts +0 -158
  106. package/src/edit-normalize.ts +0 -8
  107. package/src/edit-render.ts +0 -161
  108. package/src/edit-tool.ts +0 -116
  109. package/src/edit.ts +0 -136
  110. package/src/integrations/pi-lens/command.ts +0 -712
  111. package/src/integrations/pi-lens/config.ts +0 -148
  112. package/src/integrations/pi-lens/io-bridge-adapter.ts +0 -234
  113. package/src/integrations/pi-lens/io-bridge.ts +0 -112
  114. package/src/integrations/pi-lens/settings-overlay.ts +0 -272
  115. package/src/lifecycle-hooks/index.ts +0 -1992
  116. package/src/lifecycle-hooks/types.ts +0 -64
  117. package/src/mutated-files.ts +0 -75
  118. package/src/mutation-engine/engine.ts +0 -173
  119. package/src/mutation-engine/index.ts +0 -17
  120. package/src/payload-contract.ts +0 -637
  121. package/src/preview-controller.ts +0 -84
  122. package/src/read.ts +0 -865
  123. package/src/search-hook.ts +0 -41
  124. package/src/store-config.ts +0 -73
  125. package/src/store-janitor-warning.ts +0 -169
  126. package/src/store-janitor.ts +0 -539
  127. package/src/store-kernel/settings.ts +0 -233
  128. package/src/store.ts +0 -154
  129. package/src/tui-presenter.ts +0 -134
  130. package/src/write-hook.ts +0 -130
@@ -95,3 +95,55 @@ export declare function align(stdoutLines: readonly string[], fileLines: readonl
95
95
  * @returns the plan, or `undefined` when the host must pass the output through.
96
96
  */
97
97
  export declare function plan(deps: ToolDependencies, request: ShellViewRequest): Promise<ShellViewPlan | undefined>;
98
+ /** The host tool whose canonical value is `{ matches: [{ path, lineNumber, line }] }`. */
99
+ export declare const GREP_TOOL_NAME = "grep";
100
+ /** The host `grepMaxMatches` default (`@deepseek-ai/dsh-tool-fs-search` `Config`). */
101
+ export declare const GREP_INLINE_MAX_MATCHES = 250;
102
+ /**
103
+ * WHY: the host spill policy's own budget is `maxInlineTokens: 12500`
104
+ * (`dsh-base/cordis.patch.yml`, `@deepseek-ai/dsh-spill-policy`), and grep is not
105
+ * on its exemption list — only `read` is. A `post-execute` accept decision that
106
+ * carries a `value` bypasses that policy (`Object.hasOwn(decision, "value")`), so
107
+ * this seam must hold the same line itself, using the host's own price for one
108
+ * text block: `ceil(chars / 4) + 4` (`@deepseek-ai/dsh-token-meter/estimate`).
109
+ */
110
+ export declare const GREP_VIEW_MAX_INLINE_TOKENS = 12500;
111
+ /** The host estimator's price for one text block, so a self-check matches the policy's. */
112
+ export declare function estimateTextTokens(text: string): number;
113
+ /** One match of the host `grep` value. */
114
+ export interface GrepViewMatch {
115
+ readonly path: string;
116
+ readonly lineNumber: number;
117
+ readonly line: string;
118
+ }
119
+ /** The matches plus the session to serve them to. */
120
+ export interface GrepViewRequest {
121
+ readonly matches: readonly GrepViewMatch[];
122
+ readonly cwd: string;
123
+ readonly sessionKey: string;
124
+ readonly signal: AbortSignal | undefined;
125
+ }
126
+ /** The rewritten model-facing grep result. */
127
+ export interface GrepViewPlan {
128
+ /** The shared search banner, one count header per file, and anchored `HASH│content` rows. */
129
+ readonly text: string;
130
+ /** Total rows served (and leased) by this plan. */
131
+ readonly servedLines: number;
132
+ /** The served files, in first-seen match order. */
133
+ readonly paths: readonly string[];
134
+ }
135
+ /**
136
+ * Turn one built-in `grep` result into anchored rows for the files it matched,
137
+ * serving (and leasing) exactly those lines.
138
+ *
139
+ * Fail-closed discipline mirrors the shell view: a result that is empty, over the
140
+ * host's inline match cap, over the host's inline token budget, non-ascending per
141
+ * file, or whose matched text does not byte-equal the file's governed decode leaves
142
+ * the original result untouched and records no lease. The budget is checked BEFORE
143
+ * the first serve, so an oversized view can never leave partial leases behind.
144
+ *
145
+ * @param deps - host seams; `deps.io` is the only filesystem access used.
146
+ * @param request - the matches, working directory, session and cancellation signal.
147
+ * @returns the plan, or `undefined` when the host must pass the result through.
148
+ */
149
+ export declare function planGrepView(deps: ToolDependencies, request: GrepViewRequest): Promise<GrepViewPlan | undefined>;
@@ -0,0 +1,38 @@
1
+ import type { FileDiff, ToolDefinition } from "@deepseek-ai/dsh-tools";
2
+ import { APPLY_PATCH_TOOL_NAME } from "../../src/apply-patch/tool.js";
3
+ import type { ApplyPatchTool } from "../../src/apply-patch/tool.js";
4
+ /** Public tool name re-exported for host registration and surface tests. */
5
+ export { APPLY_PATCH_TOOL_NAME };
6
+ import type { ToolDependencies } from "../contracts.js";
7
+ /** One call's intended file diffs, parsed from the patch headers for the pending card. */
8
+ interface PatchPreview {
9
+ readonly diffs: FileDiff[];
10
+ readonly locations: string[];
11
+ }
12
+ /**
13
+ * Scan the patch headers for the pending card. This is a presentation-only
14
+ * projection: it never decides whether the patch is valid (the grammar and
15
+ * planner do), and it never throws on malformed text.
16
+ * @param input - the raw patch text.
17
+ * @returns the intended additions and every path the patch names.
18
+ */
19
+ export declare function previewPatch(input: string): PatchPreview;
20
+ /** Every path this patch writes, including a Cut From source; Copy From sources are read-only. */
21
+ export declare function writtenPatchPaths(input: string): string[];
22
+ /** Options for {@link buildApplyPatchTool}; the factory seam is for tests. */
23
+ export interface ApplyPatchToolOptions {
24
+ /** Test seam: build the use case without touching the real engine/store. */
25
+ readonly create?: () => ApplyPatchTool;
26
+ }
27
+ /**
28
+ * Build the dsh 'apply_patch' tool for one agent install.
29
+ *
30
+ * The use case is created lazily: building a tool definition must not touch the
31
+ * engine or the store, and a composition where 'apply_patch' is skipped must
32
+ * still install the rest of the toolset.
33
+ *
34
+ * @param deps - the host seams this tool may touch; 'sandbox' advertises and resolves escalation.
35
+ * @param options - test seam for the use case.
36
+ * @returns a registry-ready dsh tool definition named 'apply_patch'.
37
+ */
38
+ export declare function buildApplyPatchTool(deps: ToolDependencies, options?: ApplyPatchToolOptions): ToolDefinition;
@@ -14,7 +14,7 @@
14
14
  * @module dsh-hashline-edit/tools/context
15
15
  */
16
16
  import type { ToolRunContext } from "@deepseek-ai/dsh-tools";
17
- import type { ToolContext } from "../contracts.js";
17
+ import type { ToolContext, ToolDependencies } from "../contracts.js";
18
18
  /** The pi-shaped context the upstream use cases consume. */
19
19
  export interface UpstreamContext {
20
20
  cwd: string;
@@ -35,3 +35,21 @@ export interface UpstreamContext {
35
35
  export declare function resolveToolContext(exec: ToolRunContext): ToolContext;
36
36
  /** Translate the dsh facts into the pi-shaped context the upstream seams still take. */
37
37
  export declare function upstreamContext(ctx: ToolContext): UpstreamContext;
38
+ /**
39
+ * Announce, before the core writes them, every request path one mutating call
40
+ * owns.
41
+ *
42
+ * WHY announce and not enforce: these mutations go to `node:fs` and bypass the
43
+ * provider whose guarded write is where an intent would be applied, so the
44
+ * dispatch exists for host bookkeeping. The channel is present only when
45
+ * `ctx.fs` is mounted; without one there is nothing to announce.
46
+ */
47
+ export declare function announcePluginWrites(deps: ToolDependencies, requestPaths: readonly string[], ctx: ToolContext, exec: object): Promise<void>;
48
+ /**
49
+ * Publish what a successful write left on disk for each path it touched.
50
+ *
51
+ * WHY `fresh`: the version this call saw before writing is stale once the bytes
52
+ * changed, so the channel must re-stat for the version the host's guard is later
53
+ * compared against.
54
+ */
55
+ export declare function publishPluginWrites(deps: ToolDependencies, requestPaths: readonly string[], ctx: ToolContext, exec: object): Promise<void>;
@@ -9,12 +9,19 @@
9
9
  * - the canonical value IS `{ content, details }`, so no projection loses fields;
10
10
  * - `render` is the identity on `content`, which keeps the model-facing text byte-identical to the
11
11
  * upstream tool result;
12
- * - `details` stays available to nested (`run_code`) callers as structured data; a UI projection
13
- * can be added through `presentationMeta` without reshaping the canonical value.
12
+ * - `details` stays available to nested (`run_code`) callers as structured data; the UI projection
13
+ * rides `presentationMeta`, which is persisted beside the result and never reshapes the value.
14
+ *
15
+ * The presentation projection exists because the two consumers read different things: a host-local
16
+ * presenter consumes `presentCall`/`presentResult` directly, while the built-in Web Client derives
17
+ * its card props from persisted metadata — the same split the preset's own `write`/`edit` declare
18
+ * in their `output.presentationMeta`, which projects the applied hunks. Ours projects the annotated
19
+ * diff the tool already rendered, because that is the only before/after artifact the upstream
20
+ * envelope carries at this seam.
14
21
  *
15
22
  * @module dsh-hashline-edit/tools/output
16
23
  */
17
- import type { InferValue } from "@deepseek-ai/dsh-tools";
24
+ import type { DiffResultView, FileDiff, InferValue, ToolOutputDefinition } from "@deepseek-ai/dsh-tools";
18
25
  /** One model-facing text block; the upstream tools emit nothing else today. */
19
26
  export interface TextContentBlock {
20
27
  type: "text";
@@ -46,8 +53,17 @@ declare const TOOL_RESULT_SCHEMA: {
46
53
  };
47
54
  /** The inferred canonical value type of {@link TOOL_RESULT_SCHEMA}. */
48
55
  export type ToolResultOutput = InferValue<typeof TOOL_RESULT_SCHEMA>;
56
+ /**
57
+ * The durable presentation payload both mutation tools project: the applied hunks a diff card
58
+ * renders. `path` is the model-facing path the call named (a UI bridge relativizes it); a hunk
59
+ * carries `oldText: null` for a pure insertion, exactly like the preset's own `computeHunkDiffs`.
60
+ */
61
+ export interface HashlineResultMeta {
62
+ path?: string;
63
+ diffs: FileDiff[];
64
+ }
49
65
  /** Registry-ready output declaration shared by `edit` and `undo_last_edit`. */
50
- export declare const TOOL_RESULT_OUTPUT: {
66
+ export declare const HASHLINE_RESULT_OUTPUT: {
51
67
  readonly schema: {
52
68
  readonly type: "object";
53
69
  readonly additionalProperties: false;
@@ -66,7 +82,19 @@ export declare const TOOL_RESULT_OUTPUT: {
66
82
  };
67
83
  };
68
84
  readonly render: (_args: unknown, value: ToolResultOutput) => TextContentBlock[];
85
+ readonly presentationMeta: NonNullable<ToolOutputDefinition["presentationMeta"]>;
69
86
  };
87
+ /**
88
+ * The completed state of a mutation call as one diff card, or `undefined` when the call failed or
89
+ * applied nothing — the presenter then keeps the pending card, exactly like the preset's `edit`.
90
+ * @param title - the card header, e.g. `Edit src/a.ts`.
91
+ * @param result - the completed outcome handed to `presentResult`.
92
+ * @returns the diff result view, or `undefined` for the generic path.
93
+ */
94
+ export declare function diffResultView(title: string, result: {
95
+ readonly isError: boolean;
96
+ readonly meta?: unknown;
97
+ }): DiffResultView | undefined;
70
98
  /**
71
99
  * Present the upstream envelope as the declared canonical value.
72
100
  *
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * The dsh `read` tool: hash-anchored reads (`HASH│content` rows) that shadow
3
3
  * the built-in `read` on the agent's own scope layer. In the default `served`
4
- * mode every shown row is recorded as served, so a later `edit` can verify the
4
+ * mode every shown row is recorded as served, so a later `apply_patch` identity hunk can verify the
5
5
  * model was actually shown the lines it targets; `verbatim` mode is a plain
6
6
  * reference read that grants no anchor and leaves served state untouched.
7
7
  *
@@ -17,11 +17,14 @@
17
17
  * - `around_anchor` windows are admitted and resolved against this session's
18
18
  * active lineage, for both modes (spec §4.3 carries no mode qualifier); only
19
19
  * a served read leases the rows it renders.
20
- * - Read expansion mirrors `src/read.ts`: the host entry injects the composed
21
- * provider (`createDefaultReadSpanProvider`), the `read.expansion` mode and
22
- * every attempt's audit record come from the core seams, and a missing
23
- * provider or unavailable grammar degrades to the exact requested window —
24
- * expansion never fails a read.
20
+ * - Read expansion mirrors `src/read.ts` — every attempt's audit record comes from
21
+ * the core seams, and a missing provider or unavailable grammar degrades to the
22
+ * exact requested window: expansion never fails a read.
23
+ * - `encoding` — the word, and the parameter itself, is gone. This host reads and
24
+ * writes UTF-8 only: a target whose bytes are not valid UTF-8 is refused with
25
+ * `E_UNSUPPORTED_FILE` naming the path and telling the model to ask the user how
26
+ * to handle the file. The bytes still round-trip verbatim — only admission is
27
+ * narrowed.
25
28
  * - ADR-0043 stat-identity adoption mirrors the core: on the default decode path
26
29
  * the loader's stat identity (ino/size/mtime/ctime) is passed through, so an
27
30
  * unchanged file adopts its committed snapshot and reuses its anchors; a moved
@@ -51,22 +54,16 @@ export declare function createDefaultReadSpanProvider(io?: CoreReadIo): SpanProv
51
54
  export interface ServedFileLine {
52
55
  /** 1-based line number the anchor addresses. */
53
56
  readonly line: number;
54
- /** The line's 4-character anchor, usable by `edit`. */
57
+ /** The line's 4-character anchor, usable by `apply_patch`. */
55
58
  readonly hash: string;
56
59
  /** The line's decoded text, without the anchor prefix. */
57
60
  readonly text: string;
58
61
  }
59
- /** The facts one line-set serve needs; `autoGuess` admits a probabilistic decode. */
62
+ /** The facts one line-set serve needs. */
60
63
  export interface ServeFileLinesOptions {
61
64
  readonly path: string;
62
65
  readonly cwd: string;
63
66
  readonly signal: AbortSignal | undefined;
64
- /**
65
- * Admit a probabilistic decode. WHY off for the read tool: only a caller with
66
- * independent evidence of the text (a shell view's own stdout, aligned line by
67
- * line) may accept a guessed code page.
68
- */
69
- readonly autoGuess?: boolean;
70
67
  }
71
68
  /** A line-set serve: the file plus the lines to materialize and lease. */
72
69
  export interface ServeFileLinesRequest extends ServeFileLinesOptions {
@@ -87,7 +84,7 @@ export declare function decodeFileLines(deps: ToolDependencies, request: ServeFi
87
84
  *
88
85
  * WHY a separate seam: the shell view must turn already-aligned disk lines into
89
86
  * editable anchors without re-implementing the read pipeline, and those anchors
90
- * must be leased to the session the following `edit` uses.
87
+ * must be leased to the session the following `apply_patch` hunk uses.
91
88
  */
92
89
  export declare function serveFileLines(deps: ToolDependencies, request: ServeFileLinesRequest): Promise<ServedFileLine[]>;
93
90
  /**
@@ -2,6 +2,11 @@ import type { ToolDefinition } from "@deepseek-ai/dsh-tools";
2
2
  import type { ToolDependencies } from "../contracts.js";
3
3
  /** dsh tool name; `undo_last_edit` is plugin-owned (no preset shadow). */
4
4
  export declare const UNDO_TOOL_NAME = "undo_last_edit";
5
+ /**
6
+ * The core's prompt text, kept in sync with `prompts/undo-last-edit.md`. Exported only so the drift
7
+ * test can pin it to the file the guidance section loads; the tool itself uses the inlined copy.
8
+ */
9
+ export declare const UNDO_DESCRIPTION: string;
5
10
  /**
6
11
  * Build the dsh `undo_last_edit` tool for one agent install.
7
12
  * @param deps - the host seams this tool may touch; `sandbox` advertises and resolves escalation.
@@ -33,8 +33,7 @@
33
33
  */
34
34
  import type { Context } from "@deepseek-ai/cordis";
35
35
  import type { PostToolDecision, PreToolDecision, ToolExecution, ToolExecutionResult } from "@deepseek-ai/dsh-tools";
36
- import type { FileIO } from "./contracts.js";
37
- import { type EncodingState } from "./encoding.js";
36
+ import type { FileIO, FsObservationChannel } from "./contracts.js";
38
37
  /** The heading the post-write auto-read appends to the write result. */
39
38
  export declare const AUTO_READ_HEADING = "--- Auto-read (hashline anchors) ---";
40
39
  /** The served evidence the guard compares a submitted write against. */
@@ -44,11 +43,6 @@ export interface ServedSnapshot {
44
43
  /** Canon digests parallel to `served` (null = no evidence for that position). */
45
44
  readonly canonDigests: readonly (string | null)[];
46
45
  }
47
- /** A write's captured encoding state: the restore target taken before the tool wrote. */
48
- interface CapturedEncodingState {
49
- readonly absolutePath: string;
50
- readonly state: EncodingState;
51
- }
52
46
  /** Everything the hook needs from the host, injectable for tests. */
53
47
  export interface WriteHookDeps {
54
48
  /** Resolve a model-supplied path against the session cwd; must match the read/edit path keys. */
@@ -58,17 +52,11 @@ export interface WriteHookDeps {
58
52
  /** Read + serve the just-written file, returning the model-facing anchored text. */
59
53
  autoRead(rawPath: string, cwd: string, sessionKey: string, signal?: AbortSignal): Promise<string>;
60
54
  /**
61
- * Optional pre-write capture of the encoding state this write must restore.
62
- * WHY before the write: the write changes the file version the state is gated
63
- * on, so a capture taken afterwards could never match what the read recorded.
64
- */
65
- captureEncodingState?(rawPath: string, cwd: string, signal?: AbortSignal): Promise<CapturedEncodingState | undefined>;
66
- /**
67
- * Optional host step between the committed write and the auto-read: re-encode
68
- * the built-in tool's UTF-8 bytes to the captured encoding. Returns a
69
- * model-facing warning when the bytes had to stay UTF-8.
55
+ * Optional host observation channel. The built-in write's own dispatch is what
56
+ * observes the write itself, so the hook only publishes the auto-read's re-observation
57
+ * here; a composition without the channel silently skips the announcement.
70
58
  */
71
- finalizeWrite?(captured: CapturedEncodingState | undefined, rawPath: string, cwd: string, signal?: AbortSignal): Promise<string | undefined>;
59
+ readonly observations?: FsObservationChannel;
72
60
  /** Host diagnostic sink; the hook must never throw through this. */
73
61
  warn(message: string): void;
74
62
  }
@@ -84,16 +72,14 @@ export interface WriteHooks {
84
72
  */
85
73
  export declare function createWriteHooks(deps: WriteHookDeps): WriteHooks;
86
74
  /** The production dependency set: the byte seam plus core served state, core read pipeline, local paths. */
87
- export declare function defaultWriteHookDeps(warn: (message: string) => void, io: FileIO): WriteHookDeps;
75
+ export declare function defaultWriteHookDeps(warn: (message: string) => void, io: FileIO, observations?: FsObservationChannel): WriteHookDeps;
88
76
  /**
89
77
  * Register the write integration for one agent on its own scope layer.
90
78
  *
91
79
  * @param rootCtx - host context, used only for diagnostics.
92
80
  * @param agentCtx - the agent's scoped context; the listeners see only that
93
81
  * agent's tool calls.
94
- * @param io - the host byte seam: the auto-read decodes through it and the
95
- * post-write step inverts the recorded encoding with it.
82
+ * @param io - the host byte seam: the auto-read decodes the written bytes through it.
96
83
  * @returns the exact disposer that unregisters both listeners.
97
84
  */
98
- export declare function registerWriteHook(rootCtx: Context, agentCtx: Context, io: FileIO): () => void;
99
- export {};
85
+ export declare function registerWriteHook(rootCtx: Context, agentCtx: Context, io: FileIO, observations?: FsObservationChannel): () => void;
@@ -0,0 +1,49 @@
1
+ import type { HashStore } from "../hash-store.js";
2
+ import type { ApplyPatchFailure, PatchPlan, ServedFileBlock } from "./types.js";
3
+ /** Options for {@link commitPlan}. */
4
+ export interface CommitPlanOptions {
5
+ readonly cwd: string;
6
+ readonly sessionKey: string;
7
+ readonly signal?: AbortSignal;
8
+ readonly store?: HashStore;
9
+ /** POSIX access-mode bitmask; defaults to read+write. */
10
+ readonly accessMode?: number;
11
+ /** TEST-ONLY: fires before the first rename, while the pre-images/undo rows are durable. */
12
+ readonly onBeforeFirstWrite?: () => void | Promise<void>;
13
+ /** TEST-ONLY: fires between two member renames, after the earlier one is durable. */
14
+ readonly onBetweenWrites?: (committedAbsolutePath: string) => void | Promise<void>;
15
+ /** TEST-ONLY: fires after a move rename and before the destination rewrite (mid-member failure). */
16
+ readonly onAfterMoveRename?: (toAbsolutePath: string) => void | Promise<void>;
17
+ }
18
+ /** One committed file's lifecycle status and line accounting. */
19
+ export interface CommittedFile {
20
+ readonly path: string;
21
+ readonly status: "A" | "M" | "R" | "D";
22
+ /** Original path for a rename (`R` only). */
23
+ readonly fromPath?: string;
24
+ readonly addedLines: number;
25
+ readonly removedLines: number;
26
+ /** New anchors for the changed lines; absent when the member wrote no content. */
27
+ readonly served?: ServedFileBlock;
28
+ }
29
+ /** Commit outcome: the per-file facts on success, the whole failure on rejection. */
30
+ export type CommitPlanResult = {
31
+ readonly ok: true;
32
+ readonly files: readonly CommittedFile[];
33
+ readonly diff: string;
34
+ readonly warnings: readonly string[];
35
+ } | {
36
+ readonly ok: false;
37
+ readonly failure: ApplyPatchFailure;
38
+ };
39
+ /**
40
+ * Commit one plan atomically (one correlated transaction, one undo record set).
41
+ *
42
+ * The implementation reuses `commitTransaction` — the existing correlated skeleton — and extends
43
+ * its member shape with the plan's lifecycle action; the undo schema gains the matching action.
44
+ *
45
+ * @param plan - the fully resolved plan.
46
+ * @param options - cwd/session/store, cancellation and the test fault seams.
47
+ * @returns the committed facts, or a failure whose `changedNothing` reflects the rollback.
48
+ */
49
+ export declare function commitPlan(plan: PatchPlan, options: CommitPlanOptions): Promise<CommitPlanResult>;
@@ -0,0 +1,29 @@
1
+ import type { AnchorSpan, ParsePatchResult } from "./types.js";
2
+ /** Hard cap on one patch input (bytes), mirroring the host request budget. */
3
+ export declare const PATCH_MAX_BYTES: number;
4
+ /** Options for {@link parseApplyPatch}. */
5
+ export interface ParsePatchOptions {
6
+ /** Maximum accepted input size in bytes; defaults to {@link PATCH_MAX_BYTES}. */
7
+ readonly maxBytes?: number;
8
+ }
9
+ /**
10
+ * Parse one patch document.
11
+ *
12
+ * Success returns the AST defined in `types.ts`; failure returns every syntax
13
+ * problem the parser could diagnose in one pass (never a throw for malformed
14
+ * user input). Rejections use `E_PATCH_SYNTAX` with the offending patch line.
15
+ *
16
+ * @param input - the raw `*** Begin Patch` … `*** End Patch` text.
17
+ * @param options - size guard knobs.
18
+ * @returns the parsed document or the collected problems.
19
+ */
20
+ export declare function parseApplyPatch(input: string, options?: ParsePatchOptions): ParsePatchResult;
21
+ /**
22
+ * Parse an anchor token: `<a>` (single line) or `<a>-<b>` (closed span).
23
+ * Returns `undefined` when the token is not a valid four-character anchor pair; the
24
+ * grammar layer turns that into an `E_PATCH_SYNTAX` problem at the patch line.
25
+ *
26
+ * @param token - the text after `@@` (no leading space).
27
+ * @returns the closed span, or undefined for a malformed token.
28
+ */
29
+ export declare function parseAnchorSpan(token: string): AnchorSpan | undefined;
@@ -0,0 +1,32 @@
1
+ import { type HashStore } from "../hash-store.js";
2
+ import type { ProcessedEditFile } from "../mutation-engine/types.js";
3
+ import type { PatchPath, PlannedFile } from "./types.js";
4
+ /**
5
+ * apply_patch lifecycle materialization — the plan-to-engine adapter.
6
+ *
7
+ * The planner resolves every mutation against a pre-call baseline; this module turns that plan
8
+ * into the mutation engine's `ProcessedEditFile` member shape (plus the engine-local lifecycle
9
+ * action). It never writes: the correlated transaction in `mutation-engine/pipeline.ts` owns the
10
+ * one commit path. Pure text application (splice at baseline coordinates) is the only work here,
11
+ * and the resulting anchors are always derived under the member's COMMITTED path.
12
+ *
13
+ * @module dsh-hashline-edit/apply-patch/lifecycle
14
+ */
15
+ /** One materialized member: the plan facts plus the engine member the commit consumes. */
16
+ export interface MaterializedMember {
17
+ readonly planned: PlannedFile;
18
+ readonly member: ProcessedEditFile;
19
+ readonly status: "A" | "M" | "R" | "D";
20
+ /** The committed display path (destination for a move; the pre path otherwise). */
21
+ readonly committedPath: PatchPath;
22
+ /** The pre-transaction display path (move only). */
23
+ readonly fromPath?: PatchPath;
24
+ }
25
+ /**
26
+ * Materialize one planned file into the engine's member shape.
27
+ *
28
+ * The result text is produced by applying the plan's baseline-coordinate mutations; the anchors
29
+ * are then derived under the committed path. No file is read or written here — the plan already
30
+ * carries every baseline byte the commit needs.
31
+ */
32
+ export declare function materializeMember(planned: PlannedFile, store?: HashStore): Promise<MaterializedMember>;
@@ -0,0 +1,50 @@
1
+ import type { LeaseSpanSource } from "../hashline/resolve.js";
2
+ import { type HashStore } from "../hash-store.js";
3
+ import { type ServedAnchors } from "./resolve.js";
4
+ import type { ApplyPatchEngineOptions, FileBaseline, PatchDocument, PatchPath, PlannedMutation, PlanPatchResult } from "./types.js";
5
+ /**
6
+ * The baseline/served loader port.
7
+ *
8
+ * Production wires `readBaseline` to the file-content decode path,
9
+ * `readServed` to `createSessionHandle(...).load()` and `readLeaseSource` to
10
+ * the read-only lease identity source. Tests inject a pure in-memory
11
+ * implementation, which is what makes the planner testable without a store.
12
+ */
13
+ export interface PlanFileLoader {
14
+ /** Load one file's baseline (LF-normalized text + anchors + BOM + ending). */
15
+ readBaseline(path: PatchPath, absolutePath: string, signal?: AbortSignal): Promise<FileBaseline>;
16
+ /** Load this session's served anchors for one file, parallel to its lines. */
17
+ readServed(absolutePath: string, sessionKey: string, store?: HashStore): Promise<ServedAnchors>;
18
+ /** Session-wide anchor homes, for foreign-anchor diagnosis (optional legacy port). */
19
+ anchorHomes?(anchor: string, sessionKey: string, store?: HashStore): readonly PatchPath[];
20
+ /** Build the read-only lease identity source for one file's baseline content. */
21
+ readLeaseSource?(absolutePath: string, sessionKey: string, content: string, store?: HashStore): Promise<LeaseSpanSource>;
22
+ }
23
+ /** Options for {@link planApplyPatch}; `loader` is the test seam. */
24
+ export interface PlanApplyPatchOptions extends ApplyPatchEngineOptions {
25
+ readonly loader?: PlanFileLoader;
26
+ }
27
+ /** A baseline for a file that does not exist yet (Add File / missing target). */
28
+ export declare function absentBaseline(path: PatchPath, absolutePath: string): FileBaseline;
29
+ /** The production loader over node:fs, the file-content decoder and the hash store. */
30
+ export declare function createDefaultPlanFileLoader(): PlanFileLoader;
31
+ /** Apply resolved baseline-coordinate mutations to a line array (descending, so coordinates stay valid). */
32
+ export declare function applyPlannedMutations(lines: readonly string[], mutations: readonly PlannedMutation[]): string[];
33
+ export declare function mutationDelta(baselineLines: readonly string[], mutation: PlannedMutation): {
34
+ added: number;
35
+ removed: number;
36
+ };
37
+ /**
38
+ * Plan one apply_patch call without writing anything.
39
+ *
40
+ * All hunks resolve against the pre-call baseline (never chained application);
41
+ * a later hunk's fragment window begins after the previous hunk's end. Any
42
+ * problem aborts the whole call: the returned list is every independently
43
+ * diagnosable problem, not just the first, together with the current rows of
44
+ * every named file (reject-and-serve).
45
+ *
46
+ * @param document - the parsed patch AST.
47
+ * @param options - cwd/session/store plus the optional baseline loader.
48
+ * @returns the plan, or every problem found plus the rows to serve.
49
+ */
50
+ export declare function planApplyPatch(document: PatchDocument, options: PlanApplyPatchOptions): Promise<PlanPatchResult>;
@@ -0,0 +1,51 @@
1
+ /**
2
+ * apply_patch v4 — renderer.
3
+ *
4
+ * One renderer owns both surfaces:
5
+ *
6
+ * - success: file-level summary lines first (`A`/`M`/`R`/`D`), then the
7
+ * changed-line anchor rows (`HASH│content`) per file, so a second call needs
8
+ * no `read`;
9
+ * - rejection: every problem as `- patch line N ([E_*], path): message`,
10
+ * followed by each problem file's current rows.
11
+ *
12
+ * @module dsh-hashline-edit/apply-patch/render
13
+ */
14
+ import type { ApplyPatchFailure, ApplyPatchResult, ApplyPatchSuccess, ApplyPatchToolResult, AppliedFileSummary, PatchProblem, ServedFileBlock } from "./types.js";
15
+ /**
16
+ * Render one apply_patch outcome into the model-facing tool envelope.
17
+ * @param result - the committed success or the collected rejection.
18
+ * @returns `{ content, details }` ready for the dsh tool registry.
19
+ */
20
+ export declare function renderApplyPatchResult(result: ApplyPatchResult): ApplyPatchToolResult;
21
+ /**
22
+ * Render a successful outcome.
23
+ * @param success - committed file facts plus new anchor rows.
24
+ * @returns the model-facing envelope.
25
+ */
26
+ export declare function renderSuccess(success: ApplyPatchSuccess): ApplyPatchToolResult;
27
+ /**
28
+ * Render a rejected outcome: all problems first, then the served rows.
29
+ * @param failure - the collected problems and current rows.
30
+ * @returns the model-facing envelope (as an `isError` tool result at the host seam).
31
+ */
32
+ export declare function renderFailure(failure: ApplyPatchFailure): ApplyPatchToolResult;
33
+ /**
34
+ * Render the file-level summary block: `Updated N files:` then one status line
35
+ * per file (`R src/old.ts -> src/new.ts` for a rename).
36
+ * @param files - the committed file summaries.
37
+ * @returns the summary text.
38
+ */
39
+ export declare function renderFileSummary(files: readonly AppliedFileSummary[]): string;
40
+ /**
41
+ * Render the new-anchor blocks for every changed file.
42
+ * @param blocks - per-file served rows.
43
+ * @returns one `<path>\nHASH│content` section per block.
44
+ */
45
+ export declare function renderServedBlocks(blocks: readonly ServedFileBlock[]): string;
46
+ /**
47
+ * Render one problem line: `- patch line N ([E_*], path): message`.
48
+ * @param problem - one collected problem.
49
+ * @returns the single-line rendering, without a trailing newline.
50
+ */
51
+ export declare function formatPatchProblem(problem: PatchProblem): string;