@lmzhen/dsh-evolution-merge 0.30.4 → 0.30.6
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 +60 -15
- package/lib/types/commands.js +1 -1
- package/lib/types/domain/errors.d.ts +2 -0
- package/lib/types/domain/errors.js +4 -2
- package/lib/types/read-commands.js +6 -5
- package/lib/types/revision.d.ts +3 -1
- package/lib/types/revision.js +11 -2
- package/lib/types/runtime.js +17 -3
- package/lib/types/scan-ports.js +4 -1
- package/lib/types/scan-run.js +11 -1
- package/package.json +3 -3
package/lib/index.js
CHANGED
|
@@ -1284,7 +1284,7 @@ function makeMergeRoutes(deps, _audit) {
|
|
|
1284
1284
|
}
|
|
1285
1285
|
//#endregion
|
|
1286
1286
|
//#region lib/types/commands.js
|
|
1287
|
-
const USAGE = "usage: /evolution merge scan | pair [--force] <a> <b> | status [id] | report [id] | cancel [id] | runs | doctor | pending [--expired] | candidates [--retry] | approve <id> | reject <id> | resume [id] [--retry] [--clear <id>] | skipped [--restore <id>] | discard <id> | undo <target> --run <id
|
|
1287
|
+
const USAGE = "usage: /evolution merge scan | pair [--force] <a> <b> | status [id] | report [id] | cancel [id] | runs | doctor | pending [--expired] | candidates [--retry] | approve <id> | reject <id> | resume [id] [--retry] [--clear <id>] | skipped [--restore <id>] | discard <id> | undo <target> --run <id>\ndefaults: mergeApproval=false, mergeSkippedTtlDays=0, mergeRunTimeoutMinutes=0, mergePairTimeoutMinutes=15, mergeModelsRoot=\"\"";
|
|
1288
1288
|
function flags(args, allowed) {
|
|
1289
1289
|
const values = /* @__PURE__ */ new Map();
|
|
1290
1290
|
for (let index = 0; index < args.length; index += 1) {
|
|
@@ -1517,7 +1517,7 @@ function parseMergeCommand(argv) {
|
|
|
1517
1517
|
default: return {
|
|
1518
1518
|
ok: false,
|
|
1519
1519
|
code: "usage",
|
|
1520
|
-
message: "unknown merge command: " + verb + "\nusage: /evolution merge scan | pair [--force] <a> <b> | status [id] | report [id] | cancel [id] | runs | doctor | pending [--expired] | candidates [--retry] | approve <id> | reject <id> | resume [id] [--retry] [--clear <id>] | skipped [--restore <id>] | discard <id> | undo <target> --run <id
|
|
1520
|
+
message: "unknown merge command: " + verb + "\nusage: /evolution merge scan | pair [--force] <a> <b> | status [id] | report [id] | cancel [id] | runs | doctor | pending [--expired] | candidates [--retry] | approve <id> | reject <id> | resume [id] [--retry] [--clear <id>] | skipped [--restore <id>] | discard <id> | undo <target> --run <id>\ndefaults: mergeApproval=false, mergeSkippedTtlDays=0, mergeRunTimeoutMinutes=0, mergePairTimeoutMinutes=15, mergeModelsRoot=\"\""
|
|
1521
1521
|
};
|
|
1522
1522
|
}
|
|
1523
1523
|
}
|
|
@@ -1527,24 +1527,28 @@ function mergeUsage() {
|
|
|
1527
1527
|
//#endregion
|
|
1528
1528
|
//#region lib/types/read-commands.js
|
|
1529
1529
|
async function handleReadOnlyMergeCommand(command, io, home) {
|
|
1530
|
+
const withNote = (text, note) => note === void 0 ? text : "note: " + note + "\n" + text;
|
|
1530
1531
|
if (command.verb === "status") {
|
|
1531
1532
|
const loaded = await loadMergeState(io, home);
|
|
1532
1533
|
if (command.id !== void 0) {
|
|
1533
1534
|
const row = loaded.state.rows.find((item) => item.id === command.id);
|
|
1534
|
-
return row === void 0 ? "merge row not found: " + command.id : formatStatus([row]);
|
|
1535
|
+
return withNote(row === void 0 ? "merge row not found: " + command.id : formatStatus([row]), loaded.note);
|
|
1535
1536
|
}
|
|
1536
|
-
return formatStatus(loaded.state.rows);
|
|
1537
|
+
return withNote(formatStatus(loaded.state.rows), loaded.note);
|
|
1537
1538
|
}
|
|
1538
1539
|
if (command.verb === "pending") {
|
|
1539
1540
|
const loaded = await loadMergeState(io, home);
|
|
1540
1541
|
const rows = command.expired === true ? loaded.state.rows.filter((row) => row.expired !== void 0) : loaded.state.rows;
|
|
1541
|
-
return command.expired === true ? formatStatus(rows) : formatQueue(rows, "pending");
|
|
1542
|
+
return withNote(command.expired === true ? formatStatus(rows) : formatQueue(rows, "pending"), loaded.note);
|
|
1542
1543
|
}
|
|
1543
1544
|
if (command.verb === "candidates") {
|
|
1544
1545
|
const loaded = await loadMergeState(io, home);
|
|
1545
|
-
return formatQueue(command.retry === true ? loaded.state.rows.filter((row) => row.state === "approved") : loaded.state.rows, command.retry === true ? "fuse" : "verify");
|
|
1546
|
+
return withNote(formatQueue(command.retry === true ? loaded.state.rows.filter((row) => row.state === "approved") : loaded.state.rows, command.retry === true ? "fuse" : "verify"), loaded.note);
|
|
1547
|
+
}
|
|
1548
|
+
if (command.verb === "skipped") {
|
|
1549
|
+
const loaded = await loadMergeState(io, home);
|
|
1550
|
+
return withNote(formatQueue(loaded.state.rows, "skipped"), loaded.note);
|
|
1546
1551
|
}
|
|
1547
|
-
if (command.verb === "skipped") return formatQueue((await loadMergeState(io, home)).state.rows, "skipped");
|
|
1548
1552
|
return null;
|
|
1549
1553
|
}
|
|
1550
1554
|
function mergeCommandHelp() {
|
|
@@ -1554,8 +1558,8 @@ function mergeCommandHelp() {
|
|
|
1554
1558
|
//#region lib/types/domain/errors.js
|
|
1555
1559
|
function formatMergeError(error) {
|
|
1556
1560
|
if (error.kind === "parameter") return "merge: " + error.message + " (" + error.code + ")";
|
|
1557
|
-
const
|
|
1558
|
-
return "merge failed: " + error.message + " (" + error.code + ")" +
|
|
1561
|
+
const where = error.stage === void 0 && error.runId === void 0 ? error.detail ?? "no additional detail" : [error.stage === void 0 ? void 0 : "stage=" + error.stage, error.runId === void 0 ? void 0 : "run=" + error.runId].filter(Boolean).join(" ");
|
|
1562
|
+
return "merge failed: " + error.message + " (" + error.code + ")\n" + where + "\nnext: fix the target or rerun after the cause is gone";
|
|
1559
1563
|
}
|
|
1560
1564
|
//#endregion
|
|
1561
1565
|
//#region lib/types/merge-command.js
|
|
@@ -1767,7 +1771,16 @@ async function runScan(options) {
|
|
|
1767
1771
|
if (!created.ok || created.row === null) throw new Error(created.ok ? "candidate row missing" : created.message);
|
|
1768
1772
|
row = created.row;
|
|
1769
1773
|
const placed = await upsertRow(options.io, options.home, options.now, row, "verify");
|
|
1770
|
-
if (!placed.ok
|
|
1774
|
+
if (!placed.ok && placed.current.state === "skipped") {
|
|
1775
|
+
const restored = transition(placed.current, {
|
|
1776
|
+
type: "restore",
|
|
1777
|
+
at: at(options.now)
|
|
1778
|
+
});
|
|
1779
|
+
if (!restored.ok || restored.row === null) throw new Error("cannot restore skipped pair");
|
|
1780
|
+
row = restored.row;
|
|
1781
|
+
const retried = await upsertRow(options.io, options.home, options.now, row, "verify");
|
|
1782
|
+
if (!retried.ok) throw new Error("pair already in " + retried.current.state);
|
|
1783
|
+
} else if (!placed.ok) throw new Error("pair already in " + placed.current.state);
|
|
1771
1784
|
} catch (error) {
|
|
1772
1785
|
totals.failed += 1;
|
|
1773
1786
|
pairs.push({
|
|
@@ -3286,14 +3299,23 @@ async function runAuthoring(options) {
|
|
|
3286
3299
|
}
|
|
3287
3300
|
//#endregion
|
|
3288
3301
|
//#region lib/types/revision.js
|
|
3289
|
-
|
|
3302
|
+
function fingerprint(card) {
|
|
3303
|
+
const files = [...card.files ?? []].sort((left, right) => left.path.localeCompare(right.path));
|
|
3304
|
+
return card.name + "\0" + card.body + "\0" + files.map((file) => file.path + "\0" + file.content).join("\0");
|
|
3305
|
+
}
|
|
3306
|
+
/** Stable source revision for a pair: names sorted, body+support hashed in that order. */
|
|
3290
3307
|
function sourceRevisionOf(cards, a, b) {
|
|
3291
3308
|
const byName = new Map(cards.map((card) => [card.name, card]));
|
|
3292
3309
|
const names = [a, b].sort();
|
|
3293
3310
|
const first = byName.get(names[0] ?? "");
|
|
3294
3311
|
const second = byName.get(names[1] ?? "");
|
|
3295
3312
|
if (first === void 0 || second === void 0) return void 0;
|
|
3296
|
-
return contentHash(first
|
|
3313
|
+
return contentHash(fingerprint(first) + "\0" + fingerprint(second));
|
|
3314
|
+
}
|
|
3315
|
+
/** Revision of the merge target; undefined when the target is not a live card. */
|
|
3316
|
+
function targetRevisionOf(cards, target) {
|
|
3317
|
+
const card = cards.find((item) => item.name === target);
|
|
3318
|
+
return card === void 0 ? void 0 : contentHash(fingerprint(card));
|
|
3297
3319
|
}
|
|
3298
3320
|
//#endregion
|
|
3299
3321
|
//#region lib/types/scan-ports.js
|
|
@@ -3354,11 +3376,14 @@ function createChannelScanPorts(options) {
|
|
|
3354
3376
|
});
|
|
3355
3377
|
}
|
|
3356
3378
|
const sourceRevision = result.outcome.kind === "merge" ? sourceRevisionOf(options.cards, candidate.a, candidate.b) : void 0;
|
|
3379
|
+
const target = result.outcome.kind === "merge" ? result.outcome.decision?.target : void 0;
|
|
3380
|
+
const targetRevision = target === void 0 ? void 0 : targetRevisionOf(options.cards, target);
|
|
3357
3381
|
return result.outcome.kind === "merge" ? {
|
|
3358
3382
|
...result.outcome,
|
|
3359
3383
|
stagingRef: stageKey,
|
|
3360
3384
|
stagingExpiresAt: options.now() + 7 * 864e5,
|
|
3361
|
-
...sourceRevision === void 0 ? {} : { sourceRevision }
|
|
3385
|
+
...sourceRevision === void 0 ? {} : { sourceRevision },
|
|
3386
|
+
...targetRevision === void 0 ? {} : { targetRevision }
|
|
3362
3387
|
} : result.outcome;
|
|
3363
3388
|
},
|
|
3364
3389
|
async round2(row, decision, signal) {
|
|
@@ -3653,6 +3678,10 @@ function createMergeRuntime(deps) {
|
|
|
3653
3678
|
const detached = deps.detached !== false;
|
|
3654
3679
|
if (command.verb === "pair") {
|
|
3655
3680
|
const signal = new AbortController().signal;
|
|
3681
|
+
if (command.a === command.b) return {
|
|
3682
|
+
ok: false,
|
|
3683
|
+
text: "merge pair 需要两个不同技能源: " + command.a
|
|
3684
|
+
};
|
|
3656
3685
|
const cards = await deps.cards(signal);
|
|
3657
3686
|
const cardA = cards.find((card) => card.name === command.a);
|
|
3658
3687
|
const cardB = cards.find((card) => card.name === command.b);
|
|
@@ -3660,6 +3689,10 @@ function createMergeRuntime(deps) {
|
|
|
3660
3689
|
ok: false,
|
|
3661
3690
|
text: "merge pair 仅支持 family-managed 技能: " + command.a + " / " + command.b
|
|
3662
3691
|
};
|
|
3692
|
+
if (cardA.protectedBy != null || cardB.protectedBy != null || cardA.protectionUnknown === true || cardB.protectionUnknown === true) return {
|
|
3693
|
+
ok: false,
|
|
3694
|
+
text: "merge pair 不接受受保护/保护状态未知技能: " + command.a + " / " + command.b
|
|
3695
|
+
};
|
|
3663
3696
|
const pair = [{
|
|
3664
3697
|
a: command.a,
|
|
3665
3698
|
b: command.b
|
|
@@ -3726,10 +3759,10 @@ function createMergeRuntime(deps) {
|
|
|
3726
3759
|
if (command.verb === "scan") {
|
|
3727
3760
|
const signal = new AbortController().signal;
|
|
3728
3761
|
const cards = await deps.cards(signal);
|
|
3729
|
-
const managed = new Set(cards.filter((card) => card.managed === true).map((card) => card.name));
|
|
3762
|
+
const managed = new Set(cards.filter((card) => card.managed === true && card.protectedBy == null).map((card) => card.name));
|
|
3730
3763
|
const loadedState = await loadMergeState(deps.io, deps.home);
|
|
3731
3764
|
const skipped = skippedPairKeys(loadedState.state.rows, deps.params.mergeSkippedTtlDays, deps.now());
|
|
3732
|
-
const held = new Set(loadedState.state.rows.filter((row) => row.queue === "pending" || row.queue === "fuse" || !canRetry(row)).map((row) => row.pairKey));
|
|
3765
|
+
const held = new Set(loadedState.state.rows.filter((row) => row.queue === "pending" || row.queue === "fuse" || row.queue === "verify" && !canRetry(row)).map((row) => row.pairKey));
|
|
3733
3766
|
const conflictNotes = loadedState.state.rows.filter((row) => row.state === "pending" || row.state === "approved" || row.state === "fuse" || row.state === "skipped").map((row) => "conflict:" + row.state + ":" + row.pairKey + ":" + row.id);
|
|
3734
3767
|
const result = await runScanCommand({
|
|
3735
3768
|
...common,
|
|
@@ -4012,6 +4045,18 @@ function createMergeRuntime(deps) {
|
|
|
4012
4045
|
text: "source-changed:源技能内容在决策后变化,拒绝照跑;出口:重新 merge pair,或 merge skipped --discard"
|
|
4013
4046
|
};
|
|
4014
4047
|
}
|
|
4048
|
+
const currentTargetRevision = targetRevisionOf(cards, candidate.decision.target);
|
|
4049
|
+
if (candidate.targetRevision !== void 0 && currentTargetRevision !== void 0 && currentTargetRevision !== candidate.targetRevision) {
|
|
4050
|
+
const taken = transition(candidate, {
|
|
4051
|
+
type: "target-taken",
|
|
4052
|
+
at: (/* @__PURE__ */ new Date()).toISOString()
|
|
4053
|
+
});
|
|
4054
|
+
if (taken.ok && taken.row !== null) await upsertRow(deps.io, deps.home, deps.now, taken.row);
|
|
4055
|
+
return {
|
|
4056
|
+
ok: false,
|
|
4057
|
+
text: "target-taken:目标 " + candidate.decision.target + " 在决策后变化,拒绝照跑;可重新 merge pair 或 discard"
|
|
4058
|
+
};
|
|
4059
|
+
}
|
|
4015
4060
|
if (candidate.decision.mode === "merge-into-existing" && !cards.some((card) => card.name === candidate.decision?.target)) {
|
|
4016
4061
|
const expired = transition(candidate, {
|
|
4017
4062
|
type: "target-taken",
|
package/lib/types/commands.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
const USAGE = 'usage: /evolution merge scan | pair [--force] <a> <b> | status [id] | report [id] | cancel [id] | runs | doctor | pending [--expired] | candidates [--retry] | approve <id> | reject <id> | resume [id] [--retry] [--clear <id>] | skipped [--restore <id>] | discard <id> | undo <target> --run <id
|
|
1
|
+
const USAGE = 'usage: /evolution merge scan | pair [--force] <a> <b> | status [id] | report [id] | cancel [id] | runs | doctor | pending [--expired] | candidates [--retry] | approve <id> | reject <id> | resume [id] [--retry] [--clear <id>] | skipped [--restore <id>] | discard <id> | undo <target> --run <id>\ndefaults: mergeApproval=false, mergeSkippedTtlDays=0, mergeRunTimeoutMinutes=0, mergePairTimeoutMinutes=15, mergeModelsRoot=""';
|
|
2
2
|
function flags(args, allowed) {
|
|
3
3
|
const values = new Map();
|
|
4
4
|
for (let index = 0; index < args.length; index += 1) {
|
|
@@ -5,6 +5,8 @@ export interface MergeError {
|
|
|
5
5
|
readonly code: string;
|
|
6
6
|
readonly message: string;
|
|
7
7
|
readonly detail?: string;
|
|
8
|
+
readonly stage?: string;
|
|
9
|
+
readonly runId?: string;
|
|
8
10
|
}
|
|
9
11
|
export declare function formatMergeError(error: MergeError): string;
|
|
10
12
|
//# sourceMappingURL=errors.d.ts.map
|
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
export function formatMergeError(error) {
|
|
2
2
|
if (error.kind === 'parameter')
|
|
3
3
|
return 'merge: ' + error.message + ' (' + error.code + ')';
|
|
4
|
-
const
|
|
5
|
-
|
|
4
|
+
const where = error.stage === undefined && error.runId === undefined
|
|
5
|
+
? (error.detail ?? 'no additional detail')
|
|
6
|
+
: [error.stage === undefined ? undefined : 'stage=' + error.stage, error.runId === undefined ? undefined : 'run=' + error.runId].filter(Boolean).join(' ');
|
|
7
|
+
return 'merge failed: ' + error.message + ' (' + error.code + ')\n' + where + '\nnext: fix the target or rerun after the cause is gone';
|
|
6
8
|
}
|
|
7
9
|
//# sourceMappingURL=errors.js.map
|
|
@@ -2,27 +2,28 @@ import { loadMergeState } from "./state.js";
|
|
|
2
2
|
import { formatQueue, formatStatus } from "./status.js";
|
|
3
3
|
import { mergeUsage } from "./commands.js";
|
|
4
4
|
export async function handleReadOnlyMergeCommand(command, io, home) {
|
|
5
|
+
const withNote = (text, note) => note === undefined ? text : 'note: ' + note + '\n' + text;
|
|
5
6
|
if (command.verb === 'status') {
|
|
6
7
|
const loaded = await loadMergeState(io, home);
|
|
7
8
|
if (command.id !== undefined) {
|
|
8
9
|
const row = loaded.state.rows.find(item => item.id === command.id);
|
|
9
|
-
return row === undefined ? 'merge row not found: ' + command.id : formatStatus([row]);
|
|
10
|
+
return withNote(row === undefined ? 'merge row not found: ' + command.id : formatStatus([row]), loaded.note);
|
|
10
11
|
}
|
|
11
|
-
return formatStatus(loaded.state.rows);
|
|
12
|
+
return withNote(formatStatus(loaded.state.rows), loaded.note);
|
|
12
13
|
}
|
|
13
14
|
if (command.verb === 'pending') {
|
|
14
15
|
const loaded = await loadMergeState(io, home);
|
|
15
16
|
const rows = command.expired === true ? loaded.state.rows.filter(row => row.expired !== undefined) : loaded.state.rows;
|
|
16
|
-
return command.expired === true ? formatStatus(rows) : formatQueue(rows, 'pending');
|
|
17
|
+
return withNote(command.expired === true ? formatStatus(rows) : formatQueue(rows, 'pending'), loaded.note);
|
|
17
18
|
}
|
|
18
19
|
if (command.verb === 'candidates') {
|
|
19
20
|
const loaded = await loadMergeState(io, home);
|
|
20
21
|
const rows = command.retry === true ? loaded.state.rows.filter(row => row.state === 'approved') : loaded.state.rows;
|
|
21
|
-
return formatQueue(rows, command.retry === true ? 'fuse' : 'verify');
|
|
22
|
+
return withNote(formatQueue(rows, command.retry === true ? 'fuse' : 'verify'), loaded.note);
|
|
22
23
|
}
|
|
23
24
|
if (command.verb === 'skipped') {
|
|
24
25
|
const loaded = await loadMergeState(io, home);
|
|
25
|
-
return formatQueue(loaded.state.rows, 'skipped');
|
|
26
|
+
return withNote(formatQueue(loaded.state.rows, 'skipped'), loaded.note);
|
|
26
27
|
}
|
|
27
28
|
return null;
|
|
28
29
|
}
|
package/lib/types/revision.d.ts
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import type { SkillCard } from './skill-card.ts';
|
|
2
|
-
/** Stable source revision for a pair: names sorted, body hashed in that order. */
|
|
2
|
+
/** Stable source revision for a pair: names sorted, body+support hashed in that order. */
|
|
3
3
|
export declare function sourceRevisionOf(cards: readonly SkillCard[], a: string, b: string): string | undefined;
|
|
4
|
+
/** Revision of the merge target; undefined when the target is not a live card. */
|
|
5
|
+
export declare function targetRevisionOf(cards: readonly SkillCard[], target: string): string | undefined;
|
|
4
6
|
//# sourceMappingURL=revision.d.ts.map
|
package/lib/types/revision.js
CHANGED
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
import { contentHash } from '@lmzhen/dsh-evolution-core';
|
|
2
|
-
|
|
2
|
+
function fingerprint(card) {
|
|
3
|
+
const files = [...(card.files ?? [])].sort((left, right) => left.path.localeCompare(right.path));
|
|
4
|
+
return card.name + '\u0000' + card.body + '\u0000' + files.map(file => file.path + '\u0000' + file.content).join('\u0000');
|
|
5
|
+
}
|
|
6
|
+
/** Stable source revision for a pair: names sorted, body+support hashed in that order. */
|
|
3
7
|
export function sourceRevisionOf(cards, a, b) {
|
|
4
8
|
const byName = new Map(cards.map(card => [card.name, card]));
|
|
5
9
|
const names = [a, b].sort();
|
|
@@ -7,6 +11,11 @@ export function sourceRevisionOf(cards, a, b) {
|
|
|
7
11
|
const second = byName.get(names[1] ?? '');
|
|
8
12
|
if (first === undefined || second === undefined)
|
|
9
13
|
return undefined;
|
|
10
|
-
return contentHash(first
|
|
14
|
+
return contentHash(fingerprint(first) + '\u0000' + fingerprint(second));
|
|
15
|
+
}
|
|
16
|
+
/** Revision of the merge target; undefined when the target is not a live card. */
|
|
17
|
+
export function targetRevisionOf(cards, target) {
|
|
18
|
+
const card = cards.find(item => item.name === target);
|
|
19
|
+
return card === undefined ? undefined : contentHash(fingerprint(card));
|
|
11
20
|
}
|
|
12
21
|
//# sourceMappingURL=revision.js.map
|
package/lib/types/runtime.js
CHANGED
|
@@ -22,7 +22,7 @@ import { createChannelScanPorts } from "./scan-ports.js";
|
|
|
22
22
|
import { createNewChildRound2 } from "./new-child-authoring.js";
|
|
23
23
|
import { skillLibraryPort } from "./adapters/library.js";
|
|
24
24
|
import { fitToolText } from "./presentation-budget.js";
|
|
25
|
-
import { sourceRevisionOf } from "./revision.js";
|
|
25
|
+
import { sourceRevisionOf, targetRevisionOf } from "./revision.js";
|
|
26
26
|
function forcedDecisionFor(a, b) {
|
|
27
27
|
const sorted = [a, b].sort();
|
|
28
28
|
const target = ('merge-' + sorted.join('-')).replace(/[^a-zA-Z0-9._-]+/g, '-').replace(/^-+|-+$/g, '').slice(0, 64);
|
|
@@ -47,12 +47,18 @@ export function createMergeRuntime(deps) {
|
|
|
47
47
|
const detached = deps.detached !== false;
|
|
48
48
|
if (command.verb === 'pair') {
|
|
49
49
|
const signal = new AbortController().signal;
|
|
50
|
+
if (command.a === command.b)
|
|
51
|
+
return { ok: false, text: 'merge pair 需要两个不同技能源: ' + command.a };
|
|
50
52
|
const cards = await deps.cards(signal);
|
|
51
53
|
const cardA = cards.find(card => card.name === command.a);
|
|
52
54
|
const cardB = cards.find(card => card.name === command.b);
|
|
53
55
|
if (cardA === undefined || cardB === undefined || cardA.managed !== true || cardB.managed !== true) {
|
|
54
56
|
return { ok: false, text: 'merge pair 仅支持 family-managed 技能: ' + command.a + ' / ' + command.b };
|
|
55
57
|
}
|
|
58
|
+
if (cardA.protectedBy != null || cardB.protectedBy != null
|
|
59
|
+
|| cardA.protectionUnknown === true || cardB.protectionUnknown === true) {
|
|
60
|
+
return { ok: false, text: 'merge pair 不接受受保护/保护状态未知技能: ' + command.a + ' / ' + command.b };
|
|
61
|
+
}
|
|
56
62
|
const pair = [{ a: command.a, b: command.b }];
|
|
57
63
|
const existing = (await loadMergeState(deps.io, deps.home)).state.rows.find(row => row.pairKey === pairKey(command.a, command.b));
|
|
58
64
|
if (existing?.state === 'skipped') {
|
|
@@ -95,10 +101,10 @@ export function createMergeRuntime(deps) {
|
|
|
95
101
|
if (command.verb === 'scan') {
|
|
96
102
|
const signal = new AbortController().signal;
|
|
97
103
|
const cards = await deps.cards(signal);
|
|
98
|
-
const managed = new Set(cards.filter(card => card.managed === true).map(card => card.name));
|
|
104
|
+
const managed = new Set(cards.filter(card => card.managed === true && card.protectedBy == null).map(card => card.name));
|
|
99
105
|
const loadedState = await loadMergeState(deps.io, deps.home);
|
|
100
106
|
const skipped = skippedPairKeys(loadedState.state.rows, deps.params.mergeSkippedTtlDays, deps.now());
|
|
101
|
-
const held = new Set(loadedState.state.rows.filter(row => row.queue === 'pending' || row.queue === 'fuse' || !canRetry(row)).map(row => row.pairKey));
|
|
107
|
+
const held = new Set(loadedState.state.rows.filter(row => row.queue === 'pending' || row.queue === 'fuse' || (row.queue === 'verify' && !canRetry(row))).map(row => row.pairKey));
|
|
102
108
|
const conflictNotes = loadedState.state.rows
|
|
103
109
|
.filter(row => row.state === 'pending' || row.state === 'approved' || row.state === 'fuse' || row.state === 'skipped')
|
|
104
110
|
.map(row => 'conflict:' + row.state + ':' + row.pairKey + ':' + row.id);
|
|
@@ -273,6 +279,14 @@ export function createMergeRuntime(deps) {
|
|
|
273
279
|
await upsertRow(deps.io, deps.home, deps.now, changed.row);
|
|
274
280
|
return { ok: false, text: 'source-changed:源技能内容在决策后变化,拒绝照跑;出口:重新 merge pair,或 merge skipped --discard' };
|
|
275
281
|
}
|
|
282
|
+
const currentTargetRevision = targetRevisionOf(cards, candidate.decision.target);
|
|
283
|
+
if (candidate.targetRevision !== undefined && currentTargetRevision !== undefined
|
|
284
|
+
&& currentTargetRevision !== candidate.targetRevision) {
|
|
285
|
+
const taken = transition(candidate, { type: 'target-taken', at: new Date().toISOString() });
|
|
286
|
+
if (taken.ok && taken.row !== null)
|
|
287
|
+
await upsertRow(deps.io, deps.home, deps.now, taken.row);
|
|
288
|
+
return { ok: false, text: 'target-taken:目标 ' + candidate.decision.target + ' 在决策后变化,拒绝照跑;可重新 merge pair 或 discard' };
|
|
289
|
+
}
|
|
276
290
|
if (candidate.decision.mode === 'merge-into-existing' && !cards.some(card => card.name === candidate.decision?.target)) {
|
|
277
291
|
const expired = transition(candidate, { type: 'target-taken', at: new Date().toISOString() });
|
|
278
292
|
if (expired.ok && expired.row !== null)
|
package/lib/types/scan-ports.js
CHANGED
|
@@ -2,7 +2,7 @@ import { pairKey } from "./contracts.js";
|
|
|
2
2
|
import { runRound1OverChannel } from "./channel-port.js";
|
|
3
3
|
import { runAuthoring } from "./authoring-flow.js";
|
|
4
4
|
import { dropStaging, ensureStaging, stageKeyOf, stagingRoot } from "./staging.js";
|
|
5
|
-
import { sourceRevisionOf } from "./revision.js";
|
|
5
|
+
import { sourceRevisionOf, targetRevisionOf } from "./revision.js";
|
|
6
6
|
export function createChannelScanPorts(options) {
|
|
7
7
|
const sessions = new Map();
|
|
8
8
|
return {
|
|
@@ -44,10 +44,13 @@ export function createChannelScanPorts(options) {
|
|
|
44
44
|
}
|
|
45
45
|
}
|
|
46
46
|
const sourceRevision = result.outcome.kind === 'merge' ? sourceRevisionOf(options.cards, candidate.a, candidate.b) : undefined;
|
|
47
|
+
const target = result.outcome.kind === 'merge' ? result.outcome.decision?.target : undefined;
|
|
48
|
+
const targetRevision = target === undefined ? undefined : targetRevisionOf(options.cards, target);
|
|
47
49
|
return result.outcome.kind === 'merge'
|
|
48
50
|
? {
|
|
49
51
|
...result.outcome, stagingRef: stageKey, stagingExpiresAt: options.now() + 7 * 86_400_000,
|
|
50
52
|
...(sourceRevision === undefined ? {} : { sourceRevision }),
|
|
53
|
+
...(targetRevision === undefined ? {} : { targetRevision }),
|
|
51
54
|
}
|
|
52
55
|
: result.outcome;
|
|
53
56
|
},
|
package/lib/types/scan-run.js
CHANGED
|
@@ -66,8 +66,18 @@ export async function runScan(options) {
|
|
|
66
66
|
throw new Error(created.ok ? 'candidate row missing' : created.message);
|
|
67
67
|
row = created.row;
|
|
68
68
|
const placed = await upsertRow(options.io, options.home, options.now, row, 'verify');
|
|
69
|
-
if (!placed.ok)
|
|
69
|
+
if (!placed.ok && placed.current.state === 'skipped') {
|
|
70
|
+
const restored = transition(placed.current, { type: 'restore', at: at(options.now) });
|
|
71
|
+
if (!restored.ok || restored.row === null)
|
|
72
|
+
throw new Error('cannot restore skipped pair');
|
|
73
|
+
row = restored.row;
|
|
74
|
+
const retried = await upsertRow(options.io, options.home, options.now, row, 'verify');
|
|
75
|
+
if (!retried.ok)
|
|
76
|
+
throw new Error('pair already in ' + retried.current.state);
|
|
77
|
+
}
|
|
78
|
+
else if (!placed.ok) {
|
|
70
79
|
throw new Error('pair already in ' + placed.current.state);
|
|
80
|
+
}
|
|
71
81
|
}
|
|
72
82
|
catch (error) {
|
|
73
83
|
totals.failed += 1;
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lmzhen/dsh-evolution-merge",
|
|
3
3
|
"description": "Skill auto-merge domain: screening, two-round authoring, queues and commit orchestration (community build)",
|
|
4
|
-
"version": "0.30.
|
|
4
|
+
"version": "0.30.6",
|
|
5
5
|
"publishConfig": {
|
|
6
6
|
"access": "public"
|
|
7
7
|
},
|
|
@@ -27,8 +27,8 @@
|
|
|
27
27
|
"license": "MIT",
|
|
28
28
|
"dependencies": {
|
|
29
29
|
"@deepseek-ai/schemastery": "^3.18.5",
|
|
30
|
-
"@lmzhen/dsh-evolution-core": "^0.30.
|
|
31
|
-
"@lmzhen/dsh-evolution-merge-engine": "^0.30.
|
|
30
|
+
"@lmzhen/dsh-evolution-core": "^0.30.6",
|
|
31
|
+
"@lmzhen/dsh-evolution-merge-engine": "^0.30.6"
|
|
32
32
|
},
|
|
33
33
|
"peerDependencies": {
|
|
34
34
|
"@deepseek-ai/cordis": "^4.0.5"
|