@lmzhen/dsh-evolution-core 0.29.0 → 0.30.1
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/{http-routes-w09FdvOp.js → http-routes-ifJKGnu6.js} +2 -2
- package/lib/http-routes.js +1 -1
- package/lib/index.js +66 -21
- package/lib/types/http-routes.d.ts +2 -2
- package/lib/types/http-routes.js +2 -2
- package/lib/types/run-registry.d.ts +15 -0
- package/lib/types/run-registry.js +15 -1
- package/lib/types/skill-store.js +54 -18
- package/package.json +1 -1
- package/persisted-write-inventory.json +9 -0
|
@@ -77,8 +77,8 @@ const HOST_ROUTES = {
|
|
|
77
77
|
merge: {
|
|
78
78
|
view: "/api/dsh-evolution/merge/view",
|
|
79
79
|
rows: "/api/dsh-evolution/merge/rows",
|
|
80
|
-
|
|
81
|
-
|
|
80
|
+
body: "/api/dsh-evolution/merge/body",
|
|
81
|
+
history: "/api/dsh-evolution/merge/history"
|
|
82
82
|
},
|
|
83
83
|
diagnostics: { view: "/api/dsh-evolution/diagnostics" }
|
|
84
84
|
};
|
package/lib/http-routes.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import { a as methodNotAllowed, i as json, n as MAX_REQUEST_BODY_BYTES, o as readJsonObject, r as hostSurface, s as registerHostRoutes, t as HOST_ROUTES } from "./http-routes-
|
|
1
|
+
import { a as methodNotAllowed, i as json, n as MAX_REQUEST_BODY_BYTES, o as readJsonObject, r as hostSurface, s as registerHostRoutes, t as HOST_ROUTES } from "./http-routes-ifJKGnu6.js";
|
|
2
2
|
export { HOST_ROUTES, MAX_REQUEST_BODY_BYTES, hostSurface, json, methodNotAllowed, readJsonObject, registerHostRoutes };
|
package/lib/index.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { _ as scopedPackageName, c as MAINTAINER_PRESET_ID, d as RESTRICTED_TOOLS_ROW_ID, f as RESTRICTOR_MODULE, g as scopeOfNearestManifest, p as REVIEWER_PRESET_ID, s as EVOLUTION_PRESET_IDS, u as RESTRICTED_PRESET_DISPLAY } from "./ensure-MEYMj9jn.js";
|
|
2
|
-
import { a as methodNotAllowed, c as asRecord, i as json, l as isRecord, n as MAX_REQUEST_BODY_BYTES, o as readJsonObject, r as hostSurface, s as registerHostRoutes, t as HOST_ROUTES } from "./http-routes-
|
|
2
|
+
import { a as methodNotAllowed, c as asRecord, i as json, l as isRecord, n as MAX_REQUEST_BODY_BYTES, o as readJsonObject, r as hostSurface, s as registerHostRoutes, t as HOST_ROUTES } from "./http-routes-ifJKGnu6.js";
|
|
3
3
|
import { w as allowRowCollisions } from "./composition-D8Y3-ROd.js";
|
|
4
4
|
import { n as errorText, t as EVOLUTION_ERRORS } from "./errors-Br2-uUSK.js";
|
|
5
5
|
import { createSubagentChannel, probeChannelState, subagentChannelState } from "./subagent/index.js";
|
|
@@ -5608,7 +5608,11 @@ function parseIndex(raw) {
|
|
|
5608
5608
|
...typeof record.resultRef === "string" ? { resultRef: record.resultRef } : {},
|
|
5609
5609
|
...typeof record.failure === "string" ? { failure: record.failure } : {},
|
|
5610
5610
|
...typeof record.supersededAt === "number" ? { supersededAt: record.supersededAt } : {},
|
|
5611
|
-
...typeof record.parentRunId === "string" ? { parentRunId: record.parentRunId } : {}
|
|
5611
|
+
...typeof record.parentRunId === "string" ? { parentRunId: record.parentRunId } : {},
|
|
5612
|
+
...record.mergeStatus === "running" || record.mergeStatus === "succeeded" || record.mergeStatus === "failed" || record.mergeStatus === "cancelled" || record.mergeStatus === "awaiting-approval" || record.mergeStatus === "undone" || record.mergeStatus === "superseded" || record.mergeStatus === "interrupted" || record.mergeStatus === "orphan" ? { mergeStatus: record.mergeStatus } : {},
|
|
5613
|
+
...typeof record.stagingRef === "string" ? { stagingRef: record.stagingRef } : {},
|
|
5614
|
+
...typeof record.pairKey === "string" ? { pairKey: record.pairKey } : {},
|
|
5615
|
+
...Array.isArray(record.pairs) ? { pairs: record.pairs } : {}
|
|
5612
5616
|
});
|
|
5613
5617
|
}
|
|
5614
5618
|
return {
|
|
@@ -5642,6 +5646,7 @@ function newRunRegistry(options) {
|
|
|
5642
5646
|
const orphanOf = (row) => ({
|
|
5643
5647
|
...row,
|
|
5644
5648
|
state: "failed",
|
|
5649
|
+
mergeStatus: "orphan",
|
|
5645
5650
|
endedAt: now(),
|
|
5646
5651
|
failure: ORPHAN_FAILURE
|
|
5647
5652
|
});
|
|
@@ -5780,6 +5785,10 @@ function newRunRegistry(options) {
|
|
|
5780
5785
|
pid: process.pid,
|
|
5781
5786
|
...seed?.command !== void 0 ? { command: seed.command } : {},
|
|
5782
5787
|
...seed?.parentRunId !== void 0 ? { parentRunId: seed.parentRunId } : {},
|
|
5788
|
+
...seed?.mergeStatus !== void 0 ? { mergeStatus: seed.mergeStatus } : {},
|
|
5789
|
+
...seed?.stagingRef !== void 0 ? { stagingRef: seed.stagingRef } : {},
|
|
5790
|
+
...seed?.pairKey !== void 0 ? { pairKey: seed.pairKey } : {},
|
|
5791
|
+
...seed?.pairs !== void 0 ? { pairs: seed.pairs } : {},
|
|
5783
5792
|
...seed?.total !== void 0 ? { progress: {
|
|
5784
5793
|
done: 0,
|
|
5785
5794
|
total: seed.total
|
|
@@ -5811,6 +5820,7 @@ function newRunRegistry(options) {
|
|
|
5811
5820
|
const row = owned.get(id);
|
|
5812
5821
|
if (row === void 0 || row.supersededAt !== void 0) continue;
|
|
5813
5822
|
row.supersededAt = now();
|
|
5823
|
+
if (row.kind === "merge") row.mergeStatus = "superseded";
|
|
5814
5824
|
changed = true;
|
|
5815
5825
|
}
|
|
5816
5826
|
if (changed) await persist();
|
|
@@ -5825,6 +5835,7 @@ function newRunRegistry(options) {
|
|
|
5825
5835
|
for (const row of merged.values()) {
|
|
5826
5836
|
if (row.kind !== kind || row.supersededAt !== void 0) continue;
|
|
5827
5837
|
row.supersededAt = now();
|
|
5838
|
+
if (row.kind === "merge") row.mergeStatus = "superseded";
|
|
5828
5839
|
marked += 1;
|
|
5829
5840
|
}
|
|
5830
5841
|
const kept = capped([...merged.values()]);
|
|
@@ -5848,6 +5859,7 @@ function newRunRegistry(options) {
|
|
|
5848
5859
|
row.endedAt = now();
|
|
5849
5860
|
if (outcome.resultRef !== void 0) row.resultRef = outcome.resultRef;
|
|
5850
5861
|
if (outcome.failure !== void 0) row.failure = outcome.failure;
|
|
5862
|
+
if (outcome.mergeStatus !== void 0) row.mergeStatus = outcome.mergeStatus;
|
|
5851
5863
|
controllers.delete(id);
|
|
5852
5864
|
await persist();
|
|
5853
5865
|
},
|
|
@@ -10965,24 +10977,41 @@ var SkillLibrary = class {
|
|
|
10965
10977
|
ok: false,
|
|
10966
10978
|
message: stuck.length === 0 ? `合并失败,已回滚:${reason}` : `合并失败(${reason});已回滚,但以下未回滚:${stuck.join(", ")}:它们仍在 .archive 里,请用 /evolution skill restore 恢复。`
|
|
10967
10979
|
});
|
|
10968
|
-
|
|
10969
|
-
|
|
10970
|
-
|
|
10971
|
-
|
|
10972
|
-
|
|
10973
|
-
|
|
10974
|
-
|
|
10975
|
-
|
|
10980
|
+
const rollbackTarget = async () => {
|
|
10981
|
+
const stuck = [];
|
|
10982
|
+
const support = plan.supportFiles.map((file) => ({
|
|
10983
|
+
target: join(dir, ...file.path.replace(/\\/g, "/").split("/").filter(Boolean)),
|
|
10984
|
+
content: supportPrevious.get(file.path) ?? null,
|
|
10985
|
+
previous: file.content
|
|
10986
|
+
}));
|
|
10987
|
+
const writes = [{
|
|
10988
|
+
target: targetPath,
|
|
10989
|
+
content: plan.expectedTargetBody ?? "",
|
|
10990
|
+
expected: plan.body.trimEnd() + "\n"
|
|
10991
|
+
}, ...support.map((file) => ({
|
|
10992
|
+
target: file.target,
|
|
10993
|
+
content: file.content ?? "",
|
|
10994
|
+
expected: file.previous
|
|
10995
|
+
}))];
|
|
10996
|
+
try {
|
|
10997
|
+
if (!(await this.applyTreeChange({
|
|
10998
|
+
name: target,
|
|
10999
|
+
origin: plan.origin,
|
|
11000
|
+
protection: "write",
|
|
11001
|
+
writes,
|
|
11002
|
+
auditAction: "merge-rollback",
|
|
11003
|
+
auditSummary: "merge rollback: restore target snapshot",
|
|
11004
|
+
eventAction: "merge-rollback",
|
|
10976
11005
|
unitId,
|
|
10977
|
-
unitHead
|
|
10978
|
-
|
|
10979
|
-
|
|
10980
|
-
|
|
10981
|
-
const stuck = await rollbackSources();
|
|
10982
|
-
return rollbackMessage(result.message, stuck);
|
|
10983
|
-
}
|
|
10984
|
-
archived.push(source);
|
|
11006
|
+
unitHead
|
|
11007
|
+
})).ok) stuck.push(target);
|
|
11008
|
+
} catch {
|
|
11009
|
+
stuck.push(target);
|
|
10985
11010
|
}
|
|
11011
|
+
return stuck;
|
|
11012
|
+
};
|
|
11013
|
+
if (plan.targetExists) {
|
|
11014
|
+
for (const source of sources) if (await this.io.readText(join(this.dirOf(source), "SKILL.md")) !== sourceBodies.get(source)) return rollbackMessage(`来源技能「${source}」在归档前被改过`, []);
|
|
10986
11015
|
const writes = [{
|
|
10987
11016
|
target: targetPath,
|
|
10988
11017
|
content: plan.body.trimEnd() + "\n",
|
|
@@ -11003,9 +11032,25 @@ var SkillLibrary = class {
|
|
|
11003
11032
|
unitId,
|
|
11004
11033
|
unitHead
|
|
11005
11034
|
});
|
|
11006
|
-
if (!result.ok)
|
|
11007
|
-
|
|
11008
|
-
|
|
11035
|
+
if (!result.ok) return rollbackMessage(result.message, []);
|
|
11036
|
+
for (const source of sources) {
|
|
11037
|
+
if (await this.io.readText(join(this.dirOf(source), "SKILL.md")) !== sourceBodies.get(source)) {
|
|
11038
|
+
const stuck = await rollbackSources();
|
|
11039
|
+
stuck.push(...await rollbackTarget());
|
|
11040
|
+
return rollbackMessage(`来源技能「${source}」在归档前被改过`, stuck);
|
|
11041
|
+
}
|
|
11042
|
+
const archivedResult = await this.archive(source, {
|
|
11043
|
+
absorbedInto: target,
|
|
11044
|
+
unitId,
|
|
11045
|
+
unitHead: { skill: target },
|
|
11046
|
+
reason: `Merged into ${target}`
|
|
11047
|
+
}, plan.origin);
|
|
11048
|
+
if (!archivedResult.ok) {
|
|
11049
|
+
const stuck = await rollbackSources();
|
|
11050
|
+
stuck.push(...await rollbackTarget());
|
|
11051
|
+
return rollbackMessage(archivedResult.message, stuck);
|
|
11052
|
+
}
|
|
11053
|
+
archived.push(source);
|
|
11009
11054
|
}
|
|
11010
11055
|
return {
|
|
11011
11056
|
ok: true,
|
|
@@ -56,8 +56,8 @@ export declare const HOST_ROUTES: {
|
|
|
56
56
|
readonly merge: {
|
|
57
57
|
readonly view: "/api/dsh-evolution/merge/view";
|
|
58
58
|
readonly rows: "/api/dsh-evolution/merge/rows";
|
|
59
|
-
readonly
|
|
60
|
-
readonly
|
|
59
|
+
readonly body: "/api/dsh-evolution/merge/body";
|
|
60
|
+
readonly history: "/api/dsh-evolution/merge/history";
|
|
61
61
|
};
|
|
62
62
|
readonly diagnostics: {
|
|
63
63
|
readonly view: "/api/dsh-evolution/diagnostics";
|
package/lib/types/http-routes.js
CHANGED
|
@@ -53,8 +53,8 @@ export const HOST_ROUTES = {
|
|
|
53
53
|
merge: {
|
|
54
54
|
view: '/api/dsh-evolution/merge/view',
|
|
55
55
|
rows: '/api/dsh-evolution/merge/rows',
|
|
56
|
-
|
|
57
|
-
|
|
56
|
+
body: '/api/dsh-evolution/merge/body',
|
|
57
|
+
history: '/api/dsh-evolution/merge/history',
|
|
58
58
|
},
|
|
59
59
|
diagnostics: {
|
|
60
60
|
view: '/api/dsh-evolution/diagnostics',
|
|
@@ -75,6 +75,11 @@ export interface RunRecord {
|
|
|
75
75
|
supersededAt?: number | undefined;
|
|
76
76
|
/** The run this run was spawned from (approve/resume after an earlier command). */
|
|
77
77
|
parentRunId?: string | undefined;
|
|
78
|
+
/** Merge-side projection; core lifecycle stays four-state. */
|
|
79
|
+
mergeStatus?: 'running' | 'succeeded' | 'failed' | 'cancelled' | 'awaiting-approval' | 'undone' | 'superseded' | 'interrupted' | 'orphan' | undefined;
|
|
80
|
+
stagingRef?: string | undefined;
|
|
81
|
+
pairKey?: string | undefined;
|
|
82
|
+
pairs?: readonly unknown[] | undefined;
|
|
78
83
|
}
|
|
79
84
|
/** What a started run gets: its id and the signal its work must honour. */
|
|
80
85
|
export interface RunHandle {
|
|
@@ -85,12 +90,18 @@ export interface RunHandle {
|
|
|
85
90
|
readonly startedAt: number;
|
|
86
91
|
readonly command?: RunCommand | undefined;
|
|
87
92
|
readonly parentRunId?: string | undefined;
|
|
93
|
+
/** Merge-side projection; core lifecycle stays four-state. */
|
|
94
|
+
readonly mergeStatus?: 'running' | 'succeeded' | 'failed' | 'cancelled' | 'awaiting-approval' | 'undone' | 'superseded' | 'interrupted' | 'orphan' | undefined;
|
|
95
|
+
readonly stagingRef?: string | undefined;
|
|
96
|
+
readonly pairKey?: string | undefined;
|
|
97
|
+
readonly pairs?: readonly unknown[] | undefined;
|
|
88
98
|
}
|
|
89
99
|
/** How a run ended, as reported by its owner. */
|
|
90
100
|
export interface RunOutcome {
|
|
91
101
|
state: 'succeeded' | 'failed' | 'cancelled';
|
|
92
102
|
resultRef?: string | undefined;
|
|
93
103
|
failure?: string | undefined;
|
|
104
|
+
mergeStatus?: 'running' | 'succeeded' | 'failed' | 'cancelled' | 'awaiting-approval' | 'undone' | 'superseded' | 'interrupted' | 'orphan' | undefined;
|
|
94
105
|
}
|
|
95
106
|
/** The registry surface the commands row drives. */
|
|
96
107
|
export interface RunRegistry {
|
|
@@ -114,6 +125,10 @@ export interface RunRegistry {
|
|
|
114
125
|
command?: RunCommand;
|
|
115
126
|
total?: number;
|
|
116
127
|
parentRunId?: string;
|
|
128
|
+
mergeStatus?: 'running' | 'succeeded' | 'failed' | 'cancelled' | 'awaiting-approval' | 'undone' | 'superseded' | 'interrupted' | 'orphan';
|
|
129
|
+
stagingRef?: string;
|
|
130
|
+
pairKey?: string;
|
|
131
|
+
pairs?: readonly unknown[];
|
|
117
132
|
}): Promise<RunHandle>;
|
|
118
133
|
/** Record a run's terminal state (first writer wins). */
|
|
119
134
|
settle(id: string, outcome: RunOutcome): Promise<void>;
|
|
@@ -110,6 +110,10 @@ function parseIndex(raw) {
|
|
|
110
110
|
...(typeof record.failure === 'string' ? { failure: record.failure } : {}),
|
|
111
111
|
...(typeof record.supersededAt === 'number' ? { supersededAt: record.supersededAt } : {}),
|
|
112
112
|
...(typeof record.parentRunId === 'string' ? { parentRunId: record.parentRunId } : {}),
|
|
113
|
+
...(record.mergeStatus === 'running' || record.mergeStatus === 'succeeded' || record.mergeStatus === 'failed' || record.mergeStatus === 'cancelled' || record.mergeStatus === 'awaiting-approval' || record.mergeStatus === 'undone' || record.mergeStatus === 'superseded' || record.mergeStatus === 'interrupted' || record.mergeStatus === 'orphan' ? { mergeStatus: record.mergeStatus } : {}),
|
|
114
|
+
...(typeof record.stagingRef === 'string' ? { stagingRef: record.stagingRef } : {}),
|
|
115
|
+
...(typeof record.pairKey === 'string' ? { pairKey: record.pairKey } : {}),
|
|
116
|
+
...(Array.isArray(record.pairs) ? { pairs: record.pairs } : {}),
|
|
113
117
|
});
|
|
114
118
|
}
|
|
115
119
|
return { ok: true, records, foreign, ...(notes.length > 0 ? { note: notes.join('; ') } : {}) };
|
|
@@ -135,7 +139,7 @@ export function newRunRegistry(options) {
|
|
|
135
139
|
/** A row whose owner cannot be reached: `pid` absent (an older build) or not alive. */
|
|
136
140
|
const ownerIsGone = (row) => row.pid === undefined || !isProcessAlive(row.pid);
|
|
137
141
|
/** The verdict is materialized ONCE, so its `endedAt` (and the duration a reader sees) is stable. */
|
|
138
|
-
const orphanOf = (row) => ({ ...row, state: 'failed', endedAt: now(), failure: ORPHAN_FAILURE });
|
|
142
|
+
const orphanOf = (row) => ({ ...row, state: 'failed', mergeStatus: 'orphan', endedAt: now(), failure: ORPHAN_FAILURE });
|
|
139
143
|
/** Every row this process knows, newest first. ALL reads go through here. */
|
|
140
144
|
const view = () => [...owned.values(), ...foreign.values()].sort((a, b) => b.startedAt - a.startedAt);
|
|
141
145
|
/** Retained history: every running row plus the newest terminal rows per kind. */
|
|
@@ -301,6 +305,10 @@ export function newRunRegistry(options) {
|
|
|
301
305
|
pid: process.pid,
|
|
302
306
|
...(seed?.command !== undefined ? { command: seed.command } : {}),
|
|
303
307
|
...(seed?.parentRunId !== undefined ? { parentRunId: seed.parentRunId } : {}),
|
|
308
|
+
...(seed?.mergeStatus !== undefined ? { mergeStatus: seed.mergeStatus } : {}),
|
|
309
|
+
...(seed?.stagingRef !== undefined ? { stagingRef: seed.stagingRef } : {}),
|
|
310
|
+
...(seed?.pairKey !== undefined ? { pairKey: seed.pairKey } : {}),
|
|
311
|
+
...(seed?.pairs !== undefined ? { pairs: seed.pairs } : {}),
|
|
304
312
|
...(seed?.total !== undefined ? { progress: { done: 0, total: seed.total } } : {}),
|
|
305
313
|
});
|
|
306
314
|
// Write-through (0.19.1, review P2): answering "started" promises a run another
|
|
@@ -332,6 +340,8 @@ export function newRunRegistry(options) {
|
|
|
332
340
|
if (row === undefined || row.supersededAt !== undefined)
|
|
333
341
|
continue;
|
|
334
342
|
row.supersededAt = now();
|
|
343
|
+
if (row.kind === 'merge')
|
|
344
|
+
row.mergeStatus = 'superseded';
|
|
335
345
|
changed = true;
|
|
336
346
|
}
|
|
337
347
|
if (changed)
|
|
@@ -350,6 +360,8 @@ export function newRunRegistry(options) {
|
|
|
350
360
|
if (row.kind !== kind || row.supersededAt !== undefined)
|
|
351
361
|
continue;
|
|
352
362
|
row.supersededAt = now();
|
|
363
|
+
if (row.kind === 'merge')
|
|
364
|
+
row.mergeStatus = 'superseded';
|
|
353
365
|
marked += 1;
|
|
354
366
|
}
|
|
355
367
|
const kept = capped([...merged.values()]);
|
|
@@ -371,6 +383,8 @@ export function newRunRegistry(options) {
|
|
|
371
383
|
row.resultRef = outcome.resultRef;
|
|
372
384
|
if (outcome.failure !== undefined)
|
|
373
385
|
row.failure = outcome.failure;
|
|
386
|
+
if (outcome.mergeStatus !== undefined)
|
|
387
|
+
row.mergeStatus = outcome.mergeStatus;
|
|
374
388
|
controllers.delete(id);
|
|
375
389
|
await persist();
|
|
376
390
|
},
|
package/lib/types/skill-store.js
CHANGED
|
@@ -2036,24 +2036,42 @@ export class SkillLibrary {
|
|
|
2036
2036
|
? `合并失败,已回滚:${reason}`
|
|
2037
2037
|
: `合并失败(${reason});已回滚,但以下未回滚:${stuck.join(', ')}:它们仍在 .archive 里,请用 /evolution skill restore 恢复。`,
|
|
2038
2038
|
});
|
|
2039
|
+
const rollbackTarget = async () => {
|
|
2040
|
+
const stuck = [];
|
|
2041
|
+
const support = plan.supportFiles.map(file => ({
|
|
2042
|
+
target: join(dir, ...file.path.replace(/\\/g, '/').split('/').filter(Boolean)),
|
|
2043
|
+
content: supportPrevious.get(file.path) ?? null,
|
|
2044
|
+
previous: file.content,
|
|
2045
|
+
}));
|
|
2046
|
+
const writes = [
|
|
2047
|
+
{ target: targetPath, content: plan.expectedTargetBody ?? '', expected: plan.body.trimEnd() + '\n' },
|
|
2048
|
+
...support.map(file => ({ target: file.target, content: file.content ?? '', expected: file.previous })),
|
|
2049
|
+
];
|
|
2050
|
+
try {
|
|
2051
|
+
const restored = await this.applyTreeChange({
|
|
2052
|
+
name: target,
|
|
2053
|
+
origin: plan.origin,
|
|
2054
|
+
protection: 'write',
|
|
2055
|
+
writes,
|
|
2056
|
+
auditAction: 'merge-rollback',
|
|
2057
|
+
auditSummary: 'merge rollback: restore target snapshot',
|
|
2058
|
+
eventAction: 'merge-rollback',
|
|
2059
|
+
unitId,
|
|
2060
|
+
unitHead,
|
|
2061
|
+
});
|
|
2062
|
+
if (!restored.ok)
|
|
2063
|
+
stuck.push(target);
|
|
2064
|
+
}
|
|
2065
|
+
catch {
|
|
2066
|
+
stuck.push(target);
|
|
2067
|
+
}
|
|
2068
|
+
return stuck;
|
|
2069
|
+
};
|
|
2039
2070
|
if (plan.targetExists) {
|
|
2040
2071
|
for (const source of sources) {
|
|
2041
2072
|
const fresh = await this.io.readText(join(this.dirOf(source), 'SKILL.md'));
|
|
2042
|
-
if (fresh !== sourceBodies.get(source))
|
|
2043
|
-
|
|
2044
|
-
return rollbackMessage(`来源技能「${source}」在归档前被改过`, stuck);
|
|
2045
|
-
}
|
|
2046
|
-
const result = await this.archive(source, {
|
|
2047
|
-
absorbedInto: target,
|
|
2048
|
-
unitId,
|
|
2049
|
-
unitHead: { skill: target },
|
|
2050
|
-
reason: `Merged into ${target}`,
|
|
2051
|
-
}, plan.origin);
|
|
2052
|
-
if (!result.ok) {
|
|
2053
|
-
const stuck = await rollbackSources();
|
|
2054
|
-
return rollbackMessage(result.message, stuck);
|
|
2055
|
-
}
|
|
2056
|
-
archived.push(source);
|
|
2073
|
+
if (fresh !== sourceBodies.get(source))
|
|
2074
|
+
return rollbackMessage(`来源技能「${source}」在归档前被改过`, []);
|
|
2057
2075
|
}
|
|
2058
2076
|
const writes = [
|
|
2059
2077
|
{ target: targetPath, content: plan.body.trimEnd() + '\n', expected: plan.expectedTargetBody },
|
|
@@ -2074,9 +2092,27 @@ export class SkillLibrary {
|
|
|
2074
2092
|
unitId,
|
|
2075
2093
|
unitHead,
|
|
2076
2094
|
});
|
|
2077
|
-
if (!result.ok)
|
|
2078
|
-
|
|
2079
|
-
|
|
2095
|
+
if (!result.ok)
|
|
2096
|
+
return rollbackMessage(result.message, []);
|
|
2097
|
+
for (const source of sources) {
|
|
2098
|
+
const fresh = await this.io.readText(join(this.dirOf(source), 'SKILL.md'));
|
|
2099
|
+
if (fresh !== sourceBodies.get(source)) {
|
|
2100
|
+
const stuck = await rollbackSources();
|
|
2101
|
+
stuck.push(...await rollbackTarget());
|
|
2102
|
+
return rollbackMessage(`来源技能「${source}」在归档前被改过`, stuck);
|
|
2103
|
+
}
|
|
2104
|
+
const archivedResult = await this.archive(source, {
|
|
2105
|
+
absorbedInto: target,
|
|
2106
|
+
unitId,
|
|
2107
|
+
unitHead: { skill: target },
|
|
2108
|
+
reason: `Merged into ${target}`,
|
|
2109
|
+
}, plan.origin);
|
|
2110
|
+
if (!archivedResult.ok) {
|
|
2111
|
+
const stuck = await rollbackSources();
|
|
2112
|
+
stuck.push(...await rollbackTarget());
|
|
2113
|
+
return rollbackMessage(archivedResult.message, stuck);
|
|
2114
|
+
}
|
|
2115
|
+
archived.push(source);
|
|
2080
2116
|
}
|
|
2081
2117
|
return { ok: true, message: `已把 ${sources.join(', ')} 合并进「${target}」。`, unitId, targetPath: dir, archived, mutations: 1 + sources.length };
|
|
2082
2118
|
}
|
package/package.json
CHANGED
|
@@ -207,6 +207,15 @@
|
|
|
207
207
|
"marker": "transactIo(options.io, path",
|
|
208
208
|
"state": [],
|
|
209
209
|
"note": "The release half of the same lock: delete only when the recorded token still matches."
|
|
210
|
+
},
|
|
211
|
+
{
|
|
212
|
+
"id": "merge-pair-index",
|
|
213
|
+
"path": "<evolutionHome>/merge/pair-index.json",
|
|
214
|
+
"writer": "evolution-merge/src/domain/pair-index-store.ts",
|
|
215
|
+
"serializedBy": "transact",
|
|
216
|
+
"marker": "transactIo(io, pairIndexPath(home)",
|
|
217
|
+
"state": [],
|
|
218
|
+
"note": "pairKey → result/runId/target index; RMW under the same IO lock so two merge runs cannot lose each other's entries."
|
|
210
219
|
}
|
|
211
220
|
]
|
|
212
221
|
}
|