@lmzhen/dsh-evolution-merge 0.47.0 → 0.48.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.
Files changed (73) hide show
  1. package/lib/index.js +175 -758
  2. package/lib/types/adapters/engine-screen.d.ts +0 -3
  3. package/lib/types/authoring-flow.d.ts +5 -0
  4. package/lib/types/authoring-flow.js +17 -10
  5. package/lib/types/commit-execute.js +1 -22
  6. package/lib/types/contracts.d.ts +0 -1
  7. package/lib/types/contracts.js +0 -1
  8. package/lib/types/doctor.d.ts +0 -1
  9. package/lib/types/doctor.js +2 -31
  10. package/lib/types/domain/consistency.d.ts +0 -1
  11. package/lib/types/domain/consistency.js +2 -11
  12. package/lib/types/domain/merge-limits.d.ts +0 -10
  13. package/lib/types/domain/merge-limits.js +0 -3
  14. package/lib/types/domain/merge-snapshot.js +4 -6
  15. package/lib/types/domain/pair-ledger.d.ts +0 -1
  16. package/lib/types/domain/row-v3.d.ts +0 -45
  17. package/lib/types/domain/row-v3.js +0 -68
  18. package/lib/types/domain/row.d.ts +2 -84
  19. package/lib/types/domain/row.js +1 -110
  20. package/lib/types/domain/run-coordinator.d.ts +0 -24
  21. package/lib/types/domain/run-coordinator.js +0 -1
  22. package/lib/types/domain/stage-aggregate.d.ts +0 -42
  23. package/lib/types/domain/stage-aggregate.js +1 -116
  24. package/lib/types/domain/state.d.ts +1 -53
  25. package/lib/types/domain/state.js +0 -55
  26. package/lib/types/domain/undo-journal.d.ts +4 -4
  27. package/lib/types/domain/undo-journal.js +5 -7
  28. package/lib/types/index.d.ts +0 -1
  29. package/lib/types/index.js +0 -1
  30. package/lib/types/new-child-authoring.js +4 -10
  31. package/lib/types/prompts.d.ts +0 -1
  32. package/lib/types/prompts.js +0 -1
  33. package/lib/types/read-commands.d.ts +0 -1
  34. package/lib/types/read-commands.js +0 -4
  35. package/lib/types/recovery-projection.js +0 -3
  36. package/lib/types/round2-outcome.js +15 -37
  37. package/lib/types/scan-run.js +0 -3
  38. package/lib/types/skipped.d.ts +1 -2
  39. package/lib/types/skipped.js +0 -8
  40. package/lib/types/staging.d.ts +0 -2
  41. package/lib/types/staging.js +0 -9
  42. package/lib/types/state.d.ts +0 -2
  43. package/lib/types/state.js +0 -13
  44. package/lib/types/surface/merge-view.d.ts +1 -2
  45. package/lib/types/surface/merge-view.js +0 -1
  46. package/lib/types/undo-operation.js +0 -9
  47. package/lib/types/unit-protocol.d.ts +6 -0
  48. package/lib/types/unit-protocol.js +72 -102
  49. package/package.json +4 -4
  50. package/lib/types/commit-recovery.d.ts +0 -14
  51. package/lib/types/commit-recovery.js +0 -96
  52. package/lib/types/coordinator.d.ts +0 -77
  53. package/lib/types/coordinator.js +0 -137
  54. package/lib/types/domain/commit-journal-store.d.ts +0 -6
  55. package/lib/types/domain/commit-journal-store.js +0 -26
  56. package/lib/types/domain/commit-journal.d.ts +0 -16
  57. package/lib/types/domain/commit-journal.js +0 -26
  58. package/lib/types/domain/indexes.d.ts +0 -21
  59. package/lib/types/domain/indexes.js +0 -15
  60. package/lib/types/domain/merge-report.d.ts +0 -41
  61. package/lib/types/domain/merge-report.js +0 -2
  62. package/lib/types/domain/pair-index-store.d.ts +0 -6
  63. package/lib/types/domain/pair-index-store.js +0 -25
  64. package/lib/types/domain/pair-index.d.ts +0 -9
  65. package/lib/types/domain/pair-index.js +0 -28
  66. package/lib/types/domain/resolve-pair.d.ts +0 -4
  67. package/lib/types/domain/resolve-pair.js +0 -4
  68. package/lib/types/domain/row-tx.d.ts +0 -27
  69. package/lib/types/domain/row-tx.js +0 -31
  70. package/lib/types/domain/run-record.d.ts +0 -45
  71. package/lib/types/domain/run-record.js +0 -24
  72. package/lib/types/domain/state-v3.d.ts +0 -4
  73. package/lib/types/domain/state-v3.js +0 -4
package/lib/index.js CHANGED
@@ -148,12 +148,6 @@ const MERGE_PAIR_STATES = [
148
148
  "fuse",
149
149
  "skipped"
150
150
  ];
151
- const MERGE_QUEUES = [
152
- "verify",
153
- "pending",
154
- "fuse",
155
- "skipped"
156
- ];
157
151
  /** User-facing queue of one data state. verify/fuse share retry, not two tabs. */
