@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.
Files changed (73) hide show
  1. package/lib/index.js +175 -758
  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 -1
  30. package/lib/types/new-child-authoring.js +4 -10
  31. package/lib/types/prompts.d.ts +0 -1
  32. package/lib/types/prompts.js +0 -1
  33. package/lib/types/read-commands.d.ts +0 -1
  34. package/lib/types/read-commands.js +0 -4
  35. package/lib/types/recovery-projection.js +0 -3
  36. package/lib/types/round2-outcome.js +15 -37
  37. package/lib/types/scan-run.js +0 -3
  38. package/lib/types/skipped.d.ts +1 -2
  39. package/lib/types/skipped.js +0 -8
  40. package/lib/types/staging.d.ts +0 -2
  41. package/lib/types/staging.js +0 -9
  42. package/lib/types/state.d.ts +0 -2
  43. package/lib/types/state.js +0 -13
  44. package/lib/types/surface/merge-view.d.ts +1 -2
  45. package/lib/types/surface/merge-view.js +0 -1
  46. package/lib/types/undo-operation.js +0 -9
  47. package/lib/types/unit-protocol.d.ts +6 -0
  48. package/lib/types/unit-protocol.js +72 -102
  49. package/package.json +4 -4
  50. package/lib/types/commit-recovery.d.ts +0 -14
  51. package/lib/types/commit-recovery.js +0 -96
  52. package/lib/types/coordinator.d.ts +0 -77
  53. package/lib/types/coordinator.js +0 -137
  54. package/lib/types/domain/commit-journal-store.d.ts +0 -6
  55. package/lib/types/domain/commit-journal-store.js +0 -26
  56. package/lib/types/domain/commit-journal.d.ts +0 -16
  57. package/lib/types/domain/commit-journal.js +0 -26
  58. package/lib/types/domain/indexes.d.ts +0 -21
  59. package/lib/types/domain/indexes.js +0 -15
  60. package/lib/types/domain/merge-report.d.ts +0 -41
  61. package/lib/types/domain/merge-report.js +0 -2
  62. package/lib/types/domain/pair-index-store.d.ts +0 -6
  63. package/lib/types/domain/pair-index-store.js +0 -25
  64. package/lib/types/domain/pair-index.d.ts +0 -9
  65. package/lib/types/domain/pair-index.js +0 -28
  66. package/lib/types/domain/resolve-pair.d.ts +0 -4
  67. package/lib/types/domain/resolve-pair.js +0 -4
  68. package/lib/types/domain/row-tx.d.ts +0 -27
  69. package/lib/types/domain/row-tx.js +0 -31
  70. package/lib/types/domain/run-record.d.ts +0 -45
  71. package/lib/types/domain/run-record.js +0 -24
  72. package/lib/types/domain/state-v3.d.ts +0 -4
  73. 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: { id: row.id, pairKey: row.pairKey, a: row.a, b: row.b, queue: row.queue, ...(row.decision === undefined ? {} : { decision: row.decision }), outcome: 'merged', archived: outcome.archived ?? [], ...(outcome.lengths === undefined ? {} : { lengths: outcome.lengths }), ...(outcome.unitId === undefined ? {} : { unitId: outcome.unitId }), ...(outcome.target === undefined ? {} : { target: outcome.target }), ...reportExtra } };
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
- id: row.id, pairKey: row.pairKey, a: row.a, b: row.b, queue: row.queue,
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: { id: row.id, pairKey: row.pairKey, a: row.a, b: row.b, queue: row.queue, ...(row.decision === undefined ? {} : { decision: row.decision }), outcome: 'failed', archived: [], failure, ...reportExtra } };
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: { id: row.id, pairKey: row.pairKey, a: row.a, b: row.b, queue: row.queue, ...(row.decision === undefined ? {} : { decision: row.decision }), outcome: 'failed', archived: [], failure: terminalFailure, ...reportExtra } };
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: { id: row.id, pairKey: row.pairKey, a: row.a, b: row.b, queue: row.queue, outcome: 'failed', archived: [], failure } };
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: { id: row.id, pairKey: row.pairKey, a: row.a, b: row.b, queue: row.queue, outcome: 'failed', archived: [], failure: { ...failure, code: 'row-changed', message: 'row changed before failure record' }, ...reportExtra } };
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
@@ -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;
@@ -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 { type MergeRow } from './contracts.ts';
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
@@ -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
@@ -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[];
@@ -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;
@@ -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
@@ -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, 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,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
- 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.47.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.47.0",
38
- "@lmzhen/dsh-evolution-commands": "^0.47.0",
39
- "@lmzhen/dsh-evolution-merge-engine": "0.47.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