@lmzhen/dsh-evolution-merge 0.29.0 → 0.30.1

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 (97) hide show
  1. package/lib/index.js +1125 -568
  2. package/lib/types/adapters/channel.js +29 -0
  3. package/lib/types/adapters/engine-screen.d.ts +1 -1
  4. package/lib/types/adapters/engine-screen.js +45 -0
  5. package/lib/types/adapters/library.js +14 -0
  6. package/lib/types/adapters/model-runner.js +34 -0
  7. package/lib/types/adapters/select.d.ts +3 -0
  8. package/lib/types/adapters/select.js +35 -0
  9. package/lib/types/adapters/undo.js +35 -0
  10. package/lib/types/approve-command.js +42 -0
  11. package/lib/types/authoring-flow.js +48 -0
  12. package/lib/types/channel-port.js +74 -0
  13. package/lib/types/commands.d.ts +9 -0
  14. package/lib/types/commands.js +79 -0
  15. package/lib/types/commit-assembly.js +48 -0
  16. package/lib/types/commit-execute.js +52 -0
  17. package/lib/types/commit-plan.js +56 -0
  18. package/lib/types/commit-receipt.d.ts +2 -0
  19. package/lib/types/commit-receipt.js +85 -0
  20. package/lib/types/contracts.d.ts +14 -5
  21. package/lib/types/contracts.js +57 -0
  22. package/lib/types/coordinator.d.ts +3 -0
  23. package/lib/types/coordinator.js +131 -0
  24. package/lib/types/decision.js +61 -0
  25. package/lib/types/doctor.js +50 -0
  26. package/lib/types/domain/claim.d.ts +10 -0
  27. package/lib/types/domain/claim.js +12 -0
  28. package/lib/types/domain/commit-journal-store.d.ts +6 -0
  29. package/lib/types/domain/commit-journal-store.js +22 -0
  30. package/lib/types/domain/commit-journal.d.ts +16 -0
  31. package/lib/types/domain/commit-journal.js +25 -0
  32. package/lib/types/domain/errors.d.ts +10 -0
  33. package/lib/types/domain/errors.js +7 -0
  34. package/lib/types/domain/failure-route.d.ts +25 -0
  35. package/lib/types/domain/failure-route.js +22 -0
  36. package/lib/types/domain/indexes.d.ts +21 -0
  37. package/lib/types/domain/indexes.js +15 -0
  38. package/lib/types/domain/ledger.d.ts +17 -0
  39. package/lib/types/domain/ledger.js +65 -0
  40. package/lib/types/domain/markdown.d.ts +2 -0
  41. package/lib/types/domain/markdown.js +57 -0
  42. package/lib/types/domain/migrate-v1.d.ts +41 -0
  43. package/lib/types/domain/migrate-v1.js +23 -0
  44. package/lib/types/domain/pair-index-store.d.ts +6 -0
  45. package/lib/types/domain/pair-index-store.js +25 -0
  46. package/lib/types/domain/pair-index.d.ts +9 -0
  47. package/lib/types/domain/pair-index.js +28 -0
  48. package/lib/types/domain/resolve-pair.d.ts +11 -0
  49. package/lib/types/domain/resolve-pair.js +17 -0
  50. package/lib/types/domain/row.d.ts +113 -0
  51. package/lib/types/domain/row.js +111 -0
  52. package/lib/types/domain/run-record.d.ts +45 -0
  53. package/lib/types/domain/run-record.js +24 -0
  54. package/lib/types/domain/state.d.ts +57 -0
  55. package/lib/types/domain/state.js +65 -0
  56. package/lib/types/gate-cache.js +36 -0
  57. package/lib/types/index.js +241 -0
  58. package/lib/types/ladder.js +162 -0
  59. package/lib/types/merge-command.js +43 -0
  60. package/lib/types/merge-routes.d.ts +14 -4
  61. package/lib/types/merge-routes.js +99 -0
  62. package/lib/types/merge-tool.js +69 -0
  63. package/lib/types/new-child-authoring.js +83 -0
  64. package/lib/types/pair-command.js +18 -0
  65. package/lib/types/params.js +49 -0
  66. package/lib/types/pending-actions.js +28 -0
  67. package/lib/types/prompts.js +60 -0
  68. package/lib/types/read-commands.js +32 -0
  69. package/lib/types/report.js +70 -0
  70. package/lib/types/resume-command.js +57 -0
  71. package/lib/types/round2-outcome.js +50 -0
  72. package/lib/types/routes.d.ts +5 -2
  73. package/lib/types/routes.js +75 -0
  74. package/lib/types/rules.js +55 -0
  75. package/lib/types/run-shell.d.ts +7 -0
  76. package/lib/types/run-shell.js +69 -0
  77. package/lib/types/runtime.d.ts +2 -0
  78. package/lib/types/runtime.js +269 -0
  79. package/lib/types/scan-command.d.ts +1 -0
  80. package/lib/types/scan-command.js +30 -0
  81. package/lib/types/scan-ports.js +93 -0
  82. package/lib/types/scan-run.d.ts +2 -0
  83. package/lib/types/scan-run.js +186 -0
  84. package/lib/types/skill-card.js +2 -0
  85. package/lib/types/skipped.js +24 -0
  86. package/lib/types/stage-tool-registry.js +32 -0
  87. package/lib/types/stage-tool.js +31 -0
  88. package/lib/types/staged-tree.js +75 -0
  89. package/lib/types/staging-files.js +86 -0
  90. package/lib/types/staging.d.ts +7 -0
  91. package/lib/types/staging.js +149 -0
  92. package/lib/types/state.js +103 -0
  93. package/lib/types/status.d.ts +2 -1
  94. package/lib/types/status.js +28 -0
  95. package/lib/types/supersede.js +14 -0
  96. package/lib/types/undo-merge.js +23 -0
  97. package/package.json +4 -4
