@lmzhen/dsh-evolution-merge 0.47.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 +175 -758
- 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 -1
- 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 +0 -2
- package/lib/types/state.js +0 -13
- package/lib/types/surface/merge-view.d.ts +1 -2
- package/lib/types/surface/merge-view.js +0 -1
- 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
|
@@ -2,7 +2,6 @@ import { v4ToLegacy } from "./domain/row-v4.js";
|
|
|
2
2
|
import { pairLedger } from "./domain/pair-ledger.js";
|
|
3
3
|
import { dropStage } from "./domain/stage-aggregate.js";
|
|
4
4
|
import { casExpiredHook } from "./domain/failure-route.js";
|
|
5
|
-
import { recordPair } from "./domain/run-coordinator.js";
|
|
6
5
|
export async function recordRound2Outcome(io, home, now, row, outcome, deferProjection = false, fence) {
|
|
7
6
|
void now;
|
|
8
7
|
const reportExtra = {
|
|
@@ -10,9 +9,13 @@ export async function recordRound2Outcome(io, home, now, row, outcome, deferProj
|
|
|
10
9
|
...(outcome.criteria === undefined ? {} : { criteria: outcome.criteria }),
|
|
11
10
|
...(outcome.validationIssues === undefined ? {} : { validation: { issues: outcome.validationIssues } }),
|
|
12
11
|
};
|
|
12
|
+
const pair = (outcomeName, extra = {}, withDecision = true) => ({
|
|
13
|
+
id: row.id, pairKey: row.pairKey, a: row.a, b: row.b, queue: row.queue,
|
|
14
|
+
...(withDecision && row.decision !== undefined ? { decision: row.decision } : {}),
|
|
15
|
+
outcome: outcomeName, archived: [], ...extra,
|
|
16
|
+
});
|
|
13
17
|
if (outcome.kind === 'merged') {
|
|
14
18
|
const unitRef = { unitId: outcome.unitId ?? 'unknown', evidenceSha256: outcome.evidenceSha256 ?? 'unknown' };
|
|
15
|
-
const target = outcome.target?.actual ?? outcome.target?.requested ?? row.decision?.target;
|
|
16
19
|
const finalize = async () => {
|
|
17
20
|
const removed = await pairLedger(io, home, fence).transact(tx => {
|
|
18
21
|
const current = tx.rows().find(item => item.id === row.id);
|
|
@@ -21,31 +24,20 @@ export async function recordRound2Outcome(io, home, now, row, outcome, deferProj
|
|
|
21
24
|
if (!removed.ok)
|
|
22
25
|
throw new Error('row changed before commit record: ' + row.id);
|
|
23
26
|
await dropStage(io, home, row.id);
|
|
24
|
-
await recordPair(io, home, {
|
|
25
|
-
pairKey: row.pairKey,
|
|
26
|
-
runId: row.runId ?? 'merge',
|
|
27
|
-
result: 'merged',
|
|
28
|
-
...(target === undefined ? {} : { target }),
|
|
29
|
-
...(outcome.unitId === undefined ? {} : { reportRef: outcome.unitId }),
|
|
30
|
-
});
|
|
31
27
|
};
|
|
32
28
|
if (deferProjection)
|
|
33
|
-
return { merged: true, finalize, pair:
|
|
34
|
-
|
|
35
|
-
await finalize();
|
|
36
|
-
}
|
|
29
|
+
return { merged: true, finalize, pair: pair('merged', { ...(outcome.lengths === undefined ? {} : { lengths: outcome.lengths }), ...(outcome.unitId === undefined ? {} : { unitId: outcome.unitId }), ...(outcome.target === undefined ? {} : { target: outcome.target }), ...reportExtra }) };
|
|
30
|
+
await finalize();
|
|
37
31
|
return {
|
|
38
32
|
merged: true,
|
|
39
|
-
pair: {
|
|
40
|
-
|
|
41
|
-
...(row.decision === undefined ? {} : { decision: row.decision }),
|
|
42
|
-
outcome: 'merged', archived: outcome.archived ?? [],
|
|
33
|
+
pair: pair('merged', {
|
|
34
|
+
...(outcome.archived === undefined ? {} : { archived: outcome.archived }),
|
|
43
35
|
...(outcome.lengths === undefined ? {} : { lengths: outcome.lengths }),
|
|
44
36
|
...reportExtra,
|
|
45
37
|
...(outcome.unitId === undefined ? {} : { unitId: outcome.unitId }),
|
|
46
38
|
...(outcome.target === undefined ? {} : { target: outcome.target }),
|
|
47
39
|
...(outcome.validationPlan === undefined ? {} : { validationPlan: outcome.validationPlan }),
|
|
48
|
-
},
|
|
40
|
+
}),
|
|
49
41
|
};
|
|
50
42
|
}
|
|
51
43
|
const failure = outcome.failure ?? { stage: 'author', code: 'merge-failed', message: 'round2 failed', retryable: true, retryFrom: 'agent2' };
|
|
@@ -57,7 +49,7 @@ export async function recordRound2Outcome(io, home, now, row, outcome, deferProj
|
|
|
57
49
|
});
|
|
58
50
|
if (skipped)
|
|
59
51
|
await dropStage(io, home, row.id);
|
|
60
|
-
return { merged: false, pair:
|
|
52
|
+
return { merged: false, pair: pair('failed', { failure, ...reportExtra }) };
|
|
61
53
|
}
|
|
62
54
|
const cas = casExpiredHook(failure.code);
|
|
63
55
|
if (cas !== null) {
|
|
@@ -70,7 +62,7 @@ export async function recordRound2Outcome(io, home, now, row, outcome, deferProj
|
|
|
70
62
|
return moved.ok;
|
|
71
63
|
});
|
|
72
64
|
await dropStage(io, home, row.id);
|
|
73
|
-
return { merged: false, pair:
|
|
65
|
+
return { merged: false, pair: pair('failed', { failure: terminalFailure, ...reportExtra }) };
|
|
74
66
|
}
|
|
75
67
|
if (failure.code === 'commit-failed' || failure.code === 'validation-failed') {
|
|
76
68
|
// FR-EXEC-11/14: report-only failure. The pair leaves the active queue (so a later scan
|
|
@@ -81,7 +73,6 @@ export async function recordRound2Outcome(io, home, now, row, outcome, deferProj
|
|
|
81
73
|
});
|
|
82
74
|
if (dropped) {
|
|
83
75
|
await dropStage(io, home, row.id, row.stagingRef);
|
|
84
|
-
await recordPair(io, home, { pairKey: row.pairKey, runId: row.runId ?? 'merge', result: 'failed', ...(row.decision?.target === undefined ? {} : { target: row.decision.target }) });
|
|
85
76
|
}
|
|
86
77
|
}
|
|
87
78
|
if (failure.code === 'source-gone') {
|
|
@@ -91,7 +82,7 @@ export async function recordRound2Outcome(io, home, now, row, outcome, deferProj
|
|
|
91
82
|
});
|
|
92
83
|
if (dropped)
|
|
93
84
|
await dropStage(io, home, row.id, row.stagingRef);
|
|
94
|
-
return { merged: false, pair:
|
|
85
|
+
return { merged: false, pair: pair('failed', { failure }, false) };
|
|
95
86
|
}
|
|
96
87
|
const projected = await pairLedger(io, home, fence).transact(tx => {
|
|
97
88
|
const current = tx.rows().find(item => item.id === row.id);
|
|
@@ -101,23 +92,10 @@ export async function recordRound2Outcome(io, home, now, row, outcome, deferProj
|
|
|
101
92
|
return moved.ok && moved.row !== null ? v4ToLegacy(moved.row) : null;
|
|
102
93
|
});
|
|
103
94
|
if (projected === null)
|
|
104
|
-
return { merged: false, pair:
|
|
105
|
-
{
|
|
106
|
-
await recordPair(io, home, {
|
|
107
|
-
pairKey: row.pairKey,
|
|
108
|
-
runId: row.runId ?? 'merge',
|
|
109
|
-
result: 'failed',
|
|
110
|
-
...(row.decision?.target === undefined ? {} : { target: row.decision.target }),
|
|
111
|
-
});
|
|
112
|
-
}
|
|
95
|
+
return { merged: false, pair: pair('failed', { failure: { ...failure, code: 'row-changed', message: 'row changed before failure record' }, ...reportExtra }, false) };
|
|
113
96
|
return {
|
|
114
97
|
merged: false,
|
|
115
|
-
pair: {
|
|
116
|
-
id: row.id, pairKey: row.pairKey, a: row.a, b: row.b, queue: row.queue,
|
|
117
|
-
...(row.decision === undefined ? {} : { decision: row.decision }),
|
|
118
|
-
outcome: 'failed', archived: [], failure,
|
|
119
|
-
...reportExtra,
|
|
120
|
-
},
|
|
98
|
+
pair: pair('failed', { failure, ...reportExtra }),
|
|
121
99
|
};
|
|
122
100
|
}
|
|
123
101
|
//# sourceMappingURL=round2-outcome.js.map
|
package/lib/types/scan-run.js
CHANGED
|
@@ -6,7 +6,6 @@ import { pairKey, rowIdOf, } from "./contracts.js";
|
|
|
6
6
|
import { loadMergeState } from "./state.js";
|
|
7
7
|
import { pairLedger } from "./domain/pair-ledger.js";
|
|
8
8
|
import { presentStage, dropStage } from "./domain/stage-aggregate.js";
|
|
9
|
-
import { recordPair } from "./domain/run-coordinator.js";
|
|
10
9
|
import { v4ToLegacy } from "./domain/row-v4.js";
|
|
11
10
|
import { criteriaFrom } from "./domain/engine-criteria.js";
|
|
12
11
|
import { withDecisionRevisions } from "./revision.js";
|
|
@@ -176,7 +175,6 @@ export async function runScan(options) {
|
|
|
176
175
|
continue;
|
|
177
176
|
}
|
|
178
177
|
row = projected;
|
|
179
|
-
await recordPair(options.io, options.home, { pairKey: row.pairKey, runId: row.runId ?? 'merge', result: 'kept' });
|
|
180
178
|
pairs.push(pairOf(row, candidate, 'kept', { ...(round1.decision === undefined ? {} : { decision: round1.decision }), ...(round1.usage === undefined ? {} : { usage: round1.usage }) }));
|
|
181
179
|
totals.kept += 1;
|
|
182
180
|
continue;
|
|
@@ -324,7 +322,6 @@ export async function runScan(options) {
|
|
|
324
322
|
});
|
|
325
323
|
if (dropped) {
|
|
326
324
|
await dropStage(options.io, options.home, row.id, row.stagingRef);
|
|
327
|
-
await recordPair(options.io, options.home, { pairKey: row.pairKey, runId: row.runId ?? 'merge', result: 'failed', ...(row.decision?.target === undefined ? {} : { target: row.decision.target }) });
|
|
328
325
|
}
|
|
329
326
|
pairs.push(pairOf(row, candidate, 'failed', { decision: mergeDecision, failure: failed, ...(round2.manifestFiles === undefined ? {} : { manifestFiles: round2.manifestFiles }), ...(round2.criteria === undefined ? {} : { criteria: round2.criteria }), ...(round2.validationIssues === undefined ? {} : { validation: { issues: round2.validationIssues } }) }));
|
|
330
327
|
totals.failed += 1;
|
package/lib/types/skipped.d.ts
CHANGED
|
@@ -2,7 +2,6 @@
|
|
|
2
2
|
* Skipped filtering: a pair the operator marked as not-worth-merging stays out of automatic
|
|
3
3
|
* screening until mergeSkippedTtlDays expires. Explicit /merge pair ignores this set.
|
|
4
4
|
*/
|
|
5
|
-
import {
|
|
5
|
+
import type { MergeRow } from './contracts.ts';
|
|
6
6
|
export declare function skippedPairKeys(rows: readonly MergeRow[], ttlDays: number, now: number): ReadonlySet<string>;
|
|
7
|
-
export declare function isSkippedPair(keys: ReadonlySet<string>, a: string, b: string): boolean;
|
|
8
7
|
//# sourceMappingURL=skipped.d.ts.map
|
package/lib/types/skipped.js
CHANGED
|
@@ -1,8 +1,3 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Skipped filtering: a pair the operator marked as not-worth-merging stays out of automatic
|
|
3
|
-
* screening until mergeSkippedTtlDays expires. Explicit /merge pair ignores this set.
|
|
4
|
-
*/
|
|
5
|
-
import { pairKey } from "./contracts.js";
|
|
6
1
|
export function skippedPairKeys(rows, ttlDays, now) {
|
|
7
2
|
const keys = new Set();
|
|
8
3
|
for (const row of rows) {
|
|
@@ -18,7 +13,4 @@ export function skippedPairKeys(rows, ttlDays, now) {
|
|
|
18
13
|
}
|
|
19
14
|
return keys;
|
|
20
15
|
}
|
|
21
|
-
export function isSkippedPair(keys, a, b) {
|
|
22
|
-
return keys.has(pairKey(a, b));
|
|
23
|
-
}
|
|
24
16
|
//# sourceMappingURL=skipped.js.map
|
package/lib/types/staging.d.ts
CHANGED
|
@@ -43,9 +43,7 @@ export interface StagedView {
|
|
|
43
43
|
readonly manifest: StagingManifest;
|
|
44
44
|
}
|
|
45
45
|
export declare function readStaging(io: EvolutionIoLike, home: string, runId: string): Promise<Probe<StagedView>>;
|
|
46
|
-
export declare function ensureStaging(io: EvolutionIoLike, home: string, input: CreateStagingInput): Promise<CreateStagingResult>;
|
|
47
46
|
export declare function setStagingTarget(io: EvolutionIoLike, home: string, stageKey: string, target: string, mode?: 'merge-into-existing' | 'create-umbrella' | 'keep'): Promise<boolean>;
|
|
48
|
-
export declare function dropStaging(io: EvolutionIoLike, home: string, stageKey: string): Promise<void>;
|
|
49
47
|
export interface StagingCleanupResult {
|
|
50
48
|
readonly removed: readonly string[];
|
|
51
49
|
readonly kept: readonly string[];
|
package/lib/types/staging.js
CHANGED
|
@@ -93,12 +93,6 @@ export async function readStaging(io, home, runId) {
|
|
|
93
93
|
return probeUnknown('staging manifest is not JSON: ' + (error instanceof Error ? error.message : String(error)));
|
|
94
94
|
}
|
|
95
95
|
}
|
|
96
|
-
export async function ensureStaging(io, home, input) {
|
|
97
|
-
const existing = await readStaging(io, home, input.runId);
|
|
98
|
-
if (existing.kind === 'present')
|
|
99
|
-
return { ok: true, path: stagingRoot(home, input.runId), manifest: existing.value.manifest };
|
|
100
|
-
return createStaging(io, home, input);
|
|
101
|
-
}
|
|
102
96
|
export async function setStagingTarget(io, home, stageKey, target, mode) {
|
|
103
97
|
const view = await readStaging(io, home, stageKey);
|
|
104
98
|
if (view.kind !== 'present')
|
|
@@ -107,9 +101,6 @@ export async function setStagingTarget(io, home, stageKey, target, mode) {
|
|
|
107
101
|
await io.writeText(join(stagingRoot(home, stageKey), 'manifest.json'), JSON.stringify(manifest, null, 2) + '\n');
|
|
108
102
|
return true;
|
|
109
103
|
}
|
|
110
|
-
export async function dropStaging(io, home, stageKey) {
|
|
111
|
-
await io.remove(stagingRoot(home, stageKey));
|
|
112
|
-
}
|
|
113
104
|
/** Remove expired run-level staging trees; `keep` names runs a live row still points at. */
|
|
114
105
|
export async function cleanupExpiredStaging(io, home, now, keep = new Set()) {
|
|
115
106
|
let runs;
|
package/lib/types/state.d.ts
CHANGED
|
@@ -8,7 +8,5 @@ export interface LoadMergeStateResult {
|
|
|
8
8
|
}
|
|
9
9
|
/** Pure read: never writes, never deletes a row. */
|
|
10
10
|
export declare function loadMergeState(io: EvolutionIoLike, home: string): Promise<LoadMergeStateResult>;
|
|
11
|
-
/** Schema probe for doctor; the only other reader of mergeStatePath. */
|
|
12
|
-
export declare function loadStateSchema(io: EvolutionIoLike, home: string): Promise<number | null>;
|
|
13
11
|
export declare function rowsOf(io: EvolutionIoLike, home: string, queue?: MergeQueue): Promise<readonly MergeRow[]>;
|
|
14
12
|
//# sourceMappingURL=state.d.ts.map
|
package/lib/types/state.js
CHANGED
|
@@ -54,19 +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
57
|
export async function rowsOf(io, home, queue) {
|
|
71
58
|
const loaded = await loadMergeState(io, home);
|
|
72
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,7 +130,6 @@ 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)
|
|
@@ -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
|