dsh-hashline-edit 0.2.1 → 0.2.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (109) hide show
  1. package/README.md +60 -56
  2. package/cordis.patch.yml +3 -3
  3. package/lib/index.js +7224 -6454
  4. package/lib/types/dsh/contracts.d.ts +3 -12
  5. package/lib/types/dsh/fs.d.ts +0 -3
  6. package/lib/types/dsh/guidance.d.ts +5 -5
  7. package/lib/types/dsh/host-compat.d.ts +78 -7
  8. package/lib/types/dsh/index.d.ts +9 -21
  9. package/lib/types/dsh/metrics/events.d.ts +1 -1
  10. package/lib/types/dsh/metrics/observer.d.ts +1 -1
  11. package/lib/types/dsh/sandbox.d.ts +2 -2
  12. package/lib/types/dsh/shell-hook.d.ts +1 -1
  13. package/lib/types/dsh/tools/edit.d.ts +10 -0
  14. package/lib/types/dsh/tools/read.d.ts +3 -3
  15. package/lib/types/src/domain-errors.d.ts +2 -16
  16. package/lib/types/src/edit-tool.d.ts +41 -0
  17. package/lib/types/src/edit-undo.d.ts +2 -11
  18. package/lib/types/src/file-content/index.d.ts +1 -1
  19. package/lib/types/src/file-content/preview.d.ts +1 -1
  20. package/lib/types/src/hash-store.d.ts +2 -2
  21. package/lib/types/src/hashline/alphabet.d.ts +16 -1
  22. package/lib/types/src/hashline/hash-identity.d.ts +10 -4
  23. package/lib/types/src/mutation-engine/engine.d.ts +31 -0
  24. package/lib/types/src/mutation-engine/pipeline.d.ts +1 -8
  25. package/lib/types/src/mutation-engine/types.d.ts +50 -40
  26. package/lib/types/src/payload-contract.d.ts +135 -0
  27. package/lib/types/src/served-session/serve-render.d.ts +0 -6
  28. package/lib/types/src/undo-store.d.ts +1 -17
  29. package/package.json +21 -18
  30. package/prompts/edit-guidelines.md +10 -0
  31. package/prompts/edit-snippet.md +1 -0
  32. package/prompts/edit.md +1 -0
  33. package/prompts/read-guidelines.md +4 -4
  34. package/prompts/read.md +1 -1
  35. package/prompts/undo-last-edit-guidelines.md +2 -2
  36. package/prompts/undo-last-edit-snippet.md +1 -1
  37. package/prompts/undo-last-edit.md +1 -1
  38. package/src/bash-classifier.ts +1626 -0
  39. package/src/bash-conversion/decide.ts +812 -0
  40. package/src/bash-display-transform.ts +50 -0
  41. package/src/bash-multifile-config.ts +239 -0
  42. package/src/bash-view-config.ts +158 -0
  43. package/src/cut-repair.ts +11 -77
  44. package/src/domain-errors.ts +3 -35
  45. package/src/edit-normalize.ts +8 -0
  46. package/src/edit-render.ts +161 -0
  47. package/src/edit-tool.ts +118 -0
  48. package/src/edit-undo.ts +25 -379
  49. package/src/edit.ts +136 -0
  50. package/src/file-content/index.ts +1 -1
  51. package/src/file-content/preview.ts +1 -1
  52. package/src/file-content/served-header.ts +1 -1
  53. package/src/hash-store.ts +1 -18
  54. package/src/hashline/alphabet.ts +42 -1
  55. package/src/hashline/hash-identity.ts +12 -3
  56. package/src/integrations/pi-lens/command.ts +712 -0
  57. package/src/integrations/pi-lens/config.ts +148 -0
  58. package/src/integrations/pi-lens/io-bridge-adapter.ts +234 -0
  59. package/src/integrations/pi-lens/io-bridge.ts +112 -0
  60. package/src/integrations/pi-lens/settings-overlay.ts +272 -0
  61. package/src/lifecycle-hooks/index.ts +1992 -0
  62. package/src/lifecycle-hooks/types.ts +64 -0
  63. package/src/mutated-files.ts +75 -0
  64. package/src/mutation-engine/engine.ts +239 -0
  65. package/src/mutation-engine/index.ts +17 -0
  66. package/src/mutation-engine/pipeline.ts +27 -198
  67. package/src/mutation-engine/types.ts +57 -39
  68. package/src/mutation-queue.ts +1 -1
  69. package/src/payload-contract.ts +685 -0
  70. package/src/preview-controller.ts +84 -0
  71. package/src/prompts.ts +43 -10
  72. package/src/read.ts +865 -0
  73. package/src/search-hook.ts +41 -0
  74. package/src/served-session/serve-render.ts +2 -2
  75. package/src/store-config.ts +73 -0
  76. package/src/store-janitor-warning.ts +169 -0
  77. package/src/store-janitor.ts +539 -0
  78. package/src/store-kernel/settings.ts +233 -0
  79. package/src/store.ts +154 -0
  80. package/src/tui-presenter.ts +134 -0
  81. package/src/undo-store.ts +5 -74
  82. package/src/write-hook.ts +130 -0
  83. package/lib/types/dsh/hide.d.ts +0 -79
  84. package/lib/types/dsh/tools/apply-patch.d.ts +0 -38
  85. package/lib/types/src/apply-patch/commit.d.ts +0 -49
  86. package/lib/types/src/apply-patch/grammar.d.ts +0 -29
  87. package/lib/types/src/apply-patch/lifecycle.d.ts +0 -32
  88. package/lib/types/src/apply-patch/plan.d.ts +0 -50
  89. package/lib/types/src/apply-patch/render.d.ts +0 -51
  90. package/lib/types/src/apply-patch/resolve.d.ts +0 -129
  91. package/lib/types/src/apply-patch/tool.d.ts +0 -54
  92. package/lib/types/src/apply-patch/types.d.ts +0 -362
  93. package/lib/types/src/fs-lifecycle.d.ts +0 -44
  94. package/lib/types/src/host-utils.d.ts +0 -63
  95. package/lib/types/src/mutation-engine/edit-request.d.ts +0 -34
  96. package/prompts/apply-patch-guidelines.md +0 -10
  97. package/prompts/apply-patch.md +0 -1
  98. package/src/apply-patch/commit.ts +0 -190
  99. package/src/apply-patch/grammar.ts +0 -620
  100. package/src/apply-patch/index.ts +0 -17
  101. package/src/apply-patch/lifecycle.ts +0 -195
  102. package/src/apply-patch/plan.ts +0 -800
  103. package/src/apply-patch/render.ts +0 -137
  104. package/src/apply-patch/resolve.ts +0 -470
  105. package/src/apply-patch/tool.ts +0 -257
  106. package/src/apply-patch/types.ts +0 -387
  107. package/src/fs-lifecycle.ts +0 -201
  108. package/src/host-utils.ts +0 -224
  109. package/src/mutation-engine/edit-request.ts +0 -39
