@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
@@ -1,120 +1,97 @@
1
- /** Startup replay for non-terminal commit journals. */
1
+ /** Startup replay for non-terminal commit journals: report only, never write skills. */
2
2
  import { join } from 'node:path';
3
- import { recoveryFor } from "./domain/commit-journal.js";
4
3
  import { writeJournal } from "./domain/commit-journal-store.js";
5
- import { readCommitSnapshot, skillBodyPath, skillFilePath } from "./commit-snapshot.js";
4
+ import { snapshotPath } from "./commit-snapshot.js";
6
5
  import { readCommitReceipt } from "./commit-receipt.js";
7
- export async function recoverCommitJournals(io, home, now, skillRoot, restoreSource, isActive) {
6
+ import { readUnitEvidence } from '@lmzhen/dsh-evolution-core';
7
+ export async function recoverCommitJournals(io, home, now, isActive) {
8
8
  const dir = join(home, 'merge', 'journals');
9
- if (!(await io.exists(dir)))
10
- return [];
11
- const files = (await io.list(dir)).filter(name => name.endsWith('.json'));
12
9
  const reports = [];
13
- for (const file of files) {
14
- let journal;
15
- try {
16
- journal = JSON.parse(await io.readText(join(dir, file)) ?? '');
17
- }
18
- catch {
19
- continue;
20
- }
21
- if (isActive?.(journal.runId) === true) {
22
- reports.push({ runId: journal.runId, phase: journal.phase, action: 'skipped-active-run', ok: true });
23
- continue;
24
- }
25
- const recovery = recoveryFor(journal);
26
- if (recovery === 'none')
27
- continue;
28
- if (recovery === 'refuse') {
29
- reports.push({ runId: journal.runId, phase: journal.phase, action: 'unknown-phase-refused', ok: false });
30
- continue;
31
- }
32
- let receipt = null;
10
+ let files = [];
11
+ if (await io.exists(dir)) {
33
12
  try {
34
- receipt = await readCommitReceipt(io, home, journal.runId, journal.target);
13
+ files = (await io.list(dir)).filter(name => name.endsWith('.json'));
35
14
  }
36
15
  catch {
37
- reports.push({ runId: journal.runId, phase: journal.phase, action: 'receipt-read-failed', ok: false });
38
- continue;
16
+ reports.push({ runId: '(list)', phase: 'unknown', action: 'journal-list-failed', ok: false, journalPath: dir });
39
17
  }
40
- if (receipt !== null) {
41
- await writeJournal(io, home, { ...journal, phase: 'committed', unitId: receipt.unitId, updatedAt: new Date(now).toISOString() });
42
- reports.push({ runId: journal.runId, phase: journal.phase, action: 'receipt-committed', ok: true });
43
- continue;
44
- }
45
- let snapshot;
18
+ }
19
+ for (const file of files) {
20
+ const path = join(dir, file);
46
21
  try {
47
- snapshot = await readCommitSnapshot(io, home, journal.runId);
22
+ let journal;
23
+ try {
24
+ journal = JSON.parse(await io.readText(path) ?? '');
25
+ }
26
+ catch {
27
+ reports.push({ runId: file.replace(/\.json$/, ''), phase: 'unknown', action: 'journal-unreadable', ok: false, journalPath: path });
28
+ continue;
29
+ }
30
+ if (isActive?.(journal.runId) === true) {
31
+ reports.push({ runId: journal.runId, phase: journal.phase, action: 'skipped-active-run', ok: true, target: journal.target, sources: journal.sources, journalPath: path });
32
+ continue;
33
+ }
34
+ let receipt = null;
35
+ try {
36
+ receipt = await readCommitReceipt(io, home, journal.runId, journal.target);
37
+ }
38
+ catch {
39
+ reports.push({ runId: journal.runId, phase: journal.phase, action: 'receipt-read-failed', ok: false, target: journal.target, sources: journal.sources, journalPath: path });
40
+ continue;
41
+ }
42
+ if (receipt !== null) {
43
+ await writeJournal(io, home, { ...journal, phase: 'committed', unitId: receipt.unitId, updatedAt: new Date(now).toISOString() });
44
+ reports.push({ runId: journal.runId, phase: journal.phase, action: 'receipt-committed', ok: true, target: journal.target, sources: journal.sources, journalPath: path, unitId: receipt.unitId });
45
+ continue;
46
+ }
47
+ if (journal.phase === 'committed') {
48
+ reports.push({ runId: journal.runId, phase: journal.phase, action: 'receipt-missing', ok: false, target: journal.target, sources: journal.sources, journalPath: path });
49
+ continue;
50
+ }
51
+ if (journal.phase === 'failed' || journal.phase === 'rolled-back')
52
+ continue;
53
+ reports.push({
54
+ runId: journal.runId, phase: journal.phase, action: 'manual-required', ok: false,
55
+ target: journal.target, sources: journal.sources, journalPath: path, snapshotPath: snapshotPath(home, journal.runId),
56
+ });
48
57
  }
49
58
  catch (error) {
50
- reports.push({ runId: journal.runId, phase: journal.phase, action: 'snapshot-read-failed: ' + (error instanceof Error ? error.message : String(error)), ok: false });
51
- continue;
52
- }
53
- let action = 'report-only';
54
- let ok = true;
55
- if (recovery === 'retry-commit') {
56
- action = 'no-host-write';
59
+ reports.push({ runId: file.replace(/\.json$/, ''), phase: 'unknown', action: 'recovery-error: ' + (error instanceof Error ? error.message : String(error)), ok: false, journalPath: path });
57
60
  }
58
- else if (snapshot === null) {
59
- action = 'snapshot-missing';
60
- ok = false;
61
+ }
62
+ const unitDir = join(home, 'merge', 'unit-journals');
63
+ if (await io.exists(unitDir)) {
64
+ let unitFiles = [];
65
+ try {
66
+ unitFiles = (await io.list(unitDir)).filter(name => name.endsWith('.json'));
61
67
  }
62
- else {
68
+ catch { /* no unit journals */ }
69
+ for (const file of unitFiles) {
70
+ const path = join(unitDir, file);
63
71
  try {
64
- if (recovery === 'rollback-sources') {
65
- let restoredArchived = restoreSource !== undefined;
66
- if (restoreSource !== undefined) {
67
- for (const source of snapshot.sourceBodies) {
68
- if (!(await restoreSource(source.name)))
69
- restoredArchived = false;
70
- }
71
- }
72
- if (!restoredArchived) {
73
- for (const source of snapshot.sourceBodies)
74
- await io.writeText(skillBodyPath(skillRoot, source.name), source.body);
75
- }
76
- }
77
- if (snapshot.create && snapshot.targetBody === null) {
78
- try {
79
- await io.remove(skillBodyPath(skillRoot, snapshot.target));
80
- }
81
- catch { /* already gone */ }
82
- try {
83
- const dir = join(skillRoot, snapshot.target);
84
- if ((await io.list(dir)).length === 0)
85
- await io.remove(dir);
86
- }
87
- catch { /* absent or non-empty */ }
72
+ const journal = JSON.parse(await io.readText(path) ?? '');
73
+ const evidence = await readUnitEvidence(io, home, journal.unitId);
74
+ if (evidence !== null) {
75
+ reports.push({ runId: journal.runId, phase: 'committed', action: 'unit-committed', ok: true, target: journal.target, sources: journal.sources, journalPath: path, unitId: journal.unitId });
76
+ continue;
88
77
  }
89
- else if (snapshot.targetBody !== null) {
90
- await io.writeText(skillBodyPath(skillRoot, snapshot.target), snapshot.targetBody);
78
+ if (journal.phase === 'committed') {
79
+ reports.push({ runId: journal.runId, phase: 'committed', action: 'unit-evidence-missing', ok: false, target: journal.target, sources: journal.sources, journalPath: path, unitId: journal.unitId });
80
+ continue;
91
81
  }
92
- const oldPaths = new Set((snapshot.targetSupport ?? []).map(file => file.path));
93
- for (const file of snapshot.targetSupport ?? []) {
94
- await io.writeText(skillFilePath(skillRoot, snapshot.target, file.path), file.content);
95
- }
96
- for (const file of snapshot.targetSupportAfter ?? []) {
97
- if (!oldPaths.has(file.path)) {
98
- try {
99
- await io.remove(skillFilePath(skillRoot, snapshot.target, file.path));
100
- }
101
- catch { /* already gone */ }
102
- }
103
- }
104
- for (const file of snapshot.sourceSupport ?? [])
105
- await io.writeText(skillFilePath(skillRoot, file.name, file.path), file.content);
106
- action = recovery === 'rollback-sources' ? 'restored-sources-and-target' : 'restored-target';
82
+ if (journal.phase === 'failed' || journal.phase === 'rolled-back')
83
+ continue;
84
+ reports.push({ runId: journal.runId, phase: journal.phase, action: 'unit-manual-required', ok: false, target: journal.target, sources: journal.sources, journalPath: path, unitId: journal.unitId });
107
85
  }
108
- catch (error) {
109
- action = 'restore-failed: ' + (error instanceof Error ? error.message : String(error));
110
- ok = false;
86
+ catch {
87
+ reports.push({ runId: '(unit)', phase: 'unknown', action: 'unit-journal-unreadable', ok: false, journalPath: path });
111
88
  }
112
89
  }
113
- if (ok)
114
- await writeJournal(io, home, { ...journal, phase: 'rolled-back', updatedAt: new Date(now).toISOString() });
115
- reports.push({ runId: journal.runId, phase: journal.phase, action, ok });
116
90
  }
117
- await io.writeText(join(home, 'merge', 'recovery.json'), JSON.stringify(reports, null, 2));
91
+ try {
92
+ await io.writeText(join(home, 'merge', 'recovery.json'), JSON.stringify(reports, null, 2));
93
+ }
94
+ catch { /* report file is best-effort; reports are still returned */ }
118
95
  return reports;
119
96
  }
120
97
  //# sourceMappingURL=commit-recovery.js.map
@@ -4,7 +4,7 @@
4
4
  * The data layer keeps four states; the user face collapses verify/fuse into one retry
5
5
  * queue and keeps the stage as a chip. The API queue must never expose the raw state.
6
6
  */
7
- export declare const MERGE_STATE_VERSION = 2;
7
+ export declare const MERGE_STATE_VERSION = 3;
8
8
  export type MergePairState = 'verify' | 'pending' | 'approved' | 'fuse' | 'skipped';
9
9
  export type MergeQueue = 'verify' | 'pending' | 'fuse' | 'skipped';
10
10
  export type MergeStage = 'verify' | 'decide' | 'author' | 'commit';
@@ -25,6 +25,7 @@ export interface MergeFailure {
25
25
  readonly message: string;
26
26
  readonly retryable: boolean;
27
27
  readonly retryFrom: 'agent1' | 'agent2' | 'none';
28
+ readonly lengths?: Readonly<Record<string, number>>;
28
29
  }
29
30
  /** One persisted pair. The id is stable for the UI; the pairKey is the natural key. */
30
31
  export interface MergeGateFact {
@@ -43,6 +44,12 @@ export interface MergeCandidate {
43
44
  readonly gate?: MergeGateFact;
44
45
  readonly source?: 'lexical' | 'proposed' | 'engine';
45
46
  }
47
+ export type RevisionRef = {
48
+ readonly kind: 'absent';
49
+ } | {
50
+ readonly kind: 'present';
51
+ readonly sha256: string;
52
+ };
46
53
  export interface MergeDecision {
47
54
  readonly shouldMerge: boolean;
48
55
  readonly mode: 'merge-into-existing' | 'create-umbrella' | 'keep';
@@ -50,9 +57,21 @@ export interface MergeDecision {
50
57
  readonly sources: readonly string[];
51
58
  readonly rationale: string;
52
59
  readonly confidence?: number;
60
+ readonly promptVersion?: string;
61
+ readonly promptHash?: string;
53
62
  readonly aSummary?: string;
54
63
  readonly bSummary?: string;
55
64
  readonly cacheRead?: number;
65
+ readonly targetRevisionRef?: RevisionRef;
66
+ readonly sourceRevisionRef?: RevisionRef;
67
+ /** Per-source anchors recorded with the decision; a commit re-checks each one. */
68
+ readonly sourceRevisions?: Readonly<Record<string, RevisionRef>>;
69
+ readonly supportDigest?: string;
70
+ }
71
+ export interface MergeValidationPlanRef {
72
+ readonly planVersion: 1;
73
+ readonly stageDigest: string;
74
+ readonly revision: string;
56
75
  }
57
76
  export interface MergeUsage {
58
77
  readonly inputTokens?: number;
@@ -86,7 +105,18 @@ export interface MergeReportPair {
86
105
  readonly unitId?: string;
87
106
  readonly failure?: MergeFailure;
88
107
  readonly usage?: MergeUsage;
108
+ readonly lengths?: Readonly<Record<string, number>>;
89
109
  readonly timing?: MergeTiming;
110
+ readonly validationPlan?: MergeValidationPlanRef;
111
+ readonly criteria?: MergeCriteria;
112
+ readonly reportOnly?: boolean;
113
+ }
114
+ export interface MergeCriteria {
115
+ readonly decision: 'keep' | 'merge' | 'candidate';
116
+ readonly validation: 'not-run' | 'report-only' | 'required';
117
+ readonly commit: 'never' | 'if-approved' | 'required';
118
+ readonly archive: 'none' | 'sources';
119
+ readonly evidence: 'none' | 'receipt';
90
120
  }
91
121
  export interface MergeReport {
92
122
  readonly schemaVersion: 2;
@@ -111,6 +141,17 @@ export interface MergeReport {
111
141
  readonly pairKey: string;
112
142
  readonly queue: MergeQueue;
113
143
  }[];
144
+ readonly reportOnly?: readonly MergeReportPair[];
145
+ readonly validationFailures?: readonly MergeReportPair[];
146
+ readonly archived?: readonly string[];
147
+ readonly commit?: {
148
+ readonly unitId?: string;
149
+ readonly target?: string;
150
+ readonly created?: boolean;
151
+ };
152
+ readonly usage?: MergeUsage;
153
+ readonly timing?: MergeTiming;
154
+ readonly criteria?: readonly MergeCriteria[];
114
155
  }
115
156
  export interface MergeRow {
116
157
  readonly id: string;
@@ -131,6 +172,8 @@ export interface MergeRow {
131
172
  readonly decision?: MergeDecision;
132
173
  readonly sourceRevision?: string;
133
174
  readonly targetRevision?: string;
175
+ readonly sourceRevisionRef?: RevisionRef;
176
+ readonly targetRevisionRef?: RevisionRef;
134
177
  readonly runId?: string;
135
178
  readonly unitId?: string;
136
179
  readonly origin?: 'agent' | 'human' | 'system';
@@ -4,7 +4,7 @@
4
4
  * The data layer keeps four states; the user face collapses verify/fuse into one retry
5
5
  * queue and keeps the stage as a chip. The API queue must never expose the raw state.
6
6
  */
7
- export const MERGE_STATE_VERSION = 2;
7
+ export const MERGE_STATE_VERSION = 3;
8
8
  export const MERGE_PAIR_STATES = ['verify', 'pending', 'approved', 'fuse', 'skipped'];
9
9
  export const MERGE_QUEUES = ['verify', 'pending', 'fuse', 'skipped'];
10
10
  /** User-facing queue of one data state. verify/fuse share retry, not two tabs. */
@@ -2,7 +2,7 @@
2
2
  * Pure merge transition table. No io, no run shell, no subagents.
3
3
  *
4
4
  * Every transition returns the whole next row (or a drop), so the caller can persist it
5
- * through mutateMergeState in one transaction. Invalid transitions are refused by name;
5
+ * through one state transaction. Invalid transitions are refused by name;
6
6
  * a caller never "repairs" a row by guessing which state the operator meant.
7
7
  */
8
8
  import { type MergeDecision, type MergeFailure, type MergeRow } from './contracts.ts';
@@ -72,5 +72,6 @@ export type TransitionResult = {
72
72
  readonly code: 'invalid-transition';
73
73
  readonly message: string;
74
74
  };
75
- export declare function transition(current: MergeRow | undefined, event: MergeEvent): TransitionResult;
75
+ /** Legacy v2 projection only; runtime writes must use transitionV3/RowTx. */
76
+ export declare function transitionLegacyForProjection(current: MergeRow | undefined, event: MergeEvent): TransitionResult;
76
77
  //# sourceMappingURL=coordinator.d.ts.map
@@ -2,7 +2,7 @@
2
2
  * Pure merge transition table. No io, no run shell, no subagents.
3
3
  *
4
4
  * Every transition returns the whole next row (or a drop), so the caller can persist it
5
- * through mutateMergeState in one transaction. Invalid transitions are refused by name;
5
+ * through one state transaction. Invalid transitions are refused by name;
6
6
  * a caller never "repairs" a row by guessing which state the operator meant.
7
7
  */
8
8
  import { pairKey, queueOf, rowIdOf, } from "./contracts.js";
@@ -49,7 +49,8 @@ function next(row, at, state, patch = {}) {
49
49
  },
50
50
  };
51
51
  }
52
- export function transition(current, event) {
52
+ /** Legacy v2 projection only; runtime writes must use transitionV3/RowTx. */
53
+ export function transitionLegacyForProjection(current, event) {
53
54
  if (event.type === 'candidate') {
54
55
  if (current !== undefined)
55
56
  return { ok: true, row: current };
@@ -106,7 +107,7 @@ export function transition(current, event) {
106
107
  : invalid(event, current.state);
107
108
  case 'approve':
108
109
  return current.state === 'pending'
109
- ? next(current, event.at, 'approved', { ...(event.runId === undefined ? {} : { runId: event.runId }) })
110
+ ? next(current, event.at, 'approved', (event.runId === undefined ? {} : { runId: event.runId }))
110
111
  : invalid(event, current.state);
111
112
  case 'reject':
112
113
  return current.state === 'pending' ? next(current, event.at, 'skipped', { origin: 'human' }) : invalid(event, current.state);
@@ -1,5 +1,5 @@
1
1
  function extractJson(text) {
2
- const fenced = /\`\`\`(?:json)?\s*([\s\S]*?)\`\`\`/i.exec(text);
2
+ const fenced = /`\``(?:json)?\s*([\s\S]*?)`\``/i.exec(text);
3
3
  const body = fenced?.[1] ?? text;
4
4
  const start = body.indexOf('{');
5
5
  if (start < 0)
@@ -1,10 +1,27 @@
1
1
  import { type EvolutionIoLike } from '@lmzhen/dsh-evolution-core';
2
+ import { type ReconcileReport } from './domain/reconcile.ts';
3
+ import type { DoctorView } from './wire.ts';
2
4
  export declare const MERGE_DOCTOR_SCHEMA_VERSION = 3;
5
+ /** The global doctor's merge line; built from the DoctorView envelope, never from raw files. */
6
+ export interface MergeDoctorFact {
7
+ readonly status: 'ok' | 'warn' | 'fail' | 'unknown';
8
+ readonly detail: string;
9
+ readonly remedy?: string;
10
+ }
11
+ /** Compose the one-line global-doctor fact from buildDoctorView + the last reconcile report. */
12
+ export declare function mergeDoctorFact(view: DoctorView, report: ReconcileReport | null): MergeDoctorFact;
3
13
  export interface MergeDoctorRow {
4
14
  readonly key: string;
5
15
  readonly label: string;
6
- readonly status: 'ok' | 'warn' | 'unknown';
16
+ readonly status: 'ok' | 'warn' | 'blocked' | 'unknown';
7
17
  readonly detail: string;
8
18
  }
19
+ /** Global doctor host section: a missing merge mount must fail with E-325, not render empty. */
20
+ export declare function mergeDoctorAvailability(mounted: boolean): {
21
+ readonly ok: true;
22
+ } | {
23
+ readonly ok: false;
24
+ readonly code: 'E-325';
25
+ };
9
26
  export declare function mergeDoctor(io: EvolutionIoLike, home: string, configuredRoot?: string): Promise<readonly MergeDoctorRow[]>;
10
27
  //# sourceMappingURL=doctor.d.ts.map
@@ -1,27 +1,163 @@
1
+ import { readTextOrNull } from "./domain/read-probe.js";
1
2
  /**
2
3
  * Merge doctor rows: state, pending, gate cache and staging are each probed read-only.
3
4
  * A missing file is an empty feature, not a failure; an unreadable file is a warning the
4
5
  * operator can act on. This module never repairs or writes.
5
6
  */
7
+ import { createHash } from 'node:crypto';
8
+ import { stat } from 'node:fs/promises';
6
9
  import { join } from 'node:path';
7
- import { probeList } from '@lmzhen/dsh-evolution-core';
8
- import { loadMergeState } from "./state.js";
9
- import { countRows } from "./status.js";
10
+ import { probeList, readTreeLock, runsFile } from '@lmzhen/dsh-evolution-core';
11
+ import { migrationJournalPath, migrationReportPath, v2BackupPath } from "./domain/state-bootstrap.js";
12
+ import { readMergeSnapshot } from "./domain/merge-snapshot.js";
13
+ import { validateArtifacts } from "./domain/consistency.js";
14
+ import { readJournal } from "./domain/commit-journal-store.js";
10
15
  import { stagingRoot } from "./staging.js";
16
+ import { readReconcileReport } from "./domain/reconcile.js";
11
17
  export const MERGE_DOCTOR_SCHEMA_VERSION = 3;
18
+ /** Compose the one-line global-doctor fact from buildDoctorView + the last reconcile report. */
19
+ export function mergeDoctorFact(view, report) {
20
+ const counts = view.counts;
21
+ const reconcileText = report === null
22
+ ? 'reconcile 尚未运行'
23
+ : report.state === 'blocked'
24
+ ? 'reconcile blocked ' + (report.code ?? 'merge-state-blocked')
25
+ : 'reconcile ' + String(report.actions.length) + ' 项动作';
26
+ const problems = [];
27
+ if (view.blocked !== undefined)
28
+ problems.push('state blocked ' + view.blocked.code);
29
+ if (view.hostOnly !== undefined)
30
+ problems.push('宿主数据面不可用 ' + view.hostOnly.code);
31
+ if (view.staging.state === 'unreadable')
32
+ problems.push('staging 索引不可读' + (view.staging.detail === undefined ? '' : ':' + view.staging.detail));
33
+ if (counts.expired > 0)
34
+ problems.push('expired ' + String(counts.expired));
35
+ const unstable = view.rows.filter(row => row.status === 'warn' || row.status === 'fail');
36
+ if (unstable.length > 0)
37
+ problems.push(unstable.map(row => row.key + '=' + row.status).join(','));
38
+ if (report?.state === 'blocked')
39
+ problems.push('reconcile blocked');
40
+ const latest = view.latestRun;
41
+ const latestText = latest === undefined ? '' : ';最近 run ' + latest.id + ' [' + latest.state + ']';
42
+ const detail = 'verify ' + String(counts.active.verify) + ' / pending ' + String(counts.active.pending) + ' / fuse ' + String(counts.active.fuse) + ' / skipped ' + String(counts.active.skipped) + ' / expired ' + String(counts.expired)
43
+ + ';staging ' + view.staging.state + ' dirs=' + String(view.staging.dirs)
44
+ + ';审批 ' + (view.approval.mergeApproval ? '开' : '关') + ';' + reconcileText + latestText
45
+ + (problems.length === 0 ? '' : ';问题:' + problems.join(';'));
46
+ const status = problems.length === 0
47
+ ? 'ok'
48
+ : view.blocked !== undefined || view.hostOnly !== undefined || report?.state === 'blocked' ? 'fail' : 'warn';
49
+ return problems.length === 0 ? { status, detail } : { status, detail, remedy: '运行 /evolution merge doctor 查看逐项;必要时 /evolution merge reconcile 收敛' };
50
+ }
51
+ /** Global doctor host section: a missing merge mount must fail with E-325, not render empty. */
52
+ export function mergeDoctorAvailability(mounted) {
53
+ return mounted ? { ok: true } : { ok: false, code: 'E-325' };
54
+ }
12
55
  export async function mergeDoctor(io, home, configuredRoot) {
13
56
  const rows = [];
14
- const loaded = await loadMergeState(io, home);
15
- const counts = countRows(loaded.state.rows);
57
+ const snapshot = await readMergeSnapshot({ io, home, runs: [], approval: false });
58
+ const hasState = await io.exists(join(home, 'merge', 'merge-state.json'));
59
+ const counts = snapshot.doctor.counts.active;
16
60
  rows.push({
17
61
  key: 'state',
18
62
  label: '合并状态',
19
- status: loaded.note === undefined ? 'ok' : 'warn',
20
- detail: loaded.note ?? 'pairs ' + String(loaded.state.rows.length) + ';verify ' + String(counts.verify) + ' / pending ' + String(counts.pending) + ' / fuse ' + String(counts.fuse) + ' / skipped ' + String(counts.skipped),
63
+ status: snapshot.blocked === undefined || !hasState ? 'ok' : 'warn',
64
+ detail: snapshot.blocked === undefined ? 'pairs ' + String(snapshot.rows.length) + ';verify ' + String(counts.verify) + ' / pending ' + String(counts.pending) + ' / fuse ' + String(counts.fuse) + ' / skipped ' + String(counts.skipped) : snapshot.blocked.code + ': ' + snapshot.blocked.message,
65
+ });
66
+ const recoveryRuns = new Set();
67
+ try {
68
+ const raw = await io.readText(join(home, 'merge', 'recovery.json'));
69
+ if (raw !== null)
70
+ for (const report of JSON.parse(raw))
71
+ if (report.ok === true && report.runId !== undefined)
72
+ recoveryRuns.add(report.runId);
73
+ }
74
+ catch { }
75
+ const receiptRuns = new Set();
76
+ const receiptDir = await probeList(io, join(home, 'merge', 'receipts'));
77
+ if (receiptDir.kind === 'present')
78
+ for (const name of receiptDir.value)
79
+ receiptRuns.add(name);
80
+ const invalid = [];
81
+ if (snapshot.blocked !== undefined)
82
+ invalid.push({ id: 'state', reason: snapshot.blocked.code });
83
+ for (const item of snapshot.rows) {
84
+ const row = item.row;
85
+ const runId = row.runId ?? (item.stage?.ref.split('/')[0]);
86
+ const artifactRow = row.state === 'approved' ? 'approved' : row.state === 'fuse' ? 'fuse' : row.state;
87
+ const run = runId === undefined ? 'none' : row.state === 'pending' ? 'awaiting-approval' : 'running';
88
+ const staging = item.stage === null ? (row.state === 'fuse' ? 'missing' : 'none') : item.stage.state === 'present' || item.stage.state === 'reserved' ? 'kept' : row.state === 'fuse' ? 'missing' : 'none';
89
+ const phase = runId === undefined ? null : await readJournal(io, home, runId);
90
+ const journal = phase === null ? 'none' : phase.phase === 'prepared' ? 'prepared' : phase.phase === 'target-written' || phase.phase === 'sources-archived' ? 'committing' : phase.phase === 'committed' ? 'committed' : phase.phase === 'rolled-back' ? 'rolled-back' : 'failed';
91
+ const evidence = runId !== undefined && await readTextOrNull(io, join(home, 'merge', 'receipts', runId + '.json')) !== null ? 'receipt' : 'none';
92
+ const result = validateArtifacts({ row: artifactRow, run, staging, journal, evidence, activeRuns: run === 'running' || run === 'awaiting-approval' ? 1 : 0, ...(runId !== undefined && recoveryRuns.has(runId) ? { recovery: true } : {}) });
93
+ if (!result.ok)
94
+ invalid.push({ id: row.id, reason: result.reason ?? 'not-allowed' });
95
+ }
96
+ rows.push({ key: 'merge-state-v3', label: 'state schema', status: snapshot.blocked === undefined ? 'ok' : 'warn', detail: snapshot.blocked === undefined ? 'schemaVersion 4' : snapshot.blocked.code });
97
+ let quarantineCount = 0;
98
+ try {
99
+ const qraw = await io.readText(join(home, 'merge', 'merge-state.quarantine.json'));
100
+ if (qraw !== null) {
101
+ const parsed = JSON.parse(qraw);
102
+ quarantineCount = Array.isArray(parsed) ? parsed.length : 0;
103
+ }
104
+ }
105
+ catch {
106
+ quarantineCount = -1;
107
+ }
108
+ rows.push({ key: 'quarantine', label: 'quarantine', status: quarantineCount < 0 ? 'unknown' : quarantineCount === 0 ? 'ok' : 'warn', detail: quarantineCount < 0 ? 'quarantine 不可读' : quarantineCount + ' 行待人工检查' });
109
+ const holder = await readTreeLock(io, home).catch(() => null);
110
+ rows.push({ key: 'tree-lock', label: 'tree lock', status: holder == null ? 'ok' : 'warn', detail: holder == null ? 'free' : 'holder pid=' + String(holder.pid) + ' epoch=' + String(holder.epoch) + ' token=' + holder.token.slice(0, 8) });
111
+ const backupPath = v2BackupPath(home);
112
+ const backupRaw = await readTextOrNull(io, backupPath);
113
+ const backupMode = await stat(backupPath).then(value => value.mode).catch(() => null);
114
+ const backupReadOnly = backupMode === null ? null : (backupMode & 0o200) === 0;
115
+ rows.push({ key: 'backup', label: 'v2 backup', status: backupRaw === null ? 'unknown' : backupReadOnly === false ? 'warn' : 'ok', detail: backupRaw === null ? 'no v2 backup (not migrated or already cleaned)' : 'merge-state.v2.bak.json sha256=' + createHash('sha256').update(backupRaw).digest('hex').slice(0, 16) + ' readonly=' + String(backupReadOnly) });
116
+ const journal = await readTextOrNull(io, migrationJournalPath(home));
117
+ const report = await readTextOrNull(io, migrationReportPath(home));
118
+ rows.push({ key: 'migration', label: 'state migration', status: snapshot.blocked === undefined ? (journal === null ? 'ok' : 'warn') : 'warn', detail: snapshot.blocked === undefined ? (journal === null ? (report === null ? 'ready (no migration report)' : 'ready; report present') : 'ready; migration journal pending') : snapshot.blocked.code + ': ' + snapshot.blocked.message });
119
+ rows.push({ key: 'staging-index', label: 'staging index', status: 'ok', detail: String(snapshot.stages.length) + ' index entries' });
120
+ const claims = await probeList(io, join(home, 'merge', 'claims'));
121
+ rows.push({ key: 'target-claim', label: 'target claim', status: claims.kind === 'unknown' ? 'unknown' : 'ok', detail: claims.kind === 'unknown' ? 'claims unreadable' : claims.kind === 'absent' ? 'no claims' : String(claims.value.length) + ' claim files' });
122
+ let orphanRuns = 0;
123
+ let runCount = 0;
124
+ try {
125
+ const raw = await io.readText(runsFile(home));
126
+ if (raw !== null) {
127
+ const parsed = JSON.parse(raw);
128
+ runCount = parsed.runs?.length ?? 0;
129
+ for (const run of parsed.runs ?? []) {
130
+ if (run.state !== 'running' || typeof run.pid !== 'number')
131
+ continue;
132
+ try {
133
+ process.kill(run.pid, 0);
134
+ }
135
+ catch {
136
+ orphanRuns += 1;
137
+ }
138
+ }
139
+ }
140
+ }
141
+ catch {
142
+ orphanRuns = -1;
143
+ }
144
+ rows.push({ key: 'runs', label: 'run owner', status: orphanRuns < 0 ? 'unknown' : orphanRuns === 0 ? 'ok' : 'warn', detail: orphanRuns < 0 ? 'runs.json unreadable' : String(runCount) + ' runs; ' + String(orphanRuns) + ' dead-owner running' });
145
+ rows.push({ key: 'install', label: 'install form', status: io.exists === undefined ? 'unknown' : 'ok', detail: 'merge plugin loaded' });
146
+ const reconcile = await readReconcileReport(io, home);
147
+ rows.push({
148
+ key: 'reconcile',
149
+ label: '状态收敛',
150
+ status: reconcile === null ? 'unknown' : reconcile.state === 'blocked' ? 'warn' : reconcile.actions.length > 0 ? 'warn' : 'ok',
151
+ detail: reconcile === null
152
+ ? '尚无 reconcile 报告(启动或 /evolution merge reconcile 会生成)'
153
+ : reconcile.state === 'blocked'
154
+ ? 'blocked:' + (reconcile.code ?? 'merge-state-blocked') + (reconcile.message === undefined ? '' : ' — ' + reconcile.message)
155
+ : String(reconcile.actions.length) + ' 项动作(' + new Date(reconcile.at).toISOString() + ')' + (reconcile.actions.length === 0 ? '' : ':' + reconcile.actions.map(action => action.rowId + '=' + action.code).join(', ')),
21
156
  });
22
- const modelRoot = configuredRoot !== undefined && configuredRoot !== '' ? configuredRoot : process.env.SKILL_MERGE_MODELS_ROOT;
23
- if (modelRoot === undefined || modelRoot === '') {
24
- rows.push({ key: 'models', label: '合并模型', status: 'warn', detail: '未设置 SKILL_MERGE_MODELS_ROOT;合并扫描会 fail loud' });
157
+ rows.push({ key: 'consistency', label: '一致性', status: snapshot.blocked !== undefined ? 'blocked' : invalid.length === 0 ? 'ok' : 'warn', detail: snapshot.blocked !== undefined ? 'blocked: ' + snapshot.blocked.code : invalid.length === 0 ? '无非法组合' : String(invalid.length) + ' 行需人工检查: ' + invalid.map(row => row.id + '=' + row.reason).join(', ') });
158
+ const modelRoot = configuredRoot?.trim() ?? '';
159
+ if (modelRoot === '') {
160
+ rows.push({ key: 'models', label: '合并模型', status: 'warn', detail: '未配置 mergeModelsRoot;合并扫描会 fail loud' });
25
161
  }
26
162
  else {
27
163
  const int8 = join(modelRoot, 'potion-multilingual-128M-int8');
@@ -45,6 +181,24 @@ export async function mergeDoctor(io, home, configuredRoot) {
45
181
  status: 'ok',
46
182
  detail: stagingRoot(home, '<runId>'),
47
183
  });
184
+ try {
185
+ const raw = await io.readText(join(home, 'merge', 'recovery.json'));
186
+ if (raw !== null) {
187
+ const reports = JSON.parse(raw);
188
+ const failed = reports.filter(report => report.ok !== true);
189
+ rows.push({
190
+ key: 'recovery',
191
+ label: '提交恢复',
192
+ status: failed.length === 0 ? 'ok' : 'warn',
193
+ detail: failed.length === 0
194
+ ? String(reports.length) + ' 条恢复报告,无待处理'
195
+ : String(failed.length) + ' 条待处理:' + failed.map(report => (report.runId ?? '?') + '=' + (report.action ?? '?')).join(', '),
196
+ });
197
+ }
198
+ }
199
+ catch {
200
+ rows.push({ key: 'recovery', label: '提交恢复', status: 'unknown', detail: '无法读取 merge/recovery.json' });
201
+ }
48
202
  return rows;
49
203
  }
50
204
  //# sourceMappingURL=doctor.js.map
@@ -0,0 +1,26 @@
1
+ /**
2
+ * The round-2 JSON envelope a child writes after authoring: which files the merge left
3
+ * behind. Both authoring paths (same-child scan round2 and the new-child approve/resume
4
+ * path) parse it identically, so a declared-but-missing file fails the same way on both.
5
+ */
6
+ export interface AuthoringManifest {
7
+ readonly target?: string;
8
+ readonly files: readonly string[];
9
+ }
10
+ /**
11
+ * Parse `{"target":..., "files":[...]}` from a child's final turn text.
12
+ * @param text - the child's answer (JSON is expected; prose is tolerated).
13
+ * @returns the manifest, or null when the turn carried no well-formed envelope.
14
+ */
15
+ export declare function parseAuthoringManifest(text: string): AuthoringManifest | null;
16
+ /** Files a manifest promises for a staged tree (declared list, or the tree when undeclared). */
17
+ export declare function manifestFilesOf(manifest: AuthoringManifest | null, stagedPaths: readonly string[]): readonly string[];
18
+ /**
19
+ * The first file the manifest promises but the staged tree does not contain.
20
+ * SKILL.md itself is always required, whether or not the manifest lists it.
21
+ * @param files - manifest files (see {@link manifestFilesOf}).
22
+ * @param stagedPaths - support-file paths actually present in the staged tree.
23
+ * @returns the missing path, or null when the declaration is satisfied.
24
+ */
25
+ export declare function missingManifestFile(files: readonly string[], stagedPaths: readonly string[]): string | null;
26
+ //# sourceMappingURL=authoring-manifest.d.ts.map