@lmzhen/dsh-evolution-merge 0.30.18 → 0.30.19

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (143) hide show
  1. package/README.md +21 -4
  2. package/data/minhash_params.json +1063 -0
  3. package/lib/index.js +7313 -3272
  4. package/lib/types/adapters/channel.d.ts +1 -0
  5. package/lib/types/adapters/channel.js +15 -1
  6. package/lib/types/adapters/engine-screen.d.ts +1 -1
  7. package/lib/types/adapters/engine-screen.js +5 -2
  8. package/lib/types/adapters/model-runner.d.ts +0 -5
  9. package/lib/types/adapters/model-runner.js +8 -2
  10. package/lib/types/adapters/model.js +2 -1
  11. package/lib/types/adapters/select.js +12 -8
  12. package/lib/types/approve-command.d.ts +12 -1
  13. package/lib/types/approve-command.js +39 -6
  14. package/lib/types/authoring-flow.d.ts +16 -0
  15. package/lib/types/authoring-flow.js +37 -13
  16. package/lib/types/channel-port.d.ts +3 -1
  17. package/lib/types/channel-port.js +8 -3
  18. package/lib/types/command-defs.d.ts +5 -0
  19. package/lib/types/command-defs.js +33 -0
  20. package/lib/types/command-surface.d.ts +1 -1
  21. package/lib/types/command-surface.js +10 -43
  22. package/lib/types/commands.d.ts +3 -4
  23. package/lib/types/commands.js +33 -72
  24. package/lib/types/commit-assembly.d.ts +14 -1
  25. package/lib/types/commit-assembly.js +65 -9
  26. package/lib/types/commit-execute.d.ts +7 -1
  27. package/lib/types/commit-execute.js +54 -22
  28. package/lib/types/commit-plan.d.ts +7 -1
  29. package/lib/types/commit-plan.js +4 -0
  30. package/lib/types/commit-receipt.d.ts +3 -0
  31. package/lib/types/contracts.d.ts +43 -0
  32. package/lib/types/doctor.d.ts +11 -1
  33. package/lib/types/doctor.js +81 -27
  34. package/lib/types/domain/authoring-manifest.d.ts +26 -0
  35. package/lib/types/domain/authoring-manifest.js +43 -0
  36. package/lib/types/domain/engine-criteria.d.ts +10 -0
  37. package/lib/types/domain/engine-criteria.js +11 -0
  38. package/lib/types/domain/engine-input.d.ts +10 -0
  39. package/lib/types/domain/engine-input.js +26 -0
  40. package/lib/types/domain/errors.d.ts +15 -1
  41. package/lib/types/domain/errors.js +23 -4
  42. package/lib/types/domain/evidence-hash.d.ts +3 -0
  43. package/lib/types/domain/evidence-hash.js +14 -0
  44. package/lib/types/domain/failure-route.d.ts +14 -0
  45. package/lib/types/domain/failure-route.js +12 -0
  46. package/lib/types/domain/live-facts.d.ts +31 -0
  47. package/lib/types/domain/live-facts.js +54 -0
  48. package/lib/types/domain/merge-limits.d.ts +18 -0
  49. package/lib/types/domain/merge-limits.js +22 -0
  50. package/lib/types/domain/merge-report.d.ts +41 -0
  51. package/lib/types/domain/merge-report.js +2 -0
  52. package/lib/types/domain/merge-snapshot.d.ts +99 -0
  53. package/lib/types/domain/merge-snapshot.js +82 -0
  54. package/lib/types/domain/pair-ledger.d.ts +34 -0
  55. package/lib/types/domain/pair-ledger.js +96 -0
  56. package/lib/types/domain/pre-1b-backup.d.ts +43 -0
  57. package/lib/types/domain/pre-1b-backup.js +140 -0
  58. package/lib/types/domain/provider-key.d.ts +2 -0
  59. package/lib/types/domain/provider-key.js +11 -0
  60. package/lib/types/domain/read-probe.d.ts +5 -0
  61. package/lib/types/domain/read-probe.js +11 -0
  62. package/lib/types/domain/reconcile.d.ts +47 -0
  63. package/lib/types/domain/reconcile.js +178 -0
  64. package/lib/types/domain/resolve-pair.d.ts +2 -9
  65. package/lib/types/domain/resolve-pair.js +2 -15
  66. package/lib/types/domain/row-v4.d.ts +148 -0
  67. package/lib/types/domain/row-v4.js +159 -0
  68. package/lib/types/domain/run-coordinator.d.ts +51 -0
  69. package/lib/types/domain/run-coordinator.js +45 -0
  70. package/lib/types/domain/schema4.d.ts +53 -0
  71. package/lib/types/domain/schema4.js +129 -0
  72. package/lib/types/domain/selection-policy.d.ts +14 -0
  73. package/lib/types/domain/selection-policy.js +6 -0
  74. package/lib/types/domain/skipped-expiry.d.ts +5 -0
  75. package/lib/types/domain/skipped-expiry.js +16 -0
  76. package/lib/types/domain/stage-aggregate.d.ts +96 -0
  77. package/lib/types/domain/stage-aggregate.js +293 -0
  78. package/lib/types/domain/staging-index.d.ts +1 -0
  79. package/lib/types/domain/state-bootstrap.d.ts +43 -0
  80. package/lib/types/domain/state-bootstrap.js +267 -0
  81. package/lib/types/domain/state-v3.d.ts +3 -14
  82. package/lib/types/domain/state-v3.js +3 -79
  83. package/lib/types/domain/undo-journal.d.ts +19 -0
  84. package/lib/types/domain/undo-journal.js +20 -0
  85. package/lib/types/domain/undo-receipt.d.ts +18 -0
  86. package/lib/types/domain/undo-receipt.js +22 -0
  87. package/lib/types/domain/validation-plan.d.ts +65 -0
  88. package/lib/types/domain/validation-plan.js +49 -0
  89. package/lib/types/facts.d.ts +3 -1
  90. package/lib/types/facts.js +13 -16
  91. package/lib/types/index.d.ts +2 -0
  92. package/lib/types/index.js +181 -68
  93. package/lib/types/merge-command.d.ts +3 -1
  94. package/lib/types/merge-command.js +10 -6
  95. package/lib/types/merge-routes.d.ts +10 -5
  96. package/lib/types/merge-routes.js +72 -7
  97. package/lib/types/merge-tool.d.ts +1 -1
  98. package/lib/types/merge-tool.js +2 -2
  99. package/lib/types/metrics.js +1 -1
  100. package/lib/types/new-child-authoring.d.ts +2 -0
  101. package/lib/types/new-child-authoring.js +57 -10
  102. package/lib/types/params.d.ts +2 -4
  103. package/lib/types/params.js +13 -0
  104. package/lib/types/pending-actions.d.ts +2 -2
  105. package/lib/types/pending-actions.js +4 -4
  106. package/lib/types/prompts.d.ts +2 -4
  107. package/lib/types/prompts.js +21 -8
  108. package/lib/types/read-commands.d.ts +3 -3
  109. package/lib/types/read-commands.js +19 -28
  110. package/lib/types/recovery-coordinator.d.ts +34 -0
  111. package/lib/types/recovery-coordinator.js +84 -0
  112. package/lib/types/recovery-projection.d.ts +31 -0
  113. package/lib/types/recovery-projection.js +96 -0
  114. package/lib/types/resume-command.d.ts +5 -1
  115. package/lib/types/resume-command.js +46 -13
  116. package/lib/types/revision.d.ts +17 -0
  117. package/lib/types/revision.js +39 -3
  118. package/lib/types/round2-outcome.d.ts +2 -1
  119. package/lib/types/round2-outcome.js +59 -11
  120. package/lib/types/run-shell.d.ts +16 -2
  121. package/lib/types/run-shell.js +49 -5
  122. package/lib/types/runtime.d.ts +0 -5
  123. package/lib/types/runtime.js +274 -89
  124. package/lib/types/scan-command.d.ts +1 -0
  125. package/lib/types/scan-command.js +2 -0
  126. package/lib/types/scan-ports.d.ts +18 -0
  127. package/lib/types/scan-ports.js +49 -23
  128. package/lib/types/scan-run.d.ts +17 -0
  129. package/lib/types/scan-run.js +143 -48
  130. package/lib/types/stage-tool-registry.d.ts +7 -0
  131. package/lib/types/stage-tool-registry.js +12 -1
  132. package/lib/types/state.d.ts +5 -5
  133. package/lib/types/state.js +22 -69
  134. package/lib/types/status.d.ts +6 -1
  135. package/lib/types/status.js +47 -0
  136. package/lib/types/surface/body.d.ts +4 -0
  137. package/lib/types/surface/body.js +16 -0
  138. package/lib/types/surface/merge-view.d.ts +14 -0
  139. package/lib/types/surface/merge-view.js +126 -0
  140. package/lib/types/undo-operation.d.ts +24 -0
  141. package/lib/types/undo-operation.js +135 -0
  142. package/lib/types/wire.d.ts +22 -3
  143. package/package.json +6 -4
