@lmzhen/dsh-evolution-merge 0.30.17 → 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 (187) hide show
  1. package/README.md +25 -7
  2. package/data/minhash_params.json +1063 -0
  3. package/lib/index.js +7308 -1800
  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/library.js +4 -1
  9. package/lib/types/adapters/model-runner.d.ts +0 -5
  10. package/lib/types/adapters/model-runner.js +8 -2
  11. package/lib/types/adapters/model.d.ts +31 -0
  12. package/lib/types/adapters/model.js +47 -0
  13. package/lib/types/adapters/select.js +12 -8
  14. package/lib/types/approve-command.d.ts +14 -1
  15. package/lib/types/approve-command.js +72 -44
  16. package/lib/types/authoring-flow.d.ts +19 -1
  17. package/lib/types/authoring-flow.js +49 -15
  18. package/lib/types/channel-port.d.ts +3 -1
  19. package/lib/types/channel-port.js +8 -3
  20. package/lib/types/command-defs.d.ts +5 -0
  21. package/lib/types/command-defs.js +33 -0
  22. package/lib/types/command-surface.d.ts +5 -0
  23. package/lib/types/command-surface.js +11 -0
  24. package/lib/types/commands.d.ts +3 -4
  25. package/lib/types/commands.js +33 -72
  26. package/lib/types/commit-assembly.d.ts +16 -1
  27. package/lib/types/commit-assembly.js +72 -11
  28. package/lib/types/commit-execute.d.ts +14 -2
  29. package/lib/types/commit-execute.js +76 -14
  30. package/lib/types/commit-plan.d.ts +7 -1
  31. package/lib/types/commit-plan.js +4 -0
  32. package/lib/types/commit-receipt.d.ts +3 -0
  33. package/lib/types/commit-recovery.d.ts +7 -2
  34. package/lib/types/commit-recovery.js +73 -96
  35. package/lib/types/contracts.d.ts +44 -1
  36. package/lib/types/contracts.js +1 -1
  37. package/lib/types/coordinator.d.ts +3 -2
  38. package/lib/types/coordinator.js +4 -3
  39. package/lib/types/decision.js +1 -1
  40. package/lib/types/doctor.d.ts +18 -1
  41. package/lib/types/doctor.js +164 -10
  42. package/lib/types/domain/authoring-manifest.d.ts +26 -0
  43. package/lib/types/domain/authoring-manifest.js +43 -0
  44. package/lib/types/domain/claim.d.ts +4 -1
  45. package/lib/types/domain/claim.js +2 -2
  46. package/lib/types/domain/commit-journal-store.js +5 -1
  47. package/lib/types/domain/commit-journal.js +1 -1
  48. package/lib/types/domain/consistency.d.ts +22 -0
  49. package/lib/types/domain/consistency.js +50 -0
  50. package/lib/types/domain/engine-criteria.d.ts +10 -0
  51. package/lib/types/domain/engine-criteria.js +11 -0
  52. package/lib/types/domain/engine-input.d.ts +10 -0
  53. package/lib/types/domain/engine-input.js +26 -0
  54. package/lib/types/domain/errors.d.ts +15 -1
  55. package/lib/types/domain/errors.js +23 -4
  56. package/lib/types/domain/evidence-hash.d.ts +3 -0
  57. package/lib/types/domain/evidence-hash.js +14 -0
  58. package/lib/types/domain/failure-route.d.ts +14 -0
  59. package/lib/types/domain/failure-route.js +12 -0
  60. package/lib/types/domain/ledger.js +10 -1
  61. package/lib/types/domain/live-facts.d.ts +31 -0
  62. package/lib/types/domain/live-facts.js +54 -0
  63. package/lib/types/domain/merge-limits.d.ts +18 -0
  64. package/lib/types/domain/merge-limits.js +22 -0
  65. package/lib/types/domain/merge-report.d.ts +41 -0
  66. package/lib/types/domain/merge-report.js +2 -0
  67. package/lib/types/domain/merge-snapshot.d.ts +99 -0
  68. package/lib/types/domain/merge-snapshot.js +82 -0
  69. package/lib/types/domain/migrate-v3.d.ts +20 -0
  70. package/lib/types/domain/migrate-v3.js +68 -0
  71. package/lib/types/domain/pair-ledger.d.ts +34 -0
  72. package/lib/types/domain/pair-ledger.js +96 -0
  73. package/lib/types/domain/pre-1b-backup.d.ts +43 -0
  74. package/lib/types/domain/pre-1b-backup.js +140 -0
  75. package/lib/types/domain/provider-key.d.ts +2 -0
  76. package/lib/types/domain/provider-key.js +11 -0
  77. package/lib/types/domain/read-probe.d.ts +5 -0
  78. package/lib/types/domain/read-probe.js +11 -0
  79. package/lib/types/domain/reconcile.d.ts +47 -0
  80. package/lib/types/domain/reconcile.js +178 -0
  81. package/lib/types/domain/resolve-pair.d.ts +2 -9
  82. package/lib/types/domain/resolve-pair.js +2 -15
  83. package/lib/types/domain/row-tx.d.ts +27 -0
  84. package/lib/types/domain/row-tx.js +31 -0
  85. package/lib/types/domain/row-v3-bridge.d.ts +6 -0
  86. package/lib/types/domain/row-v3-bridge.js +47 -0
  87. package/lib/types/domain/row-v3.d.ts +113 -0
  88. package/lib/types/domain/row-v3.js +107 -0
  89. package/lib/types/domain/row-v4.d.ts +148 -0
  90. package/lib/types/domain/row-v4.js +159 -0
  91. package/lib/types/domain/row.d.ts +1 -0
  92. package/lib/types/domain/run-coordinator.d.ts +51 -0
  93. package/lib/types/domain/run-coordinator.js +45 -0
  94. package/lib/types/domain/schema4.d.ts +53 -0
  95. package/lib/types/domain/schema4.js +129 -0
  96. package/lib/types/domain/selection-policy.d.ts +14 -0
  97. package/lib/types/domain/selection-policy.js +6 -0
  98. package/lib/types/domain/skipped-expiry.d.ts +5 -0
  99. package/lib/types/domain/skipped-expiry.js +16 -0
  100. package/lib/types/domain/stage-aggregate.d.ts +96 -0
  101. package/lib/types/domain/stage-aggregate.js +293 -0
  102. package/lib/types/domain/staging-index.d.ts +15 -0
  103. package/lib/types/domain/staging-index.js +41 -0
  104. package/lib/types/domain/state-bootstrap.d.ts +43 -0
  105. package/lib/types/domain/state-bootstrap.js +267 -0
  106. package/lib/types/domain/state-v3.d.ts +4 -0
  107. package/lib/types/domain/state-v3.js +4 -0
  108. package/lib/types/domain/undo-journal.d.ts +19 -0
  109. package/lib/types/domain/undo-journal.js +20 -0
  110. package/lib/types/domain/undo-receipt.d.ts +18 -0
  111. package/lib/types/domain/undo-receipt.js +22 -0
  112. package/lib/types/domain/validation-plan.d.ts +65 -0
  113. package/lib/types/domain/validation-plan.js +49 -0
  114. package/lib/types/facts.d.ts +26 -0
  115. package/lib/types/facts.js +65 -0
  116. package/lib/types/gate-filter.d.ts +31 -0
  117. package/lib/types/gate-filter.js +64 -0
  118. package/lib/types/index.d.ts +7 -2
  119. package/lib/types/index.js +185 -82
  120. package/lib/types/ladder.js +1 -1
  121. package/lib/types/merge-command.d.ts +3 -1
  122. package/lib/types/merge-command.js +10 -6
  123. package/lib/types/merge-routes.d.ts +12 -7
  124. package/lib/types/merge-routes.js +87 -23
  125. package/lib/types/merge-tool.d.ts +1 -1
  126. package/lib/types/merge-tool.js +2 -2
  127. package/lib/types/metrics.d.ts +16 -0
  128. package/lib/types/metrics.js +27 -0
  129. package/lib/types/new-child-authoring.d.ts +5 -1
  130. package/lib/types/new-child-authoring.js +71 -13
  131. package/lib/types/params.d.ts +2 -4
  132. package/lib/types/params.js +13 -0
  133. package/lib/types/pending-actions.d.ts +2 -2
  134. package/lib/types/pending-actions.js +17 -17
  135. package/lib/types/prompts.d.ts +2 -4
  136. package/lib/types/prompts.js +21 -8
  137. package/lib/types/propose.d.ts +26 -0
  138. package/lib/types/propose.js +69 -0
  139. package/lib/types/read-commands.d.ts +4 -4
  140. package/lib/types/read-commands.js +19 -26
  141. package/lib/types/recovery-coordinator.d.ts +34 -0
  142. package/lib/types/recovery-coordinator.js +84 -0
  143. package/lib/types/recovery-projection.d.ts +31 -0
  144. package/lib/types/recovery-projection.js +96 -0
  145. package/lib/types/resume-command.d.ts +7 -1
  146. package/lib/types/resume-command.js +50 -14
  147. package/lib/types/revision.d.ts +17 -0
  148. package/lib/types/revision.js +39 -3
  149. package/lib/types/round2-outcome.d.ts +2 -1
  150. package/lib/types/round2-outcome.js +68 -12
  151. package/lib/types/run-shell.d.ts +16 -2
  152. package/lib/types/run-shell.js +49 -5
  153. package/lib/types/runtime.d.ts +4 -6
  154. package/lib/types/runtime.js +341 -86
  155. package/lib/types/scan-command.d.ts +1 -0
  156. package/lib/types/scan-command.js +2 -0
  157. package/lib/types/scan-ports.d.ts +21 -1
  158. package/lib/types/scan-ports.js +51 -23
  159. package/lib/types/scan-run.d.ts +17 -0
  160. package/lib/types/scan-run.js +221 -63
  161. package/lib/types/screening-port.d.ts +47 -0
  162. package/lib/types/screening-port.js +93 -0
  163. package/lib/types/screening.d.ts +30 -0
  164. package/lib/types/screening.js +75 -0
  165. package/lib/types/stage-tool-registry.d.ts +7 -0
  166. package/lib/types/stage-tool-registry.js +12 -1
  167. package/lib/types/state.d.ts +15 -1
  168. package/lib/types/state.js +39 -17
  169. package/lib/types/status.d.ts +8 -0
  170. package/lib/types/status.js +64 -0
  171. package/lib/types/surface/body.d.ts +4 -0
  172. package/lib/types/surface/body.js +16 -0
  173. package/lib/types/surface/merge-view.d.ts +14 -0
  174. package/lib/types/surface/merge-view.js +126 -0
  175. package/lib/types/target-claim.d.ts +1 -0
  176. package/lib/types/target-claim.js +63 -18
  177. package/lib/types/undo-operation.d.ts +24 -0
  178. package/lib/types/undo-operation.js +135 -0
  179. package/lib/types/unit-claim.d.ts +5 -0
  180. package/lib/types/unit-claim.js +15 -0
  181. package/lib/types/wire-projection.d.ts +36 -0
  182. package/lib/types/wire-projection.js +69 -0
  183. package/lib/types/wire.d.ts +273 -0
  184. package/lib/types/wire.js +3 -0
  185. package/package.json +10 -4
  186. package/lib/types/routes.d.ts +0 -70
  187. package/lib/types/routes.js +0 -78
