dsh-hashline-edit 0.2.2 → 0.3.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 (118) hide show
  1. package/README.md +57 -45
  2. package/cordis.patch.yml +9 -3
  3. package/lib/client.js +231 -0
  4. package/lib/index.js +3298 -3957
  5. package/lib/types/dsh/contracts.d.ts +12 -3
  6. package/lib/types/dsh/fs.d.ts +3 -0
  7. package/lib/types/dsh/guidance.d.ts +12 -6
  8. package/lib/types/dsh/hide.d.ts +79 -0
  9. package/lib/types/dsh/host-compat.d.ts +7 -78
  10. package/lib/types/dsh/index.d.ts +53 -10
  11. package/lib/types/dsh/metrics/events.d.ts +1 -1
  12. package/lib/types/dsh/metrics/observer.d.ts +1 -1
  13. package/lib/types/dsh/sandbox.d.ts +2 -2
  14. package/lib/types/dsh/shell-hook.d.ts +1 -1
  15. package/lib/types/dsh/tools/apply-patch.d.ts +38 -0
  16. package/lib/types/dsh/tools/read.d.ts +3 -3
  17. package/lib/types/src/apply-patch/commit.d.ts +49 -0
  18. package/lib/types/src/apply-patch/grammar.d.ts +29 -0
  19. package/lib/types/src/apply-patch/lifecycle.d.ts +32 -0
  20. package/lib/types/src/apply-patch/plan.d.ts +50 -0
  21. package/lib/types/src/apply-patch/render.d.ts +51 -0
  22. package/lib/types/src/apply-patch/resolve.d.ts +129 -0
  23. package/lib/types/src/apply-patch/tool.d.ts +54 -0
  24. package/lib/types/src/apply-patch/types.d.ts +362 -0
  25. package/lib/types/src/domain-errors.d.ts +16 -2
  26. package/lib/types/src/edit-undo.d.ts +11 -2
  27. package/lib/types/src/file-content/index.d.ts +1 -1
  28. package/lib/types/src/file-content/preview.d.ts +1 -1
  29. package/lib/types/src/fs-lifecycle.d.ts +44 -0
  30. package/lib/types/src/hashline/alphabet.d.ts +4 -3
  31. package/lib/types/src/hashline/hash-identity.d.ts +12 -7
  32. package/lib/types/src/host-utils.d.ts +63 -0
  33. package/lib/types/src/mutation-engine/edit-request.d.ts +34 -0
  34. package/lib/types/src/mutation-engine/pipeline.d.ts +8 -1
  35. package/lib/types/src/mutation-engine/types.d.ts +40 -50
  36. package/lib/types/src/served-session/serve-render.d.ts +6 -0
  37. package/lib/types/src/undo-store.d.ts +17 -1
  38. package/package.json +37 -21
  39. package/prompts/apply-patch-guidelines.md +4 -0
  40. package/prompts/apply-patch.md +1 -0
  41. package/prompts/read-guidelines.md +6 -6
  42. package/prompts/read.md +1 -1
  43. package/prompts/undo-last-edit-guidelines.md +2 -2
  44. package/prompts/undo-last-edit-snippet.md +1 -1
  45. package/prompts/undo-last-edit.md +1 -1
  46. package/prompts/zh/apply-patch-guidelines.md +4 -0
  47. package/prompts/zh/apply-patch.md +1 -0
  48. package/prompts/zh/read-guidelines.md +6 -0
  49. package/prompts/zh/read-snippet.md +1 -0
  50. package/prompts/zh/read.md +1 -0
  51. package/prompts/zh/undo-last-edit-guidelines.md +2 -0
  52. package/prompts/zh/undo-last-edit-snippet.md +1 -0
  53. package/prompts/zh/undo-last-edit.md +1 -0
  54. package/src/apply-patch/commit.ts +190 -0
  55. package/src/apply-patch/grammar.ts +620 -0
  56. package/src/apply-patch/index.ts +17 -0
  57. package/src/apply-patch/lifecycle.ts +195 -0
  58. package/src/apply-patch/plan.ts +800 -0
  59. package/src/apply-patch/render.ts +137 -0
  60. package/src/apply-patch/resolve.ts +470 -0
  61. package/src/apply-patch/tool.ts +257 -0
  62. package/src/apply-patch/types.ts +387 -0
  63. package/src/cut-repair.ts +77 -11
  64. package/src/domain-errors.ts +35 -3
  65. package/src/edit-undo.ts +379 -25
  66. package/src/file-content/index.ts +1 -1
  67. package/src/file-content/preview.ts +1 -1
  68. package/src/file-content/served-header.ts +1 -1
  69. package/src/fs-lifecycle.ts +201 -0
  70. package/src/hash-store.ts +18 -1
  71. package/src/hashline/alphabet.ts +7 -7
  72. package/src/hashline/hash-identity.ts +12 -7
  73. package/src/host-utils.ts +224 -0
  74. package/src/mutation-engine/edit-request.ts +39 -0
  75. package/src/mutation-engine/pipeline.ts +198 -27
  76. package/src/mutation-engine/types.ts +39 -57
  77. package/src/mutation-queue.ts +1 -1
  78. package/src/prompts.ts +3 -30
  79. package/src/served-session/serve-render.ts +2 -2
  80. package/src/undo-store.ts +74 -5
  81. package/lib/types/dsh/tools/edit.d.ts +0 -10
  82. package/lib/types/src/edit-tool.d.ts +0 -41
  83. package/lib/types/src/mutation-engine/engine.d.ts +0 -31
  84. package/lib/types/src/payload-contract.d.ts +0 -135
  85. package/prompts/edit-guidelines.md +0 -10
  86. package/prompts/edit-snippet.md +0 -1
  87. package/prompts/edit.md +0 -1
  88. package/prompts/read-snippet.md +0 -1
  89. package/src/bash-classifier.ts +0 -1626
  90. package/src/bash-conversion/decide.ts +0 -812
  91. package/src/bash-display-transform.ts +0 -50
  92. package/src/bash-multifile-config.ts +0 -239
  93. package/src/bash-view-config.ts +0 -158
  94. package/src/edit-normalize.ts +0 -8
  95. package/src/edit-render.ts +0 -161
  96. package/src/edit-tool.ts +0 -118
  97. package/src/edit.ts +0 -136
  98. package/src/integrations/pi-lens/command.ts +0 -712
  99. package/src/integrations/pi-lens/config.ts +0 -148
  100. package/src/integrations/pi-lens/io-bridge-adapter.ts +0 -234
  101. package/src/integrations/pi-lens/io-bridge.ts +0 -112
  102. package/src/integrations/pi-lens/settings-overlay.ts +0 -272
  103. package/src/lifecycle-hooks/index.ts +0 -1992
  104. package/src/lifecycle-hooks/types.ts +0 -64
  105. package/src/mutated-files.ts +0 -75
  106. package/src/mutation-engine/engine.ts +0 -239
  107. package/src/mutation-engine/index.ts +0 -17
  108. package/src/payload-contract.ts +0 -685
  109. package/src/preview-controller.ts +0 -84
  110. package/src/read.ts +0 -865
  111. package/src/search-hook.ts +0 -41
  112. package/src/store-config.ts +0 -73
  113. package/src/store-janitor-warning.ts +0 -169
  114. package/src/store-janitor.ts +0 -539
  115. package/src/store-kernel/settings.ts +0 -233
  116. package/src/store.ts +0 -154
  117. package/src/tui-presenter.ts +0 -134
  118. package/src/write-hook.ts +0 -130
