@lmzhen/dsh-evolution-merge 0.46.0 → 0.48.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 (81) hide show
  1. package/lib/index.js +248 -1224
  2. package/lib/types/adapters/engine-screen.d.ts +0 -3
  3. package/lib/types/authoring-flow.d.ts +5 -0
  4. package/lib/types/authoring-flow.js +17 -10
  5. package/lib/types/commit-execute.js +1 -22
  6. package/lib/types/contracts.d.ts +0 -1
  7. package/lib/types/contracts.js +0 -1
  8. package/lib/types/doctor.d.ts +0 -1
  9. package/lib/types/doctor.js +2 -31
  10. package/lib/types/domain/consistency.d.ts +0 -1
  11. package/lib/types/domain/consistency.js +2 -11
  12. package/lib/types/domain/merge-limits.d.ts +0 -10
  13. package/lib/types/domain/merge-limits.js +0 -3
  14. package/lib/types/domain/merge-snapshot.js +4 -6
  15. package/lib/types/domain/pair-ledger.d.ts +0 -1
  16. package/lib/types/domain/row-v3.d.ts +0 -45
  17. package/lib/types/domain/row-v3.js +0 -68
  18. package/lib/types/domain/row.d.ts +2 -84
  19. package/lib/types/domain/row.js +1 -110
  20. package/lib/types/domain/run-coordinator.d.ts +0 -24
  21. package/lib/types/domain/run-coordinator.js +0 -1
  22. package/lib/types/domain/stage-aggregate.d.ts +0 -42
  23. package/lib/types/domain/stage-aggregate.js +1 -116
  24. package/lib/types/domain/state.d.ts +1 -53
  25. package/lib/types/domain/state.js +0 -55
  26. package/lib/types/domain/undo-journal.d.ts +4 -4
  27. package/lib/types/domain/undo-journal.js +5 -7
  28. package/lib/types/index.d.ts +0 -1
  29. package/lib/types/index.js +0 -6
  30. package/lib/types/merge-routes.d.ts +5 -21
  31. package/lib/types/merge-routes.js +53 -93
  32. package/lib/types/new-child-authoring.js +4 -10
  33. package/lib/types/prompts.d.ts +0 -1
  34. package/lib/types/prompts.js +0 -1
  35. package/lib/types/read-commands.d.ts +0 -1
  36. package/lib/types/read-commands.js +0 -4
  37. package/lib/types/recovery-projection.js +0 -3
  38. package/lib/types/round2-outcome.js +15 -37
  39. package/lib/types/scan-run.js +0 -3
  40. package/lib/types/skipped.d.ts +1 -2
  41. package/lib/types/skipped.js +0 -8
  42. package/lib/types/staging.d.ts +0 -2
  43. package/lib/types/staging.js +0 -9
  44. package/lib/types/state.d.ts +1 -39
  45. package/lib/types/state.js +2 -78
  46. package/lib/types/surface/merge-view.d.ts +1 -2
  47. package/lib/types/surface/merge-view.js +1 -2
  48. package/lib/types/undo-operation.js +0 -9
  49. package/lib/types/unit-protocol.d.ts +6 -0
  50. package/lib/types/unit-protocol.js +72 -102
  51. package/package.json +4 -4
  52. package/lib/types/commit-recovery.d.ts +0 -14
  53. package/lib/types/commit-recovery.js +0 -96
  54. package/lib/types/coordinator.d.ts +0 -77
  55. package/lib/types/coordinator.js +0 -137
  56. package/lib/types/domain/commit-journal-store.d.ts +0 -6
  57. package/lib/types/domain/commit-journal-store.js +0 -26
  58. package/lib/types/domain/commit-journal.d.ts +0 -16
  59. package/lib/types/domain/commit-journal.js +0 -26
  60. package/lib/types/domain/indexes.d.ts +0 -21
  61. package/lib/types/domain/indexes.js +0 -15
  62. package/lib/types/domain/merge-report.d.ts +0 -41
  63. package/lib/types/domain/merge-report.js +0 -2
  64. package/lib/types/domain/pair-index-store.d.ts +0 -6
  65. package/lib/types/domain/pair-index-store.js +0 -25
  66. package/lib/types/domain/pair-index.d.ts +0 -9
  67. package/lib/types/domain/pair-index.js +0 -28
  68. package/lib/types/domain/resolve-pair.d.ts +0 -4
  69. package/lib/types/domain/resolve-pair.js +0 -4
  70. package/lib/types/domain/row-tx.d.ts +0 -27
  71. package/lib/types/domain/row-tx.js +0 -31
  72. package/lib/types/domain/run-record.d.ts +0 -45
  73. package/lib/types/domain/run-record.js +0 -24
  74. package/lib/types/domain/state-v3.d.ts +0 -4
  75. package/lib/types/domain/state-v3.js +0 -4
  76. package/lib/types/facts.d.ts +0 -26
  77. package/lib/types/facts.js +0 -65
  78. package/lib/types/metrics.d.ts +0 -16
  79. package/lib/types/metrics.js +0 -27
  80. package/lib/types/wire-projection.d.ts +0 -36
  81. package/lib/types/wire-projection.js +0 -77