@@ -0,0 +1,69 @@
1
+ const SYSTEM = 'You propose skill pairs that may be merged. Return only a JSON object: {"pairs":[{"a":"skill-a","b":"skill-b","reason":"one sentence"}]}. Use only supplied skill names. Do not decide whether a merge is good.';
2
+ function parsePairs(raw, known, cap) {
3
+ let value = raw;
4
+ if (typeof raw === 'string') {
5
+ try {
6
+ value = JSON.parse(raw);
7
+ }
8
+ catch {
9
+ return [];
10
+ }
11
+ }
12
+ if (typeof value !== 'object' || value === null)
13
+ return [];
14
+ const pairs = value.pairs;
15
+ if (!Array.isArray(pairs))
16
+ return [];
17
+ const out = [];
18
+ const seen = new Set();
19
+ for (const item of pairs) {
20
+ if (out.length >= cap)
21
+ break;
22
+ const pair = item;
23
+ if (typeof pair.a !== 'string' || typeof pair.b !== 'string')
24
+ continue;
25
+ const a = pair.a.trim();
26
+ const b = pair.b.trim();
27
+ if (a === b || !known.has(a) || !known.has(b))
28
+ continue;
29
+ const [first, second] = a < b ? [a, b] : [b, a];
30
+ const key = first + '\u0000' + second;
31
+ if (seen.has(key))
32
+ continue;
33
+ seen.add(key);
34
+ out.push({ a: first, b: second, source: 'proposed', ...(typeof pair.reason === 'string' && pair.reason.trim() !== '' ? { reason: pair.reason.trim() } : {}) });
35
+ }
36
+ return out;
37
+ }
38
+ export async function proposePairs(cards, port, options) {
39
+ const shardSize = Math.max(2, options.shardSize ?? 60);
40
+ const cap = Math.max(0, options.maxPairs ?? 200);
41
+ const known = new Set(cards.map(card => card.name));
42
+ const candidates = [];
43
+ const degraded = [];
44
+ let shards = 0;
45
+ for (let start = 0; start < cards.length && candidates.length < cap; start += shardSize) {
46
+ const shard = cards.slice(start, start + shardSize);
47
+ if (shard.length < 2)
48
+ continue;
49
+ shards += 1;
50
+ try {
51
+ const raw = await port.callJson({
52
+ system: SYSTEM,
53
+ user: JSON.stringify(shard.map(card => ({
54
+ name: card.name,
55
+ description: card.description.slice(0, 400),
56
+ body: card.body.slice(0, 600),
57
+ }))),
58
+ maxTokens: 1200,
59
+ signal: options.signal,
60
+ });
61
+ candidates.push(...parsePairs(raw, known, cap - candidates.length));
62
+ }
63
+ catch (error) {
64
+ degraded.push('proposal shard failed: ' + (error instanceof Error ? error.message : String(error)));
65
+ }
66
+ }
67
+ return { candidates, shards, degraded };
68
+ }
69
+ //# sourceMappingURL=propose.js.map
@@ -1,11 +1,11 @@
1
1
  /**
2
- * Read-only command handlers: status/pending/skipped are served from merge-state.
3
- * Mutating verbs return null so the host shell can route them to the run engine.
2
+ * Read-only command handlers: all served from one MergeSnapshot; never writes.
4
3
  */
