@lmzhen/dsh-evolution-merge 0.30.12 → 0.30.13

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
@@ -1796,7 +1796,10 @@ async function runScan(options) {
1796
1796
  at: at(options.now)
1797
1797
  });
1798
1798
  if (!created.ok || created.row === null) throw new Error(created.ok ? "candidate row missing" : created.message);
1799
- row = created.row;
1799
+ row = options.runId === void 0 ? created.row : {
1800
+ ...created.row,
1801
+ runId: options.runId
1802
+ };
1800
1803
  const before = (await loadMergeState(options.io, options.home)).state.rows.find((item) => item.pairKey === row.pairKey);
1801
1804
  const placed = await upsertRow(options.io, options.home, options.now, row, "verify", before?.updatedAt ?? null);
1802
1805
  if (!placed.ok && before?.state === "skipped") {
@@ -2173,6 +2176,7 @@ async function runScanCommand(options) {
2173
2176
  ports,
2174
2177
  stage: ctx.stage,
2175
2178
  startedAt: ctx.startedAt,
2179
+ runId: ctx.runId,
2176
2180
  ...options.timeoutMinutes === void 0 ? {} : { timeoutMinutes: options.timeoutMinutes },
2177
2181
  ...options.notes === void 0 ? {} : { notes: options.notes }
2178
2182
  });
