@lmzhen/dsh-evolution-merge 0.41.0 → 0.43.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 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 "@deepseek-ai/dsh-llm";
5
- import { DEFAULT_SKILL_LIMITS, LeaseStore, SkillLibrary, UnitTransactions, authorityFrom, contentHash, createSubagentChannel, errorText, evolutionHome, evolutionIoAdapter, executeUnitUndo, isPresent, isProcessAlive, newRunRegistry, newSkillLibrary, parseFrontmatter, planUnitUndoAcross, policyStageLimits, probeAbsent, probeList, probePresent, probeText, probeUnknown, readTreeLock, readUnitEvidence, readUnitJournal, registerOwnerSettings, reportsSweepLockTarget, revisionAnchorOf, revisionFingerprint, revisionSupportDigest, runsFile, scanContentThreats, skillLibraryUnitBackend, supportRefs, sweepReports, transactIo, validateFrontmatter, validateSupportPath } from "@lmzhen/dsh-evolution-core";
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$2(report) {
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$2(report));
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 ?? legacyUnitAdapter(port, assembled.plan, options, runId);
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 UnitTransactions({
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 UnitTransactions({
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);