158
152
  function queueOf(state) {
159
153
  switch (state) {
@@ -1346,17 +1340,6 @@ async function loadMergeState(io, home) {
1346
1340
  ...parsed.note === void 0 ? {} : { note: parsed.note }
1347
1341
  };
1348
1342
  }
1349
- /** Schema probe for doctor; the only other reader of mergeStatePath. */
1350
- async function loadStateSchema(io, home) {
1351
- try {
1352
- const raw = await io.readText(mergeStatePath(home));
1353
- if (raw === null) return null;
1354
- const parsed = JSON.parse(raw);
1355
- return typeof parsed.schemaVersion === "number" ? parsed.schemaVersion : null;
1356
- } catch {
1357
- return null;
1358
- }
1359
- }
1360
1343
  async function rowsOf(io, home, queue) {
1361
1344
  const loaded = await loadMergeState(io, home);
1362
1345
  return queue === void 0 ? loaded.state.rows : loaded.state.rows.filter((row) => row.queue === queue);
@@ -1619,7 +1602,7 @@ function v3State(body) {
1619
1602
  rows
1620
1603
  };
1621
1604
  }
1622
- async function readJournal$1(io, home) {
1605
+ async function readJournal(io, home) {
1623
1606
  const raw = await readTextOrNull(io, migrationJournalPath(home));
1624
1607
  if (raw === null) return null;
1625
1608
  try {
@@ -1696,7 +1679,7 @@ async function readStateReadiness(io, home) {
1696
1679
  if (version === 3) {
1697
1680
  const state = v3State(body);
1698
1681
  if (state === null) return blocked("merge-state-needs-repair", "v3 state has an invalid row");
1699
- const journal = await readJournal$1(io, home);
1682
+ const journal = await readJournal(io, home);
1700
1683
  const report = await readReport(io, home);
1701
1684
  const marker = readMarker(body);
1702
1685
  if (body.migration !== void 0 && marker === null) return blocked("merge-state-needs-repair", "migration marker is invalid");
@@ -1755,7 +1738,7 @@ async function ensureMergeStateReady(io, home, fence) {
1755
1738
  outcome = blocked("merge-state-needs-repair", "v3 state has an invalid row");
1756
1739
  return current;
1757
1740
  }
1758
- const journal = await readJournal$1(io, home);
1741
+ const journal = await readJournal(io, home);
1759
1742
  if (journal !== null) {
1760
1743
  outcome = await completeJournal(io, home, journal, current);
1761
1744
  return current;
@@ -2000,15 +1983,6 @@ async function readStaging(io, home, runId) {
2000
1983
  return probeUnknown("staging manifest is not JSON: " + (error instanceof Error ? error.message : String(error)));
2001
1984
  }
2002
1985
  }
2003
- async function ensureStaging(io, home, input) {
2004
- const existing = await readStaging(io, home, input.runId);
2005
- if (existing.kind === "present") return {
2006
- ok: true,
2007
- path: stagingRoot(home, input.runId),
2008
- manifest: existing.value.manifest
2009
- };
2010
- return createStaging(io, home, input);
2011
- }
2012
1986
  async function setStagingTarget(io, home, stageKey, target, mode) {
2013
1987
  const view = await readStaging(io, home, stageKey);
2014
1988
  if (view.kind !== "present") return false;
@@ -2016,9 +1990,6 @@ async function setStagingTarget(io, home, stageKey, target, mode) {
2016
1990
  await io.writeText(join(stagingRoot(home, stageKey), "manifest.json"), JSON.stringify(manifest, null, 2) + "\n");
2017
1991
  return true;
2018
1992
  }
2019
- async function dropStaging(io, home, stageKey) {
2020
- await io.remove(stagingRoot(home, stageKey));
2021
- }
2022
1993
  /** Remove expired run-level staging trees; `keep` names runs a live row still points at. */
2023
1994
  async function cleanupExpiredStaging(io, home, now, keep = /* @__PURE__ */ new Set()) {
2024
1995
  let runs;
@@ -2175,6 +2146,7 @@ async function adoptLegacyProduction(io, home) {
2175
2146
  wrote
2176
2147
  };
2177
2148
  }
2149
+ /** Production stage-tree facade. These wrappers keep the low-level tree module private to StageAggregate. */
2178
2150
  async function createStageTree(io, home, input) {
2179
2151
  return await createStaging(io, home, input);
2180
2152
  }
@@ -2447,61 +2419,6 @@ async function readPairLedgerV4(io, home) {
2447
2419
  };
2448
2420
  }
2449
2421
  //#endregion
2450
- //#region lib/types/domain/pair-index.js
2451
- function parsePairIndex(raw) {
2452
- const index = /* @__PURE__ */ new Map();
2453
- if (raw === null) return index;
2454
- let parsed;
2455
- try {
2456
- parsed = JSON.parse(raw);
2457
- } catch {
2458
- return index;
2459
- }
2460
- const body = parsed;
2461
- if (!Array.isArray(body.entries)) return index;
2462
- for (const value of body.entries) {
2463
- const entry = value;
2464
- if (typeof entry.pairKey !== "string" || typeof entry.runId !== "string" || typeof entry.result !== "string") continue;
2465
- if (![
2466
- "merged",
2467
- "kept",
2468
- "failed",
2469
- "cancelled",
2470
- "undone"
2471
- ].includes(entry.result)) continue;
2472
- index.set(entry.pairKey, entry);
2473
- }
2474
- return index;
2475
- }
2476
- function serializePairIndex(index) {
2477
- return JSON.stringify({
2478
- schemaVersion: 1,
2479
- entries: [...index.values()]
2480
- }, null, 2);
2481
- }
2482
- //#endregion
2483
- //#region lib/types/domain/pair-index-store.js
2484
- /** Durable pairKey index at <home>/merge/pair-index.json. */
2485
- function pairIndexPath(home) {
2486
- return join(home, "merge", "pair-index.json");
2487
- }
2488
- async function loadPairIndex(io, home) {
2489
- let raw = null;
2490
- try {
2491
- raw = await io.readText(pairIndexPath(home));
2492
- } catch {
2493
- return /* @__PURE__ */ new Map();
2494
- }
2495
- return parsePairIndex(raw);
2496
- }
2497
- async function recordPair(io, home, entry) {
2498
- await transactIo(io, pairIndexPath(home), (current) => {
2499
- const index = parsePairIndex(current);
2500
- index.set(entry.pairKey, entry);
2501
- return serializePairIndex(index);
2502
- });
2503
- }
2504
- //#endregion
2505
2422
  //#region lib/types/domain/run-coordinator.js
2506
2423
  /** 1b-1 additive types for the merge run lifecycle; no writer is active in this phase. */
2507
2424
  function runMetaPath(home, runId) {
@@ -2908,19 +2825,18 @@ async function readMergeSnapshot(options) {
2908
2825
  });
2909
2826
  bodyRefs.push(...bodies);
2910
2827
  }
2911
- const pairIndex = await loadPairIndex(options.io, options.home);
2912
2828
  const runs = [];
2913
2829
  for (const record of options.runs) {
2914
2830
  const meta = await readRunMeta(options.io, options.home, record.id);
2915
- const pairs = [...pairIndex.values()].filter((item) => item.runId === record.id).map((item) => ({
2916
- pairKey: item.pairKey,
2917
- runId: item.runId,
2918
- result: item.result,
2919
- ...item.target === void 0 ? {} : { target: item.target }
2920
- }));
2921
2831
  const reportPath = mergeReportPath(options.home, record.id);
2922
2832
  const probe = await readMergeReport(options.io, options.home, record.id);
2923
2833
  const report = probe.kind === "present" ? probe.value : null;
2834
+ const pairs = (report?.pairs ?? []).map((pair) => ({
2835
+ pairKey: pair.pairKey,
2836
+ runId: record.id,
2837
+ result: pair.outcome,
2838
+ ...pair.target === void 0 ? {} : { target: pair.target.requested }
2839
+ }));
2924
2840
  runs.push({
2925
2841
  record,
2926
2842
  meta,
@@ -2992,8 +2908,9 @@ async function readMergeSnapshot(options) {
2992
2908
  })).digest("hex");
2993
2909
  const notes = [...options.notes ?? []];
2994
2910
  if (historyDegraded) notes.push("history archive port unavailable; receipts only");
2995
- const ledgerConflicts = [...pairIndex.values()].filter((entry) => (entry.result === "merged" || entry.result === "undone") && rows.some((item) => item.row.pairKey === entry.pairKey && (item.row.state === "verify" || item.row.state === "pending" || item.row.state === "approved" || item.row.state === "fuse")));
2996
- if (ledgerConflicts.length > 0) notes.push("ledger-conflict: pair-index says completed but the ledger still has it active (" + String(ledgerConflicts.length) + ")");
2911
+ const activePairKeys = new Set(rows.filter((item) => item.row.state === "verify" || item.row.state === "pending" || item.row.state === "approved" || item.row.state === "fuse").map((item) => item.row.pairKey));
2912
+ const ledgerConflicts = runs.flatMap((run) => run.pairs).filter((entry) => (entry.result === "merged" || entry.result === "undone") && activePairKeys.has(entry.pairKey));
2913
+ if (ledgerConflicts.length > 0) notes.push("ledger-conflict: a run says completed but the ledger still has it active (" + String(ledgerConflicts.length) + ")");
2997
2914
  return {
2998
2915
  wireVersion: 1,
2999
2916
  schemaVersion: 4,
@@ -3205,34 +3122,10 @@ var MergeUnitTransactions = class {
3205
3122
  await this.options.onPhase?.(phase);
3206
3123
  };
3207
3124
  try {
3208
- if (this.options.fence !== void 0) {
3209
- fenceLease = await this.options.fence.acquire();
3210
- lock = fenceLease.lock;
3211
- this.options.onLock?.("tree");
3212
- } else {
3213
- const fallbackLease = await new HomeAuthority({
3214
- home: this.options.home,
3215
- waitMs: this.options.waitMs ?? 5e3
3216
- }).lease("tree").catch(() => void 0);
3217
- if (fallbackLease === void 0) return {
3218
- ok: false,
3219
- code: "unit-fence-failed",
3220
- message: "tree lock unavailable"
3221
- };
3222
- fenceLease = {
3223
- lock: leaseAsTreeLock(fallbackLease),
3224
- release: async () => {
3225
- await fallbackLease.release();
3226
- }
3227
- };
3228
- lock = fenceLease.lock;
3229
- this.options.onLock?.("tree");
3230
- }
3231
- if (!await lock.assertHeld()) return {
3232
- ok: false,
3233
- code: "unit-fence-failed",
3234
- message: "tree fence lost"
3235
- };
3125
+ const acquired = await this.acquireTreeFence();
3126
+ fenceLease = acquired.fenceLease;
3127
+ lock = acquired.lock;
3128
+ this.options.onLock?.("tree");
3236
3129
  stage = "claim";
3237
3130
  claim = await this.options.claim.acquire(stored.plan.target, stored.plan.runId);
3238
3131
  this.options.onLock?.("claim");
@@ -3368,6 +3261,82 @@ var MergeUnitTransactions = class {
3368
3261
  await lease.release();
3369
3262
  }
3370
3263
  }
3264
+ /** Acquire the one tree fence (or a HomeAuthority fallback) and prove it is held. */
3265
+ async acquireTreeFence() {
3266
+ let fenceLease;
3267
+ if (this.options.fence !== void 0) fenceLease = await this.options.fence.acquire();
3268
+ else {
3269
+ const fallbackLease = await new HomeAuthority({
3270
+ home: this.options.home,
3271
+ waitMs: this.options.waitMs ?? 5e3
3272
+ }).lease("tree").catch(() => void 0);
3273
+ if (fallbackLease === void 0) throw new Error("tree lock unavailable");
3274
+ fenceLease = {
3275
+ lock: leaseAsTreeLock(fallbackLease),
3276
+ release: async () => {
3277
+ await fallbackLease.release();
3278
+ }
3279
+ };
3280
+ }
3281
+ if (!await fenceLease.lock.assertHeld()) throw new Error("tree fence lost");
3282
+ return {
3283
+ fenceLease,
3284
+ lock: fenceLease.lock
3285
+ };
3286
+ }
3287
+ /** Finish the idempotent support writes from the prepared snapshot, or name the drift. */
3288
+ async completeSupport(snapshot, journal) {
3289
+ let liveSupport = await Promise.all(snapshot.supportAfter.map(async (file) => ({
3290
+ path: file.path,
3291
+ content: await this.options.backend.readSupportFile(snapshot.target, file.path)
3292
+ })));
3293
+ const supportMatches = () => liveSupport.every((file) => file.content === (snapshot.supportAfter.find((item) => item.path === file.path)?.content ?? null));
3294
+ if (supportMatches()) return { ok: true };
3295
+ if (![
3296
+ "support-pending",
3297
+ "sources-archived",
3298
+ "unit-written"
3299
+ ].includes(journal.phase) || this.options.backend.restoreSupportFile === void 0) return {
3300
+ ok: false,
3301
+ message: "support file live hash does not match the prepared support; manual recovery required"
3302
+ };
3303
+ for (const file of snapshot.supportAfter) await this.options.backend.restoreSupportFile(snapshot.target, file.path, file.content);
3304
+ liveSupport = await Promise.all(snapshot.supportAfter.map(async (file) => ({
3305
+ path: file.path,
3306
+ content: await this.options.backend.readSupportFile(snapshot.target, file.path)
3307
+ })));
3308
+ return supportMatches() ? { ok: true } : {
3309
+ ok: false,
3310
+ message: "support file could not be completed from the prepared snapshot"
3311
+ };
3312
+ }
3313
+ /** The complete evidence record both settlement paths write. */
3314
+ async evidenceFor(unitId, snapshot, journal, targetAfter) {
3315
+ const checkpoints = journal.checkpoints ?? [];
3316
+ return {
3317
+ schemaVersion: 1,
3318
+ unitId,
3319
+ runId: snapshot.runId,
3320
+ planDigest: snapshot.planDigest,
3321
+ state: "complete",
3322
+ target: {
3323
+ before: snapshot.targetBefore,
3324
+ after: targetAfter,
3325
+ supportDigest: createHash("sha256").update(JSON.stringify(snapshot.supportAfter)).digest("hex"),
3326
+ created: !snapshot.targetExists
3327
+ },
3328
+ sources: snapshot.sourceBefore.map((source) => ({
3329
+ name: source.name,
3330
+ before: source.body,
3331
+ after: null,
3332
+ archived: checkpoints.some((item) => item.source === source.name),
3333
+ absorbedInto: snapshot.target
3334
+ })),
3335
+ mutations: 0,
3336
+ ...snapshot.validationPlan === void 0 ? {} : { validationPlan: snapshot.validationPlan },
3337
+ at: this.now()
3338
+ };
3339
+ }
3371
3340
  async finalize(unitId) {
3372
3341
  return await this.withRecoveryFence(async () => this.finalizeUnlocked(unitId));
3373
3342
  }
@@ -3412,55 +3381,13 @@ var MergeUnitTransactions = class {
3412
3381
  code: "unit-rollback-blocked",
3413
3382
  message: "source is still live: " + liveSource.name
3414
3383
  };
3415
- let liveSupport = await Promise.all(snapshot.supportAfter.map(async (file) => ({
3416
- path: file.path,
3417
- content: await this.options.backend.readSupportFile(snapshot.target, file.path)
3418
- })));
3419
- const supportMatches = () => liveSupport.every((file) => file.content === (snapshot.supportAfter.find((item) => item.path === file.path)?.content ?? null));
3420
- if (!supportMatches()) {
3421
- if (![
3422
- "support-pending",
3423
- "sources-archived",
3424
- "unit-written"
3425
- ].includes(journal.phase) || this.options.backend.restoreSupportFile === void 0) return {
3426
- ok: false,
3427
- code: "unit-rollback-blocked",
3428
- message: "support file live hash does not match the prepared support; manual recovery required"
3429
- };
3430
- for (const file of snapshot.supportAfter) await this.options.backend.restoreSupportFile(snapshot.target, file.path, file.content);
3431
- liveSupport = await Promise.all(snapshot.supportAfter.map(async (file) => ({
3432
- path: file.path,
3433
- content: await this.options.backend.readSupportFile(snapshot.target, file.path)
3434
- })));
3435
- if (!supportMatches()) return {
3436
- ok: false,
3437
- code: "unit-rollback-blocked",
3438
- message: "support file could not be completed from the prepared snapshot"
3439
- };
3440
- }
3441
- const evidence = {
3442
- schemaVersion: 1,
3443
- unitId,
3444
- runId: snapshot.runId,
3445
- planDigest: snapshot.planDigest,
3446
- state: "complete",
3447
- target: {
3448
- before: snapshot.targetBefore,
3449
- after: await this.options.backend.readSkill(snapshot.target) ?? "",
3450
- supportDigest: createHash("sha256").update(JSON.stringify(snapshot.supportAfter)).digest("hex"),
3451
- created: !snapshot.targetExists
3452
- },
3453
- sources: snapshot.sourceBefore.map((source) => ({
3454
- name: source.name,
3455
- before: source.body,
3456
- after: null,
3457
- archived: checkpoints.some((item) => item.source === source.name),
3458
- absorbedInto: snapshot.target
3459
- })),
3460
- mutations: 0,
3461
- ...snapshot.validationPlan === void 0 ? {} : { validationPlan: snapshot.validationPlan },
3462
- at: this.now()
3384
+ const support = await this.completeSupport(snapshot, journal);
3385
+ if (!support.ok) return {
3386
+ ok: false,
3387
+ code: "unit-rollback-blocked",
3388
+ message: support.message
3463
3389
  };
3390
+ const evidence = await this.evidenceFor(unitId, snapshot, journal, await this.options.backend.readSkill(snapshot.target) ?? "");
3464
3391
  if (receipt?.evidenceSha256 !== void 0 && unitEvidenceDigest(evidence) !== receipt.evidenceSha256) return {
3465
3392
  ok: false,
3466
3393
  code: "unit-rollback-blocked",
@@ -3522,55 +3449,13 @@ var MergeUnitTransactions = class {
3522
3449
  code: "unit-rollback-blocked",
3523
3450
  message: "source is still live: " + liveSource.name
3524
3451
  };
3525
- let liveSupport = await Promise.all(snapshot.supportAfter.map(async (file) => ({
3526
- path: file.path,
3527
- content: await this.options.backend.readSupportFile(snapshot.target, file.path)
3528
- })));
3529
- const supportMatches = () => liveSupport.every((file) => file.content === (snapshot.supportAfter.find((item) => item.path === file.path)?.content ?? null));
3530
- if (!supportMatches()) {
3531
- if (![
3532
- "support-pending",
3533
- "sources-archived",
3534
- "unit-written"
3535
- ].includes(journal.phase) || this.options.backend.restoreSupportFile === void 0) return {
3536
- ok: false,
3537
- code: "unit-rollback-blocked",
3538
- message: "support file live hash does not match the prepared support; manual recovery required"
3539
- };
3540
- for (const file of snapshot.supportAfter) await this.options.backend.restoreSupportFile(snapshot.target, file.path, file.content);
3541
- liveSupport = await Promise.all(snapshot.supportAfter.map(async (file) => ({
3542
- path: file.path,
3543
- content: await this.options.backend.readSupportFile(snapshot.target, file.path)
3544
- })));
3545
- if (!supportMatches()) return {
3546
- ok: false,
3547
- code: "unit-rollback-blocked",
3548
- message: "support file could not be completed from the prepared snapshot"
3549
- };
3550
- }
3551
- const evidence = {
3552
- schemaVersion: 1,
3553
- unitId,
3554
- runId: snapshot.runId,
3555
- planDigest: snapshot.planDigest,
3556
- state: "complete",
3557
- target: {
3558
- before: snapshot.targetBefore,
3559
- after: targetAfter,
3560
- supportDigest: createHash("sha256").update(JSON.stringify(snapshot.supportAfter)).digest("hex"),
3561
- created: !snapshot.targetExists
3562
- },
3563
- sources: snapshot.sourceBefore.map((source) => ({
3564
- name: source.name,
3565
- before: source.body,
3566
- after: null,
3567
- archived: checkpoints.some((item) => item.source === source.name),
3568
- absorbedInto: snapshot.target
3569
- })),
3570
- mutations: 0,
3571
- ...snapshot.validationPlan === void 0 ? {} : { validationPlan: snapshot.validationPlan },
3572
- at: this.now()
3452
+ const support = await this.completeSupport(snapshot, journal);
3453
+ if (!support.ok) return {
3454
+ ok: false,
3455
+ code: "unit-rollback-blocked",
3456
+ message: support.message
3573
3457
  };
3458
+ const evidence = await this.evidenceFor(unitId, snapshot, journal, targetAfter);
3574
3459
  await writeUnitEvidence(this.options.io, this.options.home, evidence);
3575
3460
  await writeUnitJournal(this.options.io, this.options.home, {
3576
3461
  ...journal,
@@ -3602,33 +3487,10 @@ var MergeUnitTransactions = class {
3602
3487
  let lock;
3603
3488
  let claim;
3604
3489
  try {
3605
- if (this.options.fence !== void 0) {
3606
- fenceLease = await this.options.fence.acquire();
3607
- lock = fenceLease.lock;
3608
- } else {
3609
- const fallbackLease = await new HomeAuthority({
3610
- home: this.options.home,
3611
- waitMs: this.options.waitMs ?? 5e3
3612
- }).lease("tree").catch(() => void 0);
3613
- if (fallbackLease === void 0) return {
3614
- ok: false,
3615
- code: "unit-fence-failed",
3616
- message: "tree lock unavailable"
3617
- };
3618
- fenceLease = {
3619
- lock: leaseAsTreeLock(fallbackLease),
3620
- release: async () => {
3621
- await fallbackLease.release();
3622
- }
3623
- };
3624
- lock = fenceLease.lock;
3625
- }
3490
+ const acquired = await this.acquireTreeFence();
3491
+ fenceLease = acquired.fenceLease;
3492
+ lock = acquired.lock;
3626
3493
  this.options.onLock?.("tree");
3627
- if (!await lock.assertHeld()) return {
3628
- ok: false,
3629
- code: "unit-fence-failed",
3630
- message: "tree fence lost"
3631
- };
3632
3494
  claim = await this.options.claim.acquire(snapshot.target, snapshot.runId);
3633
3495
  this.options.onLock?.("claim");
3634
3496
  const currentTarget = await this.options.backend.readSkill(snapshot.target);
@@ -4668,78 +4530,6 @@ const ALLOWED = [
4668
4530
  evidence: "none",
4669
4531
  activeRuns: 0
4670
4532
  },
4671
- {
4672
- row: "fuse",
4673
- run: "none",
4674
- staging: "none",
4675
- journal: "none",
4676
- evidence: "none",
4677
- activeRuns: 0,
4678
- recovery: true
4679
- },
4680
- {
4681
- row: "fuse",
4682
- run: "failed",
4683
- staging: "none",
4684
- journal: "none",
4685
- evidence: "none",
4686
- activeRuns: 0,
4687
- recovery: true
4688
- },
4689
- {
4690
- row: "fuse",
4691
- run: "orphan",
4692
- staging: "none",
4693
- journal: "none",
4694
- evidence: "none",
4695
- activeRuns: 0,
4696
- recovery: true
4697
- },
4698
- {
4699
- row: "fuse",
4700
- run: "running",
4701
- staging: "none",
4702
- journal: "none",
4703
- evidence: "none",
4704
- activeRuns: 1,
4705
- recovery: true
4706
- },
4707
- {
4708
- row: "fuse",
4709
- run: "none",
4710
- staging: "missing",
4711
- journal: "none",
4712
- evidence: "none",
4713
- activeRuns: 0,
4714
- recovery: true
4715
- },
4716
- {
4717
- row: "fuse",
4718
- run: "failed",
4719
- staging: "missing",
4720
- journal: "none",
4721
- evidence: "none",
4722
- activeRuns: 0,
4723
- recovery: true
4724
- },
4725
- {
4726
- row: "fuse",
4727
- run: "orphan",
4728
- staging: "missing",
4729
- journal: "none",
4730
- evidence: "none",
4731
- activeRuns: 0,
4732
- recovery: true
4733
- },
4734
- {
4735
- row: "fuse",
4736
- run: "running",
4737
- staging: "missing",
4738
- journal: "none",
4739
- evidence: "none",
4740
- activeRuns: 1,
4741
- recovery: true
4742
- },
4743
4533
  {
4744
4534
  row: "fuse",
4745
4535
  run: "running",
@@ -4774,7 +4564,7 @@ const ALLOWED = [
4774
4564
  }
4775
4565
  ];
4776
4566
  function same(a, b) {
4777
- return a.row === b.row && a.run === b.run && a.staging === b.staging && a.journal === b.journal && a.evidence === b.evidence && a.activeRuns === b.activeRuns && (b.recovery ?? false) === (a.recovery ?? false);
4567
+ return a.row === b.row && a.run === b.run && a.staging === b.staging && a.journal === b.journal && a.evidence === b.evidence && a.activeRuns === b.activeRuns;
4778
4568
  }
4779
4569
  function validateArtifacts(state) {
4780
4570
  if (state.activeRuns > 1) return {
@@ -4794,7 +4584,7 @@ function validateArtifacts(state) {
4794
4584
  ok: false,
4795
4585
  reason: "skipped-running"
4796
4586
  };
4797
- if (state.row === "fuse" && state.staging === "missing" && state.recovery !== true) return {
4587
+ if (state.row === "fuse" && state.staging === "missing") return {
4798
4588
  ok: false,
4799
4589
  reason: "fuse-lost-staging"
4800
4590
  };
@@ -4812,35 +4602,12 @@ function validateArtifacts(state) {
4812
4602
  };
4813
4603
  }
4814
4604
  //#endregion
4815
- //#region lib/types/domain/commit-journal-store.js
4816
- /** Durable commit journal: <home>/merge/journals/<run>.json. */
4817
- function journalPath(home, runId) {
4818
- return join(home, "merge", "journals", runId.replace(/[^A-Za-z0-9_-]+/g, "_") + ".json");
4819
- }
4820
- async function writeJournal(io, home, journal) {
4821
- const path = journalPath(home, journal.runId);
4822
- const content = JSON.stringify(journal, null, 2);
4823
- if (io.transact !== void 0) await io.transact(path, () => content);
4824
- else await io.writeText(path, content);
4825
- return path;
4826
- }
4827
- async function readJournal(io, home, runId) {
4828
- const raw = await io.readText(journalPath(home, runId));
4829
- if (raw === null) return null;
4830
- try {
4831
- return JSON.parse(raw);
4832
- } catch {
4833
- return null;
4834
- }
4835
- }
4836
- //#endregion
4837
4605
  //#region lib/types/doctor.js
4838
4606
  /**
4839
4607
  * Merge doctor rows: state, pending, gate cache and staging are each probed read-only.
4840
4608
  * A missing file is an empty feature, not a failure; an unreadable file is a warning the
4841
4609
  * operator can act on. This module never repairs or writes.
4842
4610
  */
4843
- const MERGE_DOCTOR_SCHEMA_VERSION = 3;
4844
4611
  /** Compose the one-line global-doctor fact from buildDoctorView + the last reconcile report. */
4845
4612
  function mergeDoctorFact(view, report) {
4846
4613
  const counts = view.counts;
@@ -4889,13 +4656,6 @@ async function mergeDoctor(io, home, configuredRoot) {
4889
4656
  status: snapshot.blocked === void 0 || !hasState ? "ok" : "warn",
4890
4657
  detail: snapshot.blocked === void 0 ? "pairs " + String(snapshot.rows.length) + ";verify " + String(counts.verify) + " / pending " + String(counts.pending) + " / fuse " + String(counts.fuse) + " / skipped " + String(counts.skipped) + " / expired " + String(snapshot.doctor.counts.expired) : snapshot.blocked.code + ": " + snapshot.blocked.message
4891
4658
  });
4892
- const recoveryRuns = /* @__PURE__ */ new Set();
4893
- try {
4894
- const raw = await io.readText(join(home, "merge", "recovery.json"));
4895
- if (raw !== null) {
4896
- for (const report of JSON.parse(raw)) if (report.ok === true && report.runId !== void 0) recoveryRuns.add(report.runId);
4897
- }
4898
- } catch {}
4899
4659
  const invalid = [];
4900
4660
  if (snapshot.blocked !== void 0) invalid.push({
4901
4661
  id: "state",
@@ -4908,15 +4668,14 @@ async function mergeDoctor(io, home, configuredRoot) {
4908
4668
  const run = runId === void 0 ? "none" : row.state === "pending" ? "awaiting-approval" : "running";
4909
4669
  const staging = item.stage === null ? row.state === "fuse" ? "missing" : "none" : item.stage.state === "present" || item.stage.state === "reserved" ? "kept" : row.state === "fuse" ? "missing" : "none";
4910
4670
  const receipt = runId === void 0 ? null : await readCommitReceipt(io, home, runId);
4911
- const phase = receipt !== null ? await readUnitJournal(io, home, receipt.unitId) : runId === void 0 ? null : await readJournal(io, home, runId);
4671
+ const phase = receipt === null ? null : await readUnitJournal(io, home, receipt.unitId);
4912
4672
  const result = validateArtifacts({
4913
4673
  row: artifactRow,
4914
4674
  run,
4915
4675
  staging,
4916
4676
  journal: phase === null ? "none" : phase.phase === "prepared" || phase.phase === "target-pending" ? "prepared" : phase.phase === "target-written" || phase.phase === "sources-archived" || phase.phase === "support-pending" || phase.phase === "source-archiving" ? "committing" : phase.phase === "committed" ? "committed" : phase.phase === "rolled-back" ? "rolled-back" : "failed",
4917
4677
  evidence: receipt === null ? "none" : "receipt",
4918
- activeRuns: run === "running" || run === "awaiting-approval" ? 1 : 0,
4919
- ...runId !== void 0 && recoveryRuns.has(runId) ? { recovery: true } : {}
4678
+ activeRuns: run === "running" || run === "awaiting-approval" ? 1 : 0
4920
4679
  });
4921
4680
  if (!result.ok) invalid.push({
4922
4681
  id: row.id,
@@ -5059,26 +4818,6 @@ async function mergeDoctor(io, home, configuredRoot) {
5059
4818
  status: "ok",
5060
4819
  detail: stagingRoot(home, "<runId>")
5061
4820
  });
5062
- try {
5063
- const raw = await io.readText(join(home, "merge", "recovery.json"));
5064
- if (raw !== null) {
5065
- const reports = JSON.parse(raw);
5066
- const failed = reports.filter((report) => report.ok !== true);
5067
- rows.push({
5068
- key: "recovery",
5069
- label: "提交恢复",
5070
- status: failed.length === 0 ? "ok" : "warn",
5071
- detail: failed.length === 0 ? String(reports.length) + " 条恢复报告,无待处理" : String(failed.length) + " 条待处理:" + failed.map((report) => (report.runId ?? "?") + "=" + (report.action ?? "?")).join(", ")
5072
- });
5073
- }
5074
- } catch {
5075
- rows.push({
5076
- key: "recovery",
5077
- label: "提交恢复",
5078
- status: "unknown",
5079
- detail: "无法读取 merge/recovery.json"
5080
- });
5081
- }
5082
4821
  return rows;
5083
4822
  }
5084
4823
  //#endregion
@@ -5275,13 +5014,6 @@ async function projectRecoveredUnit(options) {
5275
5014
  await options.onPhase?.("stage-drop");
5276
5015
  await dropStage(options.io, options.home, stageRowId);
5277
5016
  }
5278
- await options.onPhase?.("pair-index");
5279
- await recordPair(options.io, options.home, {
5280
- pairKey,
5281
- runId: options.runId,
5282
- result: "merged",
5283
- target
5284
- });
5285
5017
  await options.onPhase?.("run-meta");
5286
5018
  const meta = await readRunMeta(options.io, options.home, options.runId);
5287
5019
  await writeRunMeta(options.io, options.home, {
@@ -5996,9 +5728,6 @@ async function handleReadOnlyMergeCommand(command, io, home, cards, runs, approv
5996
5728
  }
5997
5729
  return formatSnapshotQueue(rows.filter((row) => row.queue === "skipped"), "skipped");
5998
5730
  }
5999
- function mergeCommandHelp() {
6000
- return mergeUsage();
6001
- }
6002
5731
  //#endregion
6003
5732
  //#region lib/types/merge-command.js
6004
5733
  function createReadOnlyMergeService(io, home) {
@@ -6052,10 +5781,6 @@ function createMergeCommand(service, version = MERGE_PACKAGE_VERSION) {
6052
5781
  }
6053
5782
  //#endregion
6054
5783
  //#region lib/types/skipped.js
6055
- /**
6056
- * Skipped filtering: a pair the operator marked as not-worth-merging stays out of automatic
6057
- * screening until mergeSkippedTtlDays expires. Explicit /merge pair ignores this set.
6058
- */
6059
5784
  function skippedPairKeys(rows, ttlDays, now) {
6060
5785
  const keys = /* @__PURE__ */ new Set();
6061
5786
  for (const row of rows) {
@@ -6069,9 +5794,6 @@ function skippedPairKeys(rows, ttlDays, now) {
6069
5794
  }
6070
5795
  return keys;
6071
5796
  }
6072
- function isSkippedPair(keys, a, b) {
6073
- return keys.has(pairKey(a, b));
6074
- }
6075
5797
  //#endregion
6076
5798
  //#region lib/types/domain/engine-criteria.js
6077
5799
  function criteriaFrom(level, verdict, action) {
@@ -6324,11 +6046,6 @@ async function runScan(options) {
6324
6046
  continue;
6325
6047
  }
6326
6048
  row = projected;
6327
- await recordPair(options.io, options.home, {
6328
- pairKey: row.pairKey,
6329
- runId: row.runId ?? "merge",
6330
- result: "kept"
6331
- });
6332
6049
  pairs.push(pairOf(row, candidate, "kept", {
6333
6050
  ...round1.decision === void 0 ? {} : { decision: round1.decision },
6334
6051
  ...round1.usage === void 0 ? {} : { usage: round1.usage }
@@ -6538,15 +6255,7 @@ async function runScan(options) {
6538
6255
  if (await pairLedger(options.io, options.home).transact((tx) => {
6539
6256
  const current = tx.rows().find((item) => item.id === row.id);
6540
6257
  return current === void 0 ? false : tx.remove(row.id, "superseded", void 0, current.version).ok;
6541
- })) {
6542
- await dropStage(options.io, options.home, row.id, row.stagingRef);
6543
- await recordPair(options.io, options.home, {
6544
- pairKey: row.pairKey,
6545
- runId: row.runId ?? "merge",
6546
- result: "failed",
6547
- ...row.decision?.target === void 0 ? {} : { target: row.decision.target }
6548
- });
6549
- }
6258
+ })) await dropStage(options.io, options.home, row.id, row.stagingRef);
6550
6259
  pairs.push(pairOf(row, candidate, "failed", {
6551
6260
  decision: mergeDecision,
6552
6261
  failure: failed,
@@ -7048,12 +6757,22 @@ async function recordRound2Outcome(io, home, now, row, outcome, deferProjection
7048
6757
  ...outcome.criteria === void 0 ? {} : { criteria: outcome.criteria },
7049
6758
  ...outcome.validationIssues === void 0 ? {} : { validation: { issues: outcome.validationIssues } }
7050
6759
  };
6760
+ const pair = (outcomeName, extra = {}, withDecision = true) => ({
6761
+ id: row.id,
6762
+ pairKey: row.pairKey,
6763
+ a: row.a,
6764
+ b: row.b,
6765
+ queue: row.queue,
6766
+ ...withDecision && row.decision !== void 0 ? { decision: row.decision } : {},
6767
+ outcome: outcomeName,
6768
+ archived: [],
6769
+ ...extra
6770
+ });
7051
6771
  if (outcome.kind === "merged") {
7052
6772
  const unitRef = {
7053
6773
  unitId: outcome.unitId ?? "unknown",
7054
6774
  evidenceSha256: outcome.evidenceSha256 ?? "unknown"
7055
6775
  };
7056
- const target = outcome.target?.actual ?? outcome.target?.requested ?? row.decision?.target;
7057
6776
  const finalize = async () => {
7058
6777
  if (!(await pairLedger(io, home, fence).transact((tx) => {
7059
6778
  const current = tx.rows().find((item) => item.id === row.id);
@@ -7064,50 +6783,28 @@ async function recordRound2Outcome(io, home, now, row, outcome, deferProjection
7064
6783
  } : tx.remove(row.id, "committed", unitRef, current.version);
7065
6784
  })).ok) throw new Error("row changed before commit record: " + row.id);
7066
6785
  await dropStage(io, home, row.id);
7067
- await recordPair(io, home, {
7068
- pairKey: row.pairKey,
7069
- runId: row.runId ?? "merge",
7070
- result: "merged",
7071
- ...target === void 0 ? {} : { target },
7072
- ...outcome.unitId === void 0 ? {} : { reportRef: outcome.unitId }
7073
- });
7074
6786
  };
7075
6787
  if (deferProjection) return {
7076
6788
  merged: true,
7077
6789
  finalize,
7078
- pair: {
7079
- id: row.id,
7080
- pairKey: row.pairKey,
7081
- a: row.a,
7082
- b: row.b,
7083
- queue: row.queue,
7084
- ...row.decision === void 0 ? {} : { decision: row.decision },
7085
- outcome: "merged",
7086
- archived: outcome.archived ?? [],
6790
+ pair: pair("merged", {
7087
6791
  ...outcome.lengths === void 0 ? {} : { lengths: outcome.lengths },
7088
6792
  ...outcome.unitId === void 0 ? {} : { unitId: outcome.unitId },
7089
6793
  ...outcome.target === void 0 ? {} : { target: outcome.target },
7090
6794
  ...reportExtra
7091
- }
6795
+ })
7092
6796
  };
7093
6797
  await finalize();
7094
6798
  return {
7095
6799
  merged: true,
7096
- pair: {
7097
- id: row.id,
7098
- pairKey: row.pairKey,
7099
- a: row.a,
7100
- b: row.b,
7101
- queue: row.queue,
7102
- ...row.decision === void 0 ? {} : { decision: row.decision },
7103
- outcome: "merged",
7104
- archived: outcome.archived ?? [],
6800
+ pair: pair("merged", {
6801
+ ...outcome.archived === void 0 ? {} : { archived: outcome.archived },
7105
6802
  ...outcome.lengths === void 0 ? {} : { lengths: outcome.lengths },
7106
6803
  ...reportExtra,
7107
6804
  ...outcome.unitId === void 0 ? {} : { unitId: outcome.unitId },
7108
6805
  ...outcome.target === void 0 ? {} : { target: outcome.target },
7109
6806
  ...outcome.validationPlan === void 0 ? {} : { validationPlan: outcome.validationPlan }
7110
- }
6807
+ })
7111
6808
  };
7112
6809
  }
7113
6810
  const failure = outcome.failure ?? {
@@ -7130,18 +6827,10 @@ async function recordRound2Outcome(io, home, now, row, outcome, deferProjection
7130
6827
  })) await dropStage(io, home, row.id);
7131
6828
  return {
7132
6829
  merged: false,
7133
- pair: {
7134
- id: row.id,
7135
- pairKey: row.pairKey,
7136
- a: row.a,
7137
- b: row.b,
7138
- queue: row.queue,
7139
- ...row.decision === void 0 ? {} : { decision: row.decision },
7140
- outcome: "failed",
7141
- archived: [],
6830
+ pair: pair("failed", {
7142
6831
  failure,
7143
6832
  ...reportExtra
7144
- }
6833
+ })
7145
6834
  };
7146
6835
  }
7147
6836
  const cas = casExpiredHook(failure.code);
@@ -7163,33 +6852,17 @@ async function recordRound2Outcome(io, home, now, row, outcome, deferProjection
7163
6852
  await dropStage(io, home, row.id);
7164
6853
  return {
7165
6854
  merged: false,
7166
- pair: {
7167
- id: row.id,
7168
- pairKey: row.pairKey,
7169
- a: row.a,
7170
- b: row.b,
7171
- queue: row.queue,
7172
- ...row.decision === void 0 ? {} : { decision: row.decision },
7173
- outcome: "failed",
7174
- archived: [],
6855
+ pair: pair("failed", {
7175
6856
  failure: terminalFailure,
7176
6857
  ...reportExtra
7177
- }
6858
+ })
7178
6859
  };
7179
6860
  }
7180
6861
  if (failure.code === "commit-failed" || failure.code === "validation-failed") {
7181
6862
  if (await pairLedger(io, home, fence).transact((tx) => {
7182
6863
  const current = tx.rows().find((item) => item.id === row.id);
7183
6864
  return current === void 0 ? false : tx.remove(row.id, "superseded", void 0, current.version).ok;
7184
- })) {
7185
- await dropStage(io, home, row.id, row.stagingRef);
7186
- await recordPair(io, home, {
7187
- pairKey: row.pairKey,
7188
- runId: row.runId ?? "merge",
7189
- result: "failed",
7190
- ...row.decision?.target === void 0 ? {} : { target: row.decision.target }
7191
- });
7192
- }
6865
+ })) await dropStage(io, home, row.id, row.stagingRef);
7193
6866
  }
7194
6867
  if (failure.code === "source-gone") {
7195
6868
  if (await pairLedger(io, home, fence).transact((tx) => {
@@ -7198,16 +6871,7 @@ async function recordRound2Outcome(io, home, now, row, outcome, deferProjection
7198
6871
  })) await dropStage(io, home, row.id, row.stagingRef);
7199
6872
  return {
7200
6873
  merged: false,
7201
- pair: {
7202
- id: row.id,
7203
- pairKey: row.pairKey,
7204
- a: row.a,
7205
- b: row.b,
7206
- queue: row.queue,
7207
- outcome: "failed",
7208
- archived: [],
7209
- failure
7210
- }
6874
+ pair: pair("failed", { failure }, false)
7211
6875
  };
7212
6876
  }
7213
6877
  if (await pairLedger(io, home, fence).transact((tx) => {
@@ -7220,42 +6884,21 @@ async function recordRound2Outcome(io, home, now, row, outcome, deferProjection
7220
6884
  return moved.ok && moved.row !== null ? v4ToLegacy(moved.row) : null;
7221
6885
  }) === null) return {
7222
6886
  merged: false,
7223
- pair: {
7224
- id: row.id,
7225
- pairKey: row.pairKey,
7226
- a: row.a,
7227
- b: row.b,
7228
- queue: row.queue,
7229
- outcome: "failed",
7230
- archived: [],
6887
+ pair: pair("failed", {
7231
6888
  failure: {
7232
6889
  ...failure,
7233
6890
  code: "row-changed",
7234
6891
  message: "row changed before failure record"
7235
6892
  },
7236
6893
  ...reportExtra
7237
- }
6894
+ }, false)
7238
6895
  };
7239
- await recordPair(io, home, {
7240
- pairKey: row.pairKey,
7241
- runId: row.runId ?? "merge",
7242
- result: "failed",
7243
- ...row.decision?.target === void 0 ? {} : { target: row.decision.target }
7244
- });
7245
6896
  return {
7246
6897
  merged: false,
7247
- pair: {
7248
- id: row.id,
7249
- pairKey: row.pairKey,
7250
- a: row.a,
7251
- b: row.b,
7252
- queue: row.queue,
7253
- ...row.decision === void 0 ? {} : { decision: row.decision },
7254
- outcome: "failed",
7255
- archived: [],
6898
+ pair: pair("failed", {
7256
6899
  failure,
7257
6900
  ...reportExtra
7258
- }
6901
+ })
7259
6902
  };
7260
6903
  }
7261
6904
  //#endregion
@@ -7704,14 +7347,6 @@ async function runUndoSteps(options) {
7704
7347
  };
7705
7348
  }
7706
7349
  if (await readUndoReceipt(options.io, options.home, options.runId, pairKey) !== null) {
7707
- await markPhase("pair-index");
7708
- await options.onPhase?.("pair-index", pairKey);
7709
- await recordPair(options.io, options.home, {
7710
- pairKey,
7711
- runId: options.runId,
7712
- result: "undone",
7713
- target: receipt.target
7714
- });
7715
7350
  await markPhase("run-meta");
7716
7351
  await options.onPhase?.("run-meta", pairKey);
7717
7352
  await settleRun(options.io, options.home, options.runId, pairKey, allPairKeys, options.now());
@@ -7738,14 +7373,6 @@ async function runUndoSteps(options) {
7738
7373
  restoredSourceHashes: Object.fromEntries(checked.evidence.sources.map((source) => [source.name, contentHash(source.before)])),
7739
7374
  at: options.now()
7740
7375
  });
7741
- await markPhase("pair-index");
7742
- await options.onPhase?.("pair-index", pairKey);
7743
- await recordPair(options.io, options.home, {
7744
- pairKey,
7745
- runId: options.runId,
7746
- result: "undone",
7747
- target: receipt.target
7748
- });
7749
7376
  await markPhase("run-meta");
7750
7377
  await options.onPhase?.("run-meta", pairKey);
7751
7378
  await settleRun(options.io, options.home, options.runId, pairKey, allPairKeys, options.now());
@@ -7805,13 +7432,6 @@ async function runUndoSteps(options) {
7805
7432
  restoredSourceHashes: Object.fromEntries(checked.evidence.sources.map((source) => [source.name, contentHash(source.before)])),
7806
7433
  at: options.now()
7807
7434
  });
7808
- await options.onPhase?.("pair-index", pairKey);
7809
- await recordPair(options.io, options.home, {
7810
- pairKey,
7811
- runId: options.runId,
7812
- result: "undone",
7813
- target: receipt.target
7814
- });
7815
7435
  await options.onPhase?.("run-meta", pairKey);
7816
7436
  await settleRun(options.io, options.home, options.runId, pairKey, allPairKeys, options.now());
7817
7437
  undone.push(pairKey);
@@ -8042,9 +7662,6 @@ function buildRound2Prompt(row, decision, mode = "continue") {
8042
7662
  ...body
8043
7663
  ] : [...head, ...body]).join("\n");
8044
7664
  }
8045
- function buildStandbyPrompt() {
8046
- return STANDBY_PROMPT;
8047
- }
8048
7665
  //#endregion
8049
7666
  //#region lib/types/channel-port.js
8050
7667
  async function runRound1OverChannel(options, candidate, signal) {
@@ -8552,14 +8169,6 @@ async function commitStagedRun(io, home, runId, port, options) {
8552
8169
  message: "target is blocked by curator: " + assembled.plan.target,
8553
8170
  plan: assembled.plan
8554
8171
  };
8555
- const journal = {
8556
- runId,
8557
- target: assembled.plan.target,
8558
- create: !assembled.plan.targetExists,
8559
- sources: assembled.plan.sources,
8560
- phase: "prepared",
8561
- updatedAt: new Date(options.now()).toISOString()
8562
- };
8563
8172
  let result;
8564
8173
  let evidenceSha256$1;
8565
8174
  let warning;
@@ -8589,9 +8198,6 @@ async function commitStagedRun(io, home, runId, port, options) {
8589
8198
  ...options.sourceRevisions === void 0 ? {} : { sourceRevisions: options.sourceRevisions },
8590
8199
  ...options.supportDigest === void 0 ? {} : { supportDigest: options.supportDigest }
8591
8200
  };
8592
- try {
8593
- await writeJournal(io, home, journal);
8594
- } catch {}
8595
8201
  const prepared = await transaction.prepare(unitPlan);
8596
8202
  if (!prepared.ok) return {
8597
8203
  ok: false,
@@ -8599,21 +8205,12 @@ async function commitStagedRun(io, home, runId, port, options) {
8599
8205
  plan: assembled.plan
8600
8206
  };
8601
8207
  const committed = await transaction.commit(prepared.prepared, options.signal === void 0 ? void 0 : { signal: options.signal });
8602
- if (!committed.ok) {
8603
- try {
8604
- await writeJournal(io, home, {
8605
- ...journal,
8606
- phase: "failed",
8607
- updatedAt: new Date(options.now()).toISOString()
8608
- });
8609
- } catch {}
8610
- return {
8611
- ok: false,
8612
- message: committed.message,
8613
- plan: assembled.plan,
8614
- ...committed.code === "target-changed" || committed.code === "source-changed" || committed.code === "support-changed" ? { code: committed.code } : {}
8615
- };
8616
- }
8208
+ if (!committed.ok) return {
8209
+ ok: false,
8210
+ message: committed.message,
8211
+ plan: assembled.plan,
8212
+ ...committed.code === "target-changed" || committed.code === "source-changed" || committed.code === "support-changed" ? { code: committed.code } : {}
8213
+ };
8617
8214
  const unitId = committed.evidence.unitId;
8618
8215
  if (committed.evidence.unitId !== prepared.prepared.unitId && !prepared.prepared.unitId.startsWith("legacy:")) return {
8619
8216
  ok: false,
@@ -8634,29 +8231,11 @@ async function commitStagedRun(io, home, runId, port, options) {
8634
8231
  mutations: committed.evidence.mutations
8635
8232
  };
8636
8233
  }
8637
- if (!result.ok) {
8638
- try {
8639
- await writeJournal(io, home, {
8640
- ...journal,
8641
- phase: "failed",
8642
- updatedAt: new Date(options.now()).toISOString()
8643
- });
8644
- } catch {}
8645
- return {
8646
- ok: false,
8647
- message: result.message,
8648
- plan: assembled.plan
8649
- };
8650
- }
8651
- const committed = {
8652
- ...journal,
8653
- phase: "committed",
8654
- updatedAt: new Date(options.now()).toISOString(),
8655
- ...result.unitId === void 0 ? {} : { unitId: result.unitId }
8234
+ if (!result.ok) return {
8235
+ ok: false,
8236
+ message: result.message,
8237
+ plan: assembled.plan
8656
8238
  };
8657
- try {
8658
- await writeJournal(io, home, committed);
8659
- } catch {}
8660
8239
  if (result.unitId === void 0) return {
8661
8240
  ok: true,
8662
8241
  plan: assembled.plan,
@@ -8938,32 +8517,38 @@ function missingManifestFile(files, stagedPaths) {
8938
8517
  }
8939
8518
  //#endregion
8940
8519
  //#region lib/types/authoring-flow.js
8941
- async function runAuthoring(options) {
8942
- if (options.target.includes("/") || options.target.includes(":")) return {
8943
- ok: false,
8520
+ /** The target facts both authoring entries may refuse on before writing anything. */
8521
+ async function targetAdmission(target, targetCard, createMode, isBlocked) {
8522
+ if (target.includes("/") || target.includes(":")) return {
8944
8523
  code: "target-invalid",
8945
- message: "target must be a bare skill name: " + options.target
8524
+ message: "target must be a bare skill name: " + target
8946
8525
  };
8947
- const targetCard = options.cards.find((card) => card.name === options.target);
8948
- const requestedMode = options.mode ?? (targetCard === void 0 ? "create-umbrella" : "merge-into-existing");
8949
- const mode = requestedMode === "merge-into-existing" && targetCard === void 0 ? "create-umbrella" : requestedMode;
8950
- const createMode = mode === "create-umbrella";
8951
8526
  if (!createMode && targetCard !== void 0) {
8952
8527
  if (targetCard.managed !== true) return {
8953
- ok: false,
8954
8528
  code: "target-not-managed",
8955
- message: "target is not family-managed: " + options.target
8529
+ message: "target is not family-managed: " + target
8956
8530
  };
8957
8531
  if (targetCard.protectionUnknown === true || targetCard.protectedBy != null) return {
8958
- ok: false,
8959
8532
  code: "target-protected",
8960
- message: "target is protected (" + (targetCard.protectedBy ?? "unknown") + "): " + options.target
8533
+ message: "target is protected (" + (targetCard.protectedBy ?? "unknown") + "): " + target
8961
8534
  };
8962
8535
  }
8963
- if (!createMode && await options.isBlocked?.(options.target) === true) return {
8964
- ok: false,
8536
+ if (!createMode && await isBlocked?.(target) === true) return {
8965
8537
  code: "target-blocked",
8966
- message: "target is blocked by curator: " + options.target
8538
+ message: "target is blocked by curator: " + target
8539
+ };
8540
+ return null;
8541
+ }
8542
+ async function runAuthoring(options) {
8543
+ const targetCard = options.cards.find((card) => card.name === options.target);
8544
+ const requestedMode = options.mode ?? (targetCard === void 0 ? "create-umbrella" : "merge-into-existing");
8545
+ const mode = requestedMode === "merge-into-existing" && targetCard === void 0 ? "create-umbrella" : requestedMode;
8546
+ const createMode = mode === "create-umbrella";
8547
+ const targetRefusal = await targetAdmission(options.target, targetCard, createMode, options.isBlocked);
8548
+ if (targetRefusal !== null) return {
8549
+ ok: false,
8550
+ code: targetRefusal.code,
8551
+ message: targetRefusal.message
8967
8552
  };
8968
8553
  const stageKey = options.stageKey ?? options.runId;
8969
8554
  if (options.reuseStaging === true) {
@@ -9285,46 +8870,15 @@ const STALE_CAUSES = new Set([
9285
8870
  "target-gone"
9286
8871
  ]);
9287
8872
  async function runNewChildAuthoring(options, row, decision, signal) {
9288
- if (decision.target.includes("/") || decision.target.includes(":")) return {
9289
- kind: "failed",
9290
- failure: {
9291
- stage: "author",
9292
- code: "target-invalid",
9293
- message: "target must be a bare skill name: " + decision.target,
9294
- retryable: false,
9295
- retryFrom: "none"
9296
- }
9297
- };
9298
8873
  const targetCard = options.cards.find((card) => card.name === decision.target);
9299
8874
  const createMode = decision.mode === "create-umbrella";
9300
- if (!createMode && targetCard !== void 0) {
9301
- if (targetCard.managed !== true) return {
9302
- kind: "failed",
9303
- failure: {
9304
- stage: "author",
9305
- code: "target-not-managed",
9306
- message: "target is not family-managed: " + decision.target,
9307
- retryable: false,
9308
- retryFrom: "none"
9309
- }
9310
- };
9311
- if (targetCard.protectionUnknown === true || targetCard.protectedBy != null) return {
9312
- kind: "failed",
9313
- failure: {
9314
- stage: "author",
9315
- code: "target-protected",
9316
- message: "target is protected: " + decision.target,
9317
- retryable: false,
9318
- retryFrom: "none"
9319
- }
9320
- };
9321
- }
9322
- if (!createMode && await options.isBlocked?.(decision.target) === true) return {
8875
+ const targetRefusal = await targetAdmission(decision.target, targetCard, createMode, options.isBlocked);
8876
+ if (targetRefusal !== null) return {
9323
8877
  kind: "failed",
9324
8878
  failure: {
9325
8879
  stage: "author",
9326
- code: "target-blocked",
9327
- message: "target is blocked by curator: " + decision.target,
8880
+ code: targetRefusal.code,
8881
+ message: targetRefusal.message,
9328
8882
  retryable: false,
9329
8883
  retryFrom: "none"
9330
8884
  }
@@ -10787,143 +10341,6 @@ async function supersedeMergeRuns(registry) {
10787
10341
  return ids.length;
10788
10342
  }
10789
10343
  //#endregion
10790
- //#region lib/types/coordinator.js
10791
- /**
10792
- * Pure merge transition table. No io, no run shell, no subagents.
10793
- *
10794
- * Every transition returns the whole next row (or a drop), so the caller can persist it
10795
- * through one state transaction. Invalid transitions are refused by name;
10796
- * a caller never "repairs" a row by guessing which state the operator meant.
10797
- */
10798
- function invalid(event, state) {
10799
- return {
10800
- ok: false,
10801
- code: "invalid-transition",
10802
- message: "cannot apply " + event.type + " to " + (state ?? "no row")
10803
- };
10804
- }
10805
- /** Drop fields that must not survive a transition; optional fields stay absent. */
10806
- function base(row, at) {
10807
- return {
10808
- id: row.id,
10809
- pairKey: row.pairKey,
10810
- a: row.a,
10811
- b: row.b,
10812
- state: row.state,
10813
- queue: row.queue,
10814
- createdAt: row.createdAt,
10815
- updatedAt: at,
10816
- ...row.runId === void 0 ? {} : { runId: row.runId },
10817
- ...row.unitId === void 0 ? {} : { unitId: row.unitId },
10818
- ...row.origin === void 0 ? {} : { origin: row.origin },
10819
- ...row.decision === void 0 ? {} : { decision: row.decision },
10820
- ...row.sourceRevision === void 0 ? {} : { sourceRevision: row.sourceRevision },
10821
- ...row.targetRevision === void 0 ? {} : { targetRevision: row.targetRevision },
10822
- ...row.attempts === void 0 ? {} : { attempts: row.attempts },
10823
- ...row.stagingRef === void 0 ? {} : { stagingRef: row.stagingRef },
10824
- ...row.stagingExpiresAt === void 0 ? {} : { stagingExpiresAt: row.stagingExpiresAt },
10825
- ...row.expired === void 0 ? {} : { expired: row.expired },
10826
- ...row.reason === void 0 ? {} : { reason: row.reason },
10827
- ...row.target === void 0 ? {} : { target: row.target }
10828
- };
10829
- }
10830
- function next(row, at, state, patch = {}) {
10831
- return {
10832
- ok: true,
10833
- row: {
10834
- ...base(row, at),
10835
- state,
10836
- queue: queueOf(state),
10837
- ...patch
10838
- }
10839
- };
10840
- }
10841
- /** Legacy v2 projection only; runtime writes must use transitionV3/RowTx. */
10842
- function transitionLegacyForProjection(current, event) {
10843
- if (event.type === "candidate") {
10844
- if (current !== void 0) return {
10845
- ok: true,
10846
- row: current
10847
- };
10848
- const key = pairKey(event.a, event.b);
10849
- return {
10850
- ok: true,
10851
- row: {
10852
- id: rowIdOf(key),
10853
- pairKey: key,
10854
- a: event.a,
10855
- b: event.b,
10856
- state: "verify",
10857
- queue: "verify",
10858
- createdAt: event.at,
10859
- updatedAt: event.at
10860
- }
10861
- };
10862
- }
10863
- if (event.type === "source-gone") return {
10864
- ok: true,
10865
- row: null
10866
- };
10867
- if (event.type === "gate-keep") {
10868
- if (current !== void 0) return {
10869
- ok: true,
10870
- row: current
10871
- };
10872
- const key = pairKey(event.a, event.b);
10873
- return {
10874
- ok: true,
10875
- row: {
10876
- id: rowIdOf(key),
10877
- pairKey: key,
10878
- a: event.a,
10879
- b: event.b,
10880
- state: "skipped",
10881
- queue: "skipped",
10882
- origin: "agent",
10883
- createdAt: event.at,
10884
- updatedAt: event.at
10885
- }
10886
- };
10887
- }
10888
- if (current === void 0) return invalid(event, void 0);
10889
- switch (event.type) {
10890
- case "round1-keep": return current.state === "verify" ? next(current, event.at, "skipped", { origin: "agent" }) : invalid(event, current.state);
10891
- case "round1-merge":
10892
- if (current.state !== "verify") return invalid(event, current.state);
10893
- return event.approval === "on" ? next(current, event.at, "pending", {
10894
- ...event.decision === void 0 ? {} : { decision: event.decision },
10895
- ...event.stagingRef === void 0 ? {} : { stagingRef: event.stagingRef },
10896
- ...event.stagingExpiresAt === void 0 ? {} : { stagingExpiresAt: event.stagingExpiresAt },
10897
- ...event.sourceRevision === void 0 ? {} : { sourceRevision: event.sourceRevision },
10898
- ...event.targetRevision === void 0 ? {} : { targetRevision: event.targetRevision }
10899
- }) : next(current, event.at, "approved", {
10900
- ...event.decision === void 0 ? {} : { decision: event.decision },
10901
- ...event.stagingRef === void 0 ? {} : { stagingRef: event.stagingRef },
10902
- ...event.stagingExpiresAt === void 0 ? {} : { stagingExpiresAt: event.stagingExpiresAt },
10903
- ...event.sourceRevision === void 0 ? {} : { sourceRevision: event.sourceRevision },
10904
- ...event.targetRevision === void 0 ? {} : { targetRevision: event.targetRevision }
10905
- });
10906
- case "round1-fail": return current.state === "verify" ? next(current, event.at, "verify", {
10907
- failure: event.failure,
10908
- attempts: (current.attempts ?? 0) + 1
10909
- }) : invalid(event, current.state);
10910
- case "approve": return current.state === "pending" ? next(current, event.at, "approved", event.runId === void 0 ? {} : { runId: event.runId }) : invalid(event, current.state);
10911
- case "reject": return current.state === "pending" ? next(current, event.at, "skipped", { origin: "human" }) : invalid(event, current.state);
10912
- case "round2-cancel": return current.state === "fuse" || current.state === "approved" ? next(current, event.at, "approved") : invalid(event, current.state);
10913
- case "round2-fail": return current.state === "fuse" || current.state === "approved" ? next(current, event.at, "approved", {
10914
- failure: event.failure,
10915
- attempts: (current.attempts ?? 0) + 1
10916
- }) : invalid(event, current.state);
10917
- case "round2-success": return current.state === "fuse" || current.state === "approved" ? {
10918
- ok: true,
10919
- row: null
10920
- } : invalid(event, current.state);
10921
- case "target-taken":
10922
- case "source-changed": return current.state === "pending" || current.state === "approved" || current.state === "fuse" ? next(current, event.at, current.state, { expired: event.type }) : invalid(event, current.state);
10923
- case "restore": return current.state === "skipped" ? next(current, event.at, "verify") : invalid(event, current.state);
10924
- }
10925
- }
10926
- //#endregion
10927
10344
  //#region lib/types/pair-command.js
10928
10345
  async function runPairCommand(options) {
10929
10346
  return await runScanCommand({
@@ -11386,4 +10803,4 @@ function apply(ctx, rawConfig = {}) {
11386
10803
  });
11387
10804
  }
11388
10805
  //#endregion
11389
- export { Config, MERGE_DOCTOR_SCHEMA_VERSION, MERGE_LIFECYCLE_VERBS, MERGE_MAX_ATTEMPTS, MERGE_PAIR_STATES, MERGE_PARAM_DEFAULTS, MERGE_PROMPT_VERSION, MERGE_QUEUES, MERGE_ROUTES, MERGE_STAGE_TOOL_NAME, MERGE_STATE_VERSION, STAGING_SCHEMA_VERSION, STANDBY_PROMPT, StageToolRegistry, apply, applyStageTool, approvePending, assembleStagedCommit, buildCommitPlan, buildRound1Prompt, buildRound2Prompt, buildStandbyPrompt, busyTarget, canRetry, cleanupExpiredStaging, commitReceiptPath, commitStagedRun, committedPairs, configureMergePresentation, copyStagedFile, countRows, createChannelScanPorts, createEngineRunner, createEngineScreening, createMergeCommand, createMergeRuntime, createMergeStageTool, createNewChildRound2, createReadOnlyMergeService, createStageForCards, createStaging, dropStaging, durableReceiptDir, durableReceiptPath, emptyMergeState, engineConfigFromRoot, ensureStaging, forcedDecisionFor, formatFacts, formatQueue, formatRunDetail, formatRunsView, formatSnapshotQueue, formatSnapshotStatus, formatStatus, handleReadOnlyMergeCommand, hasBlockingFinding, inject, isSkippedPair, listStagedFiles, liveRowFactsOf, loadMergeState, loadStateSchema, makeMergeRoutes, manifestFilesOf, mergeCommandHelp, mergeDoctor, mergeDoctorAvailability, mergeDoctorFact, mergeReportDigestPath, mergeReportPath, mergeRunIdsToSupersede, mergeStatePath, mergeUsage, missingManifestFile, pairKey, parseAuthoringManifest, parseDecision, parseMergeCommand, pendingExpiry, persistedExpiry, promptHashOf, queueOf, readCommitReceipt, readCommitReceipts, readMergeReport, readStagedFile, readStagedTree, readStaging, recordRound2Outcome, rejectPending, resolveMergeParams, resolveTargetName, retryMergeReport, rewriteFrontmatterName, rowIdOf, rowsOf, runApproveCommand, runAuthoring, runMergeCommand, runNewChildAuthoring, runPairCommand, runResumeCommand, runRound1OverChannel, runScan, runScanCommand, setStagingTarget, skillLibraryPort, skillLibraryUndoPort, skippedPairKeys, snapshotCards, sourceBlock, stageKeyOf, stageOf, stagingRoot, subagentChannelAdapter, supersedeMergeRuns, transitionLegacyForProjection, undoMergeReceipt, unitFor, validateStagedTree, withMergeRun, writeCommitReceipt, writeMergeReport, writeStagedFile };
10806
+ export { Config, MERGE_LIFECYCLE_VERBS, MERGE_MAX_ATTEMPTS, MERGE_PAIR_STATES, MERGE_PARAM_DEFAULTS, MERGE_PROMPT_VERSION, MERGE_ROUTES, MERGE_STAGE_TOOL_NAME, MERGE_STATE_VERSION, STAGING_SCHEMA_VERSION, STANDBY_PROMPT, StageToolRegistry, apply, applyStageTool, approvePending, assembleStagedCommit, buildCommitPlan, buildRound1Prompt, buildRound2Prompt, busyTarget, canRetry, cleanupExpiredStaging, commitReceiptPath, commitStagedRun, committedPairs, configureMergePresentation, copyStagedFile, countRows, createChannelScanPorts, createEngineRunner, createEngineScreening, createMergeCommand, createMergeRuntime, createMergeStageTool, createNewChildRound2, createReadOnlyMergeService, createStageForCards, createStaging, durableReceiptDir, durableReceiptPath, emptyMergeState, engineConfigFromRoot, forcedDecisionFor, formatFacts, formatQueue, formatRunDetail, formatRunsView, formatSnapshotQueue, formatSnapshotStatus, formatStatus, handleReadOnlyMergeCommand, hasBlockingFinding, inject, listStagedFiles, liveRowFactsOf, loadMergeState, makeMergeRoutes, manifestFilesOf, mergeDoctor, mergeDoctorAvailability, mergeDoctorFact, mergeReportDigestPath, mergeReportPath, mergeRunIdsToSupersede, mergeStatePath, mergeUsage, missingManifestFile, pairKey, parseAuthoringManifest, parseDecision, parseMergeCommand, pendingExpiry, persistedExpiry, promptHashOf, queueOf, readCommitReceipt, readCommitReceipts, readMergeReport, readStagedFile, readStagedTree, readStaging, recordRound2Outcome, rejectPending, resolveMergeParams, resolveTargetName, retryMergeReport, rewriteFrontmatterName, rowIdOf, rowsOf, runApproveCommand, runAuthoring, runMergeCommand, runNewChildAuthoring, runPairCommand, runResumeCommand, runRound1OverChannel, runScan, runScanCommand, setStagingTarget, skillLibraryPort, skillLibraryUndoPort, skippedPairKeys, snapshotCards, sourceBlock, stageKeyOf, stageOf, stagingRoot, subagentChannelAdapter, supersedeMergeRuns, targetAdmission, undoMergeReceipt, unitFor, validateStagedTree, withMergeRun, writeCommitReceipt, writeMergeReport, writeStagedFile };