@lmzhen/dsh-evolution-merge 0.30.16 → 0.30.18
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +4 -3
- package/lib/index.js +2325 -855
- package/lib/types/adapters/library.js +4 -1
- package/lib/types/adapters/model.d.ts +31 -0
- package/lib/types/adapters/model.js +46 -0
- package/lib/types/approve-command.d.ts +2 -0
- package/lib/types/approve-command.js +41 -46
- package/lib/types/authoring-flow.d.ts +3 -1
- package/lib/types/authoring-flow.js +15 -5
- package/lib/types/command-surface.d.ts +5 -0
- package/lib/types/command-surface.js +44 -0
- package/lib/types/commit-assembly.d.ts +2 -0
- package/lib/types/commit-assembly.js +7 -2
- package/lib/types/commit-execute.d.ts +7 -1
- package/lib/types/commit-execute.js +40 -10
- package/lib/types/commit-recovery.d.ts +7 -2
- package/lib/types/commit-recovery.js +73 -96
- package/lib/types/contracts.d.ts +1 -1
- package/lib/types/contracts.js +1 -1
- package/lib/types/coordinator.d.ts +3 -2
- package/lib/types/coordinator.js +4 -3
- package/lib/types/decision.js +1 -1
- package/lib/types/doctor.d.ts +7 -0
- package/lib/types/doctor.js +104 -4
- package/lib/types/domain/claim.d.ts +4 -1
- package/lib/types/domain/claim.js +2 -2
- package/lib/types/domain/commit-journal-store.js +5 -1
- package/lib/types/domain/commit-journal.js +1 -1
- package/lib/types/domain/consistency.d.ts +22 -0
- package/lib/types/domain/consistency.js +50 -0
- package/lib/types/domain/ledger.js +10 -1
- package/lib/types/domain/migrate-v3.d.ts +20 -0
- package/lib/types/domain/migrate-v3.js +68 -0
- package/lib/types/domain/row-tx.d.ts +27 -0
- package/lib/types/domain/row-tx.js +31 -0
- package/lib/types/domain/row-v3-bridge.d.ts +6 -0
- package/lib/types/domain/row-v3-bridge.js +47 -0
- package/lib/types/domain/row-v3.d.ts +113 -0
- package/lib/types/domain/row-v3.js +107 -0
- package/lib/types/domain/row.d.ts +1 -0
- package/lib/types/domain/staging-index.d.ts +14 -0
- package/lib/types/domain/staging-index.js +41 -0
- package/lib/types/domain/state-v3.d.ts +15 -0
- package/lib/types/domain/state-v3.js +80 -0
- package/lib/types/facts.d.ts +24 -0
- package/lib/types/facts.js +68 -0
- package/lib/types/gate-filter.d.ts +31 -0
- package/lib/types/gate-filter.js +64 -0
- package/lib/types/index.d.ts +5 -2
- package/lib/types/index.js +16 -26
- package/lib/types/ladder.js +1 -1
- package/lib/types/merge-routes.d.ts +2 -2
- package/lib/types/merge-routes.js +21 -22
- package/lib/types/metrics.d.ts +16 -0
- package/lib/types/metrics.js +27 -0
- package/lib/types/new-child-authoring.d.ts +3 -1
- package/lib/types/new-child-authoring.js +14 -3
- package/lib/types/pending-actions.d.ts +2 -2
- package/lib/types/pending-actions.js +17 -17
- package/lib/types/propose.d.ts +26 -0
- package/lib/types/propose.js +69 -0
- package/lib/types/read-commands.d.ts +2 -2
- package/lib/types/read-commands.js +5 -3
- package/lib/types/resume-command.d.ts +2 -0
- package/lib/types/resume-command.js +4 -1
- package/lib/types/round2-outcome.js +17 -9
- package/lib/types/runtime.d.ts +4 -1
- package/lib/types/runtime.js +99 -29
- package/lib/types/scan-ports.d.ts +3 -1
- package/lib/types/scan-ports.js +2 -0
- package/lib/types/scan-run.js +116 -51
- package/lib/types/screening-port.d.ts +47 -0
- package/lib/types/screening-port.js +93 -0
- package/lib/types/screening.d.ts +30 -0
- package/lib/types/screening.js +75 -0
- package/lib/types/state.d.ts +14 -0
- package/lib/types/state.js +72 -1
- package/lib/types/status.d.ts +3 -0
- package/lib/types/status.js +17 -0
- package/lib/types/target-claim.d.ts +1 -0
- package/lib/types/target-claim.js +63 -18
- package/lib/types/unit-claim.d.ts +5 -0
- package/lib/types/unit-claim.js +15 -0
- package/lib/types/wire-projection.d.ts +36 -0
- package/lib/types/wire-projection.js +69 -0
- package/lib/types/wire.d.ts +254 -0
- package/lib/types/wire.js +3 -0
- package/package.json +8 -4
- package/lib/types/routes.d.ts +0 -70
- package/lib/types/routes.js +0 -78
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
const SYSTEM = 'You propose skill pairs that may be merged. Return only a JSON object: {"pairs":[{"a":"skill-a","b":"skill-b","reason":"one sentence"}]}. Use only supplied skill names. Do not decide whether a merge is good.';
|
|
2
|
+
function parsePairs(raw, known, cap) {
|
|
3
|
+
let value = raw;
|
|
4
|
+
if (typeof raw === 'string') {
|
|
5
|
+
try {
|
|
6
|
+
value = JSON.parse(raw);
|
|
7
|
+
}
|
|
8
|
+
catch {
|
|
9
|
+
return [];
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
if (typeof value !== 'object' || value === null)
|
|
13
|
+
return [];
|
|
14
|
+
const pairs = value.pairs;
|
|
15
|
+
if (!Array.isArray(pairs))
|
|
16
|
+
return [];
|
|
17
|
+
const out = [];
|
|
18
|
+
const seen = new Set();
|
|
19
|
+
for (const item of pairs) {
|
|
20
|
+
if (out.length >= cap)
|
|
21
|
+
break;
|
|
22
|
+
const pair = item;
|
|
23
|
+
if (typeof pair.a !== 'string' || typeof pair.b !== 'string')
|
|
24
|
+
continue;
|
|
25
|
+
const a = pair.a.trim();
|
|
26
|
+
const b = pair.b.trim();
|
|
27
|
+
if (a === b || !known.has(a) || !known.has(b))
|
|
28
|
+
continue;
|
|
29
|
+
const [first, second] = a < b ? [a, b] : [b, a];
|
|
30
|
+
const key = first + '\u0000' + second;
|
|
31
|
+
if (seen.has(key))
|
|
32
|
+
continue;
|
|
33
|
+
seen.add(key);
|
|
34
|
+
out.push({ a: first, b: second, source: 'proposed', ...(typeof pair.reason === 'string' && pair.reason.trim() !== '' ? { reason: pair.reason.trim() } : {}) });
|
|
35
|
+
}
|
|
36
|
+
return out;
|
|
37
|
+
}
|
|
38
|
+
export async function proposePairs(cards, port, options) {
|
|
39
|
+
const shardSize = Math.max(2, options.shardSize ?? 60);
|
|
40
|
+
const cap = Math.max(0, options.maxPairs ?? 200);
|
|
41
|
+
const known = new Set(cards.map(card => card.name));
|
|
42
|
+
const candidates = [];
|
|
43
|
+
const degraded = [];
|
|
44
|
+
let shards = 0;
|
|
45
|
+
for (let start = 0; start < cards.length && candidates.length < cap; start += shardSize) {
|
|
46
|
+
const shard = cards.slice(start, start + shardSize);
|
|
47
|
+
if (shard.length < 2)
|
|
48
|
+
continue;
|
|
49
|
+
shards += 1;
|
|
50
|
+
try {
|
|
51
|
+
const raw = await port.callJson({
|
|
52
|
+
system: SYSTEM,
|
|
53
|
+
user: JSON.stringify(shard.map(card => ({
|
|
54
|
+
name: card.name,
|
|
55
|
+
description: card.description.slice(0, 400),
|
|
56
|
+
body: card.body.slice(0, 600),
|
|
57
|
+
}))),
|
|
58
|
+
maxTokens: 1200,
|
|
59
|
+
signal: options.signal,
|
|
60
|
+
});
|
|
61
|
+
candidates.push(...parsePairs(raw, known, cap - candidates.length));
|
|
62
|
+
}
|
|
63
|
+
catch (error) {
|
|
64
|
+
degraded.push('proposal shard failed: ' + (error instanceof Error ? error.message : String(error)));
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
return { candidates, shards, degraded };
|
|
68
|
+
}
|
|
69
|
+
//# sourceMappingURL=propose.js.map
|
|
@@ -2,10 +2,10 @@
|
|
|
2
2
|
* Read-only command handlers: status/pending/skipped are served from merge-state.
|
|
3
3
|
* Mutating verbs return null so the host shell can route them to the run engine.
|
|
4
4
|
*/
|
|
5
|
-
import type { EvolutionIoLike } from '@lmzhen/dsh-evolution-core';
|
|
5
|
+
import type { EvolutionIoLike, RunRecord } from '@lmzhen/dsh-evolution-core';
|
|
6
6
|
import type { MergeCliCommand } from './commands.ts';
|
|
7
7
|
export declare function handleReadOnlyMergeCommand(command: MergeCliCommand, io: EvolutionIoLike, home: string, cards?: readonly {
|
|
8
8
|
readonly name: string;
|
|
9
|
-
}[]): Promise<string | null>;
|
|
9
|
+
}[], runs?: readonly RunRecord[], approval?: boolean): Promise<string | null>;
|
|
10
10
|
export declare function mergeCommandHelp(): string;
|
|
11
11
|
//# sourceMappingURL=read-commands.d.ts.map
|
|
@@ -1,7 +1,8 @@
|
|
|
1
|
+
import { buildFacts } from "./facts.js";
|
|
1
2
|
import { loadMergeState } from "./state.js";
|
|
2
|
-
import { formatQueue, formatStatus } from "./status.js";
|
|
3
|
+
import { formatFacts, formatQueue, formatStatus } from "./status.js";
|
|
3
4
|
import { mergeUsage } from "./commands.js";
|
|
4
|
-
export async function handleReadOnlyMergeCommand(command, io, home, cards) {
|
|
5
|
+
export async function handleReadOnlyMergeCommand(command, io, home, cards, runs, approval = false) {
|
|
5
6
|
const withNote = (text, note) => note === undefined ? text : 'note: ' + note + '\n' + text;
|
|
6
7
|
const load = async () => {
|
|
7
8
|
const result = await loadMergeState(io, home);
|
|
@@ -15,7 +16,8 @@ export async function handleReadOnlyMergeCommand(command, io, home, cards) {
|
|
|
15
16
|
const row = loaded.state.rows.find(item => item.id === command.id);
|
|
16
17
|
return withNote(row === undefined ? 'merge row not found: ' + command.id : formatStatus([row]), loaded.note);
|
|
17
18
|
}
|
|
18
|
-
|
|
19
|
+
const facts = buildFacts({ rows: loaded.state.rows, runs: runs ?? [], approval, readAt: Date.now(), ...(loaded.note === undefined ? {} : { notes: [loaded.note] }) });
|
|
20
|
+
return withNote(formatFacts(facts), loaded.note);
|
|
19
21
|
}
|
|
20
22
|
if (command.verb === 'pending') {
|
|
21
23
|
const loaded = await load();
|
|
@@ -17,6 +17,8 @@ export interface RunResumeCommandOptions {
|
|
|
17
17
|
readonly round2WithRun?: (row: MergeRow, decision: MergeDecision, signal: AbortSignal, runId: string) => Promise<ScanRound2Outcome>;
|
|
18
18
|
readonly rowId: string;
|
|
19
19
|
readonly platformWrite: boolean;
|
|
20
|
+
readonly detached?: boolean;
|
|
21
|
+
readonly pairTimeoutMinutes?: number;
|
|
20
22
|
readonly timeoutMinutes?: number;
|
|
21
23
|
readonly now: () => number;
|
|
22
24
|
readonly isOwnerActive?: (runId: string) => boolean | Promise<boolean>;
|
|
@@ -14,6 +14,7 @@ export async function runResumeCommand(options) {
|
|
|
14
14
|
return await runScanCommand({
|
|
15
15
|
io: options.io, home: options.home, registry: options.registry,
|
|
16
16
|
approval: true, platformWrite: options.platformWrite, now: options.now,
|
|
17
|
+
...(options.detached === true ? { detached: true } : {}),
|
|
17
18
|
...(options.makePorts === undefined
|
|
18
19
|
? { ports: { ...options.ports, screening: () => Promise.resolve([{ a: row.a, b: row.b }]) } }
|
|
19
20
|
: {
|
|
@@ -36,9 +37,10 @@ export async function runResumeCommand(options) {
|
|
|
36
37
|
const decision = row.decision;
|
|
37
38
|
if (decision === undefined)
|
|
38
39
|
return { ok: false, message: 'fuse row has no decision' };
|
|
39
|
-
|
|
40
|
+
const result = await withMergeRun({
|
|
40
41
|
io: options.io, home: options.home, registry: options.registry,
|
|
41
42
|
command: 'resume', approval: { platformWrite: options.platformWrite, mergeApproval: true }, now: options.now,
|
|
43
|
+
...(options.detached === true ? { detached: true } : {}),
|
|
42
44
|
async body(ctx) {
|
|
43
45
|
await ctx.stage('author', { done: 0, total: 1 });
|
|
44
46
|
let outcome;
|
|
@@ -60,5 +62,6 @@ export async function runResumeCommand(options) {
|
|
|
60
62
|
};
|
|
61
63
|
},
|
|
62
64
|
});
|
|
65
|
+
return result;
|
|
63
66
|
}
|
|
64
67
|
//# sourceMappingURL=resume-command.js.map
|
|
@@ -1,12 +1,13 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
1
|
+
import { v3ToLegacy } from "./domain/row-v3-bridge.js";
|
|
2
|
+
import { dropRowV3, transactRows } from "./state.js";
|
|
3
3
|
import { recordPair } from "./domain/pair-index-store.js";
|
|
4
4
|
export async function recordRound2Outcome(io, home, now, row, outcome) {
|
|
5
|
-
|
|
5
|
+
void now;
|
|
6
6
|
if (outcome.kind === 'merged') {
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
7
|
+
{
|
|
8
|
+
const dropped = await dropRowV3(io, home, row.id, 'committed', outcome.unitId ?? 'unknown');
|
|
9
|
+
if (!dropped)
|
|
10
|
+
return { merged: false, pair: { id: row.id, pairKey: row.pairKey, a: row.a, b: row.b, queue: row.queue, outcome: 'failed', archived: [], failure: { stage: 'commit', code: 'row-changed', message: 'row changed before commit record', retryable: true, retryFrom: 'agent2' } } };
|
|
10
11
|
const target = outcome.target?.actual ?? outcome.target?.requested ?? row.decision?.target;
|
|
11
12
|
await recordPair(io, home, {
|
|
12
13
|
pairKey: row.pairKey,
|
|
@@ -28,9 +29,16 @@ export async function recordRound2Outcome(io, home, now, row, outcome) {
|
|
|
28
29
|
};
|
|
29
30
|
}
|
|
30
31
|
const failure = outcome.failure ?? { stage: 'author', code: 'merge-failed', message: 'round2 failed', retryable: true, retryFrom: 'agent2' };
|
|
31
|
-
const
|
|
32
|
-
|
|
33
|
-
|
|
32
|
+
const projected = await transactRows(io, home, tx => {
|
|
33
|
+
const current = tx.rows().find(item => item.id === row.id);
|
|
34
|
+
if (current === undefined)
|
|
35
|
+
return null;
|
|
36
|
+
const moved = tx.transition(row.id, { type: 'failed', failure }, current.version);
|
|
37
|
+
return moved.ok && moved.row !== null ? v3ToLegacy(moved.row) : null;
|
|
38
|
+
});
|
|
39
|
+
if (projected === null)
|
|
40
|
+
return { merged: false, pair: { id: row.id, pairKey: row.pairKey, a: row.a, b: row.b, queue: row.queue, outcome: 'failed', archived: [], failure: { ...failure, code: 'row-changed', message: 'row changed before failure record' } } };
|
|
41
|
+
{
|
|
34
42
|
await recordPair(io, home, {
|
|
35
43
|
pairKey: row.pairKey,
|
|
36
44
|
runId: row.runId ?? 'merge',
|
package/lib/types/runtime.d.ts
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
* Plugin wiring supplies the channel, model and library; this module keeps the orchestration
|
|
4
4
|
* out of apply() and makes the mutating verbs testable with fakes.
|
|
5
5
|
*/
|
|
6
|
-
import { type EvolutionIoLike, type
|
|
6
|
+
import { SkillLibrary, type EvolutionIoLike, type UnitTransaction } from '@lmzhen/dsh-evolution-core';
|
|
7
7
|
import { type MergeCandidate } from './contracts.ts';
|
|
8
8
|
import type { MergeParams } from './params.ts';
|
|
9
9
|
import type { SkillCard } from './skill-card.ts';
|
|
@@ -37,6 +37,9 @@ export interface MergeRuntimeDeps {
|
|
|
37
37
|
/** Run-scoped engine screening. */
|
|
38
38
|
readonly screeningForRun: (cards: readonly SkillCard[], runId: string, signal: AbortSignal) => Promise<readonly MergeCandidate[]>;
|
|
39
39
|
readonly stage?: MergeStageBindingPort;
|
|
40
|
+
/** G2 transitional: when set, authoring commits through core UnitTransaction. */
|
|
41
|
+
readonly unit?: UnitTransaction;
|
|
40
42
|
}
|
|
43
|
+
export declare function unitFor(deps: MergeRuntimeDeps): UnitTransaction | undefined;
|
|
41
44
|
export declare function createMergeRuntime(deps: MergeRuntimeDeps): MergeCommandService;
|
|
42
45
|
//# sourceMappingURL=runtime.d.ts.map
|
package/lib/types/runtime.js
CHANGED
|
@@ -3,19 +3,22 @@
|
|
|
3
3
|
* Plugin wiring supplies the channel, model and library; this module keeps the orchestration
|
|
4
4
|
* out of apply() and makes the mutating verbs testable with fakes.
|
|
5
5
|
*/
|
|
6
|
-
import { probeText } from '@lmzhen/dsh-evolution-core';
|
|
6
|
+
import { probeText, SkillLibrary, UnitTransactions, skillLibraryUnitBackend } from '@lmzhen/dsh-evolution-core';
|
|
7
|
+
import { targetClaimUnitPort } from "./unit-claim.js";
|
|
7
8
|
import { canRetry, pairKey } from "./contracts.js";
|
|
8
9
|
import { skippedPairKeys } from "./skipped.js";
|
|
9
10
|
import { createReadOnlyMergeService } from "./merge-command.js";
|
|
11
|
+
import { formatRunsView } from "./status.js";
|
|
12
|
+
import { toRunsView } from "./wire-projection.js";
|
|
10
13
|
import { handleReadOnlyMergeCommand } from "./read-commands.js";
|
|
11
14
|
import { runScanCommand } from "./scan-command.js";
|
|
12
15
|
import { runApproveCommand } from "./approve-command.js";
|
|
13
16
|
import { runResumeCommand } from "./resume-command.js";
|
|
14
|
-
import {
|
|
15
|
-
import { transition } from "./coordinator.js";
|
|
17
|
+
import { dropRowV3, loadMergeState, transactRows } from "./state.js";
|
|
16
18
|
import { readCommitReceipt } from "./commit-receipt.js";
|
|
17
19
|
import { undoMergeReceipt } from "./undo-merge.js";
|
|
18
20
|
import { recordPair } from "./domain/pair-index-store.js";
|
|
21
|
+
import { readStagingIndex } from "./domain/staging-index.js";
|
|
19
22
|
import { skillLibraryUndoPort } from "./adapters/undo.js";
|
|
20
23
|
import { mergeDoctor } from "./doctor.js";
|
|
21
24
|
import { createChannelScanPorts } from "./scan-ports.js";
|
|
@@ -28,6 +31,13 @@ function forcedDecisionFor(a, b) {
|
|
|
28
31
|
const target = ('merge-' + sorted.join('-')).replace(/[^a-zA-Z0-9._-]+/g, '-').replace(/^-+|-+$/g, '').slice(0, 64);
|
|
29
32
|
return { shouldMerge: true, mode: 'create-umbrella', target, sources: [a, b], rationale: 'forced pair merge of ' + sorted.join(' and ') };
|
|
30
33
|
}
|
|
34
|
+
export function unitFor(deps) {
|
|
35
|
+
if (deps.unit !== undefined)
|
|
36
|
+
return deps.unit;
|
|
37
|
+
if (!(deps.skills instanceof SkillLibrary))
|
|
38
|
+
return undefined;
|
|
39
|
+
return new UnitTransactions({ io: deps.io, home: deps.home, backend: skillLibraryUnitBackend(deps.skills), claim: targetClaimUnitPort(deps.io, deps.home, deps.now), now: deps.now });
|
|
40
|
+
}
|
|
31
41
|
export function createMergeRuntime(deps) {
|
|
32
42
|
const readOnly = createReadOnlyMergeService(deps.io, deps.home);
|
|
33
43
|
const timeout = deps.timeoutMinutes > 0 ? deps.timeoutMinutes : undefined;
|
|
@@ -66,7 +76,9 @@ export function createMergeRuntime(deps) {
|
|
|
66
76
|
const pair = [{ a: command.a, b: command.b }];
|
|
67
77
|
const existing = (await loadMergeState(deps.io, deps.home)).state.rows.find(row => row.pairKey === pairKey(command.a, command.b));
|
|
68
78
|
if (existing?.state === 'skipped') {
|
|
69
|
-
await
|
|
79
|
+
const dropped = await dropRowV3(deps.io, deps.home, existing.id, 'superseded');
|
|
80
|
+
if (!dropped)
|
|
81
|
+
return { ok: false, text: 'row changed before cleanup: ' + existing.id };
|
|
70
82
|
}
|
|
71
83
|
if (existing?.state === 'pending') {
|
|
72
84
|
return { ok: false, text: '该 pair 已在待批准队列,先 approve/reject/discard: ' + existing.id };
|
|
@@ -88,7 +100,8 @@ export function createMergeRuntime(deps) {
|
|
|
88
100
|
...(deps.cwd === undefined ? {} : { cwd: deps.cwd }),
|
|
89
101
|
timeoutMs: deps.params.mergePairTimeoutMinutes * 60_000,
|
|
90
102
|
approval,
|
|
91
|
-
commitPort: skillLibraryPort(deps.skills), now: deps.now,
|
|
103
|
+
commitPort: skillLibraryPort(deps.skills), now: deps.now, ...(unitFor(deps) === undefined ? {} : { unit: unitFor(deps) }),
|
|
104
|
+
...(deps.isBlocked === undefined ? {} : { isBlocked: deps.isBlocked }),
|
|
92
105
|
...stageHooks,
|
|
93
106
|
});
|
|
94
107
|
return forced === undefined ? ports : { ...ports, round1: () => Promise.resolve({ kind: 'merge', decision: forced }) };
|
|
@@ -140,7 +153,8 @@ export function createMergeRuntime(deps) {
|
|
|
140
153
|
...(deps.cwd === undefined ? {} : { cwd: deps.cwd }),
|
|
141
154
|
timeoutMs: deps.params.mergePairTimeoutMinutes * 60_000,
|
|
142
155
|
approval: deps.params.mergeApproval,
|
|
143
|
-
commitPort: skillLibraryPort(deps.skills), now: deps.now,
|
|
156
|
+
commitPort: skillLibraryPort(deps.skills), now: deps.now, ...(unitFor(deps) === undefined ? {} : { unit: unitFor(deps) }),
|
|
157
|
+
...(deps.isBlocked === undefined ? {} : { isBlocked: deps.isBlocked }),
|
|
144
158
|
...stageHooks,
|
|
145
159
|
}),
|
|
146
160
|
});
|
|
@@ -157,10 +171,14 @@ export function createMergeRuntime(deps) {
|
|
|
157
171
|
const row = loaded.state.rows.find(item => item.id === command.id);
|
|
158
172
|
if (row === undefined)
|
|
159
173
|
return { ok: false, text: 'merge row not found: ' + command.id };
|
|
160
|
-
const
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
174
|
+
const changed = await transactRows(deps.io, deps.home, tx => {
|
|
175
|
+
const current = tx.rows().find(item => item.id === row.id);
|
|
176
|
+
if (current === undefined || current.state !== 'pending')
|
|
177
|
+
return false;
|
|
178
|
+
return tx.transition(row.id, { type: 'skipped', reason: 'rejected', origin: 'human' }, current.version).ok;
|
|
179
|
+
});
|
|
180
|
+
if (!changed)
|
|
181
|
+
return { ok: false, text: 'merge row is not pending or changed: ' + command.id };
|
|
164
182
|
return { ok: true, text: '已拒绝合并:' + command.id };
|
|
165
183
|
}
|
|
166
184
|
if (command.verb === 'doctor') {
|
|
@@ -171,7 +189,7 @@ export function createMergeRuntime(deps) {
|
|
|
171
189
|
const registry = deps.registry;
|
|
172
190
|
if (command.verb === 'runs') {
|
|
173
191
|
const mergeRuns = registry.runs('merge');
|
|
174
|
-
return { ok: true, text:
|
|
192
|
+
return { ok: true, text: formatRunsView(toRunsView(mergeRuns, deps.now())) };
|
|
175
193
|
}
|
|
176
194
|
if (command.verb === 'cancel') {
|
|
177
195
|
if (command.id !== undefined) {
|
|
@@ -200,7 +218,9 @@ export function createMergeRuntime(deps) {
|
|
|
200
218
|
return { ok: false, text: 'merge row not found: ' + command.id };
|
|
201
219
|
if (row.state !== 'pending' && row.state !== 'approved' && row.state !== 'fuse')
|
|
202
220
|
return { ok: false, text: 'merge row is not discardable: ' + command.id };
|
|
203
|
-
await
|
|
221
|
+
const dropped = await dropRowV3(deps.io, deps.home, row.id, 'superseded');
|
|
222
|
+
if (!dropped)
|
|
223
|
+
return { ok: false, text: 'row changed before discard: ' + command.id };
|
|
204
224
|
return { ok: true, text: '已丢弃合并行:' + command.id };
|
|
205
225
|
}
|
|
206
226
|
if (command.verb === 'undo') {
|
|
@@ -220,7 +240,9 @@ export function createMergeRuntime(deps) {
|
|
|
220
240
|
const row = loaded.state.rows.find(item => item.id === command.restore);
|
|
221
241
|
if (row === undefined || row.state !== 'skipped')
|
|
222
242
|
return { ok: false, text: 'skipped row not found: ' + command.restore };
|
|
223
|
-
await
|
|
243
|
+
const dropped = await dropRowV3(deps.io, deps.home, row.id, 'restored');
|
|
244
|
+
if (!dropped)
|
|
245
|
+
return { ok: false, text: 'row changed before restore: ' + row.id };
|
|
224
246
|
return { ok: true, text: '已恢复参与筛选:' + row.id };
|
|
225
247
|
}
|
|
226
248
|
if (command.verb === 'resume') {
|
|
@@ -228,7 +250,9 @@ export function createMergeRuntime(deps) {
|
|
|
228
250
|
const cleared = (await loadMergeState(deps.io, deps.home)).state.rows.find(item => item.id === command.clear);
|
|
229
251
|
if (cleared === undefined)
|
|
230
252
|
return { ok: false, text: 'merge row not found: ' + command.clear };
|
|
231
|
-
await
|
|
253
|
+
const dropped = await dropRowV3(deps.io, deps.home, cleared.id, 'superseded');
|
|
254
|
+
if (!dropped)
|
|
255
|
+
return { ok: false, text: 'row changed before clear: ' + cleared.id };
|
|
232
256
|
return { ok: true, text: '已清除合并行:' + cleared.id };
|
|
233
257
|
}
|
|
234
258
|
const loaded = await loadMergeState(deps.io, deps.home);
|
|
@@ -247,6 +271,15 @@ export function createMergeRuntime(deps) {
|
|
|
247
271
|
return { ok: false, text: 'merge row not found: ' + (command.id ?? '(auto)') };
|
|
248
272
|
const signal = new AbortController().signal;
|
|
249
273
|
const cards = await deps.cards(signal);
|
|
274
|
+
if ((row.state === 'fuse' || row.state === 'approved') && row.decision !== undefined && row.decision.mode !== 'create-umbrella') {
|
|
275
|
+
const targetCard = cards.find(card => card.name === row.decision?.target);
|
|
276
|
+
if (targetCard !== undefined && targetCard.managed !== true)
|
|
277
|
+
return { ok: false, text: 'target 不是 family-managed:' + row.decision.target };
|
|
278
|
+
if (targetCard !== undefined && (targetCard.protectionUnknown === true || targetCard.protectedBy != null))
|
|
279
|
+
return { ok: false, text: 'target 受保护:' + row.decision.target };
|
|
280
|
+
if (await deps.isBlocked?.(row.decision.target) === true)
|
|
281
|
+
return { ok: false, text: 'target 被范围门拦下:' + row.decision.target };
|
|
282
|
+
}
|
|
250
283
|
const pair = () => Promise.resolve([{ a: row.a, b: row.b }]);
|
|
251
284
|
const failed = (stage, code, message) => Promise.resolve({
|
|
252
285
|
kind: 'failed',
|
|
@@ -259,19 +292,23 @@ export function createMergeRuntime(deps) {
|
|
|
259
292
|
};
|
|
260
293
|
const result = await runResumeCommand({
|
|
261
294
|
...common, rowId: row.id, ports: basePorts,
|
|
295
|
+
...(detached ? { detached: true } : {}),
|
|
296
|
+
pairTimeoutMinutes: deps.params.mergePairTimeoutMinutes,
|
|
262
297
|
...(deps.isOwnerActive === undefined ? {} : { isOwnerActive: deps.isOwnerActive }),
|
|
263
298
|
makePorts: runId => createChannelScanPorts({
|
|
264
299
|
io: deps.io, home: deps.home, runId, cards, screening: pair,
|
|
265
300
|
channel: deps.channel, parent, allow: deps.allow,
|
|
266
301
|
...(deps.cwd === undefined ? {} : { cwd: deps.cwd }),
|
|
267
302
|
timeoutMs: deps.params.mergePairTimeoutMinutes * 60_000,
|
|
268
|
-
approval: deps.params.mergeApproval, commitPort: skillLibraryPort(deps.skills), now: deps.now,
|
|
303
|
+
approval: deps.params.mergeApproval, commitPort: skillLibraryPort(deps.skills), now: deps.now, ...(unitFor(deps) === undefined ? {} : { unit: unitFor(deps) }),
|
|
304
|
+
...(deps.isBlocked === undefined ? {} : { isBlocked: deps.isBlocked }),
|
|
269
305
|
}),
|
|
270
306
|
round2WithRun: createNewChildRound2({
|
|
271
307
|
io: deps.io, home: deps.home, cards, channel: deps.channel, parent,
|
|
272
308
|
allow: deps.allow, ...(deps.cwd === undefined ? {} : { cwd: deps.cwd }),
|
|
273
309
|
timeoutMs: deps.params.mergePairTimeoutMinutes * 60_000,
|
|
274
|
-
commitPort: skillLibraryPort(deps.skills), now: deps.now,
|
|
310
|
+
commitPort: skillLibraryPort(deps.skills), now: deps.now, ...(unitFor(deps) === undefined ? {} : { unit: unitFor(deps) }),
|
|
311
|
+
...(deps.isBlocked === undefined ? {} : { isBlocked: deps.isBlocked }),
|
|
275
312
|
...stageHooks,
|
|
276
313
|
}),
|
|
277
314
|
});
|
|
@@ -286,44 +323,75 @@ export function createMergeRuntime(deps) {
|
|
|
286
323
|
if (command.verb === 'approve') {
|
|
287
324
|
const signal = new AbortController().signal;
|
|
288
325
|
const cards = await deps.cards(signal);
|
|
289
|
-
|
|
326
|
+
let candidate = (await loadMergeState(deps.io, deps.home)).state.rows.find(row => row.id === command.id);
|
|
327
|
+
if (candidate !== undefined && candidate.stagingRef === undefined) {
|
|
328
|
+
const entry = await readStagingIndex(deps.io, deps.home, candidate.id, deps.now());
|
|
329
|
+
if (entry !== null)
|
|
330
|
+
candidate = { ...candidate, stagingRef: entry.ref, stagingExpiresAt: entry.expiresAt };
|
|
331
|
+
}
|
|
290
332
|
if (candidate !== undefined && candidate.state === 'pending' && candidate.decision !== undefined) {
|
|
291
333
|
const missingSource = [candidate.a, candidate.b].find(name => !cards.some(card => card.name === name && card.managed === true));
|
|
292
334
|
if (missingSource !== undefined) {
|
|
293
|
-
await
|
|
335
|
+
const dropped = await dropRowV3(deps.io, deps.home, candidate.id, 'superseded');
|
|
336
|
+
if (!dropped)
|
|
337
|
+
return { ok: false, text: 'source-gone 但 row 已变化,未清理:' + candidate.id };
|
|
294
338
|
return { ok: false, text: 'source-gone:' + missingSource + ' 已不在 family-managed 技能库,已从 active 队列清除;可 merge skipped --discard 清理' };
|
|
295
339
|
}
|
|
296
340
|
const currentRevision = sourceRevisionOf(cards, candidate.a, candidate.b);
|
|
297
341
|
if (candidate.sourceRevision !== undefined && currentRevision !== undefined && currentRevision !== candidate.sourceRevision) {
|
|
298
|
-
const
|
|
299
|
-
|
|
300
|
-
|
|
342
|
+
const recorded = await transactRows(deps.io, deps.home, tx => {
|
|
343
|
+
const current = tx.rows().find(item => item.id === candidate.id);
|
|
344
|
+
return current === undefined ? false : tx.transition(candidate.id, { type: 'expired', kind: 'source-changed' }, current.version).ok;
|
|
345
|
+
});
|
|
346
|
+
if (!recorded)
|
|
347
|
+
return { ok: false, text: 'row changed before source-changed record: ' + candidate.id };
|
|
301
348
|
return { ok: false, text: 'source-changed:源技能内容在决策后变化,拒绝照跑;出口:重新 merge pair,或 merge skipped --discard' };
|
|
302
349
|
}
|
|
303
350
|
const currentTargetRevision = targetRevisionOf(cards, candidate.decision.target);
|
|
304
351
|
if (candidate.targetRevision !== undefined && currentTargetRevision !== undefined
|
|
305
352
|
&& currentTargetRevision !== candidate.targetRevision) {
|
|
306
|
-
const
|
|
307
|
-
|
|
308
|
-
|
|
353
|
+
const recorded = await transactRows(deps.io, deps.home, tx => {
|
|
354
|
+
const current = tx.rows().find(item => item.id === candidate.id);
|
|
355
|
+
return current === undefined ? false : tx.transition(candidate.id, { type: 'expired', kind: 'target-taken' }, current.version).ok;
|
|
356
|
+
});
|
|
357
|
+
if (!recorded)
|
|
358
|
+
return { ok: false, text: 'row changed before target-taken record: ' + candidate.id };
|
|
309
359
|
return { ok: false, text: 'target-taken:目标 ' + candidate.decision.target + ' 在决策后变化,拒绝照跑;可重新 merge pair 或 discard' };
|
|
310
360
|
}
|
|
361
|
+
if (candidate.decision.mode !== 'create-umbrella') {
|
|
362
|
+
const targetCard = cards.find(card => card.name === candidate.decision?.target);
|
|
363
|
+
if (targetCard !== undefined && targetCard.managed !== true) {
|
|
364
|
+
return { ok: false, text: 'target 不是 family-managed:' + candidate.decision.target };
|
|
365
|
+
}
|
|
366
|
+
if (targetCard !== undefined && (targetCard.protectionUnknown === true || targetCard.protectedBy != null)) {
|
|
367
|
+
return { ok: false, text: 'target 受保护:' + candidate.decision.target };
|
|
368
|
+
}
|
|
369
|
+
if (await deps.isBlocked?.(candidate.decision.target) === true) {
|
|
370
|
+
return { ok: false, text: 'target 被范围门拦下:' + candidate.decision.target };
|
|
371
|
+
}
|
|
372
|
+
}
|
|
311
373
|
if (candidate.decision.mode === 'merge-into-existing' && !cards.some(card => card.name === candidate.decision?.target)) {
|
|
312
|
-
const
|
|
313
|
-
|
|
314
|
-
|
|
374
|
+
const recorded = await transactRows(deps.io, deps.home, tx => {
|
|
375
|
+
const current = tx.rows().find(item => item.id === candidate.id);
|
|
376
|
+
return current === undefined ? false : tx.transition(candidate.id, { type: 'expired', kind: 'target-taken' }, current.version).ok;
|
|
377
|
+
});
|
|
378
|
+
if (!recorded)
|
|
379
|
+
return { ok: false, text: 'row changed before target-taken record: ' + candidate.id };
|
|
315
380
|
return { ok: false, text: 'target-taken:目标 ' + candidate.decision.target + ' 已不存在,行保留 pending --expired;出口:重新 merge pair,或 merge skipped --discard' };
|
|
316
381
|
}
|
|
317
382
|
}
|
|
318
383
|
const result = await runApproveCommand({
|
|
319
384
|
...common,
|
|
320
385
|
rowId: command.id,
|
|
386
|
+
...(detached ? { detached: true } : {}),
|
|
387
|
+
pairTimeoutMinutes: deps.params.mergePairTimeoutMinutes,
|
|
321
388
|
ports: {
|
|
322
389
|
round2: createNewChildRound2({
|
|
323
390
|
io: deps.io, home: deps.home, cards, channel: deps.channel, parent,
|
|
324
391
|
allow: deps.allow, ...(deps.cwd === undefined ? {} : { cwd: deps.cwd }),
|
|
325
392
|
timeoutMs: deps.params.mergePairTimeoutMinutes * 60_000,
|
|
326
|
-
commitPort: skillLibraryPort(deps.skills), now: deps.now,
|
|
393
|
+
commitPort: skillLibraryPort(deps.skills), now: deps.now, ...(unitFor(deps) === undefined ? {} : { unit: unitFor(deps) }),
|
|
394
|
+
...(deps.isBlocked === undefined ? {} : { isBlocked: deps.isBlocked }),
|
|
327
395
|
}),
|
|
328
396
|
},
|
|
329
397
|
});
|
|
@@ -336,7 +404,9 @@ export function createMergeRuntime(deps) {
|
|
|
336
404
|
return { ok: true, text: 'merge approve 完成:merged ' + String(result.report.totals.merged) };
|
|
337
405
|
}
|
|
338
406
|
const cards = command.verb === 'pending' ? await deps.cards(new AbortController().signal) : undefined;
|
|
339
|
-
const
|
|
407
|
+
const registry = deps.registry;
|
|
408
|
+
const runs = command.verb === 'status' ? registry.runs('merge') : undefined;
|
|
409
|
+
const text = await handleReadOnlyMergeCommand(command, deps.io, deps.home, cards, runs, deps.params.mergeApproval);
|
|
340
410
|
if (text !== null)
|
|
341
411
|
return { ok: true, text };
|
|
342
412
|
return readOnly.execute(command);
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* Approval on: round1 disposes the child immediately; the row waits in pending and the later
|
|
6
6
|
* approve run must open a new child (not handled by this factory).
|
|
7
7
|
*/
|
|
8
|
-
import type { EvolutionIoLike } from '@lmzhen/dsh-evolution-core';
|
|
8
|
+
import type { EvolutionIoLike, UnitTransaction } from '@lmzhen/dsh-evolution-core';
|
|
9
9
|
import type { SkillCard } from './skill-card.ts';
|
|
10
10
|
import { type ConversationChannelLike } from './channel-port.ts';
|
|
11
11
|
import type { CommitPort } from './commit-execute.ts';
|
|
@@ -24,7 +24,9 @@ export interface CreateScanPortsOptions {
|
|
|
24
24
|
readonly timeoutMs: number;
|
|
25
25
|
readonly approval: boolean;
|
|
26
26
|
readonly commitPort: CommitPort;
|
|
27
|
+
readonly unit?: UnitTransaction | undefined;
|
|
27
28
|
readonly now: () => number;
|
|
29
|
+
readonly isBlocked?: (name: string) => boolean | Promise<boolean>;
|
|
28
30
|
readonly onChild?: (context: StageToolContext) => void;
|
|
29
31
|
readonly onChildClosed?: (childId: string) => void;
|
|
30
32
|
}
|
package/lib/types/scan-ports.js
CHANGED
|
@@ -72,7 +72,9 @@ export function createChannelScanPorts(options) {
|
|
|
72
72
|
mode: decision.mode,
|
|
73
73
|
cards: options.cards,
|
|
74
74
|
commitPort: options.commitPort,
|
|
75
|
+
...(options.unit === undefined ? {} : { unit: options.unit }),
|
|
75
76
|
now: options.now,
|
|
77
|
+
...(options.isBlocked === undefined ? {} : { isBlocked: options.isBlocked }),
|
|
76
78
|
summary: 'merge ' + row.a + ' + ' + row.b,
|
|
77
79
|
author: async () => {
|
|
78
80
|
await entry.result.author?.(row, decision, signal);
|