@lmzhen/dsh-evolution-merge 0.40.1 → 0.42.0
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 +760 -101
- package/lib/types/adapters/library.d.ts +1 -1
- package/lib/types/adapters/model-runner.d.ts +1 -1
- package/lib/types/adapters/model-runner.js +1 -1
- package/lib/types/adapters/undo.d.ts +1 -6
- package/lib/types/authoring-flow.d.ts +2 -1
- package/lib/types/commit-assembly.d.ts +4 -2
- package/lib/types/commit-assembly.js +2 -1
- package/lib/types/commit-execute.d.ts +3 -2
- package/lib/types/commit-execute.js +1 -25
- package/lib/types/commit-plan.d.ts +2 -1
- package/lib/types/commit-recovery.js +1 -1
- package/lib/types/doctor.js +2 -1
- package/lib/types/domain/evidence-hash.d.ts +1 -1
- package/lib/types/index.js +5 -3
- package/lib/types/new-child-authoring.d.ts +2 -1
- package/lib/types/recovery-coordinator.d.ts +2 -1
- package/lib/types/recovery-coordinator.js +2 -1
- package/lib/types/recovery-projection.d.ts +2 -1
- package/lib/types/revision.js +2 -1
- package/lib/types/rules.js +3 -1
- package/lib/types/runtime.d.ts +3 -1
- package/lib/types/runtime.js +5 -2
- package/lib/types/scan-ports.d.ts +2 -1
- package/lib/types/skill-library-unit-backend.d.ts +5 -0
- package/lib/types/skill-library-unit-backend.js +56 -0
- package/lib/types/undo-merge.d.ts +2 -2
- package/lib/types/undo-merge.js +1 -1
- package/lib/types/undo-operation.js +1 -1
- package/lib/types/unit-claim.d.ts +1 -1
- package/lib/types/unit-protocol.d.ts +227 -0
- package/lib/types/unit-protocol.js +566 -0
- package/package.json +5 -6
package/lib/index.js
CHANGED
|
@@ -1,14 +1,15 @@
|
|
|
1
1
|
import { dirname, isAbsolute, join } from "node:path";
|
|
2
2
|
import z from "@deepseek-ai/schemastery";
|
|
3
3
|
import { pairKeyOf, screen, selectCandidates } from "@lmzhen/dsh-evolution-merge-engine";
|
|
4
|
-
import { createUserMessage } from "@
|
|
5
|
-
import { DEFAULT_SKILL_LIMITS, LeaseStore,
|
|
4
|
+
import { createUserMessage, defineTool } from "@lmzhen/dsh-evolution-core/platform";
|
|
5
|
+
import { DEFAULT_SKILL_LIMITS, LeaseStore, acquireTreeWriteLock, authorityFrom, contentHash, createSubagentChannel, errorText, evolutionHome, evolutionIoAdapter, isPresent, isProcessAlive, newRunRegistry, policyStageLimits, probeAbsent, probeList, probePresent, probeText, probeUnknown, readTreeLock, registerOwnerSettings, reportsSweepLockTarget, runsFile, scanContentThreats, supportRefs, sweepReports, transactIo } from "@lmzhen/dsh-evolution-core";
|
|
6
|
+
import { SkillLibrary, newSkillLibrary, parseFrontmatter, revisionAnchorOf, revisionFingerprint, revisionSupportDigest, validateFrontmatter, validateSupportPath } from "@lmzhen/dsh-evolution-core/skills";
|
|
6
7
|
import { HOST_ROUTES, json, methodNotAllowed, registerHostRoutes } from "@lmzhen/dsh-evolution-core/http-routes";
|
|
7
8
|
import { createHash, randomUUID } from "node:crypto";
|
|
8
9
|
import { chmod, mkdir, open, readFile, stat, unlink } from "node:fs/promises";
|
|
10
|
+
import { executeUnitUndo, planUnitUndoAcross, readUnitEvidence, readUnitIndex, readUnitJournal, readUnitReceipt, readUnitSnapshot, unitStorageKey, writeUnitEvidence, writeUnitJournal, writeUnitReceipt, writeUnitSnapshot } from "@lmzhen/dsh-evolution-core/write";
|
|
9
11
|
import { aliasRegistration, buildSurface, parseCommand } from "@lmzhen/dsh-evolution-commands";
|
|
10
12
|
import { readFileSync } from "node:fs";
|
|
11
|
-
import { defineTool } from "@lmzhen/dsh-evolution-core/platform";
|
|
12
13
|
//#region lib/types/adapters/select.js
|
|
13
14
|
/** Host selector correction: engine selector does not route judge errors to candidates. */
|
|
14
15
|
function selectWithErrors(report) {
|
|
@@ -2678,7 +2679,7 @@ function isReport(value) {
|
|
|
2678
2679
|
const report = value;
|
|
2679
2680
|
return report.schemaVersion === 2 && report.kind === "merge" && typeof report.runId === "string" && typeof report.command === "string" && typeof report.startedAt === "number" && typeof report.endedAt === "number" && Array.isArray(report.pairs) && typeof report.totals === "object" && Array.isArray(report.notes);
|
|
2680
2681
|
}
|
|
2681
|
-
function digest$
|
|
2682
|
+
function digest$3(report) {
|
|
2682
2683
|
const lines = [];
|
|
2683
2684
|
lines.push("# merge " + report.runId);
|
|
2684
2685
|
lines.push("");
|
|
@@ -2697,7 +2698,7 @@ function digest$2(report) {
|
|
|
2697
2698
|
async function writeMergeReport(io, home, report) {
|
|
2698
2699
|
const path = mergeReportPath(home, report.runId);
|
|
2699
2700
|
await io.writeText(path, JSON.stringify(report, null, 2) + "\n");
|
|
2700
|
-
await io.writeText(mergeReportDigestPath(home, report.runId), digest$
|
|
2701
|
+
await io.writeText(mergeReportDigestPath(home, report.runId), digest$3(report));
|
|
2701
2702
|
return path;
|
|
2702
2703
|
}
|
|
2703
2704
|
async function readMergeReport(io, home, runId) {
|
|
@@ -3124,6 +3125,754 @@ async function readHistory(io, home) {
|
|
|
3124
3125
|
}));
|
|
3125
3126
|
}
|
|
3126
3127
|
//#endregion
|
|
3128
|
+
//#region lib/types/unit-protocol.js
|
|
3129
|
+
/** G2 opaque UnitTransaction. Core owns prepare/commit/verify/rollback lifecycle. */
|
|
3130
|
+
function canonicalJson(value) {
|
|
3131
|
+
if (value === null || typeof value !== "object") return JSON.stringify(value);
|
|
3132
|
+
if (Array.isArray(value)) return "[" + value.map(canonicalJson).join(",") + "]";
|
|
3133
|
+
const object = value;
|
|
3134
|
+
return "{" + Object.keys(object).sort().map((key) => JSON.stringify(key) + ":" + canonicalJson(object[key])).join(",") + "}";
|
|
3135
|
+
}
|
|
3136
|
+
function sameTarget(a, b) {
|
|
3137
|
+
return (a ?? "").trimEnd() === (b ?? "").trimEnd();
|
|
3138
|
+
}
|
|
3139
|
+
function digest$2(plan) {
|
|
3140
|
+
return createHash("sha256").update(canonicalJson(plan)).digest("hex");
|
|
3141
|
+
}
|
|
3142
|
+
function unitEvidenceDigest(evidence) {
|
|
3143
|
+
return createHash("sha256").update(canonicalJson({
|
|
3144
|
+
...evidence,
|
|
3145
|
+
at: 0,
|
|
3146
|
+
mutations: 0
|
|
3147
|
+
})).digest("hex");
|
|
3148
|
+
}
|
|
3149
|
+
var MergeUnitTransactions = class {
|
|
3150
|
+
options;
|
|
3151
|
+
prepared = /* @__PURE__ */ new Map();
|
|
3152
|
+
commits = /* @__PURE__ */ new Map();
|
|
3153
|
+
now;
|
|
3154
|
+
constructor(options) {
|
|
3155
|
+
this.options = options;
|
|
3156
|
+
this.now = options.now ?? (() => Date.now());
|
|
3157
|
+
}
|
|
3158
|
+
unitId(plan) {
|
|
3159
|
+
return plan.unitId ?? plan.runId + ":" + createHash("sha256").update(plan.pairKey).digest("hex").slice(0, 16);
|
|
3160
|
+
}
|
|
3161
|
+
async snapshot(plan, unitId, planDigest) {
|
|
3162
|
+
const targetBefore = await this.options.backend.readSkill(plan.target);
|
|
3163
|
+
const supportBefore = await Promise.all(plan.supportFiles.map(async (file) => ({
|
|
3164
|
+
path: file.path,
|
|
3165
|
+
content: await this.options.backend.readSupportFile(plan.target, file.path)
|
|
3166
|
+
})));
|
|
3167
|
+
const sourceBefore = await Promise.all(plan.sources.filter((source) => source !== plan.target).map(async (name) => ({
|
|
3168
|
+
name,
|
|
3169
|
+
body: await this.options.backend.readSkill(name) ?? ""
|
|
3170
|
+
})));
|
|
3171
|
+
return {
|
|
3172
|
+
schemaVersion: 1,
|
|
3173
|
+
unitId,
|
|
3174
|
+
planDigest,
|
|
3175
|
+
runId: plan.runId,
|
|
3176
|
+
target: plan.target,
|
|
3177
|
+
targetExists: plan.targetExists,
|
|
3178
|
+
targetBefore,
|
|
3179
|
+
targetAfter: plan.body,
|
|
3180
|
+
supportBefore,
|
|
3181
|
+
supportAfter: plan.supportFiles,
|
|
3182
|
+
sourceBefore,
|
|
3183
|
+
origin: plan.origin,
|
|
3184
|
+
summary: plan.summary,
|
|
3185
|
+
...plan.validationPlan === void 0 ? {} : { validationPlan: plan.validationPlan },
|
|
3186
|
+
createdAt: this.now()
|
|
3187
|
+
};
|
|
3188
|
+
}
|
|
3189
|
+
async prepare(plan) {
|
|
3190
|
+
const unitId = this.unitId(plan);
|
|
3191
|
+
const planDigest = digest$2(plan);
|
|
3192
|
+
const existing = this.prepared.get(unitId);
|
|
3193
|
+
if (existing !== void 0) return existing.digest === planDigest ? {
|
|
3194
|
+
ok: true,
|
|
3195
|
+
prepared: {
|
|
3196
|
+
unitId,
|
|
3197
|
+
planDigest
|
|
3198
|
+
}
|
|
3199
|
+
} : {
|
|
3200
|
+
ok: false,
|
|
3201
|
+
code: "unit-conflict",
|
|
3202
|
+
message: "unitId already prepared with a different plan"
|
|
3203
|
+
};
|
|
3204
|
+
const snapshot = await this.snapshot(plan, unitId, planDigest);
|
|
3205
|
+
await writeUnitSnapshot(this.options.io, this.options.home, snapshot);
|
|
3206
|
+
const journal = {
|
|
3207
|
+
schemaVersion: 1,
|
|
3208
|
+
unitId,
|
|
3209
|
+
runId: plan.runId,
|
|
3210
|
+
planDigest,
|
|
3211
|
+
target: plan.target,
|
|
3212
|
+
sources: plan.sources,
|
|
3213
|
+
phase: "prepared",
|
|
3214
|
+
storageKey: unitStorageKey(unitId),
|
|
3215
|
+
snapshotSha256: createHash("sha256").update(JSON.stringify(snapshot, null, 2) + "\n").digest("hex"),
|
|
3216
|
+
updatedAt: this.now()
|
|
3217
|
+
};
|
|
3218
|
+
await writeUnitJournal(this.options.io, this.options.home, journal);
|
|
3219
|
+
await this.options.onPhase?.("prepared");
|
|
3220
|
+
this.prepared.set(unitId, {
|
|
3221
|
+
plan,
|
|
3222
|
+
digest: planDigest,
|
|
3223
|
+
at: this.now(),
|
|
3224
|
+
snapshotSha256: createHash("sha256").update(JSON.stringify(snapshot, null, 2) + "\n").digest("hex")
|
|
3225
|
+
});
|
|
3226
|
+
return {
|
|
3227
|
+
ok: true,
|
|
3228
|
+
prepared: {
|
|
3229
|
+
unitId,
|
|
3230
|
+
planDigest
|
|
3231
|
+
}
|
|
3232
|
+
};
|
|
3233
|
+
}
|
|
3234
|
+
async commit(prepared, options) {
|
|
3235
|
+
const running = this.commits.get(prepared.unitId);
|
|
3236
|
+
if (running !== void 0) return await running;
|
|
3237
|
+
const run = () => this.commitOnce(prepared, options);
|
|
3238
|
+
const task = this.options.serial === void 0 ? run() : this.options.serial(run);
|
|
3239
|
+
this.commits.set(prepared.unitId, task);
|
|
3240
|
+
try {
|
|
3241
|
+
return await task;
|
|
3242
|
+
} finally {
|
|
3243
|
+
this.commits.delete(prepared.unitId);
|
|
3244
|
+
}
|
|
3245
|
+
}
|
|
3246
|
+
async commitOnce(prepared, options) {
|
|
3247
|
+
const existing = await readUnitEvidence(this.options.io, this.options.home, prepared.unitId);
|
|
3248
|
+
if (existing !== null) return existing.planDigest === prepared.planDigest ? {
|
|
3249
|
+
ok: true,
|
|
3250
|
+
evidence: existing
|
|
3251
|
+
} : {
|
|
3252
|
+
ok: false,
|
|
3253
|
+
code: "unit-conflict",
|
|
3254
|
+
message: "unit already committed with a different digest"
|
|
3255
|
+
};
|
|
3256
|
+
const stored = this.prepared.get(prepared.unitId);
|
|
3257
|
+
if (stored === void 0 || stored.digest !== prepared.planDigest) return {
|
|
3258
|
+
ok: false,
|
|
3259
|
+
code: "unit-plan-missing",
|
|
3260
|
+
message: "prepared plan is not in this process; manual recovery required"
|
|
3261
|
+
};
|
|
3262
|
+
if (options?.signal?.aborted === true) return {
|
|
3263
|
+
ok: false,
|
|
3264
|
+
code: "unit-commit-failed",
|
|
3265
|
+
message: "cancelled before commit"
|
|
3266
|
+
};
|
|
3267
|
+
if (this.options.isActive !== void 0 && !await this.options.isActive(stored.plan.runId)) return {
|
|
3268
|
+
ok: false,
|
|
3269
|
+
code: "unit-run-inactive",
|
|
3270
|
+
message: "run is no longer active; refusing to commit"
|
|
3271
|
+
};
|
|
3272
|
+
const journalBase = {
|
|
3273
|
+
schemaVersion: 1,
|
|
3274
|
+
unitId: prepared.unitId,
|
|
3275
|
+
runId: stored.plan.runId,
|
|
3276
|
+
planDigest: stored.digest,
|
|
3277
|
+
target: stored.plan.target,
|
|
3278
|
+
sources: stored.plan.sources,
|
|
3279
|
+
storageKey: unitStorageKey(prepared.unitId),
|
|
3280
|
+
snapshotSha256: stored.snapshotSha256,
|
|
3281
|
+
updatedAt: this.now()
|
|
3282
|
+
};
|
|
3283
|
+
let lock;
|
|
3284
|
+
let fenceLease;
|
|
3285
|
+
let claim;
|
|
3286
|
+
let stage = "fence";
|
|
3287
|
+
const checkpoints = [];
|
|
3288
|
+
const writePhase = async (phase, checkpointsNext = checkpoints) => {
|
|
3289
|
+
if (lock !== void 0 && !await lock.assertHeld()) throw new Error("unit-fence-lost");
|
|
3290
|
+
await writeUnitJournal(this.options.io, this.options.home, {
|
|
3291
|
+
...journalBase,
|
|
3292
|
+
phase,
|
|
3293
|
+
checkpoints: checkpointsNext
|
|
3294
|
+
});
|
|
3295
|
+
await this.options.onPhase?.(phase);
|
|
3296
|
+
};
|
|
3297
|
+
try {
|
|
3298
|
+
if (this.options.fence !== void 0) {
|
|
3299
|
+
fenceLease = await this.options.fence.acquire();
|
|
3300
|
+
lock = fenceLease.lock;
|
|
3301
|
+
this.options.onLock?.("tree");
|
|
3302
|
+
} else {
|
|
3303
|
+
const acquired = await acquireTreeWriteLock({
|
|
3304
|
+
io: this.options.io,
|
|
3305
|
+
home: this.options.home,
|
|
3306
|
+
waitMs: this.options.waitMs ?? 5e3
|
|
3307
|
+
});
|
|
3308
|
+
if (!acquired.ok) return {
|
|
3309
|
+
ok: false,
|
|
3310
|
+
code: "unit-fence-failed",
|
|
3311
|
+
message: acquired.message ?? acquired.reason
|
|
3312
|
+
};
|
|
3313
|
+
lock = acquired.lock;
|
|
3314
|
+
this.options.onLock?.("tree");
|
|
3315
|
+
}
|
|
3316
|
+
if (!await lock.assertHeld()) return {
|
|
3317
|
+
ok: false,
|
|
3318
|
+
code: "unit-fence-failed",
|
|
3319
|
+
message: "tree fence lost"
|
|
3320
|
+
};
|
|
3321
|
+
stage = "claim";
|
|
3322
|
+
claim = await this.options.claim.acquire(stored.plan.target, stored.plan.runId);
|
|
3323
|
+
this.options.onLock?.("claim");
|
|
3324
|
+
const committedWhileWaiting = await readUnitEvidence(this.options.io, this.options.home, prepared.unitId);
|
|
3325
|
+
if (committedWhileWaiting !== null) return committedWhileWaiting.planDigest === prepared.planDigest ? {
|
|
3326
|
+
ok: true,
|
|
3327
|
+
evidence: committedWhileWaiting
|
|
3328
|
+
} : {
|
|
3329
|
+
ok: false,
|
|
3330
|
+
code: "unit-conflict",
|
|
3331
|
+
message: "unit already committed with a different digest"
|
|
3332
|
+
};
|
|
3333
|
+
stage = "commit";
|
|
3334
|
+
await writeUnitJournal(this.options.io, this.options.home, {
|
|
3335
|
+
...journalBase,
|
|
3336
|
+
phase: "target-pending"
|
|
3337
|
+
});
|
|
3338
|
+
const result = await this.options.backend.commit({
|
|
3339
|
+
...stored.plan,
|
|
3340
|
+
unitId: prepared.unitId
|
|
3341
|
+
}, async (phase, detail) => {
|
|
3342
|
+
if (phase === "support-pending") {
|
|
3343
|
+
await writePhase("support-pending");
|
|
3344
|
+
await this.options.onPhase?.("support-pending");
|
|
3345
|
+
return;
|
|
3346
|
+
}
|
|
3347
|
+
if (phase === "target-applied") {
|
|
3348
|
+
await this.options.onPhase?.("target-applied");
|
|
3349
|
+
return;
|
|
3350
|
+
}
|
|
3351
|
+
if (phase === "unit-written") {
|
|
3352
|
+
if (lock !== void 0 && !await lock.assertHeld()) throw new Error("unit-fence-lost");
|
|
3353
|
+
await writeUnitReceipt(this.options.io, this.options.home, {
|
|
3354
|
+
schemaVersion: 1,
|
|
3355
|
+
unitId: prepared.unitId,
|
|
3356
|
+
runId: stored.plan.runId,
|
|
3357
|
+
planDigest: stored.digest,
|
|
3358
|
+
target: stored.plan.target,
|
|
3359
|
+
archived: stored.plan.sources.filter((source) => source !== stored.plan.target),
|
|
3360
|
+
at: this.now()
|
|
3361
|
+
});
|
|
3362
|
+
await writePhase("unit-written");
|
|
3363
|
+
return;
|
|
3364
|
+
}
|
|
3365
|
+
if (phase === "source-archiving" && detail?.source !== void 0) checkpoints.push({
|
|
3366
|
+
source: detail.source,
|
|
3367
|
+
phase: "archived",
|
|
3368
|
+
hash: createHash("sha256").update(stored.plan.expectedSourceBodies.find((item) => item.name === detail.source)?.body ?? "").digest("hex"),
|
|
3369
|
+
at: this.now()
|
|
3370
|
+
});
|
|
3371
|
+
await writePhase(phase, checkpoints);
|
|
3372
|
+
}, options?.signal, async () => lock !== void 0 && await lock.assertHeld(), this.options.serial === void 0 ? void 0 : { serialized: true });
|
|
3373
|
+
if (!result.ok) {
|
|
3374
|
+
await writePhase("failed");
|
|
3375
|
+
return {
|
|
3376
|
+
ok: false,
|
|
3377
|
+
code: result.code ?? "unit-commit-failed",
|
|
3378
|
+
message: result.message
|
|
3379
|
+
};
|
|
3380
|
+
}
|
|
3381
|
+
const targetAfter = await this.options.backend.readSkill(stored.plan.target) ?? "";
|
|
3382
|
+
const evidence = {
|
|
3383
|
+
schemaVersion: 1,
|
|
3384
|
+
unitId: prepared.unitId,
|
|
3385
|
+
runId: stored.plan.runId,
|
|
3386
|
+
planDigest: stored.digest,
|
|
3387
|
+
state: "complete",
|
|
3388
|
+
target: {
|
|
3389
|
+
before: stored.plan.expectedTargetBody,
|
|
3390
|
+
after: targetAfter,
|
|
3391
|
+
supportDigest: createHash("sha256").update(JSON.stringify(stored.plan.supportFiles)).digest("hex"),
|
|
3392
|
+
created: !stored.plan.targetExists
|
|
3393
|
+
},
|
|
3394
|
+
sources: stored.plan.sources.filter((source) => source !== stored.plan.target).map((name) => ({
|
|
3395
|
+
name,
|
|
3396
|
+
before: stored.plan.expectedSourceBodies.find((item) => item.name === name)?.body ?? "",
|
|
3397
|
+
after: null,
|
|
3398
|
+
archived: (result.archived ?? []).includes(name),
|
|
3399
|
+
absorbedInto: stored.plan.target
|
|
3400
|
+
})),
|
|
3401
|
+
mutations: result.mutations ?? 0,
|
|
3402
|
+
...stored.plan.validationPlan === void 0 ? {} : { validationPlan: stored.plan.validationPlan },
|
|
3403
|
+
at: this.now()
|
|
3404
|
+
};
|
|
3405
|
+
if (!await lock.assertHeld()) throw new Error("unit-fence-lost");
|
|
3406
|
+
const evidenceSha256 = unitEvidenceDigest(evidence);
|
|
3407
|
+
await writeUnitEvidence(this.options.io, this.options.home, evidence);
|
|
3408
|
+
await writeUnitReceipt(this.options.io, this.options.home, {
|
|
3409
|
+
schemaVersion: 1,
|
|
3410
|
+
unitId: prepared.unitId,
|
|
3411
|
+
runId: stored.plan.runId,
|
|
3412
|
+
planDigest: stored.digest,
|
|
3413
|
+
target: stored.plan.target,
|
|
3414
|
+
archived: stored.plan.sources.filter((source) => source !== stored.plan.target),
|
|
3415
|
+
at: this.now(),
|
|
3416
|
+
evidenceSha256
|
|
3417
|
+
});
|
|
3418
|
+
await writePhase("committed", checkpoints);
|
|
3419
|
+
this.prepared.delete(prepared.unitId);
|
|
3420
|
+
return {
|
|
3421
|
+
ok: true,
|
|
3422
|
+
evidence
|
|
3423
|
+
};
|
|
3424
|
+
} catch (error) {
|
|
3425
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
3426
|
+
return {
|
|
3427
|
+
ok: false,
|
|
3428
|
+
code: message === "unit-fence-lost" ? "unit-fence-failed" : stage === "claim" ? "unit-claim-failed" : stage === "fence" ? "unit-fence-failed" : "unit-commit-failed",
|
|
3429
|
+
message
|
|
3430
|
+
};
|
|
3431
|
+
} finally {
|
|
3432
|
+
try {
|
|
3433
|
+
await claim?.release();
|
|
3434
|
+
} finally {
|
|
3435
|
+
if (fenceLease !== void 0) await fenceLease.release();
|
|
3436
|
+
else await lock?.release();
|
|
3437
|
+
}
|
|
3438
|
+
}
|
|
3439
|
+
}
|
|
3440
|
+
async verify(ref) {
|
|
3441
|
+
if ("unitId" in ref) return await readUnitEvidence(this.options.io, this.options.home, ref.unitId);
|
|
3442
|
+
const entries = (await readUnitIndex(this.options.io, this.options.home)).filter((entry) => entry.runId === ref.runId);
|
|
3443
|
+
const found = (await Promise.all(entries.map(async (entry) => readUnitEvidence(this.options.io, this.options.home, entry.unitId)))).filter((item) => item !== null && item.runId === ref.runId);
|
|
3444
|
+
const only = found[0];
|
|
3445
|
+
return found.length === 0 ? null : found.length === 1 ? only ?? null : found;
|
|
3446
|
+
}
|
|
3447
|
+
async withRecoveryFence(task) {
|
|
3448
|
+
if (this.options.fence === void 0) return await task();
|
|
3449
|
+
const lease = await this.options.fence.acquire();
|
|
3450
|
+
try {
|
|
3451
|
+
return await task();
|
|
3452
|
+
} finally {
|
|
3453
|
+
await lease.release();
|
|
3454
|
+
}
|
|
3455
|
+
}
|
|
3456
|
+
async finalize(unitId) {
|
|
3457
|
+
return await this.withRecoveryFence(async () => this.finalizeUnlocked(unitId));
|
|
3458
|
+
}
|
|
3459
|
+
async finalizeUnlocked(unitId) {
|
|
3460
|
+
const existing = await readUnitEvidence(this.options.io, this.options.home, unitId);
|
|
3461
|
+
if (existing !== null) return {
|
|
3462
|
+
ok: true,
|
|
3463
|
+
evidence: existing
|
|
3464
|
+
};
|
|
3465
|
+
const snapshot = await readUnitSnapshot(this.options.io, this.options.home, unitId);
|
|
3466
|
+
const journal = await readUnitJournal(this.options.io, this.options.home, unitId);
|
|
3467
|
+
const receipt = await readUnitReceipt(this.options.io, this.options.home, unitId);
|
|
3468
|
+
if (snapshot === null || journal === null || journal.phase !== "committed" && receipt === null) return {
|
|
3469
|
+
ok: false,
|
|
3470
|
+
code: "unit-rollback-blocked",
|
|
3471
|
+
message: "unit is not committed; manual recovery required"
|
|
3472
|
+
};
|
|
3473
|
+
if (receipt !== null && (receipt.unitId !== unitId || receipt.runId !== snapshot.runId || receipt.planDigest !== snapshot.planDigest || receipt.target !== snapshot.target)) return {
|
|
3474
|
+
ok: false,
|
|
3475
|
+
code: "unit-rollback-blocked",
|
|
3476
|
+
message: "unit receipt does not match snapshot; manual recovery required"
|
|
3477
|
+
};
|
|
3478
|
+
const checkpoints = journal.checkpoints ?? [];
|
|
3479
|
+
const missing = snapshot.sourceBefore.filter((source) => !checkpoints.some((item) => item.source === source.name));
|
|
3480
|
+
if (missing.length > 0) return {
|
|
3481
|
+
ok: false,
|
|
3482
|
+
code: "unit-rollback-blocked",
|
|
3483
|
+
message: "source checkpoint missing: " + missing.map((item) => item.name).join(",")
|
|
3484
|
+
};
|
|
3485
|
+
const liveTarget = await this.options.backend.readSkill(snapshot.target);
|
|
3486
|
+
if (liveTarget === null || !sameTarget(liveTarget, snapshot.targetAfter)) return {
|
|
3487
|
+
ok: false,
|
|
3488
|
+
code: "unit-rollback-blocked",
|
|
3489
|
+
message: "target live hash does not match the prepared target; manual recovery required"
|
|
3490
|
+
};
|
|
3491
|
+
const liveSource = (await Promise.all(snapshot.sourceBefore.map(async (source) => ({
|
|
3492
|
+
name: source.name,
|
|
3493
|
+
body: await this.options.backend.readSkill(source.name)
|
|
3494
|
+
})))).find((source) => source.body !== null);
|
|
3495
|
+
if (liveSource !== void 0) return {
|
|
3496
|
+
ok: false,
|
|
3497
|
+
code: "unit-rollback-blocked",
|
|
3498
|
+
message: "source is still live: " + liveSource.name
|
|
3499
|
+
};
|
|
3500
|
+
let liveSupport = await Promise.all(snapshot.supportAfter.map(async (file) => ({
|
|
3501
|
+
path: file.path,
|
|
3502
|
+
content: await this.options.backend.readSupportFile(snapshot.target, file.path)
|
|
3503
|
+
})));
|
|
3504
|
+
const supportMatches = () => liveSupport.every((file) => file.content === (snapshot.supportAfter.find((item) => item.path === file.path)?.content ?? null));
|
|
3505
|
+
if (!supportMatches()) {
|
|
3506
|
+
if (![
|
|
3507
|
+
"support-pending",
|
|
3508
|
+
"sources-archived",
|
|
3509
|
+
"unit-written"
|
|
3510
|
+
].includes(journal.phase) || this.options.backend.restoreSupportFile === void 0) return {
|
|
3511
|
+
ok: false,
|
|
3512
|
+
code: "unit-rollback-blocked",
|
|
3513
|
+
message: "support file live hash does not match the prepared support; manual recovery required"
|
|
3514
|
+
};
|
|
3515
|
+
for (const file of snapshot.supportAfter) await this.options.backend.restoreSupportFile(snapshot.target, file.path, file.content);
|
|
3516
|
+
liveSupport = await Promise.all(snapshot.supportAfter.map(async (file) => ({
|
|
3517
|
+
path: file.path,
|
|
3518
|
+
content: await this.options.backend.readSupportFile(snapshot.target, file.path)
|
|
3519
|
+
})));
|
|
3520
|
+
if (!supportMatches()) return {
|
|
3521
|
+
ok: false,
|
|
3522
|
+
code: "unit-rollback-blocked",
|
|
3523
|
+
message: "support file could not be completed from the prepared snapshot"
|
|
3524
|
+
};
|
|
3525
|
+
}
|
|
3526
|
+
const evidence = {
|
|
3527
|
+
schemaVersion: 1,
|
|
3528
|
+
unitId,
|
|
3529
|
+
runId: snapshot.runId,
|
|
3530
|
+
planDigest: snapshot.planDigest,
|
|
3531
|
+
state: "complete",
|
|
3532
|
+
target: {
|
|
3533
|
+
before: snapshot.targetBefore,
|
|
3534
|
+
after: await this.options.backend.readSkill(snapshot.target) ?? "",
|
|
3535
|
+
supportDigest: createHash("sha256").update(JSON.stringify(snapshot.supportAfter)).digest("hex"),
|
|
3536
|
+
created: !snapshot.targetExists
|
|
3537
|
+
},
|
|
3538
|
+
sources: snapshot.sourceBefore.map((source) => ({
|
|
3539
|
+
name: source.name,
|
|
3540
|
+
before: source.body,
|
|
3541
|
+
after: null,
|
|
3542
|
+
archived: checkpoints.some((item) => item.source === source.name),
|
|
3543
|
+
absorbedInto: snapshot.target
|
|
3544
|
+
})),
|
|
3545
|
+
mutations: 0,
|
|
3546
|
+
...snapshot.validationPlan === void 0 ? {} : { validationPlan: snapshot.validationPlan },
|
|
3547
|
+
at: this.now()
|
|
3548
|
+
};
|
|
3549
|
+
if (receipt?.evidenceSha256 !== void 0 && unitEvidenceDigest(evidence) !== receipt.evidenceSha256) return {
|
|
3550
|
+
ok: false,
|
|
3551
|
+
code: "unit-rollback-blocked",
|
|
3552
|
+
message: "receipt evidence hash does not match rebuilt evidence"
|
|
3553
|
+
};
|
|
3554
|
+
await writeUnitEvidence(this.options.io, this.options.home, evidence);
|
|
3555
|
+
if (journal.phase !== "committed") await writeUnitJournal(this.options.io, this.options.home, {
|
|
3556
|
+
...journal,
|
|
3557
|
+
phase: "committed",
|
|
3558
|
+
checkpoints,
|
|
3559
|
+
updatedAt: this.now()
|
|
3560
|
+
});
|
|
3561
|
+
return {
|
|
3562
|
+
ok: true,
|
|
3563
|
+
evidence
|
|
3564
|
+
};
|
|
3565
|
+
}
|
|
3566
|
+
async rollForward(unitId) {
|
|
3567
|
+
return await this.withRecoveryFence(async () => this.rollForwardUnlocked(unitId));
|
|
3568
|
+
}
|
|
3569
|
+
async rollForwardUnlocked(unitId) {
|
|
3570
|
+
const existing = await readUnitEvidence(this.options.io, this.options.home, unitId);
|
|
3571
|
+
if (existing !== null) return {
|
|
3572
|
+
ok: true,
|
|
3573
|
+
evidence: existing
|
|
3574
|
+
};
|
|
3575
|
+
const snapshot = await readUnitSnapshot(this.options.io, this.options.home, unitId);
|
|
3576
|
+
const journal = await readUnitJournal(this.options.io, this.options.home, unitId);
|
|
3577
|
+
if (snapshot === null || journal === null || ![
|
|
3578
|
+
"target-written",
|
|
3579
|
+
"support-pending",
|
|
3580
|
+
"source-archiving",
|
|
3581
|
+
"sources-archived",
|
|
3582
|
+
"unit-written"
|
|
3583
|
+
].includes(journal.phase)) return {
|
|
3584
|
+
ok: false,
|
|
3585
|
+
code: "unit-rollback-blocked",
|
|
3586
|
+
message: "unit is not roll-forwardable; manual recovery required"
|
|
3587
|
+
};
|
|
3588
|
+
const checkpoints = journal.checkpoints ?? [];
|
|
3589
|
+
const missing = snapshot.sourceBefore.filter((source) => !checkpoints.some((item) => item.source === source.name));
|
|
3590
|
+
if (missing.length > 0) return {
|
|
3591
|
+
ok: false,
|
|
3592
|
+
code: "unit-rollback-blocked",
|
|
3593
|
+
message: "source checkpoint missing: " + missing.map((item) => item.name).join(",")
|
|
3594
|
+
};
|
|
3595
|
+
const targetAfter = await this.options.backend.readSkill(snapshot.target);
|
|
3596
|
+
if (targetAfter === null || !sameTarget(targetAfter, snapshot.targetAfter)) return {
|
|
3597
|
+
ok: false,
|
|
3598
|
+
code: "unit-rollback-blocked",
|
|
3599
|
+
message: "target missing or drifted; manual recovery required"
|
|
3600
|
+
};
|
|
3601
|
+
const liveSource = (await Promise.all(snapshot.sourceBefore.map(async (source) => ({
|
|
3602
|
+
name: source.name,
|
|
3603
|
+
body: await this.options.backend.readSkill(source.name)
|
|
3604
|
+
})))).find((source) => source.body !== null);
|
|
3605
|
+
if (liveSource !== void 0) return {
|
|
3606
|
+
ok: false,
|
|
3607
|
+
code: "unit-rollback-blocked",
|
|
3608
|
+
message: "source is still live: " + liveSource.name
|
|
3609
|
+
};
|
|
3610
|
+
let liveSupport = await Promise.all(snapshot.supportAfter.map(async (file) => ({
|
|
3611
|
+
path: file.path,
|
|
3612
|
+
content: await this.options.backend.readSupportFile(snapshot.target, file.path)
|
|
3613
|
+
})));
|
|
3614
|
+
const supportMatches = () => liveSupport.every((file) => file.content === (snapshot.supportAfter.find((item) => item.path === file.path)?.content ?? null));
|
|
3615
|
+
if (!supportMatches()) {
|
|
3616
|
+
if (![
|
|
3617
|
+
"support-pending",
|
|
3618
|
+
"sources-archived",
|
|
3619
|
+
"unit-written"
|
|
3620
|
+
].includes(journal.phase) || this.options.backend.restoreSupportFile === void 0) return {
|
|
3621
|
+
ok: false,
|
|
3622
|
+
code: "unit-rollback-blocked",
|
|
3623
|
+
message: "support file live hash does not match the prepared support; manual recovery required"
|
|
3624
|
+
};
|
|
3625
|
+
for (const file of snapshot.supportAfter) await this.options.backend.restoreSupportFile(snapshot.target, file.path, file.content);
|
|
3626
|
+
liveSupport = await Promise.all(snapshot.supportAfter.map(async (file) => ({
|
|
3627
|
+
path: file.path,
|
|
3628
|
+
content: await this.options.backend.readSupportFile(snapshot.target, file.path)
|
|
3629
|
+
})));
|
|
3630
|
+
if (!supportMatches()) return {
|
|
3631
|
+
ok: false,
|
|
3632
|
+
code: "unit-rollback-blocked",
|
|
3633
|
+
message: "support file could not be completed from the prepared snapshot"
|
|
3634
|
+
};
|
|
3635
|
+
}
|
|
3636
|
+
const evidence = {
|
|
3637
|
+
schemaVersion: 1,
|
|
3638
|
+
unitId,
|
|
3639
|
+
runId: snapshot.runId,
|
|
3640
|
+
planDigest: snapshot.planDigest,
|
|
3641
|
+
state: "complete",
|
|
3642
|
+
target: {
|
|
3643
|
+
before: snapshot.targetBefore,
|
|
3644
|
+
after: targetAfter,
|
|
3645
|
+
supportDigest: createHash("sha256").update(JSON.stringify(snapshot.supportAfter)).digest("hex"),
|
|
3646
|
+
created: !snapshot.targetExists
|
|
3647
|
+
},
|
|
3648
|
+
sources: snapshot.sourceBefore.map((source) => ({
|
|
3649
|
+
name: source.name,
|
|
3650
|
+
before: source.body,
|
|
3651
|
+
after: null,
|
|
3652
|
+
archived: checkpoints.some((item) => item.source === source.name),
|
|
3653
|
+
absorbedInto: snapshot.target
|
|
3654
|
+
})),
|
|
3655
|
+
mutations: 0,
|
|
3656
|
+
...snapshot.validationPlan === void 0 ? {} : { validationPlan: snapshot.validationPlan },
|
|
3657
|
+
at: this.now()
|
|
3658
|
+
};
|
|
3659
|
+
await writeUnitEvidence(this.options.io, this.options.home, evidence);
|
|
3660
|
+
await writeUnitJournal(this.options.io, this.options.home, {
|
|
3661
|
+
...journal,
|
|
3662
|
+
phase: "committed",
|
|
3663
|
+
checkpoints,
|
|
3664
|
+
updatedAt: this.now()
|
|
3665
|
+
});
|
|
3666
|
+
return {
|
|
3667
|
+
ok: true,
|
|
3668
|
+
evidence
|
|
3669
|
+
};
|
|
3670
|
+
}
|
|
3671
|
+
async rollback(ref) {
|
|
3672
|
+
const evidence = await readUnitEvidence(this.options.io, this.options.home, ref.unitId);
|
|
3673
|
+
const receipt = await readUnitReceipt(this.options.io, this.options.home, ref.unitId);
|
|
3674
|
+
if (evidence !== null || receipt !== null) return {
|
|
3675
|
+
ok: false,
|
|
3676
|
+
code: "unit-committed",
|
|
3677
|
+
message: "committed unit cannot roll back"
|
|
3678
|
+
};
|
|
3679
|
+
const snapshot = await readUnitSnapshot(this.options.io, this.options.home, ref.unitId);
|
|
3680
|
+
if (snapshot === null) return {
|
|
3681
|
+
ok: false,
|
|
3682
|
+
code: "unit-rollback-blocked",
|
|
3683
|
+
message: "snapshot missing"
|
|
3684
|
+
};
|
|
3685
|
+
const journal = await readUnitJournal(this.options.io, this.options.home, ref.unitId);
|
|
3686
|
+
let fenceLease;
|
|
3687
|
+
let lock;
|
|
3688
|
+
let claim;
|
|
3689
|
+
try {
|
|
3690
|
+
if (this.options.fence !== void 0) {
|
|
3691
|
+
fenceLease = await this.options.fence.acquire();
|
|
3692
|
+
lock = fenceLease.lock;
|
|
3693
|
+
} else {
|
|
3694
|
+
const acquired = await acquireTreeWriteLock({
|
|
3695
|
+
io: this.options.io,
|
|
3696
|
+
home: this.options.home,
|
|
3697
|
+
waitMs: this.options.waitMs ?? 5e3
|
|
3698
|
+
});
|
|
3699
|
+
if (!acquired.ok) return {
|
|
3700
|
+
ok: false,
|
|
3701
|
+
code: "unit-fence-failed",
|
|
3702
|
+
message: acquired.message ?? acquired.reason
|
|
3703
|
+
};
|
|
3704
|
+
lock = acquired.lock;
|
|
3705
|
+
}
|
|
3706
|
+
this.options.onLock?.("tree");
|
|
3707
|
+
if (!await lock.assertHeld()) return {
|
|
3708
|
+
ok: false,
|
|
3709
|
+
code: "unit-fence-failed",
|
|
3710
|
+
message: "tree fence lost"
|
|
3711
|
+
};
|
|
3712
|
+
claim = await this.options.claim.acquire(snapshot.target, snapshot.runId);
|
|
3713
|
+
this.options.onLock?.("claim");
|
|
3714
|
+
const currentTarget = await this.options.backend.readSkill(snapshot.target);
|
|
3715
|
+
if (currentTarget !== snapshot.targetBefore && !sameTarget(currentTarget, snapshot.targetAfter)) return {
|
|
3716
|
+
ok: false,
|
|
3717
|
+
code: "unit-rollback-blocked",
|
|
3718
|
+
message: "target drifted since snapshot; manual recovery required"
|
|
3719
|
+
};
|
|
3720
|
+
for (const source of snapshot.sourceBefore) {
|
|
3721
|
+
const current = await this.options.backend.readSkill(source.name);
|
|
3722
|
+
if (current !== null && current !== source.body) return {
|
|
3723
|
+
ok: false,
|
|
3724
|
+
code: "unit-rollback-blocked",
|
|
3725
|
+
message: "source drifted since snapshot: " + source.name
|
|
3726
|
+
};
|
|
3727
|
+
}
|
|
3728
|
+
if (!await lock.assertHeld()) return {
|
|
3729
|
+
ok: false,
|
|
3730
|
+
code: "unit-fence-failed",
|
|
3731
|
+
message: "tree fence lost before target restore"
|
|
3732
|
+
};
|
|
3733
|
+
await this.options.backend.restoreTarget(snapshot.target, snapshot.targetBefore);
|
|
3734
|
+
for (const file of snapshot.supportBefore) {
|
|
3735
|
+
if (!await lock.assertHeld()) return {
|
|
3736
|
+
ok: false,
|
|
3737
|
+
code: "unit-fence-failed",
|
|
3738
|
+
message: "tree fence lost before support restore"
|
|
3739
|
+
};
|
|
3740
|
+
await this.options.backend.restoreSupportFile?.(snapshot.target, file.path, file.content);
|
|
3741
|
+
}
|
|
3742
|
+
for (const source of snapshot.sourceBefore) {
|
|
3743
|
+
if (!await lock.assertHeld()) return {
|
|
3744
|
+
ok: false,
|
|
3745
|
+
code: "unit-fence-failed",
|
|
3746
|
+
message: "tree fence lost before source restore"
|
|
3747
|
+
};
|
|
3748
|
+
await this.options.backend.restoreSource(source.name, source.body);
|
|
3749
|
+
}
|
|
3750
|
+
if (journal !== null) await writeUnitJournal(this.options.io, this.options.home, {
|
|
3751
|
+
...journal,
|
|
3752
|
+
phase: "rolled-back",
|
|
3753
|
+
updatedAt: this.now()
|
|
3754
|
+
});
|
|
3755
|
+
return {
|
|
3756
|
+
ok: true,
|
|
3757
|
+
evidence: {
|
|
3758
|
+
schemaVersion: 1,
|
|
3759
|
+
unitId: ref.unitId,
|
|
3760
|
+
runId: snapshot.runId,
|
|
3761
|
+
planDigest: snapshot.planDigest,
|
|
3762
|
+
state: "target-only",
|
|
3763
|
+
target: {
|
|
3764
|
+
before: snapshot.targetBefore,
|
|
3765
|
+
after: snapshot.targetBefore ?? "",
|
|
3766
|
+
supportDigest: "",
|
|
3767
|
+
created: false
|
|
3768
|
+
},
|
|
3769
|
+
sources: snapshot.sourceBefore.map((source) => ({
|
|
3770
|
+
name: source.name,
|
|
3771
|
+
before: source.body,
|
|
3772
|
+
after: null,
|
|
3773
|
+
archived: false,
|
|
3774
|
+
absorbedInto: snapshot.target
|
|
3775
|
+
})),
|
|
3776
|
+
mutations: 0,
|
|
3777
|
+
at: this.now()
|
|
3778
|
+
}
|
|
3779
|
+
};
|
|
3780
|
+
} finally {
|
|
3781
|
+
try {
|
|
3782
|
+
await claim?.release();
|
|
3783
|
+
} finally {
|
|
3784
|
+
if (fenceLease !== void 0) await fenceLease.release();
|
|
3785
|
+
else await lock?.release();
|
|
3786
|
+
}
|
|
3787
|
+
}
|
|
3788
|
+
}
|
|
3789
|
+
async cancel(unitId, reason) {
|
|
3790
|
+
const journal = await readUnitJournal(this.options.io, this.options.home, unitId);
|
|
3791
|
+
if (journal === null) return {
|
|
3792
|
+
ok: false,
|
|
3793
|
+
phase: "none"
|
|
3794
|
+
};
|
|
3795
|
+
if (journal.phase === "prepared" && this.prepared.has(unitId)) {
|
|
3796
|
+
this.prepared.delete(unitId);
|
|
3797
|
+
return {
|
|
3798
|
+
ok: true,
|
|
3799
|
+
phase: "prepared"
|
|
3800
|
+
};
|
|
3801
|
+
}
|
|
3802
|
+
if (journal.phase === "target-pending" || journal.phase === "target-written" || journal.phase === "source-archiving" || journal.phase === "support-pending" || journal.phase === "sources-archived") return {
|
|
3803
|
+
ok: (await this.rollback({
|
|
3804
|
+
unitId,
|
|
3805
|
+
reason
|
|
3806
|
+
})).ok,
|
|
3807
|
+
phase: journal.phase
|
|
3808
|
+
};
|
|
3809
|
+
return {
|
|
3810
|
+
ok: false,
|
|
3811
|
+
phase: journal.phase
|
|
3812
|
+
};
|
|
3813
|
+
}
|
|
3814
|
+
};
|
|
3815
|
+
//#endregion
|
|
3816
|
+
//#region lib/types/skill-library-unit-backend.js
|
|
3817
|
+
function skillLibraryUnitBackend(skills) {
|
|
3818
|
+
return {
|
|
3819
|
+
async readSkill(name) {
|
|
3820
|
+
return await skills.read(name);
|
|
3821
|
+
},
|
|
3822
|
+
async readSupportFile(name, path) {
|
|
3823
|
+
const probe = skills.readSupportFile;
|
|
3824
|
+
return typeof probe === "function" ? await probe.call(skills, name, path) : null;
|
|
3825
|
+
},
|
|
3826
|
+
async commit(plan, onPhase, signal, assertFence, options) {
|
|
3827
|
+
const result = await skills.commitMerge({
|
|
3828
|
+
...plan.runId === void 0 ? {} : { runId: plan.runId },
|
|
3829
|
+
...plan.unitId === void 0 ? {} : { unitId: plan.unitId },
|
|
3830
|
+
target: plan.target,
|
|
3831
|
+
nested: true,
|
|
3832
|
+
targetExists: plan.targetExists,
|
|
3833
|
+
sources: plan.sources,
|
|
3834
|
+
body: plan.body,
|
|
3835
|
+
supportFiles: plan.supportFiles,
|
|
3836
|
+
expectedTargetBody: plan.expectedTargetBody,
|
|
3837
|
+
expectedSourceBodies: plan.expectedSourceBodies,
|
|
3838
|
+
origin: plan.origin,
|
|
3839
|
+
summary: plan.summary,
|
|
3840
|
+
...plan.skillLimits === void 0 ? {} : { limits: plan.skillLimits },
|
|
3841
|
+
...plan.targetRevision === void 0 ? {} : { targetRevision: plan.targetRevision },
|
|
3842
|
+
...plan.sourceRevisions === void 0 ? {} : { sourceRevisions: plan.sourceRevisions },
|
|
3843
|
+
...plan.supportDigest === void 0 ? {} : { supportDigest: plan.supportDigest },
|
|
3844
|
+
...onPhase === void 0 ? {} : { onPhase },
|
|
3845
|
+
...signal === void 0 ? {} : { signal },
|
|
3846
|
+
...assertFence === void 0 ? {} : { assertFence }
|
|
3847
|
+
}, { ...options?.serialized === void 0 ? {} : { serialized: options.serialized } });
|
|
3848
|
+
return {
|
|
3849
|
+
ok: result.ok,
|
|
3850
|
+
message: result.message,
|
|
3851
|
+
...result.code === void 0 ? {} : { code: result.code },
|
|
3852
|
+
...result.archived === void 0 ? {} : { archived: result.archived },
|
|
3853
|
+
...result.mutations === void 0 ? {} : { mutations: result.mutations }
|
|
3854
|
+
};
|
|
3855
|
+
},
|
|
3856
|
+
async restoreTarget(name, body) {
|
|
3857
|
+
if (body === null) {
|
|
3858
|
+
await skills.archive(name, { reason: "unit-rollback" });
|
|
3859
|
+
return;
|
|
3860
|
+
}
|
|
3861
|
+
const result = await skills.update(name, body);
|
|
3862
|
+
if (!result.ok) throw new Error(result.message);
|
|
3863
|
+
},
|
|
3864
|
+
async restoreSupportFile(name, path, content) {
|
|
3865
|
+
const result = content === null ? await skills.removeSupportFile(name, path) : await skills.writeSupportFile(name, path, content);
|
|
3866
|
+
if (!result.ok) throw new Error(result.message);
|
|
3867
|
+
},
|
|
3868
|
+
async restoreSource(name, body) {
|
|
3869
|
+
if ((await skills.restoreFromArchive(name)).ok) return;
|
|
3870
|
+
const result = await skills.update(name, body);
|
|
3871
|
+
if (!result.ok) throw new Error(result.message);
|
|
3872
|
+
}
|
|
3873
|
+
};
|
|
3874
|
+
}
|
|
3875
|
+
//#endregion
|
|
3127
3876
|
//#region lib/types/domain/claim.js
|
|
3128
3877
|
function acquireClaim(current, now, leaseMs, token, pid, startedAt) {
|
|
3129
3878
|
if (current !== void 0 && current.expiresAt > now) return void 0;
|
|
@@ -8136,7 +8885,7 @@ async function commitStagedRun(io, home, runId, port, options) {
|
|
|
8136
8885
|
let result;
|
|
8137
8886
|
let evidenceSha256$1;
|
|
8138
8887
|
let warning;
|
|
8139
|
-
const transaction = options.unit ?? port.unit
|
|
8888
|
+
const transaction = options.unit ?? port.unit;
|
|
8140
8889
|
if (transaction === void 0) return {
|
|
8141
8890
|
ok: false,
|
|
8142
8891
|
message: "commit port missing: UnitTransaction or commitMerge required",
|
|
@@ -8267,98 +9016,6 @@ async function commitStagedRun(io, home, runId, port, options) {
|
|
|
8267
9016
|
};
|
|
8268
9017
|
}
|
|
8269
9018
|
}
|
|
8270
|
-
function legacyUnitAdapter(port, plan, options, runId) {
|
|
8271
|
-
if (port.commitMerge === void 0) return void 0;
|
|
8272
|
-
let evidence;
|
|
8273
|
-
const unitId = "legacy:" + plan.target + ":" + String(options.now());
|
|
8274
|
-
return {
|
|
8275
|
-
async prepare() {
|
|
8276
|
-
return {
|
|
8277
|
-
ok: true,
|
|
8278
|
-
prepared: {
|
|
8279
|
-
unitId,
|
|
8280
|
-
planDigest: unitId
|
|
8281
|
-
}
|
|
8282
|
-
};
|
|
8283
|
-
},
|
|
8284
|
-
async commit() {
|
|
8285
|
-
if (port.commitMerge === void 0) return {
|
|
8286
|
-
ok: false,
|
|
8287
|
-
code: "unit-commit-failed",
|
|
8288
|
-
message: "commitMerge missing"
|
|
8289
|
-
};
|
|
8290
|
-
const result = await port.commitMerge(plan);
|
|
8291
|
-
if (!result.ok) return {
|
|
8292
|
-
ok: false,
|
|
8293
|
-
code: result.code ?? "unit-commit-failed",
|
|
8294
|
-
message: result.message
|
|
8295
|
-
};
|
|
8296
|
-
const actualUnitId = result.unitId ?? unitId;
|
|
8297
|
-
evidence = {
|
|
8298
|
-
schemaVersion: 1,
|
|
8299
|
-
unitId: actualUnitId,
|
|
8300
|
-
runId,
|
|
8301
|
-
planDigest: actualUnitId,
|
|
8302
|
-
state: "complete",
|
|
8303
|
-
target: {
|
|
8304
|
-
before: plan.expectedTargetBody,
|
|
8305
|
-
after: plan.body,
|
|
8306
|
-
supportDigest: "",
|
|
8307
|
-
created: !plan.targetExists
|
|
8308
|
-
},
|
|
8309
|
-
sources: plan.expectedSourceBodies.map((source) => ({
|
|
8310
|
-
name: source.name,
|
|
8311
|
-
before: source.body,
|
|
8312
|
-
after: null,
|
|
8313
|
-
archived: (result.archived ?? []).includes(source.name),
|
|
8314
|
-
absorbedInto: plan.target
|
|
8315
|
-
})),
|
|
8316
|
-
mutations: result.mutations ?? 0,
|
|
8317
|
-
at: options.now()
|
|
8318
|
-
};
|
|
8319
|
-
return {
|
|
8320
|
-
ok: true,
|
|
8321
|
-
evidence
|
|
8322
|
-
};
|
|
8323
|
-
},
|
|
8324
|
-
async verify() {
|
|
8325
|
-
return evidence ?? null;
|
|
8326
|
-
},
|
|
8327
|
-
async rollback() {
|
|
8328
|
-
return {
|
|
8329
|
-
ok: false,
|
|
8330
|
-
code: "unit-committed",
|
|
8331
|
-
message: "legacy commit cannot roll back"
|
|
8332
|
-
};
|
|
8333
|
-
},
|
|
8334
|
-
async finalize() {
|
|
8335
|
-
return evidence === void 0 ? {
|
|
8336
|
-
ok: false,
|
|
8337
|
-
code: "unit-plan-missing",
|
|
8338
|
-
message: "legacy unit has no evidence"
|
|
8339
|
-
} : {
|
|
8340
|
-
ok: true,
|
|
8341
|
-
evidence
|
|
8342
|
-
};
|
|
8343
|
-
},
|
|
8344
|
-
async rollForward() {
|
|
8345
|
-
return evidence === void 0 ? {
|
|
8346
|
-
ok: false,
|
|
8347
|
-
code: "unit-plan-missing",
|
|
8348
|
-
message: "legacy unit has no evidence"
|
|
8349
|
-
} : {
|
|
8350
|
-
ok: true,
|
|
8351
|
-
evidence
|
|
8352
|
-
};
|
|
8353
|
-
},
|
|
8354
|
-
async cancel() {
|
|
8355
|
-
return {
|
|
8356
|
-
ok: false,
|
|
8357
|
-
phase: "none"
|
|
8358
|
-
};
|
|
8359
|
-
}
|
|
8360
|
-
};
|
|
8361
|
-
}
|
|
8362
9019
|
//#endregion
|
|
8363
9020
|
//#region lib/types/domain/validation-plan.js
|
|
8364
9021
|
/** Immutable post-stage validation plan: Q09/support/threat/revision facts. */
|
|
@@ -9367,12 +10024,13 @@ async function removeRuntimeRow(io, home, id, event, unitId, fence) {
|
|
|
9367
10024
|
function unitFor(deps) {
|
|
9368
10025
|
if (deps.unit !== void 0) return deps.unit;
|
|
9369
10026
|
if (!(deps.skills instanceof SkillLibrary)) return void 0;
|
|
9370
|
-
return new
|
|
10027
|
+
return new MergeUnitTransactions({
|
|
9371
10028
|
io: deps.io,
|
|
9372
10029
|
home: deps.home,
|
|
9373
10030
|
backend: skillLibraryUnitBackend(deps.skills),
|
|
9374
10031
|
claim: targetClaimUnitPort(deps.io, deps.home, deps.now),
|
|
9375
|
-
now: deps.now
|
|
10032
|
+
now: deps.now,
|
|
10033
|
+
serial: async (task) => await deps.skills.runSerial(task)
|
|
9376
10034
|
});
|
|
9377
10035
|
}
|
|
9378
10036
|
function createMergeRuntime(deps) {
|
|
@@ -10778,13 +11436,14 @@ function apply(ctx, rawConfig = {}) {
|
|
|
10778
11436
|
ctx.logger.warn("merge state is not ready: " + value.code + ": " + value.message);
|
|
10779
11437
|
return;
|
|
10780
11438
|
}
|
|
10781
|
-
const unit = new
|
|
11439
|
+
const unit = new MergeUnitTransactions({
|
|
10782
11440
|
io,
|
|
10783
11441
|
home,
|
|
10784
11442
|
backend: skillLibraryUnitBackend(skills),
|
|
10785
11443
|
claim: targetClaimUnitPort(io, home, () => Date.now()),
|
|
10786
11444
|
now: () => Date.now(),
|
|
10787
11445
|
isActive: async (runId) => await runs.hasOwner(runId),
|
|
11446
|
+
serial: async (task) => await skills.runSerial(task),
|
|
10788
11447
|
...mutationAuthority?.homeFence === void 0 ? {} : { fence: mutationAuthority.homeFence }
|
|
10789
11448
|
});
|
|
10790
11449
|
const cards = async () => await readSkillCards(skills);
|