@lmzhen/dsh-evolution-core 0.30.17 → 0.30.18
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/composition/index.js +2 -2
- package/lib/{composition-D8Y3-ROd.js → composition-DSesmTvf.js} +3 -3
- package/lib/{http-routes-ifJKGnu6.js → http-routes-D1Ty6nnh.js} +2 -0
- package/lib/http-routes.js +1 -1
- package/lib/index.js +2863 -2049
- package/lib/subagent/index.js +1 -1
- package/lib/types/composition/composer.js +1 -1
- package/lib/types/frontmatter.js +1 -1
- package/lib/types/http-routes.d.ts +2 -0
- package/lib/types/http-routes.js +2 -0
- package/lib/types/index.d.ts +4 -0
- package/lib/types/index.js +4 -0
- package/lib/types/params.js +0 -1
- package/lib/types/platform-services.d.ts +1 -1
- package/lib/types/platform-services.js +2 -2
- package/lib/types/redact.js +2 -2
- package/lib/types/run-registry.d.ts +8 -0
- package/lib/types/run-registry.js +20 -0
- package/lib/types/skill-store.d.ts +10 -0
- package/lib/types/skill-store.js +70 -3
- package/lib/types/tree-lock.d.ts +3 -0
- package/lib/types/tree-lock.js +13 -5
- package/lib/types/unit-backend.d.ts +5 -0
- package/lib/types/unit-backend.js +49 -0
- package/lib/types/unit-journal.d.ts +29 -0
- package/lib/types/unit-journal.js +50 -0
- package/lib/types/unit-recovery.d.ts +5 -0
- package/lib/types/unit-recovery.js +12 -0
- package/lib/types/unit-storage.d.ts +83 -0
- package/lib/types/unit-storage.js +124 -0
- package/lib/types/unit-transaction.d.ts +195 -0
- package/lib/types/unit-transaction.js +445 -0
- package/package.json +1 -1
- package/lib/{ensure-MEYMj9jn.js → ensure-BuWsjPGD.js} +1 -1
|
@@ -0,0 +1,445 @@
|
|
|
1
|
+
/** G2 opaque UnitTransaction. Core owns prepare/commit/verify/rollback lifecycle. */
|
|
2
|
+
import { createHash } from "node:crypto";
|
|
3
|
+
import { acquireTreeWriteLock } from "./tree-lock.js";
|
|
4
|
+
import { readUnitEvidence, readUnitReceipt, readUnitSnapshot, unitStorageKey, writeUnitEvidence, writeUnitReceipt, writeUnitSnapshot, } from "./unit-storage.js";
|
|
5
|
+
import { readUnitJournal, writeUnitJournal } from "./unit-journal.js";
|
|
6
|
+
function canonical(value) {
|
|
7
|
+
if (value === null || typeof value !== "object")
|
|
8
|
+
return JSON.stringify(value);
|
|
9
|
+
if (Array.isArray(value))
|
|
10
|
+
return "[" + value.map(canonical).join(",") + "]";
|
|
11
|
+
const object = value;
|
|
12
|
+
return ("{" +
|
|
13
|
+
Object.keys(object)
|
|
14
|
+
.sort()
|
|
15
|
+
.map((key) => JSON.stringify(key) + ":" + canonical(object[key]))
|
|
16
|
+
.join(",") +
|
|
17
|
+
"}");
|
|
18
|
+
}
|
|
19
|
+
function digest(plan) {
|
|
20
|
+
return createHash("sha256").update(canonical(plan)).digest("hex");
|
|
21
|
+
}
|
|
22
|
+
export class UnitTransactions {
|
|
23
|
+
options;
|
|
24
|
+
prepared = new Map();
|
|
25
|
+
commits = new Map();
|
|
26
|
+
now;
|
|
27
|
+
constructor(options) {
|
|
28
|
+
this.options = options;
|
|
29
|
+
this.now = options.now ?? (() => Date.now());
|
|
30
|
+
}
|
|
31
|
+
unitId(plan) {
|
|
32
|
+
return plan.runId + ":" + createHash("sha256").update(plan.pairKey).digest("hex").slice(0, 16);
|
|
33
|
+
}
|
|
34
|
+
async snapshot(plan, unitId, planDigest) {
|
|
35
|
+
const targetBefore = await this.options.backend.readSkill(plan.target);
|
|
36
|
+
const supportBefore = await Promise.all(plan.supportFiles.map(async (file) => ({
|
|
37
|
+
path: file.path,
|
|
38
|
+
content: await this.options.backend.readSupportFile(plan.target, file.path),
|
|
39
|
+
})));
|
|
40
|
+
const sourceBefore = await Promise.all(plan.sources
|
|
41
|
+
.filter((source) => source !== plan.target)
|
|
42
|
+
.map(async (name) => ({ name, body: (await this.options.backend.readSkill(name)) ?? "" })));
|
|
43
|
+
return {
|
|
44
|
+
schemaVersion: 1,
|
|
45
|
+
unitId,
|
|
46
|
+
planDigest,
|
|
47
|
+
runId: plan.runId,
|
|
48
|
+
target: plan.target,
|
|
49
|
+
targetExists: plan.targetExists,
|
|
50
|
+
targetBefore,
|
|
51
|
+
targetAfter: plan.body,
|
|
52
|
+
supportBefore,
|
|
53
|
+
supportAfter: plan.supportFiles,
|
|
54
|
+
sourceBefore,
|
|
55
|
+
origin: plan.origin,
|
|
56
|
+
summary: plan.summary,
|
|
57
|
+
createdAt: this.now(),
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
async prepare(plan) {
|
|
61
|
+
const unitId = this.unitId(plan);
|
|
62
|
+
const planDigest = digest(plan);
|
|
63
|
+
const existing = this.prepared.get(unitId);
|
|
64
|
+
if (existing !== undefined)
|
|
65
|
+
return existing.digest === planDigest
|
|
66
|
+
? { ok: true, prepared: { unitId, planDigest } }
|
|
67
|
+
: { ok: false, code: "unit-conflict", message: "unitId already prepared with a different plan" };
|
|
68
|
+
const snapshot = await this.snapshot(plan, unitId, planDigest);
|
|
69
|
+
const path = await writeUnitSnapshot(this.options.io, this.options.home, snapshot);
|
|
70
|
+
const journal = {
|
|
71
|
+
schemaVersion: 1,
|
|
72
|
+
unitId,
|
|
73
|
+
runId: plan.runId,
|
|
74
|
+
planDigest,
|
|
75
|
+
target: plan.target,
|
|
76
|
+
sources: plan.sources,
|
|
77
|
+
phase: "prepared",
|
|
78
|
+
storageKey: unitStorageKey(unitId),
|
|
79
|
+
snapshotSha256: createHash("sha256")
|
|
80
|
+
.update(JSON.stringify(snapshot, null, 2) + "\n")
|
|
81
|
+
.digest("hex"),
|
|
82
|
+
updatedAt: this.now(),
|
|
83
|
+
};
|
|
84
|
+
await writeUnitJournal(this.options.io, this.options.home, journal);
|
|
85
|
+
await this.options.onPhase?.("prepared");
|
|
86
|
+
this.prepared.set(unitId, {
|
|
87
|
+
plan,
|
|
88
|
+
digest: planDigest,
|
|
89
|
+
at: this.now(),
|
|
90
|
+
snapshotSha256: createHash("sha256")
|
|
91
|
+
.update(JSON.stringify(snapshot, null, 2) + "\n")
|
|
92
|
+
.digest("hex"),
|
|
93
|
+
});
|
|
94
|
+
void path;
|
|
95
|
+
return { ok: true, prepared: { unitId, planDigest } };
|
|
96
|
+
}
|
|
97
|
+
async commit(prepared, options) {
|
|
98
|
+
const running = this.commits.get(prepared.unitId);
|
|
99
|
+
if (running !== undefined)
|
|
100
|
+
return await running;
|
|
101
|
+
const task = this.commitOnce(prepared, options);
|
|
102
|
+
this.commits.set(prepared.unitId, task);
|
|
103
|
+
try {
|
|
104
|
+
return await task;
|
|
105
|
+
}
|
|
106
|
+
finally {
|
|
107
|
+
this.commits.delete(prepared.unitId);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
async commitOnce(prepared, options) {
|
|
111
|
+
const existing = await readUnitEvidence(this.options.io, this.options.home, prepared.unitId);
|
|
112
|
+
if (existing !== null)
|
|
113
|
+
return existing.planDigest === prepared.planDigest
|
|
114
|
+
? { ok: true, evidence: existing }
|
|
115
|
+
: { ok: false, code: "unit-conflict", message: "unit already committed with a different digest" };
|
|
116
|
+
const stored = this.prepared.get(prepared.unitId);
|
|
117
|
+
if (stored === undefined || stored.digest !== prepared.planDigest)
|
|
118
|
+
return {
|
|
119
|
+
ok: false,
|
|
120
|
+
code: "unit-plan-missing",
|
|
121
|
+
message: "prepared plan is not in this process; manual recovery required",
|
|
122
|
+
};
|
|
123
|
+
if (options?.signal?.aborted === true)
|
|
124
|
+
return { ok: false, code: "unit-commit-failed", message: "cancelled before commit" };
|
|
125
|
+
const journalBase = {
|
|
126
|
+
schemaVersion: 1,
|
|
127
|
+
unitId: prepared.unitId,
|
|
128
|
+
runId: stored.plan.runId,
|
|
129
|
+
planDigest: stored.digest,
|
|
130
|
+
target: stored.plan.target,
|
|
131
|
+
sources: stored.plan.sources,
|
|
132
|
+
storageKey: unitStorageKey(prepared.unitId),
|
|
133
|
+
snapshotSha256: stored.snapshotSha256,
|
|
134
|
+
updatedAt: this.now(),
|
|
135
|
+
};
|
|
136
|
+
let lock;
|
|
137
|
+
let claim;
|
|
138
|
+
const checkpoints = [];
|
|
139
|
+
const writePhase = async (phase, checkpointsNext) => {
|
|
140
|
+
if (lock !== undefined && !(await lock.assertHeld()))
|
|
141
|
+
throw new Error("unit-fence-lost");
|
|
142
|
+
await writeUnitJournal(this.options.io, this.options.home, {
|
|
143
|
+
...journalBase,
|
|
144
|
+
phase,
|
|
145
|
+
...(checkpointsNext === undefined ? {} : { checkpoints: checkpointsNext }),
|
|
146
|
+
});
|
|
147
|
+
await this.options.onPhase?.(phase);
|
|
148
|
+
};
|
|
149
|
+
try {
|
|
150
|
+
const acquired = await acquireTreeWriteLock({ io: this.options.io, home: this.options.home, waitMs: this.options.waitMs ?? 5000 });
|
|
151
|
+
if (!acquired.ok)
|
|
152
|
+
return { ok: false, code: "unit-fence-failed", message: acquired.message ?? acquired.reason };
|
|
153
|
+
lock = acquired.lock;
|
|
154
|
+
if (!(await lock.assertHeld()))
|
|
155
|
+
return { ok: false, code: "unit-fence-failed", message: "tree fence lost" };
|
|
156
|
+
this.options.onLock?.("tree");
|
|
157
|
+
claim = await this.options.claim.acquire(stored.plan.target, stored.plan.runId);
|
|
158
|
+
this.options.onLock?.("claim");
|
|
159
|
+
await writeUnitJournal(this.options.io, this.options.home, { ...journalBase, phase: "target-pending" });
|
|
160
|
+
const result = await this.options.backend.commit(stored.plan, async (phase, detail) => {
|
|
161
|
+
if (phase === "support-pending") {
|
|
162
|
+
await writePhase("support-pending");
|
|
163
|
+
await this.options.onPhase?.("support-pending");
|
|
164
|
+
return;
|
|
165
|
+
}
|
|
166
|
+
if (phase === "target-applied") {
|
|
167
|
+
await this.options.onPhase?.("target-applied");
|
|
168
|
+
return;
|
|
169
|
+
}
|
|
170
|
+
if (phase === "unit-written") {
|
|
171
|
+
if (lock !== undefined && !(await lock.assertHeld()))
|
|
172
|
+
throw new Error("unit-fence-lost");
|
|
173
|
+
await writeUnitReceipt(this.options.io, this.options.home, {
|
|
174
|
+
schemaVersion: 1,
|
|
175
|
+
unitId: prepared.unitId,
|
|
176
|
+
runId: stored.plan.runId,
|
|
177
|
+
planDigest: stored.digest,
|
|
178
|
+
target: stored.plan.target,
|
|
179
|
+
archived: stored.plan.sources.filter((source) => source !== stored.plan.target),
|
|
180
|
+
at: this.now(),
|
|
181
|
+
});
|
|
182
|
+
await this.options.onPhase?.("unit-written");
|
|
183
|
+
return;
|
|
184
|
+
}
|
|
185
|
+
if (phase === "source-archiving" && detail?.source !== undefined) {
|
|
186
|
+
checkpoints.push({
|
|
187
|
+
source: detail.source,
|
|
188
|
+
phase: "archived",
|
|
189
|
+
hash: createHash("sha256")
|
|
190
|
+
.update(stored.plan.expectedSourceBodies.find((item) => item.name === detail.source)?.body ?? "")
|
|
191
|
+
.digest("hex"),
|
|
192
|
+
at: this.now(),
|
|
193
|
+
});
|
|
194
|
+
}
|
|
195
|
+
await writePhase(phase, checkpoints);
|
|
196
|
+
}, options?.signal, async () => lock !== undefined && (await lock.assertHeld()));
|
|
197
|
+
if (!result.ok) {
|
|
198
|
+
await writePhase("failed");
|
|
199
|
+
return { ok: false, code: "unit-commit-failed", message: result.message };
|
|
200
|
+
}
|
|
201
|
+
const targetAfter = (await this.options.backend.readSkill(stored.plan.target)) ?? "";
|
|
202
|
+
const evidence = {
|
|
203
|
+
schemaVersion: 1,
|
|
204
|
+
unitId: prepared.unitId,
|
|
205
|
+
runId: stored.plan.runId,
|
|
206
|
+
planDigest: stored.digest,
|
|
207
|
+
state: "complete",
|
|
208
|
+
target: {
|
|
209
|
+
before: stored.plan.expectedTargetBody,
|
|
210
|
+
after: targetAfter,
|
|
211
|
+
supportDigest: createHash("sha256").update(JSON.stringify(stored.plan.supportFiles)).digest("hex"),
|
|
212
|
+
created: !stored.plan.targetExists,
|
|
213
|
+
},
|
|
214
|
+
sources: stored.plan.sources
|
|
215
|
+
.filter((source) => source !== stored.plan.target)
|
|
216
|
+
.map((name) => ({
|
|
217
|
+
name,
|
|
218
|
+
before: stored.plan.expectedSourceBodies.find((item) => item.name === name)?.body ?? "",
|
|
219
|
+
after: null,
|
|
220
|
+
archived: (result.archived ?? []).includes(name),
|
|
221
|
+
absorbedInto: stored.plan.target,
|
|
222
|
+
})),
|
|
223
|
+
mutations: result.mutations ?? 0,
|
|
224
|
+
at: this.now(),
|
|
225
|
+
};
|
|
226
|
+
if (!(await lock.assertHeld()))
|
|
227
|
+
throw new Error("unit-fence-lost");
|
|
228
|
+
await writeUnitEvidence(this.options.io, this.options.home, evidence);
|
|
229
|
+
await writePhase("committed", checkpoints);
|
|
230
|
+
this.prepared.delete(prepared.unitId);
|
|
231
|
+
return { ok: true, evidence };
|
|
232
|
+
}
|
|
233
|
+
catch (error) {
|
|
234
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
235
|
+
return {
|
|
236
|
+
ok: false,
|
|
237
|
+
code: message === "unit-fence-lost" ? "unit-fence-failed" : "unit-claim-failed",
|
|
238
|
+
message,
|
|
239
|
+
};
|
|
240
|
+
}
|
|
241
|
+
finally {
|
|
242
|
+
await claim?.release();
|
|
243
|
+
await lock?.release();
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
async verify(ref) {
|
|
247
|
+
if ("unitId" in ref)
|
|
248
|
+
return await readUnitEvidence(this.options.io, this.options.home, ref.unitId);
|
|
249
|
+
const all = await Promise.all([...this.prepared.keys()].map(async (id) => readUnitEvidence(this.options.io, this.options.home, id)));
|
|
250
|
+
const found = all.filter((item) => item !== null && item.runId === ref.runId);
|
|
251
|
+
const only = found[0];
|
|
252
|
+
return found.length === 0 ? null : found.length === 1 ? (only ?? null) : found;
|
|
253
|
+
}
|
|
254
|
+
async finalize(unitId) {
|
|
255
|
+
const existing = await readUnitEvidence(this.options.io, this.options.home, unitId);
|
|
256
|
+
if (existing !== null)
|
|
257
|
+
return { ok: true, evidence: existing };
|
|
258
|
+
const snapshot = await readUnitSnapshot(this.options.io, this.options.home, unitId);
|
|
259
|
+
const journal = await readUnitJournal(this.options.io, this.options.home, unitId);
|
|
260
|
+
const receipt = await readUnitReceipt(this.options.io, this.options.home, unitId);
|
|
261
|
+
if (snapshot === null || journal === null || (journal.phase !== "committed" && receipt === null))
|
|
262
|
+
return { ok: false, code: "unit-rollback-blocked", message: "unit is not committed; manual recovery required" };
|
|
263
|
+
if (receipt !== null &&
|
|
264
|
+
(receipt.unitId !== unitId ||
|
|
265
|
+
receipt.runId !== snapshot.runId ||
|
|
266
|
+
receipt.planDigest !== snapshot.planDigest ||
|
|
267
|
+
receipt.target !== snapshot.target))
|
|
268
|
+
return {
|
|
269
|
+
ok: false,
|
|
270
|
+
code: "unit-rollback-blocked",
|
|
271
|
+
message: "unit receipt does not match snapshot; manual recovery required",
|
|
272
|
+
};
|
|
273
|
+
const checkpoints = journal.checkpoints ?? [];
|
|
274
|
+
const missing = snapshot.sourceBefore.filter((source) => !checkpoints.some((item) => item.source === source.name));
|
|
275
|
+
if (missing.length > 0)
|
|
276
|
+
return {
|
|
277
|
+
ok: false,
|
|
278
|
+
code: "unit-rollback-blocked",
|
|
279
|
+
message: "source checkpoint missing: " + missing.map((item) => item.name).join(","),
|
|
280
|
+
};
|
|
281
|
+
const evidence = {
|
|
282
|
+
schemaVersion: 1,
|
|
283
|
+
unitId,
|
|
284
|
+
runId: snapshot.runId,
|
|
285
|
+
planDigest: snapshot.planDigest,
|
|
286
|
+
state: "complete",
|
|
287
|
+
target: {
|
|
288
|
+
before: snapshot.targetBefore,
|
|
289
|
+
after: (await this.options.backend.readSkill(snapshot.target)) ?? "",
|
|
290
|
+
supportDigest: createHash("sha256").update(JSON.stringify(snapshot.supportAfter)).digest("hex"),
|
|
291
|
+
created: !snapshot.targetExists,
|
|
292
|
+
},
|
|
293
|
+
sources: snapshot.sourceBefore.map((source) => ({
|
|
294
|
+
name: source.name,
|
|
295
|
+
before: source.body,
|
|
296
|
+
after: null,
|
|
297
|
+
archived: checkpoints.some((item) => item.source === source.name),
|
|
298
|
+
absorbedInto: snapshot.target,
|
|
299
|
+
})),
|
|
300
|
+
mutations: 0,
|
|
301
|
+
at: this.now(),
|
|
302
|
+
};
|
|
303
|
+
await writeUnitEvidence(this.options.io, this.options.home, evidence);
|
|
304
|
+
if (journal.phase !== "committed")
|
|
305
|
+
await writeUnitJournal(this.options.io, this.options.home, { ...journal, phase: "committed", checkpoints, updatedAt: this.now() });
|
|
306
|
+
return { ok: true, evidence };
|
|
307
|
+
}
|
|
308
|
+
async rollForward(unitId) {
|
|
309
|
+
const existing = await readUnitEvidence(this.options.io, this.options.home, unitId);
|
|
310
|
+
if (existing !== null)
|
|
311
|
+
return { ok: true, evidence: existing };
|
|
312
|
+
const snapshot = await readUnitSnapshot(this.options.io, this.options.home, unitId);
|
|
313
|
+
const journal = await readUnitJournal(this.options.io, this.options.home, unitId);
|
|
314
|
+
if (snapshot === null || journal === null || (journal.phase !== "target-written" && journal.phase !== "sources-archived"))
|
|
315
|
+
return {
|
|
316
|
+
ok: false,
|
|
317
|
+
code: "unit-rollback-blocked",
|
|
318
|
+
message: "unit is not roll-forwardable; manual recovery required",
|
|
319
|
+
};
|
|
320
|
+
const checkpoints = journal.checkpoints ?? [];
|
|
321
|
+
const missing = snapshot.sourceBefore.filter((source) => !checkpoints.some((item) => item.source === source.name));
|
|
322
|
+
if (missing.length > 0)
|
|
323
|
+
return {
|
|
324
|
+
ok: false,
|
|
325
|
+
code: "unit-rollback-blocked",
|
|
326
|
+
message: "source checkpoint missing: " + missing.map((item) => item.name).join(","),
|
|
327
|
+
};
|
|
328
|
+
const targetAfter = await this.options.backend.readSkill(snapshot.target);
|
|
329
|
+
if (targetAfter === null)
|
|
330
|
+
return { ok: false, code: "unit-rollback-blocked", message: "target missing; manual recovery required" };
|
|
331
|
+
const evidence = {
|
|
332
|
+
schemaVersion: 1,
|
|
333
|
+
unitId,
|
|
334
|
+
runId: snapshot.runId,
|
|
335
|
+
planDigest: snapshot.planDigest,
|
|
336
|
+
state: "complete",
|
|
337
|
+
target: {
|
|
338
|
+
before: snapshot.targetBefore,
|
|
339
|
+
after: targetAfter,
|
|
340
|
+
supportDigest: createHash("sha256").update(JSON.stringify(snapshot.supportAfter)).digest("hex"),
|
|
341
|
+
created: !snapshot.targetExists,
|
|
342
|
+
},
|
|
343
|
+
sources: snapshot.sourceBefore.map((source) => ({
|
|
344
|
+
name: source.name,
|
|
345
|
+
before: source.body,
|
|
346
|
+
after: null,
|
|
347
|
+
archived: checkpoints.some((item) => item.source === source.name),
|
|
348
|
+
absorbedInto: snapshot.target,
|
|
349
|
+
})),
|
|
350
|
+
mutations: 0,
|
|
351
|
+
at: this.now(),
|
|
352
|
+
};
|
|
353
|
+
await writeUnitEvidence(this.options.io, this.options.home, evidence);
|
|
354
|
+
await writeUnitJournal(this.options.io, this.options.home, { ...journal, phase: "committed", checkpoints, updatedAt: this.now() });
|
|
355
|
+
return { ok: true, evidence };
|
|
356
|
+
}
|
|
357
|
+
async rollback(ref) {
|
|
358
|
+
const evidence = await readUnitEvidence(this.options.io, this.options.home, ref.unitId);
|
|
359
|
+
const receipt = await readUnitReceipt(this.options.io, this.options.home, ref.unitId);
|
|
360
|
+
if (evidence !== null || receipt !== null)
|
|
361
|
+
return { ok: false, code: "unit-committed", message: "committed unit cannot roll back" };
|
|
362
|
+
const snapshot = await readUnitSnapshot(this.options.io, this.options.home, ref.unitId);
|
|
363
|
+
if (snapshot === null)
|
|
364
|
+
return { ok: false, code: "unit-rollback-blocked", message: "snapshot missing" };
|
|
365
|
+
const journal = await readUnitJournal(this.options.io, this.options.home, ref.unitId);
|
|
366
|
+
const acquired = await acquireTreeWriteLock({ io: this.options.io, home: this.options.home, waitMs: this.options.waitMs ?? 5000 });
|
|
367
|
+
if (!acquired.ok)
|
|
368
|
+
return { ok: false, code: "unit-fence-failed", message: acquired.message ?? acquired.reason };
|
|
369
|
+
let claim;
|
|
370
|
+
try {
|
|
371
|
+
if (!(await acquired.lock.assertHeld()))
|
|
372
|
+
return { ok: false, code: "unit-fence-failed", message: "tree fence lost" };
|
|
373
|
+
this.options.onLock?.("tree");
|
|
374
|
+
claim = await this.options.claim.acquire(snapshot.target, snapshot.runId);
|
|
375
|
+
this.options.onLock?.("claim");
|
|
376
|
+
const currentTarget = await this.options.backend.readSkill(snapshot.target);
|
|
377
|
+
if (currentTarget !== snapshot.targetBefore && currentTarget !== (snapshot.targetAfter ?? null))
|
|
378
|
+
return {
|
|
379
|
+
ok: false,
|
|
380
|
+
code: "unit-rollback-blocked",
|
|
381
|
+
message: "target drifted since snapshot; manual recovery required",
|
|
382
|
+
};
|
|
383
|
+
for (const source of snapshot.sourceBefore) {
|
|
384
|
+
const current = await this.options.backend.readSkill(source.name);
|
|
385
|
+
if (current !== null && current !== source.body)
|
|
386
|
+
return { ok: false, code: "unit-rollback-blocked", message: "source drifted since snapshot: " + source.name };
|
|
387
|
+
}
|
|
388
|
+
if (!(await acquired.lock.assertHeld()))
|
|
389
|
+
return { ok: false, code: "unit-fence-failed", message: "tree fence lost before target restore" };
|
|
390
|
+
await this.options.backend.restoreTarget(snapshot.target, snapshot.targetBefore);
|
|
391
|
+
for (const file of snapshot.supportBefore) {
|
|
392
|
+
if (!(await acquired.lock.assertHeld()))
|
|
393
|
+
return { ok: false, code: "unit-fence-failed", message: "tree fence lost before support restore" };
|
|
394
|
+
await this.options.backend.restoreSupportFile?.(snapshot.target, file.path, file.content);
|
|
395
|
+
}
|
|
396
|
+
for (const source of snapshot.sourceBefore) {
|
|
397
|
+
if (!(await acquired.lock.assertHeld()))
|
|
398
|
+
return { ok: false, code: "unit-fence-failed", message: "tree fence lost before source restore" };
|
|
399
|
+
await this.options.backend.restoreSource(source.name, source.body);
|
|
400
|
+
}
|
|
401
|
+
if (journal !== null)
|
|
402
|
+
await writeUnitJournal(this.options.io, this.options.home, { ...journal, phase: "rolled-back", updatedAt: this.now() });
|
|
403
|
+
const evidenceRecord = {
|
|
404
|
+
schemaVersion: 1,
|
|
405
|
+
unitId: ref.unitId,
|
|
406
|
+
runId: snapshot.runId,
|
|
407
|
+
planDigest: snapshot.planDigest,
|
|
408
|
+
state: "target-only",
|
|
409
|
+
target: { before: snapshot.targetBefore, after: snapshot.targetBefore ?? "", supportDigest: "", created: false },
|
|
410
|
+
sources: snapshot.sourceBefore.map((source) => ({
|
|
411
|
+
name: source.name,
|
|
412
|
+
before: source.body,
|
|
413
|
+
after: null,
|
|
414
|
+
archived: false,
|
|
415
|
+
absorbedInto: snapshot.target,
|
|
416
|
+
})),
|
|
417
|
+
mutations: 0,
|
|
418
|
+
at: this.now(),
|
|
419
|
+
};
|
|
420
|
+
return { ok: true, evidence: evidenceRecord };
|
|
421
|
+
}
|
|
422
|
+
finally {
|
|
423
|
+
await claim?.release();
|
|
424
|
+
await acquired.lock.release();
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
async cancel(unitId, reason) {
|
|
428
|
+
const journal = await readUnitJournal(this.options.io, this.options.home, unitId);
|
|
429
|
+
if (journal === null)
|
|
430
|
+
return { ok: false, phase: "none" };
|
|
431
|
+
if (journal.phase === "prepared" && this.prepared.has(unitId)) {
|
|
432
|
+
this.prepared.delete(unitId);
|
|
433
|
+
return { ok: true, phase: "prepared" };
|
|
434
|
+
}
|
|
435
|
+
if (journal.phase === "target-pending" ||
|
|
436
|
+
journal.phase === "target-written" ||
|
|
437
|
+
journal.phase === "source-archiving" ||
|
|
438
|
+
journal.phase === "sources-archived") {
|
|
439
|
+
const rolled = await this.rollback({ unitId, reason });
|
|
440
|
+
return { ok: rolled.ok, phase: journal.phase };
|
|
441
|
+
}
|
|
442
|
+
return { ok: false, phase: journal.phase };
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
//# sourceMappingURL=unit-transaction.js.map
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { n as errorText } from "./errors-Br2-uUSK.js";
|
|
2
|
-
import { existsSync, readFileSync } from "node:fs";
|
|
3
2
|
import { dirname, join } from "node:path";
|
|
3
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
4
4
|
import { fileURLToPath } from "node:url";
|
|
5
5
|
import * as yaml from "js-yaml";
|
|
6
6
|
//#region lib/types/package-identity.js
|