@@ -2,12 +2,12 @@
2
2
  * The one merge-state ledger: <home>/merge/merge-state.json.
3
3
  *
4
4
  * Reads are pure projections (no write, no reconciliation side effect). Every write goes
5
- * through mutateMergeState and a single transactIo, so there is no second file and no
5
+ * through pairLedger and a single transactIo, so there is no second file and no
6
6
  * incremental writer. A row's queue is always derived from its state on read; a hand-edited
7
7
  * mismatch is repaired by the next write, never by a GET.
8
8
  */
9
9
  import { join } from 'node:path';
10
- import { MERGE_STATE_VERSION, queueOf, } from "./contracts.js";
10
+ import { MERGE_STATE_VERSION, } from "./contracts.js";
11
11
  import { parseLedger } from "./domain/ledger.js";
12
12
  import { parseV4State } from "./domain/schema4.js";
13
13
  import { v4ToLegacy } from "./domain/row-v4.js";
@@ -54,82 +54,6 @@ export async function loadMergeState(io, home) {
54
54
  ...(parsed.note === undefined ? {} : { note: parsed.note }),
55
55
  };
56
56
  }
57
- /** Schema probe for doctor; the only other reader of mergeStatePath. */
58
- export async function loadStateSchema(io, home) {
59
- try {
60
- const raw = await io.readText(mergeStatePath(home));
61
- if (raw === null)
62
- return null;
63
- const parsed = JSON.parse(raw);
64
- return typeof parsed.schemaVersion === 'number' ? parsed.schemaVersion : null;
65
- }
66
- catch {
67
- return null;
68
- }
69
- }
70
- /** G3-5 runtime write API: only createVerify/createSkipped/transition. v2 writes are rejected. */
71
- export async function transactRows(_io, _home, _fn) {
72
- throw new Error('legacy-writer-removed');
73
- }
74
- /** The ONE write point. The callback returns the whole row set, not a delta. */
75
- export async function mutateMergeState(_io, _home, _now, _fn) {
76
- throw new Error('legacy-writer-removed');
77
- }
78
- export async function upsertRow(io, home, now, row, expect = 'any', cas) {
79
- return await mutateMergeState(io, home, now, (rows) => {
80
- const index = rows.findIndex(item => item.pairKey === row.pairKey);
81
- const current = index < 0 ? undefined : rows[index];
82
- if (cas !== undefined && ((cas === null && current !== undefined) || (cas !== null && current?.updatedAt !== cas))) {
83
- return { rows, result: { ok: false, current: current ?? row } };
84
- }
85
- if (current !== undefined && expect !== 'any' && (expect === 'absent' || queueOf(current.state) !== expect)) {
86
- return { rows, result: { ok: false, current } };
87
- }
88
- if (current === undefined && expect !== 'any' && expect !== 'absent' && expect !== 'verify') {
89
- return { rows, result: { ok: false, current: row } };
90
- }
91
- const normalized = { ...row, queue: queueOf(row.state) };
92
- const next = [...rows];
93
- if (current === undefined)
94
- next.push(normalized);
95
- else
96
- next[index] = { ...normalized, createdAt: current.createdAt };
97
- return { rows: next, result: { ok: true } };
98
- });
99
- }
100
- export async function moveRow(io, home, now, id, patch) {
101
- return await mutateMergeState(io, home, now, (rows) => {
102
- const index = rows.findIndex(item => item.id === id);
103
- if (index < 0)
104
- return { rows, result: undefined };
105
- const current = rows[index];
106
- const state = patch.state ?? current.state;
107
- const next = {
108
- ...current,
109
- state,
110
- queue: queueOf(state),
111
- updatedAt: new Date(now()).toISOString(),
112
- ...(patch.failure === undefined ? {} : { failure: patch.failure }),
113
- ...(patch.runId === undefined ? {} : { runId: patch.runId }),
114
- ...(patch.unitId === undefined ? {} : { unitId: patch.unitId }),
115
- ...(patch.origin === undefined ? {} : { origin: patch.origin }),
116
- ...(patch.attempts === undefined ? {} : { attempts: patch.attempts }),
117
- };
118
- const copy = [...rows];
119
- copy[index] = next;
120
- return { rows: copy, result: next };
121
- });
122
- }
123
- /** G3-5 v3-aware drop: only terminal events. */
124
- export async function dropRowV3(_io, _home, _id, _event, _unitId = 'unknown') {
125
- throw new Error('legacy-writer-removed');
126
- }
127
- export async function dropRow(io, home, now, id) {
128
- return await mutateMergeState(io, home, now, (rows) => {
129
- const next = rows.filter(item => item.id !== id);
130
- return { rows: next, result: next.length !== rows.length };
131
- });
132
- }
133
57
  export async function rowsOf(io, home, queue) {
134
58
  const loaded = await loadMergeState(io, home);
135
59
  return queue === undefined ? loaded.state.rows : loaded.state.rows.filter(row => row.queue === queue);
@@ -1,13 +1,12 @@
1
1
  /** Pure wireVersion=1 builders over one MergeSnapshot. No IO, no writes. */
2
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';
3
+ import type { Facts, QueuePage, RowFact, BodyView, HistoryView, RunDetail, RunsView, DoctorView, QueueId } from '../wire.ts';
4
4
  export declare function toRowFact(row: SnapshotRow): RowFact;
5
5
  export declare function buildFacts(snapshot: MergeSnapshot): Facts;
6
6
  export declare function parseCursor(cursor: string, snapshot: MergeSnapshot): number;
7
7
  export declare function buildQueuePage(snapshot: MergeSnapshot, queue: QueueId, expiredOnly: boolean, cursor?: string, limit?: number): QueuePage;
8
8
  export declare function buildBodyView(snapshot: MergeSnapshot, ref: BodyRef, text: string): BodyView;
9
9
  export declare function buildHistoryView(snapshot: MergeSnapshot): HistoryView;
10
- export declare function buildRunView(snapshot: MergeSnapshot, runId: string): RunView | null;
11
10
  export declare function buildRunDetail(snapshot: MergeSnapshot, runId: string): RunDetail | null;
12
11
  export declare function buildRunsView(snapshot: MergeSnapshot): RunsView;
13
12
  export declare function buildDoctorView(snapshot: MergeSnapshot): DoctorView;
@@ -130,12 +130,11 @@ export function buildBodyView(snapshot, ref, text) {
130
130
  return { wireVersion: 1, readAt: snapshot.readAt, name: ref.name, side: ref.side, text: body, markdown: true, truncated, totalBytes: Buffer.byteLength(text, 'utf8') };
131
131
  }
132
132
  export function buildHistoryView(snapshot) { return { wireVersion: 1, readAt: snapshot.readAt, items: snapshot.history }; }
133
- export function buildRunView(snapshot, runId) { const run = snapshot.runs.find(item => item.record.id === runId); return run === undefined ? null : runView(run, snapshot.readAt); }
134
133
  export function buildRunDetail(snapshot, runId) {
135
134
  const run = snapshot.runs.find(item => item.record.id === runId);
136
135
  if (run === undefined)
137
136
  return null;
138
- 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 };
137
+ 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' : pair.result === 'undone' ? 'undone' : 'merged', ...(pair.target === undefined ? {} : { target: { requested: pair.target } }) })), pairsTruncated: false };
139
138
  }