5
- import type { EvolutionIoLike } from '@lmzhen/dsh-evolution-core';
4
+ import type { EvolutionIoLike, RunRecord } from '@lmzhen/dsh-evolution-core';
6
5
  import type { MergeCliCommand } from './commands.ts';
6
+ import type { LiveRowFacts } from './domain/live-facts.ts';
7
7
  export declare function handleReadOnlyMergeCommand(command: MergeCliCommand, io: EvolutionIoLike, home: string, cards?: readonly {
8
8
  readonly name: string;
9
- }[]): Promise<string | null>;
9
+ }[], runs?: readonly RunRecord[], approval?: boolean, live?: () => Promise<LiveRowFacts | undefined>): Promise<string | null>;
10
10
  export declare function mergeCommandHelp(): string;
11
11
  //# sourceMappingURL=read-commands.d.ts.map
@@ -1,38 +1,31 @@
1
- import { loadMergeState } from "./state.js";
2
- import { formatQueue, formatStatus } from "./status.js";
1
+ import { readMergeSnapshot } from "./domain/merge-snapshot.js";
2
+ import { buildFacts, toRowFact } from "./surface/merge-view.js";
3
+ import { formatFacts, formatSnapshotQueue, formatSnapshotStatus } from "./status.js";
3
4
  import { mergeUsage } from "./commands.js";