@@ -0,0 +1,84 @@
1
+ /** Batch 2c unit-journal recovery coordinator. UnitTransaction remains the only skill writer. */
2
+ import { join } from 'node:path';
3
+ import { probeList, readUnitEvidence } from '@lmzhen/dsh-evolution-core';
4
+ import { evidenceSha256 } from "./domain/evidence-hash.js";
5
+ import { projectRecoveredUnit } from "./recovery-projection.js";
6
+ const branchOf = (phase) => {
7
+ if (phase === 'prepared' || phase === 'target-pending')
8
+ return 'A';
9
+ if (phase === 'target-written' || phase === 'support-pending' || phase === 'source-archiving')
10
+ return 'B';
11
+ if (phase === 'sources-archived' || phase === 'unit-written')
12
+ return 'C';
13
+ if (phase === 'committed')
14
+ return 'D';
15
+ return 'A';
16
+ };
17
+ export async function recoverCommitUnits(options) {
18
+ const dir = join(options.home, 'merge', 'unit-journals');
19
+ const listed = await probeList(options.io, dir);
20
+ if (listed.kind !== 'present')
21
+ return [];
22
+ const files = listed.value.filter(name => name.endsWith('.json'));
23
+ const reports = [];
24
+ for (const file of files.sort()) {
25
+ let journal = null;
26
+ try {
27
+ journal = JSON.parse(await options.io.readText(join(dir, file)) ?? '');
28
+ }
29
+ catch {
30
+ journal = null;
31
+ }
32
+ if (journal === null || typeof journal.unitId !== 'string')
33
+ continue;
34
+ if (options.isActive?.(journal.runId) === true) {
35
+ reports.push({ unitId: journal.unitId, runId: journal.runId, phase: journal.phase, branch: branchOf(journal.phase), action: 'none', ok: true, code: 'skipped-active-run' });
36
+ continue;
37
+ }
38
+ const evidence = await readUnitEvidence(options.io, options.home, journal.unitId);
39
+ const branch = branchOf(journal.phase);
40
+ if (journal.phase === 'committed' || evidence !== null || journal.phase === 'unit-written' || journal.phase === 'sources-archived') {
41
+ const result = await options.unit.finalize(journal.unitId);
42
+ if (result.ok) {
43
+ let projection;
44
+ try {
45
+ projection = await projectRecoveredUnit({ io: options.io, home: options.home, runId: journal.runId, unitId: journal.unitId, evidence: result.evidence, now: options.now, ...(options.runs === undefined ? {} : { runs: options.runs }), ...(options.onProjectionPhase === undefined ? {} : { onPhase: options.onProjectionPhase }) });
46
+ }
47
+ catch (error) {
48
+ projection = { ok: false, action: 'projection-threw', message: error instanceof Error ? error.message : String(error) };
49
+ }
50
+ if (!projection.ok)
51
+ reports.push({ unitId: journal.unitId, runId: journal.runId, phase: journal.phase, branch, action: 'blocked', ok: false, code: 'projection-failed', message: projection.message });
52
+ else
53
+ reports.push({ unitId: journal.unitId, runId: journal.runId, phase: journal.phase, branch, action: 'finalize', ok: true, evidenceSha256: evidenceSha256(result.evidence) });
54
+ continue;
55
+ }
56
+ reports.push({ unitId: journal.unitId, runId: journal.runId, phase: journal.phase, branch, action: 'finalize', ok: false, code: result.code, message: result.message });
57
+ continue;
58
+ }
59
+ if (branch === 'A') {
60
+ const result = await options.unit.rollback({ unitId: journal.unitId, reason: 'startup recovery branch A' });
61
+ reports.push({ unitId: journal.unitId, runId: journal.runId, phase: journal.phase, branch, action: 'rollback', ok: result.ok, ...(result.ok ? {} : { code: result.code, message: result.message }) });
62
+ continue;
63
+ }
64
+ if (branch === 'B') {
65
+ const result = await options.unit.rollForward(journal.unitId);
66
+ if (result.ok) {
67
+ let projection;
68
+ try {
69
+ projection = await projectRecoveredUnit({ io: options.io, home: options.home, runId: journal.runId, unitId: journal.unitId, evidence: result.evidence, now: options.now, ...(options.runs === undefined ? {} : { runs: options.runs }), ...(options.onProjectionPhase === undefined ? {} : { onPhase: options.onProjectionPhase }) });
70
+ }
71
+ catch (error) {
72
+ projection = { ok: false, action: 'projection-threw', message: error instanceof Error ? error.message : String(error) };
73
+ }
74
+ reports.push({ unitId: journal.unitId, runId: journal.runId, phase: journal.phase, branch, action: projection.ok ? 'rollForward' : 'blocked', ok: projection.ok, ...(projection.ok ? { evidenceSha256: evidenceSha256(result.evidence) } : { code: 'projection-failed', message: projection.message }) });
75
+ }
76
+ else
77
+ reports.push({ unitId: journal.unitId, runId: journal.runId, phase: journal.phase, branch, action: 'rollForward', ok: false, code: result.code, message: result.message });
78
+ continue;
79
+ }
80
+ reports.push({ unitId: journal.unitId, runId: journal.runId, phase: journal.phase, branch, action: 'blocked', ok: false, code: 'unit-recovery-blocked' });
81
+ }
82
+ return reports;
83
+ }
84
+ //# sourceMappingURL=recovery-coordinator.js.map
@@ -0,0 +1,31 @@
1
+ /** Post-recovery projection: evidence -> report -> row remove -> stage drop -> settle/meta. */
2
+ import type { EvolutionIoLike, UnitEvidenceRecord } from '@lmzhen/dsh-evolution-core';
3
+ export interface ProjectionResult {
4
+ readonly ok: boolean;
5
+ readonly action: string;
6
+ readonly message?: string;
7
+ readonly reportPath?: string;
8
+ }
9
+ export declare function projectRecoveredUnit(options: {
10
+ readonly io: EvolutionIoLike;
11
+ readonly home: string;
12
+ readonly runId: string;
13
+ readonly unitId: string;
14
+ readonly evidence: UnitEvidenceRecord;
15
+ readonly now: () => number;
16
+ readonly runs?: {
17
+ settle(id: string, outcome: {
18
+ state: 'succeeded' | 'failed' | 'cancelled';
19
+ resultRef?: string;
20
+ failure?: string;
21
+ }): Promise<void>;
22
+ /** Controlled orphan upgrade: lets a committed unit turn failed(orphan) back into succeeded. */
23
+ settleRecovered?(id: string, outcome: {
24
+ state: 'succeeded' | 'failed' | 'cancelled';
25
+ resultRef?: string;
26
+ failure?: string;
27
+ }): Promise<boolean>;
28
+ };
29
+ readonly onPhase?: (phase: string) => void | Promise<void>;
30
+ }): Promise<ProjectionResult>;
31
+ //# sourceMappingURL=recovery-projection.d.ts.map
@@ -0,0 +1,96 @@
1
+ import { readCommitReceipts, writeCommitReceipt } from "./commit-receipt.js";
2
+ import { mergeReportPath, readMergeReport, writeMergeReport } from "./report.js";
3
+ import { pairLedger, readPairLedgerV4 } from "./domain/pair-ledger.js";
4
+ import { readAllStages, readStageTree, writeStageIndex } from "./domain/stage-aggregate.js";
5
+ import { dropStage } from "./domain/stage-aggregate.js";
6
+ import { recordPair } from "./domain/pair-index-store.js";
7
+ import { readRunMeta, writeRunMeta } from "./domain/run-coordinator.js";
8
+ import { evidenceSha256 } from "./domain/evidence-hash.js";
9
+ export async function projectRecoveredUnit(options) {
10
+ const evidenceSha = evidenceSha256(options.evidence);
11
+ const ledger = await readPairLedgerV4(options.io, options.home).catch(() => null);
12
+ const row = ledger?.rows.find(item => item.runId === options.runId && (item.state === 'fuse' || item.state === 'approved'));
13
+ const rowTarget = row !== undefined && 'target' in row && typeof row.target === 'string' ? row.target : undefined;
14
+ const rowDecisionTarget = row !== undefined && 'decision' in row ? row.decision.target : undefined;
15
+ const target = rowTarget ?? rowDecisionTarget ?? options.evidence.sources[0]?.absorbedInto ?? options.evidence.target.after;
16
+ const pairKey = row?.pairKey ?? target + '\u0000' + (options.evidence.sources[0]?.name ?? '');
17
+ const expectedPairKey = pairKey;
18
+ const stageEntry = (await readAllStages(options.io, options.home)).find(item => item.rowId === row?.id || item.pairKey === expectedPairKey);
19
+ const stageRowId = row?.id ?? stageEntry?.rowId;
20
+ if (row?.state === 'approved' && stageEntry?.state === 'reserved') {
21
+ const tree = await readStageTree(options.io, options.home, stageEntry.ref);
22
+ if (tree.kind !== 'present') {
23
+ await writeStageIndex(options.io, options.home, { ...stageEntry, state: 'lost' });
24
+ return { ok: false, action: 'stage-lost', message: 'approved+reserved has no stage tree' };
25
+ }
26
+ }
27
+ const receipts = await readCommitReceipts(options.io, options.home);
28
+ const receipt = receipts.find(item => item.unitId === options.unitId);
29
+ const evidencePlan = options.evidence.validationPlan;
30
+ const validationPlan = receipt?.validationPlan ?? evidencePlan;
31
+ if (receipt === undefined) {
32
+ await options.onPhase?.('merge-receipt');
33
+ await writeCommitReceipt(options.io, options.home, {
34
+ runId: options.runId, unitId: options.unitId, evidenceSha256: evidenceSha,
35
+ target, create: options.evidence.target.created,
36
+ sources: options.evidence.sources.map(source => source.name),
37
+ archived: options.evidence.sources.filter(source => source.archived).map(source => source.name),
38
+ pairKey, at: options.now(),
39
+ ...(validationPlan === undefined ? {} : { validationPlan }),
40
+ });
41
+ }
42
+ const at = options.now();
43
+ let reportPath;
44
+ const existing = await readMergeReport(options.io, options.home, options.runId);
45
+ if (existing.kind === 'present') {
46
+ reportPath = mergeReportPath(options.home, options.runId);
47
+ if (validationPlan !== undefined && existing.value.pairs.every(pair => pair.validationPlan === undefined)) {
48
+ await writeMergeReport(options.io, options.home, { ...existing.value, pairs: existing.value.pairs.map(pair => ({ ...pair, validationPlan })) });
49
+ }
50
+ }
51
+ else {
52
+ const report = {
53
+ schemaVersion: 2, runId: options.runId, kind: 'merge', command: 'approve', startedAt: at, endedAt: at,
54
+ stopReason: 'completed', approval: { platformWrite: false, mergeApproval: true },
55
+ scope: { candidatePairs: 1, queueBefore: { verify: 0, pending: 0, fuse: 0, skipped: 0 } },
56
+ pairs: [{ id: row?.id ?? options.unitId, pairKey, a: row?.a ?? target, b: row?.b ?? options.evidence.sources[0]?.name ?? '', queue: 'fuse', outcome: 'merged', archived: options.evidence.sources.filter(source => source.archived).map(source => source.name), unitId: options.unitId, ...(validationPlan === undefined ? {} : { validationPlan }) }],
57
+ totals: { merged: 1, kept: 0, pending: 0, failed: 0 }, notes: ['recovered from unit journal'],
58
+ };
59
+ await options.onPhase?.('report');
60
+ try {
61
+ reportPath = await writeMergeReport(options.io, options.home, report);
62
+ }
63
+ catch (error) {
64
+ return { ok: false, action: 'report-write-failed', message: error instanceof Error ? error.message : String(error) };
65
+ }
66
+ }
67
+ if (row !== undefined) {
68
+ await options.onPhase?.('row-remove');
69
+ const removed = await pairLedger(options.io, options.home).transact(tx => {
70
+ const current = tx.rows().find(item => item.id === row.id);
71
+ return current === undefined ? true : tx.remove(row.id, 'committed', { unitId: options.unitId, evidenceSha256: evidenceSha }, current.version).ok;
72
+ });
73
+ if (!removed)
74
+ return { ok: false, action: 'row-remove-failed' };
75
+ await options.onPhase?.('stage-drop');
76
+ await dropStage(options.io, options.home, row.id);
77
+ }
78
+ else if (stageRowId !== undefined) {
79
+ await options.onPhase?.('stage-drop');
80
+ await dropStage(options.io, options.home, stageRowId);
81
+ }
82
+ await options.onPhase?.('pair-index');
83
+ await recordPair(options.io, options.home, { pairKey, runId: options.runId, result: 'merged', target });
84
+ await options.onPhase?.('run-meta');
85
+ const meta = await readRunMeta(options.io, options.home, options.runId);
86
+ await writeRunMeta(options.io, options.home, { version: 1, runId: options.runId, phase: 'settled', reportRef: reportPath, pairCount: 1, ...(meta?.parentRunId === undefined ? {} : { parentRunId: meta.parentRunId }), createdAt: meta?.createdAt ?? new Date(at).toISOString(), updatedAt: new Date(at).toISOString() });
87
+ await options.onPhase?.('settle');
88
+ if (options.runs !== undefined) {
89
+ const outcome = { state: 'succeeded', resultRef: reportPath };
90
+ const upgraded = options.runs.settleRecovered === undefined ? false : await options.runs.settleRecovered(options.runId, outcome);
91
+ if (!upgraded)
92
+ await options.runs.settle(options.runId, outcome);
93
+ }
94
+ return { ok: true, action: 'projected', reportPath };
95
+ }
96
+ //# sourceMappingURL=recovery-projection.js.map
@@ -2,7 +2,7 @@
2
2
  * resume command: verify rows re-enter round1; fuse rows re-enter round2 with the persisted