@@ -6,6 +6,7 @@ export interface CommitReceipt {
6
6
  readonly target: string;
7
7
  readonly create: boolean;
8
8
  readonly sources: readonly string[];
9
+ readonly pairKey?: string;
9
10
  readonly archived: readonly string[];
10
11
  readonly at: number;
11
12
  }
@@ -13,5 +14,6 @@ export declare function commitReceiptPath(home: string, runId: string): string;
13
14
  export declare function durableReceiptDir(home: string, parentRunId: string): string;
14
15
  export declare function durableReceiptPath(home: string, parentRunId: string, stageKey: string): string;
15
16
  export declare function writeCommitReceipt(io: EvolutionIoLike, home: string, receipt: CommitReceipt): Promise<string>;
17
+ export declare function readCommitReceipts(io: EvolutionIoLike, home: string): Promise<readonly CommitReceipt[]>;
16
18
  export declare function readCommitReceipt(io: EvolutionIoLike, home: string, runId: string, target?: string): Promise<CommitReceipt | null>;
17
19
  //# sourceMappingURL=commit-receipt.d.ts.map
@@ -0,0 +1,85 @@
1
+ /**
2
+ * Commit receipt: <run>/commit.json, written immediately after a successful commit.
3
+ * It is the idempotency/recovery marker — merge resume and run start read it before
4
+ * replaying a commit. Receipt write failure must not turn a landed commit into a failure;
5
+ * the caller reports the warning instead.
6
+ */
7
+ import { join } from 'node:path';
8
+ import { stagingRoot } from "./staging.js";
9
+ export function commitReceiptPath(home, runId) {
10
+ return join(stagingRoot(home, runId), 'commit.json');
11
+ }
12
+ export function durableReceiptDir(home, parentRunId) {
13
+ return join(home, 'merge', 'receipts', parentRunId);
14
+ }
15
+ export function durableReceiptPath(home, parentRunId, stageKey) {
16
+ const leaf = stageKey.split('/').pop() ?? stageKey;
17
+ return join(durableReceiptDir(home, parentRunId), leaf + '.json');
18
+ }
19
+ function parseReceipt(raw) {
20
+ if (raw === null)
21
+ return null;
22
+ try {
23
+ const parsed = JSON.parse(raw);
24
+ if (typeof parsed.runId !== 'string' || typeof parsed.unitId !== 'string' || typeof parsed.target !== 'string'
25
+ || typeof parsed.create !== 'boolean' || !Array.isArray(parsed.sources) || !Array.isArray(parsed.archived) || typeof parsed.at !== 'number') {
26
+ return null;
27
+ }
28
+ return parsed;
29
+ }
30
+ catch {
31
+ return null;
32
+ }
33
+ }
34
+ export async function writeCommitReceipt(io, home, receipt) {
35
+ const path = commitReceiptPath(home, receipt.runId);
36
+ await io.writeText(path, JSON.stringify(receipt, null, 2) + '\n');
37
+ const parent = receipt.parentRunId ?? receipt.runId.split('/pairs/')[0] ?? receipt.runId;
38
+ await io.writeText(durableReceiptPath(home, parent, receipt.runId), JSON.stringify({ ...receipt, parentRunId: parent }, null, 2) + '\n');
39
+ return path;
40
+ }
41
+ export async function readCommitReceipts(io, home) {
42
+ const root = join(home, 'merge', 'receipts');
43
+ const out = [];
44
+ let runs;
45
+ try {
46
+ runs = await io.list(root);
47
+ }
48
+ catch {
49
+ return out;
50
+ }
51
+ for (const run of runs.sort()) {
52
+ const dir = join(root, run);
53
+ let files;
54
+ try {
55
+ files = await io.list(dir);
56
+ }
57
+ catch {
58
+ continue;
59
+ }
60
+ for (const file of files.sort()) {
61
+ if (!file.endsWith('.json'))
62
+ continue;
63
+ const receipt = parseReceipt(await io.readText(join(dir, file)));
64
+ if (receipt !== null)
65
+ out.push(receipt);
66
+ }
67
+ }
68
+ return out;
69
+ }
70
+ export async function readCommitReceipt(io, home, runId, target) {
71
+ const direct = parseReceipt(await io.readText(commitReceiptPath(home, runId)));
72
+ if (direct !== null && (target === undefined || direct.target === target))
73
+ return direct;
74
+ try {
75
+ const names = await io.list(durableReceiptDir(home, runId));
76
+ for (const name of names.sort()) {
77
+ const receipt = parseReceipt(await io.readText(join(durableReceiptDir(home, runId), name)));
78
+ if (receipt !== null && (target === undefined || receipt.target === target))
79
+ return receipt;
80
+ }
81
+ }
82
+ catch { /* absent */ }
83
+ return null;
84
+ }
85
+ //# sourceMappingURL=commit-receipt.js.map
@@ -4,9 +4,9 @@
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 = 1;
8
- export type MergePairState = 'verify' | 'pending' | 'fuse' | 'skipped';
9
- export type MergeQueue = 'pending' | 'retry' | 'skipped';
7
+ export declare const MERGE_STATE_VERSION = 2;
8
+ export type MergePairState = 'verify' | 'pending' | 'approved' | 'fuse' | 'skipped';
9
+ export type MergeQueue = 'verify' | 'pending' | 'fuse' | 'skipped';
10
10
  export type MergeStage = 'verify' | 'decide' | 'author' | 'commit';