4
- export async function handleReadOnlyMergeCommand(command, io, home, cards) {
5
- const withNote = (text, note) => note === undefined ? text : 'note: ' + note + '\n' + text;
6
- const load = async () => {
7
- const result = await loadMergeState(io, home);
8
- if (result.note === 'merge-state-forward-only')
9
- throw new Error('merge-state-forward-only');
10
- return result;
11
- };
5
+ export async function handleReadOnlyMergeCommand(command, io, home, cards, runs, approval = false, live) {
6
+ if (!['status', 'pending', 'candidates', 'skipped'].includes(command.verb))
7
+ return null;
8
+ const snapshot = await readMergeSnapshot({ io, home, runs: runs ?? [], approval, ...(live === undefined ? {} : { live }) });
9
+ if (snapshot.blocked !== undefined)
10
+ throw new Error(snapshot.blocked.code + ': ' + snapshot.blocked.message);
11
+ const rows = snapshot.rows.map(toRowFact);
12
12
  if (command.verb === 'status') {
13
- const loaded = await load();
14
13
  if (command.id !== undefined) {
15
- const row = loaded.state.rows.find(item => item.id === command.id);
16
- return withNote(row === undefined ? 'merge row not found: ' + command.id : formatStatus([row]), loaded.note);
14
+ const row = rows.find(item => item.id === command.id);
15
+ return row === undefined ? 'merge row not found: ' + command.id : formatSnapshotStatus([row]);
17
16
  }
18
- return withNote(formatStatus(loaded.state.rows), loaded.note);
17
+ return formatFacts(buildFacts(snapshot));
19
18
  }
20
19
  if (command.verb === 'pending') {
21
- const loaded = await load();
22
- const rows = command.expired === true ? loaded.state.rows.filter(row => row.expired !== undefined) : loaded.state.rows;
23
- const live = cards === undefined ? rows : rows.filter(row => cards.some(card => card.name === row.a) && cards.some(card => card.name === row.b));
24
- return withNote(command.expired === true ? formatStatus(live) : formatQueue(live, 'pending'), loaded.note);
20
+ const live = command.expired === true ? rows.filter(row => row.expired !== undefined) : rows.filter(row => row.queue === 'pending' && row.expired === undefined);
21
+ const filtered = cards === undefined ? live : live.filter(row => cards.some(card => card.name === row.a) && cards.some(card => card.name === row.b));
22
+ return command.expired === true ? formatSnapshotStatus(filtered) : formatSnapshotQueue(filtered, 'pending');
25
23
  }
26
24
  if (command.verb === 'candidates') {
27
- const loaded = await load();
28
- const rows = command.retry === true ? loaded.state.rows.filter(row => row.state === 'approved') : loaded.state.rows;
29
- return withNote(formatQueue(rows, command.retry === true ? 'fuse' : 'verify'), loaded.note);
25
+ const queue = command.retry === true ? 'fuse' : 'verify';
26
+ return formatSnapshotQueue(rows.filter(row => row.queue === queue && (command.id === undefined || row.id === command.id)), queue);
30
27
  }
31
- if (command.verb === 'skipped') {
32
- const loaded = await load();
33
- return withNote(formatQueue(loaded.state.rows, 'skipped'), loaded.note);
34
- }
35
- return null;
28
+ return formatSnapshotQueue(rows.filter(row => row.queue === 'skipped'), 'skipped');
36
29
  }
37
30
  export function mergeCommandHelp() {
38
31
  return mergeUsage();
@@ -0,0 +1,34 @@
1
+ import type { EvolutionIoLike, UnitTransaction } from '@lmzhen/dsh-evolution-core';
2
+ export type RecoveryBranch = 'A' | 'B' | 'C' | 'D';
3
+ export interface RecoveryReport {
4
+ readonly unitId: string;
5
+ readonly runId: string;
6
+ readonly phase: string;
7
+ readonly branch: RecoveryBranch;
8
+ readonly action: 'rollback' | 'rollForward' | 'finalize' | 'blocked' | 'none';
9
+ readonly ok: boolean;
10
+ readonly code?: string | undefined;
11
+ readonly message?: string | undefined;
12
+ readonly evidenceSha256?: string;
13
+ }
14
+ export declare function recoverCommitUnits(options: {
15
+ readonly io: EvolutionIoLike;
16
+ readonly home: string;
17
+ readonly unit: UnitTransaction;
18
+ readonly now: () => number;
19
+ readonly isActive?: (runId: string) => boolean;
20
+ readonly runs?: {
21
+ settle(id: string, outcome: {
22
+ state: 'succeeded' | 'failed' | 'cancelled';
23
+ resultRef?: string;
24
+ failure?: string;
25
+ }): Promise<void>;
26
+ settleRecovered?(id: string, outcome: {
27
+ state: 'succeeded' | 'failed' | 'cancelled';
28
+ resultRef?: string;
29
+ failure?: string;
30
+ }): Promise<boolean>;
31
+ };
32
+ readonly onProjectionPhase?: (phase: string) => void | Promise<void>;
33
+ }): Promise<readonly RecoveryReport[]>;
34
+ //# sourceMappingURL=recovery-coordinator.d.ts.map
@@ -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,9 +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;
22
+ readonly detached?: boolean;
23
+ readonly pairTimeoutMinutes?: number;
20
24
  readonly timeoutMinutes?: number;
21
25
  readonly now: () => number;
22
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;
23
29
  }
24
30
  export declare function runResumeCommand(options: RunResumeCommandOptions): Promise<WithMergeRunResult>;
25
31
  //# sourceMappingURL=resume-command.d.ts.map
@@ -1,27 +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,
27
+ ...(options.detached === true ? { detached: true } : {}),
17
28
  ...(options.makePorts === undefined
18
- ? { 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 }]) } }
19
30
  : {
20
31
  makePorts: (runId) => {
21
32
  const makePorts = options.makePorts;
22
33
  if (makePorts === undefined)
23
34
  throw new Error('resume makePorts disappeared');
24
- 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 }]) };
25
36
  },