3
3
  * decision. pending/skipped rows are not resumable.
4
4
  */
5
- import type { EvolutionIoLike } from '@lmzhen/dsh-evolution-core';
5
+ import { type EvolutionIoLike } from '@lmzhen/dsh-evolution-core';
6
6
  import { type MergeRegistryPort, type WithMergeRunResult } from './run-shell.ts';
7
7
  import type { ScanPorts, ScanRound2Outcome } from './scan-run.ts';
8
8
  import type { MergeDecision, MergeRow } from './contracts.ts';
@@ -17,11 +17,15 @@ export interface RunResumeCommandOptions {
17
17
  readonly round2WithRun?: (row: MergeRow, decision: MergeDecision, signal: AbortSignal, runId: string) => Promise<ScanRound2Outcome>;
18
18
  readonly rowId: string;
19
19
  readonly platformWrite: boolean;
20
+ readonly approval: boolean;
21
+ readonly parentRunId?: string;
20
22
  readonly detached?: boolean;
21
23
  readonly pairTimeoutMinutes?: number;
22
24
  readonly timeoutMinutes?: number;
23
25
  readonly now: () => number;
24
26
  readonly isOwnerActive?: (runId: string) => boolean | Promise<boolean>;
27
+ /** The invocation carried no agent at all (E-305), as opposed to an agent with no parent run. */
28
+ readonly agentless?: boolean;
25
29
  }