@@ -1,4 +1,4 @@
1
- import { type TruncationResult } from "@earendil-works/pi-coding-agent";
1
+ import { type TruncationResult } from "../host-utils.js";
2
2
  import { type AnchorWalk } from "../hashline/hash-identity.js";
3
3
  import type { ServedRow } from "../served-session/serve-render.js";
4
4
  /** The lines one page actually rendered, 1-based and inclusive. */
@@ -0,0 +1,44 @@
1
+ /**
2
+ * Filesystem primitives for apply_patch's file lifecycle: classify, create directories, delete a
3
+ * regular file and rename one. Every entry point resolves through {@link resolveTarget}, the same
4
+ * symlink-resolving boundary every read/write uses, so a lifecycle action can never escape it.
5
+ *
6
+ * These are primitives, not a second write engine: file CONTENTS still go through writeAtomic;
7
+ * this module only owns namespace operations that writeAtomic deliberately does not express.
8
+ *
9
+ * @module dsh-hashline-edit/fs-lifecycle
10
+ */
11
+ /** The physical kind of a path as the file layer sees it (symlinks resolved first). */
12
+ export type PathKind = "file" | "directory" | "other" | "absent";
13
+ /** Classify one path through the file layer's symlink-resolving boundary. */
14
+ export declare function pathKind(path: string): Promise<PathKind>;
15
+ /**
16
+ * True when `path` resolves inside `root` once every existing symlink/junction component is
17
+ * followed. A missing tail is checked against its nearest existing ancestor: an Add File under a
18
+ * junction ancestor therefore fails before mkdir can create anything outside the workspace.
19
+ */
20
+ export declare function isPathWithinWorkspace(root: string, path: string): Promise<boolean>;
21
+ /** Throw the E_PATCH_SYNTAX boundary refusal for one escaping lifecycle path. */
22
+ export declare function assertPathWithinWorkspace(root: string, path: string): Promise<void>;
23
+ /** Create one directory (and missing parents), through the symlink-resolving boundary. */
24
+ export declare function mkdirp(path: string, workspaceRoot?: string): Promise<void>;
25
+ /**
26
+ * Delete one regular file. Deliberately NOT recursive: a lifecycle delete targets a file, and a
27
+ * directory at the same path is a caller bug that must surface, never be removed wholesale.
28
+ */
29
+ export declare function removeFile(path: string, signal?: AbortSignal): Promise<void>;
30
+ /**
31
+ * Rename one regular file, creating the destination parent first. The destination must not exist:
32
+ * a move never clobbers another file (spec §6: target-exists is a rejection, not an overwrite).
33
+ */
34
+ export declare function renameFile(from: string, to: string, signal?: AbortSignal, workspaceRoot?: string): Promise<void>;
35
+ /**
36
+ * The directories on the way to `path` that do not exist yet, OUTERMOST FIRST. Captured before a
37
+ * lifecycle add writes, so an undo removes exactly the directories the add created and no others.
38
+ */
39
+ export declare function missingAncestorDirs(path: string): Promise<string[]>;
40
+ /**
41
+ * Best-effort removal of directories a lifecycle add created, deepest first. Returns the removed
42
+ * paths. A directory that is no longer empty (an outside writer put something there) is kept.
43
+ */
44
+ export declare function pruneEmptyDirs(dirs: readonly string[]): Promise<string[]>;
@@ -3,9 +3,10 @@
3
3
  *
4
4
  * WHY an environment variable rather than a settings-file key: `alphabet.ts` is a zero-import leaf
5
5
  * and every shape, regex, bitset and capacity constant below is derived at module load, so the
6
- * width can only be a process-wide, load-time value. Switching it changes every served anchor; the
7
- * persisted store stays safe because the anchor generation (`hash-identity.ts`) folds the width in,
8
- * so a switch is a hard generation miss (re-read), never a cross-width hit.
6
+ * width can only be a process-wide, load-time value. The build bakes it via esbuild `define`
7
+ * (`__ANCHOR_WIDTH__`), so a shipped bundle carries a literal and never reads the environment; the
8
+ * anchor generation (`hash-identity.ts`) folds the width in, so a width switch is a hard generation
9
+ * miss (re-read), never a cross-width hit.
9
10
  */
10
11
  export declare const ANCHOR_WIDTH_ENV_VAR = "PI_BETTER_EDIT_ANCHOR_WIDTH";
11
12
  /**
@@ -61,23 +61,28 @@ export declare const HASH_PROBE_STRIDE: number;
61
61
  */
62
62
  export declare const CANON_VERSION = 3;