140
139
  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)) }; }
141
140
  export function buildDoctorView(snapshot) {
@@ -3,7 +3,6 @@ import { readCommitReceipts } from "./commit-receipt.js";
3
3
  import { undoMergeReceipt } from "./undo-merge.js";
4
4
  import { readUndoJournal, writeUndoJournal } from "./domain/undo-journal.js";
5
5
  import { readUndoReceipt, writeUndoReceipt } from "./domain/undo-receipt.js";
6
- import { recordPair } from "./domain/pair-index-store.js";
7
6
  import { readRunMeta, writeRunMeta } from "./domain/run-coordinator.js";
8
7
  import { readUnitEvidence } from '@lmzhen/dsh-evolution-core/write';
9
8
  import { evidenceSha256 as unitEvidenceDigest } from "./domain/evidence-hash.js";
@@ -115,9 +114,6 @@ async function runUndoSteps(options) {
115
114
  }
116
115
  const existing = await readUndoReceipt(options.io, options.home, options.runId, pairKey);
117
116
  if (existing !== null) {
118
- await markPhase('pair-index');
119
- await options.onPhase?.('pair-index', pairKey);
120
- await recordPair(options.io, options.home, { pairKey, runId: options.runId, result: 'undone', target: receipt.target });
121
117
  await markPhase('run-meta');
122
118
  await options.onPhase?.('run-meta', pairKey);
123
119
  await settleRun(options.io, options.home, options.runId, pairKey, allPairKeys, options.now());
@@ -130,9 +126,6 @@ async function runUndoSteps(options) {
130
126
  if (checked.done) {
131
127
  await options.onPhase?.('receipt', pairKey);
132
128
  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() });
133
- await markPhase('pair-index');
134
- await options.onPhase?.('pair-index', pairKey);
135
- await recordPair(options.io, options.home, { pairKey, runId: options.runId, result: 'undone', target: receipt.target });
136
129
  await markPhase('run-meta');
137
130
  await options.onPhase?.('run-meta', pairKey);
138
131
  await settleRun(options.io, options.home, options.runId, pairKey, allPairKeys, options.now());
@@ -156,8 +149,6 @@ async function runUndoSteps(options) {
156
149
  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() });
157
150
  await options.onPhase?.('receipt', pairKey);
158
151
  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() });
