@lmzhen/dsh-evolution-merge 0.30.24 → 0.30.27
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/lib/index.js +549 -183
- package/lib/types/approve-command.js +2 -1
- package/lib/types/authoring-flow.d.ts +5 -0
- package/lib/types/authoring-flow.js +14 -6
- package/lib/types/commands.d.ts +7 -0
- package/lib/types/commands.js +8 -1
- package/lib/types/commit-execute.d.ts +4 -0
- package/lib/types/commit-execute.js +8 -5
- package/lib/types/commit-receipt.js +3 -1
- package/lib/types/contracts.d.ts +20 -1
- package/lib/types/doctor.js +1 -1
- package/lib/types/domain/errors.js +5 -1
- package/lib/types/domain/merge-snapshot.d.ts +12 -3
- package/lib/types/domain/merge-snapshot.js +9 -4
- package/lib/types/domain/reconcile.js +1 -1
- package/lib/types/domain/row-v4.d.ts +4 -0
- package/lib/types/domain/row-v4.js +9 -9
- package/lib/types/domain/run-coordinator.d.ts +9 -0
- package/lib/types/domain/stage-aggregate.d.ts +6 -1
- package/lib/types/domain/stage-aggregate.js +16 -9
- package/lib/types/domain/validation-plan.js +5 -1
- package/lib/types/host-capabilities.d.ts +22 -0
- package/lib/types/host-capabilities.js +24 -0
- package/lib/types/index.js +12 -2
- package/lib/types/kernel/adapter-v4.d.ts +12 -0
- package/lib/types/kernel/adapter-v4.js +54 -0
- package/lib/types/kernel/failure.d.ts +8 -0
- package/lib/types/kernel/failure.js +33 -0
- package/lib/types/kernel/index.d.ts +9 -0
- package/lib/types/kernel/index.js +9 -0
- package/lib/types/kernel/invariants.d.ts +4 -0
- package/lib/types/kernel/invariants.js +47 -0
- package/lib/types/kernel/ledger.d.ts +46 -0
- package/lib/types/kernel/ledger.js +62 -0
- package/lib/types/kernel/machine.d.ts +13 -0
- package/lib/types/kernel/machine.js +108 -0
- package/lib/types/kernel/model.d.ts +154 -0
- package/lib/types/kernel/model.js +5 -0
- package/lib/types/kernel/projection.d.ts +28 -0
- package/lib/types/kernel/projection.js +44 -0
- package/lib/types/kernel/recovery.d.ts +31 -0
- package/lib/types/kernel/recovery.js +19 -0
- package/lib/types/kernel/unit.d.ts +43 -0
- package/lib/types/kernel/unit.js +63 -0
- package/lib/types/merge-routes.js +9 -0
- package/lib/types/new-child-authoring.d.ts +1 -0
- package/lib/types/new-child-authoring.js +15 -7
- package/lib/types/pending-actions.d.ts +1 -1
- package/lib/types/pending-actions.js +4 -0
- package/lib/types/recovery-coordinator.d.ts +2 -0
- package/lib/types/recovery-coordinator.js +14 -4
- package/lib/types/recovery-projection.d.ts +4 -2
- package/lib/types/recovery-projection.js +3 -3
- package/lib/types/resume-command.js +2 -1
- package/lib/types/round2-outcome.js +26 -9
- package/lib/types/run-shell.d.ts +1 -0
- package/lib/types/run-shell.js +57 -15
- package/lib/types/runtime.d.ts +6 -1
- package/lib/types/runtime.js +46 -22
- package/lib/types/scan-command.d.ts +1 -0
- package/lib/types/scan-command.js +1 -0
- package/lib/types/scan-ports.js +3 -0
- package/lib/types/scan-run.d.ts +6 -1
- package/lib/types/scan-run.js +34 -18
- package/lib/types/surface/merge-view.js +12 -4
- package/lib/types/undo-operation.d.ts +2 -1
- package/lib/types/undo-operation.js +10 -4
- package/lib/types/wire-projection.js +7 -2
- package/lib/types/wire.d.ts +1 -0
- package/package.json +5 -5
- package/lib/types/adapters/model.d.ts +0 -31
- package/lib/types/adapters/model.js +0 -47
- package/lib/types/gate-cache.d.ts +0 -12
- package/lib/types/gate-cache.js +0 -36
- package/lib/types/gate-filter.d.ts +0 -31
- package/lib/types/gate-filter.js +0 -64
- package/lib/types/ladder.d.ts +0 -65
- package/lib/types/ladder.js +0 -162
- package/lib/types/propose.d.ts +0 -26
- package/lib/types/propose.js +0 -69
- package/lib/types/screening-port.d.ts +0 -47
- package/lib/types/screening-port.js +0 -93
- package/lib/types/screening.d.ts +0 -30
- package/lib/types/screening.js +0 -75
package/lib/types/run-shell.js
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { writeMergeReport } from "./report.js";
|
|
2
2
|
import { writeRunMeta } from "./domain/run-coordinator.js";
|
|
3
|
+
/** Bounded wait for a detached body before the run is reported as timed out. */
|
|
4
|
+
const DETACHED_DRAIN_MS = 30_000;
|
|
3
5
|
function emptyDraft(reason) {
|
|
4
6
|
return {
|
|
5
7
|
stopReason: 'failed',
|
|
@@ -21,7 +23,16 @@ export async function withMergeRun(options) {
|
|
|
21
23
|
const startedAt = handle.startedAt;
|
|
22
24
|
const stage = (name, progress) => options.registry.stage(handle.id, name, progress);
|
|
23
25
|
await stage('screening', options.total === undefined ? undefined : { done: 0, total: options.total });
|
|
24
|
-
await writeRunMeta(options.io, options.home, {
|
|
26
|
+
await writeRunMeta(options.io, options.home, {
|
|
27
|
+
version: 1, runId: handle.id, pid: process.pid,
|
|
28
|
+
...(options.parentRunId === undefined ? {} : { parentRunId: options.parentRunId }),
|
|
29
|
+
...(options.pairKey === undefined ? {} : { pairKey: options.pairKey }),
|
|
30
|
+
...(options.stagingRef === undefined ? {} : { stagingRef: options.stagingRef }),
|
|
31
|
+
...(options.pairKey === undefined ? {} : { pairs: [{ pairKey: options.pairKey, state: 'pending' }] }),
|
|
32
|
+
pairCount: options.pairCount ?? (options.pairKey === undefined ? 0 : 1),
|
|
33
|
+
phase: 'running', approval: options.approval.mergeApproval ? 'pending' : 'approved',
|
|
34
|
+
createdAt: new Date(startedAt).toISOString(), updatedAt: new Date(options.now()).toISOString(),
|
|
35
|
+
});
|
|
25
36
|
const finish = async () => {
|
|
26
37
|
let draft;
|
|
27
38
|
const runController = new AbortController();
|
|
@@ -29,19 +40,46 @@ export async function withMergeRun(options) {
|
|
|
29
40
|
handle.signal.addEventListener('abort', onAbort, { once: true });
|
|
30
41
|
let timer;
|
|
31
42
|
const timeoutMinutes = options.timeoutMinutes;
|
|
32
|
-
const
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
});
|
|
43
|
+
const bodyOutcome = options.body({ runId: handle.id, startedAt, signal: runController.signal, stage })
|
|
44
|
+
.then(value => ({ kind: 'draft', value }))
|
|
45
|
+
.catch(error => ({ kind: 'error', error }));
|
|
46
|
+
let outcome;
|
|
47
|
+
let timedOut = false;
|
|
38
48
|
try {
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
49
|
+
if (timeoutMinutes === undefined || timeoutMinutes <= 0) {
|
|
50
|
+
outcome = await bodyOutcome;
|
|
51
|
+
}
|
|
52
|
+
else {
|
|
53
|
+
const timeout = new Promise(resolve => {
|
|
54
|
+
timer = setTimeout(() => {
|
|
55
|
+
// Abort first, then wait for the body, but never without a second bound: a body
|
|
56
|
+
// that ignores the signal must not leave the run open forever.
|
|
57
|
+
runController.abort();
|
|
58
|
+
resolve({ kind: 'timeout' });
|
|
59
|
+
}, timeoutMinutes * 60_000);
|
|
60
|
+
});
|
|
61
|
+
const raced = await Promise.race([bodyOutcome, timeout]);
|
|
62
|
+
if (raced.kind !== 'timeout')
|
|
63
|
+
outcome = raced;
|
|
64
|
+
else {
|
|
65
|
+
timedOut = true;
|
|
66
|
+
let drainTimer;
|
|
67
|
+
const drained = await Promise.race([
|
|
68
|
+
bodyOutcome,
|
|
69
|
+
new Promise(resolve => { drainTimer = setTimeout(() => resolve({ kind: 'drain-timeout' }), DETACHED_DRAIN_MS); }),
|
|
70
|
+
]);
|
|
71
|
+
if (drainTimer !== undefined)
|
|
72
|
+
clearTimeout(drainTimer);
|
|
73
|
+
outcome = drained.kind === 'drain-timeout'
|
|
74
|
+
? { kind: 'error', error: new Error('timeout: body did not stop within 30s drain window') }
|
|
75
|
+
: drained;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
draft = outcome.kind === 'draft'
|
|
79
|
+
? outcome.value
|
|
80
|
+
: emptyDraft('run body failed: ' + (outcome.error instanceof Error ? outcome.error.message : String(outcome.error)));
|
|
81
|
+
if (timedOut)
|
|
82
|
+
draft = { ...draft, stopReason: 'timeout', notes: [...draft.notes, 'merge run timeout reached; body aborted'] };
|
|
45
83
|
}
|
|
46
84
|
finally {
|
|
47
85
|
if (timer !== undefined)
|
|
@@ -59,9 +97,10 @@ export async function withMergeRun(options) {
|
|
|
59
97
|
approval: options.approval,
|
|
60
98
|
scope: draft.scope,
|
|
61
99
|
pairs: draft.pairs,
|
|
100
|
+
processedPairs: draft.pairs,
|
|
62
101
|
totals: draft.totals,
|
|
63
102
|
notes: draft.notes,
|
|
64
|
-
...(draft.remaining === undefined ? {} : { remaining: draft.remaining }),
|
|
103
|
+
...(draft.remaining === undefined ? {} : { remaining: draft.remaining, remainingPairs: draft.remaining }),
|
|
65
104
|
...(draft.reportOnly === undefined ? {} : { reportOnly: draft.reportOnly }),
|
|
66
105
|
...(draft.validationFailures === undefined ? {} : { validationFailures: draft.validationFailures }),
|
|
67
106
|
...(draft.archived === undefined ? {} : { archived: draft.archived }),
|
|
@@ -99,7 +138,10 @@ export async function withMergeRun(options) {
|
|
|
99
138
|
&& (options.command === 'scan' || options.command === 'pair')
|
|
100
139
|
&& report.stopReason === 'completed'
|
|
101
140
|
&& report.totals.pending > 0;
|
|
102
|
-
|
|
141
|
+
const finalPairs = draft.pairs.length > 0
|
|
142
|
+
? draft.pairs.map(pair => ({ pairKey: pair.pairKey, state: pair.outcome === 'merged' ? 'fused' : pair.outcome === 'pending' ? 'pending' : 'skipped' }))
|
|
143
|
+
: options.pairKey === undefined ? [] : [{ pairKey: options.pairKey, state: 'pending' }];
|
|
144
|
+
await writeRunMeta(options.io, options.home, { version: 1, runId: handle.id, pid: process.pid, ...(options.parentRunId === undefined ? {} : { parentRunId: options.parentRunId }), ...(options.pairKey === undefined ? {} : { pairKey: options.pairKey }), ...(options.stagingRef === undefined ? {} : { stagingRef: options.stagingRef }), ...(finalPairs.length === 0 ? {} : { pairs: finalPairs }), pairCount: options.pairCount ?? (options.pairKey === undefined ? 0 : 1), phase: awaiting ? 'awaiting-approval' : 'settled', approval: awaiting ? 'pending' : 'approved', stopReason: report.stopReason, reportRef: reportPath, createdAt: new Date(startedAt).toISOString(), updatedAt: new Date(options.now()).toISOString() });
|
|
103
145
|
if (report.stopReason === 'completed')
|
|
104
146
|
return { ok: true, runId: handle.id, reportPath, report };
|
|
105
147
|
return { ok: false, runId: handle.id, message: 'merge ' + report.stopReason, reportPath };
|
package/lib/types/runtime.d.ts
CHANGED
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
import { SkillLibrary, type EvolutionIoLike, type UnitTransaction } from '@lmzhen/dsh-evolution-core';
|
|
2
|
-
import { type MergeCandidate } from './contracts.ts';
|
|
2
|
+
import { type MergeCandidate, type MergeDecision } from './contracts.ts';
|
|
3
3
|
import type { MergeParams } from './params.ts';
|
|
4
4
|
import type { SkillCard } from './skill-card.ts';
|
|
5
5
|
import type { MergeRegistryPort } from './run-shell.ts';
|
|
6
6
|
import type { MergeCommandService } from './merge-command.ts';
|
|
7
7
|
import type { StageToolContext } from './stage-tool.ts';
|
|
8
8
|
import type { ConversationChannelLike } from './channel-port.ts';
|
|
9
|
+
import type { HostCapabilities } from './host-capabilities.ts';
|
|
10
|
+
export declare function forcedDecisionFor(a: string, b: string): MergeDecision;
|
|
9
11
|
export interface MergeStageBindingPort {
|
|
10
12
|
bind(context: StageToolContext): void;
|
|
11
13
|
unbind(childId: string): void;
|
|
@@ -19,6 +21,7 @@ export interface MergeRuntimeDeps {
|
|
|
19
21
|
readonly params: MergeParams;
|
|
20
22
|
readonly platformWrite: boolean;
|
|
21
23
|
readonly channel: ConversationChannelLike;
|
|
24
|
+
readonly hostCapabilities?: HostCapabilities;
|
|
22
25
|
readonly parent: unknown;
|
|
23
26
|
readonly allow: readonly string[];
|
|
24
27
|
readonly cwd?: string;
|
|
@@ -29,6 +32,8 @@ export interface MergeRuntimeDeps {
|
|
|
29
32
|
/** Curator gate set: excluded/referenced/suppressed/protected builtins. */
|
|
30
33
|
readonly isBlocked?: (name: string) => boolean | Promise<boolean>;
|
|
31
34
|
readonly isOwnerActive?: (runId: string) => boolean | Promise<boolean>;
|
|
35
|
+
/** The host's current model identity, recorded on pending decisions. */
|
|
36
|
+
readonly model?: string;
|
|
32
37
|
/** Run-scoped engine screening. */
|
|
33
38
|
readonly screeningForRun: (cards: readonly SkillCard[], runId: string, signal: AbortSignal) => Promise<readonly MergeCandidate[]>;
|
|
34
39
|
readonly stage?: MergeStageBindingPort;
|
package/lib/types/runtime.js
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* out of apply() and makes the mutating verbs testable with fakes.
|
|
5
5
|
*/
|
|
6
6
|
import { createHash } from 'node:crypto';
|
|
7
|
-
import { errorText, probeText, SkillLibrary, UnitTransactions, skillLibraryUnitBackend } from '@lmzhen/dsh-evolution-core';
|
|
7
|
+
import { errorText, isProcessAlive, probeText, SkillLibrary, UnitTransactions, skillLibraryUnitBackend } from '@lmzhen/dsh-evolution-core';
|
|
8
8
|
import { targetClaimUnitPort } from "./unit-claim.js";
|
|
9
9
|
import { canRetry, pairKey } from "./contracts.js";
|
|
10
10
|
import { skippedPairKeys } from "./skipped.js";
|
|
@@ -18,7 +18,7 @@ import { runApproveCommand } from "./approve-command.js";
|
|
|
18
18
|
import { runResumeCommand } from "./resume-command.js";
|
|
19
19
|
import { loadMergeState } from "./state.js";
|
|
20
20
|
import { pairLedger } from "./domain/pair-ledger.js";
|
|
21
|
-
import {
|
|
21
|
+
import { readCommitReceipts } from "./commit-receipt.js";
|
|
22
22
|
import { runUndoOperation } from "./undo-operation.js";
|
|
23
23
|
import { readStageIndex } from "./domain/stage-aggregate.js";
|
|
24
24
|
import { skillLibraryUndoPort } from "./adapters/undo.js";
|
|
@@ -38,6 +38,12 @@ function fail(stage, code, hint, next, runId) {
|
|
|
38
38
|
function failRun(stage, hint, next, result) {
|
|
39
39
|
return result.error === undefined ? fail(stage, 'run-failed', hint, next, result.runId) : { ok: false, text: formatMergeError(result.error) };
|
|
40
40
|
}
|
|
41
|
+
async function hostOnlyFailure(verb, capabilities) {
|
|
42
|
+
if (capabilities === undefined)
|
|
43
|
+
return undefined;
|
|
44
|
+
const state = await capabilities.hostOnly();
|
|
45
|
+
return state === undefined ? undefined : fail(verb, 'e-323', 'host data plane unavailable: ' + state.code + (state.detail === undefined ? '' : ' (' + state.detail + ')'), 'run /evolution merge doctor');
|
|
46
|
+
}
|
|
41
47
|
/**
|
|
42
48
|
* The approved/fuse row that already holds a target. Pair/approve move a row to
|
|
43
49
|
* approved/fuse BEFORE their run starts, so this row IS the in-flight claim a second
|
|
@@ -47,7 +53,7 @@ function busyTarget(rows, target, excludeRowId) {
|
|
|
47
53
|
const row = rows.find(item => item.id !== excludeRowId && (item.state === 'approved' || item.state === 'fuse') && item.decision?.target === target);
|
|
48
54
|
return row === undefined ? undefined : { id: row.id, ...(row.runId === undefined ? {} : { runId: row.runId }) };
|
|
49
55
|
}
|
|
50
|
-
function forcedDecisionFor(a, b) {
|
|
56
|
+
export function forcedDecisionFor(a, b) {
|
|
51
57
|
const sorted = [a, b].sort();
|
|
52
58
|
const raw = ('merge-' + sorted.join('-')).replace(/[^a-zA-Z0-9._-]+/g, '-').replace(/^-+|-+$/g, '');
|
|
53
59
|
const target = raw.length <= 64 ? raw : raw.slice(0, 55) + '-' + createHash('sha256').update(raw).digest('hex').slice(0, 8);
|
|
@@ -75,6 +81,7 @@ export function createMergeRuntime(deps) {
|
|
|
75
81
|
const common = {
|
|
76
82
|
io: deps.io, home: deps.home, registry: deps.registry,
|
|
77
83
|
platformWrite: deps.platformWrite, now: deps.now,
|
|
84
|
+
...(deps.model === undefined ? {} : { model: deps.model }),
|
|
78
85
|
...(deps.isOwnerActive === undefined ? {} : { isOwnerActive: deps.isOwnerActive }),
|
|
79
86
|
...(timeout === undefined ? {} : { timeoutMinutes: timeout }),
|
|
80
87
|
};
|
|
@@ -92,17 +99,29 @@ export function createMergeRuntime(deps) {
|
|
|
92
99
|
const signal = new AbortController().signal;
|
|
93
100
|
if (command.a === command.b)
|
|
94
101
|
return fail('pair', 'pair-same-source', 'merge pair 需要两个不同技能源:' + command.a, '重新指定两个不同的 family-managed 技能');
|
|
95
|
-
if ([command.a, command.b].some(name => name.includes('/') || name.includes(':')))
|
|
96
|
-
return
|
|
102
|
+
if ([command.a, command.b].some(name => name.includes('/') || name.includes(':'))) {
|
|
103
|
+
return { ok: false, text: 'merge: 技能名不能包含 provider/name 或 provider:name 语法:' + command.a + ' / ' + command.b + ' (provider-conflict)\nusage: /evolution merge pair <a> <b>' };
|
|
104
|
+
}
|
|
97
105
|
// FR-RUN-03: a session-less invocation cannot host a merge child. Answer E-305 in
|
|
98
106
|
// the three-line shape instead of opening a run that is guaranteed to fail.
|
|
99
107
|
if (agentless)
|
|
100
108
|
return fail('pair', 'e-305', errorText('e-305-this-invocation-carries-no', { a1: 'pair' }), '在 GUI 或 CLI 的会话里运行 /evolution merge pair ' + command.a + ' ' + command.b);
|
|
109
|
+
const pairHost = await hostOnlyFailure('pair', deps.hostCapabilities);
|
|
110
|
+
if (pairHost !== undefined)
|
|
111
|
+
return pairHost;
|
|
101
112
|
// FR-EXEC-17/NFR-04: two pair runs for one pair must not race for the same row.
|
|
102
113
|
const pairKeyValue = pairKey(command.a, command.b);
|
|
103
114
|
const inFlight = inFlightPairRun(await readRunMetas(deps.io, deps.home), pairKeyValue);
|
|
104
|
-
|
|
105
|
-
|
|
115
|
+
let ownerActive = false;
|
|
116
|
+
if (inFlight !== undefined) {
|
|
117
|
+
if (deps.isOwnerActive !== undefined)
|
|
118
|
+
ownerActive = await deps.isOwnerActive(inFlight.runId);
|
|
119
|
+
else
|
|
120
|
+
ownerActive = inFlight.pid === undefined ? true : isProcessAlive(inFlight.pid);
|
|
121
|
+
}
|
|
122
|
+
if (inFlight !== undefined && ownerActive) {
|
|
123
|
+
// NFR-04: the same pair is idempotent, not an error — answer the run that already owns it.
|
|
124
|
+
return { ok: true, text: 'merge pair 已在运行:' + inFlight.runId + ';作用域:family-managed 技能对,1 对,无 per-run 上限' };
|
|
106
125
|
}
|
|
107
126
|
const cards = await deps.cards(signal);
|
|
108
127
|
const cardA = cards.find(card => card.name === command.a);
|
|
@@ -119,10 +138,12 @@ export function createMergeRuntime(deps) {
|
|
|
119
138
|
}
|
|
120
139
|
const pair = [{ a: command.a, b: command.b }];
|
|
121
140
|
const existing = (await loadMergeState(deps.io, deps.home)).state.rows.find(row => row.pairKey === pairKey(command.a, command.b));
|
|
122
|
-
|
|
141
|
+
let cleared = false;
|
|
142
|
+
if (existing?.state === 'skipped' || (existing?.state === 'fuse' && existing.failure?.retryable === false)) {
|
|
123
143
|
const dropped = await removeRuntimeRow(deps.io, deps.home, existing.id, 'superseded');
|
|
124
144
|
if (!dropped)
|
|
125
|
-
return fail('pair', 'row-changed', '
|
|
145
|
+
return fail('pair', 'row-changed', '清理可替换行时发现并发修改:' + existing.id, '运行 /evolution merge status 后重试');
|
|
146
|
+
cleared = true;
|
|
126
147
|
}
|
|
127
148
|
if (existing?.state === 'verify' && existing.runId !== undefined && (await deps.isOwnerActive?.(existing.runId)) === true) {
|
|
128
149
|
return fail('pair', 'merge-pair-in-flight', '同一 pair 已在运行 ' + existing.runId, '等待该 run 结束,或运行 /evolution merge status ' + existing.runId, existing.runId);
|
|
@@ -130,7 +151,7 @@ export function createMergeRuntime(deps) {
|
|
|
130
151
|
if (existing?.state === 'pending') {
|
|
131
152
|
return fail('pair', 'pair-pending', '该 pair 已在待批准队列:' + existing.id, '先运行 /evolution merge approve/reject/discard ' + existing.id, existing.runId);
|
|
132
153
|
}
|
|
133
|
-
if (existing?.state === 'approved' || existing?.state === 'fuse') {
|
|
154
|
+
if (!cleared && (existing?.state === 'approved' || existing?.state === 'fuse')) {
|
|
134
155
|
return fail('pair', 'pair-fuse', '该 pair 已在待合并队列,可复用决策重跑', '运行 /evolution merge resume --retry ' + (existing.runId ?? existing.id), existing.runId);
|
|
135
156
|
}
|
|
136
157
|
const forced = command.force === true ? forcedDecisionFor(command.a, command.b) : undefined;
|
|
@@ -159,7 +180,7 @@ export function createMergeRuntime(deps) {
|
|
|
159
180
|
if (!result.ok)
|
|
160
181
|
return failRun('pair', result.message, '修复后重新发起 pair;run 报告见 /evolution merge report', result);
|
|
161
182
|
if ('accepted' in result)
|
|
162
|
-
return { ok: true, text: 'merge pair 已启动:' + result.runId };
|
|
183
|
+
return { ok: true, text: 'merge pair 已启动:' + result.runId + ';作用域:family-managed 技能对,1 对,无 per-run 上限' };
|
|
163
184
|
if (!('report' in result))
|
|
164
185
|
return fail('pair', 'report-missing', 'merge pair 已结束但没有写报告', '重新运行 pair,或运行 /evolution merge runs 查看运行');
|
|
165
186
|
return { ok: true, text: 'merge pair 完成:merged ' + String(result.report.totals.merged) + ' / pending ' + String(result.report.totals.pending) + ' / failed ' + String(result.report.totals.failed) };
|
|
@@ -167,6 +188,9 @@ export function createMergeRuntime(deps) {
|
|
|
167
188
|
if (command.verb === 'scan') {
|
|
168
189
|
if (agentless)
|
|
169
190
|
return fail('scan', 'e-305', errorText('e-305-this-invocation-carries-no', { a1: 'scan' }), '在 GUI 或 CLI 的会话里运行 /evolution merge scan');
|
|
191
|
+
const scanHost = await hostOnlyFailure('scan', deps.hostCapabilities);
|
|
192
|
+
if (scanHost !== undefined)
|
|
193
|
+
return scanHost;
|
|
170
194
|
const signal = new AbortController().signal;
|
|
171
195
|
const cards = await deps.cards(signal);
|
|
172
196
|
const blocked = new Set();
|
|
@@ -225,7 +249,7 @@ export function createMergeRuntime(deps) {
|
|
|
225
249
|
if (!result.ok)
|
|
226
250
|
return failRun('scan', result.message, '修复后重新 scan;run 报告见 /evolution merge report', result);
|
|
227
251
|
if ('accepted' in result)
|
|
228
|
-
return { ok: true, text: 'merge scan 已启动:' + result.runId };
|
|
252
|
+
return { ok: true, text: 'merge scan 已启动:' + result.runId + ';作用域:family-managed 技能库,候选来源 engine screening,无 per-run 上限' };
|
|
229
253
|
if (!('report' in result))
|
|
230
254
|
return fail('scan', 'report-missing', 'merge scan 已结束但没有写报告', '重新运行 scan,或运行 /evolution merge runs 查看运行');
|
|
231
255
|
return { ok: true, text: 'merge scan 完成:merged ' + String(result.report.totals.merged) + ' / pending ' + String(result.report.totals.pending) + ' / failed ' + String(result.report.totals.failed) };
|
|
@@ -267,7 +291,7 @@ export function createMergeRuntime(deps) {
|
|
|
267
291
|
if (!result.ok)
|
|
268
292
|
return failRun('candidates', result.message, '修复后重试 candidates --retry', result);
|
|
269
293
|
if ('accepted' in result)
|
|
270
|
-
return { ok: true, text: 'candidates --retry 已启动:' + result.runId };
|
|
294
|
+
return { ok: true, text: 'candidates --retry 已启动:' + result.runId + ';作用域:单条 verify 行重跑,无 per-run 上限' };
|
|
271
295
|
if (!('report' in result))
|
|
272
296
|
return fail('candidates', 'report-missing', 'candidates --retry 已结束但没有写报告', '运行 /evolution merge runs 查看运行');
|
|
273
297
|
return { ok: true, text: 'candidates --retry 完成:merged ' + String(result.report.totals.merged) + ' / pending ' + String(result.report.totals.pending) + ' / failed ' + String(result.report.totals.failed) };
|
|
@@ -351,17 +375,15 @@ export function createMergeRuntime(deps) {
|
|
|
351
375
|
return { ok: true, text: '已丢弃合并行:' + command.id };
|
|
352
376
|
}
|
|
353
377
|
if (command.verb === 'undo') {
|
|
354
|
-
const
|
|
355
|
-
if (
|
|
356
|
-
return fail('undo', 'receipt-not-found', '该 run
|
|
357
|
-
|
|
358
|
-
return fail('undo', 'receipt-target-mismatch', '回执目标是 ' + receipt.target + ',不是 ' + command.target, '按回执目标重新运行 undo');
|
|
359
|
-
const undoRunId = receipt.parentRunId ?? receipt.runId;
|
|
378
|
+
const receipts = (await readCommitReceipts(deps.io, deps.home)).filter(receipt => (receipt.parentRunId ?? receipt.runId) === command.run && receipt.target === command.target);
|
|
379
|
+
if (receipts.length === 0)
|
|
380
|
+
return fail('undo', 'receipt-not-found', '该 run 没有目标回执:' + command.run + ' / ' + command.target, '只有已提交的 run 可 undo;运行 /evolution merge runs 查看');
|
|
381
|
+
const undoRunId = receipts[0].parentRunId ?? receipts[0].runId;
|
|
360
382
|
const undoRegistry = deps.registry;
|
|
361
383
|
if (undoRegistry.runs('merge').some(run => run.id === undoRunId && run.supersededAt !== undefined))
|
|
362
384
|
return fail('undo', 'run-superseded', '该 run 已因技能树 restore 失效,不能再 undo:' + undoRunId, '运行 /evolution merge runs 查看失效标记');
|
|
363
385
|
const unit = unitFor(deps);
|
|
364
|
-
const result = await runUndoOperation({ io: deps.io, home: deps.home, runId: undoRunId, port: skillLibraryUndoPort(deps.skills), now: deps.now,
|
|
386
|
+
const result = await runUndoOperation({ io: deps.io, home: deps.home, runId: undoRunId, port: skillLibraryUndoPort(deps.skills), now: deps.now, target: command.target, ...(unit === undefined ? {} : { finalizeUnit: id => unit.finalize.call(unit, id) }) });
|
|
365
387
|
return result.ok ? { ok: true, text: result.message } : fail('undo', 'undo-failed', result.message, '检查回执与暂存后重试');
|
|
366
388
|
}
|
|
367
389
|
if (command.verb === 'skipped' && command.restore !== undefined) {
|
|
@@ -437,6 +459,7 @@ export function createMergeRuntime(deps) {
|
|
|
437
459
|
}),
|
|
438
460
|
round2WithRun: createNewChildRound2({
|
|
439
461
|
io: deps.io, home: deps.home, cards, channel: deps.channel, parent,
|
|
462
|
+
parentRunId: (runId) => runId,
|
|
440
463
|
allow: deps.allow, ...(deps.cwd === undefined ? {} : { cwd: deps.cwd }),
|
|
441
464
|
timeoutMs: deps.params.mergePairTimeoutMinutes * 60_000,
|
|
442
465
|
commitPort: skillLibraryPort(deps.skills), now: deps.now, ...(unitFor(deps) === undefined ? {} : { unit: unitFor(deps) }),
|
|
@@ -448,7 +471,7 @@ export function createMergeRuntime(deps) {
|
|
|
448
471
|
if (!result.ok)
|
|
449
472
|
return failRun('resume', result.message, '修复后重新 resume;run 报告见 /evolution merge report', result);
|
|
450
473
|
if ('accepted' in result)
|
|
451
|
-
return { ok: true, text: 'merge resume 已启动:' + result.runId };
|
|
474
|
+
return { ok: true, text: 'merge resume 已启动:' + result.runId + ';作用域:单条待合并行重跑 agent2,无 per-run 上限' };
|
|
452
475
|
if (!('report' in result))
|
|
453
476
|
return fail('resume', 'report-missing', 'merge resume 已结束但没有写报告', '重新运行 resume,或运行 /evolution merge runs 查看运行');
|
|
454
477
|
return { ok: true, text: 'merge resume 完成:merged ' + String(result.report.totals.merged) };
|
|
@@ -549,6 +572,7 @@ export function createMergeRuntime(deps) {
|
|
|
549
572
|
stage: createStageForCards({ io: deps.io, home: deps.home, runId: command.id, cards, now: deps.now }),
|
|
550
573
|
round2: createNewChildRound2({
|
|
551
574
|
io: deps.io, home: deps.home, cards, channel: deps.channel, parent,
|
|
575
|
+
parentRunId: (runId) => runId,
|
|
552
576
|
allow: deps.allow, ...(deps.cwd === undefined ? {} : { cwd: deps.cwd }),
|
|
553
577
|
timeoutMs: deps.params.mergePairTimeoutMinutes * 60_000,
|
|
554
578
|
commitPort: skillLibraryPort(deps.skills), now: deps.now, ...(unitFor(deps) === undefined ? {} : { unit: unitFor(deps) }),
|
|
@@ -561,7 +585,7 @@ export function createMergeRuntime(deps) {
|
|
|
561
585
|
if (!result.ok)
|
|
562
586
|
return failRun('approve', result.message, '修复后重新 approve;run 报告见 /evolution merge report', result);
|
|
563
587
|
if ('accepted' in result)
|
|
564
|
-
return { ok: true, text: 'merge approve 已启动:' + result.runId };
|
|
588
|
+
return { ok: true, text: 'merge approve 已启动:' + result.runId + ';作用域:单条 pending 行批准后重跑 agent2,无 per-run 上限' };
|
|
565
589
|
if (!('report' in result))
|
|
566
590
|
return fail('approve', 'report-missing', 'merge approve 已结束但没有写报告', '重新运行 approve,或运行 /evolution merge runs 查看运行');
|
|
567
591
|
return { ok: true, text: 'merge approve 完成:merged ' + String(result.report.totals.merged) };
|
|
@@ -20,6 +20,7 @@ export interface RunScanCommandOptions {
|
|
|
20
20
|
readonly notes?: readonly string[];
|
|
21
21
|
readonly isOwnerActive?: (runId: string) => boolean | Promise<boolean>;
|
|
22
22
|
readonly cards?: readonly import('./skill-card.ts').SkillCard[];
|
|
23
|
+
readonly model?: string;
|
|
23
24
|
}
|
|
24
25
|
export declare function runScanCommand(options: RunScanCommandOptions): Promise<WithMergeRunResult>;
|
|
25
26
|
//# sourceMappingURL=scan-command.d.ts.map
|
|
@@ -28,6 +28,7 @@ export async function runScanCommand(options) {
|
|
|
28
28
|
...(options.notes === undefined ? {} : { notes: options.notes }),
|
|
29
29
|
...(options.isOwnerActive === undefined ? {} : { isOwnerActive: options.isOwnerActive }),
|
|
30
30
|
...(options.cards === undefined ? {} : { cards: options.cards }),
|
|
31
|
+
...(options.model === undefined ? {} : { model: options.model }),
|
|
31
32
|
});
|
|
32
33
|
},
|
|
33
34
|
});
|
package/lib/types/scan-ports.js
CHANGED
|
@@ -85,6 +85,7 @@ export function createChannelScanPorts(options) {
|
|
|
85
85
|
reuseStaging: true,
|
|
86
86
|
target: decision.target,
|
|
87
87
|
mode: decision.mode,
|
|
88
|
+
sources: [row.a, row.b],
|
|
88
89
|
cards: options.cards,
|
|
89
90
|
commitPort: options.commitPort,
|
|
90
91
|
...(options.unit === undefined ? {} : { unit: options.unit }),
|
|
@@ -94,6 +95,7 @@ export function createChannelScanPorts(options) {
|
|
|
94
95
|
...(options.limits === undefined ? {} : { limits: options.limits }),
|
|
95
96
|
promptVersion: MERGE_PROMPT_VERSION,
|
|
96
97
|
promptHash: promptHashOf(buildRound2Prompt(row, decision)),
|
|
98
|
+
signal,
|
|
97
99
|
revisions: {
|
|
98
100
|
...(decision.targetRevisionRef === undefined ? {} : { targetRevision: decision.targetRevisionRef }),
|
|
99
101
|
...(decision.sourceRevisions === undefined ? {} : { sourceRevisions: decision.sourceRevisions }),
|
|
@@ -113,6 +115,7 @@ export function createChannelScanPorts(options) {
|
|
|
113
115
|
...(receipt?.unitId === undefined ? {} : { unitId: receipt.unitId }),
|
|
114
116
|
...(receipt?.evidenceSha256 === undefined ? {} : { evidenceSha256: receipt.evidenceSha256 }),
|
|
115
117
|
...(receipt?.validationPlan === undefined ? {} : { validationPlan: receipt.validationPlan }),
|
|
118
|
+
...(authored.lengths === undefined ? {} : { lengths: authored.lengths }),
|
|
116
119
|
...(usage === undefined ? {} : { usage }),
|
|
117
120
|
archived: receipt?.archived ?? [],
|
|
118
121
|
target: { requested: decision.target, actual: authored.commit.plan.target, created: receipt?.create ?? false },
|
package/lib/types/scan-run.d.ts
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* The scan pair loop. Registry/report lifecycle stays in withMergeRun; this module owns
|
|
3
3
|
* candidate → round1 → pending/fuse → commit transitions and returns the report draft.
|
|
4
4
|
*/
|
|
5
|
-
import { type MergeCandidate, type MergeDecision, type MergeFailure, type MergeRow } from './contracts.ts';
|
|
5
|
+
import { type MergeCandidate, type MergeDecision, type MergeFailure, type MergeValidationIssue, type MergeRow } from './contracts.ts';
|
|
6
6
|
import { type StageEntry } from './domain/stage-aggregate.ts';
|
|
7
7
|
import type { EvolutionIoLike, RunStage } from '@lmzhen/dsh-evolution-core';
|
|
8
8
|
import type { SkillCard } from './skill-card.ts';
|
|
@@ -23,6 +23,9 @@ export interface ScanRound2Outcome {
|
|
|
23
23
|
readonly unitId?: string;
|
|
24
24
|
readonly evidenceSha256?: string;
|
|
25
25
|
readonly validationPlan?: import('./contracts.ts').MergeValidationPlanRef;
|
|
26
|
+
readonly manifestFiles?: readonly string[];
|
|
27
|
+
readonly criteria?: import('./contracts.ts').MergeCriteria;
|
|
28
|
+
readonly validationIssues?: readonly MergeValidationIssue[];
|
|
26
29
|
readonly archived?: readonly string[];
|
|
27
30
|
readonly target?: {
|
|
28
31
|
readonly requested: string;
|
|
@@ -63,6 +66,8 @@ export interface RunScanOptions {
|
|
|
63
66
|
readonly runId?: string;
|
|
64
67
|
readonly isOwnerActive?: (runId: string) => boolean | Promise<boolean>;
|
|
65
68
|
readonly cards?: readonly SkillCard[];
|
|
69
|
+
/** The model identity recorded on pending decisions/summary; absent when the host has none. */
|
|
70
|
+
readonly model?: string;
|
|
66
71
|
}
|
|
67
72
|
export declare function runScan(options: RunScanOptions): Promise<MergeRunReportDraft>;
|
|
68
73
|
//# sourceMappingURL=scan-run.d.ts.map
|
package/lib/types/scan-run.js
CHANGED
|
@@ -23,6 +23,15 @@ function failureOf(stage, error) {
|
|
|
23
23
|
retryFrom: stage === 'decide' ? 'agent1' : 'agent2',
|
|
24
24
|
};
|
|
25
25
|
}
|
|
26
|
+
function pairCriteria(outcome) {
|
|
27
|
+
if (outcome === 'merged')
|
|
28
|
+
return { decision: 'merge', validation: 'required', commit: 'required', archive: 'sources', evidence: 'receipt' };
|
|
29
|
+
if (outcome === 'kept')
|
|
30
|
+
return { decision: 'keep', validation: 'report-only', commit: 'never', archive: 'none', evidence: 'none' };
|
|
31
|
+
if (outcome === 'pending')
|
|
32
|
+
return { decision: 'merge', validation: 'required', commit: 'if-approved', archive: 'none', evidence: 'none' };
|
|
33
|
+
return { decision: 'merge', validation: 'required', commit: 'never', archive: 'none', evidence: 'none' };
|
|
34
|
+
}
|
|
26
35
|
function pairOf(row, candidate, outcome, extra = {}) {
|
|
27
36
|
return {
|
|
28
37
|
id: row.id,
|
|
@@ -34,6 +43,7 @@ function pairOf(row, candidate, outcome, extra = {}) {
|
|
|
34
43
|
...(candidate.gate === undefined ? {} : { gate: candidate.gate }),
|
|
35
44
|
outcome,
|
|
36
45
|
archived: [],
|
|
46
|
+
criteria: pairCriteria(outcome),
|
|
37
47
|
...extra,
|
|
38
48
|
};
|
|
39
49
|
}
|
|
@@ -176,7 +186,8 @@ export async function runScan(options) {
|
|
|
176
186
|
totals.failed += 1;
|
|
177
187
|
continue;
|
|
178
188
|
}
|
|
179
|
-
const
|
|
189
|
+
const decided = options.model === undefined || round1.decision.model !== undefined ? round1.decision : { ...round1.decision, model: options.model };
|
|
190
|
+
const mergeDecision = options.cards !== undefined ? withDecisionRevisions(decided, options.cards, row.a, row.b) : decided;
|
|
180
191
|
if (options.approval) {
|
|
181
192
|
const event = { type: 'verify-passed', decision: mergeDecision, target: mergeDecision.target, ...(round1.sourceRevision === undefined ? {} : { sourceRevision: round1.sourceRevision }), ...(mergeDecision.promptVersion === undefined ? {} : { promptVersion: mergeDecision.promptVersion }), ...(mergeDecision.promptHash === undefined ? {} : { promptHash: mergeDecision.promptHash }) };
|
|
182
193
|
const projected = await pairLedger(options.io, options.home).transact(tx => {
|
|
@@ -223,7 +234,7 @@ export async function runScan(options) {
|
|
|
223
234
|
const current = tx.rows().find(item => item.id === row.id);
|
|
224
235
|
if (current === undefined)
|
|
225
236
|
return null;
|
|
226
|
-
const moved = tx.transition(row.id, { type: 'staged' }, current.version);
|
|
237
|
+
const moved = tx.transition(row.id, { type: 'staged', ...(stageEntry === undefined ? {} : { stagingRef: stageEntry.ref, stagingExpiresAt: stageEntry.expiresAt }) }, current.version);
|
|
227
238
|
return moved.ok && moved.row !== null ? v4ToLegacy(moved.row) : null;
|
|
228
239
|
});
|
|
229
240
|
if (fused === null) {
|
|
@@ -235,17 +246,15 @@ export async function runScan(options) {
|
|
|
235
246
|
if (stageEntry !== undefined)
|
|
236
247
|
await presentStage(options.io, options.home, stageEntry);
|
|
237
248
|
if (isAborted(options.signal)) {
|
|
238
|
-
|
|
249
|
+
// Cancel/timeout after staging is not a policy close: remove the in-flight row (so the
|
|
250
|
+
// next scan can re-evaluate it) and drop its stage tree. Never write skipped here.
|
|
251
|
+
await pairLedger(options.io, options.home).transact(tx => {
|
|
239
252
|
const current = tx.rows().find(item => item.id === row.id);
|
|
240
|
-
|
|
241
|
-
return null;
|
|
242
|
-
const moved = tx.transition(row.id, { type: 'skipped', reason: 'cancelled' }, current.version);
|
|
243
|
-
return moved.ok && moved.row !== null ? v4ToLegacy(moved.row) : null;
|
|
253
|
+
return current === undefined ? false : tx.remove(row.id, 'superseded', undefined, current.version).ok;
|
|
244
254
|
});
|
|
245
|
-
|
|
246
|
-
row = projected;
|
|
255
|
+
await dropStage(options.io, options.home, row.id, row.stagingRef);
|
|
247
256
|
stopReason = 'cancelled';
|
|
248
|
-
remaining.push(...candidates.slice(index).map(candidate => ({ pairKey: pairKey(candidate.a, candidate.b), queue: '
|
|
257
|
+
remaining.push(...candidates.slice(index).map(candidate => ({ pairKey: pairKey(candidate.a, candidate.b), queue: 'verify' })));
|
|
249
258
|
break;
|
|
250
259
|
}
|
|
251
260
|
await stage('author', { done: index, total: candidates.length });
|
|
@@ -260,6 +269,8 @@ export async function runScan(options) {
|
|
|
260
269
|
finalizeRows.push({ id: row.id, unitId: round2.unitId ?? 'unknown', evidenceSha256: round2.evidenceSha256 ?? 'unknown' });
|
|
261
270
|
pairs.push({ ...pairOf(row, candidate, 'merged', {
|
|
262
271
|
decision: mergeDecision,
|
|
272
|
+
...(round2.manifestFiles === undefined ? {} : { manifestFiles: round2.manifestFiles }),
|
|
273
|
+
...(round2.validationIssues === undefined ? {} : { validation: { issues: round2.validationIssues } }),
|
|
263
274
|
...(round2.unitId === undefined ? {} : { unitId: round2.unitId }),
|
|
264
275
|
...(round2.target === undefined ? {} : { target: round2.target }),
|
|
265
276
|
...(round2.usage === undefined ? {} : { usage: round2.usage }),
|
|
@@ -270,30 +281,31 @@ export async function runScan(options) {
|
|
|
270
281
|
continue;
|
|
271
282
|
}
|
|
272
283
|
const failed = round2.failure ?? failureOf('author', 'round2 failed');
|
|
273
|
-
if (failed.code === 'merge-over-limit' || failed.code === 'limit'
|
|
274
|
-
const reason =
|
|
284
|
+
if (failed.code === 'merge-over-limit' || failed.code === 'limit') {
|
|
285
|
+
const reason = 'merge-over-limit';
|
|
275
286
|
const skipped = await pairLedger(options.io, options.home).transact(tx => {
|
|
276
287
|
const current = tx.rows().find(item => item.id === row.id);
|
|
277
288
|
return current === undefined ? false : tx.transition(row.id, { type: 'skipped', reason, origin: 'system', skipSource: 'system' }, current.version).ok;
|
|
278
289
|
});
|
|
279
290
|
if (skipped)
|
|
280
291
|
await dropStage(options.io, options.home, row.id);
|
|
281
|
-
pairs.push(pairOf(row, candidate, 'failed', { decision: mergeDecision, failure: failed }));
|
|
292
|
+
pairs.push(pairOf(row, candidate, 'failed', { decision: mergeDecision, failure: failed, ...(round2.manifestFiles === undefined ? {} : { manifestFiles: round2.manifestFiles }), ...(round2.criteria === undefined ? {} : { criteria: round2.criteria }), ...(round2.validationIssues === undefined ? {} : { validation: { issues: round2.validationIssues } }) }));
|
|
282
293
|
totals.failed += 1;
|
|
283
294
|
continue;
|
|
284
295
|
}
|
|
285
296
|
if (failed.code === 'target-changed' || failed.code === 'target-taken' || failed.code === 'source-changed' || failed.code === 'support-changed' || failed.code === 'source-gone') {
|
|
286
297
|
const code = failed.code === 'target-changed' ? 'target-taken' : failed.code === 'support-changed' ? 'source-changed' : failed.code;
|
|
298
|
+
const terminalFailure = { ...failed, code, retryable: false, retryFrom: 'none' };
|
|
287
299
|
await pairLedger(options.io, options.home).transact(tx => {
|
|
288
300
|
const current = tx.rows().find(item => item.id === row.id);
|
|
289
301
|
if (current === undefined)
|
|
290
302
|
return false;
|
|
291
303
|
if (code === 'source-gone')
|
|
292
304
|
return tx.remove(row.id, 'superseded', undefined, current.version).ok;
|
|
293
|
-
return tx.transition(row.id, { type: '
|
|
305
|
+
return tx.transition(row.id, { type: 'failed', failure: terminalFailure }, current.version).ok;
|
|
294
306
|
});
|
|
295
307
|
await dropStage(options.io, options.home, row.id);
|
|
296
|
-
pairs.push(pairOf(row, candidate, 'failed', { decision: mergeDecision, failure:
|
|
308
|
+
pairs.push(pairOf(row, candidate, 'failed', { decision: mergeDecision, failure: terminalFailure }));
|
|
297
309
|
totals.failed += 1;
|
|
298
310
|
continue;
|
|
299
311
|
}
|
|
@@ -306,11 +318,15 @@ export async function runScan(options) {
|
|
|
306
318
|
return moved.ok && moved.row !== null ? v4ToLegacy(moved.row) : null;
|
|
307
319
|
});
|
|
308
320
|
if (projected === null && terminal) {
|
|
309
|
-
await pairLedger(options.io, options.home).transact(tx => {
|
|
321
|
+
const dropped = await pairLedger(options.io, options.home).transact(tx => {
|
|
310
322
|
const current = tx.rows().find(item => item.id === row.id);
|
|
311
323
|
return current === undefined ? false : tx.remove(row.id, 'superseded', undefined, current.version).ok;
|
|
312
324
|
});
|
|
313
|
-
|
|
325
|
+
if (dropped) {
|
|
326
|
+
await dropStage(options.io, options.home, row.id, row.stagingRef);
|
|
327
|
+
await recordPair(options.io, options.home, { pairKey: row.pairKey, runId: row.runId ?? 'merge', result: 'failed', ...(row.decision?.target === undefined ? {} : { target: row.decision.target }) });
|
|
328
|
+
}
|
|
329
|
+
pairs.push(pairOf(row, candidate, 'failed', { decision: mergeDecision, failure: failed, ...(round2.manifestFiles === undefined ? {} : { manifestFiles: round2.manifestFiles }), ...(round2.criteria === undefined ? {} : { criteria: round2.criteria }), ...(round2.validationIssues === undefined ? {} : { validation: { issues: round2.validationIssues } }) }));
|
|
314
330
|
totals.failed += 1;
|
|
315
331
|
continue;
|
|
316
332
|
}
|
|
@@ -320,7 +336,7 @@ export async function runScan(options) {
|
|
|
320
336
|
continue;
|
|
321
337
|
}
|
|
322
338
|
row = projected;
|
|
323
|
-
pairs.push(pairOf(row, candidate, 'failed', { decision: mergeDecision, failure: failed }));
|
|
339
|
+
pairs.push(pairOf(row, candidate, 'failed', { decision: mergeDecision, failure: failed, ...(round2.manifestFiles === undefined ? {} : { manifestFiles: round2.manifestFiles }), ...(round2.criteria === undefined ? {} : { criteria: round2.criteria }), ...(round2.validationIssues === undefined ? {} : { validation: { issues: round2.validationIssues } }) }));
|
|
324
340
|
totals.failed += 1;
|
|
325
341
|
}
|
|
326
342
|
const selectionReportOnly = candidates.reportOnly ?? [];
|
|
@@ -26,10 +26,15 @@ function actions(row) {
|
|
|
26
26
|
return [renderAction('discard', context, true, true), renderAction('pair', context, true, true)];
|
|
27
27
|
if (row.row.state === 'pending')
|
|
28
28
|
return [renderAction('approve', context), renderAction('reject', context, true, true), renderAction('discard', context, true, true)];
|
|
29
|
-
if (row.row.state === 'approved' || row.row.state === 'fuse')
|
|
29
|
+
if (row.row.state === 'approved' || row.row.state === 'fuse') {
|
|
30
|
+
if (row.row.failure !== undefined && row.row.failure.retryable === false)
|
|
31
|
+
return [renderAction('pair', context, true, true), renderAction('discard', context, true, true)];
|
|
30
32
|
return [renderAction('retry-resume', context, true, true), renderAction('discard', context, true, true), renderAction('status', context)];
|
|
33
|
+
}
|
|
31
34
|
if (row.row.state === 'skipped')
|
|
32
35
|
return [renderAction('restore', context, true, true)];
|
|
36
|
+
if (row.row.state === 'verify')
|
|
37
|
+
return [renderAction('retry-candidates', context)];
|
|
33
38
|
return [renderAction('pair', context)];
|
|
34
39
|
}
|
|
35
40
|
function verifyState(row) {
|
|
@@ -59,7 +64,7 @@ export function toRowFact(row) {
|
|
|
59
64
|
a: state.a, b: state.b,
|
|
60
65
|
...(target === undefined ? {} : { target }),
|
|
61
66
|
...(summary === undefined ? {} : { summary }),
|
|
62
|
-
...(state.state === 'skipped' ? { reason: state.reason } : {}),
|
|
67
|
+
...(state.state === 'skipped' ? { reason: state.reason, reasonCode: state.reason } : {}),
|
|
63
68
|
...(state.origin === undefined ? {} : { origin: state.origin }),
|
|
64
69
|
...(state.failure === undefined ? {} : { failure: { stage: state.failure.stage, code: state.failure.code, retryable: state.failure.retryable, retryFrom: state.failure.retryFrom, detail: state.failure.message } }),
|
|
65
70
|
...(staging === undefined ? {} : { staging }),
|
|
@@ -76,7 +81,10 @@ function runView(run, readAt, includeResultRef = false) {
|
|
|
76
81
|
void includeResultRef;
|
|
77
82
|
const record = run.record;
|
|
78
83
|
const metaPhase = run.meta?.phase;
|
|
79
|
-
const state = metaPhase === 'undone' ? 'undone'
|
|
84
|
+
const state = metaPhase === 'undone' ? 'undone'
|
|
85
|
+
: run.meta?.stopReason === 'interrupted' ? 'interrupted'
|
|
86
|
+
: run.meta?.approval === 'pending' ? 'awaiting-approval'
|
|
87
|
+
: record.state;
|
|
80
88
|
const stopReason = run.meta?.stopReason ?? (record.state === 'cancelled' ? 'cancelled' : record.state === 'failed' ? 'failed' : record.state === 'succeeded' ? 'completed' : undefined);
|
|
81
89
|
return { wireVersion: 1, readAt, id: record.id, state, command: record.command ?? 'scan', ...(record.stage === undefined ? {} : { stage: record.stage }), progress: record.progress === undefined ? null : { done: record.progress.done, total: record.progress.total }, startedAt: record.startedAt, ...(record.endedAt === undefined ? {} : { endedAt: record.endedAt }), ...(stopReason === undefined ? {} : { stopReason }), ...(record.failure === undefined ? {} : { failure: { stage: 'settle', code: 'run-failed', retryable: false, retryFrom: 'none', detail: record.failure } }), ...(record.parentRunId === undefined ? {} : { parentRunId: record.parentRunId }), ...(run.pairs[0]?.pairKey === undefined ? {} : { currentPairKey: run.pairs[0].pairKey }), ...(record.resultRef === undefined ? {} : { resultRef: record.resultRef }), actions: [{ id: 'status', command: base + 'status ' + record.id, confirm: false, enabled: true }, { id: 'report', command: base + 'report ' + record.id, confirm: false, enabled: record.resultRef !== undefined }, { id: 'cancel', command: base + 'cancel ' + record.id, confirm: false, enabled: record.state === 'running' }] };
|
|
82
90
|
}
|
|
@@ -121,6 +129,6 @@ export function buildRunDetail(snapshot, runId) {
|
|
|
121
129
|
}
|
|
122
130
|
export function buildRunsView(snapshot) { return { wireVersion: 1, readAt: snapshot.readAt, runs: [...snapshot.runs].sort((a, b) => b.record.startedAt - a.record.startedAt).map(run => runView(run, snapshot.readAt, true)) }; }
|
|
123
131
|
export function buildDoctorView(snapshot) {
|
|
124
|
-
return { wireVersion: 1, readAt: snapshot.readAt, counts: snapshot.doctor.counts, ...(snapshot.runs[0] === undefined ? {} : { latestRun: runView(snapshot.runs[0], snapshot.readAt) }), staging: snapshot.doctor.staging, approval: snapshot.approval, ...(snapshot.blocked === undefined ? {} : { blocked: { code: snapshot.blocked.code, message: snapshot.blocked.message } }), rows: snapshot.doctor.rows };
|
|
132
|
+
return { wireVersion: 1, readAt: snapshot.readAt, counts: snapshot.doctor.counts, ...(snapshot.runs[0] === undefined ? {} : { latestRun: runView(snapshot.runs[0], snapshot.readAt) }), staging: snapshot.doctor.staging, approval: snapshot.approval, ...(snapshot.hostOnly === undefined ? {} : { hostOnly: snapshot.hostOnly }), ...(snapshot.blocked === undefined ? {} : { blocked: { code: snapshot.blocked.code, message: snapshot.blocked.message } }), rows: snapshot.doctor.rows };
|
|
125
133
|
}
|
|
126
134
|
//# sourceMappingURL=merge-view.js.map
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/** Batch 2b undo operation: per-pair preflight, journal, restore, receipt and settlement. */
|
|
2
2
|
import type { EvolutionIoLike } from '@lmzhen/dsh-evolution-core';
|
|
3
3
|
import { type MergeUndoPort } from './undo-merge.ts';
|
|
4
|
-
export type UndoErrorCode = 'undo-unavailable-legacy-receipt' | 'undo-evidence-hash-mismatch' | 'undo-target-drift' | 'undo-source-drift' | 'undo-partial-state' | 'undo-unit-not-committed';
|
|
4
|
+
export type UndoErrorCode = 'undo-unavailable-legacy-receipt' | 'undo-evidence-expired' | 'undo-evidence-hash-mismatch' | 'undo-target-drift' | 'undo-source-drift' | 'undo-partial-state' | 'undo-unit-not-committed';
|
|
5
5
|
export interface UndoResult {
|
|
6
6
|
readonly ok: boolean;
|
|
7
7
|
readonly code?: UndoErrorCode;
|
|
@@ -15,6 +15,7 @@ export declare function runUndoOperation(options: {
|
|
|
15
15
|
readonly port: MergeUndoPort;
|
|
16
16
|
readonly now: () => number;
|
|
17
17
|
readonly pairKey?: string;
|
|
18
|
+
readonly target?: string;
|
|
18
19
|
readonly finalizeUnit?: (unitId: string) => Promise<{
|
|
19
20
|
readonly ok: boolean;
|
|
20
21
|
readonly message?: string;
|