@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.
- package/lib/index.js +248 -1224
- package/lib/types/adapters/engine-screen.d.ts +0 -3
- package/lib/types/authoring-flow.d.ts +5 -0
- package/lib/types/authoring-flow.js +17 -10
- package/lib/types/commit-execute.js +1 -22
- package/lib/types/contracts.d.ts +0 -1
- package/lib/types/contracts.js +0 -1
- package/lib/types/doctor.d.ts +0 -1
- package/lib/types/doctor.js +2 -31
- package/lib/types/domain/consistency.d.ts +0 -1
- package/lib/types/domain/consistency.js +2 -11
- package/lib/types/domain/merge-limits.d.ts +0 -10
- package/lib/types/domain/merge-limits.js +0 -3
- package/lib/types/domain/merge-snapshot.js +4 -6
- package/lib/types/domain/pair-ledger.d.ts +0 -1
- package/lib/types/domain/row-v3.d.ts +0 -45
- package/lib/types/domain/row-v3.js +0 -68
- package/lib/types/domain/row.d.ts +2 -84
- package/lib/types/domain/row.js +1 -110
- package/lib/types/domain/run-coordinator.d.ts +0 -24
- package/lib/types/domain/run-coordinator.js +0 -1
- package/lib/types/domain/stage-aggregate.d.ts +0 -42
- package/lib/types/domain/stage-aggregate.js +1 -116
- package/lib/types/domain/state.d.ts +1 -53
- package/lib/types/domain/state.js +0 -55
- package/lib/types/domain/undo-journal.d.ts +4 -4
- package/lib/types/domain/undo-journal.js +5 -7
- package/lib/types/index.d.ts +0 -1
- package/lib/types/index.js +0 -6
- package/lib/types/merge-routes.d.ts +5 -21
- package/lib/types/merge-routes.js +53 -93
- package/lib/types/new-child-authoring.js +4 -10
- package/lib/types/prompts.d.ts +0 -1
- package/lib/types/prompts.js +0 -1
- package/lib/types/read-commands.d.ts +0 -1
- package/lib/types/read-commands.js +0 -4
- package/lib/types/recovery-projection.js +0 -3
- package/lib/types/round2-outcome.js +15 -37
- package/lib/types/scan-run.js +0 -3
- package/lib/types/skipped.d.ts +1 -2
- package/lib/types/skipped.js +0 -8
- package/lib/types/staging.d.ts +0 -2
- package/lib/types/staging.js +0 -9
- package/lib/types/state.d.ts +1 -39
- package/lib/types/state.js +2 -78
- package/lib/types/surface/merge-view.d.ts +1 -2
- package/lib/types/surface/merge-view.js +1 -2
- package/lib/types/undo-operation.js +0 -9
- package/lib/types/unit-protocol.d.ts +6 -0
- package/lib/types/unit-protocol.js +72 -102
- package/package.json +4 -4
- package/lib/types/commit-recovery.d.ts +0 -14
- package/lib/types/commit-recovery.js +0 -96
- package/lib/types/coordinator.d.ts +0 -77
- package/lib/types/coordinator.js +0 -137
- package/lib/types/domain/commit-journal-store.d.ts +0 -6
- package/lib/types/domain/commit-journal-store.js +0 -26
- package/lib/types/domain/commit-journal.d.ts +0 -16
- package/lib/types/domain/commit-journal.js +0 -26
- package/lib/types/domain/indexes.d.ts +0 -21
- package/lib/types/domain/indexes.js +0 -15
- package/lib/types/domain/merge-report.d.ts +0 -41
- package/lib/types/domain/merge-report.js +0 -2
- package/lib/types/domain/pair-index-store.d.ts +0 -6
- package/lib/types/domain/pair-index-store.js +0 -25
- package/lib/types/domain/pair-index.d.ts +0 -9
- package/lib/types/domain/pair-index.js +0 -28
- package/lib/types/domain/resolve-pair.d.ts +0 -4
- package/lib/types/domain/resolve-pair.js +0 -4
- package/lib/types/domain/row-tx.d.ts +0 -27
- package/lib/types/domain/row-tx.js +0 -31
- package/lib/types/domain/run-record.d.ts +0 -45
- package/lib/types/domain/run-record.js +0 -24
- package/lib/types/domain/state-v3.d.ts +0 -4
- package/lib/types/domain/state-v3.js +0 -4
- package/lib/types/facts.d.ts +0 -26
- package/lib/types/facts.js +0 -65
- package/lib/types/metrics.d.ts +0 -16
- package/lib/types/metrics.js +0 -27
- package/lib/types/wire-projection.d.ts +0 -36
- package/lib/types/wire-projection.js +0 -77
package/lib/types/state.js
CHANGED
|
@@ -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
|
|
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,
|
|
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,
|
|
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
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
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
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
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
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
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
|
-
|
|
477
|
-
|
|
478
|
-
|
|
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.
|
|
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.
|
|
38
|
-
"@lmzhen/dsh-evolution-commands": "^0.
|
|
39
|
-
"@lmzhen/dsh-evolution-merge-engine": "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
|