11
11
  export type MergeCommand = 'scan' | 'pair' | 'approve' | 'resume';
12
12
  export declare const MERGE_PAIR_STATES: readonly MergePairState[];
@@ -88,7 +88,7 @@ export interface MergeReportPair {
88
88
  readonly timing?: MergeTiming;
89
89
  }
90
90
  export interface MergeReport {
91
- readonly schemaVersion: 1;
91
+ readonly schemaVersion: 2;
92
92
  readonly runId: string;
93
93
  readonly kind: 'merge';
94
94
  readonly command: MergeRunCommand;
@@ -118,12 +118,21 @@ export interface MergeRow {
118
118
  readonly b: string;
119
119
  readonly state: MergePairState;
120
120
  readonly queue: MergeQueue;
121
+ readonly expired?: 'target-taken' | 'source-changed';
122
+ readonly reason?: string;
123
+ readonly target?: {
124
+ readonly requested: string;
125
+ readonly actual?: string;
126
+ readonly created?: boolean;
127
+ };
121
128
  readonly failure?: MergeFailure;
122
129
  /** Round-1 decision, persisted for the later approve round. */
123
130
  readonly decision?: MergeDecision;
131
+ readonly sourceRevision?: string;
132
+ readonly targetRevision?: string;
124
133
  readonly runId?: string;
125
134
  readonly unitId?: string;
126
- readonly origin?: 'agent' | 'human';
135
+ readonly origin?: 'agent' | 'human' | 'system';
127
136
  readonly attempts?: number;
128
137
  readonly stagingRef?: string;
129
138
  readonly stagingExpiresAt?: number;
@@ -0,0 +1,57 @@
1
+ /**
2
+ * Frozen merge-domain vocabulary. No io, no host, no imports from the plugin shell.
3
+ *
4
+ * The data layer keeps four states; the user face collapses verify/fuse into one retry
5
+ * queue and keeps the stage as a chip. The API queue must never expose the raw state.
6
+ */
7
+ export const MERGE_STATE_VERSION = 2;
8
+ export const MERGE_PAIR_STATES = ['verify', 'pending', 'approved', 'fuse', 'skipped'];
9
+ export const MERGE_QUEUES = ['verify', 'pending', 'fuse', 'skipped'];
10
+ /** User-facing queue of one data state. verify/fuse share retry, not two tabs. */
11
+ export function queueOf(state) {
12
+ switch (state) {
13
+ case 'verify': return 'verify';
14
+ case 'pending': return 'pending';
15
+ case 'approved':
16
+ case 'fuse': return 'fuse';
17
+ case 'skipped': return 'skipped';
18
+ }
19
+ }
20
+ /** Coarse stage chip; skipped has no active stage. */
21
+ export function stageOf(state) {
22
+ switch (state) {
23
+ case 'verify': return 'verify';
24
+ case 'pending': return 'decide';
25
+ case 'approved':
26
+ case 'fuse': return 'author';
27
+ case 'skipped': return null;
28
+ }
29
+ }
30
+ /** Stable pair identity; NUL cannot appear in a skill name. */
31
+ export function pairKey(a, b) {
32
+ return a < b ? a + '\u0000' + b : b + '\u0000' + a;
33
+ }
34
+ /** Deterministic, URL-safe id derived from the pair key (FNV-1a 32). */
35
+ export function rowIdOf(key) {
36
+ let hash = 0x811c9dc5;
37
+ for (let index = 0; index < key.length; index += 1) {
38
+ hash ^= key.charCodeAt(index);
39
+ hash = Math.imul(hash, 0x01000193) >>> 0;
40
+ }
41
+ return 'mr_' + hash.toString(36).padStart(7, '0');
42
+ }
43
+ export function emptyMergeState() {
44
+ return { schemaVersion: MERGE_STATE_VERSION, rows: [] };
45
+ }
46
+ /** One record can be retried only when the failure says so and names a stage. */
47
+ export const MERGE_MAX_ATTEMPTS = 3;
48
+ export function canRetry(record) {
49
+ if (record.state === 'skipped')
50
+ return false;
51
+ if ((record.attempts ?? 0) >= MERGE_MAX_ATTEMPTS)
52
+ return false;
53
+ if (record.failure === undefined)
54
+ return record.state !== 'pending';
55
+ return record.failure.retryable && record.failure.retryFrom !== 'none';
56
+ }
57
+ //# sourceMappingURL=contracts.js.map
@@ -36,6 +36,9 @@ export type MergeEvent = {
36
36
  readonly type: 'round2-fail';
37
37
  readonly failure: MergeFailure;
38
38
  readonly at: string;
39
+ } | {
40
+ readonly type: 'round2-cancel';
41
+ readonly at: string;
39
42
  } | {
40
43
  readonly type: 'round2-success';
41
44
  readonly unitId?: string;
@@ -0,0 +1,131 @@
1
+ /**
2
+ * Pure merge transition table. No io, no run shell, no subagents.
3
+ *
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;
6
+ * a caller never "repairs" a row by guessing which state the operator meant.
7
+ */
8
+ import { pairKey, queueOf, rowIdOf, } from "./contracts.js";
9
+ function invalid(event, state) {
10
+ return {
11
+ ok: false,
12
+ code: 'invalid-transition',
13
+ message: 'cannot apply ' + event.type + ' to ' + (state ?? 'no row'),
14
+ };
15
+ }
16
+ /** Drop fields that must not survive a transition; optional fields stay absent. */
17
+ function base(row, at) {
18
+ return {
19
+ id: row.id,
20
+ pairKey: row.pairKey,
21
+ a: row.a,
22
+ b: row.b,
23
+ state: row.state,
24
+ queue: row.queue,
25
+ createdAt: row.createdAt,
26
+ updatedAt: at,
27
+ ...(row.runId === undefined ? {} : { runId: row.runId }),
28
+ ...(row.unitId === undefined ? {} : { unitId: row.unitId }),
29
+ ...(row.origin === undefined ? {} : { origin: row.origin }),
30
+ ...(row.decision === undefined ? {} : { decision: row.decision }),
31
+ ...(row.sourceRevision === undefined ? {} : { sourceRevision: row.sourceRevision }),
32
+ ...(row.targetRevision === undefined ? {} : { targetRevision: row.targetRevision }),
33
+ ...(row.attempts === undefined ? {} : { attempts: row.attempts }),
34
+ ...(row.stagingRef === undefined ? {} : { stagingRef: row.stagingRef }),
35
+ ...(row.stagingExpiresAt === undefined ? {} : { stagingExpiresAt: row.stagingExpiresAt }),
36
+ ...(row.expired === undefined ? {} : { expired: row.expired }),
37
+ ...(row.reason === undefined ? {} : { reason: row.reason }),
38
+ ...(row.target === undefined ? {} : { target: row.target }),
39
+ };
40
+ }
41
+ function next(row, at, state, patch = {}) {
42
+ return {
43
+ ok: true,
44
+ row: {
45
+ ...base(row, at),
46
+ state,
47
+ queue: queueOf(state),
48
+ ...patch,
49
+ },
50
+ };
51
+ }
52
+ export function transition(current, event) {
53
+ if (event.type === 'candidate') {
54
+ if (current !== undefined)
55
+ return { ok: true, row: current };
56
+ const key = pairKey(event.a, event.b);
57
+ return {
58
+ ok: true,
59
+ row: {
60
+ id: rowIdOf(key),
61
+ pairKey: key,
62
+ a: event.a,
63
+ b: event.b,
64
+ state: 'verify',
65
+ queue: 'verify',
66
+ createdAt: event.at,
67
+ updatedAt: event.at,
68
+ },
69
+ };
70
+ }
71
+ if (event.type === 'source-gone')
72
+ return { ok: true, row: null };
73
+ if (event.type === 'gate-keep') {
74
+ if (current !== undefined)
75
+ return { ok: true, row: current };
76
+ const key = pairKey(event.a, event.b);
77
+ return {
78
+ ok: true,
79
+ row: {
80
+ id: rowIdOf(key),
81
+ pairKey: key,
82
+ a: event.a,
83
+ b: event.b,
84
+ state: 'skipped',
85
+ queue: 'skipped',
86
+ origin: 'agent',
87
+ createdAt: event.at,
88
+ updatedAt: event.at,
89
+ },
90
+ };
91
+ }
92
+ if (current === undefined)
93
+ return invalid(event, undefined);
94
+ switch (event.type) {
95
+ case 'round1-keep':
96
+ return current.state === 'verify' ? next(current, event.at, 'skipped', { origin: 'agent' }) : invalid(event, current.state);
97
+ case 'round1-merge':
98
+ if (current.state !== 'verify')
99
+ return invalid(event, current.state);
100
+ return event.approval === 'on'
101
+ ? next(current, event.at, 'pending', { ...(event.decision === undefined ? {} : { decision: event.decision }), ...(event.stagingRef === undefined ? {} : { stagingRef: event.stagingRef }), ...(event.stagingExpiresAt === undefined ? {} : { stagingExpiresAt: event.stagingExpiresAt }) })
102
+ : next(current, event.at, 'approved', { ...(event.decision === undefined ? {} : { decision: event.decision }), ...(event.stagingRef === undefined ? {} : { stagingRef: event.stagingRef }), ...(event.stagingExpiresAt === undefined ? {} : { stagingExpiresAt: event.stagingExpiresAt }) });
103
+ case 'round1-fail':
104
+ return current.state === 'verify'
105
+ ? next(current, event.at, 'verify', { failure: event.failure, attempts: (current.attempts ?? 0) + 1 })
106
+ : invalid(event, current.state);
107
+ case 'approve':
108
+ return current.state === 'pending'
109
+ ? next(current, event.at, 'approved', { ...(event.runId === undefined ? {} : { runId: event.runId }) })
110
+ : invalid(event, current.state);
111
+ case 'reject':
112
+ return current.state === 'pending' ? next(current, event.at, 'skipped', { origin: 'human' }) : invalid(event, current.state);
113
+ case 'round2-cancel':
114
+ return current.state === 'fuse' || current.state === 'approved'
115
+ ? next(current, event.at, 'approved')
116
+ : invalid(event, current.state);
117
+ case 'round2-fail':
118
+ return current.state === 'fuse' || current.state === 'approved'
119
+ ? next(current, event.at, 'approved', { failure: event.failure, attempts: (current.attempts ?? 0) + 1 })
120
+ : invalid(event, current.state);
121
+ case 'round2-success':
122
+ return current.state === 'fuse' || current.state === 'approved'
123
+ ? { ok: true, row: null }
124
+ : invalid(event, current.state);
125
+ case 'restore':
126
+ return current.state === 'skipped'
127
+ ? next(current, event.at, 'verify')
128
+ : invalid(event, current.state);
129
+ }
130
+ }
131
+ //# sourceMappingURL=coordinator.js.map
@@ -0,0 +1,61 @@
1
+ function extractJson(text) {
2
+ const fenced = /\`\`\`(?:json)?\s*([\s\S]*?)\`\`\`/i.exec(text);
3
+ const body = fenced?.[1] ?? text;
4
+ const start = body.indexOf('{');
5
+ if (start < 0)
6
+ return undefined;
7
+ let depth = 0;
8
+ for (let index = start; index < body.length; index += 1) {
9
+ if (body[index] === '{')
10
+ depth += 1;
11
+ else if (body[index] === '}') {
12
+ depth -= 1;
13
+ if (depth === 0) {
14
+ try {
15
+ return JSON.parse(body.slice(start, index + 1));
16
+ }
17
+ catch {
18
+ return undefined;
19
+ }
20
+ }
21
+ }
22
+ }
23
+ return undefined;
24
+ }
25
+ export function parseDecision(text, pair) {
26
+ if (text === undefined || text.trim() === '')
27
+ return { ok: false, code: 'decision-invalid', message: 'empty decision text' };
28
+ const value = extractJson(text);
29
+ if (typeof value !== 'object' || value === null)
30
+ return { ok: false, code: 'decision-invalid', message: 'decision is not a JSON object' };
31
+ const record = value;
32
+ const shouldMerge = record.shouldMerge === true || record.should_merge === true;
33
+ const mode = record.mode;
34
+ if (mode !== 'merge-into-existing' && mode !== 'create-umbrella' && mode !== 'keep') {
35
+ return { ok: false, code: 'decision-invalid', message: 'mode must be merge-into-existing, create-umbrella or keep' };
36
+ }
37
+ const target = typeof record.target === 'string' ? record.target.trim() : '';
38
+ const sources = Array.isArray(record.sources) ? record.sources.filter((item) => typeof item === 'string').map(item => item.trim()).filter(Boolean) : [];
39
+ const rationale = typeof record.rationale === 'string' ? record.rationale.trim() : '';
40
+ if (!shouldMerge || mode === 'keep') {
41
+ return { ok: true, decision: { shouldMerge: false, mode: 'keep', target, sources, rationale } };
42
+ }
43
+ if (target === '')
44
+ return { ok: false, code: 'decision-invalid', message: 'merge decision needs target' };
45
+ if (sources.length === 0)
46
+ return { ok: false, code: 'decision-invalid', message: 'merge decision needs sources' };
47
+ if (!sources.includes(pair.a) || !sources.includes(pair.b))
48
+ return { ok: false, code: 'decision-invalid', message: 'sources must include both pair skills' };
49
+ return {
50
+ ok: true,
51
+ decision: {
52
+ shouldMerge: true,
53
+ mode,
54
+ target,
55
+ sources,
56
+ rationale,
57
+ ...(typeof record.confidence === 'number' ? { confidence: record.confidence } : {}),
58
+ },
59
+ };
60
+ }
61
+ //# sourceMappingURL=decision.js.map
@@ -0,0 +1,50 @@
1
+ /**
2
+ * Merge doctor rows: state, pending, gate cache and staging are each probed read-only.
3
+ * A missing file is an empty feature, not a failure; an unreadable file is a warning the
4
+ * operator can act on. This module never repairs or writes.
5
+ */
6
+ 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 { stagingRoot } from "./staging.js";
11
+ export const MERGE_DOCTOR_SCHEMA_VERSION = 3;
12
+ export async function mergeDoctor(io, home, configuredRoot) {
13
+ const rows = [];
14
+ const loaded = await loadMergeState(io, home);
15
+ const counts = countRows(loaded.state.rows);
16
+ rows.push({
17
+ key: 'state',
18
+ 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),
21
+ });
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' });
25
+ }
26
+ else {
27
+ const int8 = join(modelRoot, 'potion-multilingual-128M-int8');
28
+ const pca = join(modelRoot, 'potion-multilingual-128M-pca64-int8');
29
+ const ok = await io.exists(int8) && await io.exists(pca);
30
+ rows.push({ key: 'models', label: '合并模型', status: ok ? 'ok' : 'warn', detail: ok ? modelRoot : '缺少 int8/pca64:' + modelRoot });
31
+ }
32
+ const runsDir = join(home, 'merge', 'runs');
33
+ const listed = await probeList(io, runsDir);
34
+ rows.push({
35
+ key: 'staging',
36
+ label: '暂存树',
37
+ status: listed.kind === 'unknown' ? 'unknown' : 'ok',
38
+ detail: listed.kind === 'unknown'
39
+ ? '无法列出 ' + runsDir + ':' + listed.reason
40
+ : listed.kind === 'absent' ? '0 个 run 目录' : String(listed.value.length) + ' 个 run 目录',
41
+ });
42
+ rows.push({
43
+ key: 'staging-path',
44
+ label: '暂存路径',
45
+ status: 'ok',
46
+ detail: stagingRoot(home, '<runId>'),
47
+ });
48
+ return rows;
49
+ }
50
+ //# sourceMappingURL=doctor.js.map
@@ -0,0 +1,10 @@
1
+ /** Pure approve-claim lease. No IO. */
2
+ export interface Claim {
3
+ readonly id: string;
4
+ readonly token: string;
5
+ readonly expiresAt: number;
6
+ }
7
+ export declare function acquireClaim(current: Claim | undefined, now: number, leaseMs: number, token: string): Claim | undefined;
8
+ export declare function releaseClaim(current: Claim | undefined, token: string): Claim | undefined;
9
+ export declare function isClaimExpired(current: Claim | undefined, now: number): boolean;
10
+ //# sourceMappingURL=claim.d.ts.map
@@ -0,0 +1,12 @@
1
+ export function acquireClaim(current, now, leaseMs, token) {
2
+ if (current !== undefined && current.expiresAt > now)
3
+ return undefined;
4
+ return { id: 'claim', token, expiresAt: now + Math.max(1, leaseMs) };
5
+ }
6
+ export function releaseClaim(current, token) {
7
+ return current?.token === token ? undefined : current;
8
+ }
9
+ export function isClaimExpired(current, now) {
10
+ return current !== undefined && current.expiresAt <= now;
11
+ }
12
+ //# sourceMappingURL=claim.js.map
@@ -0,0 +1,6 @@
1
+ import type { EvolutionIoLike } from '@lmzhen/dsh-evolution-core';
2
+ import type { CommitJournal } from './commit-journal.ts';
3
+ export declare function journalPath(home: string, runId: string): string;
4
+ export declare function writeJournal(io: EvolutionIoLike, home: string, journal: CommitJournal): Promise<string>;
5
+ export declare function readJournal(io: EvolutionIoLike, home: string, runId: string): Promise<CommitJournal | null>;
6
+ //# sourceMappingURL=commit-journal-store.d.ts.map
@@ -0,0 +1,22 @@
1
+ /** Durable commit journal: <home>/merge/journals/<run>.json. */
2
+ import { join } from 'node:path';
3
+ export function journalPath(home, runId) {
4
+ return join(home, 'merge', 'journals', runId.replace(/[^A-Za-z0-9_-]+/g, '_') + '.json');
5
+ }
6
+ export async function writeJournal(io, home, journal) {
7
+ const path = journalPath(home, journal.runId);
8
+ await io.writeText(path, JSON.stringify(journal, null, 2));
9
+ return path;
10
+ }
11
+ export async function readJournal(io, home, runId) {
12
+ const raw = await io.readText(journalPath(home, runId));
13
+ if (raw === null)
14
+ return null;
15
+ try {
16
+ return JSON.parse(raw);
17
+ }
18
+ catch {
19
+ return null;
20
+ }
21
+ }
22
+ //# sourceMappingURL=commit-journal-store.js.map
@@ -0,0 +1,16 @@
1
+ /** Pure commit-journal phases for target-first merge. */
2
+ export type JournalPhase = 'prepared' | 'target-written' | 'sources-archived' | 'committed' | 'rolled-back' | 'failed';
3
+ export interface CommitJournal {
4
+ readonly runId: string;
5
+ readonly unitId?: string;
6
+ readonly target: string;
7
+ readonly create: boolean;
8
+ readonly sources: readonly string[];
9
+ readonly phase: JournalPhase;
10
+ readonly updatedAt: string;
11
+ }
12
+ export declare function advanceJournal(journal: CommitJournal, phase: JournalPhase, at: string): CommitJournal;
13
+ export type JournalRecovery = 'none' | 'retry-commit' | 'rollback-target' | 'rollback-sources';
14
+ /** What a restart may do with the last durable phase. */
15
+ export declare function recoveryFor(journal: CommitJournal): JournalRecovery;
16
+ //# sourceMappingURL=commit-journal.d.ts.map
@@ -0,0 +1,25 @@
1
+ const NEXT = {
2
+ prepared: ['target-written', 'failed'],
3
+ 'target-written': ['sources-archived', 'rolled-back', 'failed'],
4
+ 'sources-archived': ['committed', 'rolled-back', 'failed'],
5
+ committed: [],
6
+ 'rolled-back': [],
7
+ failed: [],
8
+ };
9
+ export function advanceJournal(journal, phase, at) {
10
+ if (!NEXT[journal.phase].includes(phase))
11
+ throw new Error('invalid journal transition: ' + journal.phase + ' -> ' + phase);
12
+ return { ...journal, phase, updatedAt: at };
13
+ }
14
+ /** What a restart may do with the last durable phase. */
15
+ export function recoveryFor(journal) {
16
+ switch (journal.phase) {
17
+ case 'prepared': return 'retry-commit';
18
+ case 'target-written': return 'rollback-target';
19
+ case 'sources-archived': return 'rollback-sources';
20
+ case 'committed':
21
+ case 'rolled-back':
22
+ case 'failed': return 'none';
23
+ }
24
+ }
25
+ //# sourceMappingURL=commit-journal.js.map
@@ -0,0 +1,10 @@
1
+ /** Pure merge error rendering: parameter errors are two lines, runtime/target failures three. */
2
+ export type MergeErrorKind = 'parameter' | 'runtime' | 'target' | 'approval';
3
+ export interface MergeError {
4
+ readonly kind: MergeErrorKind;
5
+ readonly code: string;
6
+ readonly message: string;
7
+ readonly detail?: string;
8
+ }
9
+ export declare function formatMergeError(error: MergeError): string;
10
+ //# sourceMappingURL=errors.d.ts.map
@@ -0,0 +1,7 @@
1
+ export function formatMergeError(error) {
2
+ if (error.kind === 'parameter')
3
+ return 'merge: ' + error.message + ' (' + error.code + ')';
4
+ const detail = '\n' + (error.detail ?? 'no additional detail');
5
+ return 'merge failed: ' + error.message + ' (' + error.code + ')' + detail + '\nnext: fix the target or rerun after the cause is gone';
6
+ }
7
+ //# sourceMappingURL=errors.js.map
@@ -0,0 +1,25 @@
1
+ /** Pure failure routing for merge errors. */
2
+ export type MergeFailureRoute = {
3
+ readonly kind: 'parameter';
4
+ readonly reportOnly: false;
5
+ } | {
6
+ readonly kind: 'source-gone';
7
+ readonly reportOnly: true;
8
+ } | {
9
+ readonly kind: 'target-taken';
10
+ readonly reportOnly: true;
11
+ } | {
12
+ readonly kind: 'validation';
13
+ readonly reportOnly: true;
14
+ } | {
15
+ readonly kind: 'commit';
16
+ readonly reportOnly: true;
17
+ } | {
18
+ readonly kind: 'schema';
19
+ readonly reportOnly: true;
20
+ } | {
21
+ readonly kind: 'runtime';
22
+ readonly reportOnly: true;
23
+ };
24
+ export declare function routeFailure(code: string): MergeFailureRoute;
25
+ //# sourceMappingURL=failure-route.d.ts.map
@@ -0,0 +1,22 @@
1
+ const PARAMETER = new Set(['bad-pair', 'bad-target', 'bad-flag', 'usage']);
2
+ const SOURCE_GONE = new Set(['source-gone', 'source-missing']);
3
+ const TARGET_TAKEN = new Set(['target-taken', 'target-exists']);
4
+ const VALIDATION = new Set(['validation-failed', 'invalid-decision']);
5
+ const COMMIT = new Set(['commit-failed', 'commit-refused']);
6
+ const SCHEMA = new Set(['schema-invalid', 'schema-version']);
7
+ export function routeFailure(code) {
8
+ if (PARAMETER.has(code))
9
+ return { kind: 'parameter', reportOnly: false };
10
+ if (SOURCE_GONE.has(code))
11
+ return { kind: 'source-gone', reportOnly: true };
12
+ if (TARGET_TAKEN.has(code))
13
+ return { kind: 'target-taken', reportOnly: true };
14
+ if (VALIDATION.has(code))
15
+ return { kind: 'validation', reportOnly: true };
16
+ if (COMMIT.has(code))
17
+ return { kind: 'commit', reportOnly: true };
18
+ if (SCHEMA.has(code))
19
+ return { kind: 'schema', reportOnly: true };
20
+ return { kind: 'runtime', reportOnly: true };
21
+ }
22
+ //# sourceMappingURL=failure-route.js.map
@@ -0,0 +1,21 @@
1
+ /** Pure pairKey result index and archived-history projection. No IO. */
2
+ export interface PairIndexEntryV2 {
3
+ readonly pairKey: string;
4
+ readonly runId: string;
5
+ readonly result: 'merged' | 'kept' | 'failed' | 'cancelled' | 'undone';
6
+ readonly target?: string;
7
+ readonly reportRef?: string;
8
+ }
9
+ export type HistoryKind = 'merged' | 'deleted' | 'split' | 'other';
10
+ export interface HistoryRecord {
11
+ readonly name: string;
12
+ readonly kind: HistoryKind;
13
+ readonly reason: string;
14
+ readonly lastVersion: number;
15
+ readonly archivedAt: string;
16
+ readonly sourceRunId?: string;
17
+ }
18
+ export declare function putPair(index: ReadonlyMap<string, PairIndexEntryV2>, entry: PairIndexEntryV2): Map<string, PairIndexEntryV2>;
19
+ export declare function historyRecord(input: HistoryRecord): HistoryRecord;
20
+ export declare function summarizeHistory(records: readonly HistoryRecord[]): Record<HistoryKind, number>;
21
+ //# sourceMappingURL=indexes.d.ts.map