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
package/src/cut-repair.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  import { restoreEndings, type LineEnding } from "./edit-diff.js";
2
2
  import { readBytes, writeAtomic } from "./fs-write.js";
3
+ import { pathKind, removeFile, renameFile } from "./fs-lifecycle.js";
3
4
  import { withSortedMutationQueues } from "./mutation-queue.js";
4
5
  import { errCode } from "./utils.js";
5
6
  import { loadHashStore, type HashStore } from "./hash-store.js";
@@ -73,20 +74,87 @@ function candidateFor(row: MemberRow, which: "pre" | "post"): Buffer | undefined
73
74
  }
74
75
 
75
76
  async function stateOf(row: MemberRow): Promise<MemberState> {
76
- let bytes: Buffer;
77
- try {
78
- bytes = await readBytes(row.path);
79
- } catch (error) {
80
- if (errCode(error) === "ENOENT") return "other";
81
- throw error;
82
- }
77
+ const lifecycle = row.lifecycle ?? "edit";
83
78
  const post = candidateFor(row, "post");
84
- if (post !== undefined && bytes.equals(post)) return "post";
85
79
  const pre = candidateFor(row, "pre");
80
+ const readOrUndefined = async (path: string | null | undefined): Promise<Buffer | undefined> => {
81
+ if (path === null || path === undefined) return undefined;
82
+ try {
83
+ return await readBytes(path);
84
+ } catch (error) {
85
+ if (errCode(error) === "ENOENT") return undefined;
86
+ throw error;
87
+ }
88
+ };
89
+ if (lifecycle === "add") {
90
+ const bytes = await readOrUndefined(row.path);
91
+ if (bytes === undefined) return "pre";
92
+ return post !== undefined && bytes.equals(post) ? "post" : "other";
93
+ }
94
+ if (lifecycle === "delete") {
95
+ const bytes = await readOrUndefined(row.path);
96
+ if (bytes === undefined) return "post";
97
+ return pre !== undefined && bytes.equals(pre) ? "pre" : "other";
98
+ }
99
+ if (lifecycle === "move") {
100
+ const to = await readOrUndefined(row.path);
101
+ const from = await readOrUndefined(row.fromPath);
102
+ if (to !== undefined && post !== undefined && to.equals(post)) return "post";
103
+ if (from !== undefined && pre !== undefined && from.equals(pre)) return "pre";
104
+ // WHY: crash after the rename but before the content rewrite — the destination holds the raw
105
+ // WHY: pre bytes and the source is gone. That is the PRE state of an as-yet-unapplied move.
106
+ if (to !== undefined && pre !== undefined && to.equals(pre) && from === undefined) return "pre";
107
+ return "other";
108
+ }
109
+ const bytes = await readOrUndefined(row.path);
110
+ if (bytes === undefined) return "other";
111
+ if (post !== undefined && bytes.equals(post)) return "post";
86
112
  if (pre !== undefined && bytes.equals(pre)) return "pre";
87
113
  return "other";
88
114
  }
89
115
 
116
+ /** Write one lifecycle row to its named state; returns false when the row cannot express it. */
117
+ async function writeState(row: MemberRow, which: "pre" | "post"): Promise<boolean> {
118
+ const lifecycle = row.lifecycle ?? "edit";
119
+ const bytes = candidateFor(row, which);
120
+ if (lifecycle === "add") {
121
+ if (which === "post") {
122
+ if (bytes === undefined) return false;
123
+ await writeAtomic(row.path, bytes);
124
+ return true;
125
+ }
126
+ if ((await pathKind(row.path)) !== "absent") await removeFile(row.path);
127
+ return true;
128
+ }
129
+ if (lifecycle === "delete") {
130
+ if (which === "post") {
131
+ if ((await pathKind(row.path)) !== "absent") await removeFile(row.path);
132
+ return true;
133
+ }
134
+ if (bytes === undefined) return false;
135
+ await writeAtomic(row.path, bytes);
136
+ return true;
137
+ }
138
+ if (lifecycle === "move") {
139
+ const source = row.fromPath;
140
+ if (source === null || source === undefined) return false;
141
+ if (which === "post") {
142
+ if ((await pathKind(row.path)) === "absent") {
143
+ if ((await pathKind(source)) === "absent") return false;
144
+ await renameFile(source, row.path);
145
+ }
146
+ if (bytes !== undefined) await writeAtomic(row.path, bytes);
147
+ return true;
148
+ }
149
+ if ((await pathKind(row.path)) !== "absent") await renameFile(row.path, source);
150
+ if (bytes !== undefined) await writeAtomic(source, bytes);
151
+ return true;
152
+ }
153
+ if (bytes === undefined) return false;
154
+ await writeAtomic(row.path, bytes);
155
+ return true;
156
+ }
157
+
90
158
  /** Returns true when the resolution completed (bytes written or nothing to write) and the intent retired. */
