@lmzhen/dsh-evolution-merge 0.30.23 → 0.30.25
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 +518 -182
- package/lib/types/approve-command.js +2 -1
- package/lib/types/authoring-flow.d.ts +5 -0
- package/lib/types/authoring-flow.js +8 -5
- 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/index.js +14 -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 +4 -1
- package/lib/types/runtime.js +34 -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 +4 -4
- 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,12 @@
|
|
|
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
|
+
export declare function forcedDecisionFor(a: string, b: string): MergeDecision;
|
|
9
10
|
export interface MergeStageBindingPort {
|
|
10
11
|
bind(context: StageToolContext): void;
|
|
11
12
|
unbind(childId: string): void;
|
|
@@ -29,6 +30,8 @@ export interface MergeRuntimeDeps {
|
|
|
29
30
|
/** Curator gate set: excluded/referenced/suppressed/protected builtins. */
|
|
30
31
|
readonly isBlocked?: (name: string) => boolean | Promise<boolean>;
|
|
31
32
|
readonly isOwnerActive?: (runId: string) => boolean | Promise<boolean>;
|
|
33
|
+
/** The host's current model identity, recorded on pending decisions. */
|
|
34
|
+
readonly model?: string;
|
|
32
35
|
/** Run-scoped engine screening. */
|
|
33
36
|
readonly screeningForRun: (cards: readonly SkillCard[], runId: string, signal: AbortSignal) => Promise<readonly MergeCandidate[]>;
|
|
34
37
|
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";
|
|
@@ -47,7 +47,7 @@ function busyTarget(rows, target, excludeRowId) {
|
|
|
47
47
|
const row = rows.find(item => item.id !== excludeRowId && (item.state === 'approved' || item.state === 'fuse') && item.decision?.target === target);
|
|
48
48
|
return row === undefined ? undefined : { id: row.id, ...(row.runId === undefined ? {} : { runId: row.runId }) };
|
|
49
49
|
}
|
|
50
|
-
function forcedDecisionFor(a, b) {
|
|
50
|
+
export function forcedDecisionFor(a, b) {
|
|
51
51
|
const sorted = [a, b].sort();
|
|
52
52
|
const raw = ('merge-' + sorted.join('-')).replace(/[^a-zA-Z0-9._-]+/g, '-').replace(/^-+|-+$/g, '');
|
|
53
53
|
const target = raw.length <= 64 ? raw : raw.slice(0, 55) + '-' + createHash('sha256').update(raw).digest('hex').slice(0, 8);
|
|
@@ -75,6 +75,7 @@ export function createMergeRuntime(deps) {
|
|
|
75
75
|
const common = {
|
|
76
76
|
io: deps.io, home: deps.home, registry: deps.registry,
|
|
77
77
|
platformWrite: deps.platformWrite, now: deps.now,
|
|
78
|
+
...(deps.model === undefined ? {} : { model: deps.model }),
|
|
78
79
|
...(deps.isOwnerActive === undefined ? {} : { isOwnerActive: deps.isOwnerActive }),
|
|
79
80
|
...(timeout === undefined ? {} : { timeoutMinutes: timeout }),
|
|
80
81
|
};
|
|
@@ -92,8 +93,9 @@ export function createMergeRuntime(deps) {
|
|
|
92
93
|
const signal = new AbortController().signal;
|
|
93
94
|
if (command.a === command.b)
|
|
94
95
|
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
|
|
96
|
+
if ([command.a, command.b].some(name => name.includes('/') || name.includes(':'))) {
|
|
97
|
+
return { ok: false, text: 'merge: 技能名不能包含 provider/name 或 provider:name 语法:' + command.a + ' / ' + command.b + ' (provider-conflict)\nusage: /evolution merge pair <a> <b>' };
|
|
98
|
+
}
|
|
97
99
|
// FR-RUN-03: a session-less invocation cannot host a merge child. Answer E-305 in
|
|
98
100
|
// the three-line shape instead of opening a run that is guaranteed to fail.
|
|
99
101
|
if (agentless)
|
|
@@ -101,8 +103,16 @@ export function createMergeRuntime(deps) {
|
|
|
101
103
|
// FR-EXEC-17/NFR-04: two pair runs for one pair must not race for the same row.
|
|
102
104
|
const pairKeyValue = pairKey(command.a, command.b);
|
|
103
105
|
const inFlight = inFlightPairRun(await readRunMetas(deps.io, deps.home), pairKeyValue);
|
|
104
|
-
|
|
105
|
-
|
|
106
|
+
let ownerActive = false;
|
|
107
|
+
if (inFlight !== undefined) {
|
|
108
|
+
if (deps.isOwnerActive !== undefined)
|
|
109
|
+
ownerActive = await deps.isOwnerActive(inFlight.runId);
|
|
110
|
+
else
|
|
111
|
+
ownerActive = inFlight.pid === undefined ? true : isProcessAlive(inFlight.pid);
|
|
112
|
+
}
|
|
113
|
+
if (inFlight !== undefined && ownerActive) {
|
|
114
|
+
// NFR-04: the same pair is idempotent, not an error — answer the run that already owns it.
|
|
115
|
+
return { ok: true, text: 'merge pair 已在运行:' + inFlight.runId + ';作用域:family-managed 技能对,1 对,无 per-run 上限' };
|
|
106
116
|
}
|
|
107
117
|
const cards = await deps.cards(signal);
|
|
108
118
|
const cardA = cards.find(card => card.name === command.a);
|
|
@@ -119,10 +129,12 @@ export function createMergeRuntime(deps) {
|
|
|
119
129
|
}
|
|
120
130
|
const pair = [{ a: command.a, b: command.b }];
|
|
121
131
|
const existing = (await loadMergeState(deps.io, deps.home)).state.rows.find(row => row.pairKey === pairKey(command.a, command.b));
|
|
122
|
-
|
|
132
|
+
let cleared = false;
|
|
133
|
+
if (existing?.state === 'skipped' || (existing?.state === 'fuse' && existing.failure?.retryable === false)) {
|
|
123
134
|
const dropped = await removeRuntimeRow(deps.io, deps.home, existing.id, 'superseded');
|
|
124
135
|
if (!dropped)
|
|
125
|
-
return fail('pair', 'row-changed', '
|
|
136
|
+
return fail('pair', 'row-changed', '清理可替换行时发现并发修改:' + existing.id, '运行 /evolution merge status 后重试');
|
|
137
|
+
cleared = true;
|
|
126
138
|
}
|
|
127
139
|
if (existing?.state === 'verify' && existing.runId !== undefined && (await deps.isOwnerActive?.(existing.runId)) === true) {
|
|
128
140
|
return fail('pair', 'merge-pair-in-flight', '同一 pair 已在运行 ' + existing.runId, '等待该 run 结束,或运行 /evolution merge status ' + existing.runId, existing.runId);
|
|
@@ -130,7 +142,7 @@ export function createMergeRuntime(deps) {
|
|
|
130
142
|
if (existing?.state === 'pending') {
|
|
131
143
|
return fail('pair', 'pair-pending', '该 pair 已在待批准队列:' + existing.id, '先运行 /evolution merge approve/reject/discard ' + existing.id, existing.runId);
|
|
132
144
|
}
|
|
133
|
-
if (existing?.state === 'approved' || existing?.state === 'fuse') {
|
|
145
|
+
if (!cleared && (existing?.state === 'approved' || existing?.state === 'fuse')) {
|
|
134
146
|
return fail('pair', 'pair-fuse', '该 pair 已在待合并队列,可复用决策重跑', '运行 /evolution merge resume --retry ' + (existing.runId ?? existing.id), existing.runId);
|
|
135
147
|
}
|
|
136
148
|
const forced = command.force === true ? forcedDecisionFor(command.a, command.b) : undefined;
|
|
@@ -159,7 +171,7 @@ export function createMergeRuntime(deps) {
|
|
|
159
171
|
if (!result.ok)
|
|
160
172
|
return failRun('pair', result.message, '修复后重新发起 pair;run 报告见 /evolution merge report', result);
|
|
161
173
|
if ('accepted' in result)
|
|
162
|
-
return { ok: true, text: 'merge pair 已启动:' + result.runId };
|
|
174
|
+
return { ok: true, text: 'merge pair 已启动:' + result.runId + ';作用域:family-managed 技能对,1 对,无 per-run 上限' };
|
|
163
175
|
if (!('report' in result))
|
|
164
176
|
return fail('pair', 'report-missing', 'merge pair 已结束但没有写报告', '重新运行 pair,或运行 /evolution merge runs 查看运行');
|
|
165
177
|
return { ok: true, text: 'merge pair 完成:merged ' + String(result.report.totals.merged) + ' / pending ' + String(result.report.totals.pending) + ' / failed ' + String(result.report.totals.failed) };
|
|
@@ -225,7 +237,7 @@ export function createMergeRuntime(deps) {
|
|
|
225
237
|
if (!result.ok)
|
|
226
238
|
return failRun('scan', result.message, '修复后重新 scan;run 报告见 /evolution merge report', result);
|
|
227
239
|
if ('accepted' in result)
|
|
228
|
-
return { ok: true, text: 'merge scan 已启动:' + result.runId };
|
|
240
|
+
return { ok: true, text: 'merge scan 已启动:' + result.runId + ';作用域:family-managed 技能库,候选来源 engine screening,无 per-run 上限' };
|
|
229
241
|
if (!('report' in result))
|
|
230
242
|
return fail('scan', 'report-missing', 'merge scan 已结束但没有写报告', '重新运行 scan,或运行 /evolution merge runs 查看运行');
|
|
231
243
|
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 +279,7 @@ export function createMergeRuntime(deps) {
|
|
|
267
279
|
if (!result.ok)
|
|
268
280
|
return failRun('candidates', result.message, '修复后重试 candidates --retry', result);
|
|
269
281
|
if ('accepted' in result)
|
|
270
|
-
return { ok: true, text: 'candidates --retry 已启动:' + result.runId };
|
|
282
|
+
return { ok: true, text: 'candidates --retry 已启动:' + result.runId + ';作用域:单条 verify 行重跑,无 per-run 上限' };
|
|
271
283
|
if (!('report' in result))
|
|
272
284
|
return fail('candidates', 'report-missing', 'candidates --retry 已结束但没有写报告', '运行 /evolution merge runs 查看运行');
|
|
273
285
|
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 +363,15 @@ export function createMergeRuntime(deps) {
|
|
|
351
363
|
return { ok: true, text: '已丢弃合并行:' + command.id };
|
|
352
364
|
}
|
|
353
365
|
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;
|
|
366
|
+
const receipts = (await readCommitReceipts(deps.io, deps.home)).filter(receipt => (receipt.parentRunId ?? receipt.runId) === command.run && receipt.target === command.target);
|
|
367
|
+
if (receipts.length === 0)
|
|
368
|
+
return fail('undo', 'receipt-not-found', '该 run 没有目标回执:' + command.run + ' / ' + command.target, '只有已提交的 run 可 undo;运行 /evolution merge runs 查看');
|
|
369
|
+
const undoRunId = receipts[0].parentRunId ?? receipts[0].runId;
|
|
360
370
|
const undoRegistry = deps.registry;
|
|
361
371
|
if (undoRegistry.runs('merge').some(run => run.id === undoRunId && run.supersededAt !== undefined))
|
|
362
372
|
return fail('undo', 'run-superseded', '该 run 已因技能树 restore 失效,不能再 undo:' + undoRunId, '运行 /evolution merge runs 查看失效标记');
|
|
363
373
|
const unit = unitFor(deps);
|
|
364
|
-
const result = await runUndoOperation({ io: deps.io, home: deps.home, runId: undoRunId, port: skillLibraryUndoPort(deps.skills), now: deps.now,
|
|
374
|
+
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
375
|
return result.ok ? { ok: true, text: result.message } : fail('undo', 'undo-failed', result.message, '检查回执与暂存后重试');
|
|
366
376
|
}
|
|
367
377
|
if (command.verb === 'skipped' && command.restore !== undefined) {
|
|
@@ -437,6 +447,7 @@ export function createMergeRuntime(deps) {
|
|
|
437
447
|
}),
|
|
438
448
|
round2WithRun: createNewChildRound2({
|
|
439
449
|
io: deps.io, home: deps.home, cards, channel: deps.channel, parent,
|
|
450
|
+
parentRunId: (runId) => runId,
|
|
440
451
|
allow: deps.allow, ...(deps.cwd === undefined ? {} : { cwd: deps.cwd }),
|
|
441
452
|
timeoutMs: deps.params.mergePairTimeoutMinutes * 60_000,
|
|
442
453
|
commitPort: skillLibraryPort(deps.skills), now: deps.now, ...(unitFor(deps) === undefined ? {} : { unit: unitFor(deps) }),
|
|
@@ -448,7 +459,7 @@ export function createMergeRuntime(deps) {
|
|
|
448
459
|
if (!result.ok)
|
|
449
460
|
return failRun('resume', result.message, '修复后重新 resume;run 报告见 /evolution merge report', result);
|
|
450
461
|
if ('accepted' in result)
|
|
451
|
-
return { ok: true, text: 'merge resume 已启动:' + result.runId };
|
|
462
|
+
return { ok: true, text: 'merge resume 已启动:' + result.runId + ';作用域:单条待合并行重跑 agent2,无 per-run 上限' };
|
|
452
463
|
if (!('report' in result))
|
|
453
464
|
return fail('resume', 'report-missing', 'merge resume 已结束但没有写报告', '重新运行 resume,或运行 /evolution merge runs 查看运行');
|
|
454
465
|
return { ok: true, text: 'merge resume 完成:merged ' + String(result.report.totals.merged) };
|
|
@@ -549,6 +560,7 @@ export function createMergeRuntime(deps) {
|
|
|
549
560
|
stage: createStageForCards({ io: deps.io, home: deps.home, runId: command.id, cards, now: deps.now }),
|
|
550
561
|
round2: createNewChildRound2({
|
|
551
562
|
io: deps.io, home: deps.home, cards, channel: deps.channel, parent,
|
|
563
|
+
parentRunId: (runId) => runId,
|
|
552
564
|
allow: deps.allow, ...(deps.cwd === undefined ? {} : { cwd: deps.cwd }),
|
|
553
565
|
timeoutMs: deps.params.mergePairTimeoutMinutes * 60_000,
|
|
554
566
|
commitPort: skillLibraryPort(deps.skills), now: deps.now, ...(unitFor(deps) === undefined ? {} : { unit: unitFor(deps) }),
|
|
@@ -561,7 +573,7 @@ export function createMergeRuntime(deps) {
|
|
|
561
573
|
if (!result.ok)
|
|
562
574
|
return failRun('approve', result.message, '修复后重新 approve;run 报告见 /evolution merge report', result);
|
|
563
575
|
if ('accepted' in result)
|
|
564
|
-
return { ok: true, text: 'merge approve 已启动:' + result.runId };
|
|
576
|
+
return { ok: true, text: 'merge approve 已启动:' + result.runId + ';作用域:单条 pending 行批准后重跑 agent2,无 per-run 上限' };
|
|
565
577
|
if (!('report' in result))
|
|
566
578
|
return fail('approve', 'report-missing', 'merge approve 已结束但没有写报告', '重新运行 approve,或运行 /evolution merge runs 查看运行');
|
|
567
579
|
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;
|
|
@@ -28,8 +28,13 @@ function sortReceipts(receipts) {
|
|
|
28
28
|
}
|
|
29
29
|
async function preflight(io, home, port, receipt) {
|
|
30
30
|
const evidence = await readUnitEvidence(io, home, receipt.unitId);
|
|
31
|
-
if (evidence === null)
|
|
32
|
-
|
|
31
|
+
if (evidence === null) {
|
|
32
|
+
// A legacy receipt never had evidence to hash. A receipt WITH evidenceSha256 whose unit is
|
|
33
|
+
// gone aged out of the retention window; undo must refuse, never guess (DEC-D11).
|
|
34
|
+
return receipt.evidenceSha256 === undefined
|
|
35
|
+
? { ok: false, code: 'undo-unavailable-legacy-receipt', message: 'commit evidence is missing; undo unavailable for legacy receipt', undone: [] }
|
|
36
|
+
: { ok: false, code: 'undo-evidence-expired', message: 'commit evidence aged out of the retention window; undo unavailable (fail-closed)', undone: [] };
|
|
37
|
+
}
|
|
33
38
|
const expectedSha = receipt.evidenceSha256 ?? unitEvidenceDigest(evidence);
|
|
34
39
|
if (unitEvidenceDigest(evidence) !== expectedSha)
|
|
35
40
|
return { ok: false, code: 'undo-evidence-hash-mismatch', message: 'commit receipt evidence hash does not match evidence', undone: [] };
|
|
@@ -67,8 +72,9 @@ async function settleRun(io, home, runId, pairKey, allPairs, at) {
|
|
|
67
72
|
});
|
|
68
73
|
}
|
|
69
74
|
export async function runUndoOperation(options) {
|
|
70
|
-
const
|
|
71
|
-
const allPairKeys =
|
|
75
|
+
const runReceipts = (await readCommitReceipts(options.io, options.home)).filter(receipt => (receipt.parentRunId ?? receipt.runId) === options.runId);
|
|
76
|
+
const allPairKeys = runReceipts.map(receipt => receipt.pairKey ?? receipt.unitId);
|
|
77
|
+
const allReceipts = runReceipts.filter(receipt => options.target === undefined || receipt.target === options.target);
|
|
72
78
|
const receipts = options.pairKey === undefined ? allReceipts : allReceipts.filter(receipt => receipt.pairKey === options.pairKey);
|
|
73
79
|
if (receipts.length === 0)
|
|
74
80
|
return { ok: false, message: 'merge run has no commit receipt: ' + options.runId, undone: [] };
|