159
- await options.onPhase?.('pair-index', pairKey);
160
- await recordPair(options.io, options.home, { pairKey, runId: options.runId, result: 'undone', target: receipt.target });
161
152
  await options.onPhase?.('run-meta', pairKey);
162
153
  await settleRun(options.io, options.home, options.runId, pairKey, allPairKeys, options.now());
163
154
  undone.push(pairKey);
@@ -165,6 +165,12 @@ export declare class MergeUnitTransactions implements UnitTransaction {
165
165
  runId: string;
166
166
  }): Promise<UnitEvidenceRecord | UnitEvidenceRecord[] | null>;
167
167
  private withRecoveryFence;
168
+ /** Acquire the one tree fence (or a HomeAuthority fallback) and prove it is held. */
169
+ private acquireTreeFence;
170
+ /** Finish the idempotent support writes from the prepared snapshot, or name the drift. */
171
+ private completeSupport;
172
+ /** The complete evidence record both settlement paths write. */
173
+ private evidenceFor;
168
174
  finalize(unitId: string): Promise<{
169
175
  ok: true;
170
176
  evidence: UnitEvidenceRecord;
@@ -157,22 +157,10 @@ export class MergeUnitTransactions {
157
157
  await this.options.onPhase?.(phase);
158
158
  };
159
159
  try {
160
- if (this.options.fence !== undefined) {
161
- fenceLease = await this.options.fence.acquire();
162
- lock = fenceLease.lock;
163
- this.options.onLock?.("tree");
164
- }
165
- else {
166
- const fallback = new HomeAuthority({ home: this.options.home, waitMs: this.options.waitMs ?? 5000 });
167
- const fallbackLease = await fallback.lease("tree").catch(() => undefined);
168
- if (fallbackLease === undefined)
169
- return { ok: false, code: "unit-fence-failed", message: "tree lock unavailable" };
170
- fenceLease = { lock: leaseAsTreeLock(fallbackLease), release: async () => { await fallbackLease.release(); } };
171
- lock = fenceLease.lock;
172
- this.options.onLock?.("tree");
173
- }
174
- if (!(await lock.assertHeld()))
175
- return { ok: false, code: "unit-fence-failed", message: "tree fence lost" };
160
+ const acquired = await this.acquireTreeFence();
161
+ fenceLease = acquired.fenceLease;
162
+ lock = acquired.lock;
163
+ this.options.onLock?.("tree");
176
164
  stage = 'claim';
177
165
  claim = await this.options.claim.acquire(stored.plan.target, stored.plan.runId);
178
166
  this.options.onLock?.("claim");
@@ -310,6 +298,63 @@ export class MergeUnitTransactions {
310
298
  await lease.release();
311
299
  }
312
300
  }
301
+ /** Acquire the one tree fence (or a HomeAuthority fallback) and prove it is held. */
302
+ async acquireTreeFence() {
303
+ let fenceLease;
304
+ if (this.options.fence !== undefined) {
305
+ fenceLease = await this.options.fence.acquire();
306
+ }
307
+ else {
308
+ const fallback = new HomeAuthority({ home: this.options.home, waitMs: this.options.waitMs ?? 5000 });
309
+ const fallbackLease = await fallback.lease("tree").catch(() => undefined);
310
+ if (fallbackLease === undefined)
311
+ throw new Error("tree lock unavailable");
312
+ fenceLease = { lock: leaseAsTreeLock(fallbackLease), release: async () => { await fallbackLease.release(); } };
313
+ }
314
+ if (!(await fenceLease.lock.assertHeld()))
315
+ throw new Error("tree fence lost");
316
+ return { fenceLease, lock: fenceLease.lock };
317
+ }
318
+ /** Finish the idempotent support writes from the prepared snapshot, or name the drift. */
319
+ async completeSupport(snapshot, journal) {
320
+ let liveSupport = await Promise.all(snapshot.supportAfter.map(async (file) => ({ path: file.path, content: await this.options.backend.readSupportFile(snapshot.target, file.path) })));
321
+ const supportMatches = () => liveSupport.every(file => file.content === (snapshot.supportAfter.find(item => item.path === file.path)?.content ?? null));
322
+ if (supportMatches())
323
+ return { ok: true };
324
+ if (!["support-pending", "sources-archived", "unit-written"].includes(journal.phase) || this.options.backend.restoreSupportFile === undefined)
325
+ return { ok: false, message: "support file live hash does not match the prepared support; manual recovery required" };
326
+ for (const file of snapshot.supportAfter)
327
+ await this.options.backend.restoreSupportFile(snapshot.target, file.path, file.content);
328
+ liveSupport = await Promise.all(snapshot.supportAfter.map(async (file) => ({ path: file.path, content: await this.options.backend.readSupportFile(snapshot.target, file.path) })));
329
+ return supportMatches() ? { ok: true } : { ok: false, message: "support file could not be completed from the prepared snapshot" };
330
+ }
331
+ /** The complete evidence record both settlement paths write. */
332
+ async evidenceFor(unitId, snapshot, journal, targetAfter) {
333
+ const checkpoints = journal.checkpoints ?? [];
334
+ return {
335
+ schemaVersion: 1,
336
+ unitId,
337
+ runId: snapshot.runId,
338
+ planDigest: snapshot.planDigest,
339
+ state: "complete",
340
+ target: {
341
+ before: snapshot.targetBefore,
342
+ after: targetAfter,
343
+ supportDigest: createHash("sha256").update(JSON.stringify(snapshot.supportAfter)).digest("hex"),
344
+ created: !snapshot.targetExists,
345
+ },
346
+ sources: snapshot.sourceBefore.map((source) => ({
347
+ name: source.name,
348
+ before: source.body,
349
+ after: null,
350
+ archived: checkpoints.some((item) => item.source === source.name),
351
+ absorbedInto: snapshot.target,
352
+ })),
353
+ mutations: 0,
354
+ ...(snapshot.validationPlan === undefined ? {} : { validationPlan: snapshot.validationPlan }),
355
+ at: this.now(),
356
+ };
357
+ }
313
358
  async finalize(unitId) {
314
359
  return await this.withRecoveryFence(async () => this.finalizeUnlocked(unitId));
315
360
  }
@@ -347,42 +392,10 @@ export class MergeUnitTransactions {
347
392
  const liveSource = liveSources.find(source => source.body !== null);
348
393
  if (liveSource !== undefined)
349
394
  return { ok: false, code: "unit-rollback-blocked", message: "source is still live: " + liveSource.name };
350
- let liveSupport = await Promise.all(snapshot.supportAfter.map(async (file) => ({ path: file.path, content: await this.options.backend.readSupportFile(snapshot.target, file.path) })));
351
- const supportMatches = () => liveSupport.every(file => file.content === (snapshot.supportAfter.find(item => item.path === file.path)?.content ?? null));
352
- if (!supportMatches()) {
353
- // Crash after the sources are archived but before the support files land: finish the
354
- // idempotent support write from the prepared snapshot, then re-check.
355
- if (!["support-pending", "sources-archived", "unit-written"].includes(journal.phase) || this.options.backend.restoreSupportFile === undefined)
356
- return { ok: false, code: "unit-rollback-blocked", message: "support file live hash does not match the prepared support; manual recovery required" };
357
- for (const file of snapshot.supportAfter)
358
- await this.options.backend.restoreSupportFile(snapshot.target, file.path, file.content);
359
- liveSupport = await Promise.all(snapshot.supportAfter.map(async (file) => ({ path: file.path, content: await this.options.backend.readSupportFile(snapshot.target, file.path) })));
360
- if (!supportMatches())
361
- return { ok: false, code: "unit-rollback-blocked", message: "support file could not be completed from the prepared snapshot" };
362
- }
363
- const evidence = {
364
- schemaVersion: 1,
365
- unitId,
366
- runId: snapshot.runId,
367
- planDigest: snapshot.planDigest,
368
- state: "complete",
369
- target: {
370
- before: snapshot.targetBefore,
371
- after: (await this.options.backend.readSkill(snapshot.target)) ?? "",
372
- supportDigest: createHash("sha256").update(JSON.stringify(snapshot.supportAfter)).digest("hex"),
373
- created: !snapshot.targetExists,
374
- },
375
- sources: snapshot.sourceBefore.map((source) => ({
376
- name: source.name,
377
- before: source.body,
378
- after: null,
379
- archived: checkpoints.some((item) => item.source === source.name),
380
- absorbedInto: snapshot.target,
381
- })),
382
- mutations: 0,
383
- ...(snapshot.validationPlan === undefined ? {} : { validationPlan: snapshot.validationPlan }),
384
- at: this.now(),
385
- };
395
+ const support = await this.completeSupport(snapshot, journal);
396
+ if (!support.ok)
397
+ return { ok: false, code: "unit-rollback-blocked", message: support.message };
398
+ const evidence = await this.evidenceFor(unitId, snapshot, journal, (await this.options.backend.readSkill(snapshot.target)) ?? "");
386
399
  if (receipt?.evidenceSha256 !== undefined && unitEvidenceDigest(evidence) !== receipt.evidenceSha256)
387
400
  return { ok: false, code: "unit-rollback-blocked", message: "receipt evidence hash does not match rebuilt evidence" };
388
401
  await writeUnitEvidence(this.options.io, this.options.home, evidence);
@@ -420,42 +433,10 @@ export class MergeUnitTransactions {
420
433
  const liveSource = liveSources.find(source => source.body !== null);
421
434
  if (liveSource !== undefined)
422
435
  return { ok: false, code: "unit-rollback-blocked", message: "source is still live: " + liveSource.name };
423
- let liveSupport = await Promise.all(snapshot.supportAfter.map(async (file) => ({ path: file.path, content: await this.options.backend.readSupportFile(snapshot.target, file.path) })));
424
- const supportMatches = () => liveSupport.every(file => file.content === (snapshot.supportAfter.find(item => item.path === file.path)?.content ?? null));
425
- if (!supportMatches()) {
426
- // Crash after the sources are archived but before the support files land: finish the
427
- // idempotent support write from the prepared snapshot, then re-check.
428
- if (!["support-pending", "sources-archived", "unit-written"].includes(journal.phase) || this.options.backend.restoreSupportFile === undefined)
429
- return { ok: false, code: "unit-rollback-blocked", message: "support file live hash does not match the prepared support; manual recovery required" };
430
- for (const file of snapshot.supportAfter)
431
- await this.options.backend.restoreSupportFile(snapshot.target, file.path, file.content);
432
- liveSupport = await Promise.all(snapshot.supportAfter.map(async (file) => ({ path: file.path, content: await this.options.backend.readSupportFile(snapshot.target, file.path) })));
433
- if (!supportMatches())
434
- return { ok: false, code: "unit-rollback-blocked", message: "support file could not be completed from the prepared snapshot" };
435
- }
436
- const evidence = {
437
- schemaVersion: 1,
438
- unitId,
439
- runId: snapshot.runId,
440
- planDigest: snapshot.planDigest,
441
- state: "complete",
442
- target: {
443
- before: snapshot.targetBefore,
444
- after: targetAfter,
445
- supportDigest: createHash("sha256").update(JSON.stringify(snapshot.supportAfter)).digest("hex"),
446
- created: !snapshot.targetExists,
447
- },
448
- sources: snapshot.sourceBefore.map((source) => ({
449
- name: source.name,
450
- before: source.body,
451
- after: null,
452
- archived: checkpoints.some((item) => item.source === source.name),
453
- absorbedInto: snapshot.target,
454
- })),
455
- mutations: 0,
456
- ...(snapshot.validationPlan === undefined ? {} : { validationPlan: snapshot.validationPlan }),
457
- at: this.now(),
458
- };
436
+ const support = await this.completeSupport(snapshot, journal);
437
+ if (!support.ok)
438
+ return { ok: false, code: "unit-rollback-blocked", message: support.message };
439
+ const evidence = await this.evidenceFor(unitId, snapshot, journal, targetAfter);
459
440
  await writeUnitEvidence(this.options.io, this.options.home, evidence);
460
441
  await writeUnitJournal(this.options.io, this.options.home, { ...journal, phase: "committed", checkpoints, updatedAt: this.now() });
461
442
  return { ok: true, evidence };
@@ -473,21 +454,10 @@ export class MergeUnitTransactions {
473
454
  let lock;
474
455
  let claim;
475
456
  try {
476
- if (this.options.fence !== undefined) {
477
- fenceLease = await this.options.fence.acquire();
478
- lock = fenceLease.lock;
479
- }
480
- else {
481
- const fallback = new HomeAuthority({ home: this.options.home, waitMs: this.options.waitMs ?? 5000 });
482
- const fallbackLease = await fallback.lease("tree").catch(() => undefined);
483
- if (fallbackLease === undefined)
484
- return { ok: false, code: "unit-fence-failed", message: "tree lock unavailable" };
485
- fenceLease = { lock: leaseAsTreeLock(fallbackLease), release: async () => { await fallbackLease.release(); } };
486
- lock = fenceLease.lock;
487
- }
457
+ const acquired = await this.acquireTreeFence();
458
+ fenceLease = acquired.fenceLease;
459
+ lock = acquired.lock;
488
460
  this.options.onLock?.("tree");
489
- if (!(await lock.assertHeld()))
490
- return { ok: false, code: "unit-fence-failed", message: "tree fence lost" };
491
461
  claim = await this.options.claim.acquire(snapshot.target, snapshot.runId);
492
462
  this.options.onLock?.("claim");
493
463
  const currentTarget = await this.options.backend.readSkill(snapshot.target);
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@lmzhen/dsh-evolution-merge",
3
3
  "description": "Skill auto-merge domain: screening, two-round authoring, queues and commit orchestration (community build)",
4
- "version": "0.46.0",
4
+ "version": "0.48.0",
5
5
  "publishConfig": {
6
6
  "access": "public"
7
7
  },
@@ -34,9 +34,9 @@
34
34
  },
35
35
  "dependencies": {
36
36
  "@deepseek-ai/schemastery": "^3.18.5",
37
- "@lmzhen/dsh-evolution-core": "^0.46.0",
38
- "@lmzhen/dsh-evolution-commands": "^0.46.0",
39
- "@lmzhen/dsh-evolution-merge-engine": "0.46.0"
37
+ "@lmzhen/dsh-evolution-core": "^0.48.0",
38
+ "@lmzhen/dsh-evolution-commands": "^0.48.0",
39
+ "@lmzhen/dsh-evolution-merge-engine": "0.48.0"
40
40
  },
41
41
  "peerDependencies": {
42
42
  "@deepseek-ai/cordis": "^4.0.5"
@@ -1,14 +0,0 @@
1
- import type { EvolutionIoLike } from '@lmzhen/dsh-evolution-core';
2
- import type { CommitJournal } from './domain/commit-journal.ts';
3
- export interface CommitRecoveryReport {
4
- readonly runId: string;
5
- readonly phase: CommitJournal['phase'] | 'unknown';
6
- readonly action: string;
7
- readonly ok: boolean;
8
- readonly target?: string;
9
- readonly sources?: readonly string[];
10
- readonly journalPath?: string;
11
- readonly unitId?: string;
12
- }
13
- export declare function recoverCommitJournals(io: EvolutionIoLike, home: string, now: number, isActive?: (runId: string) => boolean): Promise<readonly CommitRecoveryReport[]>;
14
- //# sourceMappingURL=commit-recovery.d.ts.map
@@ -1,96 +0,0 @@
1
- /** Startup replay for non-terminal commit journals: report only, never write skills. */
2
- import { join } from 'node:path';
3
- import { writeJournal } from "./domain/commit-journal-store.js";
4
- import { readCommitReceipt } from "./commit-receipt.js";
5
- import { readUnitEvidence } from '@lmzhen/dsh-evolution-core/write';
6
- export async function recoverCommitJournals(io, home, now, isActive) {
7
- const dir = join(home, 'merge', 'journals');
8
- const reports = [];
9
- let files = [];
10
- if (await io.exists(dir)) {
11
- try {
12
- files = (await io.list(dir)).filter(name => name.endsWith('.json'));
13
- }
14
- catch {
15
- reports.push({ runId: '(list)', phase: 'unknown', action: 'journal-list-failed', ok: false, journalPath: dir });
16
- }
17
- }
18
- for (const file of files) {
19
- const path = join(dir, file);
20
- try {
21
- let journal;
22
- try {
23
- journal = JSON.parse(await io.readText(path) ?? '');
24
- }
25
- catch {
26
- reports.push({ runId: file.replace(/\.json$/, ''), phase: 'unknown', action: 'journal-unreadable', ok: false, journalPath: path });
27
- continue;
28
- }
29
- if (isActive?.(journal.runId) === true) {
30
- reports.push({ runId: journal.runId, phase: journal.phase, action: 'skipped-active-run', ok: true, target: journal.target, sources: journal.sources, journalPath: path });
31
- continue;
32
- }
33
- let receipt = null;
34
- try {
35
- receipt = await readCommitReceipt(io, home, journal.runId, journal.target);
36
- }
37
- catch {
38
- reports.push({ runId: journal.runId, phase: journal.phase, action: 'receipt-read-failed', ok: false, target: journal.target, sources: journal.sources, journalPath: path });
39
- continue;
40
- }
41
- if (receipt !== null) {
42
- await writeJournal(io, home, { ...journal, phase: 'committed', unitId: receipt.unitId, updatedAt: new Date(now).toISOString() });
43
- reports.push({ runId: journal.runId, phase: journal.phase, action: 'receipt-committed', ok: true, target: journal.target, sources: journal.sources, journalPath: path, unitId: receipt.unitId });
44
- continue;
45
- }
46
- if (journal.phase === 'committed') {
47
- reports.push({ runId: journal.runId, phase: journal.phase, action: 'receipt-missing', ok: false, target: journal.target, sources: journal.sources, journalPath: path });
48
- continue;
49
- }
50
- if (journal.phase === 'failed' || journal.phase === 'rolled-back')
51
- continue;
52
- reports.push({
53
- runId: journal.runId, phase: journal.phase, action: 'manual-required', ok: false,
54
- target: journal.target, sources: journal.sources, journalPath: path,
55
- });
56
- }
57
- catch (error) {
58
- reports.push({ runId: file.replace(/\.json$/, ''), phase: 'unknown', action: 'recovery-error: ' + (error instanceof Error ? error.message : String(error)), ok: false, journalPath: path });
59
- }
60
- }
61
- const unitDir = join(home, 'merge', 'unit-journals');
62
- if (await io.exists(unitDir)) {
63
- let unitFiles = [];
64
- try {
65
- unitFiles = (await io.list(unitDir)).filter(name => name.endsWith('.json'));
66
- }
67
- catch { /* no unit journals */ }
68
- for (const file of unitFiles) {
69
- const path = join(unitDir, file);
70
- try {
71
- const journal = JSON.parse(await io.readText(path) ?? '');
72
- const evidence = await readUnitEvidence(io, home, journal.unitId);
73
- if (evidence !== null) {
74
- reports.push({ runId: journal.runId, phase: 'committed', action: 'unit-committed', ok: true, target: journal.target, sources: journal.sources, journalPath: path, unitId: journal.unitId });
75
- continue;
76
- }
77
- if (journal.phase === 'committed') {
78
- reports.push({ runId: journal.runId, phase: 'committed', action: 'unit-evidence-missing', ok: false, target: journal.target, sources: journal.sources, journalPath: path, unitId: journal.unitId });
79
- continue;
80
- }
81
- if (journal.phase === 'failed' || journal.phase === 'rolled-back')
82
- continue;
83
- reports.push({ runId: journal.runId, phase: journal.phase, action: 'unit-manual-required', ok: false, target: journal.target, sources: journal.sources, journalPath: path, unitId: journal.unitId });
84
- }
85
- catch {
86
- reports.push({ runId: '(unit)', phase: 'unknown', action: 'unit-journal-unreadable', ok: false, journalPath: path });
87
- }
88
- }
89
- }
90
- try {
91
- await io.writeText(join(home, 'merge', 'recovery.json'), JSON.stringify(reports, null, 2));
92
- }
93
- catch { /* report file is best-effort; reports are still returned */ }
94
- return reports;
95
- }
96
- //# sourceMappingURL=commit-recovery.js.map
@@ -1,77 +0,0 @@
1
- /**
2
- * Pure merge transition table. No io, no run shell, no subagents.
3
- *
4
- * Every transition returns the whole next row (or a drop), so the caller can persist it
5
- * through one state transaction. Invalid transitions are refused by name;
6
- * a caller never "repairs" a row by guessing which state the operator meant.
7
- */
8
- import { type MergeDecision, type MergeFailure, type MergeRow } from './contracts.ts';
9
- export type MergeEvent = {
10
- readonly type: 'candidate';
11
- readonly a: string;
12
- readonly b: string;
13
- readonly at: string;
14
- } | {
15
- readonly type: 'round1-keep';
16
- readonly at: string;
17
- } | {
18
- readonly type: 'round1-merge';
19
- readonly approval: 'on' | 'off';
20
- readonly decision?: MergeDecision;
21
- readonly stagingRef?: string;
22
- readonly stagingExpiresAt?: number;
23
- readonly sourceRevision?: string;
24
- readonly targetRevision?: string;
25
- readonly at: string;
26
- } | {
27
- readonly type: 'round1-fail';
28
- readonly failure: MergeFailure;
29
- readonly at: string;
30
- } | {
31
- readonly type: 'approve';
32
- readonly runId?: string;
33
- readonly at: string;
34
- } | {
35
- readonly type: 'reject';
36
- readonly at: string;
37
- } | {
38
- readonly type: 'round2-fail';
39
- readonly failure: MergeFailure;
40
- readonly at: string;
41
- } | {
42
- readonly type: 'round2-cancel';
43
- readonly at: string;
44
- } | {
45
- readonly type: 'round2-success';
46
- readonly unitId?: string;
47
- readonly at: string;
48
- } | {
49
- readonly type: 'restore';
50
- readonly at: string;
51
- } | {
52
- readonly type: 'source-gone';
53
- readonly at: string;
54
- } | {
55
- readonly type: 'target-taken';
56
- readonly at: string;
57
- } | {
58
- readonly type: 'source-changed';
59
- readonly at: string;
60
- } | {
61
- readonly type: 'gate-keep';
62
- readonly a: string;
63
- readonly b: string;
64
- readonly reason?: string;
65
- readonly at: string;
66
- };
67
- export type TransitionResult = {
68
- readonly ok: true;
69
- readonly row: MergeRow | null;
70
- } | {
71
- readonly ok: false;
72
- readonly code: 'invalid-transition';
73
- readonly message: string;
74
- };
75
- /** Legacy v2 projection only; runtime writes must use transitionV3/RowTx. */
76
- export declare function transitionLegacyForProjection(current: MergeRow | undefined, event: MergeEvent): TransitionResult;
77
- //# sourceMappingURL=coordinator.d.ts.map