91
159
  async function applyResolution(
92
160
  store: HashStore,
@@ -106,9 +174,7 @@ async function applyResolution(
106
174
  if (now !== expected) return false;
107
175
  }
108
176
  for (const row of toWrite) {
109
- const bytes = candidateFor(row, which);
110
- if (bytes === undefined) return false;
111
- await writeAtomic(row.path, bytes);
177
+ if (!(await writeState(row, which))) return false;
112
178
  }
113
179
  deleteCutIntent(store, txnId);
114
180
  return true;
@@ -54,19 +54,27 @@ export type DomainErrorCode =
54
54
  | "E_UNDO_REVERT_FAILED"
55
55
  | "E_UNKNOWN"
56
56
  | "E_LARGE_FILE"
57
- | "E_INTERNAL_INVARIANT";
57
+ | "E_INTERNAL_INVARIANT"
58
+ // WHY: apply_patch's two genuinely new codes (E_STALE_ANCHOR already exists above).
59
+ | "E_PATCH_SYNTAX"
60
+ | "E_UNIQUE_MATCH";
58
61
 
59
62
  /**
60
63
  * The registered tool whose call produced an `E_BAD_PAYLOAD`. WHY the registered name: the model
61
64
  * reads the tool list, so `undo_last_edit` names the call it must repair and a short form would
62
65
  * name nothing. A tool belongs here only after a live path is measured, never by assumption.
63
66
  */
64
- export type PayloadSubject = "read" | "edit" | "undo_last_edit";
67
+ export type PayloadSubject = "read" | "edit" | "apply_patch" | "undo_last_edit";
65
68
 
66
69
  // WHY: this is the runtime companion of the `PayloadSubject` union, like `isDomainErrorCode`.
67
70
  // WHY: Only a member may name a tool.
68
71
  // WHY: A non-member renders the neutral wording instead.
69
- const PAYLOAD_SUBJECTS: readonly PayloadSubject[] = ["read", "edit", "undo_last_edit"];
72
+ const PAYLOAD_SUBJECTS: readonly PayloadSubject[] = [
73
+ "read",
74
+ "edit",
75
+ "apply_patch",
76
+ "undo_last_edit",
77
+ ];
70
78
 
71
79
  export function isPayloadSubject(value: unknown): value is PayloadSubject {
72
80
  return typeof value === "string" && (PAYLOAD_SUBJECTS as readonly string[]).includes(value);
@@ -252,6 +260,20 @@ export interface ErrorPayloadMap {
252
260
  */
253
261
  subject?: PayloadSubject;
254
262
  };
263
+ E_PATCH_SYNTAX: {
264
+ message: string;
265
+ path?: string;
266
+ patchLine?: number;
267
+ servedRows?: ServedRow[];
268
+ servedBlock?: string;
269
+ };
270
+ E_UNIQUE_MATCH: {
271
+ message: string;
272
+ path?: string;
273
+ patchLine?: number;
274
+ servedRows?: ServedRow[];
275
+ servedBlock?: string;
276
+ };
255
277
  }
256
278
 
257
279
  export interface CodeSpec<P> {
@@ -334,13 +356,29 @@ function largeFileFormat(payload: ErrorPayloadMap["E_LARGE_FILE"]): string {
334
356
  return `${where} has ${observed} lines, exceeding the ${payload.limit}-line edit limit.`;
335
357
  }
336
358
 
359
+ /** SAFETY: one copy of each distinct anchor, in submission order. */
360
+ function distinctAnchors(anchors: readonly string[]): string[] {
361
+ return [...new Set(anchors)];
362
+ }
363
+
364
+ /**
365
+ * SAFETY: render the served evidence section at most once. A producer may hand a headline that
366
+ * already carries the pre-rendered section (a forwarded rejection); appending the block again would
367
+ * make the model pay for the same rows twice in the retry round. The `heading` + block pair is the
368
+ * machine-checkable section, so a bare row echoed inside a headline is not mistreated as one.
369
+ */
370
+ function appendServedBlock(headline: string, heading: string, servedBlock: string): string {
371
+ if (servedBlock === "") return headline;
372
+ if (headline.includes(`${heading}\n${servedBlock}`)) return headline;
373
+ return `${headline}\n${heading}\n${servedBlock}`;
374
+ }
375
+
337
376
  function staleAnchorFormat(payload: ErrorPayloadMap["E_STALE_ANCHOR"]): string {
338
- if (!payload.servedBlock) return payload.headline;
339
- return `${payload.headline}\nCurrent range:\n${payload.servedBlock}`;
377
+ return appendServedBlock(payload.headline, "Current range:", payload.servedBlock ?? "");
340
378
  }
341
379
 
342
380
  function unknownAnchorFormat(payload: ErrorPayloadMap["E_UNKNOWN_ANCHOR"]): string {
343
- const anchors = payload.anchors;
381
+ const anchors = distinctAnchors(payload.anchors);
344
382
  if (anchors.length === 1) {
345
383
  return `${payload.path} has not served the anchor "${anchors[0]}".`;
346
384
  }
@@ -356,13 +394,13 @@ function foreignHomesDisplay(homes: string[]): string {
356
394
  }
357
395
 
358
396
  function foreignAnchorFormat(payload: ErrorPayloadMap["E_FOREIGN_ANCHOR"]): string {
359
- const anchors = payload.anchors;
397
+ const anchors = distinctAnchors(payload.anchors);
360
398
  const noun =
361
399
  anchors.length === 1
362
400
  ? `the anchor "${anchors[0]}"`
363
401
  : `the anchors ${anchors.map((a) => `"${a}"`).join(", ")}`;
364
402
  const verb = anchors.length === 1 ? "was" : "were";
365
- const homes = foreignHomesDisplay(payload.homes);
403
+ const homes = foreignHomesDisplay([...new Set(payload.homes)]);
366
404
  if (homes.length === 0) {
367
405
  return `${noun} ${verb} not served for ${payload.path}.`;
368
406
  }
@@ -423,7 +461,7 @@ export const ERROR_REGISTRY: { [K in DomainErrorCode]: CodeSpec<ErrorPayloadMap[
423
461
  if (servedBlock === "" && headline === "") {
424
462
  throw new TypeError("E_STALE_RANGE: headline must be non-empty when servedBlock is empty");
425
463
  }
426
- return servedBlock === "" ? headline : `${headline}\n${FRESH_READ_HEADING}\n${servedBlock}`;
464
+ return appendServedBlock(headline, FRESH_READ_HEADING, servedBlock);
427
465
  },
428
466
  },
429
467
  E_TARGET_LOST: {
@@ -445,7 +483,7 @@ export const ERROR_REGISTRY: { [K in DomainErrorCode]: CodeSpec<ErrorPayloadMap[
445
483
  "E_UNVERIFIED_RANGE: headline must be non-empty when servedBlock is empty",
446
484
  );
447
485
  }
448
- return servedBlock === "" ? resolved : `${resolved}\n${FRESH_READ_HEADING}\n${servedBlock}`;
486
+ return appendServedBlock(resolved, FRESH_READ_HEADING, servedBlock);
449
487
  },
450
488
  },
451
489
  E_MALFORMED_ANCHOR: {
@@ -562,6 +600,16 @@ export const ERROR_REGISTRY: { [K in DomainErrorCode]: CodeSpec<ErrorPayloadMap[
562
600
  // WHY: no options — read needs `mode: "verbatim"` and edit needs a split, but the payload
563
601
  // WHY: names neither tool. Add `subject` before serving advice (ADR-0047).
564
602
  },
603
+ E_PATCH_SYNTAX: {
604
+ audience: "MODEL",
605
+ // WHY: the parser already rendered every problem with its patch line; the envelope carries
606
+ // WHY: that aggregate so one resubmission can fix every syntax problem.
607
+ format: ({ message }) => message,
608
+ },
609
+ E_UNIQUE_MATCH: {
610
+ audience: "MODEL",
611
+ format: ({ message }) => message,
612
+ },
565
613
  };
566
614
 
567
615
  export interface WarningPayloadMap {