@lmzhen/dsh-evolution-merge 0.30.18 → 0.30.19

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 (143) hide show
  1. package/README.md +21 -4
  2. package/data/minhash_params.json +1063 -0
  3. package/lib/index.js +7313 -3272
  4. package/lib/types/adapters/channel.d.ts +1 -0
  5. package/lib/types/adapters/channel.js +15 -1
  6. package/lib/types/adapters/engine-screen.d.ts +1 -1
  7. package/lib/types/adapters/engine-screen.js +5 -2
  8. package/lib/types/adapters/model-runner.d.ts +0 -5
  9. package/lib/types/adapters/model-runner.js +8 -2
  10. package/lib/types/adapters/model.js +2 -1
  11. package/lib/types/adapters/select.js +12 -8
  12. package/lib/types/approve-command.d.ts +12 -1
  13. package/lib/types/approve-command.js +39 -6
  14. package/lib/types/authoring-flow.d.ts +16 -0
  15. package/lib/types/authoring-flow.js +37 -13
  16. package/lib/types/channel-port.d.ts +3 -1
  17. package/lib/types/channel-port.js +8 -3
  18. package/lib/types/command-defs.d.ts +5 -0
  19. package/lib/types/command-defs.js +33 -0
  20. package/lib/types/command-surface.d.ts +1 -1
  21. package/lib/types/command-surface.js +10 -43
  22. package/lib/types/commands.d.ts +3 -4
  23. package/lib/types/commands.js +33 -72
  24. package/lib/types/commit-assembly.d.ts +14 -1
  25. package/lib/types/commit-assembly.js +65 -9
  26. package/lib/types/commit-execute.d.ts +7 -1
  27. package/lib/types/commit-execute.js +54 -22
  28. package/lib/types/commit-plan.d.ts +7 -1
  29. package/lib/types/commit-plan.js +4 -0
  30. package/lib/types/commit-receipt.d.ts +3 -0
  31. package/lib/types/contracts.d.ts +43 -0
  32. package/lib/types/doctor.d.ts +11 -1
  33. package/lib/types/doctor.js +81 -27
  34. package/lib/types/domain/authoring-manifest.d.ts +26 -0
  35. package/lib/types/domain/authoring-manifest.js +43 -0
  36. package/lib/types/domain/engine-criteria.d.ts +10 -0
  37. package/lib/types/domain/engine-criteria.js +11 -0
  38. package/lib/types/domain/engine-input.d.ts +10 -0
  39. package/lib/types/domain/engine-input.js +26 -0
  40. package/lib/types/domain/errors.d.ts +15 -1
  41. package/lib/types/domain/errors.js +23 -4
  42. package/lib/types/domain/evidence-hash.d.ts +3 -0
  43. package/lib/types/domain/evidence-hash.js +14 -0
  44. package/lib/types/domain/failure-route.d.ts +14 -0
  45. package/lib/types/domain/failure-route.js +12 -0
  46. package/lib/types/domain/live-facts.d.ts +31 -0
  47. package/lib/types/domain/live-facts.js +54 -0
  48. package/lib/types/domain/merge-limits.d.ts +18 -0
  49. package/lib/types/domain/merge-limits.js +22 -0
  50. package/lib/types/domain/merge-report.d.ts +41 -0
  51. package/lib/types/domain/merge-report.js +2 -0
  52. package/lib/types/domain/merge-snapshot.d.ts +99 -0
  53. package/lib/types/domain/merge-snapshot.js +82 -0
  54. package/lib/types/domain/pair-ledger.d.ts +34 -0
  55. package/lib/types/domain/pair-ledger.js +96 -0
  56. package/lib/types/domain/pre-1b-backup.d.ts +43 -0
  57. package/lib/types/domain/pre-1b-backup.js +140 -0
  58. package/lib/types/domain/provider-key.d.ts +2 -0
  59. package/lib/types/domain/provider-key.js +11 -0
  60. package/lib/types/domain/read-probe.d.ts +5 -0
  61. package/lib/types/domain/read-probe.js +11 -0
  62. package/lib/types/domain/reconcile.d.ts +47 -0
  63. package/lib/types/domain/reconcile.js +178 -0
  64. package/lib/types/domain/resolve-pair.d.ts +2 -9
  65. package/lib/types/domain/resolve-pair.js +2 -15
  66. package/lib/types/domain/row-v4.d.ts +148 -0
  67. package/lib/types/domain/row-v4.js +159 -0
  68. package/lib/types/domain/run-coordinator.d.ts +51 -0
  69. package/lib/types/domain/run-coordinator.js +45 -0
  70. package/lib/types/domain/schema4.d.ts +53 -0
  71. package/lib/types/domain/schema4.js +129 -0
  72. package/lib/types/domain/selection-policy.d.ts +14 -0
  73. package/lib/types/domain/selection-policy.js +6 -0
  74. package/lib/types/domain/skipped-expiry.d.ts +5 -0
  75. package/lib/types/domain/skipped-expiry.js +16 -0
  76. package/lib/types/domain/stage-aggregate.d.ts +96 -0
  77. package/lib/types/domain/stage-aggregate.js +293 -0
  78. package/lib/types/domain/staging-index.d.ts +1 -0
  79. package/lib/types/domain/state-bootstrap.d.ts +43 -0
  80. package/lib/types/domain/state-bootstrap.js +267 -0
  81. package/lib/types/domain/state-v3.d.ts +3 -14
  82. package/lib/types/domain/state-v3.js +3 -79
  83. package/lib/types/domain/undo-journal.d.ts +19 -0
  84. package/lib/types/domain/undo-journal.js +20 -0
  85. package/lib/types/domain/undo-receipt.d.ts +18 -0
  86. package/lib/types/domain/undo-receipt.js +22 -0
  87. package/lib/types/domain/validation-plan.d.ts +65 -0
  88. package/lib/types/domain/validation-plan.js +49 -0
  89. package/lib/types/facts.d.ts +3 -1
  90. package/lib/types/facts.js +13 -16
  91. package/lib/types/index.d.ts +2 -0
  92. package/lib/types/index.js +181 -68
  93. package/lib/types/merge-command.d.ts +3 -1
  94. package/lib/types/merge-command.js +10 -6
  95. package/lib/types/merge-routes.d.ts +10 -5
  96. package/lib/types/merge-routes.js +72 -7
  97. package/lib/types/merge-tool.d.ts +1 -1
  98. package/lib/types/merge-tool.js +2 -2
  99. package/lib/types/metrics.js +1 -1
  100. package/lib/types/new-child-authoring.d.ts +2 -0
  101. package/lib/types/new-child-authoring.js +57 -10
  102. package/lib/types/params.d.ts +2 -4
  103. package/lib/types/params.js +13 -0
  104. package/lib/types/pending-actions.d.ts +2 -2
  105. package/lib/types/pending-actions.js +4 -4
  106. package/lib/types/prompts.d.ts +2 -4
  107. package/lib/types/prompts.js +21 -8
  108. package/lib/types/read-commands.d.ts +3 -3
  109. package/lib/types/read-commands.js +19 -28
  110. package/lib/types/recovery-coordinator.d.ts +34 -0
  111. package/lib/types/recovery-coordinator.js +84 -0
  112. package/lib/types/recovery-projection.d.ts +31 -0
  113. package/lib/types/recovery-projection.js +96 -0
  114. package/lib/types/resume-command.d.ts +5 -1
  115. package/lib/types/resume-command.js +46 -13
  116. package/lib/types/revision.d.ts +17 -0
  117. package/lib/types/revision.js +39 -3
  118. package/lib/types/round2-outcome.d.ts +2 -1
  119. package/lib/types/round2-outcome.js +59 -11
  120. package/lib/types/run-shell.d.ts +16 -2
  121. package/lib/types/run-shell.js +49 -5
  122. package/lib/types/runtime.d.ts +0 -5
  123. package/lib/types/runtime.js +274 -89
  124. package/lib/types/scan-command.d.ts +1 -0
  125. package/lib/types/scan-command.js +2 -0
  126. package/lib/types/scan-ports.d.ts +18 -0
  127. package/lib/types/scan-ports.js +49 -23
  128. package/lib/types/scan-run.d.ts +17 -0
  129. package/lib/types/scan-run.js +143 -48
  130. package/lib/types/stage-tool-registry.d.ts +7 -0
  131. package/lib/types/stage-tool-registry.js +12 -1
  132. package/lib/types/state.d.ts +5 -5
  133. package/lib/types/state.js +22 -69
  134. package/lib/types/status.d.ts +6 -1
  135. package/lib/types/status.js +47 -0
  136. package/lib/types/surface/body.d.ts +4 -0
  137. package/lib/types/surface/body.js +16 -0
  138. package/lib/types/surface/merge-view.d.ts +14 -0
  139. package/lib/types/surface/merge-view.js +126 -0
  140. package/lib/types/undo-operation.d.ts +24 -0
  141. package/lib/types/undo-operation.js +135 -0
  142. package/lib/types/wire.d.ts +22 -3
  143. package/package.json +6 -4
