@lmzhen/dsh-evolution-merge 0.30.18 → 0.30.20
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.
- package/README.md +21 -4
- package/data/minhash_params.json +1063 -0
- package/lib/index.js +7313 -3272
- package/lib/types/adapters/channel.d.ts +1 -0
- package/lib/types/adapters/channel.js +15 -1
- package/lib/types/adapters/engine-screen.d.ts +1 -1
- package/lib/types/adapters/engine-screen.js +5 -2
- package/lib/types/adapters/model-runner.d.ts +0 -5
- package/lib/types/adapters/model-runner.js +8 -2
- package/lib/types/adapters/model.js +2 -1
- package/lib/types/adapters/select.js +12 -8
- package/lib/types/approve-command.d.ts +12 -1
- package/lib/types/approve-command.js +39 -6
- package/lib/types/authoring-flow.d.ts +16 -0
- package/lib/types/authoring-flow.js +37 -13
- package/lib/types/channel-port.d.ts +3 -1
- package/lib/types/channel-port.js +8 -3
- package/lib/types/command-defs.d.ts +5 -0
- package/lib/types/command-defs.js +33 -0
- package/lib/types/command-surface.d.ts +1 -1
- package/lib/types/command-surface.js +10 -43
- package/lib/types/commands.d.ts +3 -4
- package/lib/types/commands.js +33 -72
- package/lib/types/commit-assembly.d.ts +14 -1
- package/lib/types/commit-assembly.js +65 -9
- package/lib/types/commit-execute.d.ts +7 -1
- package/lib/types/commit-execute.js +54 -22
- package/lib/types/commit-plan.d.ts +7 -1
- package/lib/types/commit-plan.js +4 -0
- package/lib/types/commit-receipt.d.ts +3 -0
- package/lib/types/contracts.d.ts +43 -0
- package/lib/types/doctor.d.ts +11 -1
- package/lib/types/doctor.js +81 -27
- package/lib/types/domain/authoring-manifest.d.ts +26 -0
- package/lib/types/domain/authoring-manifest.js +43 -0
- package/lib/types/domain/engine-criteria.d.ts +10 -0
- package/lib/types/domain/engine-criteria.js +11 -0
- package/lib/types/domain/engine-input.d.ts +10 -0
- package/lib/types/domain/engine-input.js +26 -0
- package/lib/types/domain/errors.d.ts +15 -1
- package/lib/types/domain/errors.js +23 -4
- package/lib/types/domain/evidence-hash.d.ts +3 -0
- package/lib/types/domain/evidence-hash.js +14 -0
- package/lib/types/domain/failure-route.d.ts +14 -0
- package/lib/types/domain/failure-route.js +12 -0
- package/lib/types/domain/live-facts.d.ts +31 -0
- package/lib/types/domain/live-facts.js +54 -0
- package/lib/types/domain/merge-limits.d.ts +18 -0
- package/lib/types/domain/merge-limits.js +22 -0
- package/lib/types/domain/merge-report.d.ts +41 -0
- package/lib/types/domain/merge-report.js +2 -0
- package/lib/types/domain/merge-snapshot.d.ts +99 -0
- package/lib/types/domain/merge-snapshot.js +82 -0
- package/lib/types/domain/pair-ledger.d.ts +34 -0
- package/lib/types/domain/pair-ledger.js +96 -0
- package/lib/types/domain/pre-1b-backup.d.ts +43 -0
- package/lib/types/domain/pre-1b-backup.js +140 -0
- package/lib/types/domain/provider-key.d.ts +2 -0
- package/lib/types/domain/provider-key.js +11 -0
- package/lib/types/domain/read-probe.d.ts +5 -0
- package/lib/types/domain/read-probe.js +11 -0
- package/lib/types/domain/reconcile.d.ts +47 -0
- package/lib/types/domain/reconcile.js +178 -0
- package/lib/types/domain/resolve-pair.d.ts +2 -9
- package/lib/types/domain/resolve-pair.js +2 -15
- package/lib/types/domain/row-v4.d.ts +148 -0
- package/lib/types/domain/row-v4.js +159 -0
- package/lib/types/domain/run-coordinator.d.ts +51 -0
- package/lib/types/domain/run-coordinator.js +45 -0
- package/lib/types/domain/schema4.d.ts +53 -0
- package/lib/types/domain/schema4.js +129 -0
- package/lib/types/domain/selection-policy.d.ts +14 -0
- package/lib/types/domain/selection-policy.js +6 -0
- package/lib/types/domain/skipped-expiry.d.ts +5 -0
- package/lib/types/domain/skipped-expiry.js +16 -0
- package/lib/types/domain/stage-aggregate.d.ts +96 -0
- package/lib/types/domain/stage-aggregate.js +293 -0
- package/lib/types/domain/staging-index.d.ts +1 -0
- package/lib/types/domain/state-bootstrap.d.ts +43 -0
- package/lib/types/domain/state-bootstrap.js +267 -0
- package/lib/types/domain/state-v3.d.ts +3 -14
- package/lib/types/domain/state-v3.js +3 -79
- package/lib/types/domain/undo-journal.d.ts +19 -0
- package/lib/types/domain/undo-journal.js +20 -0
- package/lib/types/domain/undo-receipt.d.ts +18 -0
- package/lib/types/domain/undo-receipt.js +22 -0
- package/lib/types/domain/validation-plan.d.ts +65 -0
- package/lib/types/domain/validation-plan.js +49 -0
- package/lib/types/facts.d.ts +3 -1
- package/lib/types/facts.js +13 -16
- package/lib/types/index.d.ts +2 -0
- package/lib/types/index.js +181 -68
- package/lib/types/merge-command.d.ts +3 -1
- package/lib/types/merge-command.js +10 -6
- package/lib/types/merge-routes.d.ts +10 -5
- package/lib/types/merge-routes.js +72 -7
- package/lib/types/merge-tool.d.ts +1 -1
- package/lib/types/merge-tool.js +2 -2
- package/lib/types/metrics.js +1 -1
- package/lib/types/new-child-authoring.d.ts +2 -0
- package/lib/types/new-child-authoring.js +57 -10
- package/lib/types/params.d.ts +2 -4
- package/lib/types/params.js +13 -0
- package/lib/types/pending-actions.d.ts +2 -2
- package/lib/types/pending-actions.js +4 -4
- package/lib/types/prompts.d.ts +2 -4
- package/lib/types/prompts.js +21 -8
- package/lib/types/read-commands.d.ts +3 -3
- package/lib/types/read-commands.js +19 -28
- package/lib/types/recovery-coordinator.d.ts +34 -0
- package/lib/types/recovery-coordinator.js +84 -0
- package/lib/types/recovery-projection.d.ts +31 -0
- package/lib/types/recovery-projection.js +96 -0
- package/lib/types/resume-command.d.ts +5 -1
- package/lib/types/resume-command.js +46 -13
- package/lib/types/revision.d.ts +17 -0
- package/lib/types/revision.js +39 -3
- package/lib/types/round2-outcome.d.ts +2 -1
- package/lib/types/round2-outcome.js +59 -11
- package/lib/types/run-shell.d.ts +16 -2
- package/lib/types/run-shell.js +49 -5
- package/lib/types/runtime.d.ts +0 -5
- package/lib/types/runtime.js +274 -89
- package/lib/types/scan-command.d.ts +1 -0
- package/lib/types/scan-command.js +2 -0
- package/lib/types/scan-ports.d.ts +18 -0
- package/lib/types/scan-ports.js +49 -23
- package/lib/types/scan-run.d.ts +17 -0
- package/lib/types/scan-run.js +143 -48
- package/lib/types/stage-tool-registry.d.ts +7 -0
- package/lib/types/stage-tool-registry.js +12 -1
- package/lib/types/state.d.ts +5 -5
- package/lib/types/state.js +22 -69
- package/lib/types/status.d.ts +6 -1
- package/lib/types/status.js +47 -0
- package/lib/types/surface/body.d.ts +4 -0
- package/lib/types/surface/body.js +16 -0
- package/lib/types/surface/merge-view.d.ts +14 -0
- package/lib/types/surface/merge-view.js +126 -0
- package/lib/types/undo-operation.d.ts +24 -0
- package/lib/types/undo-operation.js +135 -0
- package/lib/types/wire.d.ts +22 -3
- package/package.json +6 -4
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { HOST_ROUTES, json, methodNotAllowed } from '@lmzhen/dsh-evolution-core/http-routes';
|
|
2
|
-
import { buildFacts } from "./facts.js";
|
|
2
|
+
import { buildFacts as buildLegacyFacts } from "./facts.js";
|
|
3
3
|
import { toBodyView, toHistoryView, toQueuePage } from "./wire-projection.js";
|
|
4
|
+
import { buildBodyView, buildFacts as buildSnapshotFacts, buildHistoryView, buildQueuePage } from "./surface/merge-view.js";
|
|
4
5
|
const MERGE_PATHS = HOST_ROUTES.merge;
|
|
5
6
|
const MERGE_BASE = MERGE_PATHS.view.slice(0, -'view'.length);
|
|
6
7
|
export const MERGE_ROUTES = {
|
|
@@ -11,6 +12,13 @@ export const MERGE_ROUTES = {
|
|
|
11
12
|
};
|
|
12
13
|
export function makeMergeRoutes(deps, _audit) {
|
|
13
14
|
const absent = () => json(404, { wireVersion: 1, code: 'E-325', message: 'merge service is not mounted' });
|
|
15
|
+
const blocked = (readiness) => json(503, { wireVersion: 1, code: 'merge-state-blocked', readiness });
|
|
16
|
+
const notReady = async (source) => {
|
|
17
|
+
if (source.readiness === undefined)
|
|
18
|
+
return null;
|
|
19
|
+
const readiness = await source.readiness();
|
|
20
|
+
return readiness.ok ? null : blocked(readiness);
|
|
21
|
+
};
|
|
14
22
|
const bad = (message) => json(400, { wireVersion: 1, code: 'merge-bad-query', message });
|
|
15
23
|
const get = (request) => (request.method === 'GET' ? null : methodNotAllowed(['GET']));
|
|
16
24
|
const url = (request) => new URL(request.url, 'http://local');
|
|
@@ -26,10 +34,19 @@ export function makeMergeRoutes(deps, _audit) {
|
|
|
26
34
|
const source = deps.getSource();
|
|
27
35
|
if (source === undefined)
|
|
28
36
|
return absent();
|
|
37
|
+
if (source.snapshot !== undefined) {
|
|
38
|
+
const snapshot = await source.snapshot();
|
|
39
|
+
if (snapshot.blocked !== undefined)
|
|
40
|
+
return json(503, { wireVersion: 1, code: 'merge-state-blocked', readiness: snapshot.blocked.readiness });
|
|
41
|
+
return json(200, buildSnapshotFacts(snapshot));
|
|
42
|
+
}
|
|
43
|
+
const readiness = await notReady(source);
|
|
44
|
+
if (readiness !== null)
|
|
45
|
+
return readiness;
|
|
29
46
|
const readAt = Date.now();
|
|
30
|
-
const rows = await source.rows();
|
|
31
|
-
const runs = await source.runs(undefined, 20);
|
|
32
|
-
return json(200,
|
|
47
|
+
const rows = await source.rows?.() ?? [];
|
|
48
|
+
const runs = await source.runs?.(undefined, 20) ?? [];
|
|
49
|
+
return json(200, buildLegacyFacts({ rows, runs, approval: source.approval?.() === true, readAt }));
|
|
33
50
|
},
|
|
34
51
|
},
|
|
35
52
|
{
|
|
@@ -51,8 +68,22 @@ export function makeMergeRoutes(deps, _audit) {
|
|
|
51
68
|
if (expiredOnly && queue !== 'pending')
|
|
52
69
|
return bad('expired is only allowed for pending');
|
|
53
70
|
const limit = Number.parseInt(query.get('limit') ?? '50', 10);
|
|
71
|
+
if (source.snapshot !== undefined) {
|
|
72
|
+
const snapshot = await source.snapshot();
|
|
73
|
+
if (snapshot.blocked !== undefined)
|
|
74
|
+
return json(503, { wireVersion: 1, code: 'merge-state-blocked', readiness: snapshot.blocked.readiness });
|
|
75
|
+
try {
|
|
76
|
+
return json(200, buildQueuePage(snapshot, queue, expiredOnly, query.get('cursor') ?? undefined, Number.isFinite(limit) ? limit : 50));
|
|
77
|
+
}
|
|
78
|
+
catch (error) {
|
|
79
|
+
return bad(error instanceof Error ? error.message : 'queue cursor is stale');
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
const readiness = await notReady(source);
|
|
83
|
+
if (readiness !== null)
|
|
84
|
+
return readiness;
|
|
54
85
|
const offset = Number.parseInt(query.get('cursor') ?? '0', 10);
|
|
55
|
-
const rows = await source.rows();
|
|
86
|
+
const rows = await source.rows?.() ?? [];
|
|
56
87
|
const page = toQueuePage(rows, queue, expiredOnly, Date.now(), Number.isFinite(limit) ? limit : 50);
|
|
57
88
|
return json(200, Number.isFinite(offset) && offset > 0 ? { ...page, rows: page.rows.slice(offset) } : page);
|
|
58
89
|
},
|
|
@@ -70,10 +101,35 @@ export function makeMergeRoutes(deps, _audit) {
|
|
|
70
101
|
return absent();
|
|
71
102
|
const query = url(request).searchParams;
|
|
72
103
|
const id = query.get('id');
|
|
104
|
+
const name = query.get('name');
|
|
73
105
|
const side = query.get('side');
|
|
106
|
+
if (id !== null && name !== null)
|
|
107
|
+
return bad('id and name are mutually exclusive');
|
|
108
|
+
if ((id === null || id === '') && (name === null || name === ''))
|
|
109
|
+
return json(404, { wireVersion: 1, code: 'merge-body-not-found', message: 'merge body requires id or name' });
|
|
110
|
+
if (source.snapshot !== undefined) {
|
|
111
|
+
const snapshot = await source.snapshot();
|
|
112
|
+
if (snapshot.blocked !== undefined)
|
|
113
|
+
return json(503, { wireVersion: 1, code: 'merge-state-blocked', readiness: snapshot.blocked.readiness });
|
|
114
|
+
const refs = id !== null && id !== '' ? snapshot.bodyRefs.filter(ref => ref.rowId === id) : snapshot.bodyRefs.filter(ref => ref.name === name);
|
|
115
|
+
if (refs.length === 0)
|
|
116
|
+
return json(404, { wireVersion: 1, code: 'merge-body-not-found', message: 'merge body not found' });
|
|
117
|
+
if (name !== null && refs.length > 1)
|
|
118
|
+
return bad('name matches multiple merge rows');
|
|
119
|
+
const row = snapshot.rows.find(item => item.row.id === refs[0].rowId);
|
|
120
|
+
if (row?.stage?.state === 'lost' || row?.stage?.state === 'expired')
|
|
121
|
+
return json(404, { wireVersion: 1, code: 'merge-body-not-found', message: 'merge stage is lost' });
|
|
122
|
+
const sideFilter = side === 'a' ? 'live' : side === 'b' ? 'archive' : side === 'live' || side === 'archive' ? side : undefined;
|
|
123
|
+
const ref = (sideFilter === undefined ? refs[0] : refs.find(item => item.side === sideFilter)) ?? refs[0];
|
|
124
|
+
const content = source.bodyRef === undefined ? null : await source.bodyRef(ref);
|
|
125
|
+
return json(200, buildBodyView(snapshot, ref, content ?? ''));
|
|
126
|
+
}
|
|
127
|
+
const readiness = await notReady(source);
|
|
128
|
+
if (readiness !== null)
|
|
129
|
+
return readiness;
|
|
74
130
|
if (id === null || id === '' || (side !== 'a' && side !== 'b'))
|
|
75
131
|
return bad('id and side=a|b are required');
|
|
76
|
-
const content = await source.body(id, side);
|
|
132
|
+
const content = await source.body?.(id, side) ?? null;
|
|
77
133
|
return content === null
|
|
78
134
|
? json(200, { wireVersion: 1, code: 'merge-body-missing', message: 'merge body not found: ' + id })
|
|
79
135
|
: json(200, toBodyView(id, side === 'a' ? 'live' : 'archive', content, Date.now()));
|
|
@@ -90,7 +146,16 @@ export function makeMergeRoutes(deps, _audit) {
|
|
|
90
146
|
const source = deps.getSource();
|
|
91
147
|
if (source === undefined)
|
|
92
148
|
return absent();
|
|
93
|
-
|
|
149
|
+
if (source.snapshot !== undefined) {
|
|
150
|
+
const snapshot = await source.snapshot();
|
|
151
|
+
if (snapshot.blocked !== undefined)
|
|
152
|
+
return json(503, { wireVersion: 1, code: 'merge-state-blocked', readiness: snapshot.blocked.readiness });
|
|
153
|
+
return json(200, buildHistoryView(snapshot));
|
|
154
|
+
}
|
|
155
|
+
const readiness = await notReady(source);
|
|
156
|
+
if (readiness !== null)
|
|
157
|
+
return readiness;
|
|
158
|
+
return json(200, toHistoryView(await source.history?.() ?? [], Date.now()));
|
|
94
159
|
},
|
|
95
160
|
},
|
|
96
161
|
];
|
|
@@ -18,5 +18,5 @@ export interface ToolDefinitionLike {
|
|
|
18
18
|
};
|
|
19
19
|
readonly execute: (args: Record<string, unknown>, exec: unknown) => Promise<string>;
|
|
20
20
|
}
|
|
21
|
-
export declare function createMergeStageTool(io: EvolutionIoLike, home: string, context: (exec: unknown) => StageToolContext | undefined): ToolDefinitionLike;
|
|
21
|
+
export declare function createMergeStageTool(io: EvolutionIoLike, home: string, context: (exec: unknown) => StageToolContext | undefined, diagnose?: (exec: unknown) => unknown): ToolDefinitionLike;
|
|
22
22
|
//# sourceMappingURL=merge-tool.d.ts.map
|
package/lib/types/merge-tool.js
CHANGED
|
@@ -33,7 +33,7 @@ function operation(args) {
|
|
|
33
33
|
}
|
|
34
34
|
return shapeError('op must be list/read/write/copy');
|
|
35
35
|
}
|
|
36
|
-
export function createMergeStageTool(io, home, context) {
|
|
36
|
+
export function createMergeStageTool(io, home, context, diagnose) {
|
|
37
37
|
return {
|
|
38
38
|
name: MERGE_STAGE_TOOL_NAME,
|
|
39
39
|
description: 'Read source copies and write the merged skill into the staging tree. list/read use area=sources|merged plus source (for sources); write/copy paths are relative to merged/<target> (for example path=SKILL.md, from=SKILL.md, to=references/x.md). Write/copy are only allowed in the author phase.',
|
|
@@ -61,7 +61,7 @@ export function createMergeStageTool(io, home, context) {
|
|
|
61
61
|
return JSON.stringify(op);
|
|
62
62
|
const ctx = context(exec);
|
|
63
63
|
if (ctx === undefined)
|
|
64
|
-
return JSON.stringify({ ok: false, code: 'not-merge-child', message: 'merge_stage is only available to an active merge child' });
|
|
64
|
+
return JSON.stringify({ ok: false, code: 'not-merge-child', message: 'merge_stage is only available to an active merge child', ...(diagnose === undefined ? {} : { diagnostic: diagnose(exec) }) });
|
|
65
65
|
return JSON.stringify(await applyStageTool(io, home, ctx, op));
|
|
66
66
|
},
|
|
67
67
|
};
|
package/lib/types/metrics.js
CHANGED
|
@@ -14,7 +14,7 @@ export function buildMergeMetrics(rows, runs, counters = {}) {
|
|
|
14
14
|
samples.push({ name: 'merge_queue_depth', labels: { queue, kind: 'active' }, value: counts.active[queue] });
|
|
15
15
|
samples.push({ name: 'merge_queue_depth', labels: { queue: 'pending', kind: 'expired' }, value: counts.expired });
|
|
16
16
|
for (const run of runs) {
|
|
17
|
-
const state = run.
|
|
17
|
+
const state = run.state;
|
|
18
18
|
samples.push({ name: 'merge_run_total', labels: { state }, value: 1 });
|
|
19
19
|
if (run.endedAt !== undefined)
|
|
20
20
|
samples.push({ name: 'merge_run_duration_ms', labels: { state }, value: Math.max(0, run.endedAt - run.startedAt) });
|
|
@@ -25,7 +25,9 @@ export interface NewChildAuthoringOptions {
|
|
|
25
25
|
readonly onChild?: (context: StageToolContext) => void;
|
|
26
26
|
readonly onChildClosed?: (childId: string) => void;
|
|
27
27
|
readonly unit?: UnitTransaction | undefined;
|
|
28
|
+
readonly limits?: import('@lmzhen/dsh-evolution-core').SkillLimits;
|
|
28
29
|
}
|
|
29
30
|
export declare function runNewChildAuthoring(options: NewChildAuthoringOptions, row: MergeRow, decision: MergeDecision, signal: AbortSignal): Promise<ScanRound2Outcome>;
|
|
31
|
+
/** Bind the round-2 port to one run: approve/resume open a NEW child per run. */
|
|
30
32
|
export declare function createNewChildRound2(options: Omit<NewChildAuthoringOptions, 'runId'>): (row: MergeRow, decision: MergeDecision, signal: AbortSignal, runId: string) => Promise<ScanRound2Outcome>;
|
|
31
33
|
//# sourceMappingURL=new-child-authoring.d.ts.map
|
|
@@ -1,10 +1,20 @@
|
|
|
1
1
|
import { buildRound2Prompt, STANDBY_PROMPT } from "./prompts.js";
|
|
2
|
-
import {
|
|
2
|
+
import { stageKeyOf, stagingRoot } from "./staging.js";
|
|
3
|
+
import { createStageTree, readStageEntry, readStageTree, setStageTreeTarget, dropStage } from "./domain/stage-aggregate.js";
|
|
3
4
|
import { pairKey } from "./contracts.js";
|
|
4
5
|
import { readStagedTree } from "./staged-tree.js";
|
|
5
6
|
import { validateStagedTree, hasBlockingFinding } from "./rules.js";
|
|
7
|
+
import { DEFAULT_SKILL_LIMITS } from '@lmzhen/dsh-evolution-core';
|
|
8
|
+
import { limitsSnapshotOf } from "./domain/merge-limits.js";
|
|
9
|
+
import { buildValidationPlan } from "./domain/validation-plan.js";
|
|
6
10
|
import { commitStagedRun } from "./commit-execute.js";
|
|
11
|
+
import { manifestFilesOf, missingManifestFile, parseAuthoringManifest } from "./domain/authoring-manifest.js";
|
|
12
|
+
import { MERGE_PROMPT_VERSION, promptHashOf } from "./prompts.js";
|
|
13
|
+
/** Commit-side causes that mean "the world moved"; they must not be retried blindly. */
|
|
14
|
+
const STALE_CAUSES = new Set(['target-changed', 'source-changed', 'support-changed', 'target-taken', 'target-gone']);
|
|
7
15
|
export async function runNewChildAuthoring(options, row, decision, signal) {
|
|
16
|
+
if (decision.target.includes('/') || decision.target.includes(':'))
|
|
17
|
+
return { kind: 'failed', failure: { stage: 'author', code: 'target-invalid', message: 'target must be a bare skill name: ' + decision.target, retryable: false, retryFrom: 'none' } };
|
|
8
18
|
const targetCard = options.cards.find(card => card.name === decision.target);
|
|
9
19
|
const createMode = decision.mode === 'create-umbrella';
|
|
10
20
|
if (!createMode && targetCard !== undefined) {
|
|
@@ -15,13 +25,24 @@ export async function runNewChildAuthoring(options, row, decision, signal) {
|
|
|
15
25
|
}
|
|
16
26
|
if (!createMode && await options.isBlocked?.(decision.target) === true)
|
|
17
27
|
return { kind: 'failed', failure: { stage: 'author', code: 'target-blocked', message: 'target is blocked by curator: ' + decision.target, retryable: false, retryFrom: 'none' } };
|
|
28
|
+
const at = options.now();
|
|
29
|
+
// NFR-06: an expired stage is never reused and never re-presented. Drop its tree and
|
|
30
|
+
// index and author into a fresh one; the recorded decision anchors still guard the
|
|
31
|
+
// sources at commit, so re-staging cannot smuggle a stale merge through.
|
|
32
|
+
const stageEntry = await readStageEntry(options.io, options.home, row.id);
|
|
33
|
+
const expiredStage = stageEntry !== null && stageEntry.expiresAt <= at;
|
|
34
|
+
// The row's own staging ref wins; otherwise this run authors into its own stage key.
|
|
35
|
+
// The index entry is consulted for TTL only — switching the authoring key to it would
|
|
36
|
+
// break the approve flow, where the reserved entry and the run's tree are separate.
|
|
18
37
|
const stageKey = row.stagingRef ?? stageKeyOf(options.runId, pairKey(row.a, row.b));
|
|
19
|
-
|
|
38
|
+
if (expiredStage && stageEntry !== null)
|
|
39
|
+
await dropStage(options.io, options.home, row.id, stageEntry.ref);
|
|
40
|
+
const existing = expiredStage ? { kind: 'absent' } : await readStageTree(options.io, options.home, stageKey);
|
|
20
41
|
if (existing.kind === 'present') {
|
|
21
|
-
await
|
|
42
|
+
await setStageTreeTarget(options.io, options.home, stageKey, decision.target, decision.mode);
|
|
22
43
|
}
|
|
23
44
|
else {
|
|
24
|
-
const staging = await
|
|
45
|
+
const staging = await createStageTree(options.io, options.home, {
|
|
25
46
|
runId: stageKey,
|
|
26
47
|
target: decision.target,
|
|
27
48
|
mode: decision.mode,
|
|
@@ -39,7 +60,7 @@ export async function runNewChildAuthoring(options, row, decision, signal) {
|
|
|
39
60
|
parent: options.parent,
|
|
40
61
|
label: 'merge-round2',
|
|
41
62
|
promptText: STANDBY_PROMPT,
|
|
42
|
-
kind: '
|
|
63
|
+
kind: 'reviewer',
|
|
43
64
|
allow: options.allow,
|
|
44
65
|
cwd: stagingRoot(options.home, stageKey),
|
|
45
66
|
timeoutMs: options.timeoutMs,
|
|
@@ -62,21 +83,46 @@ export async function runNewChildAuthoring(options, row, decision, signal) {
|
|
|
62
83
|
const tree = await readStagedTree(options.io, options.home, stageKey);
|
|
63
84
|
if (tree.kind !== 'present')
|
|
64
85
|
return { kind: 'failed', failure: { stage: 'author', code: 'staging-missing', message: tree.kind === 'unknown' ? tree.reason : 'staged tree absent', retryable: true, retryFrom: 'agent2' } };
|
|
65
|
-
const
|
|
66
|
-
|
|
67
|
-
|
|
86
|
+
const manifest = parseAuthoringManifest(turn.turn.text);
|
|
87
|
+
const stagedPaths = tree.value.files.map(file => file.path);
|
|
88
|
+
const manifestFiles = manifestFilesOf(manifest, stagedPaths);
|
|
89
|
+
const missing = missingManifestFile(manifestFiles, stagedPaths);
|
|
90
|
+
if (missing !== null)
|
|
91
|
+
return { kind: 'failed', failure: { stage: 'author', code: 'merge-manifest-missing', message: 'manifest file missing from staged tree: ' + missing, retryable: true, retryFrom: 'agent2' } };
|
|
92
|
+
const limitsSnapshot = limitsSnapshotOf(options.limits ?? DEFAULT_SKILL_LIMITS);
|
|
93
|
+
const validationPlan = buildValidationPlan({ tree: tree.value, target: decision.target, limits: limitsSnapshot, cards: options.cards, promptVersion: MERGE_PROMPT_VERSION, promptHash: promptHashOf(buildRound2Prompt(row, decision)), supportFiles: tree.value.files.map(file => ({ path: file.path, content: file.content, source: 'staged' })), manifestFiles, existingSupport: (options.cards.find(card => card.name === decision.target)?.files ?? []).map(file => file.path), supportSources: options.cards.flatMap(card => (card.files ?? []).map(file => ({ path: file.path, content: file.content, source: card.name }))) });
|
|
94
|
+
const findings = [...validateStagedTree(tree.value, limitsSnapshot.skillLimits), ...validationPlan.findings];
|
|
95
|
+
if (hasBlockingFinding(findings)) {
|
|
96
|
+
const lengths = { bodyBytes: Buffer.byteLength(tree.value.body, 'utf8'), fileBytes: tree.value.files.reduce((sum, file) => sum + Buffer.byteLength(file.content, 'utf8'), 0), totalBytes: Buffer.byteLength(tree.value.body, 'utf8') + tree.value.files.reduce((sum, file) => sum + Buffer.byteLength(file.content, 'utf8'), 0) };
|
|
97
|
+
// FR-EXEC-11/14: only a LIMIT finding is over-limit; every other blocking rule
|
|
98
|
+
// (manifest mismatch, support conflict, validation plan) is a validation block and
|
|
99
|
+
// must land in skipped, not bounce back to fuse as a retryable generic failure.
|
|
100
|
+
const limit = findings.find(finding => finding.rule === 'limit');
|
|
101
|
+
return limit === undefined
|
|
102
|
+
? { kind: 'failed', failure: { stage: 'validate', code: 'merge-validation', message: 'staged tree failed the rule gate', retryable: false, retryFrom: 'none' } }
|
|
103
|
+
: { kind: 'failed', failure: { stage: 'validate', code: 'merge-over-limit', message: limit.message, retryable: false, retryFrom: 'none', lengths } };
|
|
104
|
+
}
|
|
68
105
|
const commit = await commitStagedRun(options.io, options.home, stageKey, options.commitPort, {
|
|
69
106
|
origin: 'foreground', summary: 'merge ' + row.a + ' + ' + row.b, now: options.now,
|
|
70
107
|
...(options.isBlocked === undefined ? {} : { isBlocked: options.isBlocked }),
|
|
71
108
|
...(options.unit === undefined ? {} : { unit: options.unit }),
|
|
109
|
+
...(options.limits === undefined ? {} : { limits: options.limits }),
|
|
110
|
+
validationPlan: { planVersion: validationPlan.planVersion, stageDigest: validationPlan.stageDigest, revision: validationPlan.revision },
|
|
111
|
+
...(decision.targetRevisionRef === undefined ? {} : { targetRevision: decision.targetRevisionRef }),
|
|
112
|
+
...(decision.sourceRevisions === undefined ? {} : { sourceRevisions: decision.sourceRevisions }),
|
|
113
|
+
...(decision.supportDigest === undefined ? {} : { supportDigest: decision.supportDigest }),
|
|
72
114
|
});
|
|
73
|
-
if (!commit.ok)
|
|
74
|
-
|
|
115
|
+
if (!commit.ok) {
|
|
116
|
+
const code = commit.code ?? 'commit-failed';
|
|
117
|
+
return { kind: 'failed', failure: { stage: 'commit', code, message: commit.message, retryable: !STALE_CAUSES.has(code), retryFrom: STALE_CAUSES.has(code) ? 'none' : 'agent2' } };
|
|
118
|
+
}
|
|
75
119
|
const receipt = commit.receipt;
|
|
120
|
+
const lengths = { bodyBytes: Buffer.byteLength(tree.value.body, 'utf8'), fileBytes: tree.value.files.reduce((sum, file) => sum + Buffer.byteLength(file.content, 'utf8'), 0), totalBytes: Buffer.byteLength(tree.value.body, 'utf8') + tree.value.files.reduce((sum, file) => sum + Buffer.byteLength(file.content, 'utf8'), 0) };
|
|
76
121
|
return {
|
|
77
122
|
kind: 'merged',
|
|
78
123
|
...(receipt?.unitId === undefined ? {} : { unitId: receipt.unitId }),
|
|
79
124
|
archived: receipt?.archived ?? [],
|
|
125
|
+
lengths,
|
|
80
126
|
target: { requested: decision.target, actual: commit.plan.target, created: receipt?.create ?? false },
|
|
81
127
|
};
|
|
82
128
|
}
|
|
@@ -88,6 +134,7 @@ export async function runNewChildAuthoring(options, row, decision, signal) {
|
|
|
88
134
|
options.onChildClosed?.(opened.childId);
|
|
89
135
|
}
|
|
90
136
|
}
|
|
137
|
+
/** Bind the round-2 port to one run: approve/resume open a NEW child per run. */
|
|
91
138
|
export function createNewChildRound2(options) {
|
|
92
139
|
return (row, decision, signal, runId) => runNewChildAuthoring({ ...options, runId }, row, decision, signal);
|
|
93
140
|
}
|
package/lib/types/params.d.ts
CHANGED
|
@@ -1,13 +1,11 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Merge parameter resolution. E2/E3 values arrive as one JSON object; this is the only
|
|
3
|
-
* validator. Invalid values throw loudly (no silent clamp), matching the screening contract.
|
|
4
|
-
*/
|
|
5
1
|
export interface MergeParams {
|
|
6
2
|
readonly mergeApproval: boolean;
|
|
7
3
|
readonly mergeSkippedTtlDays: number;
|
|
8
4
|
readonly mergeRunTimeoutMinutes: number;
|
|
9
5
|
readonly mergePairTimeoutMinutes: number;
|
|
10
6
|
readonly mergeModelsRoot: string;
|
|
7
|
+
readonly platformWrite?: boolean;
|
|
8
|
+
readonly limitsSnapshot?: import('./domain/merge-limits.ts').MergeLimitsSnapshot;
|
|
11
9
|
}
|
|
12
10
|
export declare const MERGE_PARAM_DEFAULTS: MergeParams;
|
|
13
11
|
export declare function resolveMergeParams(input?: unknown): MergeParams;
|
package/lib/types/params.js
CHANGED
|
@@ -1,9 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Merge parameter resolution. E2/E3 values arrive as one JSON object; this is the only
|
|
3
|
+
* validator. Invalid values throw loudly (no silent clamp), matching the screening contract.
|
|
4
|
+
*/
|
|
5
|
+
import { resolveLimitsSnapshot } from "./domain/merge-limits.js";
|
|
1
6
|
export const MERGE_PARAM_DEFAULTS = {
|
|
2
7
|
mergeApproval: false,
|
|
3
8
|
mergeSkippedTtlDays: 0,
|
|
4
9
|
mergeRunTimeoutMinutes: 0,
|
|
5
10
|
mergePairTimeoutMinutes: 15,
|
|
6
11
|
mergeModelsRoot: '',
|
|
12
|
+
platformWrite: false,
|
|
7
13
|
};
|
|
8
14
|
const KEYS = new Set(Object.keys(MERGE_PARAM_DEFAULTS));
|
|
9
15
|
function materialize(value) {
|
|
@@ -32,6 +38,13 @@ export function resolveMergeParams(input = {}) {
|
|
|
32
38
|
mergeApproval: approval,
|
|
33
39
|
mergeSkippedTtlDays: record.mergeSkippedTtlDays === undefined ? MERGE_PARAM_DEFAULTS.mergeSkippedTtlDays : nonNegative('mergeSkippedTtlDays', record.mergeSkippedTtlDays),
|
|
34
40
|
mergeRunTimeoutMinutes: record.mergeRunTimeoutMinutes === undefined ? MERGE_PARAM_DEFAULTS.mergeRunTimeoutMinutes : nonNegative('mergeRunTimeoutMinutes', record.mergeRunTimeoutMinutes),
|
|
41
|
+
platformWrite: record.platformWrite === undefined ? (MERGE_PARAM_DEFAULTS.platformWrite ?? false) : (() => {
|
|
42
|
+
const value = materialize(record.platformWrite);
|
|
43
|
+
if (typeof value !== 'boolean')
|
|
44
|
+
throw new Error('platformWrite must be a boolean');
|
|
45
|
+
return value;
|
|
46
|
+
})(),
|
|
47
|
+
limitsSnapshot: resolveLimitsSnapshot(),
|
|
35
48
|
mergeModelsRoot: record.mergeModelsRoot === undefined ? MERGE_PARAM_DEFAULTS.mergeModelsRoot : (() => {
|
|
36
49
|
const value = materialize(record.mergeModelsRoot);
|
|
37
50
|
if (typeof value !== 'string')
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Pending approval actions. The row lookup and transition happen in one
|
|
3
|
-
*
|
|
2
|
+
* Pending approval actions. The row lookup and transition happen in one PairLedger transaction,
|
|
3
|
+
* so two clients approving the same row cannot both win.
|
|
4
4
|
*/
|
|
5
5
|
import type { EvolutionIoLike } from '@lmzhen/dsh-evolution-core';
|
|
6
6
|
import type { MergeRow } from './contracts.ts';
|
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
1
|
+
import { v4ToLegacy } from "./domain/row-v4.js";
|
|
2
|
+
import { pairLedger } from "./domain/pair-ledger.js";
|
|
3
3
|
async function act(io, home, now, id, type) {
|
|
4
4
|
void now;
|
|
5
|
-
return await
|
|
5
|
+
return await pairLedger(io, home).transact(tx => {
|
|
6
6
|
const row = tx.rows().find(item => item.id === id);
|
|
7
7
|
if (row === undefined)
|
|
8
8
|
return { ok: false, reason: 'not-found', message: 'pending row not found: ' + id };
|
|
@@ -16,7 +16,7 @@ async function act(io, home, now, id, type) {
|
|
|
16
16
|
return { ok: false, reason: 'invalid', message: moved.message };
|
|
17
17
|
if (moved.row === null)
|
|
18
18
|
return { ok: false, reason: 'invalid', message: 'transition produced no row' };
|
|
19
|
-
return { ok: true, row:
|
|
19
|
+
return { ok: true, row: v4ToLegacy(moved.row) };
|
|
20
20
|
});
|
|
21
21
|
}
|
|
22
22
|
export async function approvePending(io, home, now, id) {
|
package/lib/types/prompts.d.ts
CHANGED
|
@@ -1,11 +1,9 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Round-1/round-2 prompt builders. B draft body, with only the file-tool ->
|
|
3
|
-
* merge_stage substitution and the fresh-mode bridge sentence.
|
|
4
|
-
*/
|
|
5
1
|
import type { MergeDecision, MergeRow } from './contracts.ts';
|
|
6
2
|
import type { SkillCard } from './skill-card.ts';
|
|
7
3
|
export declare const MERGE_PROMPT_VERSION = "merge-b/v1";
|
|
4
|
+
export declare function promptHashOf(prompt: string): string;
|
|
8
5
|
export declare const STANDBY_PROMPT = "You are a merge worker. Wait for instructions.";
|
|
6
|
+
export declare function sourceBlock(card: SkillCard | undefined, label: string): string[];
|
|
9
7
|
export declare function buildRound1Prompt(candidate: {
|
|
10
8
|
readonly a: string;
|
|
11
9
|
readonly b: string;
|
package/lib/types/prompts.js
CHANGED
|
@@ -1,7 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Round-1/round-2 prompt builders. B draft body, with only the file-tool ->
|
|
3
|
+
* merge_stage substitution and the fresh-mode bridge sentence.
|
|
4
|
+
*/
|
|
5
|
+
import { createHash } from 'node:crypto';
|
|
1
6
|
export const MERGE_PROMPT_VERSION = 'merge-b/v1';
|
|
7
|
+
export function promptHashOf(prompt) { return createHash('sha256').update(prompt).digest('hex'); }
|
|
2
8
|
export const STANDBY_PROMPT = 'You are a merge worker. Wait for instructions.';
|
|
3
9
|
function sourcePath(name) { return 'sources/' + name; }
|
|
4
|
-
function mergedPath(target) { return 'merged/' + target; }
|
|
5
10
|
function wireDecision(decision) {
|
|
6
11
|
return {
|
|
7
12
|
shouldMerge: decision.shouldMerge,
|
|
@@ -14,15 +19,18 @@ function wireDecision(decision) {
|
|
|
14
19
|
b_summary: decision.bSummary,
|
|
15
20
|
};
|
|
16
21
|
}
|
|
22
|
+
export function sourceBlock(card, label) {
|
|
23
|
+
if (card === undefined)
|
|
24
|
+
return ['### ' + label + '(missing)'];
|
|
25
|
+
return ['### ' + label + '(name=' + card.name + ')', 'SKILL.md:', '```', card.body, '```', ...(card.files ?? []).flatMap(file => ['#### ' + file.path, '```', file.content, '```'])];
|
|
26
|
+
}
|
|
17
27
|
export function buildRound1Prompt(candidate, cards) {
|
|
18
|
-
void cards;
|
|
19
28
|
return [
|
|
20
29
|
'你是技能合并决策 agent。第 1 轮只做判断,不要写文件、不要修改任何源目录。',
|
|
21
30
|
'',
|
|
22
|
-
'
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
'包含 SKILL.md 与 references/templates/scripts 等支撑文件。',
|
|
31
|
+
'两个源技能的完整正文已内嵌在下方(不需要读文件;源路径 ' + sourcePath(candidate.a) + ' / ' + sourcePath(candidate.b) + ' 仅作标识):',
|
|
32
|
+
...sourceBlock(cards.find(card => card.name === candidate.a), 'A: ' + candidate.a),
|
|
33
|
+
...sourceBlock(cards.find(card => card.name === candidate.b), 'B: ' + candidate.b),
|
|
26
34
|
'',
|
|
27
35
|
'判断规则:',
|
|
28
36
|
'1) 两者是否语义等价/互补到值得合并,而不是词面相似;',
|
|
@@ -36,7 +44,7 @@ export function buildRound1Prompt(candidate, cards) {
|
|
|
36
44
|
export function buildRound2Prompt(row, decision, mode = 'continue') {
|
|
37
45
|
const decisionJson = JSON.stringify(wireDecision(decision));
|
|
38
46
|
const head = [
|
|
39
|
-
'现在执行第 2
|
|
47
|
+
'现在执行第 2 轮:把两个技能融合成一个完整、可用的技能。',
|
|
40
48
|
'不要修改 ' + sourcePath(row.a) + ' 或 ' + sourcePath(row.b) + '(只读源副本);不要访问工作目录以外的路径。',
|
|
41
49
|
'',
|
|
42
50
|
];
|
|
@@ -45,12 +53,17 @@ export function buildRound2Prompt(row, decision, mode = 'continue') {
|
|
|
45
53
|
'目标技能名:' + decision.target,
|
|
46
54
|
'',
|
|
47
55
|
'工作方式:',
|
|
48
|
-
'-
|
|
56
|
+
'- 必须调用 merge_stage 的 write/copy 写目标技能树;不要用其它文件写工具。path 相对目标技能目录,不要带 merged/ 前缀:先 write path=SKILL.md,再写 references/...、templates/...、scripts/...;',
|
|
49
57
|
'- 可以复制源文件作为草稿基础,然后逐段修改;可以多轮读写;',
|
|
50
58
|
'- 保留两源独特内容,不编造事实,不留下 TODO/占位符;',
|
|
51
59
|
'- SKILL.md 以合法 YAML frontmatter 开头,name 等于目标名,description 不超过 60 字符;',
|
|
52
60
|
'- 正文与支撑文件的引用必须成对,路径只允许 references/templates/scripts/assets 下的相对路径。',
|
|
53
61
|
'',
|
|
62
|
+
'提交前逐项自检:',
|
|
63
|
+
'- 质量:语义完整、不编造事实、无 TODO/占位符;',
|
|
64
|
+
'- 结构:frontmatter 合法、引用成对、文件清单与正文一致;',
|
|
65
|
+
'- 安全:不包含提示注入、密钥或越权指令。',
|
|
66
|
+
'',
|
|
54
67
|
'完成后只输出一个 JSON(不要 markdown 围栏):',
|
|
55
68
|
'{"target":"' + decision.target + '","files":["SKILL.md","references/..."],"changelog":["一条变更"],"notes":"可选的一句话"}',
|
|
56
69
|
];
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Read-only command handlers:
|
|
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
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
|
-
}[], runs?: readonly RunRecord[], approval?: boolean): 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,40 +1,31 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import { formatFacts,
|
|
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";
|
|
4
4
|
import { mergeUsage } from "./commands.js";
|
|
5
|
-
export async function handleReadOnlyMergeCommand(command, io, home, cards, runs, approval = false) {
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
};
|
|
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);
|
|
13
12
|
if (command.verb === 'status') {
|
|
14
|
-
const loaded = await load();
|
|
15
13
|
if (command.id !== undefined) {
|
|
16
|
-
const row =
|
|
17
|
-
return
|
|
14
|
+
const row = rows.find(item => item.id === command.id);
|
|
15
|
+
return row === undefined ? 'merge row not found: ' + command.id : formatSnapshotStatus([row]);
|
|
18
16
|
}
|
|
19
|
-
|
|
20
|
-
return withNote(formatFacts(facts), loaded.note);
|
|
17
|
+
return formatFacts(buildFacts(snapshot));
|
|
21
18
|
}
|
|
22
19
|
if (command.verb === 'pending') {
|
|
23
|
-
const
|
|
24
|
-
const
|
|
25
|
-
|
|
26
|
-
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');
|
|
27
23
|
}
|
|
28
24
|
if (command.verb === 'candidates') {
|
|
29
|
-
const
|
|
30
|
-
|
|
31
|
-
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);
|
|
32
27
|
}
|
|
33
|
-
|
|
34
|
-
const loaded = await load();
|
|
35
|
-
return withNote(formatQueue(loaded.state.rows, 'skipped'), loaded.note);
|
|
36
|
-
}
|
|
37
|
-
return null;
|
|
28
|
+
return formatSnapshotQueue(rows.filter(row => row.queue === 'skipped'), 'skipped');
|
|
38
29
|
}
|
|
39
30
|
export function mergeCommandHelp() {
|
|
40
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
|