26
37
  }),
27
38
  ...(options.timeoutMinutes === undefined ? {} : { timeoutMinutes: options.timeoutMinutes }),
@@ -29,16 +40,39 @@ export async function runResumeCommand(options) {
29
40
  });
30
41
  }
31
42
  if (row.state !== 'fuse' && row.state !== 'approved')
32
- 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);
33
44
  if (row.runId !== undefined && (await options.isOwnerActive?.(row.runId)) === true) {
34
- 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);
35
46
  }
36
- 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;
37
53
  if (decision === undefined)
38
- return { ok: false, message: 'fuse row has no decision' };
39
- return await withMergeRun({
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
+ }
71
+ const result = await withMergeRun({
40
72
  io: options.io, home: options.home, registry: options.registry,
41
- 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,
75
+ ...(options.detached === true ? { detached: true } : {}),
42
76
  async body(ctx) {
43
77
  await ctx.stage('author', { done: 0, total: 1 });
44
78
  let outcome;
@@ -50,15 +84,17 @@ export async function runResumeCommand(options) {
50
84
  catch (error) {
51
85
  outcome = { kind: 'failed', failure: { stage: 'author', code: 'merge-failed', message: error instanceof Error ? error.message : String(error), retryable: true, retryFrom: 'agent2' } };
52
86
  }
53
- 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);
54
88
  return {
55
89
  stopReason: record.merged ? 'completed' : 'failed',
56
90
  scope: { candidatePairs: 1, queueBefore: { verify: 0, pending: 0, fuse: 1, skipped: 0 } },
57
91
  pairs: [record.pair],
58
92
  totals: record.merged ? { merged: 1, kept: 0, pending: 0, failed: 0 } : { merged: 0, kept: 0, pending: 0, failed: 1 },
59
93
  notes: [],
94
+ ...(record.finalize === undefined ? {} : { finalize: record.finalize }),
60
95
  };
61
96
  },
62
97
  });
98
+ return result;
63
99
  }
64
100
  //# sourceMappingURL=resume-command.js.map
@@ -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