26
30
  export declare function runResumeCommand(options: RunResumeCommandOptions): Promise<WithMergeRunResult>;
27
31
  //# sourceMappingURL=resume-command.d.ts.map
@@ -1,28 +1,38 @@
1
+ /**
2
+ * resume command: verify rows re-enter round1; fuse rows re-enter round2 with the persisted
3
+ * decision. pending/skipped rows are not resumable.
4
+ */
5
+ import { errorText } from '@lmzhen/dsh-evolution-core';
1
6
  import { loadMergeState } from "./state.js";
2
7
  import { runScanCommand } from "./scan-command.js";
3
8
  import { withMergeRun } from "./run-shell.js";
4
9
  import { recordRound2Outcome } from "./round2-outcome.js";
10
+ import { pairLedger } from "./domain/pair-ledger.js";
11
+ import { presentStage } from "./domain/stage-aggregate.js";
12
+ import { v4ToLegacy } from "./domain/row-v4.js";
13
+ import { failure } from "./domain/errors.js";
5
14
  export async function runResumeCommand(options) {
6
15
  const loaded = await loadMergeState(options.io, options.home);
7
- const row = loaded.state.rows.find(item => item.id === options.rowId);
16
+ let row = loaded.state.rows.find(item => item.id === options.rowId);
8
17
  if (row === undefined)
9
- return { ok: false, message: 'merge row not found: ' + options.rowId };
10
- if (row.state === 'verify') {
18
+ return failure('resume', 'row-not-found', '找不到可恢复行 ' + options.rowId, '运行 /evolution merge candidates 查看可恢复行');
19
+ const selected = row;
20
+ if (selected.state === 'verify') {
11
21
  if (row.runId !== undefined && (await options.isOwnerActive?.(row.runId)) === true) {
12
- return { ok: false, message: 'merge run still owns this verify row: ' + row.runId };
22
+ return failure('resume', 'run-owns-row', '该 verify 行仍由运行中的 run 持有:' + row.runId, '等待该 run 结束,或运行 /evolution merge cancel ' + row.runId, row.runId);
13
23
  }
14
24
  return await runScanCommand({
15
25
  io: options.io, home: options.home, registry: options.registry,
16
- approval: true, platformWrite: options.platformWrite, now: options.now,
26
+ approval: options.approval, platformWrite: options.platformWrite, now: options.now,
17
27
  ...(options.detached === true ? { detached: true } : {}),
18
28
  ...(options.makePorts === undefined
19
- ? { ports: { ...options.ports, screening: () => Promise.resolve([{ a: row.a, b: row.b }]) } }
29
+ ? { ports: { ...options.ports, screening: () => Promise.resolve([{ a: selected.a, b: selected.b }]) } }
20
30
  : {
21
31
  makePorts: (runId) => {
22
32
  const makePorts = options.makePorts;
23
33
  if (makePorts === undefined)
24
34
  throw new Error('resume makePorts disappeared');
25
- return { ...makePorts(runId), screening: () => Promise.resolve([{ a: row.a, b: row.b }]) };
35
+ return { ...makePorts(runId), screening: () => Promise.resolve([{ a: selected.a, b: selected.b }]) };
26
36
  },
27
37
  }),
28
38
  ...(options.timeoutMinutes === undefined ? {} : { timeoutMinutes: options.timeoutMinutes }),
@@ -30,16 +40,38 @@ export async function runResumeCommand(options) {
30
40
  });
31
41
  }
32
42
  if (row.state !== 'fuse' && row.state !== 'approved')
33
- return { ok: false, message: 'row is not resumable from state ' + row.state };
43
+ return failure('resume', 'row-not-resumable', '该行状态是 ' + row.state + ',不可恢复', '只有 approved/fuse 行可恢复;运行 /evolution merge status 查看队列', row.runId);
34
44
  if (row.runId !== undefined && (await options.isOwnerActive?.(row.runId)) === true) {
35
- return { ok: false, message: 'merge run still owns this fuse row: ' + row.runId };
45
+ return failure('resume', 'run-owns-row', '该 fuse 行仍由运行中的 run 持有:' + row.runId, '等待该 run 结束,或运行 /evolution merge cancel ' + row.runId, row.runId);
36
46
  }
37
- const decision = row.decision;
47
+ if (options.agentless === true)
48
+ return failure('resume', 'e-305', errorText('e-305-this-invocation-carries-no', { a1: 'resume' }), '在 GUI 或 CLI 的会话里运行 /evolution merge resume --retry ' + row.id);
49
+ const parentRunId = options.parentRunId ?? row.runId;
50
+ if (parentRunId === undefined)
51
+ return failure('resume', 'merge-parent-required', '缺 parent run id(E-305 提示:该行可能由无会话调用创建,或父 run 已丢失)', '在 agent 会话中执行 /evolution merge resume --retry ' + row.id, row.runId);
52
+ const decision = selected.decision;
38
53
  if (decision === undefined)
39
- return { ok: false, message: 'fuse row has no decision' };
54
+ return failure('resume', 'row-invalid', '该 fuse 行没有持久化决策', '重新 pair 该技能对后再次恢复', row.runId);
55
+ if (selected.state === 'approved' && options.ports.stage !== undefined) {
56
+ const staged = await options.ports.stage(selected, decision);
57
+ if (!staged.ok)
58
+ return failure('resume', 'stage-failed', staged.message, '检查暂存目录后重试;不影响已记录的决策', selected.runId);
59
+ const fused = await pairLedger(options.io, options.home).transact(tx => {
60
+ const current = tx.rows().find(item => item.id === selected.id);
61
+ if (current === undefined)
62
+ return null;
63
+ const moved = tx.transition(selected.id, { type: 'staged' }, current.version);
64
+ return moved.ok && moved.row !== null ? v4ToLegacy(moved.row) : null;
65
+ });
66
+ if (fused === null)
67
+ return failure('resume', 'row-changed', '行在暂存完成前被并发修改:' + selected.id, '运行 /evolution merge status ' + selected.id + ' 后重试', selected.runId);
68
+ await presentStage(options.io, options.home, staged.entry);
69
+ row = fused;
70
+ }
40
71
  const result = await withMergeRun({
41
72
  io: options.io, home: options.home, registry: options.registry,
42
- command: 'resume', approval: { platformWrite: options.platformWrite, mergeApproval: true }, now: options.now,
73
+ command: 'resume', approval: { platformWrite: options.platformWrite, mergeApproval: options.approval }, pairKey: row.pairKey,
74
+ parentRunId, now: options.now,
43
75
  ...(options.detached === true ? { detached: true } : {}),
44
76
  async body(ctx) {
45
77
  await ctx.stage('author', { done: 0, total: 1 });
@@ -52,13 +84,14 @@ export async function runResumeCommand(options) {
52
84
  catch (error) {
53
85
  outcome = { kind: 'failed', failure: { stage: 'author', code: 'merge-failed', message: error instanceof Error ? error.message : String(error), retryable: true, retryFrom: 'agent2' } };
54
86
  }
55
- const record = await recordRound2Outcome(options.io, options.home, options.now, row, outcome);
87
+ const record = await recordRound2Outcome(options.io, options.home, options.now, row, outcome, true);
56
88
  return {
57
89
  stopReason: record.merged ? 'completed' : 'failed',
58
90
  scope: { candidatePairs: 1, queueBefore: { verify: 0, pending: 0, fuse: 1, skipped: 0 } },
59
91
  pairs: [record.pair],
60
92
  totals: record.merged ? { merged: 1, kept: 0, pending: 0, failed: 0 } : { merged: 0, kept: 0, pending: 0, failed: 1 },
61
93
  notes: [],
94
+ ...(record.finalize === undefined ? {} : { finalize: record.finalize }),
62
95
  };
63
96
  },
64
97
  });
@@ -1,6 +1,23 @@
1
1
  import type { SkillCard } from './skill-card.ts';
2
+ import type { RevisionRef } from './contracts.ts';
2
3
  /** Stable source revision for a pair: names sorted, body+support hashed in that order. */
3
4
  export declare function sourceRevisionOf(cards: readonly SkillCard[], a: string, b: string): string | undefined;
4
5
  /** Revision of the merge target; undefined when the target is not a live card. */
5
6
  export declare function targetRevisionOf(cards: readonly SkillCard[], target: string): string | undefined;
7
+ export declare function targetRevisionRefOf(cards: readonly SkillCard[], target: string): RevisionRef;
8
+ /** Per-skill anchors for a set of names; absent names stay `kind: 'absent'` so a
9
+ * later commit can tell "was not there" from "was not checked". */
10
+ export declare function sourceRevisionRefsOf(cards: readonly SkillCard[], names: readonly string[]): Readonly<Record<string, RevisionRef>>;
11
+ export declare function sourceRevisionRefOf(cards: readonly SkillCard[], a: string, b: string): RevisionRef;
12
+ /** Record the round-1 anchors a later commit re-checks (target/source/support). */
13
+ export declare function withDecisionRevisions<T extends {
14
+ readonly target: string;
15
+ }>(decision: T, cards: readonly SkillCard[], a: string, b: string): T & {
16
+ readonly targetRevisionRef: RevisionRef;
17
+ readonly sourceRevisionRef: RevisionRef;
18
+ readonly sourceRevisions: Readonly<Record<string, RevisionRef>>;
19
+ readonly supportDigest: string;
20
+ };
21
+ export declare function supportDigestOf(cards: readonly SkillCard[], names: readonly string[]): string;
22
+ export declare function revisionMismatch(expected: RevisionRef | undefined, current: RevisionRef): 'target-taken' | 'source-changed' | null;
6
23
  //# sourceMappingURL=revision.d.ts.map
@@ -1,7 +1,6 @@
1
- import { contentHash } from '@lmzhen/dsh-evolution-core';
1
+ import { contentHash, revisionFingerprint, revisionSupportDigest } from '@lmzhen/dsh-evolution-core';
2
2
  function fingerprint(card) {
3
- const files = [...(card.files ?? [])].sort((left, right) => left.path.localeCompare(right.path));
4
- return card.name + '\u0000' + card.body + '\u0000' + files.map(file => file.path + '\u0000' + file.content).join('\u0000');
3
+ return revisionFingerprint(card.name, card.body, card.files ?? []);
5
4
  }
6
5
  /** Stable source revision for a pair: names sorted, body+support hashed in that order. */
7
6
  export function sourceRevisionOf(cards, a, b) {
@@ -18,4 +17,41 @@ export function targetRevisionOf(cards, target) {
18
17
  const card = cards.find(item => item.name === target);
19
18
  return card === undefined ? undefined : contentHash(fingerprint(card));
20
19
  }
20
+ export function targetRevisionRefOf(cards, target) {
21
+ const revision = targetRevisionOf(cards, target);
22
+ return revision === undefined ? { kind: 'absent' } : { kind: 'present', sha256: revision };
23
+ }
24
+ /** Per-skill anchors for a set of names; absent names stay `kind: 'absent'` so a
25
+ * later commit can tell "was not there" from "was not checked". */
26
+ export function sourceRevisionRefsOf(cards, names) {
27
+ const byName = new Map(cards.map(card => [card.name, card]));
28
+ const out = {};
29
+ for (const name of [...new Set(names)].sort()) {
30
+ const card = byName.get(name);
31
+ out[name] = card === undefined ? { kind: 'absent' } : { kind: 'present', sha256: contentHash(fingerprint(card)) };
32
+ }
33
+ return out;
34
+ }
35
+ export function sourceRevisionRefOf(cards, a, b) {
36
+ const revision = sourceRevisionOf(cards, a, b);
37
+ return revision === undefined ? { kind: 'absent' } : { kind: 'present', sha256: revision };
38
+ }
39
+ /** Record the round-1 anchors a later commit re-checks (target/source/support). */
40
+ export function withDecisionRevisions(decision, cards, a, b) {
41
+ return { ...decision, targetRevisionRef: targetRevisionRefOf(cards, decision.target), sourceRevisionRef: sourceRevisionRefOf(cards, a, b), sourceRevisions: sourceRevisionRefsOf(cards, [a, b]), supportDigest: supportDigestOf(cards, [decision.target, a, b]) };
42
+ }
43
+ export function supportDigestOf(cards, names) {
44
+ const byName = new Map(cards.map(card => [card.name, card]));
45
+ const involved = [...new Set(names)].map(name => byName.get(name)).filter((card) => card !== undefined);
46
+ return revisionSupportDigest(involved.map(fingerprint));
47
+ }
48
+ export function revisionMismatch(expected, current) {
49
+ if (expected === undefined)
50
+ return null;
51
+ if (expected.kind !== current.kind)
52
+ return 'target-taken';
53
+ if (expected.kind === 'absent')
54
+ return null;
55
+ return current.kind === 'present' && expected.sha256 === current.sha256 ? null : 'target-taken';
56
+ }
21
57
  //# sourceMappingURL=revision.js.map
@@ -8,6 +8,7 @@ import type { ScanRound2Outcome } from './scan-run.ts';
8
8
  export interface Round2Record {
9
9
  readonly pair: MergeReportPair;
10
10
  readonly merged: boolean;
11
+ readonly finalize?: () => Promise<void>;
11
12
  }
12
- export declare function recordRound2Outcome(io: EvolutionIoLike, home: string, now: () => number, row: MergeRow, outcome: ScanRound2Outcome): Promise<Round2Record>;
13
+ export declare function recordRound2Outcome(io: EvolutionIoLike, home: string, now: () => number, row: MergeRow, outcome: ScanRound2Outcome, deferProjection?: boolean): Promise<Round2Record>;
13
14
  //# sourceMappingURL=round2-outcome.d.ts.map
@@ -1,14 +1,21 @@
1
- import { v3ToLegacy } from "./domain/row-v3-bridge.js";
2
- import { dropRowV3, transactRows } from "./state.js";
3
- import { recordPair } from "./domain/pair-index-store.js";
4
- export async function recordRound2Outcome(io, home, now, row, outcome) {
1
+ import { v4ToLegacy } from "./domain/row-v4.js";
2
+ import { pairLedger } from "./domain/pair-ledger.js";
3
+ import { dropStage } from "./domain/stage-aggregate.js";
4
+ import { casExpiredHook } from "./domain/failure-route.js";
5
+ import { recordPair } from "./domain/run-coordinator.js";
6
+ export async function recordRound2Outcome(io, home, now, row, outcome, deferProjection = false) {
5
7
  void now;
6
8
  if (outcome.kind === 'merged') {
7
- {
8
- const dropped = await dropRowV3(io, home, row.id, 'committed', outcome.unitId ?? 'unknown');
9
- if (!dropped)
10
- return { merged: false, pair: { id: row.id, pairKey: row.pairKey, a: row.a, b: row.b, queue: row.queue, outcome: 'failed', archived: [], failure: { stage: 'commit', code: 'row-changed', message: 'row changed before commit record', retryable: true, retryFrom: 'agent2' } } };
11
- const target = outcome.target?.actual ?? outcome.target?.requested ?? row.decision?.target;
9
+ const unitRef = { unitId: outcome.unitId ?? 'unknown', evidenceSha256: outcome.evidenceSha256 ?? 'unknown' };
10
+ const target = outcome.target?.actual ?? outcome.target?.requested ?? row.decision?.target;
11
+ const finalize = async () => {
12
+ const removed = await pairLedger(io, home).transact(tx => {
13
+ const current = tx.rows().find(item => item.id === row.id);
14
+ return current === undefined ? { ok: false, code: 'row-not-found', message: 'row not found' } : tx.remove(row.id, 'committed', unitRef, current.version);
15
+ });
16
+ if (!removed.ok)
17
+ throw new Error('row changed before commit record: ' + row.id);
18
+ await dropStage(io, home, row.id);
12
19
  await recordPair(io, home, {
13
20
  pairKey: row.pairKey,
14
21
  runId: row.runId ?? 'merge',
@@ -16,6 +23,11 @@ export async function recordRound2Outcome(io, home, now, row, outcome) {
16
23
  ...(target === undefined ? {} : { target }),
17
24
  ...(outcome.unitId === undefined ? {} : { reportRef: outcome.unitId }),
18
25
  });
26
+ };
27
+ if (deferProjection)
28
+ 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.unitId === undefined ? {} : { unitId: outcome.unitId }), ...(outcome.target === undefined ? {} : { target: outcome.target }) } };
29
+ {
30
+ await finalize();
19
31
  }
20
32
  return {
21
33
  merged: true,
@@ -25,16 +37,52 @@ export async function recordRound2Outcome(io, home, now, row, outcome) {
25
37
  outcome: 'merged', archived: outcome.archived ?? [],
26
38
  ...(outcome.unitId === undefined ? {} : { unitId: outcome.unitId }),
27
39
  ...(outcome.target === undefined ? {} : { target: outcome.target }),
40
+ ...(outcome.validationPlan === undefined ? {} : { validationPlan: outcome.validationPlan }),
28
41
  },
29
42
  };
30
43
  }
31
44
  const failure = outcome.failure ?? { stage: 'author', code: 'merge-failed', message: 'round2 failed', retryable: true, retryFrom: 'agent2' };
32
- const projected = await transactRows(io, home, tx => {
45
+ if (failure.code === 'merge-over-limit' || failure.code === 'limit' || failure.code === 'merge-validation') {
46
+ const reason = failure.code === 'merge-validation' ? 'validation-failed' : 'merge-over-limit';
47
+ const skipped = await pairLedger(io, home).transact(tx => {
48
+ const current = tx.rows().find(item => item.id === row.id);
49
+ return current === undefined ? false : tx.transition(row.id, { type: 'skipped', reason, origin: 'system', skipSource: 'system' }, current.version).ok;
50
+ });
51
+ if (skipped)
52
+ await dropStage(io, home, row.id);
53
+ 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 } };
54
+ }
55
+ const cas = casExpiredHook(failure.code);
56
+ if (cas !== null) {
57
+ await pairLedger(io, home).transact(tx => {
58
+ const current = tx.rows().find(item => item.id === row.id);
59
+ if (current === undefined)
60
+ return false;
61
+ const moved = tx.transition(row.id, { type: 'skipped', reason: cas.reason, origin: 'system', skipSource: 'system' }, current.version);
62
+ return moved.ok;
63
+ });
64
+ await dropStage(io, home, row.id);
65
+ 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: { ...failure, code: cas.reason, retryable: false, retryFrom: 'none' } } };
66
+ }
67
+ if (failure.code === 'commit-failed' || failure.code === 'validation-failed') {
68
+ await pairLedger(io, home).transact(tx => {
69
+ const current = tx.rows().find(item => item.id === row.id);
70
+ return current === undefined ? false : tx.remove(row.id, 'superseded', undefined, current.version).ok;
71
+ });
72
+ }
73
+ if (failure.code === 'source-gone') {
74
+ await pairLedger(io, home).transact(tx => {
75
+ const current = tx.rows().find(item => item.id === row.id);
76
+ return current === undefined ? false : tx.remove(row.id, 'superseded', undefined, current.version).ok;
77
+ });
78
+ return { merged: false, pair: { id: row.id, pairKey: row.pairKey, a: row.a, b: row.b, queue: row.queue, outcome: 'failed', archived: [], failure } };
79
+ }
80
+ const projected = await pairLedger(io, home).transact(tx => {
33
81
  const current = tx.rows().find(item => item.id === row.id);
34
82
  if (current === undefined)
35
83
  return null;
36
84
  const moved = tx.transition(row.id, { type: 'failed', failure }, current.version);
37
- return moved.ok && moved.row !== null ? v3ToLegacy(moved.row) : null;
85
+ return moved.ok && moved.row !== null ? v4ToLegacy(moved.row) : null;
38
86
  });
39
87
  if (projected === null)
40
88
  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' } } };
@@ -7,12 +7,12 @@
7
7
  */
8
8
  import type { EvolutionIoLike, RunHandle, RunKind, RunStage } from '@lmzhen/dsh-evolution-core';
9
9
  import type { MergeReport, MergeReportPair, MergeRunCommand } from './contracts.ts';
10
+ import type { MergeError } from './domain/errors.ts';
10
11
  export interface MergeRegistryPort {
11
12
  begin(kind: RunKind, seed?: {
12
13
  command?: MergeRunCommand;
13
14
  total?: number;
14
15
  parentRunId?: string;
15
- mergeStatus?: 'running' | 'succeeded' | 'failed' | 'cancelled' | 'awaiting-approval' | 'undone' | 'superseded' | 'interrupted' | 'orphan';
16
16
  }): Promise<Pick<RunHandle, 'id' | 'signal' | 'startedAt'>>;
17
17
  stage(id: string, stage: RunStage, progress?: {
18
18
  done: number;
@@ -22,7 +22,6 @@ export interface MergeRegistryPort {
22
22
  state: 'succeeded' | 'failed' | 'cancelled';
23
23
  resultRef?: string;
24
24
  failure?: string;
25
- mergeStatus?: 'running' | 'succeeded' | 'failed' | 'cancelled' | 'awaiting-approval' | 'undone' | 'superseded' | 'interrupted' | 'orphan';
26
25
  }): Promise<void>;
27
26
  }
28
27
  export interface MergeRunBodyContext {
@@ -41,6 +40,15 @@ export interface MergeRunReportDraft {
41
40
  readonly totals: MergeReport['totals'];
42
41
  readonly notes: readonly string[];
43
42
  readonly remaining?: MergeReport['remaining'];
43
+ readonly reportOnly?: MergeReport['reportOnly'];
44
+ readonly validationFailures?: MergeReport['validationFailures'];
45
+ readonly archived?: MergeReport['archived'];
46
+ readonly commit?: MergeReport['commit'];
47
+ readonly usage?: MergeReport['usage'];
48
+ readonly timing?: MergeReport['timing'];
49
+ readonly criteria?: MergeReport['criteria'];
50
+ /** Runs after the report is durable and before core settle (row/stage projection). */
51
+ readonly finalize?: () => Promise<void>;
44
52
  }
45
53
  export interface WithMergeRunOptions {
46
54
  readonly io: EvolutionIoLike;
@@ -50,7 +58,11 @@ export interface WithMergeRunOptions {
50
58
  readonly approval: MergeReport['approval'];
51
59
  readonly now: () => number;
52
60
  readonly total?: number;
61
+ readonly pairKey?: string;
62
+ readonly pairCount?: number;
63
+ readonly parentRunId?: string;
53
64
  readonly detached?: boolean;
65
+ readonly timeoutMinutes?: number;
54
66
  readonly body: (ctx: MergeRunBodyContext) => Promise<MergeRunReportDraft>;
55
67
  }
56
68
  export type WithMergeRunResult = {
@@ -67,6 +79,8 @@ export type WithMergeRunResult = {
67
79
  readonly runId?: string;
68
80
  readonly message: string;
69
81
  readonly reportPath?: string;
82
+ readonly error?: MergeError;
70
83
  };
84
+ export declare function retryMergeReport(io: EvolutionIoLike, home: string, report: MergeReport): Promise<string>;
71
85
  export declare function withMergeRun(options: WithMergeRunOptions): Promise<WithMergeRunResult>;
72
86
  //# sourceMappingURL=run-shell.d.ts.map