63
63
  /**
64
- * The file-scoped anchor-derivation generation (issue #20, ADR-0031; ADR-0020 for the width axis).
64
+ * ANCHOR_GENERATION 1 (issue #20, ADR-0031): the file-scoped anchor-derivation generation.
65
65
  * It covers exactly: (1) the width (`HASH_LEN`, `ALPHA`, `HASH_SPACE`/`USABLE_HASH_SPACE`);
66
66
  * (2) the (path, content) seeding (`fileBaseIndex` seeds xxh32 with the canonical absolute path,
67
67
  * `contentBaseIndex` is the explicit content-only fallback); (3) the all-digit reservation
68
68
  * (`DIGIT_CHARS`/`RESERVED_HASH_SPELLINGS`/`RESERVED_BITS` pre-marked in allocation); (4) the probe
69
69
  * stride (`HASH_PROBE_STRIDE`, coprime with both spaces). Bump it whenever any of those change.
70
70
  *
71
- * WHY it is a function of the width now: the width is a runtime switch (default 4, optional 3), so a
72
- * width change MUST flip the generation or a store written at one width could be read at another.
73
- * Width 4 stays generation 1 — the original persisted generation — so existing stores keep their
74
- * anchors across this change; width 3 gets generation 2, and any future width gets `100 + width`.
75
- *
76
- * WHY: single owner of the anchor-generation value — anchor-bearing artifacts (the undo/snapshot
71
+ * WHY: single owner of the anchor-generation literal — anchor-bearing artifacts (the undo/snapshot
77
72
  * generation gate and the open-time sweep) read this axis only, never `CANON_VERSION`; the snapshot
78
73
  * cache key carries both (`${CANON_VERSION}:${ANCHOR_GENERATION}:${checksum}`), so a canon-only change
79
74
  * misses the cache without invalidating anchor state while a generation change invalidates both.
80
75
  */
76
+ /**
77
+ * The file-scoped anchor-derivation generation. It covers the width (`HASH_LEN`, `ALPHA`,
78
+ * `HASH_SPACE`/`USABLE_HASH_SPACE`), the (path, content) seeding, the all-digit reservation and the
79
+ * probe stride. Bump it whenever any of those change.
80
+ *
81
+ * WHY it is a function of the width: the width is a build-time switch (ADR-0020: default 4, optional
82
+ * 3), so a width change MUST flip the generation or a store written at one width could be read at
83
+ * another. Width 4 stays generation 1 — the original persisted generation — so existing stores keep
84
+ * their anchors; width 3 gets generation 2, and any future width gets `100 + width`.
85
+ */
81
86
  export declare function anchorGenerationFor(width: number): number;
82
87
  export declare const ANCHOR_GENERATION: number;
83
88
  export declare function canon(line: string): string;
@@ -0,0 +1,63 @@
1
+ /** Retained line budget, matching pi's `DEFAULT_MAX_LINES`. */
2
+ export declare const DEFAULT_MAX_LINES = 2000;
3
+ /** Retained byte budget, matching pi's `DEFAULT_MAX_BYTES` (50KB). */
4
+ export declare const DEFAULT_MAX_BYTES: number;
5
+ /** The truncation report the core's read page renders. */
6
+ export interface TruncationResult {
7
+ /** The truncated content. */
8
+ content: string;
9
+ /** Whether truncation occurred. */
10
+ truncated: boolean;
11
+ /** Which limit was hit: `"lines"`, `"bytes"`, or `null` when nothing was cut. */
12
+ truncatedBy: "lines" | "bytes" | null;
13
+ /** Total number of lines in the original content. */
14
+ totalLines: number;
15
+ /** Total number of bytes in the original content. */
16
+ totalBytes: number;
17
+ /** Number of complete lines in the truncated output. */
18
+ outputLines: number;
19
+ /** Number of bytes in the truncated output. */
20
+ outputBytes: number;
21
+ /** Whether the last line was partially truncated (tail truncation only). */
22
+ lastLinePartial: boolean;
23
+ /** Whether the first line exceeded the byte limit (head truncation only). */
24
+ firstLineExceedsLimit: boolean;
25
+ /** The line limit that was applied. */
26
+ maxLines: number;
27
+ /** The byte limit that was applied. */
28
+ maxBytes: number;
29
+ }
30
+ /** The two independent limits a truncation call applies. */
31
+ export interface TruncationOptions {
32
+ /** Maximum number of lines; defaults to {@link DEFAULT_MAX_LINES}. */
33
+ maxLines?: number;
34
+ /** Maximum number of bytes; defaults to {@link DEFAULT_MAX_BYTES}. */
35
+ maxBytes?: number;
36
+ }
37
+ /**
38
+ * Format bytes as a human-readable size (`512B`, `1.5KB`, `2.0MB`).
39
+ * @param bytes - the byte count to render.
40
+ * @returns the pi-compatible size label.
41
+ */
42
+ export declare function formatSize(bytes: number): string;
43
+ /**
44
+ * Truncate content from the head (keep the first N lines / bytes).
45
+ *
46
+ * WHY: the read page's oversize and truncation warnings must agree with the pi
47
+ * implementation the rest of the contract was written against, so this is a
48
+ * faithful port including its two edge rules: complete lines only, and an
49
+ * over-long first line yields empty content with `firstLineExceedsLimit`.
50
+ * @param content - the text to truncate.
51
+ * @param options - the line and byte limits.
52
+ * @returns the truncation report.
53
+ */
54
+ export declare function truncateHead(content: string, options?: TruncationOptions): TruncationResult;
55
+ /**
56
+ * Serialize file mutation operations that target the same file; different files still run
57
+ * in parallel. A faithful port of pi's queue so the core's cut transaction keeps its
58
+ * lock ordering guarantees under the dsh host.
59
+ * @param filePath - the file the mutation targets.
60
+ * @param run - the critical section to run once the path is free.
61
+ * @returns the critical section's result.
62
+ */
63
+ export declare function withFileMutationQueue<T>(filePath: string, run: () => Promise<T>): Promise<T>;
@@ -0,0 +1,34 @@
1
+ /** The internal payload vocabulary the mutation engine consumes; no wire schema lives here. */
2
+ type SpanRef = {
3
+ anchor_from: string;
4
+ anchor_to: string;
5
+ file?: string;
6
+ mode: "copy" | "cut";
7
+ };
8
+ type EditMode = "general" | "literal";
9
+ type Placement = "in-place" | "before" | "after";
10
+ export type DesiredContent = {
11
+ kind: "literal";
12
+ text: string;
13
+ } | {
14
+ kind: "reference";
15
+ span: SpanRef;
16
+ mode: "copy" | "cut";
17
+ } | {
18
+ kind: "empty";
19
+ };
20
+ export type NormalizedEditItem = {
21
+ target: {
22
+ anchor_from: string;
23
+ anchor_to: string;
24
+ };
25
+ at: Placement;
26
+ payload: DesiredContent;
27
+ file?: string;
28
+ };
29
+ export type NormalizedEditRequest = {
30
+ file: string;
31
+ edits: NormalizedEditItem[];
32
+ mode?: EditMode;
33
+ };
34
+ export {};
@@ -1,8 +1,9 @@
1
- import type { NormalizedEditRequest } from "../payload-contract.js";
1
+ import type { NormalizedEditRequest } from "./edit-request.js";
2
2
  import { buildBatchResult } from "../edit-response.js";