@@ -2249,37 +2253,51 @@ function releaseClaim(current, token) {
2249
2253
  //#endregion
2250
2254
  //#region lib/types/target-claim.js
2251
2255
  /** Durable target claim: one approve writer per merge target at a time. */
2252
- const LEASE_MS = 5 * 6e4;
2256
+ const LEASE_MS = 30 * 6e4;
2253
2257
  function claimPath(home, target) {
2254
2258
  return join(home, "merge", "claims", contentHash(target) + ".json");
2255
2259
  }
2256
- async function readClaim(io, home, target) {
2257
- const path = claimPath(home, target);
2258
- if (!await io.exists(path)) return void 0;
2259
- const raw = await io.readText(path);
2260
+ function parseClaim(raw) {
2260
2261
  if (raw === null) return void 0;
2261
2262
  const parsed = JSON.parse(raw);
2262
- if (typeof parsed.token !== "string" || typeof parsed.expiresAt !== "number") throw new Error("corrupt target claim: " + target);
2263
+ if (typeof parsed.token !== "string" || typeof parsed.expiresAt !== "number") throw new Error("corrupt target claim");
2263
2264
  return {
2264
2265
  id: "claim",
2265
2266
  token: parsed.token,
2266
2267
  expiresAt: parsed.expiresAt
2267
2268
  };
2268
2269
  }
2270
+ async function compareAndSwap(io, path, task) {
2271
+ if (io.transact !== void 0) {
2272
+ await io.transact(path, (current) => task(current));
2273
+ return;
2274
+ }
2275
+ const current = await io.exists(path) ? await io.readText(path) : null;
2276
+ const next = task(current);
2277
+ if (next === null) {
2278
+ if (current !== null) try {
2279
+ await io.remove(path);
2280
+ } catch {}
2281
+ } else if (next !== current) await io.writeText(path, next);
2282
+ }
2269
2283
  async function acquireTargetClaim(io, home, target, now) {
2270
- const current = await readClaim(io, home, target);
2284
+ const path = claimPath(home, target);
2271
2285
  const token = randomUUID();
2272
- const claim = acquireClaim(current, now, LEASE_MS, token);
2273
- if (claim === void 0) return null;
2274
- await io.writeText(claimPath(home, target), JSON.stringify(claim) + "\n");
2286
+ let granted;
2287
+ await compareAndSwap(io, path, (raw) => {
2288
+ const claim = acquireClaim(parseClaim(raw), now, LEASE_MS, token);
2289
+ if (claim === void 0) return raw;
2290
+ granted = claim;
2291
+ return JSON.stringify(claim) + "\n";
2292
+ });
2293
+ if (granted === void 0) return null;
2275
2294
  return {
2276
2295
  token,
2277
2296
  async release() {
2278
- const next = releaseClaim(await readClaim(io, home, target), token);
2279
- if (next === void 0) try {
2280
- await io.remove(claimPath(home, target));
2281
- } catch {}
2282
- else await io.writeText(claimPath(home, target), JSON.stringify(next) + "\n");
2297
+ await compareAndSwap(io, path, (raw) => {
2298
+ const next = releaseClaim(parseClaim(raw), token);
2299
+ return next === void 0 ? null : JSON.stringify(next) + "\n";
2300
+ });
2283
2301
  }
2284
2302
  };
2285
2303
  }
@@ -3787,7 +3805,7 @@ function createMergeRuntime(deps) {
3787
3805
  ok: false,
3788
3806
  text: "merge pair 不接受受保护/保护状态未知技能: " + command.a + " / " + command.b
3789
3807
  };
3790
- if (deps.isBlocked?.(command.a) === true || deps.isBlocked?.(command.b) === true) return {
3808
+ if (await deps.isBlocked?.(command.a) === true || await deps.isBlocked?.(command.b) === true) return {
3791
3809
  ok: false,
3792
3810
  text: "merge pair 不接受被范围门拦下的技能: " + command.a + " / " + command.b
3793
3811
  };
@@ -3857,10 +3875,12 @@ function createMergeRuntime(deps) {
3857
3875
  if (command.verb === "scan") {
3858
3876
  const signal = new AbortController().signal;
3859
3877
  const cards = await deps.cards(signal);
3860
- const managed = new Set(cards.filter((card) => card.managed === true && card.protectedBy == null && deps.isBlocked?.(card.name) !== true).map((card) => card.name));
3878
+ const blocked = /* @__PURE__ */ new Set();
3879
+ for (const card of cards) if (await deps.isBlocked?.(card.name) === true) blocked.add(card.name);
3880
+ const managed = new Set(cards.filter((card) => card.managed === true && card.protectedBy == null && !blocked.has(card.name)).map((card) => card.name));
3861
3881
  const loadedState = await loadMergeState(deps.io, deps.home);
3862
3882
  const skipped = skippedPairKeys(loadedState.state.rows, deps.params.mergeSkippedTtlDays, deps.now());
3863
- const held = new Set(loadedState.state.rows.filter((row) => row.queue === "pending" || row.queue === "fuse" || row.queue === "verify" && !canRetry(row)).map((row) => row.pairKey));
3883
+ const held = new Set(loadedState.state.rows.filter((row) => row.queue === "pending" || row.queue === "fuse" || row.queue === "verify" && (row.runId !== void 0 && deps.now() - Date.parse(row.updatedAt) < 10 * 6e4 || !canRetry(row))).map((row) => row.pairKey));
3864
3884
  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);
3865
3885
  const result = await runScanCommand({
3866
3886
  ...common,
@@ -4795,8 +4815,8 @@ function apply(ctx, rawConfig = {}) {
4795
4815
  parent: void 0,
4796
4816
  allow: ["merge_stage"],
4797
4817
  timeoutMinutes: params.mergeRunTimeoutMinutes,
4798
- isBlocked: (name) => {
4799
- return ctx.get("evolutionCurator")?.gates?.isBlocked?.(name) === true;
4818
+ isBlocked: async (name) => {
4819
+ return await ctx.get("evolutionCurator")?.isBlocked?.(name) === true;
4800
4820
  },
4801
4821
  async cards(signal) {
4802
4822
  const listed = await skills.list({ withContent: true });
@@ -218,9 +218,9 @@ export function apply(ctx, rawConfig = {}) {
218
218
  const runtime = createMergeRuntime({
219
219
  io, home, registry: runs, skills, now: () => Date.now(), params, platformWrite: true,
220
220
  channel, parent: undefined, allow: ['merge_stage'], timeoutMinutes: params.mergeRunTimeoutMinutes,
221
- isBlocked: (name) => {
221
+ isBlocked: async (name) => {
222
222
  const curator = ctx.get('evolutionCurator');
223
- return curator?.gates?.isBlocked?.(name) === true;
223
+ return (await curator?.isBlocked?.(name)) === true;
224
224
  },
225
225
  async cards(signal) {
226
226
  const listed = await skills.list({ withContent: true });
@@ -32,7 +32,7 @@ export interface MergeRuntimeDeps {
32
32
  readonly detached?: boolean;
33
33
  readonly cards: (signal: AbortSignal) => Promise<readonly SkillCard[]>;
34
34
  /** Curator gate set: excluded/referenced/suppressed/protected builtins. */
35
- readonly isBlocked?: (name: string) => boolean;
35
+ readonly isBlocked?: (name: string) => boolean | Promise<boolean>;
36
36
  /** Run-scoped engine screening. */
37
37
  readonly screeningForRun: (cards: readonly SkillCard[], runId: string, signal: AbortSignal) => Promise<readonly MergeCandidate[]>;
38
38
  readonly stage?: MergeStageBindingPort;
@@ -59,7 +59,7 @@ export function createMergeRuntime(deps) {
59
59
  || cardA.protectionUnknown === true || cardB.protectionUnknown === true) {
60
60
  return { ok: false, text: 'merge pair 不接受受保护/保护状态未知技能: ' + command.a + ' / ' + command.b };
61
61
  }
62
- if (deps.isBlocked?.(command.a) === true || deps.isBlocked?.(command.b) === true) {
62
+ if ((await deps.isBlocked?.(command.a)) === true || (await deps.isBlocked?.(command.b)) === true) {
63
63
  return { ok: false, text: 'merge pair 不接受被范围门拦下的技能: ' + command.a + ' / ' + command.b };
64
64
  }
65
65
  const pair = [{ a: command.a, b: command.b }];
@@ -104,12 +104,18 @@ export function createMergeRuntime(deps) {
104
104
  if (command.verb === 'scan') {
105
105
  const signal = new AbortController().signal;
106
106
  const cards = await deps.cards(signal);
107
+ const blocked = new Set();
108
+ for (const card of cards)
109
+ if ((await deps.isBlocked?.(card.name)) === true)
110
+ blocked.add(card.name);
107
111
  const managed = new Set(cards
108
- .filter(card => card.managed === true && card.protectedBy == null && deps.isBlocked?.(card.name) !== true)
112
+ .filter(card => card.managed === true && card.protectedBy == null && !blocked.has(card.name))
109
113
  .map(card => card.name));
110
114
  const loadedState = await loadMergeState(deps.io, deps.home);
111
115
  const skipped = skippedPairKeys(loadedState.state.rows, deps.params.mergeSkippedTtlDays, deps.now());
112
- const held = new Set(loadedState.state.rows.filter(row => row.queue === 'pending' || row.queue === 'fuse' || (row.queue === 'verify' && !canRetry(row))).map(row => row.pairKey));
116
+ const held = new Set(loadedState.state.rows.filter(row => row.queue === 'pending' || row.queue === 'fuse'
117
+ || (row.queue === 'verify' && (row.runId !== undefined && deps.now() - Date.parse(row.updatedAt) < 10 * 60_000 || !canRetry(row))))
118
+ .map(row => row.pairKey));
113
119
  const conflictNotes = loadedState.state.rows
114
120
  .filter(row => row.state === 'pending' || row.state === 'approved' || row.state === 'fuse' || row.state === 'skipped')
115
121
  .map(row => 'conflict:' + row.state + ':' + row.pairKey + ':' + row.id);
@@ -22,6 +22,7 @@ export async function runScanCommand(options) {
22
22
  ports,
23
23
  stage: ctx.stage,
24
24
  startedAt: ctx.startedAt,
25
+ runId: ctx.runId,
25
26
  ...(options.timeoutMinutes === undefined ? {} : { timeoutMinutes: options.timeoutMinutes }),
26
27
  ...(options.notes === undefined ? {} : { notes: options.notes }),
27
28
  });
@@ -44,6 +44,7 @@ export interface RunScanOptions {
44
44
  total: number;
45
45
  }) => Promise<void>;
46
46
  readonly notes?: readonly string[];
47
+ readonly runId?: string;
47
48
  }
48
49
  export declare function runScan(options: RunScanOptions): Promise<MergeRunReportDraft>;
49
50
  //# sourceMappingURL=scan-run.d.ts.map
@@ -64,7 +64,7 @@ export async function runScan(options) {
64
64
  const created = transition(undefined, { type: 'candidate', a: candidate.a, b: candidate.b, at: at(options.now) });
65
65
  if (!created.ok || created.row === null)
66
66
  throw new Error(created.ok ? 'candidate row missing' : created.message);
67
- row = created.row;
67
+ row = options.runId === undefined ? created.row : { ...created.row, runId: options.runId };
68
68
  const before = (await loadMergeState(options.io, options.home)).state.rows.find(item => item.pairKey === row.pairKey);
69
69
  const placed = await upsertRow(options.io, options.home, options.now, row, 'verify', before?.updatedAt ?? null);
70
70
  if (!placed.ok && before?.state === 'skipped') {
@@ -3,43 +3,56 @@ import { randomUUID } from 'node:crypto';
3
3
  import { join } from 'node:path';
4
4
  import { contentHash } from '@lmzhen/dsh-evolution-core';
5
5
  import { acquireClaim, releaseClaim } from "./domain/claim.js";
6
- const LEASE_MS = 5 * 60_000;
6
+ const LEASE_MS = 30 * 60_000;
7
7
  function claimPath(home, target) {
8
8
  return join(home, 'merge', 'claims', contentHash(target) + '.json');
9
9
  }
10
- async function readClaim(io, home, target) {
11
- const path = claimPath(home, target);
12
- if (!(await io.exists(path)))
13
- return undefined;
14
- const raw = await io.readText(path);
10
+ function parseClaim(raw) {
15
11
  if (raw === null)
16
12
  return undefined;
17
13
  const parsed = JSON.parse(raw);
18
14
  if (typeof parsed.token !== 'string' || typeof parsed.expiresAt !== 'number')
19
- throw new Error('corrupt target claim: ' + target);
15
+ throw new Error('corrupt target claim');
20
16
  return { id: 'claim', token: parsed.token, expiresAt: parsed.expiresAt };
21
17
  }
18
+ async function compareAndSwap(io, path, task) {
19
+ if (io.transact !== undefined) {
20
+ await io.transact(path, current => task(current));
21
+ return;
22
+ }
23
+ const current = (await io.exists(path)) ? await io.readText(path) : null;
24
+ const next = task(current);
25
+ if (next === null) {
26
+ if (current !== null)
27
+ try {
28
+ await io.remove(path);
29
+ }
30
+ catch { /* already gone */ }
31
+ }
32
+ else if (next !== current) {
33
+ await io.writeText(path, next);
34
+ }
35
+ }
22
36
  export async function acquireTargetClaim(io, home, target, now) {
23
- const current = await readClaim(io, home, target);
37
+ const path = claimPath(home, target);
24
38
  const token = randomUUID();
25
- const claim = acquireClaim(current, now, LEASE_MS, token);
26
- if (claim === undefined)
39
+ let granted;
40
+ await compareAndSwap(io, path, (raw) => {
41
+ const claim = acquireClaim(parseClaim(raw), now, LEASE_MS, token);
42
+ if (claim === undefined)
43
+ return raw;
44
+ granted = claim;
45
+ return JSON.stringify(claim) + '\n';
46
+ });
47
+ if (granted === undefined)
27
48
  return null;
28
- await io.writeText(claimPath(home, target), JSON.stringify(claim) + '\n');
29
49
  return {
30
50
  token,
31
51
  async release() {
32
- const latest = await readClaim(io, home, target);
33
- const next = releaseClaim(latest, token);
34
- if (next === undefined) {
35
- try {
36
- await io.remove(claimPath(home, target));
37
- }
38
- catch { /* already gone */ }
39
- }
40
- else {
41
- await io.writeText(claimPath(home, target), JSON.stringify(next) + '\n');
42
- }
52
+ await compareAndSwap(io, path, (raw) => {
53
+ const next = releaseClaim(parseClaim(raw), token);
54
+ return next === undefined ? null : JSON.stringify(next) + '\n';
55
+ });
43
56
  },
44
57
  };
45
58
  }
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.12",
4
+ "version": "0.30.13",
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.12",
31
- "@lmzhen/dsh-evolution-merge-engine": "^0.30.12"
30
+ "@lmzhen/dsh-evolution-core": "^0.30.13",
31
+ "@lmzhen/dsh-evolution-merge-engine": "^0.30.13"
32
32
  },
33
33
  "peerDependencies": {
34
34
  "@deepseek-ai/cordis": "^4.0.5"