@lmzhen/dsh-evolution-merge 0.30.4 → 0.30.5

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 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
  }
@@ -3286,14 +3286,23 @@ async function runAuthoring(options) {
3286
3286
  }
3287
3287
  //#endregion
3288
3288
  //#region lib/types/revision.js
3289
- /** Stable source revision for a pair: names sorted, body hashed in that order. */
3289
+ function fingerprint(card) {
3290
+ const files = [...card.files ?? []].sort((left, right) => left.path.localeCompare(right.path));
3291
+ return card.name + "\0" + card.body + "\0" + files.map((file) => file.path + "\0" + file.content).join("\0");
3292
+ }
3293
+ /** Stable source revision for a pair: names sorted, body+support hashed in that order. */
3290
3294
  function sourceRevisionOf(cards, a, b) {
3291
3295
  const byName = new Map(cards.map((card) => [card.name, card]));
3292
3296
  const names = [a, b].sort();
3293
3297
  const first = byName.get(names[0] ?? "");
3294
3298
  const second = byName.get(names[1] ?? "");
3295
3299
  if (first === void 0 || second === void 0) return void 0;
3296
- return contentHash(first.name + "\0" + first.body + "\0" + second.name + "\0" + second.body);
3300
+ return contentHash(fingerprint(first) + "\0" + fingerprint(second));
3301
+ }
3302
+ /** Revision of the merge target; undefined when the target is not a live card. */
3303
+ function targetRevisionOf(cards, target) {
3304
+ const card = cards.find((item) => item.name === target);
3305
+ return card === void 0 ? void 0 : contentHash(fingerprint(card));
3297
3306
  }
3298
3307
  //#endregion
3299
3308
  //#region lib/types/scan-ports.js
@@ -3354,11 +3363,14 @@ function createChannelScanPorts(options) {
3354
3363
  });
3355
3364
  }
3356
3365
  const sourceRevision = result.outcome.kind === "merge" ? sourceRevisionOf(options.cards, candidate.a, candidate.b) : void 0;
3366
+ const target = result.outcome.kind === "merge" ? result.outcome.decision?.target : void 0;
3367
+ const targetRevision = target === void 0 ? void 0 : targetRevisionOf(options.cards, target);
3357
3368
  return result.outcome.kind === "merge" ? {
3358
3369
  ...result.outcome,
3359
3370
  stagingRef: stageKey,
3360
3371
  stagingExpiresAt: options.now() + 7 * 864e5,
3361
- ...sourceRevision === void 0 ? {} : { sourceRevision }
3372
+ ...sourceRevision === void 0 ? {} : { sourceRevision },
3373
+ ...targetRevision === void 0 ? {} : { targetRevision }
3362
3374
  } : result.outcome;
3363
3375
  },
3364
3376
  async round2(row, decision, signal) {
@@ -3653,6 +3665,10 @@ function createMergeRuntime(deps) {
3653
3665
  const detached = deps.detached !== false;
3654
3666
  if (command.verb === "pair") {
3655
3667
  const signal = new AbortController().signal;
3668
+ if (command.a === command.b) return {
3669
+ ok: false,
3670
+ text: "merge pair 需要两个不同技能源: " + command.a
3671
+ };
3656
3672
  const cards = await deps.cards(signal);
3657
3673
  const cardA = cards.find((card) => card.name === command.a);
3658
3674
  const cardB = cards.find((card) => card.name === command.b);
@@ -3660,6 +3676,10 @@ function createMergeRuntime(deps) {
3660
3676
  ok: false,
3661
3677
  text: "merge pair 仅支持 family-managed 技能: " + command.a + " / " + command.b
3662
3678
  };
3679
+ if (cardA.protectedBy != null || cardB.protectedBy != null || cardA.protectionUnknown === true || cardB.protectionUnknown === true) return {
3680
+ ok: false,
3681
+ text: "merge pair 不接受受保护/保护状态未知技能: " + command.a + " / " + command.b
3682
+ };
3663
3683
  const pair = [{
3664
3684
  a: command.a,
3665
3685
  b: command.b
@@ -3726,7 +3746,7 @@ function createMergeRuntime(deps) {
3726
3746
  if (command.verb === "scan") {
3727
3747
  const signal = new AbortController().signal;
3728
3748
  const cards = await deps.cards(signal);
3729
- const managed = new Set(cards.filter((card) => card.managed === true).map((card) => card.name));
3749
+ const managed = new Set(cards.filter((card) => card.managed === true && card.protectedBy == null).map((card) => card.name));
3730
3750
  const loadedState = await loadMergeState(deps.io, deps.home);
3731
3751
  const skipped = skippedPairKeys(loadedState.state.rows, deps.params.mergeSkippedTtlDays, deps.now());
3732
3752
  const held = new Set(loadedState.state.rows.filter((row) => row.queue === "pending" || row.queue === "fuse" || !canRetry(row)).map((row) => row.pairKey));
@@ -4012,6 +4032,18 @@ function createMergeRuntime(deps) {
4012
4032
  text: "source-changed:源技能内容在决策后变化,拒绝照跑;出口:重新 merge pair,或 merge skipped --discard"
4013
4033
  };
4014
4034
  }
4035
+ const currentTargetRevision = targetRevisionOf(cards, candidate.decision.target);
4036
+ if (candidate.targetRevision !== void 0 && currentTargetRevision !== void 0 && currentTargetRevision !== candidate.targetRevision) {
4037
+ const taken = transition(candidate, {
4038
+ type: "target-taken",
4039
+ at: (/* @__PURE__ */ new Date()).toISOString()
4040
+ });
4041
+ if (taken.ok && taken.row !== null) await upsertRow(deps.io, deps.home, deps.now, taken.row);
4042
+ return {
4043
+ ok: false,
4044
+ text: "target-taken:目标 " + candidate.decision.target + " 在决策后变化,拒绝照跑;可重新 merge pair 或 discard"
4045
+ };
4046
+ }
4015
4047
  if (candidate.decision.mode === "merge-into-existing" && !cards.some((card) => card.name === candidate.decision?.target)) {
4016
4048
  const expired = transition(candidate, {
4017
4049
  type: "target-taken",
@@ -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) {
@@ -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
@@ -1,5 +1,9 @@
1
1
  import { contentHash } from '@lmzhen/dsh-evolution-core';
2
- /** Stable source revision for a pair: names sorted, body hashed in that order. */
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.name + '\u0000' + first.body + '\u0000' + second.name + '\u0000' + second.body);
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
@@ -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,7 +101,7 @@ 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
107
  const held = new Set(loadedState.state.rows.filter(row => row.queue === 'pending' || row.queue === 'fuse' || !canRetry(row)).map(row => row.pairKey));
@@ -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)
@@ -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/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",
4
+ "version": "0.30.5",
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.4",
31
- "@lmzhen/dsh-evolution-merge-engine": "^0.30.4"
30
+ "@lmzhen/dsh-evolution-core": "^0.30.5",
31
+ "@lmzhen/dsh-evolution-merge-engine": "^0.30.5"
32
32
  },
33
33
  "peerDependencies": {
34
34
  "@deepseek-ai/cordis": "^4.0.5"