3
3
  import type { PipelineOptions, ProcessedEditFile } from "./types.js";
4
4
  export type { PipelineOptions, ProcessedEditFile };
5
5
  export declare function previewEdits(request: NormalizedEditRequest, cwd: string, options?: Omit<PipelineOptions, "noPersist">): Promise<ProcessedEditFile>;
6
+ declare function runCorrelatedTransaction(writePlans: ProcessedEditFile[], sectionPlans: ProcessedEditFile[], options: PipelineOptions | undefined, sessionKey: string): Promise<BatchOutcome>;
6
7
  /** What one plan or committed batch reports: the primary file leads, every section rides the result. */
7
8
  interface BatchOutcome {
8
9
  result: string;
@@ -20,3 +21,9 @@ interface BatchOutcome {
20
21
  export declare function planEditTargets(request: NormalizedEditRequest, cwd: string, options?: PipelineOptions): Promise<ProcessedEditFile[] | undefined>;
21
22
  export declare function apply(request: NormalizedEditRequest, cwd: string, options?: PipelineOptions): Promise<BatchOutcome>;
22
23
  export declare function execEdits(request: NormalizedEditRequest, cwd: string, options?: PipelineOptions): Promise<ProcessedEditFile>;
24
+ /**
25
+ * Visibility-only export of the correlated transaction for apply_patch's commit seam. The
26
+ * function body is the one the cut path already uses; apply_patch members carry an optional
27
+ * lifecycle action read by that body. No engine semantics change.
28
+ */
29
+ export { runCorrelatedTransaction as commitTransaction };
@@ -7,10 +7,9 @@
7
7
  import type { LineEnding } from "../edit-diff.js";
8
8
  import type { ResolvedRange } from "../served-session/serve-render.js";
9
9
  import type { HashStore } from "../hash-store.js";
10
- import type { EditDetails, RMetrics } from "../edit-response.js";
11
- import type { DomainErrorCode, RangeCause } from "../domain-errors.js";
12
- import type { ServedRow } from "../domain-errors.js";
13
10
  export interface PipelineOptions {
11
+ /** Workspace root used to re-assert lifecycle path boundaries at commit time. */
12
+ workspaceRoot?: string;
14
13
  accessMode?: number;
15
14
  signal?: AbortSignal;
16
15
  store?: HashStore;
@@ -32,6 +31,21 @@ export interface PipelineOptions {
32
31
  * window cannot show that.
33
32
  */
34
33
  onBeforeFirstCutWrite?: () => void | Promise<void>;
34
+ /**
35
+ * TEST-ONLY mid-member fault seam: called after a move's rename is durable and before its
36
+ * destination content is rewritten, so the rollback of that in-flight member is exercised.
37
+ */
38
+ onAfterMoveRename?: (toAbsolutePath: string) => void | Promise<void>;
39
+ }
40
+ /**
41
+ * The honest outcome of a failed multi-member commit's rollback attempt. `restored` members are
42
+ * proven back at their pre-images; `remaining` members were not rolled back; `uncertain` members
43
+ * are those whose rollback attempt itself threw, so neither state can be claimed for them.
44
+ */
45
+ export interface TransactionRecoveryReport {
46
+ restored: string[];
47
+ remaining: string[];
48
+ uncertain: string[];
35
49
  }
36
50
  /**
37
51
  * (ticket-04b) One foreign `mode: "cut"` item as seen by the pre-pass: the submitted span anchors
@@ -45,6 +59,26 @@ export interface ForeignCutRecord {
45
59
  spanFrom: string;
46
60
  spanTo: string;
47
61
  }
62
+ /**
63
+ * The file-lifecycle action one transaction member carries.
64
+ *
65
+ * This is the engine-local vocabulary: apply_patch's plan IR maps onto it in
66
+ * `src/apply-patch/commit.ts` so the mutation engine never imports the apply-patch layer.
67
+ * Absent is an ordinary in-place edit.
68
+ */
69
+ export type MemberLifecycle = {
70
+ kind: "keep";
71
+ } | {
72
+ kind: "add";
73
+ createdDirs: readonly string[];
74
+ } | {
75
+ kind: "delete";
76
+ } | {
77
+ kind: "move";
78
+ fromAbsolutePath: string;
79
+ toAbsolutePath: string;
80
+ createdDirs: readonly string[];
81
+ };
48
82
  export interface ProcessedEditFile {
49
83
  path: string;
50
84
  absolutePath: string;
@@ -88,53 +122,9 @@ export interface ProcessedEditFile {
88
122
  * correlated two-file transaction; the in-memory buffer itself never touches these files.
89
123
  */
90
124
  foreignCuts: ForeignCutRecord[];
91
- }
92
- export interface MutationSuccess {
93
- ok: true;
94
- /** SAFETY: Normalized result content (LF). */
95
- result: string;
96
- /** SAFETY: Unified diff (hash-anchored) for model consumption. */
97
- diff: string;
98
- /** SAFETY: User-facing drift notice, if any (details only, not model content). */
99
- drift: string | undefined;
100
- /** SAFETY: Metrics for telemetry. */
101
- metrics: RMetrics;
102
- raw: ProcessedEditFile;
103
- /** SAFETY: Full tool result (content + details) for pi's tool_result hook. */
104
- toolResult: {
105
- content: Array<{
106
- type: "text";
107
- text: string;
108
- }>;
109
- details: EditDetails;
110
- };
111
- }
112
- export interface MutationFailure {
113
- ok: false;
114
- /**
115
- * SAFETY: Machine code — a registry member (`DomainErrorCode`). `toFailure` validates every
116
- * caught code through the envelope reader (`src/error-envelope.ts`): non-registry strings
117
- * (errno pass-through included) route to `E_UNKNOWN`, so the union is honest.
118
- */
119
- code: DomainErrorCode;
120
- /** SAFETY: Human message — model-facing signal when applicable. */
121
- message: string;
122
- /** SAFETY: Fresh served block for retry when available (reject-and-serve). */
123
- servedBlock?: string;
124
- servedRows?: ServedRow[];
125
125
  /**
126
- * SAFETY: the RAW `E_BAD_PAYLOAD` message behind a rendered refusal, when the route is one, so
127
- * SAFETY: the tool seam can name the invoking tool instead of falling back to the neutral
128
- * SAFETY: wording. Present only for `code: "E_BAD_PAYLOAD"`.
126
+ * apply_patch lifecycle action for this member; absent is an ordinary edit. The correlated
127
+ * transaction reads it for pre-image capture, commit ordering, rollback and undo-row shape.
129
128
  */
130
- payloadMessage?: string;
131
- /** SAFETY: User-facing diagnosis, never a model remedy — a GLOSSARY.md glossary term. */
132
- cause?: RangeCause;
133
- details?: {
134
- code: DomainErrorCode;
135
- cause: RangeCause;
136
- };
129
+ lifecycle?: MemberLifecycle;
137
130
  }
138
- export type MutationResult = MutationSuccess | MutationFailure;
139
- export declare function isMutationSuccess(r: MutationResult): r is MutationSuccess;
140
- export declare function isMutationFailure(r: MutationResult): r is MutationFailure;
@@ -14,7 +14,13 @@ export interface FileSnapshotContext {
14
14
  export declare function buildRangeServeRows(startLine: number, endLine: number, fileHashes: string[]): ServedRow[];
15
15
  export declare function denseServeRows(hashes: readonly string[]): ServedRow[];
16
16
  export declare function fmtServedRows(rows: ServedRow[], fileLines: string[]): string;
17
+ export declare function paginationHint(nextOffset: number, more: number): string;
17
18
  export declare function servedPositionsOf(served: (string | null)[], hash: string): number[];
19
+ /** Builds the reject-and-serve served rows + rendered block for a current on-disk range. */
20
+ export declare function buildRangeServeBlock(startLine: number, endLine: number, fileHashes: string[], fileLines: string[]): {
21
+ servedRows: ServedRow[];
22
+ rendered: string;
23
+ };
18
24
  /**
19
25
  * Builds an `[E_TARGET_LOST]` rejection for a retired leased identity whose range cannot be
20
26
  * identified (spec stale-identity-reject-and-serve D1/D6, ADR-0018 decisions 1-2). The payload
@@ -25,7 +25,23 @@ export interface UndoRecord {
25
25
  * `null` is a row from before this column, or an ordinary single-file edit.
26
26
  */
27
27
  rawPre?: string | null;
28
+ /**
29
+ * The lifecycle action this row reverses. Absent/null is an ordinary edit (back-compatible).
30
+ * add = the file did not exist before; delete = the file does not exist after; move = a pure
31
+ * rename with an optional content edit.
32
+ */
33
+ lifecycle?: UndoLifecycle | null;
34
+ /** Move: the absolute path the file had before the transaction. */
35
+ fromPath?: string | null;
36
+ /** Move: the absolute path the file has after the transaction. */
37
+ toPath?: string | null;
38
+ /** The RAW post-transaction text when the commit wrote non-canonical bytes (pure move). */
39
+ rawPost?: string | null;
40
+ /** Add: the directories the add created, outermost first, JSON-encoded in the column. */
41
+ createdDirs?: readonly string[] | null;
28
42
  }
43
+ /** The lifecycle action one undo row reverses. */
44
+ export type UndoLifecycle = "edit" | "add" | "delete" | "move";
29
45
  export type CutIntentDirection = "revert" | null;
30
46
  export interface CutIntent {
31
47
  txnId: string;
@@ -33,7 +49,7 @@ export interface CutIntent {
33
49
  direction: CutIntentDirection;
34
50
  }
35
51
  export interface UndoStmts {
36
- undoUpsert: (path: string, content: string, bom: string, ending: string, hashes: string, resultContent: string, snapshotHash: string | null, transactionId: string | null, rawPre: string | null, anchorGeneration: number, updatedAt: number) => void;
52
+ undoUpsert: (path: string, content: string, bom: string, ending: string, hashes: string, resultContent: string, snapshotHash: string | null, transactionId: string | null, rawPre: string | null, lifecycle: string | null, fromPath: string | null, toPath: string | null, rawPost: string | null, createdDirs: string | null, anchorGeneration: number, updatedAt: number) => void;
37
53
  undoGet: (path: string) => Record<string, unknown> | undefined;
38
54
  undoDelete: (path: string) => void;
39
55
  undoGetTransaction: (transactionId: string) => Record<string, unknown>[];
package/package.json CHANGED
@@ -1,14 +1,14 @@
1
1
  {
2
2
  "name": "dsh-hashline-edit",
3
- "version": "0.2.2",
4
- "description": "Hash-anchored read/edit/undo_last_edit tools for DeepSeek Harness (dsh). Every line gets a unique 3- or 4-char hash (A-Za-z0-9, configurable) that stays stable across edits; stale or ambiguous anchors are rejected, never fuzzy-matched. Undo persists across restarts.",
3
+ "version": "0.3.0",
4
+ "description": "Hash-anchored read/apply_patch tools for DeepSeek Harness (dsh). Every line gets a unique 3- or 4-char hash (A-Za-z0-9, configurable) that stays stable across edits; apply_patch is the only file-mutation entry point and commits one patch batch per transaction. Undo persists across restarts.",
5
5
  "keywords": [
6
6
  "anchors",
7
+ "apply-patch",
7
8
  "coding-agent",
8
9
  "deepseek-harness",
9
10
  "dsh",
10
11
  "dsh-plugin",
11
- "edit",
12
12
  "file-editing",
13
13
  "hash",
14
14
  "hash-anchored",
@@ -42,6 +42,9 @@
42
42
  "types": "./lib/types/dsh/index.d.ts",
43
43
  "default": "./lib/index.js"
44
44
  },
45
+ "./client": {
46
+ "default": "./lib/client.js"
47
+ },
45
48
  "./package.json": "./package.json"
46
49
  },
47
50
  "scripts": {
@@ -49,6 +52,7 @@
49
52
  "test:watch": "vitest",
50
53
  "test:coverage": "vitest run --coverage --coverage.thresholds.lines=85 --coverage.thresholds.statements=85 --coverage.thresholds.functions=85 --coverage.thresholds.branches=80",
51
54
  "test:win": "vitest run --config vitest.config.windows.ts",
55
+ "test:win:3": "node scripts/test-width.mjs 3 dsh",
52
56
  "test:win:coverage": "vitest run --config vitest.config.windows.ts --coverage --coverage.thresholds.lines=85 --coverage.thresholds.statements=85 --coverage.thresholds.functions=85 --coverage.thresholds.branches=80",
53
57
  "lint": "oxlint . && knip --max-issues 0",
54
58
  "lint:knip": "knip --max-issues 0",
@@ -56,17 +60,8 @@
56
60
  "format:fix": "oxfmt .",
57
61
  "typecheck": "tsc --noEmit && tsc --noEmit -p tsconfig.dsh.json",
58
62
  "build": "node scripts/build-dsh.mjs",
59
- "build:pi": "node scripts/build-dist.mjs",
60
63
  "prepublishOnly": "node scripts/build-dsh.mjs && pnpm run typecheck && pnpm run lint && pnpm run test:win",
61
64
  "canon:audit": "node scripts/canon-churn-audit.mjs",
62
- "prompts:show": "node scripts/served-prompts.mjs",
63
- "replay:read-expansion": "node scripts/read-expansion-replay.mjs",
64
- "test:runtime": "node scripts/runtime-edge-test.mjs",
65
- "verify:dist": "node scripts/verify-dist.mjs",
66
- "verify:dist:mutations": "node scripts/verify-dist-mutations.mjs",
67
- "measure:import": "node scripts/measure-import.mjs --max-ratio 0.75",
68
- "smoke:install": "node scripts/install-smoke.mjs",
69
- "prepare": "node scripts/prepare.mjs",
70
65
  "prepack": "node scripts/build-dsh.mjs",
71
66
  "audit": "pnpm audit --audit-level high --registry https://registry.npmjs.org/",
72
67
  "verify": "pnpm run lint && pnpm run format && pnpm run typecheck && pnpm run test:win:coverage"
@@ -74,7 +69,6 @@
74
69
  "dependencies": {
75
70
  "diff": "^8.0.2",
76
71
  "file-type": "^21.3.0",
77
- "typebox": "^1.3.27",
78
72
  "unbash": "5.0.0",
79
73
  "xxhash-wasm": "^1.1.0"
80
74
  },
@@ -82,39 +76,61 @@
82
76
  "@babel/parser": "7.29.8",
83
77
  "@commitlint/cli": "^21.2.2",
84
78
  "@commitlint/config-conventional": "^21.2.2",
85
- "@deepseek-ai/cordis": "^4.0.5",
86
- "@deepseek-ai/dsh-agent": "0.2.1-rc.2",
87
- "@deepseek-ai/dsh-fs": "0.2.1-rc.2",
88
- "@deepseek-ai/dsh-home-paths": "0.2.1-rc.2",
89
- "@deepseek-ai/dsh-sandbox": "0.2.1-rc.2",
90
- "@deepseek-ai/dsh-tools": "0.2.1-rc.2",
91
- "@earendil-works/pi-coding-agent": "^0.84.0",
92
- "@earendil-works/pi-tui": "*",
79
+ "@deepseek-ai/cordis": "4.1.0",
80
+ "@deepseek-ai/dsh-agent": "0.2.2-rc.1",
81
+ "@deepseek-ai/dsh-fs": "0.2.2-rc.1",
82
+ "@deepseek-ai/dsh-home-paths": "0.2.2-rc.1",
83
+ "@deepseek-ai/dsh-sandbox": "0.2.2-rc.1",
84
+ "@deepseek-ai/dsh-tools": "0.2.2-rc.1",
85
+ "@deepseek-ai/schemastery": "3.19.0",
93
86
  "@semantic-release/git": "^11.0.1",
94
87
  "@semantic-release/github": "^12.0.9",
95
88
  "@semantic-release/npm": "^13.1.5",
96
89
  "@types/node": "^24.0.0",
90
+ "@types/react": "18.3.1",
97
91
  "@vitest/coverage-v8": "^4.1.10",
98
92
  "esbuild": "0.28.2",
99
93
  "husky": "^9.1.7",
100
94
  "knip": "6.40.0",
101
95
  "oxfmt": "^0.66.0",
102
96
  "oxlint": "^1.81.0",
97
+ "react": "18.3.1",
103
98
  "semantic-release": "^25.0.9",
104
99
  "typescript": "^7.0.0",
105
100
  "vite": "^8.3.5",
106
101
  "vitest": "^4.1.8"
107
102
  },
103
+ "peerDependencies": {
104
+ "@deepseek-ai/cordis": "~4.1.0",
105
+ "@deepseek-ai/dsh-agent": ">=0.2.2-rc.1 <0.2.3-0",
106
+ "@deepseek-ai/dsh-fs": ">=0.2.2-rc.1 <0.2.3-0",
107
+ "@deepseek-ai/dsh-home-paths": ">=0.2.2-rc.1 <0.2.3-0",
108
+ "@deepseek-ai/dsh-sandbox": ">=0.2.2-rc.1 <0.2.3-0",
109
+ "@deepseek-ai/dsh-tools": ">=0.2.2-rc.1 <0.2.3-0"
110
+ },
108
111
  "optionalDependencies": {
109
112
  "web-tree-sitter": "0.27.1"
110
113
  },
111
114
  "engines": {
115
+ "dsh": ">=0.2.2-rc.1 <0.2.3-0",
112
116
  "node": ">=24.0.0"
113
117
  },
114
118
  "packageManager": "pnpm@12.0.0",
115
119
  "dsh": {
116
120
  "bundle": {
117
121
  "patch": "./cordis.patch.yml"
122
+ },
123
+ "client": {
124
+ "inject": [
125
+ "@deepseek-ai/dsh-api-gateway",
126
+ "@deepseek-ai/dsh-client-ui-renderer",
127
+ "@deepseek-ai/dsh-client-locale",
128
+ "@deepseek-ai/dsh-client-ui-plugin-manager",
129
+ "@deepseek-ai/dsh-client-ui-slots",
130
+ "@deepseek-ai/dsh-client-ui-primitives"
131
+ ],
132
+ "platform": "web",
133
+ "immediately": true
118
134
  }
119
135
  }
120
136
  }
@@ -0,0 +1,4 @@
1
+ - apply_patch: one call = one transaction; batch related changes and split only when an earlier RESULT decides the next.
2
+ - apply_patch: hunks are validated against the pre-call file in order (overlapping ranges reject the batch); a fragment hunk must match exactly once — add context to disambiguate, never guess — while identity hunks target served rows and a stale or unknown anchor is rejected; both serve the current rows back.
3
+ - apply_patch: a rejection writes nothing; retry from the served `HASH│content` rows without another `read`.
4
+ - apply_patch: success returns fresh anchors for changed lines; whole-file replacement is one identity hunk over every served line.
@@ -0,0 +1 @@
1
+ Create, update, delete, move, copy, or cut UTF-8 text files with a single transactional patch; prefer it over multiple single-file edits when a change spans files or separated regions. Pass the complete patch as `input`.
@@ -1,6 +1,6 @@
1
- - read: call it only for information the tool never served you, such as content past the auto-read diff. A bash view or an auto-read already served the file, so edit from those rows.
2
- - read: when you need several blocks or both boundaries of one replacement, put every range in ONE call. Use `{ file, windows: [{ offset, limit }] }`. Each window is served in that turn, so its anchors work in the same edit. A window can also be `{ around_anchor, radius }` to centre on a line this session already served. An anchor outside this session's served rows is omitted with an inline `[Window warning: ...]`.
3
- - read: a requested window of 61 lines or fewer can widen to its enclosing block; overlapping or adjacent windows fold before this test. Caps are 80 lines for code and Markdown, 40 for plain text. Widened rows begin with `[widened <requested> -> <served>, <label>]`; above three requested windows the note is `[widened N windows: <ranges>]`. An over-cap Markdown section clamps and names `(section <extent>)`; over-cap code or plain text refuses with no note. `mode: "verbatim"` widens too, with no anchors.
4
- - read: when you will not edit the file, pass `mode: "verbatim"` for plain content with no anchors. Examples are a config value, docs, a README, or skill content (a SKILL.md or any file in its directory). Omit `mode` when you intend to edit, so the rows carry anchors.
5
- - read: send one file per call as `{ file, windows?, mode? }`, the same way `edit` does. The single top-level `mode` applies to the file. To read several files, make parallel `read` calls.
6
- - read: PNG/JPG images return as image attachments for multimodal models. Request one with `file` like any other file.
1
+ - read: call it for content the tool has never served you; a bash view or an auto-read already served the file, so `apply_patch` can use those rows.
2
+ - read: batch several disjoint ranges in ONE call with `windows` — an offset/limit range, or `around_anchor` plus `radius` around a line this session served.
3
+ - read: pass `mode: "verbatim"` when you will not `apply_patch` the file (config, docs, a README, skill files); otherwise omit it so the rows carry anchors.
4
+ - read: a window of 61 lines or fewer may widen to its enclosing block; widened rows carry a `[widened …]` note. Caps are 80 lines for code/Markdown and 40 for plain text.
5
+ - read: read several files with parallel `read` calls, one file per call.
6
+ - read: PNG/JPG/GIF/WebP files return as an image attachment for multimodal models; binary files and directories are refused.
package/prompts/read.md CHANGED
@@ -1 +1 @@
1
- Read one text file via `file`, with optional `windows` and top-level `mode`. To read several files, make parallel `read` calls. Served mode (default) returns each line as HASH│content with a {{HASH_LEN}}-char alphanumeric hash. That hash is the `edit` anchor, so copy only the {{HASH_LEN}} chars before `│`. Pass `mode: "verbatim"` for plain text with no anchors when you will not edit. `mode: "verbatim"` is not capped by line budget. Name ranges with `windows`: `[{ offset, limit }]` (offset is 1-indexed, limit is a line count, maximum 16 windows) or `[{ around_anchor: "<anchor>", radius: 10 }]` for a line this session served. When a page stops short, continue from the next unshown line with `windows: [{ offset, limit }]`. A requested window of 61 lines or fewer can widen to its enclosing block before serving; overlapping or adjacent windows fold before this test. Caps are 80 lines for code and Markdown, 40 for plain text. A widened page begins with `[widened <requested> -> <served>, <label>]`; more than three requested windows collapse to `[widened N windows: <ranges>]`. An over-cap Markdown section clamps to a cap-sized window and names `(section <extent>)`; an over-cap code or plain-text block refuses, serving the requested window. `mode: "verbatim"` widens the same way but adds no anchors. Inline `[...]` notices describe the served result, not file content. Empty files return `HASH│` (edit to insert). PNG/JPG/GIF/WebP return as image attachments for multimodal models. Binary and directory files are rejected. Hashes are file-scoped: an anchor is bound to the file it was served from.
1
+ Reads a UTF-8 file as `HASH│content` rows in served mode; each row's anchor is the `apply_patch` identity anchor for that line. Pass `mode: "verbatim"` for plain text with no anchors when you will not apply_patch; verbatim is not capped by line budget. Batch disjoint ranges in one call with `windows` (offset/limit or `around_anchor`).
@@ -1,2 +1,2 @@
1
- - undo_last_edit: reverts the most recent `edit` call on the file at `path`, and every item of its `edits`. Its scope is the edited file. The exception is an edit that was a foreign `cut`: undo then restores the source file too. The file must still hold the edited bytes. An out-of-band write makes the call fail with `E_UNDO_STALE`, and the file stays byte-identical. Call it as soon as a result is bad.
2
- - undo_last_edit: a `write` to the file, or a later `edit`, replaces the undo entry. A read, a bash view or an out-of-band write leaves the entry in place. An edit is bad when its post-edit diff shows `-HASH│` rows for lines you meant to keep. Examples are a closing brace, an import, or a declaration.
1
+ - undo_last_edit: reverts the most recent `apply_patch` batch that touched the file at `path`, and every member of that batch: text hunks plus file lifecycle actions (add/delete/move). The exception is a foreign `cut`: undo then restores the source file too. The file must still hold the edited bytes. An out-of-band write makes the call fail with `E_UNDO_STALE`, and the file stays byte-identical. Call it as soon as a result is bad.
2
+ - undo_last_edit: a `write` to the file, or a later `apply_patch` batch, replaces the undo entry. A read, a bash view or an out-of-band write leaves the entry in place. An apply_patch result is bad when its diff shows `-HASH│` rows for lines you meant to keep. Examples are a closing brace, an import, or a declaration.
@@ -1 +1 @@
1
- Undo the last edit on the file at `path`
1
+ Undo the last `apply_patch` batch on the file at `path`
@@ -1 +1 @@
1
- Undo the last edit on the file at `path`, reverting it to its previous state. Use when an edit produced incorrect results (e.g., wrong content, duplicated lines, broken syntax). The file must be unchanged since that edit.
1
+ Undo the last `apply_patch` batch that touched the file at `path`, reverting the whole batch — every text hunk plus file lifecycle actions (add/delete/move) — to its previous state. Use when an `apply_patch` produced incorrect results (e.g., wrong content, duplicated lines, broken syntax). The file must be unchanged since that batch.
@@ -0,0 +1,4 @@
1
+ - apply_patch:一次调用 = 一个事务;把相关改动成批处理,只有当先前的结果决定下一步时才拆分。
2
+ - apply_patch:各 hunk 按顺序针对调用前的文件校验(重叠范围会拒绝整批);片段 hunk 必须恰好匹配一次——补充上下文以消歧,绝不猜测——身份 hunk 定位已服务的行,陈旧或未知锚点会被拒绝;两者都会回送当前行。
3
+ - apply_patch:拒绝时不写入任何内容;从回送的 `HASH│content` 行重试,无需再次 `read`。
4
+ - apply_patch:成功时返回改动行的新锚点;整文件替换是用一个身份 hunk 覆盖每一行已服务的行。
@@ -0,0 +1 @@
1
+ 用单个事务补丁创建、更新、删除、移动、复制或剪切 UTF-8 文本文件;当改动跨多个文件或分散区域时,优先于多次单文件编辑。把完整补丁作为 `input` 传入。
@@ -0,0 +1,6 @@
1
+ - read:仅在工具尚未服务给你信息时调用,例如自动读取 diff 之外的内容。bash 视图或自动读取已经服务过该文件,因此直接用那些行 apply_patch。
2
+ - read:当一次替换需要多个块或两端边界时,把每个范围放进同一次调用。用 `{ file, windows: [{ offset, limit }] }`。每个窗口都在当轮被服务,因此其锚点可在同一次 apply_patch 调用中使用。窗口也可以是 `{ around_anchor, radius }`,以本会话已服务的某行为中心。本会话未服务过的锚点会被省略,并内联标注 `[Window warning: ...]`。
3
+ - read:61 行及以内的请求窗口可扩展到其所在块;重叠或相邻窗口在此判定前合并。上限为:代码与 Markdown 80 行,纯文本 40 行。扩展的行以 `[widened <requested> -> <served>, <label>]` 开头;超过三个请求窗口的提示为 `[widened N windows: <ranges>]`。超限的 Markdown 章节会裁剪并标注 `(section <extent>)`;超限的代码或纯文本则拒绝且无提示。`mode: "verbatim"` 也会扩展,但不加锚点。
4
+ - read:当你不打算对该文件 apply_patch 时,用 `mode: "verbatim"` 获取无锚点的纯内容。例如配置值、文档、README 或技能内容(SKILL.md 或其所处目录中的任意文件)。当你打算用锚点配合 apply_patch 时,就不要指定 `mode`,让这些行带上锚点。
5
+ - read:每次调用只发送一个文件,形式为 `{ file, windows?, mode? }`,与 `apply_patch` 相同。单个顶层 `mode` 作用于该文件。要读多个文件就并行发起多次 `read` 调用。
6
+ - read:PNG/JPG 图像以图像附件形式返回,供多模态模型使用。像其他文件一样用 `file` 请求。
@@ -0,0 +1 @@
1
+ 通过 `file`、`windows?`、`mode?` 读取文件;默认返回可直接编辑的 HASH│content 行,`mode: "verbatim"` 在你不编辑时返回纯文本;用 `windows` 在一次调用中批量读取多个范围
@@ -0,0 +1 @@
1
+ 通过 `file` 读取一个文本文件,可选 `windows` 与顶层 `mode`。要读多个文件就并行发起多次 `read` 调用。服务模式(默认)把每行返回为 HASH│content,带一个 {{HASH_LEN}} 字符的字母数字哈希。该哈希就是 `apply_patch` 的身份锚点,因此只复制 `│` 之前的 {{HASH_LEN}} 个字符。当你不打算 apply_patch 该文件时,用 `mode: "verbatim"` 获取无锚点的纯文本。`mode: "verbatim"` 不受行数上限限制。用 `windows` 指定范围:`[{ offset, limit }]`(offset 从 1 开始,limit 为行数,最多 16 个窗口)或 `[{ around_anchor: "<anchor>", radius: 10 }]`(针对本会话已服务的某行)。当页面未取全时,用 `windows: [{ offset, limit }]` 从下一个未显示行继续。61 行及以内的请求窗口在返回前可能扩展到其所在块;重叠或相邻的窗口在此判定前会合并。上限为:代码与 Markdown 80 行,纯文本 40 行。扩展页以 `[widened <requested> -> <served>, <label>]` 开头;超过三个请求窗口时折叠为 `[widened N windows: <ranges>]`。超限的 Markdown 章节会裁剪为上限大小的窗口并标注 `(section <extent>)`;超限的代码或纯文本块则拒绝,只返回所请求的窗口。`mode: "verbatim"` 同样会扩展,但不加锚点。行内的 `[...]` 提示描述的是返回结果,而非文件内容。空文件返回 `HASH│`(用 apply_patch 身份 hunk 插入)。PNG/JPG/GIF/WebP 以图像附件形式返回,供多模态模型使用。二进制与目录文件会被拒绝。哈希是文件作用域的:一个锚点绑定于它被服务时所在的文件。
@@ -0,0 +1,2 @@
1
+ - undo_last_edit:还原最近一次触及 `path` 处文件的 `apply_patch` 批次及其每个成员:文本 hunk 加文件生命周期操作(增/删/移动)。例外是外来 `cut`:此时撤销也会还原源文件。文件必须仍保有被编辑后的字节。带外写入会让调用以 `E_UNDO_STALE` 失败,且文件保持字节不变。一旦结果不对就立刻调用它。
2
+ - undo_last_edit:对该文件的 `write`,或之后的一次 `apply_patch` 批次,会替换撤销记录。一次读取、bash 视图或带外写入会保留该记录。当一次 apply_patch 的 diff 显示出你本想保留的行的 `-HASH│` 行时,该结果就是错的。例子包括一个收尾花括号、一句 import 或一个声明。
@@ -0,0 +1 @@
1
+ 撤销对 `path` 处文件最近一次的 `apply_patch` 批次
@@ -0,0 +1 @@
1
+ 撤销最近一次触及 `path` 处文件的 `apply_patch` 批次,把整批——每个文本 hunk 加上文件生命周期操作(增/删/移动)——还原到先前状态。当某次 `apply_patch` 产生了错误结果(例如内容错误、重复行、语法损坏)时使用。该文件自那批之后必须未被改动。