@lmzhen/dsh-evolution-merge 0.30.12 → 0.30.14
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 +78 -44
- package/lib/types/index.js +6 -2
- package/lib/types/resume-command.d.ts +1 -0
- package/lib/types/resume-command.js +3 -0
- package/lib/types/runtime.d.ts +2 -1
- package/lib/types/runtime.js +11 -3
- package/lib/types/scan-command.d.ts +1 -0
- package/lib/types/scan-command.js +2 -0
- package/lib/types/scan-run.d.ts +2 -0
- package/lib/types/scan-run.js +5 -1
- package/lib/types/target-claim.js +35 -22
- package/package.json +3 -3
package/lib/index.js
CHANGED
|
@@ -1796,8 +1796,12 @@ 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);
|
|
1804
|
+
if (before?.state === "verify" && before.runId !== void 0 && before.runId !== options.runId && await options.isOwnerActive?.(before.runId) === true) continue;
|
|
1801
1805
|
const placed = await upsertRow(options.io, options.home, options.now, row, "verify", before?.updatedAt ?? null);
|
|
1802
1806
|
if (!placed.ok && before?.state === "skipped") {
|
|
1803
1807
|
const restored = transition(before, {
|
|
@@ -2173,8 +2177,10 @@ async function runScanCommand(options) {
|
|
|
2173
2177
|
ports,
|
|
2174
2178
|
stage: ctx.stage,
|
|
2175
2179
|
startedAt: ctx.startedAt,
|
|
2180
|
+
runId: ctx.runId,
|
|
2176
2181
|
...options.timeoutMinutes === void 0 ? {} : { timeoutMinutes: options.timeoutMinutes },
|
|
2177
|
-
...options.notes === void 0 ? {} : { notes: options.notes }
|
|
2182
|
+
...options.notes === void 0 ? {} : { notes: options.notes },
|
|
2183
|
+
...options.isOwnerActive === void 0 ? {} : { isOwnerActive: options.isOwnerActive }
|
|
2178
2184
|
});
|
|
2179
2185
|
}
|
|
2180
2186
|
});
|
|
@@ -2249,37 +2255,51 @@ function releaseClaim(current, token) {
|
|
|
2249
2255
|
//#endregion
|
|
2250
2256
|
//#region lib/types/target-claim.js
|
|
2251
2257
|
/** Durable target claim: one approve writer per merge target at a time. */
|
|
2252
|
-
const LEASE_MS =
|
|
2258
|
+
const LEASE_MS = 30 * 6e4;
|
|
2253
2259
|
function claimPath(home, target) {
|
|
2254
2260
|
return join(home, "merge", "claims", contentHash(target) + ".json");
|
|
2255
2261
|
}
|
|
2256
|
-
|
|
2257
|
-
const path = claimPath(home, target);
|
|
2258
|
-
if (!await io.exists(path)) return void 0;
|
|
2259
|
-
const raw = await io.readText(path);
|
|
2262
|
+
function parseClaim(raw) {
|
|
2260
2263
|
if (raw === null) return void 0;
|
|
2261
2264
|
const parsed = JSON.parse(raw);
|
|
2262
|
-
if (typeof parsed.token !== "string" || typeof parsed.expiresAt !== "number") throw new Error("corrupt target claim
|
|
2265
|
+
if (typeof parsed.token !== "string" || typeof parsed.expiresAt !== "number") throw new Error("corrupt target claim");
|
|
2263
2266
|
return {
|
|
2264
2267
|
id: "claim",
|
|
2265
2268
|
token: parsed.token,
|
|
2266
2269
|
expiresAt: parsed.expiresAt
|
|
2267
2270
|
};
|
|
2268
2271
|
}
|
|
2272
|
+
async function compareAndSwap(io, path, task) {
|
|
2273
|
+
if (io.transact !== void 0) {
|
|
2274
|
+
await io.transact(path, (current) => task(current));
|
|
2275
|
+
return;
|
|
2276
|
+
}
|
|
2277
|
+
const current = await io.exists(path) ? await io.readText(path) : null;
|
|
2278
|
+
const next = task(current);
|
|
2279
|
+
if (next === null) {
|
|
2280
|
+
if (current !== null) try {
|
|
2281
|
+
await io.remove(path);
|
|
2282
|
+
} catch {}
|
|
2283
|
+
} else if (next !== current) await io.writeText(path, next);
|
|
2284
|
+
}
|
|
2269
2285
|
async function acquireTargetClaim(io, home, target, now) {
|
|
2270
|
-
const
|
|
2286
|
+
const path = claimPath(home, target);
|
|
2271
2287
|
const token = randomUUID();
|
|
2272
|
-
|
|
2273
|
-
|
|
2274
|
-
|
|
2288
|
+
let granted;
|
|
2289
|
+
await compareAndSwap(io, path, (raw) => {
|
|
2290
|
+
const claim = acquireClaim(parseClaim(raw), now, LEASE_MS, token);
|
|
2291
|
+
if (claim === void 0) return raw;
|
|
2292
|
+
granted = claim;
|
|
2293
|
+
return JSON.stringify(claim) + "\n";
|
|
2294
|
+
});
|
|
2295
|
+
if (granted === void 0) return null;
|
|
2275
2296
|
return {
|
|
2276
2297
|
token,
|
|
2277
2298
|
async release() {
|
|
2278
|
-
|
|
2279
|
-
|
|
2280
|
-
|
|
2281
|
-
}
|
|
2282
|
-
else await io.writeText(claimPath(home, target), JSON.stringify(next) + "\n");
|
|
2299
|
+
await compareAndSwap(io, path, (raw) => {
|
|
2300
|
+
const next = releaseClaim(parseClaim(raw), token);
|
|
2301
|
+
return next === void 0 ? null : JSON.stringify(next) + "\n";
|
|
2302
|
+
});
|
|
2283
2303
|
}
|
|
2284
2304
|
};
|
|
2285
2305
|
}
|
|
@@ -2449,32 +2469,38 @@ async function runResumeCommand(options) {
|
|
|
2449
2469
|
ok: false,
|
|
2450
2470
|
message: "merge row not found: " + options.rowId
|
|
2451
2471
|
};
|
|
2452
|
-
if (row.state === "verify")
|
|
2453
|
-
|
|
2454
|
-
|
|
2455
|
-
|
|
2456
|
-
|
|
2457
|
-
|
|
2458
|
-
|
|
2459
|
-
|
|
2460
|
-
|
|
2461
|
-
|
|
2462
|
-
|
|
2463
|
-
|
|
2464
|
-
|
|
2465
|
-
|
|
2466
|
-
const makePorts = options.makePorts;
|
|
2467
|
-
if (makePorts === void 0) throw new Error("resume makePorts disappeared");
|
|
2468
|
-
return {
|
|
2469
|
-
...makePorts(runId),
|
|
2472
|
+
if (row.state === "verify") {
|
|
2473
|
+
if (row.runId !== void 0 && await options.isOwnerActive?.(row.runId) === true) return {
|
|
2474
|
+
ok: false,
|
|
2475
|
+
message: "merge run still owns this verify row: " + row.runId
|
|
2476
|
+
};
|
|
2477
|
+
return await runScanCommand({
|
|
2478
|
+
io: options.io,
|
|
2479
|
+
home: options.home,
|
|
2480
|
+
registry: options.registry,
|
|
2481
|
+
approval: true,
|
|
2482
|
+
platformWrite: options.platformWrite,
|
|
2483
|
+
now: options.now,
|
|
2484
|
+
...options.makePorts === void 0 ? { ports: {
|
|
2485
|
+
...options.ports,
|
|
2470
2486
|
screening: () => Promise.resolve([{
|
|
2471
2487
|
a: row.a,
|
|
2472
2488
|
b: row.b
|
|
2473
2489
|
}])
|
|
2474
|
-
}
|
|
2475
|
-
|
|
2476
|
-
|
|
2477
|
-
|
|
2490
|
+
} } : { makePorts: (runId) => {
|
|
2491
|
+
const makePorts = options.makePorts;
|
|
2492
|
+
if (makePorts === void 0) throw new Error("resume makePorts disappeared");
|
|
2493
|
+
return {
|
|
2494
|
+
...makePorts(runId),
|
|
2495
|
+
screening: () => Promise.resolve([{
|
|
2496
|
+
a: row.a,
|
|
2497
|
+
b: row.b
|
|
2498
|
+
}])
|
|
2499
|
+
};
|
|
2500
|
+
} },
|
|
2501
|
+
...options.timeoutMinutes === void 0 ? {} : { timeoutMinutes: options.timeoutMinutes }
|
|
2502
|
+
});
|
|
2503
|
+
}
|
|
2478
2504
|
if (row.state !== "fuse" && row.state !== "approved") return {
|
|
2479
2505
|
ok: false,
|
|
2480
2506
|
message: "row is not resumable from state " + row.state
|
|
@@ -3756,6 +3782,7 @@ function createMergeRuntime(deps) {
|
|
|
3756
3782
|
registry: deps.registry,
|
|
3757
3783
|
platformWrite: deps.platformWrite,
|
|
3758
3784
|
now: deps.now,
|
|
3785
|
+
...deps.isOwnerActive === void 0 ? {} : { isOwnerActive: deps.isOwnerActive },
|
|
3759
3786
|
...timeout === void 0 ? {} : { timeoutMinutes: timeout }
|
|
3760
3787
|
};
|
|
3761
3788
|
const stage = deps.stage;
|
|
@@ -3787,7 +3814,7 @@ function createMergeRuntime(deps) {
|
|
|
3787
3814
|
ok: false,
|
|
3788
3815
|
text: "merge pair 不接受受保护/保护状态未知技能: " + command.a + " / " + command.b
|
|
3789
3816
|
};
|
|
3790
|
-
if (deps.isBlocked?.(command.a) === true || deps.isBlocked?.(command.b) === true) return {
|
|
3817
|
+
if (await deps.isBlocked?.(command.a) === true || await deps.isBlocked?.(command.b) === true) return {
|
|
3791
3818
|
ok: false,
|
|
3792
3819
|
text: "merge pair 不接受被范围门拦下的技能: " + command.a + " / " + command.b
|
|
3793
3820
|
};
|
|
@@ -3857,10 +3884,12 @@ function createMergeRuntime(deps) {
|
|
|
3857
3884
|
if (command.verb === "scan") {
|
|
3858
3885
|
const signal = new AbortController().signal;
|
|
3859
3886
|
const cards = await deps.cards(signal);
|
|
3860
|
-
const
|
|
3887
|
+
const blocked = /* @__PURE__ */ new Set();
|
|
3888
|
+
for (const card of cards) if (await deps.isBlocked?.(card.name) === true) blocked.add(card.name);
|
|
3889
|
+
const managed = new Set(cards.filter((card) => card.managed === true && card.protectedBy == null && !blocked.has(card.name)).map((card) => card.name));
|
|
3861
3890
|
const loadedState = await loadMergeState(deps.io, deps.home);
|
|
3862
3891
|
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));
|
|
3892
|
+
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
3893
|
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
3894
|
const result = await runScanCommand({
|
|
3866
3895
|
...common,
|
|
@@ -4072,6 +4101,7 @@ function createMergeRuntime(deps) {
|
|
|
4072
4101
|
...common,
|
|
4073
4102
|
rowId: row.id,
|
|
4074
4103
|
ports: basePorts,
|
|
4104
|
+
...deps.isOwnerActive === void 0 ? {} : { isOwnerActive: deps.isOwnerActive },
|
|
4075
4105
|
makePorts: (runId) => createChannelScanPorts({
|
|
4076
4106
|
io: deps.io,
|
|
4077
4107
|
home: deps.home,
|
|
@@ -4795,8 +4825,12 @@ function apply(ctx, rawConfig = {}) {
|
|
|
4795
4825
|
parent: void 0,
|
|
4796
4826
|
allow: ["merge_stage"],
|
|
4797
4827
|
timeoutMinutes: params.mergeRunTimeoutMinutes,
|
|
4798
|
-
isBlocked: (name) => {
|
|
4799
|
-
return ctx.get("evolutionCurator")?.
|
|
4828
|
+
isBlocked: async (name) => {
|
|
4829
|
+
return await ctx.get("evolutionCurator")?.isBlocked?.(name) === true;
|
|
4830
|
+
},
|
|
4831
|
+
isOwnerActive: (runId) => {
|
|
4832
|
+
const run = runs.find(runId);
|
|
4833
|
+
return run !== void 0 && run.state === "running";
|
|
4800
4834
|
},
|
|
4801
4835
|
async cards(signal) {
|
|
4802
4836
|
const listed = await skills.list({ withContent: true });
|
package/lib/types/index.js
CHANGED
|
@@ -218,9 +218,13 @@ 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?.
|
|
223
|
+
return (await curator?.isBlocked?.(name)) === true;
|
|
224
|
+
},
|
|
225
|
+
isOwnerActive: (runId) => {
|
|
226
|
+
const run = runs.find(runId);
|
|
227
|
+
return run !== undefined && run.state === 'running';
|
|
224
228
|
},
|
|
225
229
|
async cards(signal) {
|
|
226
230
|
const listed = await skills.list({ withContent: true });
|
|
@@ -19,6 +19,7 @@ export interface RunResumeCommandOptions {
|
|
|
19
19
|
readonly platformWrite: boolean;
|
|
20
20
|
readonly timeoutMinutes?: number;
|
|
21
21
|
readonly now: () => number;
|
|
22
|
+
readonly isOwnerActive?: (runId: string) => boolean | Promise<boolean>;
|
|
22
23
|
}
|
|
23
24
|
export declare function runResumeCommand(options: RunResumeCommandOptions): Promise<WithMergeRunResult>;
|
|
24
25
|
//# sourceMappingURL=resume-command.d.ts.map
|
|
@@ -8,6 +8,9 @@ export async function runResumeCommand(options) {
|
|
|
8
8
|
if (row === undefined)
|
|
9
9
|
return { ok: false, message: 'merge row not found: ' + options.rowId };
|
|
10
10
|
if (row.state === 'verify') {
|
|
11
|
+
if (row.runId !== undefined && (await options.isOwnerActive?.(row.runId)) === true) {
|
|
12
|
+
return { ok: false, message: 'merge run still owns this verify row: ' + row.runId };
|
|
13
|
+
}
|
|
11
14
|
return await runScanCommand({
|
|
12
15
|
io: options.io, home: options.home, registry: options.registry,
|
|
13
16
|
approval: true, platformWrite: options.platformWrite, now: options.now,
|
package/lib/types/runtime.d.ts
CHANGED
|
@@ -32,7 +32,8 @@ 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
|
+
readonly isOwnerActive?: (runId: string) => boolean | Promise<boolean>;
|
|
36
37
|
/** Run-scoped engine screening. */
|
|
37
38
|
readonly screeningForRun: (cards: readonly SkillCard[], runId: string, signal: AbortSignal) => Promise<readonly MergeCandidate[]>;
|
|
38
39
|
readonly stage?: MergeStageBindingPort;
|
package/lib/types/runtime.js
CHANGED
|
@@ -34,6 +34,7 @@ export function createMergeRuntime(deps) {
|
|
|
34
34
|
const common = {
|
|
35
35
|
io: deps.io, home: deps.home, registry: deps.registry,
|
|
36
36
|
platformWrite: deps.platformWrite, now: deps.now,
|
|
37
|
+
...(deps.isOwnerActive === undefined ? {} : { isOwnerActive: deps.isOwnerActive }),
|
|
37
38
|
...(timeout === undefined ? {} : { timeoutMinutes: timeout }),
|
|
38
39
|
};
|
|
39
40
|
const stage = deps.stage;
|
|
@@ -59,7 +60,7 @@ export function createMergeRuntime(deps) {
|
|
|
59
60
|
|| cardA.protectionUnknown === true || cardB.protectionUnknown === true) {
|
|
60
61
|
return { ok: false, text: 'merge pair 不接受受保护/保护状态未知技能: ' + command.a + ' / ' + command.b };
|
|
61
62
|
}
|
|
62
|
-
if (deps.isBlocked?.(command.a) === true || deps.isBlocked?.(command.b) === true) {
|
|
63
|
+
if ((await deps.isBlocked?.(command.a)) === true || (await deps.isBlocked?.(command.b)) === true) {
|
|
63
64
|
return { ok: false, text: 'merge pair 不接受被范围门拦下的技能: ' + command.a + ' / ' + command.b };
|
|
64
65
|
}
|
|
65
66
|
const pair = [{ a: command.a, b: command.b }];
|
|
@@ -104,12 +105,18 @@ export function createMergeRuntime(deps) {
|
|
|
104
105
|
if (command.verb === 'scan') {
|
|
105
106
|
const signal = new AbortController().signal;
|
|
106
107
|
const cards = await deps.cards(signal);
|
|
108
|
+
const blocked = new Set();
|
|
109
|
+
for (const card of cards)
|
|
110
|
+
if ((await deps.isBlocked?.(card.name)) === true)
|
|
111
|
+
blocked.add(card.name);
|
|
107
112
|
const managed = new Set(cards
|
|
108
|
-
.filter(card => card.managed === true && card.protectedBy == null &&
|
|
113
|
+
.filter(card => card.managed === true && card.protectedBy == null && !blocked.has(card.name))
|
|
109
114
|
.map(card => card.name));
|
|
110
115
|
const loadedState = await loadMergeState(deps.io, deps.home);
|
|
111
116
|
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'
|
|
117
|
+
const held = new Set(loadedState.state.rows.filter(row => row.queue === 'pending' || row.queue === 'fuse'
|
|
118
|
+
|| (row.queue === 'verify' && (row.runId !== undefined && deps.now() - Date.parse(row.updatedAt) < 10 * 60_000 || !canRetry(row))))
|
|
119
|
+
.map(row => row.pairKey));
|
|
113
120
|
const conflictNotes = loadedState.state.rows
|
|
114
121
|
.filter(row => row.state === 'pending' || row.state === 'approved' || row.state === 'fuse' || row.state === 'skipped')
|
|
115
122
|
.map(row => 'conflict:' + row.state + ':' + row.pairKey + ':' + row.id);
|
|
@@ -244,6 +251,7 @@ export function createMergeRuntime(deps) {
|
|
|
244
251
|
};
|
|
245
252
|
const result = await runResumeCommand({
|
|
246
253
|
...common, rowId: row.id, ports: basePorts,
|
|
254
|
+
...(deps.isOwnerActive === undefined ? {} : { isOwnerActive: deps.isOwnerActive }),
|
|
247
255
|
makePorts: runId => createChannelScanPorts({
|
|
248
256
|
io: deps.io, home: deps.home, runId, cards, screening: pair,
|
|
249
257
|
channel: deps.channel, parent, allow: deps.allow,
|
|
@@ -18,6 +18,7 @@ export interface RunScanCommandOptions {
|
|
|
18
18
|
readonly now: () => number;
|
|
19
19
|
readonly detached?: boolean;
|
|
20
20
|
readonly notes?: readonly string[];
|
|
21
|
+
readonly isOwnerActive?: (runId: string) => boolean | Promise<boolean>;
|
|
21
22
|
}
|
|
22
23
|
export declare function runScanCommand(options: RunScanCommandOptions): Promise<WithMergeRunResult>;
|
|
23
24
|
//# sourceMappingURL=scan-command.d.ts.map
|
|
@@ -22,8 +22,10 @@ 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 }),
|
|
28
|
+
...(options.isOwnerActive === undefined ? {} : { isOwnerActive: options.isOwnerActive }),
|
|
27
29
|
});
|
|
28
30
|
},
|
|
29
31
|
});
|
package/lib/types/scan-run.d.ts
CHANGED
|
@@ -44,6 +44,8 @@ export interface RunScanOptions {
|
|
|
44
44
|
total: number;
|
|
45
45
|
}) => Promise<void>;
|
|
46
46
|
readonly notes?: readonly string[];
|
|
47
|
+
readonly runId?: string;
|
|
48
|
+
readonly isOwnerActive?: (runId: string) => boolean | Promise<boolean>;
|
|
47
49
|
}
|
|
48
50
|
export declare function runScan(options: RunScanOptions): Promise<MergeRunReportDraft>;
|
|
49
51
|
//# sourceMappingURL=scan-run.d.ts.map
|
package/lib/types/scan-run.js
CHANGED
|
@@ -64,8 +64,12 @@ 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
|
+
if (before?.state === 'verify' && before.runId !== undefined && before.runId !== options.runId
|
|
70
|
+
&& (await options.isOwnerActive?.(before.runId)) === true) {
|
|
71
|
+
continue;
|
|
72
|
+
}
|
|
69
73
|
const placed = await upsertRow(options.io, options.home, options.now, row, 'verify', before?.updatedAt ?? null);
|
|
70
74
|
if (!placed.ok && before?.state === 'skipped') {
|
|
71
75
|
const restored = transition(before, { type: 'restore', at: at(options.now) });
|
|
@@ -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 =
|
|
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
|
-
|
|
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
|
|
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
|
|
37
|
+
const path = claimPath(home, target);
|
|
24
38
|
const token = randomUUID();
|
|
25
|
-
|
|
26
|
-
|
|
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
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
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.
|
|
4
|
+
"version": "0.30.14",
|
|
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.14",
|
|
31
|
+
"@lmzhen/dsh-evolution-merge-engine": "^0.30.14"
|
|
32
32
|
},
|
|
33
33
|
"peerDependencies": {
|
|
34
34
|
"@deepseek-ai/cordis": "^4.0.5"
|