@@ -85,16 +85,12 @@ import { saveCutIntent, dropCutIntent } from "../undo-store.js";
85
85
  import { repairCutIntents } from "../cut-repair.js";
86
86
  import { withSortedMutationQueues } from "../mutation-queue.js";
87
87
  import { resolveTarget, writeAtomic, readBytes } from "../fs-write.js";
88
- import {
89
- missingAncestorDirs,
90
- pathKind,
91
- pruneEmptyDirs,
92
- removeFile,
93
- renameFile,
94
- assertPathWithinWorkspace,
95
- } from "../fs-lifecycle.js";
96
88
  import { toCwd } from "../paths.js";
97
- import type { DesiredContent, NormalizedEditItem, NormalizedEditRequest } from "./edit-request.js";
89
+ import type {
90
+ DesiredContent,
91
+ NormalizedEditItem,
92
+ NormalizedEditRequest,
93
+ } from "../payload-contract.js";
98
94
  import {
99
95
  batchAbortFor,
100
96
  batchAbortForMany,
@@ -103,13 +99,7 @@ import {
103
99
  wrapParseFailure,
104
100
  } from "../edit-response.js";
105
101
  import { DEFERRED_STORE_SYNC_WARNING } from "../constants.js";
106
- import type {
107
- PipelineOptions,
108
- ProcessedEditFile,
109
- ForeignCutRecord,
110
- MemberLifecycle,
111
- TransactionRecoveryReport,
112
- } from "./types.js";
102
+ import type { PipelineOptions, ProcessedEditFile, ForeignCutRecord } from "./types.js";
113
103
  import {
114
104
  loadEditFile,
115
105
  loadForeignServedView,
@@ -1318,108 +1308,6 @@ async function runCutTransaction(
1318
1308
  * (those are reported and never committed).
1319
1309
  * @returns the batch outcome, anchored on the primary file.
1320
1310
  */
1321
- function memberLifecycleOf(plan: ProcessedEditFile): MemberLifecycle {
1322
- return plan.lifecycle ?? { kind: "keep" };
1323
- }
1324
-
1325
- function memberPostBytes(plan: ProcessedEditFile): Buffer {
1326
- return Buffer.from(plan.bom + restoreEndings(plan.result, plan.originalEnding), "utf-8");
1327
- }
1328
-
1329
- /**
1330
- * Publish one already-validated member. This is the only place a lifecycle action reaches the
1331
- * filesystem: content writes keep using writeAtomic (one serializer); add/delete/move use the
1332
- * fs-lifecycle primitives that writeAtomic deliberately does not express.
1333
- */
1334
- async function commitMember(
1335
- plan: ProcessedEditFile,
1336
- rawPreBytes: ReadonlyMap<string, Buffer>,
1337
- onPublished: (plan: ProcessedEditFile) => void,
1338
- workspaceRoot: string | undefined,
1339
- onAfterMoveRename?: (toAbsolutePath: string) => void | Promise<void>,
1340
- ): Promise<void> {
1341
- const lifecycle = memberLifecycleOf(plan);
1342
- switch (lifecycle.kind) {
1343
- case "keep":
1344
- case "add":
1345
- if (workspaceRoot !== undefined) {
1346
- await assertPathWithinWorkspace(workspaceRoot, plan.absolutePath);
1347
- }
1348
- await writeAtomic(plan.absolutePath, memberPostBytes(plan));
1349
- onPublished(plan);
1350
- return;
1351
- case "delete":
1352
- await removeFile(plan.absolutePath);
1353
- onPublished(plan);
1354
- return;
1355
- case "move": {
1356
- // WHY: rename first, then (only for an edited move) rewrite the destination. A pure move
1357
- // WHY: must stay a rename: re-serializing would launder the file's raw line endings.
1358
- await renameFile(
1359
- lifecycle.fromAbsolutePath,
1360
- lifecycle.toAbsolutePath,
1361
- undefined,
1362
- workspaceRoot,
1363
- );
1364
- // WHY: the rename is already a publication: a later content-write failure must roll this
1365
- // WHY: member back or the source disappears while the failure claims nothing changed.
1366
- onPublished(plan);
1367
- if (onAfterMoveRename !== undefined) await onAfterMoveRename(lifecycle.toAbsolutePath);
1368
- const post = memberPostBytes(plan);
1369
- // WHY: a pure rename must not re-serialize: the normalized text equals the baseline text,
1370
- // WHY: so the destination keeps the exact bytes the OS rename carried.
1371
- if (
1372
- plan.originalNormalized !== plan.result &&
1373
- !post.equals(rawPreBytes.get(plan.absolutePath)!)
1374
- ) {
1375
- await writeAtomic(lifecycle.toAbsolutePath, post);
1376
- }
1377
- return;
1378
- }
1379
- }
1380
- }
1381
-
1382
- /**
1383
- * Restore one member that the commit had already published. A rollback is a BYTE restore, never
1384
- * a re-serialization: keep/delete write the captured pre-image buffer back; add removes what it
1385
- * created (and the empty directories it made); move renames back first, then restores the bytes.
1386
- */
1387
- async function rollbackMember(
1388
- plan: ProcessedEditFile,
1389
- rawPreBytes: ReadonlyMap<string, Buffer>,
1390
- workspaceRoot: string | undefined,
1391
- ): Promise<void> {
1392
- const lifecycle = memberLifecycleOf(plan);
1393
- const pre = rawPreBytes.get(plan.absolutePath)!;
1394
- switch (lifecycle.kind) {
1395
- case "add":
1396
- if ((await pathKind(plan.absolutePath)) !== "absent") await removeFile(plan.absolutePath);
1397
- await pruneEmptyDirs(lifecycle.createdDirs);
1398
- return;
1399
- case "delete":
1400
- await writeAtomic(plan.absolutePath, pre);
1401
- return;
1402
- case "move": {
1403
- if ((await pathKind(lifecycle.toAbsolutePath)) !== "absent") {
1404
- await renameFile(
1405
- lifecycle.toAbsolutePath,
1406
- lifecycle.fromAbsolutePath,
1407
- undefined,
1408
- workspaceRoot,
1409
- );
1410
- }
1411
- // WHY: the rename carries the pre bytes for a pure move and for a failed content write; only
1412
- // WHY: rewrite when the destination really holds different bytes (a completed edit rolled back).
1413
- if (!(await readBytes(lifecycle.fromAbsolutePath)).equals(pre)) {
1414
- await writeAtomic(lifecycle.fromAbsolutePath, pre);
1415
- }
1416
- await pruneEmptyDirs(lifecycle.createdDirs);
1417
- return;
1418
- }
1419
- case "keep":
1420
- await writeAtomic(plan.absolutePath, pre);
1421
- }
1422
- }
1423
1311
  async function runCorrelatedTransaction(
1424
1312
  writePlans: ProcessedEditFile[],
1425
1313
  sectionPlans: ProcessedEditFile[],
@@ -1439,60 +1327,18 @@ async function runCorrelatedTransaction(
1439
1327
  // WHY: (ticket-04b §11: after an abort EVERY file is byte-identical to its pre bytes).
1440
1328
  const rawPreBytes = new Map<string, Buffer>();
1441
1329
  for (const plan of writePlans) {
1442
- const lifecycle = plan.lifecycle;
1443
- if (lifecycle?.kind === "add") {
1444
- // WHY: an add has no pre-image by construction. A dry buffer keeps every consumer of the
1445
- // WHY: map total (rollback reads it, undo carries it as NULL through its lifecycle arm).
1446
- rawPreBytes.set(plan.absolutePath, Buffer.alloc(0));
1447
- plan.lifecycle = { ...lifecycle, createdDirs: await missingAncestorDirs(plan.absolutePath) };
1448
- continue;
1449
- }
1450
- if (lifecycle?.kind === "move") {
1451
- rawPreBytes.set(plan.absolutePath, await readBytes(lifecycle.fromAbsolutePath));
1452
- plan.lifecycle = {
1453
- ...lifecycle,
1454
- createdDirs: await missingAncestorDirs(lifecycle.toAbsolutePath),
1455
- };
1456
- continue;
1457
- }
1458
1330
  rawPreBytes.set(plan.absolutePath, await readBytes(plan.absolutePath));
1459
1331
  }
1460
1332
  const saved: { restore: () => Promise<void> }[] = [];
1461
1333
  for (const plan of writePlans) {
1462
- const lifecycle = plan.lifecycle;
1463
- const isAdd = lifecycle?.kind === "add";
1464
- const isDelete = lifecycle?.kind === "delete";
1465
- const isMove = lifecycle?.kind === "move";
1466
- const rawPost = Buffer.from(
1467
- plan.bom + restoreEndings(plan.result, plan.originalEnding),
1468
- "utf-8",
1469
- );
1470
- // WHY: a pure move carries the raw pre bytes to the destination; recording the canonical
1471
- // WHY: fold as its post image would make undo's byte oracle reject a legal mixed-ending file.
1472
- const storedRawPost =
1473
- isMove && plan.originalNormalized === plan.result
1474
- ? rawPreBytes.get(plan.absolutePath)!
1475
- : rawPost;
1476
1334
  const undo = await saveUndo(plan.absolutePath, {
1477
- content: isAdd ? "" : plan.originalNormalized,
1335
+ content: plan.originalNormalized,
1478
1336
  bom: plan.bom,
1479
1337
  originalEnding: plan.originalEnding,
1480
1338
  hashes: plan.originalHashes,
1481
- resultContent: isDelete ? "" : plan.result,
1339
+ resultContent: plan.result,
1482
1340
  transactionId: txnId,
1483
- rawPre: isAdd ? null : rawPreBytes.get(plan.absolutePath)!.toString("utf-8"),
1484
- ...(isMove
1485
- ? {
1486
- lifecycle: "move" as const,
1487
- fromPath: lifecycle.fromAbsolutePath,
1488
- toPath: lifecycle.toAbsolutePath,
1489
- rawPost: storedRawPost.toString("utf-8"),
1490
- }
1491
- : isAdd
1492
- ? { lifecycle: "add" as const, createdDirs: lifecycle.createdDirs }
1493
- : isDelete
1494
- ? { lifecycle: "delete" as const }
1495
- : { lifecycle: "edit" as const }),
1341
+ rawPre: rawPreBytes.get(plan.absolutePath)!.toString("utf-8"),
1496
1342
  });
1497
1343
  if (!undo.persisted) {
1498
1344
  for (const prior of saved) await prior.restore();
@@ -1520,45 +1366,38 @@ async function runCorrelatedTransaction(
1520
1366
  await options.onBeforeFirstCutWrite();
1521
1367
  }
1522
1368
  abortIf(options?.signal);
1523
- for (let index = 0; index < writePlans.length; index++) {
1524
- const plan = writePlans[index]!;
1525
- if (index > 0 && options?.onCutBetweenWrites) {
1369
+ await writeAtomic(
1370
+ primary.absolutePath,
1371
+ primary.bom + restoreEndings(primary.result, primary.originalEnding),
1372
+ );
1373
+ written.push(primary);
1374
+ for (const plan of writePlans.slice(1)) {
1375
+ if (options?.onCutBetweenWrites) {
1526
1376
  // WHY: test-only fault-injection seam (ticket-04b §3): fires INSIDE the two-rename
1527
1377
  // WHY: window, after the previous file's rename is durable.
1528
1378
  await options.onCutBetweenWrites(written[written.length - 1]!.absolutePath);
1529
1379
  }
1530
1380
  abortIf(options?.signal);
1531
- const publish = (member: ProcessedEditFile): void => {
1532
- written.push(member);
1533
- };
1534
- await commitMember(
1535
- plan,
1536
- rawPreBytes,
1537
- publish,
1538
- options?.workspaceRoot,
1539
- options?.onAfterMoveRename,
1381
+ await writeAtomic(
1382
+ plan.absolutePath,
1383
+ plan.bom + restoreEndings(plan.result, plan.originalEnding),
1540
1384
  );
1385
+ written.push(plan);
1541
1386
  }
1542
1387
  } catch (error) {
1543
- // WHY: the recovery report is the honest three-way split the contract requires: restored =
1544
- // WHY: proven back at its pre-image, remaining = rollback did not run / did not hold,
1545
- // WHY: uncertain = the rollback attempt itself threw (we cannot claim either state).
1546
- const report: TransactionRecoveryReport = { restored: [], remaining: [], uncertain: [] };
1388
+ let rolledBack = true;
1547
1389
  for (const plan of written) {
1548
1390
  try {
1549
- await rollbackMember(plan, rawPreBytes, options?.workspaceRoot);
1550
- report.restored.push(plan.path);
1391
+ await writeAtomic(plan.absolutePath, rawPreBytes.get(plan.absolutePath)!);
1551
1392
  } catch (rollbackError) {
1552
- report.remaining.push(plan.path);
1553
- report.uncertain.push(plan.path);
1393
+ rolledBack = false;
1554
1394
  reportDiagnostic(
1555
1395
  "mutation-engine",
1556
- "Failed to roll back a transaction member:",
1396
+ "Failed to roll back a cut-transaction file:",
1557
1397
  rollbackError,
1558
1398
  );
1559
1399
  }
1560
1400
  }
1561
- const rolledBack = report.remaining.length === 0;
1562
1401
  if (rolledBack) {
1563
1402
  for (const prior of saved) await prior.restore();
1564
1403
  try {
@@ -1572,8 +1411,8 @@ async function runCorrelatedTransaction(
1572
1411
  }
1573
1412
  }
1574
1413
  // WHY: a rollback that did not hold LEAVES the intent row and the transaction-tagged undo
1575
- // WHY: rows: next-run repair resolves to complete-the-transaction or restore-from-rows.
1576
- (error as Error & { recovery?: TransactionRecoveryReport }).recovery = report;
1414
+ // WHY: rows: next-run repair resolves to complete-the-cut or restore-both from the stored
1415
+ // WHY: pre/post bytes (ADR-0028) — never a silent loss, never a fake atomicity claim.
1577
1416
  throw error;
1578
1417
  }
1579
1418
 
@@ -1590,13 +1429,10 @@ async function runCorrelatedTransaction(
1590
1429
  // WHY: is REPORTED but never committed: its bytes did not move, so it owns no store write.
1591
1430
  const sections = [];
1592
1431
  for (const plan of sectionPlans) {
1593
- // WHY: a deleted file has no post-image to snapshot or serve; every other member commits its
1594
- // WHY: new anchors through the same finishCommittedFile the single-file edit uses.
1595
- const materializes = plan.appliedCount > 0 && memberLifecycleOf(plan).kind !== "delete";
1596
1432
  sections.push(
1597
1433
  toSection(
1598
1434
  plan,
1599
- materializes ? await finishCommittedFile(plan, options, sessionKey) : undefined,
1435
+ plan.appliedCount > 0 ? await finishCommittedFile(plan, options, sessionKey) : undefined,
1600
1436
  ),
1601
1437
  );
1602
1438
  }
@@ -1981,10 +1817,3 @@ export async function execEdits(
1981
1817
  ): Promise<ProcessedEditFile> {
1982
1818
  return runMutations(request, cwd, options);
1983
1819
  }
1984
-
1985
- /**
1986
- * Visibility-only export of the correlated transaction for apply_patch's commit seam. The
1987
- * function body is the one the cut path already uses; apply_patch members carry an optional
1988
- * lifecycle action read by that body. No engine semantics change.
1989
- */
1990
- export { runCorrelatedTransaction as commitTransaction };
@@ -8,12 +8,13 @@
8
8
  import type { LineEnding } from "../edit-diff.js";
9
9
  import type { ResolvedRange } from "../served-session/serve-render.js";
10
10
  import type { HashStore } from "../hash-store.js";
11
+ import type { EditDetails, RMetrics } from "../edit-response.js";
12
+ import type { DomainErrorCode, RangeCause } from "../domain-errors.js";
13
+ import type { ServedRow } from "../domain-errors.js";
11
14
 
12
15
  // WHY: Re-export pipeline-facing options — validated once at admission (edit.ts),
13
16
  // WHY: trusted inside the engine. No `any`.
14
17
  export interface PipelineOptions {
15
- /** Workspace root used to re-assert lifecycle path boundaries at commit time. */
16
- workspaceRoot?: string;
17
18
  accessMode?: number;
18
19
  signal?: AbortSignal;
19
20
  store?: HashStore;
@@ -35,22 +36,6 @@ export interface PipelineOptions {
35
36
  * window cannot show that.
36
37
  */
37
38
  onBeforeFirstCutWrite?: () => void | Promise<void>;
38
- /**
39
- * TEST-ONLY mid-member fault seam: called after a move's rename is durable and before its
40
- * destination content is rewritten, so the rollback of that in-flight member is exercised.
41
- */
42
- onAfterMoveRename?: (toAbsolutePath: string) => void | Promise<void>;
43
- }
44
-
45
- /**
46
- * The honest outcome of a failed multi-member commit's rollback attempt. `restored` members are
47
- * proven back at their pre-images; `remaining` members were not rolled back; `uncertain` members
48
- * are those whose rollback attempt itself threw, so neither state can be claimed for them.
49
- */
50
- export interface TransactionRecoveryReport {
51
- restored: string[];
52
- remaining: string[];
53
- uncertain: string[];
54
39
  }
55
40
 
56
41
  /**
@@ -66,24 +51,6 @@ export interface ForeignCutRecord {
66
51
  spanTo: string;
67
52
  }
68
53
 
69
- /**
70
- * The file-lifecycle action one transaction member carries.
71
- *
72
- * This is the engine-local vocabulary: apply_patch's plan IR maps onto it in
73
- * `src/apply-patch/commit.ts` so the mutation engine never imports the apply-patch layer.
74
- * Absent is an ordinary in-place edit.
75
- */
76
- export type MemberLifecycle =
77
- | { kind: "keep" }
78
- | { kind: "add"; createdDirs: readonly string[] }
79
- | { kind: "delete" }
80
- | {
81
- kind: "move";
82
- fromAbsolutePath: string;
83
- toAbsolutePath: string;
84
- createdDirs: readonly string[];
85
- };
86
-
87
54
  // WHY: Internal: the engine's view of one file's mutation outcome.
88
55
  // WHY: Mirrors `ProcessedEditFile` from the old pipeline — kept here as the engine's owned fact.
89
56
  export interface ProcessedEditFile {
@@ -129,9 +96,60 @@ export interface ProcessedEditFile {
129
96
  * correlated two-file transaction; the in-memory buffer itself never touches these files.
130
97
  */
131
98
  foreignCuts: ForeignCutRecord[];
99
+ }
100
+
101
+ // WHY: Discriminated success/failure for the deep seam.
102
+ // WHY: Callers use exhaustive switch on `ok` — no `isError` flag checks,
103
+ // WHY: no `any` threading.
104
+ export interface MutationSuccess {
105
+ ok: true;
106
+ /** SAFETY: Normalized result content (LF). */
107
+ result: string;
108
+ /** SAFETY: Unified diff (hash-anchored) for model consumption. */
109
+ diff: string;
110
+ /** SAFETY: User-facing drift notice, if any (details only, not model content). */
111
+ drift: string | undefined;
112
+ /** SAFETY: Metrics for telemetry. */
113
+ metrics: RMetrics;
114
+ raw: ProcessedEditFile;
115
+ /** SAFETY: Full tool result (content + details) for pi's tool_result hook. */
116
+ toolResult: {
117
+ content: Array<{ type: "text"; text: string }>;
118
+ details: EditDetails;
119
+ };
120
+ }
121
+
122
+ export interface MutationFailure {
123
+ ok: false;
124
+ /**
125
+ * SAFETY: Machine code — a registry member (`DomainErrorCode`). `toFailure` validates every
126
+ * caught code through the envelope reader (`src/error-envelope.ts`): non-registry strings
127
+ * (errno pass-through included) route to `E_UNKNOWN`, so the union is honest.
128
+ */
129
+ code: DomainErrorCode;
130
+ /** SAFETY: Human message — model-facing signal when applicable. */
131
+ message: string;
132
+ /** SAFETY: Fresh served block for retry when available (reject-and-serve). */
133
+ servedBlock?: string;
134
+ servedRows?: ServedRow[];
132
135
  /**
133
- * apply_patch lifecycle action for this member; absent is an ordinary edit. The correlated
134
- * transaction reads it for pre-image capture, commit ordering, rollback and undo-row shape.
136
+ * SAFETY: the RAW `E_BAD_PAYLOAD` message behind a rendered refusal, when the route is one, so
137
+ * SAFETY: the tool seam can name the invoking tool instead of falling back to the neutral
138
+ * SAFETY: wording. Present only for `code: "E_BAD_PAYLOAD"`.
135
139
  */
136
- lifecycle?: MemberLifecycle;
140
+ payloadMessage?: string;
141
+ /** SAFETY: User-facing diagnosis, never a model remedy — a GLOSSARY.md glossary term. */
142
+ cause?: RangeCause;
143
+ details?: { code: DomainErrorCode; cause: RangeCause };
144
+ }
145
+
146
+ export type MutationResult = MutationSuccess | MutationFailure;
147
+
148
+ // WHY: Narrowing helpers — keep call sites exhaustive.
149
+ export function isMutationSuccess(r: MutationResult): r is MutationSuccess {
150
+ return r.ok === true;
151
+ }
152
+
153
+ export function isMutationFailure(r: MutationResult): r is MutationFailure {
154
+ return r.ok === false;
137
155
  }
@@ -1,4 +1,4 @@
1
- import { withFileMutationQueue } from "./host-utils.js";
1
+ import { withFileMutationQueue } from "@earendil-works/pi-coding-agent";
2
2
 
3
3
  /**
4
4
  * Enter pi's per-path mutation queues for SEVERAL paths in one deterministic, sorted nesting.