@@ -1,7 +1,7 @@
1
1
  import type { EvolutionIoLike } from '@lmzhen/dsh-evolution-core';
2
2
  import { type MergePairState, type MergeQueue, type MergeRow, type MergeState } from './contracts.ts';
3
- import { type RowWriteResult } from './domain/row-tx.ts';
4
- import { type SkippedRowV3, type V3Event, type V3Row, type VerifyRowV3 } from './domain/row-v3.ts';
3
+ import type { RowWriteResult } from './domain/row-tx.ts';
4
+ import type { SkippedRowV3, V3Event, V3Row, VerifyRowV3 } from './domain/row-v3.ts';
5
5
  export declare function mergeStatePath(home: string): string;
6
6
  export interface LoadMergeStateResult {
7
7
  readonly ok: boolean;
@@ -19,9 +19,9 @@ export interface RowTx {
19
19
  transition(id: string, event: V3Event, expectedVersion: number): RowWriteResult;
20
20
  }
21
21
  /** G3-5 runtime write API: only createVerify/createSkipped/transition. v2 writes are rejected. */
22
- export declare function transactRows<T>(io: EvolutionIoLike, home: string, fn: (tx: RowTx) => T): Promise<T>;
22
+ export declare function transactRows<T>(_io: EvolutionIoLike, _home: string, _fn: (tx: RowTx) => T): Promise<T>;
23
23
  /** The ONE write point. The callback returns the whole row set, not a delta. */
24
- export declare function mutateMergeState<T>(io: EvolutionIoLike, home: string, now: () => number, fn: (rows: readonly MergeRow[]) => {
24
+ export declare function mutateMergeState<T>(_io: EvolutionIoLike, _home: string, _now: () => number, _fn: (rows: readonly MergeRow[]) => {
25
25
  rows: readonly MergeRow[];
26
26
  result: T;
27
27
  }): Promise<T>;
@@ -44,7 +44,7 @@ export type UpsertResult = {
44
44
  export declare function upsertRow(io: EvolutionIoLike, home: string, now: () => number, row: MergeRow, expect?: UpsertExpect, cas?: string | null): Promise<UpsertResult>;
45
45
  export declare function moveRow(io: EvolutionIoLike, home: string, now: () => number, id: string, patch: MoveRowPatch): Promise<MergeRow | undefined>;
46
46
  /** G3-5 v3-aware drop: only terminal events. */
47
- export declare function dropRowV3(io: EvolutionIoLike, home: string, id: string, event: 'committed' | 'superseded' | 'restored', unitId?: string): Promise<boolean>;
47
+ export declare function dropRowV3(_io: EvolutionIoLike, _home: string, _id: string, _event: 'committed' | 'superseded' | 'restored', _unitId?: string): Promise<boolean>;
48
48
  export declare function dropRow(io: EvolutionIoLike, home: string, now: () => number, id: string): Promise<boolean>;
49
49
  export declare function rowsOf(io: EvolutionIoLike, home: string, queue?: MergeQueue): Promise<readonly MergeRow[]>;
50
50
  //# sourceMappingURL=state.d.ts.map
@@ -7,13 +7,10 @@
7
7
  * mismatch is repaired by the next write, never by a GET.
8
8
  */
9
9
  import { join } from 'node:path';
10
- import { transactIo } from '@lmzhen/dsh-evolution-core';
11
10
  import { MERGE_STATE_VERSION, queueOf, } from "./contracts.js";
12
11
  import { parseLedger } from "./domain/ledger.js";
13
- import { legacyToV3 } from "./domain/row-v3-bridge.js";
14
- import { applyRowWrite } from "./domain/row-tx.js";
15
- import { isV3Row } from "./domain/row-v3.js";
16
- import { deleteStagingIndex } from "./domain/staging-index.js";
12
+ import { parseV4State } from "./domain/schema4.js";
13
+ import { v4ToLegacy } from "./domain/row-v4.js";
17
14
  export function mergeStatePath(home) {
18
15
  return join(home, 'merge', 'merge-state.json');
19
16
  }
@@ -36,6 +33,20 @@ export async function loadMergeState(io, home) {
36
33
  catch {
37
34
  return { ok: false, state: { schemaVersion: MERGE_STATE_VERSION, rows: [] } };
38
35
  }
36
+ if (raw !== null) {
37
+ try {
38
+ const header = JSON.parse(raw);
39
+ if (header.schemaVersion === 4) {
40
+ const v4 = parseV4State(raw);
41
+ if (v4 === null)
42
+ return { ok: false, state: { schemaVersion: MERGE_STATE_VERSION, rows: [] }, note: 'merge-state-needs-repair' };
43
+ return { ok: true, state: { schemaVersion: MERGE_STATE_VERSION, rows: v4.rows.map(v4ToLegacy) } };
44
+ }
45
+ if (typeof header.schemaVersion === 'number' && header.schemaVersion > 4)
46
+ return { ok: false, state: { schemaVersion: MERGE_STATE_VERSION, rows: [] }, note: 'merge-state-forward-only' };
47
+ }
48
+ catch { /* parseState below reports the detailed note */ }
49
+ }
39
50
  const parsed = parseState(raw);
40
51
  return {
41
52
  ok: parsed.ok,
@@ -57,61 +68,12 @@ export async function loadStateSchema(io, home) {
57
68
  }
58
69
  }
59
70
  /** G3-5 runtime write API: only createVerify/createSkipped/transition. v2 writes are rejected. */
60
- export async function transactRows(io, home, fn) {
61
- let result;
62
- await transactIo(io, mergeStatePath(home), (current) => {
63
- let rows = [];
64
- if (current !== null) {
65
- let parsed;
66
- try {
67
- parsed = JSON.parse(current);
68
- }
69
- catch {
70
- throw new Error('merge-state-needs-repair');
71
- }
72
- const body = parsed;
73
- if (body.schemaVersion !== 3 || !Array.isArray(body.rows) || !body.rows.every(isV3Row)) {
74
- throw new Error(body.schemaVersion === 2 ? 'merge-state-migration-required' : 'merge-state-needs-repair');
75
- }
76
- rows = body.rows;
77
- }
78
- const apply = (write) => {
79
- const next = applyRowWrite(rows, write, new Date().toISOString());
80
- if (next.ok)
81
- rows = next.rows;
82
- return next;
83
- };
84
- const tx = {
85
- rows: () => rows,
86
- createVerify: row => apply({ kind: 'createVerify', row }),
87
- createSkipped: row => apply({ kind: 'createSkipped', row }),
88
- transition: (id, event, expectedVersion) => apply({ kind: 'transition', id, event, expectedVersion }),
89
- };
90
- result = fn(tx);
91
- return JSON.stringify({ schemaVersion: 3, rows }, null, 2);
92
- });
93
- return result;
71
+ export async function transactRows(_io, _home, _fn) {
72
+ throw new Error('legacy-writer-removed');
94
73
  }
95
74
  /** The ONE write point. The callback returns the whole row set, not a delta. */
96
- export async function mutateMergeState(io, home, now, fn) {
97
- let result;
98
- let ran = false;
99
- await transactIo(io, mergeStatePath(home), (current) => {
100
- ran = true;
101
- const parsed = parseState(current);
102
- if (!parsed.ok || parsed.note === 'merge-state-forward-only')
103
- throw new Error(parsed.note ?? 'merge-state-invalid');
104
- const out = fn(parsed.rows);
105
- result = out.result;
106
- const rows = out.rows.map(row => legacyToV3(row));
107
- if (rows.some(row => row === null))
108
- throw new Error('merge-state-v3-row-invalid');
109
- return JSON.stringify({ schemaVersion: 3, rows }, null, 2);
110
- });
111
- // transactIo only returns after fn ran; a skipped write is not a state outcome.
112
- void ran;
113
- void now;
114
- return result;
75
+ export async function mutateMergeState(_io, _home, _now, _fn) {
76
+ throw new Error('legacy-writer-removed');
115
77
  }
116
78
  export async function upsertRow(io, home, now, row, expect = 'any', cas) {
117
79
  return await mutateMergeState(io, home, now, (rows) => {
@@ -159,17 +121,8 @@ export async function moveRow(io, home, now, id, patch) {
159
121
  });
160
122
  }
161
123
  /** G3-5 v3-aware drop: only terminal events. */
162
- export async function dropRowV3(io, home, id, event, unitId = 'unknown') {
163
- const dropped = await transactRows(io, home, tx => {
164
- const row = tx.rows().find(item => item.id === id);
165
- if (row === undefined)
166
- return false;
167
- const moved = tx.transition(id, event === 'committed' ? { type: 'committed', unitId } : { type: event }, row.version);
168
- return moved.ok;
169
- });
170
- if (dropped)
171
- await deleteStagingIndex(io, home, id);
172
- return dropped;
124
+ export async function dropRowV3(_io, _home, _id, _event, _unitId = 'unknown') {
125
+ throw new Error('legacy-writer-removed');
173
126
  }
174
127
  export async function dropRow(io, home, now, id) {
175
128
  return await mutateMergeState(io, home, now, (rows) => {
@@ -3,7 +3,7 @@
3
3
  * writes, no report reads. The UI and command parser share this vocabulary.
4
4
  */
5
5
  import type { MergeQueue, MergeRow } from './contracts.ts';
6
- import type { Facts, RunsView } from './wire.ts';
6
+ import type { Facts, RunDetail, RunsView } from './wire.ts';
7
7
  export interface MergeCounts {
8
8
  readonly verify: number;
9
9
  readonly pending: number;
@@ -14,5 +14,10 @@ export declare function countRows(rows: readonly MergeRow[]): MergeCounts;
14
14
  export declare function formatStatus(rows: readonly MergeRow[]): string;
15
15
  export declare function formatFacts(facts: Facts): string;
16
16
  export declare function formatRunsView(view: RunsView): string;
17
+ /** CLI face of the RunDetail wire projection: one run, its current pair and every pair outcome. */
18
+ export declare function formatRunDetail(detail: RunDetail): string;
17
19
  export declare function formatQueue(rows: readonly MergeRow[], queue: MergeQueue): string;
20
+ /** Snapshot-compatible formatting, no legacy MergeRow. */
21
+ export declare function formatSnapshotStatus(rows: readonly import('./wire.ts').RowFact[]): string;
22
+ export declare function formatSnapshotQueue(rows: readonly import('./wire.ts').RowFact[], queue: import('./wire.ts').QueueId): string;
18
23
  //# sourceMappingURL=status.d.ts.map
@@ -37,10 +37,57 @@ export function formatRunsView(view) {
37
37
  return 'merge runs: empty';
38
38
  return fitToolText(['merge runs: ' + String(view.runs.length)].concat(view.runs.map(run => '- ' + run.id + ' [' + run.state + ']' + (run.endedAt === undefined ? '' : ' ' + new Date(run.endedAt).toISOString()))).join('\n'));
39
39
  }
40
+ /** CLI face of the RunDetail wire projection: one run, its current pair and every pair outcome. */
41
+ export function formatRunDetail(detail) {
42
+ const lines = ['merge run ' + detail.id + ' [' + detail.state + '] command=' + detail.command
43
+ + (detail.stage === undefined ? '' : ' stage=' + detail.stage)
44
+ + (detail.progress === null || detail.progress === undefined ? '' : ' progress=' + String(detail.progress.done) + '/' + String(detail.progress.total))
45
+ + (detail.currentPairKey === undefined ? '' : ' currentPair=' + detail.currentPairKey.replace('\u0000', ' + '))];
46
+ lines.push('started=' + new Date(detail.startedAt).toISOString()
47
+ + (detail.endedAt === undefined ? '' : ' ended=' + new Date(detail.endedAt).toISOString())
48
+ + (detail.parentRunId === undefined ? '' : ' parent=' + detail.parentRunId)
49
+ + (detail.resultRef === undefined ? '' : ' resultRef=' + detail.resultRef)
50
+ + (detail.reportRef === undefined ? '' : ' report=' + detail.reportRef)
51
+ + (detail.meta?.phase === undefined ? '' : ' phase=' + detail.meta.phase)
52
+ + (detail.meta?.approval === undefined ? '' : ' approval=' + detail.meta.approval)
53
+ + (detail.stopReason === undefined ? '' : ' stopReason=' + detail.stopReason)
54
+ + (detail.failure === undefined ? '' : ' failure=' + detail.failure.code));
55
+ for (const pair of detail.pairs) {
56
+ lines.push('- ' + pair.a + ' + ' + pair.b + ': ' + pair.outcome
57
+ + (pair.stage === undefined ? '' : ' stage=' + pair.stage)
58
+ + (pair.code === undefined ? '' : ' code=' + pair.code)
59
+ + (pair.target === undefined ? '' : ' target=' + pair.target.requested));
60
+ }
61
+ if (detail.pairsTruncated)
62
+ lines.push('(pairs truncated)');
63
+ return fitToolText(lines.join('\n'));
64
+ }
40
65
  export function formatQueue(rows, queue) {
41
66
  const selected = rows.filter(row => queueOf(row.state) === queue);
42
67
  if (selected.length === 0)
43
68
  return 'merge ' + queue + ': empty';
44
69
  return fitToolText(['merge ' + queue + ': ' + String(selected.length)].concat(selected.map(row => '- ' + describe(row))).join('\n'));
45
70
  }
71
+ const REASON_TEXT = {
72
+ kept: '门禁判定保留,不合并。',
73
+ 'gate-keep': '门禁判定保留,不合并。',
74
+ rejected: '人工拒绝合并。',
75
+ 'merge-over-limit': '超出合并限制,未合并。',
76
+ 'validation-failed': '合并校验未通过,未合并。',
77
+ };
78
+ function humanReason(reason) { return REASON_TEXT[reason] ?? reason; }
79
+ /** Snapshot-compatible formatting, no legacy MergeRow. */
80
+ export function formatSnapshotStatus(rows) {
81
+ const counts = { verify: 0, pending: 0, fuse: 0, skipped: 0 };
82
+ for (const row of rows)
83
+ counts[row.queue] += 1;
84
+ const lines = ['merge: verify ' + String(counts.verify) + ', pending ' + String(counts.pending) + ', fuse ' + String(counts.fuse) + ', skipped ' + String(counts.skipped)];
85
+ for (const row of rows)
86
+ lines.push('- ' + row.a + ' + ' + row.b + ' [' + row.state + ']' + (row.reason === undefined ? '' : ' — ' + humanReason(row.reason)));
87
+ return fitToolText(lines.join('\n'));
88
+ }
89
+ export function formatSnapshotQueue(rows, queue) {
90
+ const selected = rows.filter(row => row.queue === queue);
91
+ return selected.length === 0 ? 'merge ' + queue + ': empty' : fitToolText(['merge ' + queue + ': ' + String(selected.length)].concat(selected.map(row => '- ' + row.a + ' + ' + row.b + ' [' + row.state + ']' + (row.reason === undefined ? '' : ' — ' + humanReason(row.reason)))).join('\n'));
92
+ }
46
93
  //# sourceMappingURL=status.js.map
@@ -0,0 +1,4 @@
1
+ import type { EvolutionIoLike } from '@lmzhen/dsh-evolution-core';
2
+ import type { BodyRef } from '../domain/merge-snapshot.ts';
3
+ export declare function resolveBody(io: EvolutionIoLike, home: string, ref: BodyRef, rowBody: () => Promise<string | null>): Promise<string | null>;
4
+ //# sourceMappingURL=body.d.ts.map
@@ -0,0 +1,16 @@
1
+ import { readTextOrNull } from "../domain/read-probe.js";
2
+ /** Body resolution over a snapshot ref: stage body wins; lost/expired stage returns null. */
3
+ import { join } from 'node:path';
4
+ import { readStageEntry } from "../domain/stage-aggregate.js";
5
+ export async function resolveBody(io, home, ref, rowBody) {
6
+ const stage = await readStageEntry(io, home, ref.rowId).catch(() => null);
7
+ if (stage !== null && (stage.state === 'lost' || stage.state === 'expired'))
8
+ return null;
9
+ if (stage !== null && stage.state === 'present') {
10
+ const staged = await readTextOrNull(io, join(home, 'merge', 'runs', stage.ref, 'merged', ref.name));
11
+ if (staged !== null)
12
+ return staged;
13
+ }
14
+ return await rowBody();
15
+ }
16
+ //# sourceMappingURL=body.js.map
@@ -0,0 +1,14 @@
1
+ /** Pure wireVersion=1 builders over one MergeSnapshot. No IO, no writes. */
2
+ import type { MergeSnapshot, SnapshotRow, BodyRef } from '../domain/merge-snapshot.ts';
3
+ import type { Facts, QueuePage, RowFact, BodyView, HistoryView, RunView, RunDetail, RunsView, DoctorView, QueueId } from '../wire.ts';
4
+ export declare function toRowFact(row: SnapshotRow): RowFact;
5
+ export declare function buildFacts(snapshot: MergeSnapshot): Facts;
6
+ export declare function parseCursor(cursor: string, snapshot: MergeSnapshot): number;
7
+ export declare function buildQueuePage(snapshot: MergeSnapshot, queue: QueueId, expiredOnly: boolean, cursor?: string, limit?: number): QueuePage;
8
+ export declare function buildBodyView(snapshot: MergeSnapshot, ref: BodyRef, text: string): BodyView;
9
+ export declare function buildHistoryView(snapshot: MergeSnapshot): HistoryView;
10
+ export declare function buildRunView(snapshot: MergeSnapshot, runId: string): RunView | null;
11
+ export declare function buildRunDetail(snapshot: MergeSnapshot, runId: string): RunDetail | null;
12
+ export declare function buildRunsView(snapshot: MergeSnapshot): RunsView;
13
+ export declare function buildDoctorView(snapshot: MergeSnapshot): DoctorView;
14
+ //# sourceMappingURL=merge-view.d.ts.map
@@ -0,0 +1,126 @@
1
+ import { MERGE_COMMAND_SURFACE } from "../command-surface.js";
2
+ const base = '/evolution merge ';
3
+ function counts(rows) {
4
+ const active = { verify: 0, pending: 0, fuse: 0, skipped: 0 };
5
+ let expired = 0;
6
+ for (const item of rows) {
7
+ const skippedExpired = item.row.state === 'skipped' && item.row.expiredAt !== undefined;
8
+ if (item.expired || skippedExpired)
9
+ expired += 1;
10
+ else
11
+ active[item.queue] += 1;
12
+ }
13
+ return { active, expired };
14
+ }
15
+ const rowActionTemplates = new Map(MERGE_COMMAND_SURFACE.commands.flatMap(command => command.rowActions.map(action => [action.id, action])));
16
+ function renderAction(id, context, enabled = true, confirmOverride, reason) {
17
+ const template = rowActionTemplates.get(id);
18
+ if (template === undefined)
19
+ throw new Error('missing CommandDef rowAction: ' + id);
20
+ const command = template.command.replaceAll('{id}', context.id).replaceAll('{a}', context.a).replaceAll('{b}', context.b).replaceAll('{runId}', context.runId ?? context.id);
21
+ return { id, command, confirm: confirmOverride ?? template.confirm === true, enabled, ...(enabled ? {} : { disabledReason: reason ?? 'invalid' }) };
22
+ }
23
+ function actions(row) {
24
+ const context = { id: row.row.id, a: row.row.a, b: row.row.b, ...(row.row.runId === undefined ? {} : { runId: row.row.runId }) };
25
+ if (row.row.state === 'pending' && row.row.expired !== undefined)
26
+ return [renderAction('discard', context, true, true), renderAction('pair', context, true, true)];
27
+ if (row.row.state === 'pending')
28
+ return [renderAction('approve', context), renderAction('reject', context, true, true), renderAction('discard', context, true, true)];
29
+ if (row.row.state === 'approved' || row.row.state === 'fuse')
30
+ return [renderAction('retry-resume', context, true, true), renderAction('discard', context, true, true), renderAction('status', context)];
31
+ if (row.row.state === 'skipped')
32
+ return [renderAction('restore', context, true, true)];
33
+ return [renderAction('pair', context)];
34
+ }
35
+ function verifyState(row) {
36
+ if (row.row.state !== 'verify')
37
+ return undefined;
38
+ if (row.row.failure?.code === 'cancelled')
39
+ return 'cancelled';
40
+ if (row.row.failure !== undefined)
41
+ return 'failed';
42
+ if (row.row.verifySubState === 'screened')
43
+ return 'running';
44
+ return 'queued';
45
+ }
46
+ export function toRowFact(row) {
47
+ const state = row.row;
48
+ const target = state.state === 'pending' || state.state === 'approved' || state.state === 'fuse' ? { requested: state.target, ...(state.state === 'approved' || state.state === 'fuse' ? { actual: state.target } : {}) } : undefined;
49
+ const summary = state.state === 'pending' ? { a: state.decision.aSummary ?? null, b: state.decision.bSummary ?? null, rationale: state.decision.rationale } : undefined;
50
+ const stage = row.stage;
51
+ const staging = stage === null ? undefined : stage.state === 'reserved' || stage.state === 'present' ? { state: 'kept', ...(stage.expiresAt === undefined ? {} : { expiresAt: stage.expiresAt }) } : undefined;
52
+ const stageField = stage === null ? undefined : stage.state === 'present' || stage.state === 'reserved' ? 'present' : stage.state === 'lost' ? 'lost' : 'expired';
53
+ const expiredKind = row.expiredKind ?? (state.state === 'pending' ? state.expired : undefined);
54
+ const expired = expiredKind !== undefined ? { kind: expiredKind, at: Date.parse(state.updatedAt) } : state.state === 'skipped' && state.expiredAt !== undefined ? { kind: 'skipped-ttl', at: state.expiredAt } : undefined;
55
+ return {
56
+ id: state.id, state: state.state, queue: row.queue,
57
+ ...(verifyState(row) === undefined ? {} : { verifySubState: verifyState(row) }),
58
+ ...(stageField === undefined ? {} : { stage: stageField }),
59
+ a: state.a, b: state.b,
60
+ ...(target === undefined ? {} : { target }),
61
+ ...(summary === undefined ? {} : { summary }),
62
+ ...(state.state === 'skipped' ? { reason: state.reason } : {}),
63
+ ...(state.origin === undefined ? {} : { origin: state.origin }),
64
+ ...(state.failure === undefined ? {} : { failure: { stage: state.failure.stage, code: state.failure.code, retryable: state.failure.retryable, retryFrom: state.failure.retryFrom, detail: state.failure.message } }),
65
+ ...(staging === undefined ? {} : { staging }),
66
+ ...(expired === undefined ? {} : { expired }),
67
+ ...(state.attempts === undefined ? {} : { attempts: state.attempts }),
68
+ ...(state.state === 'skipped' && state.expiredAt !== undefined ? { expiresAt: state.expiredAt } : {}),
69
+ bodies: row.bodies,
70
+ createdAt: Date.parse(state.createdAt),
71
+ updatedAt: Date.parse(state.updatedAt),
72
+ actions: actions(row),
73
+ };
74
+ }
75
+ function runView(run, readAt, includeResultRef = false) {
76
+ void includeResultRef;
77
+ const record = run.record;
78
+ const metaPhase = run.meta?.phase;
79
+ const state = metaPhase === 'undone' ? 'undone' : run.meta?.approval === 'pending' ? 'awaiting-approval' : record.state;
80
+ const stopReason = run.meta?.stopReason ?? (record.state === 'cancelled' ? 'cancelled' : record.state === 'failed' ? 'failed' : record.state === 'succeeded' ? 'completed' : undefined);
81
+ return { wireVersion: 1, readAt, id: record.id, state, command: record.command ?? 'scan', ...(record.stage === undefined ? {} : { stage: record.stage }), progress: record.progress === undefined ? null : { done: record.progress.done, total: record.progress.total }, startedAt: record.startedAt, ...(record.endedAt === undefined ? {} : { endedAt: record.endedAt }), ...(stopReason === undefined ? {} : { stopReason }), ...(record.failure === undefined ? {} : { failure: { stage: 'settle', code: 'run-failed', retryable: false, retryFrom: 'none', detail: record.failure } }), ...(record.parentRunId === undefined ? {} : { parentRunId: record.parentRunId }), ...(run.pairs[0]?.pairKey === undefined ? {} : { currentPairKey: run.pairs[0].pairKey }), ...(record.resultRef === undefined ? {} : { resultRef: record.resultRef }), actions: [{ id: 'status', command: base + 'status ' + record.id, confirm: false, enabled: true }, { id: 'report', command: base + 'report ' + record.id, confirm: false, enabled: record.resultRef !== undefined }, { id: 'cancel', command: base + 'cancel ' + record.id, confirm: false, enabled: record.state === 'running' }] };
82
+ }
83
+ export function buildFacts(snapshot) {
84
+ const ordered = [...snapshot.runs].sort((a, b) => b.record.startedAt - a.record.startedAt);
85
+ const active = ordered.find(run => run.record.state === 'running');
86
+ const last = ordered.find(run => run.record.state !== 'running');
87
+ return { wireVersion: 1, readAt: snapshot.readAt, approval: { mergeApproval: snapshot.approval.mergeApproval, ...(snapshot.approval.platformWrite === undefined ? {} : { platformWrite: snapshot.approval.platformWrite }) }, counts: counts(snapshot.rows), active: active === undefined ? null : runView(active, snapshot.readAt), last: last === undefined ? null : runView(last, snapshot.readAt), startCommands: { scan: base + 'scan', pair: base + 'pair <a> <b>' }, fallback: MERGE_COMMAND_SURFACE.fallback, notes: snapshot.notes, degraded: snapshot.degraded };
88
+ }
89
+ function encodeCursor(snapshot, offset) { return Buffer.from(JSON.stringify({ revision: snapshot.queueRevision, offset }), 'utf8').toString('base64url'); }
90
+ export function parseCursor(cursor, snapshot) {
91
+ try {
92
+ const value = JSON.parse(Buffer.from(cursor, 'base64url').toString('utf8'));
93
+ if (value.revision !== snapshot.queueRevision || typeof value.offset !== 'number' || value.offset < 0)
94
+ throw new Error('stale');
95
+ return value.offset;
96
+ }
97
+ catch {
98
+ throw new Error('queue cursor is stale; reload');
99
+ }
100
+ }
101
+ export function buildQueuePage(snapshot, queue, expiredOnly, cursor, limit = 50) {
102
+ const selected = snapshot.rows.filter(row => row.queue === queue && (queue === 'skipped' ? true : expiredOnly ? row.expired : !row.expired));
103
+ const offset = cursor === undefined ? 0 : parseCursor(cursor, snapshot);
104
+ const page = selected.slice(offset, offset + Math.max(0, Math.min(200, limit)));
105
+ const next = offset + page.length < selected.length ? encodeCursor(snapshot, offset + page.length) : undefined;
106
+ return { wireVersion: 1, readAt: snapshot.readAt, queue, expiredOnly, counts: counts(snapshot.rows), rows: page.map(toRowFact), ...(next === undefined ? {} : { nextCursor: next }), notes: snapshot.notes };
107
+ }
108
+ export function buildBodyView(snapshot, ref, text) {
109
+ const max = 64 * 1024;
110
+ const truncated = Buffer.byteLength(text, 'utf8') > max;
111
+ const body = truncated ? text.slice(0, max) : text;
112
+ return { wireVersion: 1, readAt: snapshot.readAt, name: ref.name, side: ref.side, text: body, markdown: true, truncated, totalBytes: Buffer.byteLength(text, 'utf8') };
113
+ }
114
+ export function buildHistoryView(snapshot) { return { wireVersion: 1, readAt: snapshot.readAt, items: snapshot.history }; }
115
+ export function buildRunView(snapshot, runId) { const run = snapshot.runs.find(item => item.record.id === runId); return run === undefined ? null : runView(run, snapshot.readAt); }
116
+ export function buildRunDetail(snapshot, runId) {
117
+ const run = snapshot.runs.find(item => item.record.id === runId);
118
+ if (run === undefined)
119
+ return null;
120
+ return { ...runView(run, snapshot.readAt, true), ...(run.meta === null ? {} : { meta: { phase: run.meta.phase, ...(run.meta.approval === undefined ? {} : { approval: run.meta.approval }) } }), ...(run.reportRef === null ? {} : { reportRef: run.reportRef }), pairs: run.pairs.map(pair => ({ pairKey: pair.pairKey, a: pair.pairKey.split('\u0000')[0] ?? '', b: pair.pairKey.split('\u0000')[1] ?? '', outcome: pair.result === 'kept' ? 'kept' : pair.result === 'failed' ? 'failed' : pair.result === 'cancelled' ? 'cancelled' : 'merged', ...(pair.target === undefined ? {} : { target: { requested: pair.target } }) })), pairsTruncated: false };
121
+ }
122
+ export function buildRunsView(snapshot) { return { wireVersion: 1, readAt: snapshot.readAt, runs: [...snapshot.runs].sort((a, b) => b.record.startedAt - a.record.startedAt).map(run => runView(run, snapshot.readAt, true)) }; }
123
+ export function buildDoctorView(snapshot) {
124
+ return { wireVersion: 1, readAt: snapshot.readAt, counts: snapshot.doctor.counts, ...(snapshot.runs[0] === undefined ? {} : { latestRun: runView(snapshot.runs[0], snapshot.readAt) }), staging: snapshot.doctor.staging, approval: snapshot.approval, ...(snapshot.blocked === undefined ? {} : { blocked: { code: snapshot.blocked.code, message: snapshot.blocked.message } }), rows: snapshot.doctor.rows };
125
+ }
126
+ //# sourceMappingURL=merge-view.js.map
@@ -0,0 +1,24 @@
1
+ /** Batch 2b undo operation: per-pair preflight, journal, restore, receipt and settlement. */
2
+ import type { EvolutionIoLike } from '@lmzhen/dsh-evolution-core';
3
+ import { type MergeUndoPort } from './undo-merge.ts';
4
+ export type UndoErrorCode = 'undo-unavailable-legacy-receipt' | 'undo-evidence-hash-mismatch' | 'undo-target-drift' | 'undo-source-drift' | 'undo-partial-state' | 'undo-unit-not-committed';
5
+ export interface UndoResult {
6
+ readonly ok: boolean;
7
+ readonly code?: UndoErrorCode;
8
+ readonly message: string;
9
+ readonly undone: readonly string[];
10
+ }
11
+ export declare function runUndoOperation(options: {
12
+ readonly io: EvolutionIoLike;
13
+ readonly home: string;
14
+ readonly runId: string;
15
+ readonly port: MergeUndoPort;
16
+ readonly now: () => number;
17
+ readonly pairKey?: string;
18
+ readonly finalizeUnit?: (unitId: string) => Promise<{
19
+ readonly ok: boolean;
20
+ readonly message?: string;
21
+ }>;
22
+ readonly onPhase?: (phase: string, pairKey: string) => void | Promise<void>;
23
+ }): Promise<UndoResult>;
24
+ //# sourceMappingURL=undo-operation.d.ts.map
@@ -0,0 +1,135 @@
1
+ import { contentHash } from '@lmzhen/dsh-evolution-core';
2
+ import { readCommitReceipts } from "./commit-receipt.js";
3
+ import { undoMergeReceipt } from "./undo-merge.js";
4
+ import { writeUndoJournal } from "./domain/undo-journal.js";
5
+ import { readUndoReceipt, writeUndoReceipt } from "./domain/undo-receipt.js";
6
+ import { recordPair } from "./domain/pair-index-store.js";
7
+ import { readRunMeta, writeRunMeta } from "./domain/run-coordinator.js";
8
+ import { readUnitEvidence } from '@lmzhen/dsh-evolution-core';
9
+ import { evidenceSha256 as unitEvidenceDigest } from "./domain/evidence-hash.js";
10
+ function digest(value) { return value === null ? null : contentHash(value); }
11
+ function sortReceipts(receipts) {
12
+ const remaining = [...receipts];
13
+ const out = [];
14
+ while (remaining.length > 0) {
15
+ const last = [...remaining].sort((a, b) => {
16
+ if (b.at !== a.at)
17
+ return b.at - a.at;
18
+ if (b.sources.includes(a.target))
19
+ return 1;
20
+ if (a.sources.includes(b.target))
21
+ return -1;
22
+ return b.sources.length - a.sources.length;
23
+ })[0];
24
+ out.push(last);
25
+ remaining.splice(remaining.indexOf(last), 1);
26
+ }
27
+ return out;
28
+ }
29
+ async function preflight(io, home, port, receipt) {
30
+ const evidence = await readUnitEvidence(io, home, receipt.unitId);
31
+ if (evidence === null)
32
+ return { ok: false, code: 'undo-unavailable-legacy-receipt', message: 'commit evidence is missing; undo unavailable for legacy receipt', undone: [] };
33
+ const expectedSha = receipt.evidenceSha256 ?? unitEvidenceDigest(evidence);
34
+ if (unitEvidenceDigest(evidence) !== expectedSha)
35
+ return { ok: false, code: 'undo-evidence-hash-mismatch', message: 'commit receipt evidence hash does not match evidence', undone: [] };
36
+ const targetNow = await port.readSkill(receipt.target);
37
+ const targetAfter = evidence.target.after;
38
+ const targetBefore = evidence.target.before;
39
+ const targetAllowed = targetNow === targetAfter || (receipt.create ? targetNow === null : targetNow === targetBefore);
40
+ if (!targetAllowed)
41
+ return { ok: false, code: 'undo-target-drift', message: 'target no longer matches the committed unit', undone: [] };
42
+ const sourcesNow = {};
43
+ for (const source of evidence.sources) {
44
+ const live = await port.readSkill(source.name);
45
+ sourcesNow[source.name] = live;
46
+ if (live !== null && live !== source.before)
47
+ return { ok: false, code: 'undo-source-drift', message: 'source no longer matches the archived unit: ' + source.name, undone: [] };
48
+ }
49
+ const targetDone = receipt.create ? targetNow === null : targetNow === targetBefore;
50
+ const sourcesDone = evidence.sources.every(source => sourcesNow[source.name] === source.before);
51
+ return { receipt, evidence, targetNow, sourcesNow, done: targetDone && sourcesDone, targetDone, sourcesDone };
52
+ }
53
+ async function settleRun(io, home, runId, pairKey, allPairs, at) {
54
+ const current = await readRunMeta(io, home, runId);
55
+ const previous = current?.undonePairs ?? [];
56
+ const undone = [...new Set([...previous, pairKey])];
57
+ const all = allPairs.length > 0 ? allPairs.every(key => undone.includes(key)) : true;
58
+ await writeRunMeta(io, home, {
59
+ version: 1, runId, phase: all ? 'undone' : 'settled',
60
+ ...(current?.parentRunId === undefined ? {} : { parentRunId: current.parentRunId }),
61
+ ...(current?.pairCount === undefined ? {} : { pairCount: current.pairCount }),
62
+ ...(current?.approval === undefined ? {} : { approval: current.approval }),
63
+ ...(current?.reportRef === undefined ? {} : { reportRef: current.reportRef }),
64
+ undonePairs: undone,
65
+ ...(all ? { undoneAt: new Date(at).toISOString() } : {}),
66
+ createdAt: current?.createdAt ?? new Date(at).toISOString(), updatedAt: new Date(at).toISOString(),
67
+ });
68
+ }
69
+ export async function runUndoOperation(options) {
70
+ const allReceipts = (await readCommitReceipts(options.io, options.home)).filter(receipt => (receipt.parentRunId ?? receipt.runId) === options.runId);
71
+ const allPairKeys = allReceipts.map(receipt => receipt.pairKey ?? receipt.unitId);
72
+ const receipts = options.pairKey === undefined ? allReceipts : allReceipts.filter(receipt => receipt.pairKey === options.pairKey);
73
+ if (receipts.length === 0)
74
+ return { ok: false, message: 'merge run has no commit receipt: ' + options.runId, undone: [] };
75
+ const pairs = receipts.map(receipt => ({ pairKey: receipt.pairKey ?? receipt.unitId, unitId: receipt.unitId, phase: 'pending' }));
76
+ await writeUndoJournal(options.io, options.home, { version: 1, runId: options.runId, phase: 'prepared', pairs, updatedAt: new Date(options.now()).toISOString() });
77
+ const undone = [];
78
+ for (const receipt of sortReceipts(receipts)) {
79
+ const pairKey = receipt.pairKey ?? receipt.unitId;
80
+ if (options.finalizeUnit !== undefined) {
81
+ const finalized = await options.finalizeUnit(receipt.unitId);
82
+ if (!finalized.ok)
83
+ return { ok: false, message: finalized.message ?? 'unit finalize failed', undone };
84
+ }
85
+ const existing = await readUndoReceipt(options.io, options.home, options.runId, pairKey);
86
+ if (existing !== null) {
87
+ await options.onPhase?.('pair-index', pairKey);
88
+ await recordPair(options.io, options.home, { pairKey, runId: options.runId, result: 'undone', target: receipt.target });
89
+ await options.onPhase?.('run-meta', pairKey);
90
+ await settleRun(options.io, options.home, options.runId, pairKey, allPairKeys, options.now());
91
+ undone.push(pairKey);
92
+ continue;
93
+ }
94
+ const checked = await preflight(options.io, options.home, options.port, receipt);
95
+ if ('ok' in checked)
96
+ return { ...checked, undone };
97
+ if (checked.done) {
98
+ await options.onPhase?.('receipt', pairKey);
99
+ await writeUndoReceipt(options.io, options.home, { version: 1, runId: options.runId, ...(receipt.parentRunId === undefined ? {} : { parentRunId: receipt.parentRunId }), pairKey, unitId: receipt.unitId, evidenceSha256: receipt.evidenceSha256 ?? unitEvidenceDigest(checked.evidence), target: receipt.target, sources: checked.evidence.sources.map(source => source.name), restoredTargetHash: receipt.create ? null : digest(checked.evidence.target.before), restoredSourceHashes: Object.fromEntries(checked.evidence.sources.map(source => [source.name, contentHash(source.before)])), at: options.now() });
100
+ await options.onPhase?.('pair-index', pairKey);
101
+ await recordPair(options.io, options.home, { pairKey, runId: options.runId, result: 'undone', target: receipt.target });
102
+ await options.onPhase?.('run-meta', pairKey);
103
+ await settleRun(options.io, options.home, options.runId, pairKey, allPairKeys, options.now());
104
+ undone.push(pairKey);
105
+ continue;
106
+ }
107
+ if (!checked.targetDone || !checked.sourcesDone) {
108
+ const targetStage = checked.targetDone ? 'skip' : 'restore';
109
+ const sourceStage = checked.sourcesDone ? 'skip' : 'restore';
110
+ if (targetStage === 'skip' && sourceStage === 'skip') { /* unreachable, done */ }
111
+ }
112
+ if (checked.targetDone !== checked.sourcesDone && (checked.targetDone || checked.sourcesDone)) {
113
+ return { ok: false, code: 'undo-partial-state', message: 'undo is partially applied; manual retry required', undone };
114
+ }
115
+ const phase = (next) => ({ pairKey, unitId: receipt.unitId, phase: next });
116
+ await writeUndoJournal(options.io, options.home, { version: 1, runId: options.runId, phase: 'running', pairs: pairs.map(pair => pair.pairKey === pairKey ? phase('restore-target') : pair), updatedAt: new Date(options.now()).toISOString() });
117
+ await options.onPhase?.('restore-target', pairKey);
118
+ const executed = await undoMergeReceipt(receipt, options.port);
119
+ if (!executed.ok)
120
+ return { ok: false, message: executed.message, undone };
121
+ await writeUndoJournal(options.io, options.home, { version: 1, runId: options.runId, phase: 'running', pairs: pairs.map(pair => pair.pairKey === pairKey ? phase('restore-source') : pair), updatedAt: new Date(options.now()).toISOString() });
122
+ await options.onPhase?.('restore-source', pairKey);
123
+ await writeUndoJournal(options.io, options.home, { version: 1, runId: options.runId, phase: 'running', pairs: pairs.map(pair => pair.pairKey === pairKey ? phase('receipt') : pair), updatedAt: new Date(options.now()).toISOString() });
124
+ await options.onPhase?.('receipt', pairKey);
125
+ await writeUndoReceipt(options.io, options.home, { version: 1, runId: options.runId, ...(receipt.parentRunId === undefined ? {} : { parentRunId: receipt.parentRunId }), pairKey, unitId: receipt.unitId, evidenceSha256: receipt.evidenceSha256 ?? unitEvidenceDigest(checked.evidence), target: receipt.target, sources: checked.evidence.sources.map(source => source.name), restoredTargetHash: receipt.create ? null : digest(checked.evidence.target.before), restoredSourceHashes: Object.fromEntries(checked.evidence.sources.map(source => [source.name, contentHash(source.before)])), at: options.now() });
126
+ await options.onPhase?.('pair-index', pairKey);
127
+ await recordPair(options.io, options.home, { pairKey, runId: options.runId, result: 'undone', target: receipt.target });
128
+ await options.onPhase?.('run-meta', pairKey);
129
+ await settleRun(options.io, options.home, options.runId, pairKey, allPairKeys, options.now());
130
+ undone.push(pairKey);
131
+ }
132
+ await writeUndoJournal(options.io, options.home, { version: 1, runId: options.runId, phase: 'settled', pairs: pairs.map(pair => ({ ...pair, phase: 'done' })), updatedAt: new Date(options.now()).toISOString() });
133
+ return { ok: true, message: 'undone ' + String(undone.length) + ' pair(s)', undone };
134
+ }
135
+ //# sourceMappingURL=undo-operation.js.map