@lmzhen/dsh-evolution-merge 0.30.16 → 0.30.18
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +4 -3
- package/lib/index.js +2325 -855
- package/lib/types/adapters/library.js +4 -1
- package/lib/types/adapters/model.d.ts +31 -0
- package/lib/types/adapters/model.js +46 -0
- package/lib/types/approve-command.d.ts +2 -0
- package/lib/types/approve-command.js +41 -46
- package/lib/types/authoring-flow.d.ts +3 -1
- package/lib/types/authoring-flow.js +15 -5
- package/lib/types/command-surface.d.ts +5 -0
- package/lib/types/command-surface.js +44 -0
- package/lib/types/commit-assembly.d.ts +2 -0
- package/lib/types/commit-assembly.js +7 -2
- package/lib/types/commit-execute.d.ts +7 -1
- package/lib/types/commit-execute.js +40 -10
- package/lib/types/commit-recovery.d.ts +7 -2
- package/lib/types/commit-recovery.js +73 -96
- package/lib/types/contracts.d.ts +1 -1
- package/lib/types/contracts.js +1 -1
- package/lib/types/coordinator.d.ts +3 -2
- package/lib/types/coordinator.js +4 -3
- package/lib/types/decision.js +1 -1
- package/lib/types/doctor.d.ts +7 -0
- package/lib/types/doctor.js +104 -4
- package/lib/types/domain/claim.d.ts +4 -1
- package/lib/types/domain/claim.js +2 -2
- package/lib/types/domain/commit-journal-store.js +5 -1
- package/lib/types/domain/commit-journal.js +1 -1
- package/lib/types/domain/consistency.d.ts +22 -0
- package/lib/types/domain/consistency.js +50 -0
- package/lib/types/domain/ledger.js +10 -1
- package/lib/types/domain/migrate-v3.d.ts +20 -0
- package/lib/types/domain/migrate-v3.js +68 -0
- package/lib/types/domain/row-tx.d.ts +27 -0
- package/lib/types/domain/row-tx.js +31 -0
- package/lib/types/domain/row-v3-bridge.d.ts +6 -0
- package/lib/types/domain/row-v3-bridge.js +47 -0
- package/lib/types/domain/row-v3.d.ts +113 -0
- package/lib/types/domain/row-v3.js +107 -0
- package/lib/types/domain/row.d.ts +1 -0
- package/lib/types/domain/staging-index.d.ts +14 -0
- package/lib/types/domain/staging-index.js +41 -0
- package/lib/types/domain/state-v3.d.ts +15 -0
- package/lib/types/domain/state-v3.js +80 -0
- package/lib/types/facts.d.ts +24 -0
- package/lib/types/facts.js +68 -0
- package/lib/types/gate-filter.d.ts +31 -0
- package/lib/types/gate-filter.js +64 -0
- package/lib/types/index.d.ts +5 -2
- package/lib/types/index.js +16 -26
- package/lib/types/ladder.js +1 -1
- package/lib/types/merge-routes.d.ts +2 -2
- package/lib/types/merge-routes.js +21 -22
- package/lib/types/metrics.d.ts +16 -0
- package/lib/types/metrics.js +27 -0
- package/lib/types/new-child-authoring.d.ts +3 -1
- package/lib/types/new-child-authoring.js +14 -3
- package/lib/types/pending-actions.d.ts +2 -2
- package/lib/types/pending-actions.js +17 -17
- package/lib/types/propose.d.ts +26 -0
- package/lib/types/propose.js +69 -0
- package/lib/types/read-commands.d.ts +2 -2
- package/lib/types/read-commands.js +5 -3
- package/lib/types/resume-command.d.ts +2 -0
- package/lib/types/resume-command.js +4 -1
- package/lib/types/round2-outcome.js +17 -9
- package/lib/types/runtime.d.ts +4 -1
- package/lib/types/runtime.js +99 -29
- package/lib/types/scan-ports.d.ts +3 -1
- package/lib/types/scan-ports.js +2 -0
- package/lib/types/scan-run.js +116 -51
- package/lib/types/screening-port.d.ts +47 -0
- package/lib/types/screening-port.js +93 -0
- package/lib/types/screening.d.ts +30 -0
- package/lib/types/screening.js +75 -0
- package/lib/types/state.d.ts +14 -0
- package/lib/types/state.js +72 -1
- package/lib/types/status.d.ts +3 -0
- package/lib/types/status.js +17 -0
- package/lib/types/target-claim.d.ts +1 -0
- package/lib/types/target-claim.js +63 -18
- package/lib/types/unit-claim.d.ts +5 -0
- package/lib/types/unit-claim.js +15 -0
- package/lib/types/wire-projection.d.ts +36 -0
- package/lib/types/wire-projection.js +69 -0
- package/lib/types/wire.d.ts +254 -0
- package/lib/types/wire.js +3 -0
- package/package.json +8 -4
- package/lib/types/routes.d.ts +0 -70
- package/lib/types/routes.js +0 -78
package/lib/index.js
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { dirname, join } from "node:path";
|
|
2
2
|
import z from "@deepseek-ai/schemastery";
|
|
3
3
|
import { pairKeyOf, screen, selectCandidates, toEngineCandidate } from "@lmzhen/dsh-evolution-merge-engine";
|
|
4
|
-
import { DEFAULT_SKILL_LIMITS, authorityFrom, contentHash, createSubagentChannel, evolutionHome, evolutionIoAdapter, executeUnitUndo, isPresent, newRunRegistry, newSkillLibrary, parseFrontmatter, planUnitUndoAcross, policyStageLimits, probeAbsent, probeList, probePresent, probeText, probeUnknown, scanContentThreats,
|
|
4
|
+
import { DEFAULT_SKILL_LIMITS, SkillLibrary, UnitTransactions, authorityFrom, contentHash, createSubagentChannel, evolutionHome, evolutionIoAdapter, executeUnitUndo, isPresent, isProcessAlive, newRunRegistry, newSkillLibrary, parseFrontmatter, planUnitUndoAcross, policyStageLimits, probeAbsent, probeList, probePresent, probeText, probeUnknown, readTreeLock, readUnitEvidence, runsFile, scanContentThreats, skillLibraryUnitBackend, supportRefs, transactIo, validateFrontmatter, validateSupportPath } from "@lmzhen/dsh-evolution-core";
|
|
5
5
|
import { HOST_ROUTES, json, methodNotAllowed, registerHostRoutes } from "@lmzhen/dsh-evolution-core/http-routes";
|
|
6
6
|
import { createHash, randomUUID } from "node:crypto";
|
|
7
|
+
import { mkdir, open, readFile, unlink } from "node:fs/promises";
|
|
7
8
|
//#region lib/types/adapters/select.js
|
|
8
9
|
/** Host selector correction: engine selector does not route judge errors to candidates. */
|
|
9
10
|
function selectWithErrors(report) {
|
|
@@ -132,7 +133,7 @@ function createEngineRunner(llm, route) {
|
|
|
132
133
|
* The data layer keeps four states; the user face collapses verify/fuse into one retry
|
|
133
134
|
* queue and keeps the stage as a chip. The API queue must never expose the raw state.
|
|
134
135
|
*/
|
|
135
|
-
const MERGE_STATE_VERSION =
|
|
136
|
+
const MERGE_STATE_VERSION = 3;
|
|
136
137
|
const MERGE_PAIR_STATES = [
|
|
137
138
|
"verify",
|
|
138
139
|
"pending",
|
|
@@ -181,7 +182,7 @@ function rowIdOf(key) {
|
|
|
181
182
|
}
|
|
182
183
|
function emptyMergeState() {
|
|
183
184
|
return {
|
|
184
|
-
schemaVersion:
|
|
185
|
+
schemaVersion: 3,
|
|
185
186
|
rows: []
|
|
186
187
|
};
|
|
187
188
|
}
|
|
@@ -218,6 +219,396 @@ function migrateV1Row(row) {
|
|
|
218
219
|
};
|
|
219
220
|
}
|
|
220
221
|
//#endregion
|
|
222
|
+
//#region lib/types/domain/row-v3.js
|
|
223
|
+
function bad$1(code, message) {
|
|
224
|
+
return {
|
|
225
|
+
ok: false,
|
|
226
|
+
code,
|
|
227
|
+
message
|
|
228
|
+
};
|
|
229
|
+
}
|
|
230
|
+
function advance(row, at, patch) {
|
|
231
|
+
return {
|
|
232
|
+
...row,
|
|
233
|
+
...patch,
|
|
234
|
+
version: row.version + 1,
|
|
235
|
+
updatedAt: at
|
|
236
|
+
};
|
|
237
|
+
}
|
|
238
|
+
function transitionV3(row, event, expectedVersion, at = (/* @__PURE__ */ new Date()).toISOString()) {
|
|
239
|
+
if (row.version !== expectedVersion) return bad$1("version-conflict", "expected " + String(expectedVersion) + ", got " + String(row.version));
|
|
240
|
+
switch (row.state) {
|
|
241
|
+
case "verify":
|
|
242
|
+
if (event.type === "verify-passed") return {
|
|
243
|
+
ok: true,
|
|
244
|
+
row: advance(row, at, {
|
|
245
|
+
state: "pending",
|
|
246
|
+
target: event.target,
|
|
247
|
+
decision: event.decision,
|
|
248
|
+
sourceRevision: event.sourceRevision,
|
|
249
|
+
attempts: 0
|
|
250
|
+
}),
|
|
251
|
+
version: expectedVersion + 1
|
|
252
|
+
};
|
|
253
|
+
if (event.type === "auto-approved") return {
|
|
254
|
+
ok: true,
|
|
255
|
+
row: advance(row, at, {
|
|
256
|
+
state: "approved",
|
|
257
|
+
target: event.target,
|
|
258
|
+
decision: event.decision,
|
|
259
|
+
attempts: 0,
|
|
260
|
+
...event.runId === void 0 ? {} : { runId: event.runId }
|
|
261
|
+
}),
|
|
262
|
+
version: expectedVersion + 1
|
|
263
|
+
};
|
|
264
|
+
if (event.type === "failed") return {
|
|
265
|
+
ok: true,
|
|
266
|
+
row: advance(row, at, {
|
|
267
|
+
failure: event.failure,
|
|
268
|
+
attempts: row.attempts + 1
|
|
269
|
+
}),
|
|
270
|
+
version: expectedVersion + 1
|
|
271
|
+
};
|
|
272
|
+
if (event.type === "skipped") return {
|
|
273
|
+
ok: true,
|
|
274
|
+
row: advance(row, at, {
|
|
275
|
+
state: "skipped",
|
|
276
|
+
reason: event.reason,
|
|
277
|
+
attempts: row.attempts
|
|
278
|
+
}),
|
|
279
|
+
version: expectedVersion + 1
|
|
280
|
+
};
|
|
281
|
+
if (event.type === "superseded") return {
|
|
282
|
+
ok: true,
|
|
283
|
+
row: null,
|
|
284
|
+
version: expectedVersion + 1
|
|
285
|
+
};
|
|
286
|
+
return bad$1("illegal-transition", "verify/" + event.type);
|
|
287
|
+
case "pending":
|
|
288
|
+
if (event.type === "approved") return {
|
|
289
|
+
ok: true,
|
|
290
|
+
row: advance(row, at, {
|
|
291
|
+
state: "approved",
|
|
292
|
+
runId: event.runId
|
|
293
|
+
}),
|
|
294
|
+
version: expectedVersion + 1
|
|
295
|
+
};
|
|
296
|
+
if (event.type === "auto-approved") return {
|
|
297
|
+
ok: true,
|
|
298
|
+
row: advance(row, at, {
|
|
299
|
+
state: "approved",
|
|
300
|
+
...event.runId === void 0 ? {} : { runId: event.runId }
|
|
301
|
+
}),
|
|
302
|
+
version: expectedVersion + 1
|
|
303
|
+
};
|
|
304
|
+
if (event.type === "staged") return {
|
|
305
|
+
ok: true,
|
|
306
|
+
row: advance(row, at, {
|
|
307
|
+
state: "fuse",
|
|
308
|
+
staging: event.ref === void 0 ? { kind: "absent" } : {
|
|
309
|
+
kind: "present",
|
|
310
|
+
ref: event.ref,
|
|
311
|
+
expiresAt: event.expiresAt ?? Date.now() + 7 * 864e5
|
|
312
|
+
}
|
|
313
|
+
}),
|
|
314
|
+
version: expectedVersion + 1
|
|
315
|
+
};
|
|
316
|
+
if (event.type === "skipped") return {
|
|
317
|
+
ok: true,
|
|
318
|
+
row: advance(row, at, {
|
|
319
|
+
state: "skipped",
|
|
320
|
+
reason: event.reason,
|
|
321
|
+
...event.origin === void 0 ? {} : { origin: event.origin }
|
|
322
|
+
}),
|
|
323
|
+
version: expectedVersion + 1
|
|
324
|
+
};
|
|
325
|
+
if (event.type === "expired") return {
|
|
326
|
+
ok: true,
|
|
327
|
+
row: advance(row, at, { expired: event.kind }),
|
|
328
|
+
version: expectedVersion + 1
|
|
329
|
+
};
|
|
330
|
+
if (event.type === "failed") return {
|
|
331
|
+
ok: true,
|
|
332
|
+
row: advance(row, at, {
|
|
333
|
+
failure: event.failure,
|
|
334
|
+
attempts: row.attempts + 1
|
|
335
|
+
}),
|
|
336
|
+
version: expectedVersion + 1
|
|
337
|
+
};
|
|
338
|
+
if (event.type === "superseded") return {
|
|
339
|
+
ok: true,
|
|
340
|
+
row: null,
|
|
341
|
+
version: expectedVersion + 1
|
|
342
|
+
};
|
|
343
|
+
return bad$1("illegal-transition", "pending/" + event.type);
|
|
344
|
+
case "approved":
|
|
345
|
+
if (event.type === "staged") return {
|
|
346
|
+
ok: true,
|
|
347
|
+
row: advance(row, at, {
|
|
348
|
+
state: "fuse",
|
|
349
|
+
staging: event.ref === void 0 ? { kind: "absent" } : {
|
|
350
|
+
kind: "present",
|
|
351
|
+
ref: event.ref,
|
|
352
|
+
expiresAt: event.expiresAt ?? Date.now() + 7 * 864e5
|
|
353
|
+
}
|
|
354
|
+
}),
|
|
355
|
+
version: expectedVersion + 1
|
|
356
|
+
};
|
|
357
|
+
if (event.type === "committed") return {
|
|
358
|
+
ok: true,
|
|
359
|
+
row: null,
|
|
360
|
+
version: expectedVersion + 1
|
|
361
|
+
};
|
|
362
|
+
if (event.type === "skipped") return {
|
|
363
|
+
ok: true,
|
|
364
|
+
row: advance(row, at, {
|
|
365
|
+
state: "skipped",
|
|
366
|
+
reason: event.reason
|
|
367
|
+
}),
|
|
368
|
+
version: expectedVersion + 1
|
|
369
|
+
};
|
|
370
|
+
if (event.type === "failed") return event.failure.retryable ? {
|
|
371
|
+
ok: true,
|
|
372
|
+
row: advance(row, at, {
|
|
373
|
+
state: "pending",
|
|
374
|
+
failure: event.failure,
|
|
375
|
+
attempts: row.attempts + 1
|
|
376
|
+
}),
|
|
377
|
+
version: expectedVersion + 1
|
|
378
|
+
} : {
|
|
379
|
+
ok: true,
|
|
380
|
+
row: advance(row, at, {
|
|
381
|
+
state: "skipped",
|
|
382
|
+
reason: event.failure.code,
|
|
383
|
+
failure: event.failure,
|
|
384
|
+
attempts: row.attempts + 1
|
|
385
|
+
}),
|
|
386
|
+
version: expectedVersion + 1
|
|
387
|
+
};
|
|
388
|
+
if (event.type === "superseded") return {
|
|
389
|
+
ok: true,
|
|
390
|
+
row: null,
|
|
391
|
+
version: expectedVersion + 1
|
|
392
|
+
};
|
|
393
|
+
return bad$1("illegal-transition", "approved/" + event.type);
|
|
394
|
+
case "fuse":
|
|
395
|
+
if (event.type === "staged") return {
|
|
396
|
+
ok: true,
|
|
397
|
+
row: advance(row, at, { staging: event.ref === void 0 ? { kind: "absent" } : {
|
|
398
|
+
kind: "present",
|
|
399
|
+
ref: event.ref,
|
|
400
|
+
expiresAt: event.expiresAt ?? Date.now() + 7 * 864e5
|
|
401
|
+
} }),
|
|
402
|
+
version: expectedVersion + 1
|
|
403
|
+
};
|
|
404
|
+
if (event.type === "committed") return {
|
|
405
|
+
ok: true,
|
|
406
|
+
row: null,
|
|
407
|
+
version: expectedVersion + 1
|
|
408
|
+
};
|
|
409
|
+
if (event.type === "skipped") return {
|
|
410
|
+
ok: true,
|
|
411
|
+
row: advance(row, at, {
|
|
412
|
+
state: "skipped",
|
|
413
|
+
reason: event.reason
|
|
414
|
+
}),
|
|
415
|
+
version: expectedVersion + 1
|
|
416
|
+
};
|
|
417
|
+
if (event.type === "failed") return event.failure.retryable ? {
|
|
418
|
+
ok: true,
|
|
419
|
+
row: advance(row, at, {
|
|
420
|
+
state: "pending",
|
|
421
|
+
failure: event.failure,
|
|
422
|
+
attempts: row.attempts + 1
|
|
423
|
+
}),
|
|
424
|
+
version: expectedVersion + 1
|
|
425
|
+
} : {
|
|
426
|
+
ok: true,
|
|
427
|
+
row: advance(row, at, {
|
|
428
|
+
state: "skipped",
|
|
429
|
+
reason: event.failure.code,
|
|
430
|
+
failure: event.failure,
|
|
431
|
+
attempts: row.attempts + 1
|
|
432
|
+
}),
|
|
433
|
+
version: expectedVersion + 1
|
|
434
|
+
};
|
|
435
|
+
if (event.type === "superseded") return {
|
|
436
|
+
ok: true,
|
|
437
|
+
row: null,
|
|
438
|
+
version: expectedVersion + 1
|
|
439
|
+
};
|
|
440
|
+
return bad$1("illegal-transition", "fuse/" + event.type);
|
|
441
|
+
case "skipped":
|
|
442
|
+
if (event.type === "restored" || event.type === "superseded") return {
|
|
443
|
+
ok: true,
|
|
444
|
+
row: null,
|
|
445
|
+
version: expectedVersion + 1
|
|
446
|
+
};
|
|
447
|
+
return bad$1("illegal-transition", "skipped is terminal");
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
function object(value) {
|
|
451
|
+
return value !== null && typeof value === "object";
|
|
452
|
+
}
|
|
453
|
+
function string(value) {
|
|
454
|
+
return typeof value === "string" && value.length > 0;
|
|
455
|
+
}
|
|
456
|
+
function number(value) {
|
|
457
|
+
return typeof value === "number" && Number.isFinite(value);
|
|
458
|
+
}
|
|
459
|
+
/** Runtime validation for a persisted v3 row. */
|
|
460
|
+
function isV3Row(value) {
|
|
461
|
+
if (!object(value)) return false;
|
|
462
|
+
if (!string(value.id) || !string(value.pairKey) || !string(value.a) || !string(value.b) || !number(value.version)) return false;
|
|
463
|
+
if (!string(value.createdAt) || !string(value.updatedAt)) return false;
|
|
464
|
+
if (value.origin !== "agent" && value.origin !== "human" && value.origin !== "system") return false;
|
|
465
|
+
if (value.failure !== void 0 && !object(value.failure)) return false;
|
|
466
|
+
if (!number(value.attempts)) return false;
|
|
467
|
+
switch (value.state) {
|
|
468
|
+
case "verify": return value.verifySubState === "candidate" || value.verifySubState === "screened" || value.verifySubState === "unverified";
|
|
469
|
+
case "pending": return string(value.target) && object(value.decision);
|
|
470
|
+
case "approved": return string(value.target) && object(value.decision);
|
|
471
|
+
case "fuse":
|
|
472
|
+
if (!string(value.target) || !object(value.decision) || !object(value.staging)) return false;
|
|
473
|
+
if (value.staging.kind === "present") return string(value.staging.ref) && number(value.staging.expiresAt);
|
|
474
|
+
if (value.staging.kind === "expired") return number(value.staging.expiresAt);
|
|
475
|
+
return value.staging.kind === "absent";
|
|
476
|
+
case "skipped": return string(value.reason);
|
|
477
|
+
default: return false;
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
//#endregion
|
|
481
|
+
//#region lib/types/domain/row-v3-bridge.js
|
|
482
|
+
function v3ToLegacy(row) {
|
|
483
|
+
const common = {
|
|
484
|
+
id: row.id,
|
|
485
|
+
pairKey: row.pairKey,
|
|
486
|
+
a: row.a,
|
|
487
|
+
b: row.b,
|
|
488
|
+
state: row.state,
|
|
489
|
+
queue: row.state === "verify" ? "verify" : row.state === "pending" ? "pending" : row.state === "skipped" ? "skipped" : "fuse",
|
|
490
|
+
version: row.version,
|
|
491
|
+
origin: row.origin,
|
|
492
|
+
...row.runId === void 0 ? {} : { runId: row.runId },
|
|
493
|
+
...row.failure === void 0 ? {} : { failure: row.failure },
|
|
494
|
+
createdAt: row.createdAt,
|
|
495
|
+
updatedAt: row.updatedAt
|
|
496
|
+
};
|
|
497
|
+
if (row.state === "verify") return {
|
|
498
|
+
...common,
|
|
499
|
+
...row.sourceRevision === void 0 ? {} : { sourceRevision: row.sourceRevision },
|
|
500
|
+
attempts: row.attempts
|
|
501
|
+
};
|
|
502
|
+
if (row.state === "skipped") return {
|
|
503
|
+
...common,
|
|
504
|
+
reason: row.reason,
|
|
505
|
+
queue: "skipped",
|
|
506
|
+
...row.expiredAt === void 0 ? {} : { expired: "target-taken" },
|
|
507
|
+
attempts: row.attempts
|
|
508
|
+
};
|
|
509
|
+
const target = {
|
|
510
|
+
requested: row.target,
|
|
511
|
+
...row.state === "fuse" ? { actual: row.target } : {}
|
|
512
|
+
};
|
|
513
|
+
if (row.state === "fuse") return {
|
|
514
|
+
...common,
|
|
515
|
+
target,
|
|
516
|
+
decision: row.decision,
|
|
517
|
+
attempts: row.attempts,
|
|
518
|
+
...row.sourceRevision === void 0 ? {} : { sourceRevision: row.sourceRevision },
|
|
519
|
+
...row.runId === void 0 ? {} : { runId: row.runId },
|
|
520
|
+
...row.staging.kind === "present" ? {
|
|
521
|
+
stagingRef: row.staging.ref,
|
|
522
|
+
stagingExpiresAt: row.staging.expiresAt
|
|
523
|
+
} : row.staging.kind === "expired" ? {
|
|
524
|
+
stagingExpiresAt: row.staging.expiresAt,
|
|
525
|
+
...row.staging.ref === void 0 ? {} : { stagingRef: row.staging.ref }
|
|
526
|
+
} : {}
|
|
527
|
+
};
|
|
528
|
+
return {
|
|
529
|
+
...common,
|
|
530
|
+
target,
|
|
531
|
+
decision: row.decision,
|
|
532
|
+
attempts: row.attempts,
|
|
533
|
+
...row.sourceRevision === void 0 ? {} : { sourceRevision: row.sourceRevision },
|
|
534
|
+
...row.state === "pending" && row.targetRevision !== void 0 ? { targetRevision: row.targetRevision } : {},
|
|
535
|
+
...row.state === "pending" && row.expired !== void 0 ? { expired: row.expired } : {},
|
|
536
|
+
...row.state === "approved" && row.runId !== void 0 ? { runId: row.runId } : {}
|
|
537
|
+
};
|
|
538
|
+
}
|
|
539
|
+
function legacyToV3(row) {
|
|
540
|
+
const base = {
|
|
541
|
+
id: row.id,
|
|
542
|
+
pairKey: row.pairKey,
|
|
543
|
+
a: row.a,
|
|
544
|
+
b: row.b,
|
|
545
|
+
version: row.version ?? 0,
|
|
546
|
+
createdAt: row.createdAt,
|
|
547
|
+
updatedAt: row.updatedAt,
|
|
548
|
+
origin: row.origin ?? "system",
|
|
549
|
+
...row.runId === void 0 ? {} : { runId: row.runId },
|
|
550
|
+
...row.failure === void 0 ? {} : { failure: row.failure }
|
|
551
|
+
};
|
|
552
|
+
if (row.state === "verify") return {
|
|
553
|
+
...base,
|
|
554
|
+
state: "verify",
|
|
555
|
+
verifySubState: "unverified",
|
|
556
|
+
attempts: row.attempts ?? 0,
|
|
557
|
+
...row.sourceRevision === void 0 ? {} : { sourceRevision: row.sourceRevision }
|
|
558
|
+
};
|
|
559
|
+
if (row.state === "skipped") return {
|
|
560
|
+
...base,
|
|
561
|
+
state: "skipped",
|
|
562
|
+
reason: row.reason ?? "skipped",
|
|
563
|
+
attempts: row.attempts ?? 0
|
|
564
|
+
};
|
|
565
|
+
const targetName = row.target?.requested ?? row.a;
|
|
566
|
+
const decision = row.decision ?? {
|
|
567
|
+
shouldMerge: false,
|
|
568
|
+
mode: "keep",
|
|
569
|
+
target: targetName,
|
|
570
|
+
sources: [row.b],
|
|
571
|
+
rationale: "legacy row without decision"
|
|
572
|
+
};
|
|
573
|
+
if (row.state === "pending") return {
|
|
574
|
+
...base,
|
|
575
|
+
state: "pending",
|
|
576
|
+
target: targetName,
|
|
577
|
+
decision,
|
|
578
|
+
attempts: row.attempts ?? 0,
|
|
579
|
+
...row.sourceRevision === void 0 ? {} : { sourceRevision: row.sourceRevision },
|
|
580
|
+
...row.targetRevision === void 0 ? {} : { targetRevision: row.targetRevision },
|
|
581
|
+
...row.expired === void 0 ? {} : { expired: row.expired }
|
|
582
|
+
};
|
|
583
|
+
if (row.state === "approved") return {
|
|
584
|
+
...base,
|
|
585
|
+
state: "approved",
|
|
586
|
+
target: targetName,
|
|
587
|
+
decision,
|
|
588
|
+
attempts: row.attempts ?? 0,
|
|
589
|
+
...row.sourceRevision === void 0 ? {} : { sourceRevision: row.sourceRevision },
|
|
590
|
+
...row.runId === void 0 ? {} : { runId: row.runId }
|
|
591
|
+
};
|
|
592
|
+
const staging = row.stagingRef !== void 0 ? row.stagingExpiresAt !== void 0 ? {
|
|
593
|
+
kind: "present",
|
|
594
|
+
ref: row.stagingRef,
|
|
595
|
+
expiresAt: row.stagingExpiresAt
|
|
596
|
+
} : {
|
|
597
|
+
kind: "expired",
|
|
598
|
+
expiresAt: 0
|
|
599
|
+
} : { kind: "absent" };
|
|
600
|
+
return {
|
|
601
|
+
...base,
|
|
602
|
+
state: "fuse",
|
|
603
|
+
target: targetName,
|
|
604
|
+
decision,
|
|
605
|
+
attempts: row.attempts ?? 0,
|
|
606
|
+
...row.sourceRevision === void 0 ? {} : { sourceRevision: row.sourceRevision },
|
|
607
|
+
...row.runId === void 0 ? {} : { runId: row.runId },
|
|
608
|
+
staging
|
|
609
|
+
};
|
|
610
|
+
}
|
|
611
|
+
//#endregion
|
|
221
612
|
//#region lib/types/domain/ledger.js
|
|
222
613
|
/** Pure v2 ledger normalization/projection. No IO. */
|
|
223
614
|
function isRow(value) {
|
|
@@ -246,7 +637,7 @@ function parseLedger(raw) {
|
|
|
246
637
|
};
|
|
247
638
|
}
|
|
248
639
|
const body = parsed;
|
|
249
|
-
if (typeof body.schemaVersion === "number" && body.schemaVersion >
|
|
640
|
+
if (typeof body.schemaVersion === "number" && body.schemaVersion > 3) return {
|
|
250
641
|
ok: false,
|
|
251
642
|
ledger: {
|
|
252
643
|
schemaVersion: 2,
|
|
@@ -262,6 +653,23 @@ function parseLedger(raw) {
|
|
|
262
653
|
},
|
|
263
654
|
note: "rows missing"
|
|
264
655
|
};
|
|
656
|
+
if (body.schemaVersion === 3) {
|
|
657
|
+
if (!body.rows.every(isV3Row)) return {
|
|
658
|
+
ok: false,
|
|
659
|
+
ledger: {
|
|
660
|
+
schemaVersion: 2,
|
|
661
|
+
rows: []
|
|
662
|
+
},
|
|
663
|
+
note: body.rows.some((row) => row !== null && typeof row === "object" && "pairKey" in row && "version" in row) ? "merge-state-needs-repair" : "merge-state-forward-only"
|
|
664
|
+
};
|
|
665
|
+
return {
|
|
666
|
+
ok: true,
|
|
667
|
+
ledger: {
|
|
668
|
+
schemaVersion: 2,
|
|
669
|
+
rows: body.rows.map(v3ToLegacy)
|
|
670
|
+
}
|
|
671
|
+
};
|
|
672
|
+
}
|
|
265
673
|
const rows = [];
|
|
266
674
|
let skipped = 0;
|
|
267
675
|
for (const value of body.rows) {
|
|
@@ -292,6 +700,80 @@ function parseLedger(raw) {
|
|
|
292
700
|
};
|
|
293
701
|
}
|
|
294
702
|
//#endregion
|
|
703
|
+
//#region lib/types/domain/row-tx.js
|
|
704
|
+
/** G3-5 RowTx pure write API: only createVerify/createSkipped/transition. */
|
|
705
|
+
function bad(code, message) {
|
|
706
|
+
return {
|
|
707
|
+
ok: false,
|
|
708
|
+
code,
|
|
709
|
+
message
|
|
710
|
+
};
|
|
711
|
+
}
|
|
712
|
+
function applyRowWrite(rows, write, at) {
|
|
713
|
+
if (write.kind === "transition") {
|
|
714
|
+
const index = rows.findIndex((row) => row.id === write.id);
|
|
715
|
+
if (index < 0) return bad("row-not-found", "row not found: " + write.id);
|
|
716
|
+
const current = rows[index];
|
|
717
|
+
const moved = transitionV3(current, write.event, write.expectedVersion, at);
|
|
718
|
+
if (!moved.ok) return bad(moved.code, moved.message);
|
|
719
|
+
if (moved.row === null) return {
|
|
720
|
+
ok: true,
|
|
721
|
+
rows: rows.filter((_, atIndex) => atIndex !== index),
|
|
722
|
+
row: null
|
|
723
|
+
};
|
|
724
|
+
const next = [...rows];
|
|
725
|
+
next[index] = moved.row;
|
|
726
|
+
return {
|
|
727
|
+
ok: true,
|
|
728
|
+
rows: next,
|
|
729
|
+
row: moved.row
|
|
730
|
+
};
|
|
731
|
+
}
|
|
732
|
+
if (write.row.version !== 0) return bad("row-invalid", "create requires version 0");
|
|
733
|
+
if (write.kind === "createVerify" && (write.row.state !== "verify" || write.row.verifySubState !== "unverified")) return bad("row-invalid", "createVerify requires verify/unverified");
|
|
734
|
+
if (write.kind === "createSkipped" && write.row.state !== "skipped") return bad("row-invalid", "createSkipped requires skipped");
|
|
735
|
+
if (!isV3Row(write.row)) return bad("row-invalid", "created row is invalid");
|
|
736
|
+
if (rows.some((row) => row.id === write.row.id)) return bad("row-duplicate", "duplicate row id: " + write.row.id);
|
|
737
|
+
return {
|
|
738
|
+
ok: true,
|
|
739
|
+
rows: [...rows, write.row],
|
|
740
|
+
row: write.row
|
|
741
|
+
};
|
|
742
|
+
}
|
|
743
|
+
//#endregion
|
|
744
|
+
//#region lib/types/domain/staging-index.js
|
|
745
|
+
/** Durable staging index for v3 pending/approved rows (rowId-keyed). */
|
|
746
|
+
function stagingIndexPath(home, rowId) {
|
|
747
|
+
return join(home, "merge", "staging-index", encodeURIComponent(rowId) + ".json");
|
|
748
|
+
}
|
|
749
|
+
async function readStagingIndex(io, home, rowId, now = Date.now()) {
|
|
750
|
+
let raw;
|
|
751
|
+
try {
|
|
752
|
+
raw = await io.readText(stagingIndexPath(home, rowId));
|
|
753
|
+
} catch {
|
|
754
|
+
return null;
|
|
755
|
+
}
|
|
756
|
+
if (raw === null) return null;
|
|
757
|
+
try {
|
|
758
|
+
const entry = JSON.parse(raw);
|
|
759
|
+
if (entry.rowId !== rowId || typeof entry.ref !== "string" || typeof entry.expiresAt !== "number") return null;
|
|
760
|
+
if (entry.expiresAt <= now) return null;
|
|
761
|
+
return entry;
|
|
762
|
+
} catch {
|
|
763
|
+
return null;
|
|
764
|
+
}
|
|
765
|
+
}
|
|
766
|
+
async function writeStagingIndex(io, home, entry) {
|
|
767
|
+
const text = JSON.stringify(entry, null, 2) + "\n";
|
|
768
|
+
if (io.transact !== void 0) await io.transact(stagingIndexPath(home, entry.rowId), () => text);
|
|
769
|
+
else await io.writeText(stagingIndexPath(home, entry.rowId), text);
|
|
770
|
+
}
|
|
771
|
+
async function deleteStagingIndex(io, home, rowId) {
|
|
772
|
+
try {
|
|
773
|
+
await io.remove(stagingIndexPath(home, rowId));
|
|
774
|
+
} catch {}
|
|
775
|
+
}
|
|
776
|
+
//#endregion
|
|
295
777
|
//#region lib/types/state.js
|
|
296
778
|
/**
|
|
297
779
|
* The one merge-state ledger: <home>/merge/merge-state.json.
|
|
@@ -326,7 +808,7 @@ async function loadMergeState(io, home) {
|
|
|
326
808
|
return {
|
|
327
809
|
ok: false,
|
|
328
810
|
state: {
|
|
329
|
-
schemaVersion:
|
|
811
|
+
schemaVersion: 3,
|
|
330
812
|
rows: []
|
|
331
813
|
}
|
|
332
814
|
};
|
|
@@ -335,21 +817,81 @@ async function loadMergeState(io, home) {
|
|
|
335
817
|
return {
|
|
336
818
|
ok: parsed.ok,
|
|
337
819
|
state: {
|
|
338
|
-
schemaVersion:
|
|
820
|
+
schemaVersion: 3,
|
|
339
821
|
rows: parsed.rows
|
|
340
822
|
},
|
|
341
823
|
...parsed.note === void 0 ? {} : { note: parsed.note }
|
|
342
824
|
};
|
|
343
825
|
}
|
|
826
|
+
/** Schema probe for doctor; the only other reader of mergeStatePath. */
|
|
827
|
+
async function loadStateSchema(io, home) {
|
|
828
|
+
try {
|
|
829
|
+
const raw = await io.readText(mergeStatePath(home));
|
|
830
|
+
if (raw === null) return null;
|
|
831
|
+
const parsed = JSON.parse(raw);
|
|
832
|
+
return typeof parsed.schemaVersion === "number" ? parsed.schemaVersion : null;
|
|
833
|
+
} catch {
|
|
834
|
+
return null;
|
|
835
|
+
}
|
|
836
|
+
}
|
|
837
|
+
/** G3-5 runtime write API: only createVerify/createSkipped/transition. v2 writes are rejected. */
|
|
838
|
+
async function transactRows(io, home, fn) {
|
|
839
|
+
let result;
|
|
840
|
+
await transactIo(io, mergeStatePath(home), (current) => {
|
|
841
|
+
let rows = [];
|
|
842
|
+
if (current !== null) {
|
|
843
|
+
let parsed;
|
|
844
|
+
try {
|
|
845
|
+
parsed = JSON.parse(current);
|
|
846
|
+
} catch {
|
|
847
|
+
throw new Error("merge-state-needs-repair");
|
|
848
|
+
}
|
|
849
|
+
const body = parsed;
|
|
850
|
+
if (body.schemaVersion !== 3 || !Array.isArray(body.rows) || !body.rows.every(isV3Row)) throw new Error(body.schemaVersion === 2 ? "merge-state-migration-required" : "merge-state-needs-repair");
|
|
851
|
+
rows = body.rows;
|
|
852
|
+
}
|
|
853
|
+
const apply = (write) => {
|
|
854
|
+
const next = applyRowWrite(rows, write, (/* @__PURE__ */ new Date()).toISOString());
|
|
855
|
+
if (next.ok) rows = next.rows;
|
|
856
|
+
return next;
|
|
857
|
+
};
|
|
858
|
+
result = fn({
|
|
859
|
+
rows: () => rows,
|
|
860
|
+
createVerify: (row) => apply({
|
|
861
|
+
kind: "createVerify",
|
|
862
|
+
row
|
|
863
|
+
}),
|
|
864
|
+
createSkipped: (row) => apply({
|
|
865
|
+
kind: "createSkipped",
|
|
866
|
+
row
|
|
867
|
+
}),
|
|
868
|
+
transition: (id, event, expectedVersion) => apply({
|
|
869
|
+
kind: "transition",
|
|
870
|
+
id,
|
|
871
|
+
event,
|
|
872
|
+
expectedVersion
|
|
873
|
+
})
|
|
874
|
+
});
|
|
875
|
+
return JSON.stringify({
|
|
876
|
+
schemaVersion: 3,
|
|
877
|
+
rows
|
|
878
|
+
}, null, 2);
|
|
879
|
+
});
|
|
880
|
+
return result;
|
|
881
|
+
}
|
|
344
882
|
/** The ONE write point. The callback returns the whole row set, not a delta. */
|
|
345
883
|
async function mutateMergeState(io, home, now, fn) {
|
|
346
884
|
let result;
|
|
347
885
|
await transactIo(io, mergeStatePath(home), (current) => {
|
|
348
|
-
const
|
|
886
|
+
const parsed = parseState(current);
|
|
887
|
+
if (!parsed.ok || parsed.note === "merge-state-forward-only") throw new Error(parsed.note ?? "merge-state-invalid");
|
|
888
|
+
const out = fn(parsed.rows);
|
|
349
889
|
result = out.result;
|
|
890
|
+
const rows = out.rows.map((row) => legacyToV3(row));
|
|
891
|
+
if (rows.some((row) => row === null)) throw new Error("merge-state-v3-row-invalid");
|
|
350
892
|
return JSON.stringify({
|
|
351
|
-
schemaVersion:
|
|
352
|
-
rows
|
|
893
|
+
schemaVersion: 3,
|
|
894
|
+
rows
|
|
353
895
|
}, null, 2);
|
|
354
896
|
});
|
|
355
897
|
return result;
|
|
@@ -423,6 +965,19 @@ async function moveRow(io, home, now, id, patch) {
|
|
|
423
965
|
};
|
|
424
966
|
});
|
|
425
967
|
}
|
|
968
|
+
/** G3-5 v3-aware drop: only terminal events. */
|
|
969
|
+
async function dropRowV3(io, home, id, event, unitId = "unknown") {
|
|
970
|
+
const dropped = await transactRows(io, home, (tx) => {
|
|
971
|
+
const row = tx.rows().find((item) => item.id === id);
|
|
972
|
+
if (row === void 0) return false;
|
|
973
|
+
return tx.transition(id, event === "committed" ? {
|
|
974
|
+
type: "committed",
|
|
975
|
+
unitId
|
|
976
|
+
} : { type: event }, row.version).ok;
|
|
977
|
+
});
|
|
978
|
+
if (dropped) await deleteStagingIndex(io, home, id);
|
|
979
|
+
return dropped;
|
|
980
|
+
}
|
|
426
981
|
async function dropRow(io, home, now, id) {
|
|
427
982
|
return await mutateMergeState(io, home, now, (rows) => {
|
|
428
983
|
const next = rows.filter((item) => item.id !== id);
|
|
@@ -594,20 +1149,6 @@ async function cleanupExpiredStaging(io, home, now, keep = /* @__PURE__ */ new S
|
|
|
594
1149
|
};
|
|
595
1150
|
}
|
|
596
1151
|
//#endregion
|
|
597
|
-
//#region lib/types/domain/commit-journal.js
|
|
598
|
-
/** What a restart may do with the last durable phase. */
|
|
599
|
-
function recoveryFor(journal) {
|
|
600
|
-
switch (journal.phase) {
|
|
601
|
-
case "prepared": return "retry-commit";
|
|
602
|
-
case "target-written": return "rollback-target";
|
|
603
|
-
case "sources-archived": return "rollback-sources";
|
|
604
|
-
case "committed":
|
|
605
|
-
case "rolled-back":
|
|
606
|
-
case "failed": return "none";
|
|
607
|
-
default: return "refuse";
|
|
608
|
-
}
|
|
609
|
-
}
|
|
610
|
-
//#endregion
|
|
611
1152
|
//#region lib/types/domain/commit-journal-store.js
|
|
612
1153
|
/** Durable commit journal: <home>/merge/journals/<run>.json. */
|
|
613
1154
|
function journalPath(home, runId) {
|
|
@@ -615,9 +1156,20 @@ function journalPath(home, runId) {
|
|
|
615
1156
|
}
|
|
616
1157
|
async function writeJournal(io, home, journal) {
|
|
617
1158
|
const path = journalPath(home, journal.runId);
|
|
618
|
-
|
|
1159
|
+
const content = JSON.stringify(journal, null, 2);
|
|
1160
|
+
if (io.transact !== void 0) await io.transact(path, () => content);
|
|
1161
|
+
else await io.writeText(path, content);
|
|
619
1162
|
return path;
|
|
620
1163
|
}
|
|
1164
|
+
async function readJournal(io, home, runId) {
|
|
1165
|
+
const raw = await io.readText(journalPath(home, runId));
|
|
1166
|
+
if (raw === null) return null;
|
|
1167
|
+
try {
|
|
1168
|
+
return JSON.parse(raw);
|
|
1169
|
+
} catch {
|
|
1170
|
+
return null;
|
|
1171
|
+
}
|
|
1172
|
+
}
|
|
621
1173
|
//#endregion
|
|
622
1174
|
//#region lib/types/commit-snapshot.js
|
|
623
1175
|
/** Pre-commit snapshot: enough to restore the target and archived sources after a crash. */
|
|
@@ -652,32 +1204,9 @@ async function writeCommitSnapshot(io, home, runId, plan, now, port) {
|
|
|
652
1204
|
await io.writeText(snapshotPath(home, runId), JSON.stringify(snapshot, null, 2));
|
|
653
1205
|
return snapshot;
|
|
654
1206
|
}
|
|
655
|
-
async function readCommitSnapshot(io, home, runId) {
|
|
656
|
-
const raw = await io.readText(snapshotPath(home, runId));
|
|
657
|
-
if (raw === null) return null;
|
|
658
|
-
try {
|
|
659
|
-
return JSON.parse(raw);
|
|
660
|
-
} catch {
|
|
661
|
-
return null;
|
|
662
|
-
}
|
|
663
|
-
}
|
|
664
1207
|
function isSafeSkillName(name) {
|
|
665
1208
|
return /^[A-Za-z0-9][A-Za-z0-9._-]*$/.test(name) && name !== "." && name !== "..";
|
|
666
1209
|
}
|
|
667
|
-
function skillFilePath(root, name, path) {
|
|
668
|
-
if (!isSafeSkillName(name) || path.startsWith("/") || path.split("/").includes("..")) throw new Error("unsafe skill file: " + name + "/" + path);
|
|
669
|
-
const base = resolve(root, name);
|
|
670
|
-
const file = resolve(base, path);
|
|
671
|
-
if (!file.startsWith(base + sep)) throw new Error("skill file escapes skill: " + name + "/" + path);
|
|
672
|
-
return file;
|
|
673
|
-
}
|
|
674
|
-
function skillBodyPath(root, name) {
|
|
675
|
-
if (!isSafeSkillName(name)) throw new Error("unsafe skill name: " + name);
|
|
676
|
-
const base = resolve(root);
|
|
677
|
-
const path = resolve(base, name, "SKILL.md");
|
|
678
|
-
if (!path.startsWith(base + sep)) throw new Error("skill path escapes root: " + name);
|
|
679
|
-
return path;
|
|
680
|
-
}
|
|
681
1210
|
//#endregion
|
|
682
1211
|
//#region lib/types/commit-receipt.js
|
|
683
1212
|
/**
|
|
@@ -755,448 +1284,488 @@ async function readCommitReceipt(io, home, runId, target) {
|
|
|
755
1284
|
}
|
|
756
1285
|
//#endregion
|
|
757
1286
|
//#region lib/types/commit-recovery.js
|
|
758
|
-
/** Startup replay for non-terminal commit journals. */
|
|
759
|
-
async function recoverCommitJournals(io, home, now,
|
|
1287
|
+
/** Startup replay for non-terminal commit journals: report only, never write skills. */
|
|
1288
|
+
async function recoverCommitJournals(io, home, now, isActive) {
|
|
760
1289
|
const dir = join(home, "merge", "journals");
|
|
761
|
-
if (!await io.exists(dir)) return [];
|
|
762
|
-
const files = (await io.list(dir)).filter((name) => name.endsWith(".json"));
|
|
763
1290
|
const reports = [];
|
|
1291
|
+
let files = [];
|
|
1292
|
+
if (await io.exists(dir)) try {
|
|
1293
|
+
files = (await io.list(dir)).filter((name) => name.endsWith(".json"));
|
|
1294
|
+
} catch {
|
|
1295
|
+
reports.push({
|
|
1296
|
+
runId: "(list)",
|
|
1297
|
+
phase: "unknown",
|
|
1298
|
+
action: "journal-list-failed",
|
|
1299
|
+
ok: false,
|
|
1300
|
+
journalPath: dir
|
|
1301
|
+
});
|
|
1302
|
+
}
|
|
764
1303
|
for (const file of files) {
|
|
765
|
-
|
|
766
|
-
try {
|
|
767
|
-
journal = JSON.parse(await io.readText(join(dir, file)) ?? "");
|
|
768
|
-
} catch {
|
|
769
|
-
continue;
|
|
770
|
-
}
|
|
771
|
-
if (isActive?.(journal.runId) === true) {
|
|
772
|
-
reports.push({
|
|
773
|
-
runId: journal.runId,
|
|
774
|
-
phase: journal.phase,
|
|
775
|
-
action: "skipped-active-run",
|
|
776
|
-
ok: true
|
|
777
|
-
});
|
|
778
|
-
continue;
|
|
779
|
-
}
|
|
780
|
-
const recovery = recoveryFor(journal);
|
|
781
|
-
if (recovery === "none") continue;
|
|
782
|
-
if (recovery === "refuse") {
|
|
783
|
-
reports.push({
|
|
784
|
-
runId: journal.runId,
|
|
785
|
-
phase: journal.phase,
|
|
786
|
-
action: "unknown-phase-refused",
|
|
787
|
-
ok: false
|
|
788
|
-
});
|
|
789
|
-
continue;
|
|
790
|
-
}
|
|
791
|
-
let receipt = null;
|
|
1304
|
+
const path = join(dir, file);
|
|
792
1305
|
try {
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
1306
|
+
let journal;
|
|
1307
|
+
try {
|
|
1308
|
+
journal = JSON.parse(await io.readText(path) ?? "");
|
|
1309
|
+
} catch {
|
|
1310
|
+
reports.push({
|
|
1311
|
+
runId: file.replace(/\.json$/, ""),
|
|
1312
|
+
phase: "unknown",
|
|
1313
|
+
action: "journal-unreadable",
|
|
1314
|
+
ok: false,
|
|
1315
|
+
journalPath: path
|
|
1316
|
+
});
|
|
1317
|
+
continue;
|
|
1318
|
+
}
|
|
1319
|
+
if (isActive?.(journal.runId) === true) {
|
|
1320
|
+
reports.push({
|
|
1321
|
+
runId: journal.runId,
|
|
1322
|
+
phase: journal.phase,
|
|
1323
|
+
action: "skipped-active-run",
|
|
1324
|
+
ok: true,
|
|
1325
|
+
target: journal.target,
|
|
1326
|
+
sources: journal.sources,
|
|
1327
|
+
journalPath: path
|
|
1328
|
+
});
|
|
1329
|
+
continue;
|
|
1330
|
+
}
|
|
1331
|
+
let receipt = null;
|
|
1332
|
+
try {
|
|
1333
|
+
receipt = await readCommitReceipt(io, home, journal.runId, journal.target);
|
|
1334
|
+
} catch {
|
|
1335
|
+
reports.push({
|
|
1336
|
+
runId: journal.runId,
|
|
1337
|
+
phase: journal.phase,
|
|
1338
|
+
action: "receipt-read-failed",
|
|
1339
|
+
ok: false,
|
|
1340
|
+
target: journal.target,
|
|
1341
|
+
sources: journal.sources,
|
|
1342
|
+
journalPath: path
|
|
1343
|
+
});
|
|
1344
|
+
continue;
|
|
1345
|
+
}
|
|
1346
|
+
if (receipt !== null) {
|
|
1347
|
+
await writeJournal(io, home, {
|
|
1348
|
+
...journal,
|
|
1349
|
+
phase: "committed",
|
|
1350
|
+
unitId: receipt.unitId,
|
|
1351
|
+
updatedAt: new Date(now).toISOString()
|
|
1352
|
+
});
|
|
1353
|
+
reports.push({
|
|
1354
|
+
runId: journal.runId,
|
|
1355
|
+
phase: journal.phase,
|
|
1356
|
+
action: "receipt-committed",
|
|
1357
|
+
ok: true,
|
|
1358
|
+
target: journal.target,
|
|
1359
|
+
sources: journal.sources,
|
|
1360
|
+
journalPath: path,
|
|
1361
|
+
unitId: receipt.unitId
|
|
1362
|
+
});
|
|
1363
|
+
continue;
|
|
1364
|
+
}
|
|
1365
|
+
if (journal.phase === "committed") {
|
|
1366
|
+
reports.push({
|
|
1367
|
+
runId: journal.runId,
|
|
1368
|
+
phase: journal.phase,
|
|
1369
|
+
action: "receipt-missing",
|
|
1370
|
+
ok: false,
|
|
1371
|
+
target: journal.target,
|
|
1372
|
+
sources: journal.sources,
|
|
1373
|
+
journalPath: path
|
|
1374
|
+
});
|
|
1375
|
+
continue;
|
|
1376
|
+
}
|
|
1377
|
+
if (journal.phase === "failed" || journal.phase === "rolled-back") continue;
|
|
810
1378
|
reports.push({
|
|
811
1379
|
runId: journal.runId,
|
|
812
1380
|
phase: journal.phase,
|
|
813
|
-
action: "
|
|
814
|
-
ok:
|
|
1381
|
+
action: "manual-required",
|
|
1382
|
+
ok: false,
|
|
1383
|
+
target: journal.target,
|
|
1384
|
+
sources: journal.sources,
|
|
1385
|
+
journalPath: path,
|
|
1386
|
+
snapshotPath: snapshotPath(home, journal.runId)
|
|
815
1387
|
});
|
|
816
|
-
continue;
|
|
817
|
-
}
|
|
818
|
-
let snapshot;
|
|
819
|
-
try {
|
|
820
|
-
snapshot = await readCommitSnapshot(io, home, journal.runId);
|
|
821
1388
|
} catch (error) {
|
|
822
1389
|
reports.push({
|
|
823
|
-
runId:
|
|
824
|
-
phase:
|
|
825
|
-
action: "
|
|
826
|
-
ok: false
|
|
1390
|
+
runId: file.replace(/\.json$/, ""),
|
|
1391
|
+
phase: "unknown",
|
|
1392
|
+
action: "recovery-error: " + (error instanceof Error ? error.message : String(error)),
|
|
1393
|
+
ok: false,
|
|
1394
|
+
journalPath: path
|
|
827
1395
|
});
|
|
828
|
-
continue;
|
|
829
1396
|
}
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
}
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
1397
|
+
}
|
|
1398
|
+
const unitDir = join(home, "merge", "unit-journals");
|
|
1399
|
+
if (await io.exists(unitDir)) {
|
|
1400
|
+
let unitFiles = [];
|
|
1401
|
+
try {
|
|
1402
|
+
unitFiles = (await io.list(unitDir)).filter((name) => name.endsWith(".json"));
|
|
1403
|
+
} catch {}
|
|
1404
|
+
for (const file of unitFiles) {
|
|
1405
|
+
const path = join(unitDir, file);
|
|
1406
|
+
try {
|
|
1407
|
+
const journal = JSON.parse(await io.readText(path) ?? "");
|
|
1408
|
+
if (await readUnitEvidence(io, home, journal.unitId) !== null) {
|
|
1409
|
+
reports.push({
|
|
1410
|
+
runId: journal.runId,
|
|
1411
|
+
phase: "committed",
|
|
1412
|
+
action: "unit-committed",
|
|
1413
|
+
ok: true,
|
|
1414
|
+
target: journal.target,
|
|
1415
|
+
sources: journal.sources,
|
|
1416
|
+
journalPath: path,
|
|
1417
|
+
unitId: journal.unitId
|
|
1418
|
+
});
|
|
1419
|
+
continue;
|
|
841
1420
|
}
|
|
842
|
-
if (
|
|
1421
|
+
if (journal.phase === "committed") {
|
|
1422
|
+
reports.push({
|
|
1423
|
+
runId: journal.runId,
|
|
1424
|
+
phase: "committed",
|
|
1425
|
+
action: "unit-evidence-missing",
|
|
1426
|
+
ok: false,
|
|
1427
|
+
target: journal.target,
|
|
1428
|
+
sources: journal.sources,
|
|
1429
|
+
journalPath: path,
|
|
1430
|
+
unitId: journal.unitId
|
|
1431
|
+
});
|
|
1432
|
+
continue;
|
|
1433
|
+
}
|
|
1434
|
+
if (journal.phase === "failed" || journal.phase === "rolled-back") continue;
|
|
1435
|
+
reports.push({
|
|
1436
|
+
runId: journal.runId,
|
|
1437
|
+
phase: journal.phase,
|
|
1438
|
+
action: "unit-manual-required",
|
|
1439
|
+
ok: false,
|
|
1440
|
+
target: journal.target,
|
|
1441
|
+
sources: journal.sources,
|
|
1442
|
+
journalPath: path,
|
|
1443
|
+
unitId: journal.unitId
|
|
1444
|
+
});
|
|
1445
|
+
} catch {
|
|
1446
|
+
reports.push({
|
|
1447
|
+
runId: "(unit)",
|
|
1448
|
+
phase: "unknown",
|
|
1449
|
+
action: "unit-journal-unreadable",
|
|
1450
|
+
ok: false,
|
|
1451
|
+
journalPath: path
|
|
1452
|
+
});
|
|
843
1453
|
}
|
|
844
|
-
if (snapshot.create && snapshot.targetBody === null) {
|
|
845
|
-
try {
|
|
846
|
-
await io.remove(skillBodyPath(skillRoot, snapshot.target));
|
|
847
|
-
} catch {}
|
|
848
|
-
try {
|
|
849
|
-
const dir = join(skillRoot, snapshot.target);
|
|
850
|
-
if ((await io.list(dir)).length === 0) await io.remove(dir);
|
|
851
|
-
} catch {}
|
|
852
|
-
} else if (snapshot.targetBody !== null) await io.writeText(skillBodyPath(skillRoot, snapshot.target), snapshot.targetBody);
|
|
853
|
-
const oldPaths = new Set((snapshot.targetSupport ?? []).map((file) => file.path));
|
|
854
|
-
for (const file of snapshot.targetSupport ?? []) await io.writeText(skillFilePath(skillRoot, snapshot.target, file.path), file.content);
|
|
855
|
-
for (const file of snapshot.targetSupportAfter ?? []) if (!oldPaths.has(file.path)) try {
|
|
856
|
-
await io.remove(skillFilePath(skillRoot, snapshot.target, file.path));
|
|
857
|
-
} catch {}
|
|
858
|
-
for (const file of snapshot.sourceSupport ?? []) await io.writeText(skillFilePath(skillRoot, file.name, file.path), file.content);
|
|
859
|
-
action = recovery === "rollback-sources" ? "restored-sources-and-target" : "restored-target";
|
|
860
|
-
} catch (error) {
|
|
861
|
-
action = "restore-failed: " + (error instanceof Error ? error.message : String(error));
|
|
862
|
-
ok = false;
|
|
863
1454
|
}
|
|
864
|
-
if (ok) await writeJournal(io, home, {
|
|
865
|
-
...journal,
|
|
866
|
-
phase: "rolled-back",
|
|
867
|
-
updatedAt: new Date(now).toISOString()
|
|
868
|
-
});
|
|
869
|
-
reports.push({
|
|
870
|
-
runId: journal.runId,
|
|
871
|
-
phase: journal.phase,
|
|
872
|
-
action,
|
|
873
|
-
ok
|
|
874
|
-
});
|
|
875
1455
|
}
|
|
876
|
-
|
|
1456
|
+
try {
|
|
1457
|
+
await io.writeText(join(home, "merge", "recovery.json"), JSON.stringify(reports, null, 2));
|
|
1458
|
+
} catch {}
|
|
877
1459
|
return reports;
|
|
878
1460
|
}
|
|
879
1461
|
//#endregion
|
|
880
|
-
//#region lib/types/
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
function
|
|
1462
|
+
//#region lib/types/command-surface.js
|
|
1463
|
+
const flag = (name, kind = "boolean", valueName) => ({
|
|
1464
|
+
name,
|
|
1465
|
+
kind,
|
|
1466
|
+
...valueName === void 0 ? {} : { valueName }
|
|
1467
|
+
});
|
|
1468
|
+
const pos = (name, kind, required = true) => ({
|
|
1469
|
+
name,
|
|
1470
|
+
kind,
|
|
1471
|
+
required
|
|
1472
|
+
});
|
|
1473
|
+
const action = (id, command, confirm = false) => ({
|
|
1474
|
+
id,
|
|
1475
|
+
command,
|
|
1476
|
+
confirm
|
|
1477
|
+
});
|
|
1478
|
+
function spec(id, words, flags, positional, readOnly, needsRun, rowActions, fallback) {
|
|
897
1479
|
return {
|
|
898
|
-
id
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
...row.origin === void 0 ? {} : { origin: row.origin },
|
|
909
|
-
...row.decision === void 0 ? {} : { decision: row.decision },
|
|
910
|
-
...row.sourceRevision === void 0 ? {} : { sourceRevision: row.sourceRevision },
|
|
911
|
-
...row.targetRevision === void 0 ? {} : { targetRevision: row.targetRevision },
|
|
912
|
-
...row.attempts === void 0 ? {} : { attempts: row.attempts },
|
|
913
|
-
...row.stagingRef === void 0 ? {} : { stagingRef: row.stagingRef },
|
|
914
|
-
...row.stagingExpiresAt === void 0 ? {} : { stagingExpiresAt: row.stagingExpiresAt },
|
|
915
|
-
...row.expired === void 0 ? {} : { expired: row.expired },
|
|
916
|
-
...row.reason === void 0 ? {} : { reason: row.reason },
|
|
917
|
-
...row.target === void 0 ? {} : { target: row.target }
|
|
1480
|
+
id,
|
|
1481
|
+
words,
|
|
1482
|
+
flags,
|
|
1483
|
+
positional,
|
|
1484
|
+
summary: id,
|
|
1485
|
+
readOnly,
|
|
1486
|
+
needsRun,
|
|
1487
|
+
defaults: {},
|
|
1488
|
+
rowActions,
|
|
1489
|
+
fallback
|
|
918
1490
|
};
|
|
919
1491
|
}
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
1492
|
+
const MERGE_COMMAND_SURFACE = {
|
|
1493
|
+
wireVersion: 1,
|
|
1494
|
+
prefix: "/evolution merge",
|
|
1495
|
+
commands: [
|
|
1496
|
+
spec("merge.scan", ["scan"], [], [], false, true, [], "/evolution merge scan"),
|
|
1497
|
+
spec("merge.pair", ["pair"], [], [pos("a", "skill"), pos("b", "skill")], false, true, [action("pair", "/evolution merge pair", true)], "/evolution merge pair <a> <b>"),
|
|
1498
|
+
spec("merge.pair.force", ["pair"], [flag("force")], [pos("a", "skill"), pos("b", "skill")], false, true, [], "/evolution merge pair --force <a> <b>"),
|
|
1499
|
+
spec("merge.status", ["status"], [], [pos("id", "id", false)], true, false, [action("status", "/evolution merge status")], "/evolution merge status"),
|
|
1500
|
+
spec("merge.report", ["report"], [], [pos("id", "id", false)], true, false, [action("report", "/evolution merge report")], "/evolution merge report"),
|
|
1501
|
+
spec("merge.cancel", ["cancel"], [], [pos("id", "id", false)], false, false, [action("cancel", "/evolution merge cancel")], "/evolution merge cancel <id>"),
|
|
1502
|
+
spec("merge.runs", ["runs"], [], [], true, false, [], "/evolution merge runs"),
|
|
1503
|
+
spec("merge.pending", ["pending"], [], [], true, false, [], "/evolution merge pending"),
|
|
1504
|
+
spec("merge.pending.expired", ["pending"], [flag("expired")], [], true, false, [], "/evolution merge pending --expired"),
|
|
1505
|
+
spec("merge.approve", ["approve"], [], [pos("id", "id")], false, true, [action("approve", "/evolution merge approve <id>")], "/evolution merge approve <id>"),
|
|
1506
|
+
spec("merge.reject", ["reject"], [], [pos("id", "id")], false, false, [action("reject", "/evolution merge reject <id>", true)], "/evolution merge reject <id>"),
|
|
1507
|
+
spec("merge.skipped", ["skipped"], [], [], true, false, [], "/evolution merge skipped"),
|
|
1508
|
+
spec("merge.skipped.restore", ["skipped"], [flag("restore", "value", "id")], [], false, false, [action("restore", "/evolution merge skipped --restore <id>", true)], "/evolution merge skipped --restore <id>"),
|
|
1509
|
+
spec("merge.candidates", ["candidates"], [], [], true, false, [], "/evolution merge candidates"),
|
|
1510
|
+
spec("merge.candidates.retry", ["candidates"], [flag("retry", "value", "id")], [], false, true, [action("retry-candidates", "/evolution merge candidates --retry <id>")], "/evolution merge candidates --retry <id>"),
|
|
1511
|
+
spec("merge.resume", ["resume"], [], [pos("id", "id", false)], true, false, [], "/evolution merge resume"),
|
|
1512
|
+
spec("merge.resume.retry", ["resume"], [flag("retry", "value", "id")], [], false, true, [action("retry-resume", "/evolution merge resume --retry <id>")], "/evolution merge resume --retry <id>"),
|
|
1513
|
+
spec("merge.discard", ["discard"], [], [pos("id", "id")], false, false, [action("discard", "/evolution merge discard <id>", true)], "/evolution merge discard <id>"),
|
|
1514
|
+
spec("merge.undo", ["undo"], [flag("run", "value", "id")], [pos("target", "target")], false, false, [], "/evolution merge undo <target> --run <id>")
|
|
1515
|
+
],
|
|
1516
|
+
fallback: [
|
|
1517
|
+
{
|
|
1518
|
+
id: "scan",
|
|
1519
|
+
command: "/evolution merge scan"
|
|
1520
|
+
},
|
|
1521
|
+
{
|
|
1522
|
+
id: "pair",
|
|
1523
|
+
command: "/evolution merge pair <a> <b>"
|
|
1524
|
+
},
|
|
1525
|
+
{
|
|
1526
|
+
id: "candidates",
|
|
1527
|
+
command: "/evolution merge candidates"
|
|
1528
|
+
},
|
|
1529
|
+
{
|
|
1530
|
+
id: "pending",
|
|
1531
|
+
command: "/evolution merge pending"
|
|
1532
|
+
},
|
|
1533
|
+
{
|
|
1534
|
+
id: "skipped",
|
|
1535
|
+
command: "/evolution merge skipped"
|
|
1536
|
+
},
|
|
1537
|
+
{
|
|
1538
|
+
id: "resume",
|
|
1539
|
+
command: "/evolution merge resume"
|
|
1540
|
+
},
|
|
1541
|
+
{
|
|
1542
|
+
id: "status",
|
|
1543
|
+
command: "/evolution merge status"
|
|
1544
|
+
},
|
|
1545
|
+
{
|
|
1546
|
+
id: "report",
|
|
1547
|
+
command: "/evolution merge report"
|
|
1548
|
+
},
|
|
1549
|
+
{
|
|
1550
|
+
id: "doctor",
|
|
1551
|
+
command: "/evolution merge doctor"
|
|
928
1552
|
}
|
|
929
|
-
|
|
930
|
-
}
|
|
931
|
-
function transition(current, event) {
|
|
932
|
-
if (event.type === "candidate") {
|
|
933
|
-
if (current !== void 0) return {
|
|
934
|
-
ok: true,
|
|
935
|
-
row: current
|
|
936
|
-
};
|
|
937
|
-
const key = pairKey(event.a, event.b);
|
|
938
|
-
return {
|
|
939
|
-
ok: true,
|
|
940
|
-
row: {
|
|
941
|
-
id: rowIdOf(key),
|
|
942
|
-
pairKey: key,
|
|
943
|
-
a: event.a,
|
|
944
|
-
b: event.b,
|
|
945
|
-
state: "verify",
|
|
946
|
-
queue: "verify",
|
|
947
|
-
createdAt: event.at,
|
|
948
|
-
updatedAt: event.at
|
|
949
|
-
}
|
|
950
|
-
};
|
|
951
|
-
}
|
|
952
|
-
if (event.type === "source-gone") return {
|
|
953
|
-
ok: true,
|
|
954
|
-
row: null
|
|
955
|
-
};
|
|
956
|
-
if (event.type === "gate-keep") {
|
|
957
|
-
if (current !== void 0) return {
|
|
958
|
-
ok: true,
|
|
959
|
-
row: current
|
|
960
|
-
};
|
|
961
|
-
const key = pairKey(event.a, event.b);
|
|
962
|
-
return {
|
|
963
|
-
ok: true,
|
|
964
|
-
row: {
|
|
965
|
-
id: rowIdOf(key),
|
|
966
|
-
pairKey: key,
|
|
967
|
-
a: event.a,
|
|
968
|
-
b: event.b,
|
|
969
|
-
state: "skipped",
|
|
970
|
-
queue: "skipped",
|
|
971
|
-
origin: "agent",
|
|
972
|
-
createdAt: event.at,
|
|
973
|
-
updatedAt: event.at
|
|
974
|
-
}
|
|
975
|
-
};
|
|
976
|
-
}
|
|
977
|
-
if (current === void 0) return invalid(event, void 0);
|
|
978
|
-
switch (event.type) {
|
|
979
|
-
case "round1-keep": return current.state === "verify" ? next(current, event.at, "skipped", { origin: "agent" }) : invalid(event, current.state);
|
|
980
|
-
case "round1-merge":
|
|
981
|
-
if (current.state !== "verify") return invalid(event, current.state);
|
|
982
|
-
return event.approval === "on" ? next(current, event.at, "pending", {
|
|
983
|
-
...event.decision === void 0 ? {} : { decision: event.decision },
|
|
984
|
-
...event.stagingRef === void 0 ? {} : { stagingRef: event.stagingRef },
|
|
985
|
-
...event.stagingExpiresAt === void 0 ? {} : { stagingExpiresAt: event.stagingExpiresAt },
|
|
986
|
-
...event.sourceRevision === void 0 ? {} : { sourceRevision: event.sourceRevision },
|
|
987
|
-
...event.targetRevision === void 0 ? {} : { targetRevision: event.targetRevision }
|
|
988
|
-
}) : next(current, event.at, "approved", {
|
|
989
|
-
...event.decision === void 0 ? {} : { decision: event.decision },
|
|
990
|
-
...event.stagingRef === void 0 ? {} : { stagingRef: event.stagingRef },
|
|
991
|
-
...event.stagingExpiresAt === void 0 ? {} : { stagingExpiresAt: event.stagingExpiresAt },
|
|
992
|
-
...event.sourceRevision === void 0 ? {} : { sourceRevision: event.sourceRevision },
|
|
993
|
-
...event.targetRevision === void 0 ? {} : { targetRevision: event.targetRevision }
|
|
994
|
-
});
|
|
995
|
-
case "round1-fail": return current.state === "verify" ? next(current, event.at, "verify", {
|
|
996
|
-
failure: event.failure,
|
|
997
|
-
attempts: (current.attempts ?? 0) + 1
|
|
998
|
-
}) : invalid(event, current.state);
|
|
999
|
-
case "approve": return current.state === "pending" ? next(current, event.at, "approved", { ...event.runId === void 0 ? {} : { runId: event.runId } }) : invalid(event, current.state);
|
|
1000
|
-
case "reject": return current.state === "pending" ? next(current, event.at, "skipped", { origin: "human" }) : invalid(event, current.state);
|
|
1001
|
-
case "round2-cancel": return current.state === "fuse" || current.state === "approved" ? next(current, event.at, "approved") : invalid(event, current.state);
|
|
1002
|
-
case "round2-fail": return current.state === "fuse" || current.state === "approved" ? next(current, event.at, "approved", {
|
|
1003
|
-
failure: event.failure,
|
|
1004
|
-
attempts: (current.attempts ?? 0) + 1
|
|
1005
|
-
}) : invalid(event, current.state);
|
|
1006
|
-
case "round2-success": return current.state === "fuse" || current.state === "approved" ? {
|
|
1007
|
-
ok: true,
|
|
1008
|
-
row: null
|
|
1009
|
-
} : invalid(event, current.state);
|
|
1010
|
-
case "target-taken":
|
|
1011
|
-
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);
|
|
1012
|
-
case "restore": return current.state === "skipped" ? next(current, event.at, "verify") : invalid(event, current.state);
|
|
1013
|
-
}
|
|
1014
|
-
}
|
|
1015
|
-
//#endregion
|
|
1016
|
-
//#region lib/types/presentation-budget.js
|
|
1017
|
-
/** Tool-result budget: keep the head and tail, mark what was omitted. */
|
|
1018
|
-
function fitToolText(text, max = 8e3) {
|
|
1019
|
-
if (text.length <= max) return text;
|
|
1020
|
-
const marker = "\n... [truncated " + String(text.length - max) + " chars] ...\n";
|
|
1021
|
-
const keep = Math.max(0, max - marker.length);
|
|
1022
|
-
const head = Math.ceil(keep * .7);
|
|
1023
|
-
const tail = keep - head;
|
|
1024
|
-
return text.slice(0, head) + marker + (tail === 0 ? "" : text.slice(-tail));
|
|
1025
|
-
}
|
|
1553
|
+
]
|
|
1554
|
+
};
|
|
1026
1555
|
//#endregion
|
|
1027
|
-
//#region lib/types/
|
|
1028
|
-
|
|
1029
|
-
|
|
1556
|
+
//#region lib/types/facts.js
|
|
1557
|
+
const queueOf$2 = (row) => row.state === "approved" ? "fuse" : row.queue;
|
|
1558
|
+
const isExpired = (row) => row.expired !== void 0;
|
|
1559
|
+
function countsOf(rows) {
|
|
1560
|
+
const active = {
|
|
1030
1561
|
verify: 0,
|
|
1031
1562
|
pending: 0,
|
|
1032
1563
|
fuse: 0,
|
|
1033
1564
|
skipped: 0
|
|
1034
1565
|
};
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1566
|
+
let expired = 0;
|
|
1567
|
+
for (const row of rows) {
|
|
1568
|
+
if (isExpired(row)) {
|
|
1569
|
+
expired += 1;
|
|
1570
|
+
continue;
|
|
1571
|
+
}
|
|
1572
|
+
active[queueOf$2(row)] += 1;
|
|
1573
|
+
}
|
|
1574
|
+
return {
|
|
1575
|
+
active,
|
|
1576
|
+
expired
|
|
1577
|
+
};
|
|
1042
1578
|
}
|
|
1043
|
-
function
|
|
1044
|
-
const
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1579
|
+
function toRunView(record, readAt) {
|
|
1580
|
+
const state = record.mergeStatus ?? record.state;
|
|
1581
|
+
return {
|
|
1582
|
+
wireVersion: 1,
|
|
1583
|
+
readAt,
|
|
1584
|
+
id: record.id,
|
|
1585
|
+
state,
|
|
1586
|
+
command: record.command ?? "scan",
|
|
1587
|
+
...record.stage === void 0 ? {} : { stage: record.stage },
|
|
1588
|
+
progress: record.progress === void 0 ? null : {
|
|
1589
|
+
done: record.progress.done,
|
|
1590
|
+
total: record.progress.total
|
|
1591
|
+
},
|
|
1592
|
+
startedAt: record.startedAt,
|
|
1593
|
+
...record.endedAt === void 0 ? {} : { endedAt: record.endedAt },
|
|
1594
|
+
...record.failure === void 0 ? {} : { failure: {
|
|
1595
|
+
stage: "settle",
|
|
1596
|
+
code: "run-failed",
|
|
1597
|
+
retryable: false,
|
|
1598
|
+
retryFrom: "none",
|
|
1599
|
+
detail: record.failure
|
|
1600
|
+
} },
|
|
1601
|
+
...record.parentRunId === void 0 ? {} : { parentRunId: record.parentRunId },
|
|
1602
|
+
...record.pairKey === void 0 ? {} : { currentPairKey: record.pairKey },
|
|
1603
|
+
actions: [
|
|
1604
|
+
{
|
|
1605
|
+
id: "status",
|
|
1606
|
+
command: "/evolution merge status " + record.id,
|
|
1607
|
+
confirm: false,
|
|
1608
|
+
enabled: true
|
|
1609
|
+
},
|
|
1610
|
+
{
|
|
1611
|
+
id: "report",
|
|
1612
|
+
command: "/evolution merge report " + record.id,
|
|
1613
|
+
confirm: false,
|
|
1614
|
+
enabled: record.resultRef !== void 0
|
|
1615
|
+
},
|
|
1616
|
+
{
|
|
1617
|
+
id: "cancel",
|
|
1618
|
+
command: "/evolution merge cancel " + record.id,
|
|
1619
|
+
confirm: false,
|
|
1620
|
+
enabled: state === "running"
|
|
1621
|
+
}
|
|
1622
|
+
]
|
|
1623
|
+
};
|
|
1048
1624
|
}
|
|
1049
|
-
function
|
|
1050
|
-
const
|
|
1051
|
-
|
|
1052
|
-
|
|
1625
|
+
function buildFacts(input) {
|
|
1626
|
+
const ordered = [...input.runs].sort((a, b) => b.startedAt - a.startedAt);
|
|
1627
|
+
const active = ordered.find((run) => (run.mergeStatus ?? run.state) === "running");
|
|
1628
|
+
const last = ordered.find((run) => (run.mergeStatus ?? run.state) !== "running");
|
|
1629
|
+
const notes = input.notes ?? [];
|
|
1630
|
+
return {
|
|
1631
|
+
wireVersion: 1,
|
|
1632
|
+
readAt: input.readAt,
|
|
1633
|
+
approval: { mergeApproval: input.approval },
|
|
1634
|
+
counts: countsOf(input.rows),
|
|
1635
|
+
active: active === void 0 ? null : toRunView(active, input.readAt),
|
|
1636
|
+
last: last === void 0 ? null : toRunView(last, input.readAt),
|
|
1637
|
+
startCommands: {
|
|
1638
|
+
scan: "/evolution merge scan",
|
|
1639
|
+
pair: "/evolution merge pair <a> <b>"
|
|
1640
|
+
},
|
|
1641
|
+
fallback: MERGE_COMMAND_SURFACE.fallback,
|
|
1642
|
+
notes,
|
|
1643
|
+
degraded: notes.length > 0
|
|
1644
|
+
};
|
|
1053
1645
|
}
|
|
1054
1646
|
//#endregion
|
|
1055
|
-
//#region lib/types/
|
|
1056
|
-
|
|
1647
|
+
//#region lib/types/wire-projection.js
|
|
1648
|
+
const queueOf$1 = (row) => row.state === "approved" ? "fuse" : row.queue;
|
|
1649
|
+
const expiredOf = (row) => row.expired !== void 0;
|
|
1650
|
+
/** Host-owned row actions; client only renders/copies them. */
|
|
1651
|
+
function actionRefs(row) {
|
|
1652
|
+
const base = "/evolution merge ";
|
|
1653
|
+
const disabled = row.expired === void 0 ? void 0 : "expired";
|
|
1654
|
+
const action = (id, confirm = false, enabled = true) => ({
|
|
1655
|
+
id,
|
|
1656
|
+
command: base + id + " " + row.id,
|
|
1657
|
+
confirm,
|
|
1658
|
+
enabled,
|
|
1659
|
+
...enabled ? {} : { disabledReason: disabled ?? "invalid" }
|
|
1660
|
+
});
|
|
1661
|
+
if (row.state === "pending" && row.expired !== void 0) return [{
|
|
1662
|
+
id: "discard",
|
|
1663
|
+
command: "/evolution merge discard " + row.id,
|
|
1664
|
+
confirm: true,
|
|
1665
|
+
enabled: true
|
|
1666
|
+
}, {
|
|
1667
|
+
id: "pair",
|
|
1668
|
+
command: "/evolution merge pair " + row.a + " " + row.b,
|
|
1669
|
+
confirm: true,
|
|
1670
|
+
enabled: true
|
|
1671
|
+
}];
|
|
1672
|
+
if (row.state === "pending") return [
|
|
1673
|
+
action("approve"),
|
|
1674
|
+
action("reject", true),
|
|
1675
|
+
action("discard", true)
|
|
1676
|
+
];
|
|
1677
|
+
if (row.state === "approved" || row.state === "fuse") return [action("discard", true), action("status")];
|
|
1678
|
+
if (row.state === "skipped") return [action("restore", true)];
|
|
1679
|
+
return [action("pair")];
|
|
1680
|
+
}
|
|
1681
|
+
function toRowFact(row, actions = actionRefs(row)) {
|
|
1057
1682
|
return {
|
|
1058
1683
|
id: row.id,
|
|
1059
|
-
|
|
1684
|
+
state: row.state,
|
|
1685
|
+
queue: queueOf$1(row),
|
|
1686
|
+
...row.state === "verify" ? { verifySubState: "queued" } : {},
|
|
1060
1687
|
a: row.a,
|
|
1061
1688
|
b: row.b,
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
...row.
|
|
1689
|
+
...row.target === void 0 ? {} : { target: {
|
|
1690
|
+
requested: row.target.requested,
|
|
1691
|
+
...row.target.actual === void 0 ? {} : { actual: row.target.actual },
|
|
1692
|
+
...row.target.created === void 0 ? {} : { created: row.target.created }
|
|
1693
|
+
} },
|
|
1694
|
+
...row.state === "pending" ? { summary: {
|
|
1695
|
+
a: null,
|
|
1696
|
+
b: null,
|
|
1697
|
+
rationale: row.decision?.rationale ?? null
|
|
1698
|
+
} } : {},
|
|
1699
|
+
...row.reason === void 0 ? {} : { reason: row.reason },
|
|
1700
|
+
...row.state === "skipped" ? { origin: row.origin ?? "system" } : {},
|
|
1073
1701
|
...row.failure === void 0 ? {} : { failure: {
|
|
1074
|
-
stage: row.failure.stage,
|
|
1702
|
+
stage: row.failure.stage === "screening" ? "screening" : row.failure.stage === "decide" ? "decide" : row.failure.stage === "author" ? "author" : row.failure.stage === "validate" ? "validate" : "commit",
|
|
1075
1703
|
code: row.failure.code,
|
|
1076
|
-
|
|
1077
|
-
|
|
1704
|
+
retryable: row.failure.retryable,
|
|
1705
|
+
retryFrom: row.failure.retryFrom
|
|
1706
|
+
} },
|
|
1707
|
+
...row.stagingRef === void 0 ? {} : { staging: {
|
|
1708
|
+
state: "kept",
|
|
1709
|
+
...row.stagingExpiresAt === void 0 ? {} : { expiresAt: row.stagingExpiresAt }
|
|
1710
|
+
} },
|
|
1711
|
+
...row.expired === void 0 ? {} : { expired: {
|
|
1712
|
+
kind: row.expired,
|
|
1713
|
+
at: Date.parse(row.updatedAt)
|
|
1078
1714
|
} },
|
|
1079
1715
|
...row.attempts === void 0 ? {} : { attempts: row.attempts },
|
|
1080
|
-
...row.
|
|
1081
|
-
...row.
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
row.decision?.aSummary ?? "未提供技能1摘要",
|
|
1086
|
-
row.decision?.bSummary ?? "未提供技能2摘要",
|
|
1087
|
-
row.decision?.rationale ?? row.reason ?? "未提供融合原因"
|
|
1088
|
-
],
|
|
1089
|
-
actions: row.state === "pending" ? ["/evolution merge approve " + row.id, "/evolution merge reject " + row.id] : row.state === "skipped" ? ["/evolution merge skipped --restore " + row.id] : ["/evolution merge resume " + row.id]
|
|
1716
|
+
...row.decision?.confidence === void 0 ? {} : { confidence: row.decision.confidence },
|
|
1717
|
+
...row.state === "skipped" && row.stagingExpiresAt !== void 0 ? { expiresAt: row.stagingExpiresAt } : {},
|
|
1718
|
+
createdAt: Date.parse(row.createdAt),
|
|
1719
|
+
updatedAt: Date.parse(row.updatedAt),
|
|
1720
|
+
actions
|
|
1090
1721
|
};
|
|
1091
1722
|
}
|
|
1092
|
-
function
|
|
1093
|
-
const
|
|
1094
|
-
|
|
1095
|
-
schemaVersion: 2,
|
|
1096
|
-
counts,
|
|
1097
|
-
tabs: [
|
|
1098
|
-
{
|
|
1099
|
-
queue: "verify",
|
|
1100
|
-
count: counts.verify
|
|
1101
|
-
},
|
|
1102
|
-
{
|
|
1103
|
-
queue: "pending",
|
|
1104
|
-
count: counts.pending
|
|
1105
|
-
},
|
|
1106
|
-
{
|
|
1107
|
-
queue: "fuse",
|
|
1108
|
-
count: counts.fuse
|
|
1109
|
-
},
|
|
1110
|
-
{
|
|
1111
|
-
queue: "skipped",
|
|
1112
|
-
count: counts.skipped
|
|
1113
|
-
}
|
|
1114
|
-
],
|
|
1115
|
-
commands,
|
|
1116
|
-
...latestRun === void 0 ? {} : { latestRun },
|
|
1117
|
-
...approval === void 0 ? {} : { approval }
|
|
1118
|
-
};
|
|
1119
|
-
}
|
|
1120
|
-
function pageQueue(rows, queue, cursor = 0, limit = 50) {
|
|
1121
|
-
const filtered = rows.filter((row) => queueOf(row.state) === queue);
|
|
1122
|
-
const size = Math.max(1, Math.min(200, limit));
|
|
1123
|
-
const start = Math.max(0, cursor);
|
|
1124
|
-
const items = filtered.slice(start, start + size).map(toRowView);
|
|
1125
|
-
const next = start + size < filtered.length ? String(start + size) : void 0;
|
|
1723
|
+
function toQueuePage(rows, queue, expiredOnly, readAt, limit = 50) {
|
|
1724
|
+
const filtered = rows.filter((row) => queueOf$1(row) === queue && (expiredOnly ? expiredOf(row) : !expiredOf(row)));
|
|
1725
|
+
const page = filtered.slice(0, Math.max(0, Math.min(200, limit)));
|
|
1126
1726
|
return {
|
|
1727
|
+
wireVersion: 1,
|
|
1728
|
+
readAt,
|
|
1127
1729
|
queue,
|
|
1128
|
-
|
|
1129
|
-
|
|
1730
|
+
expiredOnly,
|
|
1731
|
+
counts: countsOf(rows),
|
|
1732
|
+
rows: page.map((row) => toRowFact(row)),
|
|
1733
|
+
...filtered.length > page.length ? { nextCursor: String(page.length) } : {},
|
|
1734
|
+
notes: []
|
|
1130
1735
|
};
|
|
1131
1736
|
}
|
|
1132
|
-
function
|
|
1737
|
+
function toRunsView(runs, readAt) {
|
|
1133
1738
|
return {
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
...
|
|
1137
|
-
...run.stage === void 0 ? {} : { stage: run.stage },
|
|
1138
|
-
...run.progress === void 0 ? {} : { progress: run.progress },
|
|
1139
|
-
startedAt: run.startedAt,
|
|
1140
|
-
...run.endedAt === void 0 ? {} : { endedAt: run.endedAt },
|
|
1141
|
-
...run.resultRef === void 0 ? {} : { resultRef: run.resultRef },
|
|
1142
|
-
...run.failure === void 0 ? {} : { failure: run.failure },
|
|
1143
|
-
...run.supersededAt === void 0 ? {} : { supersededAt: run.supersededAt }
|
|
1739
|
+
wireVersion: 1,
|
|
1740
|
+
readAt,
|
|
1741
|
+
runs: [...runs].sort((a, b) => b.startedAt - a.startedAt).slice(0, 50).map((run) => toRunView(run, readAt))
|
|
1144
1742
|
};
|
|
1145
1743
|
}
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
continue;
|
|
1172
|
-
}
|
|
1173
|
-
const heading = /^(#{1,6})\s+(.*)$/.exec(line);
|
|
1174
|
-
if (heading !== null) {
|
|
1175
|
-
if (inList) {
|
|
1176
|
-
out.push("</ul>");
|
|
1177
|
-
inList = false;
|
|
1178
|
-
}
|
|
1179
|
-
const level = heading[1]?.length ?? 1;
|
|
1180
|
-
out.push("<h" + String(level) + ">" + escapeHtml(heading[2] ?? "") + "</h" + String(level) + ">");
|
|
1181
|
-
continue;
|
|
1182
|
-
}
|
|
1183
|
-
if (/^[-*]\s+/.test(line)) {
|
|
1184
|
-
if (!inList) {
|
|
1185
|
-
out.push("<ul>");
|
|
1186
|
-
inList = true;
|
|
1187
|
-
}
|
|
1188
|
-
out.push("<li>" + escapeHtml(line.replace(/^[-*]\s+/, "")) + "</li>");
|
|
1189
|
-
continue;
|
|
1190
|
-
}
|
|
1191
|
-
if (inList) {
|
|
1192
|
-
out.push("</ul>");
|
|
1193
|
-
inList = false;
|
|
1194
|
-
}
|
|
1195
|
-
out.push(line.trim() === "" ? "" : "<p>" + escapeHtml(line) + "</p>");
|
|
1196
|
-
}
|
|
1197
|
-
if (inList) out.push("</ul>");
|
|
1198
|
-
if (inCode) out.push("</code></pre>");
|
|
1199
|
-
return out.join("\n");
|
|
1744
|
+
function toBodyView(name, side, text, readAt) {
|
|
1745
|
+
return {
|
|
1746
|
+
wireVersion: 1,
|
|
1747
|
+
readAt,
|
|
1748
|
+
name,
|
|
1749
|
+
side,
|
|
1750
|
+
text,
|
|
1751
|
+
markdown: true,
|
|
1752
|
+
truncated: false,
|
|
1753
|
+
totalBytes: Buffer.byteLength(text, "utf8")
|
|
1754
|
+
};
|
|
1755
|
+
}
|
|
1756
|
+
function toHistoryView(items, readAt) {
|
|
1757
|
+
return {
|
|
1758
|
+
wireVersion: 1,
|
|
1759
|
+
readAt,
|
|
1760
|
+
items: items.map((item) => ({
|
|
1761
|
+
name: item.name,
|
|
1762
|
+
kind: item.kind,
|
|
1763
|
+
reason: item.reason,
|
|
1764
|
+
lastVersion: item.lastVersion,
|
|
1765
|
+
archivedAt: Date.parse(item.archivedAt),
|
|
1766
|
+
...item.sourceRunId === void 0 ? {} : { sourceRunId: item.sourceRunId }
|
|
1767
|
+
}))
|
|
1768
|
+
};
|
|
1200
1769
|
}
|
|
1201
1770
|
//#endregion
|
|
1202
1771
|
//#region lib/types/merge-routes.js
|
|
@@ -1209,12 +1778,15 @@ const MERGE_ROUTES = {
|
|
|
1209
1778
|
history: MERGE_PATHS.history ?? MERGE_BASE + "history"
|
|
1210
1779
|
};
|
|
1211
1780
|
function makeMergeRoutes(deps, _audit) {
|
|
1212
|
-
const absent = () => json(
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
|
|
1781
|
+
const absent = () => json(404, {
|
|
1782
|
+
wireVersion: 1,
|
|
1783
|
+
code: "E-325",
|
|
1784
|
+
message: "merge service is not mounted"
|
|
1785
|
+
});
|
|
1786
|
+
const bad = (message) => json(400, {
|
|
1787
|
+
wireVersion: 1,
|
|
1788
|
+
code: "merge-bad-query",
|
|
1789
|
+
message
|
|
1218
1790
|
});
|
|
1219
1791
|
const get = (request) => request.method === "GET" ? null : methodNotAllowed(["GET"]);
|
|
1220
1792
|
const url = (request) => new URL(request.url, "http://local");
|
|
@@ -1228,11 +1800,13 @@ function makeMergeRoutes(deps, _audit) {
|
|
|
1228
1800
|
if (denied !== null) return denied;
|
|
1229
1801
|
const source = deps.getSource();
|
|
1230
1802
|
if (source === void 0) return absent();
|
|
1231
|
-
const
|
|
1232
|
-
return json(200, {
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1803
|
+
const readAt = Date.now();
|
|
1804
|
+
return json(200, buildFacts({
|
|
1805
|
+
rows: await source.rows(),
|
|
1806
|
+
runs: await source.runs(void 0, 20),
|
|
1807
|
+
approval: source.approval?.() === true,
|
|
1808
|
+
readAt
|
|
1809
|
+
}));
|
|
1236
1810
|
}
|
|
1237
1811
|
},
|
|
1238
1812
|
{
|
|
@@ -1246,19 +1820,16 @@ function makeMergeRoutes(deps, _audit) {
|
|
|
1246
1820
|
if (source === void 0) return absent();
|
|
1247
1821
|
const query = url(request).searchParams;
|
|
1248
1822
|
const queue = query.get("queue") ?? "verify";
|
|
1249
|
-
if (queue !== "verify" && queue !== "pending" && queue !== "fuse" && queue !== "skipped") return
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
code: "bad-queue",
|
|
1253
|
-
message: "queue must be verify/pending/fuse/skipped"
|
|
1254
|
-
}
|
|
1255
|
-
});
|
|
1256
|
-
const cursor = Number.parseInt(query.get("cursor") ?? "0", 10);
|
|
1823
|
+
if (queue !== "verify" && queue !== "pending" && queue !== "fuse" && queue !== "skipped") return bad("queue must be verify/pending/fuse/skipped");
|
|
1824
|
+
const expiredOnly = query.get("expired") === "1";
|
|
1825
|
+
if (expiredOnly && queue !== "pending") return bad("expired is only allowed for pending");
|
|
1257
1826
|
const limit = Number.parseInt(query.get("limit") ?? "50", 10);
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
|
|
1827
|
+
const offset = Number.parseInt(query.get("cursor") ?? "0", 10);
|
|
1828
|
+
const page = toQueuePage(await source.rows(), queue, expiredOnly, Date.now(), Number.isFinite(limit) ? limit : 50);
|
|
1829
|
+
return json(200, Number.isFinite(offset) && offset > 0 ? {
|
|
1830
|
+
...page,
|
|
1831
|
+
rows: page.rows.slice(offset)
|
|
1832
|
+
} : page);
|
|
1262
1833
|
}
|
|
1263
1834
|
},
|
|
1264
1835
|
{
|
|
@@ -1273,28 +1844,13 @@ function makeMergeRoutes(deps, _audit) {
|
|
|
1273
1844
|
const query = url(request).searchParams;
|
|
1274
1845
|
const id = query.get("id");
|
|
1275
1846
|
const side = query.get("side");
|
|
1276
|
-
if (id === null || id === "" || side !== "a" && side !== "b") return
|
|
1277
|
-
ok: false,
|
|
1278
|
-
error: {
|
|
1279
|
-
code: "bad-body-query",
|
|
1280
|
-
message: "id and side=a|b are required"
|
|
1281
|
-
}
|
|
1282
|
-
});
|
|
1847
|
+
if (id === null || id === "" || side !== "a" && side !== "b") return bad("id and side=a|b are required");
|
|
1283
1848
|
const content = await source.body(id, side);
|
|
1284
|
-
return content === null ? json(
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
}
|
|
1290
|
-
}) : json(200, {
|
|
1291
|
-
ok: true,
|
|
1292
|
-
data: {
|
|
1293
|
-
content,
|
|
1294
|
-
markdown: true,
|
|
1295
|
-
html: renderMarkdown(content)
|
|
1296
|
-
}
|
|
1297
|
-
});
|
|
1849
|
+
return content === null ? json(200, {
|
|
1850
|
+
wireVersion: 1,
|
|
1851
|
+
code: "merge-body-missing",
|
|
1852
|
+
message: "merge body not found: " + id
|
|
1853
|
+
}) : json(200, toBodyView(id, side === "a" ? "live" : "archive", content, Date.now()));
|
|
1298
1854
|
}
|
|
1299
1855
|
},
|
|
1300
1856
|
{
|
|
@@ -1306,10 +1862,7 @@ function makeMergeRoutes(deps, _audit) {
|
|
|
1306
1862
|
if (denied !== null) return denied;
|
|
1307
1863
|
const source = deps.getSource();
|
|
1308
1864
|
if (source === void 0) return absent();
|
|
1309
|
-
return json(200,
|
|
1310
|
-
ok: true,
|
|
1311
|
-
data: { items: await source.history() }
|
|
1312
|
-
});
|
|
1865
|
+
return json(200, toHistoryView(await source.history(), Date.now()));
|
|
1313
1866
|
}
|
|
1314
1867
|
}
|
|
1315
1868
|
];
|
|
@@ -1557,8 +2110,58 @@ function mergeUsage() {
|
|
|
1557
2110
|
return USAGE;
|
|
1558
2111
|
}
|
|
1559
2112
|
//#endregion
|
|
2113
|
+
//#region lib/types/presentation-budget.js
|
|
2114
|
+
/** Tool-result budget: keep the head and tail, mark what was omitted. */
|
|
2115
|
+
function fitToolText(text, max = 8e3) {
|
|
2116
|
+
if (text.length <= max) return text;
|
|
2117
|
+
const marker = "\n... [truncated " + String(text.length - max) + " chars] ...\n";
|
|
2118
|
+
const keep = Math.max(0, max - marker.length);
|
|
2119
|
+
const head = Math.ceil(keep * .7);
|
|
2120
|
+
const tail = keep - head;
|
|
2121
|
+
return text.slice(0, head) + marker + (tail === 0 ? "" : text.slice(-tail));
|
|
2122
|
+
}
|
|
2123
|
+
//#endregion
|
|
2124
|
+
//#region lib/types/status.js
|
|
2125
|
+
function countRows(rows) {
|
|
2126
|
+
const counts = {
|
|
2127
|
+
verify: 0,
|
|
2128
|
+
pending: 0,
|
|
2129
|
+
fuse: 0,
|
|
2130
|
+
skipped: 0
|
|
2131
|
+
};
|
|
2132
|
+
for (const row of rows) counts[queueOf(row.state)] += 1;
|
|
2133
|
+
return counts;
|
|
2134
|
+
}
|
|
2135
|
+
function describe(row) {
|
|
2136
|
+
const stage = stageOf(row.state);
|
|
2137
|
+
const target = row.decision?.shouldMerge === true ? " → " + row.decision.target : "";
|
|
2138
|
+
return row.a + " + " + row.b + " [" + row.state + (stage === null ? "" : "/" + stage) + "]" + target + (row.failure === void 0 ? "" : " — " + row.failure.message);
|
|
2139
|
+
}
|
|
2140
|
+
function formatStatus(rows) {
|
|
2141
|
+
const counts = countRows(rows);
|
|
2142
|
+
const lines = ["merge: verify " + String(counts.verify) + ", pending " + String(counts.pending) + ", fuse " + String(counts.fuse) + ", skipped " + String(counts.skipped)];
|
|
2143
|
+
for (const row of rows) lines.push("- " + describe(row));
|
|
2144
|
+
return fitToolText(lines.join("\n"));
|
|
2145
|
+
}
|
|
2146
|
+
function formatFacts(facts) {
|
|
2147
|
+
const lines = ["merge: verify " + String(facts.counts.active.verify) + ", pending " + String(facts.counts.active.pending) + ", fuse " + String(facts.counts.active.fuse) + ", skipped " + String(facts.counts.active.skipped) + ", expired " + String(facts.counts.expired)];
|
|
2148
|
+
if (facts.active !== null) lines.push("active: " + facts.active.id + " [" + facts.active.state + "]");
|
|
2149
|
+
if (facts.last !== null) lines.push("last: " + facts.last.id + " [" + facts.last.state + "]");
|
|
2150
|
+
if (facts.degraded) lines.push("degraded: " + facts.notes.join(","));
|
|
2151
|
+
return fitToolText(lines.join("\n"));
|
|
2152
|
+
}
|
|
2153
|
+
function formatRunsView(view) {
|
|
2154
|
+
if (view.runs.length === 0) return "merge runs: empty";
|
|
2155
|
+
return fitToolText(["merge runs: " + String(view.runs.length)].concat(view.runs.map((run) => "- " + run.id + " [" + run.state + "]" + (run.endedAt === void 0 ? "" : " " + new Date(run.endedAt).toISOString()))).join("\n"));
|
|
2156
|
+
}
|
|
2157
|
+
function formatQueue(rows, queue) {
|
|
2158
|
+
const selected = rows.filter((row) => queueOf(row.state) === queue);
|
|
2159
|
+
if (selected.length === 0) return "merge " + queue + ": empty";
|
|
2160
|
+
return fitToolText(["merge " + queue + ": " + String(selected.length)].concat(selected.map((row) => "- " + describe(row))).join("\n"));
|
|
2161
|
+
}
|
|
2162
|
+
//#endregion
|
|
1560
2163
|
//#region lib/types/read-commands.js
|
|
1561
|
-
async function handleReadOnlyMergeCommand(command, io, home, cards) {
|
|
2164
|
+
async function handleReadOnlyMergeCommand(command, io, home, cards, runs, approval = false) {
|
|
1562
2165
|
const withNote = (text, note) => note === void 0 ? text : "note: " + note + "\n" + text;
|
|
1563
2166
|
const load = async () => {
|
|
1564
2167
|
const result = await loadMergeState(io, home);
|
|
@@ -1571,7 +2174,13 @@ async function handleReadOnlyMergeCommand(command, io, home, cards) {
|
|
|
1571
2174
|
const row = loaded.state.rows.find((item) => item.id === command.id);
|
|
1572
2175
|
return withNote(row === void 0 ? "merge row not found: " + command.id : formatStatus([row]), loaded.note);
|
|
1573
2176
|
}
|
|
1574
|
-
return withNote(
|
|
2177
|
+
return withNote(formatFacts(buildFacts({
|
|
2178
|
+
rows: loaded.state.rows,
|
|
2179
|
+
runs: runs ?? [],
|
|
2180
|
+
approval,
|
|
2181
|
+
readAt: Date.now(),
|
|
2182
|
+
...loaded.note === void 0 ? {} : { notes: [loaded.note] }
|
|
2183
|
+
})), loaded.note);
|
|
1575
2184
|
}
|
|
1576
2185
|
if (command.verb === "pending") {
|
|
1577
2186
|
const loaded = await load();
|
|
@@ -1657,6 +2266,115 @@ function createMergeCommand(service) {
|
|
|
1657
2266
|
};
|
|
1658
2267
|
}
|
|
1659
2268
|
//#endregion
|
|
2269
|
+
//#region lib/types/domain/claim.js
|
|
2270
|
+
function acquireClaim(current, now, leaseMs, token, pid, startedAt) {
|
|
2271
|
+
if (current !== void 0 && current.expiresAt > now) return void 0;
|
|
2272
|
+
return {
|
|
2273
|
+
id: "claim",
|
|
2274
|
+
token,
|
|
2275
|
+
expiresAt: now + Math.max(1, leaseMs),
|
|
2276
|
+
...pid === void 0 ? {} : { pid },
|
|
2277
|
+
...startedAt === void 0 ? {} : { startedAt }
|
|
2278
|
+
};
|
|
2279
|
+
}
|
|
2280
|
+
function releaseClaim(current, token) {
|
|
2281
|
+
return current?.token === token ? void 0 : current;
|
|
2282
|
+
}
|
|
2283
|
+
//#endregion
|
|
2284
|
+
//#region lib/types/target-claim.js
|
|
2285
|
+
/** Durable target claim: one approve writer per merge target at a time. */
|
|
2286
|
+
const PROCESS_STARTED_AT = Date.now() - process.uptime() * 1e3;
|
|
2287
|
+
function claimPath(home, target) {
|
|
2288
|
+
return join(home, "merge", "claims", contentHash(target) + ".json");
|
|
2289
|
+
}
|
|
2290
|
+
function parseClaim(raw) {
|
|
2291
|
+
if (raw === null) return void 0;
|
|
2292
|
+
const parsed = JSON.parse(raw);
|
|
2293
|
+
if (typeof parsed.token !== "string" || typeof parsed.expiresAt !== "number") throw new Error("corrupt target claim");
|
|
2294
|
+
return {
|
|
2295
|
+
id: "claim",
|
|
2296
|
+
token: parsed.token,
|
|
2297
|
+
expiresAt: parsed.expiresAt,
|
|
2298
|
+
...typeof parsed.pid === "number" ? { pid: parsed.pid } : {},
|
|
2299
|
+
...typeof parsed.startedAt === "number" ? { startedAt: parsed.startedAt } : {}
|
|
2300
|
+
};
|
|
2301
|
+
}
|
|
2302
|
+
async function withGuard(path, fn) {
|
|
2303
|
+
const guard = path + ".guard";
|
|
2304
|
+
await mkdir(dirname(path), { recursive: true });
|
|
2305
|
+
for (let attempt = 0; attempt < 500; attempt += 1) try {
|
|
2306
|
+
const handle = await open(guard, "wx");
|
|
2307
|
+
await handle.writeFile(String(process.pid));
|
|
2308
|
+
await handle.close();
|
|
2309
|
+
try {
|
|
2310
|
+
return await fn();
|
|
2311
|
+
} finally {
|
|
2312
|
+
await unlink(guard).catch(() => {});
|
|
2313
|
+
}
|
|
2314
|
+
} catch (error) {
|
|
2315
|
+
if (error.code !== "EEXIST") throw error;
|
|
2316
|
+
const pid = Number(await readFile(guard, "utf8").catch(() => ""));
|
|
2317
|
+
if (Number.isFinite(pid) && pid > 0 && !isProcessAlive(pid)) await unlink(guard).catch(() => {});
|
|
2318
|
+
await new Promise((resolve) => setTimeout(resolve, 10));
|
|
2319
|
+
}
|
|
2320
|
+
throw new Error("target-claim-guard-timeout");
|
|
2321
|
+
}
|
|
2322
|
+
async function compareAndSwap(io, path, task) {
|
|
2323
|
+
await withGuard(path, async () => {
|
|
2324
|
+
if (io.transact !== void 0) {
|
|
2325
|
+
await io.transact(path, (current) => task(current));
|
|
2326
|
+
return;
|
|
2327
|
+
}
|
|
2328
|
+
const current = await io.exists(path) ? await io.readText(path) : null;
|
|
2329
|
+
const next = task(current);
|
|
2330
|
+
if (next === null) {
|
|
2331
|
+
if (current !== null) try {
|
|
2332
|
+
await io.remove(path);
|
|
2333
|
+
} catch {}
|
|
2334
|
+
} else if (next !== current) await io.writeText(path, next);
|
|
2335
|
+
});
|
|
2336
|
+
}
|
|
2337
|
+
async function releaseTargetClaimQuietly(claim) {
|
|
2338
|
+
if (claim === null || claim === void 0) return;
|
|
2339
|
+
try {
|
|
2340
|
+
await claim.release();
|
|
2341
|
+
} catch {}
|
|
2342
|
+
}
|
|
2343
|
+
async function acquireTargetClaim(io, home, target, now) {
|
|
2344
|
+
const path = claimPath(home, target);
|
|
2345
|
+
const token = randomUUID();
|
|
2346
|
+
let granted;
|
|
2347
|
+
await compareAndSwap(io, path, (raw) => {
|
|
2348
|
+
const current = parseClaim(raw);
|
|
2349
|
+
const claim = acquireClaim(current !== void 0 && current.pid === process.pid && current.startedAt !== void 0 && Math.abs(current.startedAt - PROCESS_STARTED_AT) > 1e3 || current !== void 0 && current.expiresAt > now && current.pid !== void 0 && current.pid !== process.pid && !isProcessAlive(current.pid) ? void 0 : current, now, Number.MAX_SAFE_INTEGER, token, process.pid, PROCESS_STARTED_AT);
|
|
2350
|
+
if (claim === void 0) return raw;
|
|
2351
|
+
granted = claim;
|
|
2352
|
+
return JSON.stringify(claim) + "\n";
|
|
2353
|
+
});
|
|
2354
|
+
if (granted === void 0) return null;
|
|
2355
|
+
return {
|
|
2356
|
+
token,
|
|
2357
|
+
async release() {
|
|
2358
|
+
await compareAndSwap(io, path, (raw) => {
|
|
2359
|
+
const next = releaseClaim(parseClaim(raw), token);
|
|
2360
|
+
return next === void 0 ? null : JSON.stringify(next) + "\n";
|
|
2361
|
+
});
|
|
2362
|
+
}
|
|
2363
|
+
};
|
|
2364
|
+
}
|
|
2365
|
+
//#endregion
|
|
2366
|
+
//#region lib/types/unit-claim.js
|
|
2367
|
+
/** Commit-phase claim: 24h is only stale recovery; commit releases immediately, no approve TTL. */
|
|
2368
|
+
function targetClaimUnitPort(io, home, now) {
|
|
2369
|
+
return { async acquire(target) {
|
|
2370
|
+
const claim = await acquireTargetClaim(io, home, target, now());
|
|
2371
|
+
if (claim === null) throw new Error("target-claim-busy: " + target);
|
|
2372
|
+
return { async release() {
|
|
2373
|
+
await releaseTargetClaimQuietly(claim);
|
|
2374
|
+
} };
|
|
2375
|
+
} };
|
|
2376
|
+
}
|
|
2377
|
+
//#endregion
|
|
1660
2378
|
//#region lib/types/skipped.js
|
|
1661
2379
|
/**
|
|
1662
2380
|
* Skipped filtering: a pair the operator marked as not-worth-merging stays out of automatic
|
|
@@ -1761,6 +2479,7 @@ async function runScan(options) {
|
|
|
1761
2479
|
const stage = options.stage ?? (async () => {});
|
|
1762
2480
|
const candidates = await options.ports.screening(options.signal);
|
|
1763
2481
|
const before = await loadMergeState(options.io, options.home);
|
|
2482
|
+
if (!before.ok) throw new Error(before.note ?? "merge-state-invalid");
|
|
1764
2483
|
const queueBefore = {
|
|
1765
2484
|
verify: 0,
|
|
1766
2485
|
pending: 0,
|
|
@@ -1803,29 +2522,46 @@ async function runScan(options) {
|
|
|
1803
2522
|
});
|
|
1804
2523
|
let row;
|
|
1805
2524
|
try {
|
|
1806
|
-
const
|
|
1807
|
-
|
|
2525
|
+
const key = pairKey(candidate.a, candidate.b);
|
|
2526
|
+
const atNow = at(options.now);
|
|
2527
|
+
row = {
|
|
2528
|
+
id: rowIdOf(key),
|
|
2529
|
+
pairKey: key,
|
|
1808
2530
|
a: candidate.a,
|
|
1809
2531
|
b: candidate.b,
|
|
1810
|
-
|
|
1811
|
-
|
|
1812
|
-
|
|
1813
|
-
|
|
1814
|
-
...
|
|
1815
|
-
runId: options.runId
|
|
2532
|
+
state: "verify",
|
|
2533
|
+
queue: "verify",
|
|
2534
|
+
createdAt: atNow,
|
|
2535
|
+
updatedAt: atNow,
|
|
2536
|
+
...options.runId === void 0 ? {} : { runId: options.runId }
|
|
1816
2537
|
};
|
|
1817
2538
|
const before = (await loadMergeState(options.io, options.home)).state.rows.find((item) => item.pairKey === row.pairKey);
|
|
1818
2539
|
if (before?.state === "verify" && before.runId !== void 0 && before.runId !== options.runId && await options.isOwnerActive?.(before.runId) === true) continue;
|
|
1819
|
-
const
|
|
1820
|
-
|
|
1821
|
-
|
|
1822
|
-
|
|
1823
|
-
|
|
2540
|
+
const status = await transactRows(options.io, options.home, (tx) => {
|
|
2541
|
+
const current = tx.rows().find((item) => item.pairKey === row.pairKey);
|
|
2542
|
+
if (current !== void 0 && current.state !== "verify" && current.state !== "skipped") return "conflict:" + current.state;
|
|
2543
|
+
if (current?.state === "skipped") {
|
|
2544
|
+
if (!tx.transition(current.id, { type: "restored" }, current.version).ok) return "restore-failed";
|
|
2545
|
+
}
|
|
2546
|
+
if (current?.state === "verify") return "existing";
|
|
2547
|
+
const made = tx.createVerify({
|
|
2548
|
+
id: current?.id ?? rowIdOf(row.pairKey),
|
|
2549
|
+
pairKey: row.pairKey,
|
|
2550
|
+
a: row.a,
|
|
2551
|
+
b: row.b,
|
|
2552
|
+
version: 0,
|
|
2553
|
+
createdAt: at(options.now),
|
|
2554
|
+
updatedAt: at(options.now),
|
|
2555
|
+
origin: "agent",
|
|
2556
|
+
state: "verify",
|
|
2557
|
+
verifySubState: "unverified",
|
|
2558
|
+
attempts: 0,
|
|
2559
|
+
...row.runId === void 0 ? {} : { runId: row.runId }
|
|
1824
2560
|
});
|
|
1825
|
-
|
|
1826
|
-
|
|
1827
|
-
|
|
1828
|
-
|
|
2561
|
+
return made.ok ? "created" : "invalid:" + made.code;
|
|
2562
|
+
});
|
|
2563
|
+
if (status.startsWith("conflict:")) throw new Error("pair already in " + status.slice(9));
|
|
2564
|
+
if (status === "restore-failed" || status.startsWith("invalid:")) throw new Error("cannot place verify row: " + status);
|
|
1829
2565
|
} catch (error) {
|
|
1830
2566
|
totals.failed += 1;
|
|
1831
2567
|
pairs.push({
|
|
@@ -1859,33 +2595,47 @@ async function runScan(options) {
|
|
|
1859
2595
|
break;
|
|
1860
2596
|
}
|
|
1861
2597
|
if (round1.kind === "failed") {
|
|
1862
|
-
const
|
|
1863
|
-
|
|
1864
|
-
|
|
1865
|
-
|
|
2598
|
+
const failure = round1.failure ?? failureOf("decide", "round1 failed");
|
|
2599
|
+
const projected = await transactRows(options.io, options.home, (tx) => {
|
|
2600
|
+
const current = tx.rows().find((item) => item.id === row.id);
|
|
2601
|
+
if (current === void 0) return null;
|
|
2602
|
+
const moved = tx.transition(row.id, {
|
|
2603
|
+
type: "failed",
|
|
2604
|
+
failure
|
|
2605
|
+
}, current.version);
|
|
2606
|
+
return moved.ok && moved.row !== null ? v3ToLegacy(moved.row) : null;
|
|
1866
2607
|
});
|
|
1867
|
-
if (
|
|
1868
|
-
row
|
|
1869
|
-
|
|
2608
|
+
if (projected === null) {
|
|
2609
|
+
pairs.push(pairOf(row, candidate, "failed", { failure: failureOf("decide", "row changed before failure record") }));
|
|
2610
|
+
totals.failed += 1;
|
|
2611
|
+
continue;
|
|
1870
2612
|
}
|
|
1871
|
-
|
|
2613
|
+
row = projected;
|
|
2614
|
+
pairs.push(pairOf(row, candidate, "failed", { failure }));
|
|
1872
2615
|
totals.failed += 1;
|
|
1873
2616
|
continue;
|
|
1874
2617
|
}
|
|
1875
2618
|
if (round1.kind === "keep") {
|
|
1876
|
-
const
|
|
1877
|
-
|
|
1878
|
-
|
|
2619
|
+
const projected = await transactRows(options.io, options.home, (tx) => {
|
|
2620
|
+
const current = tx.rows().find((item) => item.id === row.id);
|
|
2621
|
+
if (current === void 0) return null;
|
|
2622
|
+
const moved = tx.transition(row.id, {
|
|
2623
|
+
type: "skipped",
|
|
2624
|
+
reason: "kept"
|
|
2625
|
+
}, current.version);
|
|
2626
|
+
return moved.ok && moved.row !== null ? v3ToLegacy(moved.row) : null;
|
|
1879
2627
|
});
|
|
1880
|
-
if (
|
|
1881
|
-
row
|
|
1882
|
-
|
|
1883
|
-
|
|
1884
|
-
pairKey: row.pairKey,
|
|
1885
|
-
runId: row.runId ?? "merge",
|
|
1886
|
-
result: "kept"
|
|
1887
|
-
});
|
|
2628
|
+
if (projected === null) {
|
|
2629
|
+
pairs.push(pairOf(row, candidate, "failed", { failure: failureOf("decide", "row changed before keep record") }));
|
|
2630
|
+
totals.failed += 1;
|
|
2631
|
+
continue;
|
|
1888
2632
|
}
|
|
2633
|
+
row = projected;
|
|
2634
|
+
await recordPair(options.io, options.home, {
|
|
2635
|
+
pairKey: row.pairKey,
|
|
2636
|
+
runId: row.runId ?? "merge",
|
|
2637
|
+
result: "kept"
|
|
2638
|
+
});
|
|
1889
2639
|
pairs.push(pairOf(row, candidate, "kept", round1.decision === void 0 ? {} : { decision: round1.decision }));
|
|
1890
2640
|
totals.kept += 1;
|
|
1891
2641
|
continue;
|
|
@@ -1896,49 +2646,79 @@ async function runScan(options) {
|
|
|
1896
2646
|
continue;
|
|
1897
2647
|
}
|
|
1898
2648
|
if (options.approval) {
|
|
1899
|
-
const
|
|
1900
|
-
type: "
|
|
1901
|
-
approval: "on",
|
|
2649
|
+
const event = {
|
|
2650
|
+
type: "verify-passed",
|
|
1902
2651
|
decision: round1.decision,
|
|
1903
|
-
|
|
1904
|
-
...round1.
|
|
1905
|
-
|
|
2652
|
+
target: round1.decision.target,
|
|
2653
|
+
...round1.sourceRevision === void 0 ? {} : { sourceRevision: round1.sourceRevision }
|
|
2654
|
+
};
|
|
2655
|
+
const projected = await transactRows(options.io, options.home, (tx) => {
|
|
2656
|
+
const current = tx.rows().find((item) => item.id === row.id);
|
|
2657
|
+
if (current === void 0) return null;
|
|
2658
|
+
const moved = tx.transition(row.id, event, current.version);
|
|
2659
|
+
return moved.ok && moved.row !== null ? v3ToLegacy(moved.row) : null;
|
|
1906
2660
|
});
|
|
1907
|
-
if (
|
|
1908
|
-
row
|
|
1909
|
-
|
|
2661
|
+
if (projected === null) {
|
|
2662
|
+
pairs.push(pairOf(row, candidate, "failed", {
|
|
2663
|
+
decision: round1.decision,
|
|
2664
|
+
failure: failureOf("decide", "row changed before pending record")
|
|
2665
|
+
}));
|
|
2666
|
+
totals.failed += 1;
|
|
2667
|
+
continue;
|
|
1910
2668
|
}
|
|
2669
|
+
row = projected;
|
|
1911
2670
|
pairs.push(pairOf(row, candidate, "pending", { decision: round1.decision }));
|
|
1912
2671
|
totals.pending += 1;
|
|
1913
2672
|
continue;
|
|
1914
2673
|
}
|
|
1915
|
-
const
|
|
1916
|
-
|
|
1917
|
-
|
|
1918
|
-
|
|
1919
|
-
|
|
1920
|
-
|
|
1921
|
-
|
|
2674
|
+
const staged = await transactRows(options.io, options.home, (tx) => {
|
|
2675
|
+
const current = tx.rows().find((item) => item.id === row.id);
|
|
2676
|
+
if (current === void 0) return null;
|
|
2677
|
+
const approved = tx.transition(row.id, {
|
|
2678
|
+
type: "auto-approved",
|
|
2679
|
+
decision: round1.decision,
|
|
2680
|
+
target: round1.decision.target,
|
|
2681
|
+
...row.runId === void 0 ? {} : { runId: row.runId }
|
|
2682
|
+
}, current.version);
|
|
2683
|
+
if (!approved.ok || approved.row === null) return null;
|
|
2684
|
+
const moved = tx.transition(row.id, {
|
|
2685
|
+
type: "staged",
|
|
2686
|
+
...round1.stagingRef === void 0 ? {} : { ref: round1.stagingRef },
|
|
2687
|
+
...round1.stagingExpiresAt === void 0 ? {} : { expiresAt: round1.stagingExpiresAt }
|
|
2688
|
+
}, approved.row.version);
|
|
2689
|
+
return moved.ok && moved.row !== null ? v3ToLegacy(moved.row) : null;
|
|
1922
2690
|
});
|
|
1923
|
-
if (
|
|
2691
|
+
if (staged === null) {
|
|
1924
2692
|
pairs.push(pairOf(row, candidate, "failed", {
|
|
1925
2693
|
decision: round1.decision,
|
|
1926
|
-
failure: failureOf("
|
|
2694
|
+
failure: failureOf("author", "row changed before staging record")
|
|
1927
2695
|
}));
|
|
1928
2696
|
totals.failed += 1;
|
|
1929
2697
|
continue;
|
|
1930
2698
|
}
|
|
1931
|
-
row =
|
|
1932
|
-
|
|
2699
|
+
row = staged;
|
|
2700
|
+
if (round1.stagingRef !== void 0) {
|
|
2701
|
+
const expiresAt = round1.stagingExpiresAt ?? options.now() + 7 * 864e5;
|
|
2702
|
+
await writeStagingIndex(options.io, options.home, {
|
|
2703
|
+
rowId: row.id,
|
|
2704
|
+
pairKey: row.pairKey,
|
|
2705
|
+
ref: round1.stagingRef,
|
|
2706
|
+
expiresAt,
|
|
2707
|
+
...row.runId === void 0 ? {} : { runId: row.runId },
|
|
2708
|
+
createdAt: options.now()
|
|
2709
|
+
});
|
|
2710
|
+
}
|
|
1933
2711
|
if (isAborted(options.signal)) {
|
|
1934
|
-
const
|
|
1935
|
-
|
|
1936
|
-
|
|
2712
|
+
const projected = await transactRows(options.io, options.home, (tx) => {
|
|
2713
|
+
const current = tx.rows().find((item) => item.id === row.id);
|
|
2714
|
+
if (current === void 0) return null;
|
|
2715
|
+
const moved = tx.transition(row.id, {
|
|
2716
|
+
type: "skipped",
|
|
2717
|
+
reason: "cancelled"
|
|
2718
|
+
}, current.version);
|
|
2719
|
+
return moved.ok && moved.row !== null ? v3ToLegacy(moved.row) : null;
|
|
1937
2720
|
});
|
|
1938
|
-
if (
|
|
1939
|
-
row = cancelled.row;
|
|
1940
|
-
await upsertRow(options.io, options.home, options.now, row);
|
|
1941
|
-
}
|
|
2721
|
+
if (projected !== null) row = projected;
|
|
1942
2722
|
stopReason = "cancelled";
|
|
1943
2723
|
remaining.push(...candidates.slice(index).map((candidate) => ({
|
|
1944
2724
|
pairKey: pairKey(candidate.a, candidate.b),
|
|
@@ -1960,10 +2740,14 @@ async function runScan(options) {
|
|
|
1960
2740
|
};
|
|
1961
2741
|
}
|
|
1962
2742
|
if (round2.kind === "merged") {
|
|
1963
|
-
if (
|
|
1964
|
-
|
|
1965
|
-
|
|
1966
|
-
|
|
2743
|
+
if (!await dropRowV3(options.io, options.home, row.id, "committed", round2.unitId ?? "unknown")) {
|
|
2744
|
+
pairs.push(pairOf(row, candidate, "failed", {
|
|
2745
|
+
decision: round1.decision,
|
|
2746
|
+
failure: failureOf("commit", "row changed before commit record")
|
|
2747
|
+
}));
|
|
2748
|
+
totals.failed += 1;
|
|
2749
|
+
continue;
|
|
2750
|
+
}
|
|
1967
2751
|
pairs.push(pairOf(row, candidate, "merged", {
|
|
1968
2752
|
decision: round1.decision,
|
|
1969
2753
|
...round2.unitId === void 0 ? {} : { unitId: round2.unitId },
|
|
@@ -1974,15 +2758,24 @@ async function runScan(options) {
|
|
|
1974
2758
|
continue;
|
|
1975
2759
|
}
|
|
1976
2760
|
const failed = round2.failure ?? failureOf("author", "round2 failed");
|
|
1977
|
-
const
|
|
1978
|
-
|
|
1979
|
-
|
|
1980
|
-
|
|
2761
|
+
const projected = await transactRows(options.io, options.home, (tx) => {
|
|
2762
|
+
const current = tx.rows().find((item) => item.id === row.id);
|
|
2763
|
+
if (current === void 0) return null;
|
|
2764
|
+
const moved = tx.transition(row.id, {
|
|
2765
|
+
type: "failed",
|
|
2766
|
+
failure: failed
|
|
2767
|
+
}, current.version);
|
|
2768
|
+
return moved.ok && moved.row !== null ? v3ToLegacy(moved.row) : null;
|
|
1981
2769
|
});
|
|
1982
|
-
if (
|
|
1983
|
-
row
|
|
1984
|
-
|
|
2770
|
+
if (projected === null) {
|
|
2771
|
+
pairs.push(pairOf(row, candidate, "failed", {
|
|
2772
|
+
decision: round1.decision,
|
|
2773
|
+
failure: failureOf("author", "row changed before failure record")
|
|
2774
|
+
}));
|
|
2775
|
+
totals.failed += 1;
|
|
2776
|
+
continue;
|
|
1985
2777
|
}
|
|
2778
|
+
row = projected;
|
|
1986
2779
|
pairs.push(pairOf(row, candidate, "failed", {
|
|
1987
2780
|
decision: round1.decision,
|
|
1988
2781
|
failure: failed
|
|
@@ -2202,48 +2995,40 @@ async function runScanCommand(options) {
|
|
|
2202
2995
|
//#endregion
|
|
2203
2996
|
//#region lib/types/pending-actions.js
|
|
2204
2997
|
async function act(io, home, now, id, type) {
|
|
2205
|
-
return await
|
|
2206
|
-
const
|
|
2207
|
-
if (
|
|
2208
|
-
|
|
2209
|
-
|
|
2210
|
-
|
|
2211
|
-
reason: "not-found",
|
|
2212
|
-
message: "pending row not found: " + id
|
|
2213
|
-
}
|
|
2998
|
+
return await transactRows(io, home, (tx) => {
|
|
2999
|
+
const row = tx.rows().find((item) => item.id === id);
|
|
3000
|
+
if (row === void 0) return {
|
|
3001
|
+
ok: false,
|
|
3002
|
+
reason: "not-found",
|
|
3003
|
+
message: "pending row not found: " + id
|
|
2214
3004
|
};
|
|
2215
|
-
|
|
2216
|
-
|
|
2217
|
-
|
|
2218
|
-
|
|
2219
|
-
ok: false,
|
|
2220
|
-
reason: "not-pending",
|
|
2221
|
-
message: "row is not pending: " + current.state
|
|
2222
|
-
}
|
|
3005
|
+
if (row.state !== "pending") return {
|
|
3006
|
+
ok: false,
|
|
3007
|
+
reason: "not-pending",
|
|
3008
|
+
message: "row is not pending: " + row.state
|
|
2223
3009
|
};
|
|
2224
|
-
const
|
|
2225
|
-
type: "
|
|
2226
|
-
|
|
3010
|
+
const event = type === "approve" ? {
|
|
3011
|
+
type: "approved",
|
|
3012
|
+
runId: row.runId ?? "approve:" + id
|
|
2227
3013
|
} : {
|
|
2228
|
-
type: "
|
|
2229
|
-
|
|
2230
|
-
|
|
2231
|
-
|
|
2232
|
-
|
|
2233
|
-
|
|
2234
|
-
|
|
2235
|
-
|
|
2236
|
-
|
|
2237
|
-
|
|
3014
|
+
type: "skipped",
|
|
3015
|
+
reason: "rejected",
|
|
3016
|
+
origin: "human"
|
|
3017
|
+
};
|
|
3018
|
+
const moved = tx.transition(id, event, row.version);
|
|
3019
|
+
if (!moved.ok) return {
|
|
3020
|
+
ok: false,
|
|
3021
|
+
reason: "invalid",
|
|
3022
|
+
message: moved.message
|
|
3023
|
+
};
|
|
3024
|
+
if (moved.row === null) return {
|
|
3025
|
+
ok: false,
|
|
3026
|
+
reason: "invalid",
|
|
3027
|
+
message: "transition produced no row"
|
|
2238
3028
|
};
|
|
2239
|
-
const copy = [...rows];
|
|
2240
|
-
copy[index] = next.row;
|
|
2241
3029
|
return {
|
|
2242
|
-
|
|
2243
|
-
|
|
2244
|
-
ok: true,
|
|
2245
|
-
row: next.row
|
|
2246
|
-
}
|
|
3030
|
+
ok: true,
|
|
3031
|
+
row: v3ToLegacy(moved.row)
|
|
2247
3032
|
};
|
|
2248
3033
|
});
|
|
2249
3034
|
}
|
|
@@ -2254,79 +3039,29 @@ async function rejectPending(io, home, now, id) {
|
|
|
2254
3039
|
return await act(io, home, now, id, "reject");
|
|
2255
3040
|
}
|
|
2256
3041
|
//#endregion
|
|
2257
|
-
//#region lib/types/domain/claim.js
|
|
2258
|
-
function acquireClaim(current, now, leaseMs, token) {
|
|
2259
|
-
if (current !== void 0 && current.expiresAt > now) return void 0;
|
|
2260
|
-
return {
|
|
2261
|
-
id: "claim",
|
|
2262
|
-
token,
|
|
2263
|
-
expiresAt: now + Math.max(1, leaseMs)
|
|
2264
|
-
};
|
|
2265
|
-
}
|
|
2266
|
-
function releaseClaim(current, token) {
|
|
2267
|
-
return current?.token === token ? void 0 : current;
|
|
2268
|
-
}
|
|
2269
|
-
//#endregion
|
|
2270
|
-
//#region lib/types/target-claim.js
|
|
2271
|
-
/** Durable target claim: one approve writer per merge target at a time. */
|
|
2272
|
-
const LEASE_MS = 30 * 6e4;
|
|
2273
|
-
function claimPath(home, target) {
|
|
2274
|
-
return join(home, "merge", "claims", contentHash(target) + ".json");
|
|
2275
|
-
}
|
|
2276
|
-
function parseClaim(raw) {
|
|
2277
|
-
if (raw === null) return void 0;
|
|
2278
|
-
const parsed = JSON.parse(raw);
|
|
2279
|
-
if (typeof parsed.token !== "string" || typeof parsed.expiresAt !== "number") throw new Error("corrupt target claim");
|
|
2280
|
-
return {
|
|
2281
|
-
id: "claim",
|
|
2282
|
-
token: parsed.token,
|
|
2283
|
-
expiresAt: parsed.expiresAt
|
|
2284
|
-
};
|
|
2285
|
-
}
|
|
2286
|
-
async function compareAndSwap(io, path, task) {
|
|
2287
|
-
if (io.transact !== void 0) {
|
|
2288
|
-
await io.transact(path, (current) => task(current));
|
|
2289
|
-
return;
|
|
2290
|
-
}
|
|
2291
|
-
const current = await io.exists(path) ? await io.readText(path) : null;
|
|
2292
|
-
const next = task(current);
|
|
2293
|
-
if (next === null) {
|
|
2294
|
-
if (current !== null) try {
|
|
2295
|
-
await io.remove(path);
|
|
2296
|
-
} catch {}
|
|
2297
|
-
} else if (next !== current) await io.writeText(path, next);
|
|
2298
|
-
}
|
|
2299
|
-
async function acquireTargetClaim(io, home, target, now) {
|
|
2300
|
-
const path = claimPath(home, target);
|
|
2301
|
-
const token = randomUUID();
|
|
2302
|
-
let granted;
|
|
2303
|
-
await compareAndSwap(io, path, (raw) => {
|
|
2304
|
-
const claim = acquireClaim(parseClaim(raw), now, LEASE_MS, token);
|
|
2305
|
-
if (claim === void 0) return raw;
|
|
2306
|
-
granted = claim;
|
|
2307
|
-
return JSON.stringify(claim) + "\n";
|
|
2308
|
-
});
|
|
2309
|
-
if (granted === void 0) return null;
|
|
2310
|
-
return {
|
|
2311
|
-
token,
|
|
2312
|
-
async release() {
|
|
2313
|
-
await compareAndSwap(io, path, (raw) => {
|
|
2314
|
-
const next = releaseClaim(parseClaim(raw), token);
|
|
2315
|
-
return next === void 0 ? null : JSON.stringify(next) + "\n";
|
|
2316
|
-
});
|
|
2317
|
-
}
|
|
2318
|
-
};
|
|
2319
|
-
}
|
|
2320
|
-
//#endregion
|
|
2321
3042
|
//#region lib/types/round2-outcome.js
|
|
2322
3043
|
async function recordRound2Outcome(io, home, now, row, outcome) {
|
|
2323
|
-
const at = new Date(now()).toISOString();
|
|
2324
3044
|
if (outcome.kind === "merged") {
|
|
2325
|
-
|
|
2326
|
-
|
|
2327
|
-
|
|
2328
|
-
|
|
2329
|
-
|
|
3045
|
+
{
|
|
3046
|
+
if (!await dropRowV3(io, home, row.id, "committed", outcome.unitId ?? "unknown")) return {
|
|
3047
|
+
merged: false,
|
|
3048
|
+
pair: {
|
|
3049
|
+
id: row.id,
|
|
3050
|
+
pairKey: row.pairKey,
|
|
3051
|
+
a: row.a,
|
|
3052
|
+
b: row.b,
|
|
3053
|
+
queue: row.queue,
|
|
3054
|
+
outcome: "failed",
|
|
3055
|
+
archived: [],
|
|
3056
|
+
failure: {
|
|
3057
|
+
stage: "commit",
|
|
3058
|
+
code: "row-changed",
|
|
3059
|
+
message: "row changed before commit record",
|
|
3060
|
+
retryable: true,
|
|
3061
|
+
retryFrom: "agent2"
|
|
3062
|
+
}
|
|
3063
|
+
}
|
|
3064
|
+
};
|
|
2330
3065
|
const target = outcome.target?.actual ?? outcome.target?.requested ?? row.decision?.target;
|
|
2331
3066
|
await recordPair(io, home, {
|
|
2332
3067
|
pairKey: row.pairKey,
|
|
@@ -2359,20 +3094,37 @@ async function recordRound2Outcome(io, home, now, row, outcome) {
|
|
|
2359
3094
|
retryable: true,
|
|
2360
3095
|
retryFrom: "agent2"
|
|
2361
3096
|
};
|
|
2362
|
-
|
|
2363
|
-
|
|
2364
|
-
|
|
2365
|
-
|
|
2366
|
-
|
|
2367
|
-
|
|
2368
|
-
|
|
2369
|
-
|
|
3097
|
+
if (await transactRows(io, home, (tx) => {
|
|
3098
|
+
const current = tx.rows().find((item) => item.id === row.id);
|
|
3099
|
+
if (current === void 0) return null;
|
|
3100
|
+
const moved = tx.transition(row.id, {
|
|
3101
|
+
type: "failed",
|
|
3102
|
+
failure
|
|
3103
|
+
}, current.version);
|
|
3104
|
+
return moved.ok && moved.row !== null ? v3ToLegacy(moved.row) : null;
|
|
3105
|
+
}) === null) return {
|
|
3106
|
+
merged: false,
|
|
3107
|
+
pair: {
|
|
3108
|
+
id: row.id,
|
|
2370
3109
|
pairKey: row.pairKey,
|
|
2371
|
-
|
|
2372
|
-
|
|
2373
|
-
|
|
2374
|
-
|
|
2375
|
-
|
|
3110
|
+
a: row.a,
|
|
3111
|
+
b: row.b,
|
|
3112
|
+
queue: row.queue,
|
|
3113
|
+
outcome: "failed",
|
|
3114
|
+
archived: [],
|
|
3115
|
+
failure: {
|
|
3116
|
+
...failure,
|
|
3117
|
+
code: "row-changed",
|
|
3118
|
+
message: "row changed before failure record"
|
|
3119
|
+
}
|
|
3120
|
+
}
|
|
3121
|
+
};
|
|
3122
|
+
await recordPair(io, home, {
|
|
3123
|
+
pairKey: row.pairKey,
|
|
3124
|
+
runId: row.runId ?? "merge",
|
|
3125
|
+
result: "failed",
|
|
3126
|
+
...row.decision?.target === void 0 ? {} : { target: row.decision.target }
|
|
3127
|
+
});
|
|
2376
3128
|
return {
|
|
2377
3129
|
merged: false,
|
|
2378
3130
|
pair: {
|
|
@@ -2391,89 +3143,89 @@ async function recordRound2Outcome(io, home, now, row, outcome) {
|
|
|
2391
3143
|
//#endregion
|
|
2392
3144
|
//#region lib/types/approve-command.js
|
|
2393
3145
|
async function runApproveCommand(options) {
|
|
2394
|
-
const
|
|
2395
|
-
|
|
2396
|
-
if (target !== void 0 && claim === null) return {
|
|
3146
|
+
const before = (await loadMergeState(options.io, options.home)).state.rows.find((item) => item.id === options.rowId);
|
|
3147
|
+
if (before === void 0) return {
|
|
2397
3148
|
ok: false,
|
|
2398
|
-
message: "
|
|
3149
|
+
message: "pending row not found: " + options.rowId
|
|
2399
3150
|
};
|
|
2400
|
-
|
|
2401
|
-
|
|
2402
|
-
|
|
2403
|
-
|
|
2404
|
-
|
|
2405
|
-
|
|
2406
|
-
|
|
2407
|
-
|
|
2408
|
-
|
|
2409
|
-
|
|
2410
|
-
|
|
2411
|
-
|
|
2412
|
-
|
|
2413
|
-
|
|
2414
|
-
|
|
2415
|
-
|
|
2416
|
-
|
|
2417
|
-
|
|
2418
|
-
|
|
2419
|
-
|
|
2420
|
-
|
|
2421
|
-
|
|
2422
|
-
|
|
2423
|
-
|
|
2424
|
-
|
|
2425
|
-
|
|
2426
|
-
|
|
2427
|
-
|
|
2428
|
-
|
|
2429
|
-
|
|
2430
|
-
|
|
2431
|
-
|
|
2432
|
-
|
|
2433
|
-
|
|
2434
|
-
|
|
2435
|
-
|
|
2436
|
-
|
|
2437
|
-
|
|
2438
|
-
|
|
2439
|
-
|
|
2440
|
-
|
|
2441
|
-
|
|
2442
|
-
|
|
2443
|
-
|
|
2444
|
-
scope: {
|
|
2445
|
-
candidatePairs: 1,
|
|
2446
|
-
queueBefore: {
|
|
2447
|
-
verify: 0,
|
|
2448
|
-
pending: 0,
|
|
2449
|
-
fuse: 1,
|
|
2450
|
-
skipped: 0
|
|
2451
|
-
}
|
|
2452
|
-
},
|
|
2453
|
-
notes: []
|
|
2454
|
-
};
|
|
2455
|
-
const record = await recordRound2Outcome(options.io, options.home, options.now, row, outcome);
|
|
2456
|
-
return {
|
|
2457
|
-
...base,
|
|
2458
|
-
stopReason: record.merged ? "completed" : "failed",
|
|
2459
|
-
pairs: [record.pair],
|
|
2460
|
-
totals: record.merged ? {
|
|
2461
|
-
merged: 1,
|
|
2462
|
-
kept: 0,
|
|
2463
|
-
pending: 0,
|
|
2464
|
-
failed: 0
|
|
2465
|
-
} : {
|
|
2466
|
-
merged: 0,
|
|
2467
|
-
kept: 0,
|
|
2468
|
-
pending: 0,
|
|
2469
|
-
failed: 1
|
|
3151
|
+
if (before.state !== "pending") return {
|
|
3152
|
+
ok: false,
|
|
3153
|
+
message: "row is not pending: " + before.state
|
|
3154
|
+
};
|
|
3155
|
+
const taken = await approvePending(options.io, options.home, options.now, options.rowId);
|
|
3156
|
+
if (!taken.ok) return {
|
|
3157
|
+
ok: false,
|
|
3158
|
+
message: taken.message
|
|
3159
|
+
};
|
|
3160
|
+
const row = taken.row;
|
|
3161
|
+
const decision = row.decision;
|
|
3162
|
+
if (decision === void 0) return {
|
|
3163
|
+
ok: false,
|
|
3164
|
+
message: "pending row has no decision"
|
|
3165
|
+
};
|
|
3166
|
+
return await withMergeRun({
|
|
3167
|
+
io: options.io,
|
|
3168
|
+
home: options.home,
|
|
3169
|
+
registry: options.registry,
|
|
3170
|
+
command: "approve",
|
|
3171
|
+
approval: {
|
|
3172
|
+
platformWrite: options.platformWrite,
|
|
3173
|
+
mergeApproval: true
|
|
3174
|
+
},
|
|
3175
|
+
now: options.now,
|
|
3176
|
+
...options.detached === true ? { detached: true } : {},
|
|
3177
|
+
async body(ctx) {
|
|
3178
|
+
const stage = (name) => ctx.stage(name, {
|
|
3179
|
+
done: 0,
|
|
3180
|
+
total: 1
|
|
3181
|
+
});
|
|
3182
|
+
await stage("author");
|
|
3183
|
+
let outcome;
|
|
3184
|
+
try {
|
|
3185
|
+
outcome = await options.ports.round2(row, decision, ctx.signal, ctx.runId);
|
|
3186
|
+
} catch (error) {
|
|
3187
|
+
outcome = {
|
|
3188
|
+
kind: "failed",
|
|
3189
|
+
failure: {
|
|
3190
|
+
stage: "author",
|
|
3191
|
+
code: "merge-failed",
|
|
3192
|
+
message: error instanceof Error ? error.message : String(error),
|
|
3193
|
+
retryable: true,
|
|
3194
|
+
retryFrom: "agent2"
|
|
2470
3195
|
}
|
|
2471
3196
|
};
|
|
2472
3197
|
}
|
|
2473
|
-
|
|
2474
|
-
|
|
2475
|
-
|
|
2476
|
-
|
|
3198
|
+
const base = {
|
|
3199
|
+
scope: {
|
|
3200
|
+
candidatePairs: 1,
|
|
3201
|
+
queueBefore: {
|
|
3202
|
+
verify: 0,
|
|
3203
|
+
pending: 0,
|
|
3204
|
+
fuse: 1,
|
|
3205
|
+
skipped: 0
|
|
3206
|
+
}
|
|
3207
|
+
},
|
|
3208
|
+
notes: []
|
|
3209
|
+
};
|
|
3210
|
+
const record = await recordRound2Outcome(options.io, options.home, options.now, row, outcome);
|
|
3211
|
+
return {
|
|
3212
|
+
...base,
|
|
3213
|
+
stopReason: record.merged ? "completed" : "failed",
|
|
3214
|
+
pairs: [record.pair],
|
|
3215
|
+
totals: record.merged ? {
|
|
3216
|
+
merged: 1,
|
|
3217
|
+
kept: 0,
|
|
3218
|
+
pending: 0,
|
|
3219
|
+
failed: 0
|
|
3220
|
+
} : {
|
|
3221
|
+
merged: 0,
|
|
3222
|
+
kept: 0,
|
|
3223
|
+
pending: 0,
|
|
3224
|
+
failed: 1
|
|
3225
|
+
}
|
|
3226
|
+
};
|
|
3227
|
+
}
|
|
3228
|
+
});
|
|
2477
3229
|
}
|
|
2478
3230
|
//#endregion
|
|
2479
3231
|
//#region lib/types/resume-command.js
|
|
@@ -2495,6 +3247,7 @@ async function runResumeCommand(options) {
|
|
|
2495
3247
|
approval: true,
|
|
2496
3248
|
platformWrite: options.platformWrite,
|
|
2497
3249
|
now: options.now,
|
|
3250
|
+
...options.detached === true ? { detached: true } : {},
|
|
2498
3251
|
...options.makePorts === void 0 ? { ports: {
|
|
2499
3252
|
...options.ports,
|
|
2500
3253
|
screening: () => Promise.resolve([{
|
|
@@ -2539,6 +3292,7 @@ async function runResumeCommand(options) {
|
|
|
2539
3292
|
mergeApproval: true
|
|
2540
3293
|
},
|
|
2541
3294
|
now: options.now,
|
|
3295
|
+
...options.detached === true ? { detached: true } : {},
|
|
2542
3296
|
async body(ctx) {
|
|
2543
3297
|
await ctx.stage("author", {
|
|
2544
3298
|
done: 0,
|
|
@@ -2655,6 +3409,245 @@ function skillLibraryUndoPort(skills) {
|
|
|
2655
3409
|
};
|
|
2656
3410
|
}
|
|
2657
3411
|
//#endregion
|
|
3412
|
+
//#region lib/types/domain/state-v3.js
|
|
3413
|
+
function v2BackupPath(home) {
|
|
3414
|
+
return join(home, "merge", "merge-state.v2.bak.json");
|
|
3415
|
+
}
|
|
3416
|
+
//#endregion
|
|
3417
|
+
//#region lib/types/domain/consistency.js
|
|
3418
|
+
const ALLOWED = [
|
|
3419
|
+
{
|
|
3420
|
+
row: "verify",
|
|
3421
|
+
run: "none",
|
|
3422
|
+
staging: "none",
|
|
3423
|
+
journal: "none",
|
|
3424
|
+
evidence: "none",
|
|
3425
|
+
activeRuns: 0
|
|
3426
|
+
},
|
|
3427
|
+
{
|
|
3428
|
+
row: "verify",
|
|
3429
|
+
run: "running",
|
|
3430
|
+
staging: "none",
|
|
3431
|
+
journal: "none",
|
|
3432
|
+
evidence: "none",
|
|
3433
|
+
activeRuns: 1
|
|
3434
|
+
},
|
|
3435
|
+
{
|
|
3436
|
+
row: "verify",
|
|
3437
|
+
run: "failed",
|
|
3438
|
+
staging: "none",
|
|
3439
|
+
journal: "none",
|
|
3440
|
+
evidence: "none",
|
|
3441
|
+
activeRuns: 0
|
|
3442
|
+
},
|
|
3443
|
+
{
|
|
3444
|
+
row: "pending",
|
|
3445
|
+
run: "none",
|
|
3446
|
+
staging: "none",
|
|
3447
|
+
journal: "none",
|
|
3448
|
+
evidence: "none",
|
|
3449
|
+
activeRuns: 0
|
|
3450
|
+
},
|
|
3451
|
+
{
|
|
3452
|
+
row: "pending",
|
|
3453
|
+
run: "awaiting-approval",
|
|
3454
|
+
staging: "none",
|
|
3455
|
+
journal: "none",
|
|
3456
|
+
evidence: "none",
|
|
3457
|
+
activeRuns: 1
|
|
3458
|
+
},
|
|
3459
|
+
{
|
|
3460
|
+
row: "approved",
|
|
3461
|
+
run: "none",
|
|
3462
|
+
staging: "none",
|
|
3463
|
+
journal: "none",
|
|
3464
|
+
evidence: "none",
|
|
3465
|
+
activeRuns: 0
|
|
3466
|
+
},
|
|
3467
|
+
{
|
|
3468
|
+
row: "approved",
|
|
3469
|
+
run: "running",
|
|
3470
|
+
staging: "none",
|
|
3471
|
+
journal: "none",
|
|
3472
|
+
evidence: "none",
|
|
3473
|
+
activeRuns: 1
|
|
3474
|
+
},
|
|
3475
|
+
{
|
|
3476
|
+
row: "fuse",
|
|
3477
|
+
run: "none",
|
|
3478
|
+
staging: "kept",
|
|
3479
|
+
journal: "none",
|
|
3480
|
+
evidence: "none",
|
|
3481
|
+
activeRuns: 0
|
|
3482
|
+
},
|
|
3483
|
+
{
|
|
3484
|
+
row: "fuse",
|
|
3485
|
+
run: "running",
|
|
3486
|
+
staging: "kept",
|
|
3487
|
+
journal: "none",
|
|
3488
|
+
evidence: "none",
|
|
3489
|
+
activeRuns: 1
|
|
3490
|
+
},
|
|
3491
|
+
{
|
|
3492
|
+
row: "fuse",
|
|
3493
|
+
run: "failed",
|
|
3494
|
+
staging: "kept",
|
|
3495
|
+
journal: "none",
|
|
3496
|
+
evidence: "none",
|
|
3497
|
+
activeRuns: 0
|
|
3498
|
+
},
|
|
3499
|
+
{
|
|
3500
|
+
row: "fuse",
|
|
3501
|
+
run: "orphan",
|
|
3502
|
+
staging: "kept",
|
|
3503
|
+
journal: "none",
|
|
3504
|
+
evidence: "none",
|
|
3505
|
+
activeRuns: 0
|
|
3506
|
+
},
|
|
3507
|
+
{
|
|
3508
|
+
row: "fuse",
|
|
3509
|
+
run: "none",
|
|
3510
|
+
staging: "none",
|
|
3511
|
+
journal: "none",
|
|
3512
|
+
evidence: "none",
|
|
3513
|
+
activeRuns: 0,
|
|
3514
|
+
recovery: true
|
|
3515
|
+
},
|
|
3516
|
+
{
|
|
3517
|
+
row: "fuse",
|
|
3518
|
+
run: "failed",
|
|
3519
|
+
staging: "none",
|
|
3520
|
+
journal: "none",
|
|
3521
|
+
evidence: "none",
|
|
3522
|
+
activeRuns: 0,
|
|
3523
|
+
recovery: true
|
|
3524
|
+
},
|
|
3525
|
+
{
|
|
3526
|
+
row: "fuse",
|
|
3527
|
+
run: "orphan",
|
|
3528
|
+
staging: "none",
|
|
3529
|
+
journal: "none",
|
|
3530
|
+
evidence: "none",
|
|
3531
|
+
activeRuns: 0,
|
|
3532
|
+
recovery: true
|
|
3533
|
+
},
|
|
3534
|
+
{
|
|
3535
|
+
row: "fuse",
|
|
3536
|
+
run: "running",
|
|
3537
|
+
staging: "none",
|
|
3538
|
+
journal: "none",
|
|
3539
|
+
evidence: "none",
|
|
3540
|
+
activeRuns: 1,
|
|
3541
|
+
recovery: true
|
|
3542
|
+
},
|
|
3543
|
+
{
|
|
3544
|
+
row: "fuse",
|
|
3545
|
+
run: "none",
|
|
3546
|
+
staging: "missing",
|
|
3547
|
+
journal: "none",
|
|
3548
|
+
evidence: "none",
|
|
3549
|
+
activeRuns: 0,
|
|
3550
|
+
recovery: true
|
|
3551
|
+
},
|
|
3552
|
+
{
|
|
3553
|
+
row: "fuse",
|
|
3554
|
+
run: "failed",
|
|
3555
|
+
staging: "missing",
|
|
3556
|
+
journal: "none",
|
|
3557
|
+
evidence: "none",
|
|
3558
|
+
activeRuns: 0,
|
|
3559
|
+
recovery: true
|
|
3560
|
+
},
|
|
3561
|
+
{
|
|
3562
|
+
row: "fuse",
|
|
3563
|
+
run: "orphan",
|
|
3564
|
+
staging: "missing",
|
|
3565
|
+
journal: "none",
|
|
3566
|
+
evidence: "none",
|
|
3567
|
+
activeRuns: 0,
|
|
3568
|
+
recovery: true
|
|
3569
|
+
},
|
|
3570
|
+
{
|
|
3571
|
+
row: "fuse",
|
|
3572
|
+
run: "running",
|
|
3573
|
+
staging: "missing",
|
|
3574
|
+
journal: "none",
|
|
3575
|
+
evidence: "none",
|
|
3576
|
+
activeRuns: 1,
|
|
3577
|
+
recovery: true
|
|
3578
|
+
},
|
|
3579
|
+
{
|
|
3580
|
+
row: "fuse",
|
|
3581
|
+
run: "running",
|
|
3582
|
+
staging: "kept",
|
|
3583
|
+
journal: "committing",
|
|
3584
|
+
evidence: "none",
|
|
3585
|
+
activeRuns: 1
|
|
3586
|
+
},
|
|
3587
|
+
{
|
|
3588
|
+
row: "skipped",
|
|
3589
|
+
run: "none",
|
|
3590
|
+
staging: "none",
|
|
3591
|
+
journal: "none",
|
|
3592
|
+
evidence: "none",
|
|
3593
|
+
activeRuns: 0
|
|
3594
|
+
},
|
|
3595
|
+
{
|
|
3596
|
+
row: "merged",
|
|
3597
|
+
run: "succeeded",
|
|
3598
|
+
staging: "cleaned",
|
|
3599
|
+
journal: "committed",
|
|
3600
|
+
evidence: "receipt",
|
|
3601
|
+
activeRuns: 0
|
|
3602
|
+
},
|
|
3603
|
+
{
|
|
3604
|
+
row: "merged",
|
|
3605
|
+
run: "succeeded",
|
|
3606
|
+
staging: "kept",
|
|
3607
|
+
journal: "committed",
|
|
3608
|
+
evidence: "receipt",
|
|
3609
|
+
activeRuns: 0
|
|
3610
|
+
}
|
|
3611
|
+
];
|
|
3612
|
+
function same(a, b) {
|
|
3613
|
+
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);
|
|
3614
|
+
}
|
|
3615
|
+
function validateArtifacts(state) {
|
|
3616
|
+
if (state.activeRuns > 1) return {
|
|
3617
|
+
ok: false,
|
|
3618
|
+
reason: "multiple-active-runs"
|
|
3619
|
+
};
|
|
3620
|
+
if (state.activeRuns > 0 && state.hasOwner === false) return {
|
|
3621
|
+
ok: false,
|
|
3622
|
+
reason: "active-run-no-owner"
|
|
3623
|
+
};
|
|
3624
|
+
if (ALLOWED.some((item) => same(state, item))) return { ok: true };
|
|
3625
|
+
if (state.row === "pending" && state.staging !== "none") return {
|
|
3626
|
+
ok: false,
|
|
3627
|
+
reason: "pending-staging"
|
|
3628
|
+
};
|
|
3629
|
+
if (state.row === "skipped" && state.run === "running") return {
|
|
3630
|
+
ok: false,
|
|
3631
|
+
reason: "skipped-running"
|
|
3632
|
+
};
|
|
3633
|
+
if (state.row === "fuse" && state.staging === "missing" && state.recovery !== true) return {
|
|
3634
|
+
ok: false,
|
|
3635
|
+
reason: "fuse-lost-staging"
|
|
3636
|
+
};
|
|
3637
|
+
if (state.journal === "committed" && state.evidence !== "receipt") return {
|
|
3638
|
+
ok: false,
|
|
3639
|
+
reason: "committed-no-evidence"
|
|
3640
|
+
};
|
|
3641
|
+
if (state.row === "merged" && state.journal === "rolled-back") return {
|
|
3642
|
+
ok: false,
|
|
3643
|
+
reason: "committed-rollback"
|
|
3644
|
+
};
|
|
3645
|
+
return {
|
|
3646
|
+
ok: false,
|
|
3647
|
+
reason: "not-allowed"
|
|
3648
|
+
};
|
|
3649
|
+
}
|
|
3650
|
+
//#endregion
|
|
2658
3651
|
//#region lib/types/doctor.js
|
|
2659
3652
|
/**
|
|
2660
3653
|
* Merge doctor rows: state, pending, gate cache and staging are each probed read-only.
|
|
@@ -2662,6 +3655,13 @@ function skillLibraryUndoPort(skills) {
|
|
|
2662
3655
|
* operator can act on. This module never repairs or writes.
|
|
2663
3656
|
*/
|
|
2664
3657
|
const MERGE_DOCTOR_SCHEMA_VERSION = 3;
|
|
3658
|
+
/** Global doctor host section: a missing merge mount must fail with E-325, not render empty. */
|
|
3659
|
+
function mergeDoctorAvailability(mounted) {
|
|
3660
|
+
return mounted ? { ok: true } : {
|
|
3661
|
+
ok: false,
|
|
3662
|
+
code: "E-325"
|
|
3663
|
+
};
|
|
3664
|
+
}
|
|
2665
3665
|
async function mergeDoctor(io, home, configuredRoot) {
|
|
2666
3666
|
const rows = [];
|
|
2667
3667
|
const loaded = await loadMergeState(io, home);
|
|
@@ -2672,12 +3672,120 @@ async function mergeDoctor(io, home, configuredRoot) {
|
|
|
2672
3672
|
status: loaded.note === void 0 ? "ok" : "warn",
|
|
2673
3673
|
detail: loaded.note ?? "pairs " + String(loaded.state.rows.length) + ";verify " + String(counts.verify) + " / pending " + String(counts.pending) + " / fuse " + String(counts.fuse) + " / skipped " + String(counts.skipped)
|
|
2674
3674
|
});
|
|
2675
|
-
const
|
|
2676
|
-
|
|
3675
|
+
const recoveryRuns = /* @__PURE__ */ new Set();
|
|
3676
|
+
try {
|
|
3677
|
+
const raw = await io.readText(join(home, "merge", "recovery.json"));
|
|
3678
|
+
if (raw !== null) {
|
|
3679
|
+
for (const report of JSON.parse(raw)) if (report.ok === true && report.runId !== void 0) recoveryRuns.add(report.runId);
|
|
3680
|
+
}
|
|
3681
|
+
} catch {}
|
|
3682
|
+
const receiptRuns = /* @__PURE__ */ new Set();
|
|
3683
|
+
const receiptDir = await probeList(io, join(home, "merge", "receipts"));
|
|
3684
|
+
if (receiptDir.kind === "present") for (const name of receiptDir.value) receiptRuns.add(name);
|
|
3685
|
+
const invalid = [];
|
|
3686
|
+
for (const row of loaded.state.rows) {
|
|
3687
|
+
const run = row.state === "pending" && row.runId !== void 0 ? "awaiting-approval" : row.runId === void 0 ? "none" : "running";
|
|
3688
|
+
const staging = row.stagingRef !== void 0 ? "kept" : row.state === "fuse" ? "missing" : "none";
|
|
3689
|
+
const phase = row.runId === void 0 ? void 0 : (await readJournal(io, home, row.runId))?.phase;
|
|
3690
|
+
const journal = phase === void 0 || phase === null ? "none" : phase === "committed" ? "committed" : phase === "prepared" ? "prepared" : phase === "rolled-back" ? "rolled-back" : phase === "failed" ? "failed" : "committing";
|
|
3691
|
+
const evidence = row.runId !== void 0 && receiptRuns.has(row.runId) ? "receipt" : "none";
|
|
3692
|
+
const recovery = row.runId !== void 0 && recoveryRuns.has(row.runId);
|
|
3693
|
+
if (!validateArtifacts({
|
|
3694
|
+
row: row.state,
|
|
3695
|
+
run,
|
|
3696
|
+
staging,
|
|
3697
|
+
journal,
|
|
3698
|
+
evidence,
|
|
3699
|
+
activeRuns: run === "running" || run === "awaiting-approval" ? 1 : 0,
|
|
3700
|
+
recovery
|
|
3701
|
+
}).ok) invalid.push(row);
|
|
3702
|
+
}
|
|
3703
|
+
const schema = await loadStateSchema(io, home);
|
|
3704
|
+
rows.push({
|
|
3705
|
+
key: "merge-state-v3",
|
|
3706
|
+
label: "state schema",
|
|
3707
|
+
status: schema === 3 ? "ok" : schema === 2 ? "warn" : "unknown",
|
|
3708
|
+
detail: schema === 3 ? "schemaVersion 3" : schema === 2 ? "schemaVersion 2;等待 v3 迁移" : "schemaVersion 未知/不可读"
|
|
3709
|
+
});
|
|
3710
|
+
let quarantineCount = 0;
|
|
3711
|
+
try {
|
|
3712
|
+
const qraw = await io.readText(join(home, "merge", "merge-state.quarantine.json"));
|
|
3713
|
+
if (qraw !== null) {
|
|
3714
|
+
const parsed = JSON.parse(qraw);
|
|
3715
|
+
quarantineCount = Array.isArray(parsed) ? parsed.length : 0;
|
|
3716
|
+
}
|
|
3717
|
+
} catch {
|
|
3718
|
+
quarantineCount = -1;
|
|
3719
|
+
}
|
|
3720
|
+
rows.push({
|
|
3721
|
+
key: "quarantine",
|
|
3722
|
+
label: "quarantine",
|
|
3723
|
+
status: quarantineCount < 0 ? "unknown" : quarantineCount === 0 ? "ok" : "warn",
|
|
3724
|
+
detail: quarantineCount < 0 ? "quarantine 不可读" : quarantineCount + " 行待人工检查"
|
|
3725
|
+
});
|
|
3726
|
+
const holder = await readTreeLock(io, home).catch(() => null);
|
|
3727
|
+
rows.push({
|
|
3728
|
+
key: "tree-lock",
|
|
3729
|
+
label: "tree lock",
|
|
3730
|
+
status: holder == null ? "ok" : "warn",
|
|
3731
|
+
detail: holder == null ? "free" : "holder pid=" + String(holder.pid) + " epoch=" + String(holder.epoch) + " token=" + holder.token.slice(0, 8)
|
|
3732
|
+
});
|
|
3733
|
+
rows.push({
|
|
3734
|
+
key: "backup",
|
|
3735
|
+
label: "v2 backup",
|
|
3736
|
+
status: await io.exists(v2BackupPath(home)) ? "ok" : "unknown",
|
|
3737
|
+
detail: await io.exists(v2BackupPath(home)) ? "merge-state.v2.bak.json" : "no v2 backup (not migrated or already cleaned)"
|
|
3738
|
+
});
|
|
3739
|
+
const claims = await probeList(io, join(home, "merge", "claims"));
|
|
3740
|
+
rows.push({
|
|
3741
|
+
key: "target-claim",
|
|
3742
|
+
label: "target claim",
|
|
3743
|
+
status: claims.kind === "unknown" ? "unknown" : "ok",
|
|
3744
|
+
detail: claims.kind === "unknown" ? "claims unreadable" : claims.kind === "absent" ? "no claims" : String(claims.value.length) + " claim files"
|
|
3745
|
+
});
|
|
3746
|
+
let orphanRuns = 0;
|
|
3747
|
+
let runCount = 0;
|
|
3748
|
+
try {
|
|
3749
|
+
const raw = await io.readText(runsFile(home));
|
|
3750
|
+
if (raw !== null) {
|
|
3751
|
+
const parsed = JSON.parse(raw);
|
|
3752
|
+
runCount = parsed.runs?.length ?? 0;
|
|
3753
|
+
for (const run of parsed.runs ?? []) {
|
|
3754
|
+
if (run.state !== "running" || typeof run.pid !== "number") continue;
|
|
3755
|
+
try {
|
|
3756
|
+
process.kill(run.pid, 0);
|
|
3757
|
+
} catch {
|
|
3758
|
+
orphanRuns += 1;
|
|
3759
|
+
}
|
|
3760
|
+
}
|
|
3761
|
+
}
|
|
3762
|
+
} catch {
|
|
3763
|
+
orphanRuns = -1;
|
|
3764
|
+
}
|
|
3765
|
+
rows.push({
|
|
3766
|
+
key: "runs",
|
|
3767
|
+
label: "run owner",
|
|
3768
|
+
status: orphanRuns < 0 ? "unknown" : orphanRuns === 0 ? "ok" : "warn",
|
|
3769
|
+
detail: orphanRuns < 0 ? "runs.json unreadable" : String(runCount) + " runs; " + String(orphanRuns) + " dead-owner running"
|
|
3770
|
+
});
|
|
3771
|
+
rows.push({
|
|
3772
|
+
key: "install",
|
|
3773
|
+
label: "install form",
|
|
3774
|
+
status: io.exists === void 0 ? "unknown" : "ok",
|
|
3775
|
+
detail: "merge plugin loaded"
|
|
3776
|
+
});
|
|
3777
|
+
rows.push({
|
|
3778
|
+
key: "consistency",
|
|
3779
|
+
label: "一致性",
|
|
3780
|
+
status: invalid.length === 0 ? "ok" : "warn",
|
|
3781
|
+
detail: invalid.length === 0 ? "无非法组合" : String(invalid.length) + " 行需人工检查: " + invalid.map((row) => row.id).join(", ")
|
|
3782
|
+
});
|
|
3783
|
+
const modelRoot = configuredRoot?.trim() ?? "";
|
|
3784
|
+
if (modelRoot === "") rows.push({
|
|
2677
3785
|
key: "models",
|
|
2678
3786
|
label: "合并模型",
|
|
2679
3787
|
status: "warn",
|
|
2680
|
-
detail: "
|
|
3788
|
+
detail: "未配置 mergeModelsRoot;合并扫描会 fail loud"
|
|
2681
3789
|
});
|
|
2682
3790
|
else {
|
|
2683
3791
|
const int8 = join(modelRoot, "potion-multilingual-128M-int8");
|
|
@@ -2704,12 +3812,32 @@ async function mergeDoctor(io, home, configuredRoot) {
|
|
|
2704
3812
|
status: "ok",
|
|
2705
3813
|
detail: stagingRoot(home, "<runId>")
|
|
2706
3814
|
});
|
|
3815
|
+
try {
|
|
3816
|
+
const raw = await io.readText(join(home, "merge", "recovery.json"));
|
|
3817
|
+
if (raw !== null) {
|
|
3818
|
+
const reports = JSON.parse(raw);
|
|
3819
|
+
const failed = reports.filter((report) => report.ok !== true);
|
|
3820
|
+
rows.push({
|
|
3821
|
+
key: "recovery",
|
|
3822
|
+
label: "提交恢复",
|
|
3823
|
+
status: failed.length === 0 ? "ok" : "warn",
|
|
3824
|
+
detail: failed.length === 0 ? String(reports.length) + " 条恢复报告,无待处理" : String(failed.length) + " 条待处理:" + failed.map((report) => (report.runId ?? "?") + "=" + (report.action ?? "?")).join(", ")
|
|
3825
|
+
});
|
|
3826
|
+
}
|
|
3827
|
+
} catch {
|
|
3828
|
+
rows.push({
|
|
3829
|
+
key: "recovery",
|
|
3830
|
+
label: "提交恢复",
|
|
3831
|
+
status: "unknown",
|
|
3832
|
+
detail: "无法读取 merge/recovery.json"
|
|
3833
|
+
});
|
|
3834
|
+
}
|
|
2707
3835
|
return rows;
|
|
2708
3836
|
}
|
|
2709
3837
|
//#endregion
|
|
2710
3838
|
//#region lib/types/decision.js
|
|
2711
3839
|
function extractJson(text) {
|
|
2712
|
-
const body =
|
|
3840
|
+
const body = /`\``(?:json)?\s*([\s\S]*?)`\``/i.exec(text)?.[1] ?? text;
|
|
2713
3841
|
const start = body.indexOf("{");
|
|
2714
3842
|
if (start < 0) return void 0;
|
|
2715
3843
|
let depth = 0;
|
|
@@ -3236,12 +4364,17 @@ async function assembleStagedCommit(io, home, runId, port, options) {
|
|
|
3236
4364
|
const requested = staging.value.manifest.target;
|
|
3237
4365
|
let target = requested;
|
|
3238
4366
|
let targetCurrent = await port.readSkill(requested);
|
|
4367
|
+
if (staging.value.manifest.mode !== "create-umbrella" && targetCurrent === null) return {
|
|
4368
|
+
ok: false,
|
|
4369
|
+
message: "target is gone; refusing to re-create it: " + requested
|
|
4370
|
+
};
|
|
3239
4371
|
const requestedArchived = targetCurrent === null && await port.isArchived?.(requested) === true;
|
|
3240
|
-
|
|
4372
|
+
const requestedOccupied = targetCurrent === null && await port.isNameOccupied?.(requested) === true;
|
|
4373
|
+
if (staging.value.manifest.mode === "create-umbrella" && (targetCurrent !== null || requestedArchived || requestedOccupied)) {
|
|
3241
4374
|
target = requested;
|
|
3242
4375
|
for (let suffix = 2; suffix <= 99; suffix += 1) {
|
|
3243
4376
|
const candidate = requested + "-v" + String(suffix);
|
|
3244
|
-
if (await port.readSkill(candidate) === null && await port.isArchived?.(candidate) !== true) {
|
|
4377
|
+
if (await port.readSkill(candidate) === null && await port.isArchived?.(candidate) !== true && await port.isNameOccupied?.(candidate) !== true) {
|
|
3245
4378
|
target = candidate;
|
|
3246
4379
|
targetCurrent = null;
|
|
3247
4380
|
break;
|
|
@@ -3286,27 +4419,87 @@ async function commitStagedRun(io, home, runId, port, options) {
|
|
|
3286
4419
|
ok: false,
|
|
3287
4420
|
message: assembled.message
|
|
3288
4421
|
};
|
|
3289
|
-
|
|
3290
|
-
|
|
3291
|
-
|
|
3292
|
-
|
|
3293
|
-
|
|
3294
|
-
message: "commit snapshot failed; refusing to write: " + (error instanceof Error ? error.message : String(error)),
|
|
3295
|
-
plan: assembled.plan
|
|
3296
|
-
};
|
|
3297
|
-
}
|
|
4422
|
+
if (await options.isBlocked?.(assembled.plan.target) === true) return {
|
|
4423
|
+
ok: false,
|
|
4424
|
+
message: "target is blocked by curator: " + assembled.plan.target,
|
|
4425
|
+
plan: assembled.plan
|
|
4426
|
+
};
|
|
3298
4427
|
const journal = {
|
|
3299
4428
|
runId,
|
|
3300
4429
|
target: assembled.plan.target,
|
|
3301
4430
|
create: !assembled.plan.targetExists,
|
|
3302
4431
|
sources: assembled.plan.sources,
|
|
3303
|
-
phase: "
|
|
4432
|
+
phase: "prepared",
|
|
3304
4433
|
updatedAt: new Date(options.now()).toISOString()
|
|
3305
4434
|
};
|
|
3306
|
-
|
|
3307
|
-
|
|
3308
|
-
|
|
3309
|
-
|
|
4435
|
+
let result;
|
|
4436
|
+
const transaction = options.unit ?? port.unit;
|
|
4437
|
+
if (transaction !== void 0) {
|
|
4438
|
+
const first = assembled.plan.sources[0] ?? assembled.plan.target;
|
|
4439
|
+
const unitPlan = {
|
|
4440
|
+
runId,
|
|
4441
|
+
pairKey: pairKey(assembled.plan.target, first),
|
|
4442
|
+
target: assembled.plan.target,
|
|
4443
|
+
targetExists: assembled.plan.targetExists,
|
|
4444
|
+
sources: assembled.plan.sources,
|
|
4445
|
+
body: assembled.plan.body,
|
|
4446
|
+
supportFiles: assembled.plan.supportFiles,
|
|
4447
|
+
expectedTargetBody: assembled.plan.expectedTargetBody,
|
|
4448
|
+
expectedSourceBodies: assembled.plan.expectedSourceBodies,
|
|
4449
|
+
origin: options.origin,
|
|
4450
|
+
summary: options.summary
|
|
4451
|
+
};
|
|
4452
|
+
try {
|
|
4453
|
+
await writeJournal(io, home, journal);
|
|
4454
|
+
} catch {}
|
|
4455
|
+
const prepared = await transaction.prepare(unitPlan);
|
|
4456
|
+
if (!prepared.ok) return {
|
|
4457
|
+
ok: false,
|
|
4458
|
+
message: prepared.message,
|
|
4459
|
+
plan: assembled.plan
|
|
4460
|
+
};
|
|
4461
|
+
const committed = await transaction.commit(prepared.prepared);
|
|
4462
|
+
if (!committed.ok) {
|
|
4463
|
+
try {
|
|
4464
|
+
await writeJournal(io, home, {
|
|
4465
|
+
...journal,
|
|
4466
|
+
phase: "failed",
|
|
4467
|
+
updatedAt: new Date(options.now()).toISOString()
|
|
4468
|
+
});
|
|
4469
|
+
} catch {}
|
|
4470
|
+
return {
|
|
4471
|
+
ok: false,
|
|
4472
|
+
message: committed.message,
|
|
4473
|
+
plan: assembled.plan
|
|
4474
|
+
};
|
|
4475
|
+
}
|
|
4476
|
+
result = {
|
|
4477
|
+
ok: true,
|
|
4478
|
+
message: "merged",
|
|
4479
|
+
unitId: committed.evidence.unitId,
|
|
4480
|
+
archived: committed.evidence.sources.filter((source) => source.archived).map((source) => source.name),
|
|
4481
|
+
mutations: committed.evidence.mutations
|
|
4482
|
+
};
|
|
4483
|
+
} else {
|
|
4484
|
+
try {
|
|
4485
|
+
await writeCommitSnapshot(io, home, runId, assembled.plan, options.now(), port);
|
|
4486
|
+
} catch (error) {
|
|
4487
|
+
return {
|
|
4488
|
+
ok: false,
|
|
4489
|
+
message: "commit snapshot failed; refusing to write: " + (error instanceof Error ? error.message : String(error)),
|
|
4490
|
+
plan: assembled.plan
|
|
4491
|
+
};
|
|
4492
|
+
}
|
|
4493
|
+
try {
|
|
4494
|
+
await writeJournal(io, home, journal);
|
|
4495
|
+
} catch {}
|
|
4496
|
+
if (port.commitMerge === void 0) return {
|
|
4497
|
+
ok: false,
|
|
4498
|
+
message: "commit port missing: UnitTransaction or commitMerge required",
|
|
4499
|
+
plan: assembled.plan
|
|
4500
|
+
};
|
|
4501
|
+
result = await port.commitMerge(assembled.plan);
|
|
4502
|
+
}
|
|
3310
4503
|
if (!result.ok) {
|
|
3311
4504
|
try {
|
|
3312
4505
|
await writeJournal(io, home, {
|
|
@@ -3324,7 +4517,8 @@ async function commitStagedRun(io, home, runId, port, options) {
|
|
|
3324
4517
|
const committed = {
|
|
3325
4518
|
...journal,
|
|
3326
4519
|
phase: "committed",
|
|
3327
|
-
updatedAt: new Date(options.now()).toISOString()
|
|
4520
|
+
updatedAt: new Date(options.now()).toISOString(),
|
|
4521
|
+
...result.unitId === void 0 ? {} : { unitId: result.unitId }
|
|
3328
4522
|
};
|
|
3329
4523
|
try {
|
|
3330
4524
|
await writeJournal(io, home, committed);
|
|
@@ -3370,13 +4564,25 @@ async function commitStagedRun(io, home, runId, port, options) {
|
|
|
3370
4564
|
//#region lib/types/authoring-flow.js
|
|
3371
4565
|
async function runAuthoring(options) {
|
|
3372
4566
|
const targetCard = options.cards.find((card) => card.name === options.target);
|
|
3373
|
-
|
|
4567
|
+
const mode = options.mode ?? (targetCard === void 0 ? "create-umbrella" : "merge-into-existing");
|
|
4568
|
+
const createMode = mode === "create-umbrella";
|
|
4569
|
+
if (!createMode && targetCard !== void 0) {
|
|
4570
|
+
if (targetCard.managed !== true) return {
|
|
4571
|
+
ok: false,
|
|
4572
|
+
message: "target is not family-managed: " + options.target
|
|
4573
|
+
};
|
|
4574
|
+
if (targetCard.protectionUnknown === true || targetCard.protectedBy != null) return {
|
|
4575
|
+
ok: false,
|
|
4576
|
+
message: "target is protected (" + (targetCard.protectedBy ?? "unknown") + "): " + options.target
|
|
4577
|
+
};
|
|
4578
|
+
}
|
|
4579
|
+
if (!createMode && await options.isBlocked?.(options.target) === true) return {
|
|
3374
4580
|
ok: false,
|
|
3375
|
-
message: "target is
|
|
4581
|
+
message: "target is blocked by curator: " + options.target
|
|
3376
4582
|
};
|
|
3377
4583
|
const stageKey = options.stageKey ?? options.runId;
|
|
3378
4584
|
if (options.reuseStaging === true) {
|
|
3379
|
-
if (!await setStagingTarget(options.io, options.home, stageKey, options.target,
|
|
4585
|
+
if (!await setStagingTarget(options.io, options.home, stageKey, options.target, mode)) return {
|
|
3380
4586
|
ok: false,
|
|
3381
4587
|
message: "staging missing or unreadable: " + stageKey
|
|
3382
4588
|
};
|
|
@@ -3384,7 +4590,7 @@ async function runAuthoring(options) {
|
|
|
3384
4590
|
const created = await createStaging(options.io, options.home, {
|
|
3385
4591
|
runId: stageKey,
|
|
3386
4592
|
target: options.target,
|
|
3387
|
-
|
|
4593
|
+
mode,
|
|
3388
4594
|
sources: options.cards.map((card) => ({
|
|
3389
4595
|
name: card.name,
|
|
3390
4596
|
body: card.body,
|
|
@@ -3422,9 +4628,11 @@ async function runAuthoring(options) {
|
|
|
3422
4628
|
findings
|
|
3423
4629
|
};
|
|
3424
4630
|
const commit = await commitStagedRun(options.io, options.home, stageKey, options.commitPort, {
|
|
3425
|
-
origin: "
|
|
4631
|
+
origin: "foreground",
|
|
3426
4632
|
summary: options.summary,
|
|
3427
|
-
now: options.now
|
|
4633
|
+
now: options.now,
|
|
4634
|
+
...options.isBlocked === void 0 ? {} : { isBlocked: options.isBlocked },
|
|
4635
|
+
...options.unit === void 0 ? {} : { unit: options.unit }
|
|
3428
4636
|
});
|
|
3429
4637
|
if (!commit.ok) return {
|
|
3430
4638
|
ok: false,
|
|
@@ -3550,7 +4758,9 @@ function createChannelScanPorts(options) {
|
|
|
3550
4758
|
mode: decision.mode,
|
|
3551
4759
|
cards: options.cards,
|
|
3552
4760
|
commitPort: options.commitPort,
|
|
4761
|
+
...options.unit === void 0 ? {} : { unit: options.unit },
|
|
3553
4762
|
now: options.now,
|
|
4763
|
+
...options.isBlocked === void 0 ? {} : { isBlocked: options.isBlocked },
|
|
3554
4764
|
summary: "merge " + row.a + " + " + row.b,
|
|
3555
4765
|
author: async () => {
|
|
3556
4766
|
await entry.result.author?.(row, decision, signal);
|
|
@@ -3589,12 +4799,35 @@ function createChannelScanPorts(options) {
|
|
|
3589
4799
|
//#region lib/types/new-child-authoring.js
|
|
3590
4800
|
async function runNewChildAuthoring(options, row, decision, signal) {
|
|
3591
4801
|
const targetCard = options.cards.find((card) => card.name === decision.target);
|
|
3592
|
-
|
|
4802
|
+
const createMode = decision.mode === "create-umbrella";
|
|
4803
|
+
if (!createMode && targetCard !== void 0) {
|
|
4804
|
+
if (targetCard.managed !== true) return {
|
|
4805
|
+
kind: "failed",
|
|
4806
|
+
failure: {
|
|
4807
|
+
stage: "author",
|
|
4808
|
+
code: "target-not-managed",
|
|
4809
|
+
message: "target is not family-managed: " + decision.target,
|
|
4810
|
+
retryable: false,
|
|
4811
|
+
retryFrom: "none"
|
|
4812
|
+
}
|
|
4813
|
+
};
|
|
4814
|
+
if (targetCard.protectionUnknown === true || targetCard.protectedBy != null) return {
|
|
4815
|
+
kind: "failed",
|
|
4816
|
+
failure: {
|
|
4817
|
+
stage: "author",
|
|
4818
|
+
code: "target-protected",
|
|
4819
|
+
message: "target is protected: " + decision.target,
|
|
4820
|
+
retryable: false,
|
|
4821
|
+
retryFrom: "none"
|
|
4822
|
+
}
|
|
4823
|
+
};
|
|
4824
|
+
}
|
|
4825
|
+
if (!createMode && await options.isBlocked?.(decision.target) === true) return {
|
|
3593
4826
|
kind: "failed",
|
|
3594
4827
|
failure: {
|
|
3595
4828
|
stage: "author",
|
|
3596
|
-
code: "target-
|
|
3597
|
-
message: "target is
|
|
4829
|
+
code: "target-blocked",
|
|
4830
|
+
message: "target is blocked by curator: " + decision.target,
|
|
3598
4831
|
retryable: false,
|
|
3599
4832
|
retryFrom: "none"
|
|
3600
4833
|
}
|
|
@@ -3709,9 +4942,11 @@ async function runNewChildAuthoring(options, row, decision, signal) {
|
|
|
3709
4942
|
}
|
|
3710
4943
|
};
|
|
3711
4944
|
const commit = await commitStagedRun(options.io, options.home, stageKey, options.commitPort, {
|
|
3712
|
-
origin: "
|
|
4945
|
+
origin: "foreground",
|
|
3713
4946
|
summary: "merge " + row.a + " + " + row.b,
|
|
3714
|
-
now: options.now
|
|
4947
|
+
now: options.now,
|
|
4948
|
+
...options.isBlocked === void 0 ? {} : { isBlocked: options.isBlocked },
|
|
4949
|
+
...options.unit === void 0 ? {} : { unit: options.unit }
|
|
3715
4950
|
});
|
|
3716
4951
|
if (!commit.ok) return {
|
|
3717
4952
|
kind: "failed",
|
|
@@ -3758,7 +4993,10 @@ function skillLibraryPort(skills) {
|
|
|
3758
4993
|
const names = await archivedNames.call(skills);
|
|
3759
4994
|
return names === null ? true : names.includes(name);
|
|
3760
4995
|
},
|
|
3761
|
-
|
|
4996
|
+
isNameOccupied: async (name) => {
|
|
4997
|
+
const probe = skills.skillDirExists;
|
|
4998
|
+
return typeof probe === "function" ? await probe.call(skills, name) : false;
|
|
4999
|
+
},
|
|
3762
5000
|
async readSupportFiles(name) {
|
|
3763
5001
|
if (typeof skills.listSupportFiles !== "function") return [];
|
|
3764
5002
|
const probe = await skills.listSupportFiles(name);
|
|
@@ -3792,6 +5030,17 @@ function forcedDecisionFor(a, b) {
|
|
|
3792
5030
|
rationale: "forced pair merge of " + sorted.join(" and ")
|
|
3793
5031
|
};
|
|
3794
5032
|
}
|
|
5033
|
+
function unitFor(deps) {
|
|
5034
|
+
if (deps.unit !== void 0) return deps.unit;
|
|
5035
|
+
if (!(deps.skills instanceof SkillLibrary)) return void 0;
|
|
5036
|
+
return new UnitTransactions({
|
|
5037
|
+
io: deps.io,
|
|
5038
|
+
home: deps.home,
|
|
5039
|
+
backend: skillLibraryUnitBackend(deps.skills),
|
|
5040
|
+
claim: targetClaimUnitPort(deps.io, deps.home, deps.now),
|
|
5041
|
+
now: deps.now
|
|
5042
|
+
});
|
|
5043
|
+
}
|
|
3795
5044
|
function createMergeRuntime(deps) {
|
|
3796
5045
|
const readOnly = createReadOnlyMergeService(deps.io, deps.home);
|
|
3797
5046
|
const timeout = deps.timeoutMinutes > 0 ? deps.timeoutMinutes : void 0;
|
|
@@ -3842,7 +5091,12 @@ function createMergeRuntime(deps) {
|
|
|
3842
5091
|
b: command.b
|
|
3843
5092
|
}];
|
|
3844
5093
|
const existing = (await loadMergeState(deps.io, deps.home)).state.rows.find((row) => row.pairKey === pairKey(command.a, command.b));
|
|
3845
|
-
if (existing?.state === "skipped")
|
|
5094
|
+
if (existing?.state === "skipped") {
|
|
5095
|
+
if (!await dropRowV3(deps.io, deps.home, existing.id, "superseded")) return {
|
|
5096
|
+
ok: false,
|
|
5097
|
+
text: "row changed before cleanup: " + existing.id
|
|
5098
|
+
};
|
|
5099
|
+
}
|
|
3846
5100
|
if (existing?.state === "pending") return {
|
|
3847
5101
|
ok: false,
|
|
3848
5102
|
text: "该 pair 已在待批准队列,先 approve/reject/discard: " + existing.id
|
|
@@ -3872,6 +5126,8 @@ function createMergeRuntime(deps) {
|
|
|
3872
5126
|
approval,
|
|
3873
5127
|
commitPort: skillLibraryPort(deps.skills),
|
|
3874
5128
|
now: deps.now,
|
|
5129
|
+
...unitFor(deps) === void 0 ? {} : { unit: unitFor(deps) },
|
|
5130
|
+
...deps.isBlocked === void 0 ? {} : { isBlocked: deps.isBlocked },
|
|
3875
5131
|
...stageHooks
|
|
3876
5132
|
});
|
|
3877
5133
|
return forced === void 0 ? ports : {
|
|
@@ -3937,6 +5193,8 @@ function createMergeRuntime(deps) {
|
|
|
3937
5193
|
approval: deps.params.mergeApproval,
|
|
3938
5194
|
commitPort: skillLibraryPort(deps.skills),
|
|
3939
5195
|
now: deps.now,
|
|
5196
|
+
...unitFor(deps) === void 0 ? {} : { unit: unitFor(deps) },
|
|
5197
|
+
...deps.isBlocked === void 0 ? {} : { isBlocked: deps.isBlocked },
|
|
3940
5198
|
...stageHooks
|
|
3941
5199
|
})
|
|
3942
5200
|
});
|
|
@@ -3963,15 +5221,18 @@ function createMergeRuntime(deps) {
|
|
|
3963
5221
|
ok: false,
|
|
3964
5222
|
text: "merge row not found: " + command.id
|
|
3965
5223
|
};
|
|
3966
|
-
|
|
3967
|
-
|
|
3968
|
-
|
|
3969
|
-
|
|
3970
|
-
|
|
5224
|
+
if (!await transactRows(deps.io, deps.home, (tx) => {
|
|
5225
|
+
const current = tx.rows().find((item) => item.id === row.id);
|
|
5226
|
+
if (current === void 0 || current.state !== "pending") return false;
|
|
5227
|
+
return tx.transition(row.id, {
|
|
5228
|
+
type: "skipped",
|
|
5229
|
+
reason: "rejected",
|
|
5230
|
+
origin: "human"
|
|
5231
|
+
}, current.version).ok;
|
|
5232
|
+
})) return {
|
|
3971
5233
|
ok: false,
|
|
3972
|
-
text: "merge row is not pending: " + command.id
|
|
5234
|
+
text: "merge row is not pending or changed: " + command.id
|
|
3973
5235
|
};
|
|
3974
|
-
await upsertRow(deps.io, deps.home, deps.now, next.row);
|
|
3975
5236
|
return {
|
|
3976
5237
|
ok: true,
|
|
3977
5238
|
text: "已拒绝合并:" + command.id
|
|
@@ -3983,13 +5244,10 @@ function createMergeRuntime(deps) {
|
|
|
3983
5244
|
};
|
|
3984
5245
|
if (command.verb === "report" || command.verb === "runs" || command.verb === "cancel") {
|
|
3985
5246
|
const registry = deps.registry;
|
|
3986
|
-
if (command.verb === "runs") {
|
|
3987
|
-
|
|
3988
|
-
|
|
3989
|
-
|
|
3990
|
-
text: mergeRuns.length === 0 ? "merge runs: empty" : ["merge runs: " + String(mergeRuns.length)].concat(mergeRuns.map((run) => "- " + run.id + " [" + run.state + "]" + (run.resultRef === void 0 ? "" : " " + run.resultRef))).join("\n")
|
|
3991
|
-
};
|
|
3992
|
-
}
|
|
5247
|
+
if (command.verb === "runs") return {
|
|
5248
|
+
ok: true,
|
|
5249
|
+
text: formatRunsView(toRunsView(registry.runs("merge"), deps.now()))
|
|
5250
|
+
};
|
|
3993
5251
|
if (command.verb === "cancel") {
|
|
3994
5252
|
if (command.id !== void 0) {
|
|
3995
5253
|
const cancelled = registry.cancel(command.id);
|
|
@@ -4036,7 +5294,10 @@ function createMergeRuntime(deps) {
|
|
|
4036
5294
|
ok: false,
|
|
4037
5295
|
text: "merge row is not discardable: " + command.id
|
|
4038
5296
|
};
|
|
4039
|
-
await
|
|
5297
|
+
if (!await dropRowV3(deps.io, deps.home, row.id, "superseded")) return {
|
|
5298
|
+
ok: false,
|
|
5299
|
+
text: "row changed before discard: " + command.id
|
|
5300
|
+
};
|
|
4040
5301
|
return {
|
|
4041
5302
|
ok: true,
|
|
4042
5303
|
text: "已丢弃合并行:" + command.id
|
|
@@ -4070,7 +5331,10 @@ function createMergeRuntime(deps) {
|
|
|
4070
5331
|
ok: false,
|
|
4071
5332
|
text: "skipped row not found: " + command.restore
|
|
4072
5333
|
};
|
|
4073
|
-
await
|
|
5334
|
+
if (!await dropRowV3(deps.io, deps.home, row.id, "restored")) return {
|
|
5335
|
+
ok: false,
|
|
5336
|
+
text: "row changed before restore: " + row.id
|
|
5337
|
+
};
|
|
4074
5338
|
return {
|
|
4075
5339
|
ok: true,
|
|
4076
5340
|
text: "已恢复参与筛选:" + row.id
|
|
@@ -4083,7 +5347,10 @@ function createMergeRuntime(deps) {
|
|
|
4083
5347
|
ok: false,
|
|
4084
5348
|
text: "merge row not found: " + command.clear
|
|
4085
5349
|
};
|
|
4086
|
-
await
|
|
5350
|
+
if (!await dropRowV3(deps.io, deps.home, cleared.id, "superseded")) return {
|
|
5351
|
+
ok: false,
|
|
5352
|
+
text: "row changed before clear: " + cleared.id
|
|
5353
|
+
};
|
|
4087
5354
|
return {
|
|
4088
5355
|
ok: true,
|
|
4089
5356
|
text: "已清除合并行:" + cleared.id
|
|
@@ -4103,6 +5370,21 @@ function createMergeRuntime(deps) {
|
|
|
4103
5370
|
};
|
|
4104
5371
|
const signal = new AbortController().signal;
|
|
4105
5372
|
const cards = await deps.cards(signal);
|
|
5373
|
+
if ((row.state === "fuse" || row.state === "approved") && row.decision !== void 0 && row.decision.mode !== "create-umbrella") {
|
|
5374
|
+
const targetCard = cards.find((card) => card.name === row.decision?.target);
|
|
5375
|
+
if (targetCard !== void 0 && targetCard.managed !== true) return {
|
|
5376
|
+
ok: false,
|
|
5377
|
+
text: "target 不是 family-managed:" + row.decision.target
|
|
5378
|
+
};
|
|
5379
|
+
if (targetCard !== void 0 && (targetCard.protectionUnknown === true || targetCard.protectedBy != null)) return {
|
|
5380
|
+
ok: false,
|
|
5381
|
+
text: "target 受保护:" + row.decision.target
|
|
5382
|
+
};
|
|
5383
|
+
if (await deps.isBlocked?.(row.decision.target) === true) return {
|
|
5384
|
+
ok: false,
|
|
5385
|
+
text: "target 被范围门拦下:" + row.decision.target
|
|
5386
|
+
};
|
|
5387
|
+
}
|
|
4106
5388
|
const pair = () => Promise.resolve([{
|
|
4107
5389
|
a: row.a,
|
|
4108
5390
|
b: row.b
|
|
@@ -4126,6 +5408,8 @@ function createMergeRuntime(deps) {
|
|
|
4126
5408
|
...common,
|
|
4127
5409
|
rowId: row.id,
|
|
4128
5410
|
ports: basePorts,
|
|
5411
|
+
...detached ? { detached: true } : {},
|
|
5412
|
+
pairTimeoutMinutes: deps.params.mergePairTimeoutMinutes,
|
|
4129
5413
|
...deps.isOwnerActive === void 0 ? {} : { isOwnerActive: deps.isOwnerActive },
|
|
4130
5414
|
makePorts: (runId) => createChannelScanPorts({
|
|
4131
5415
|
io: deps.io,
|
|
@@ -4140,7 +5424,9 @@ function createMergeRuntime(deps) {
|
|
|
4140
5424
|
timeoutMs: deps.params.mergePairTimeoutMinutes * 6e4,
|
|
4141
5425
|
approval: deps.params.mergeApproval,
|
|
4142
5426
|
commitPort: skillLibraryPort(deps.skills),
|
|
4143
|
-
now: deps.now
|
|
5427
|
+
now: deps.now,
|
|
5428
|
+
...unitFor(deps) === void 0 ? {} : { unit: unitFor(deps) },
|
|
5429
|
+
...deps.isBlocked === void 0 ? {} : { isBlocked: deps.isBlocked }
|
|
4144
5430
|
}),
|
|
4145
5431
|
round2WithRun: createNewChildRound2({
|
|
4146
5432
|
io: deps.io,
|
|
@@ -4153,6 +5439,8 @@ function createMergeRuntime(deps) {
|
|
|
4153
5439
|
timeoutMs: deps.params.mergePairTimeoutMinutes * 6e4,
|
|
4154
5440
|
commitPort: skillLibraryPort(deps.skills),
|
|
4155
5441
|
now: deps.now,
|
|
5442
|
+
...unitFor(deps) === void 0 ? {} : { unit: unitFor(deps) },
|
|
5443
|
+
...deps.isBlocked === void 0 ? {} : { isBlocked: deps.isBlocked },
|
|
4156
5444
|
...stageHooks
|
|
4157
5445
|
})
|
|
4158
5446
|
});
|
|
@@ -4176,11 +5464,22 @@ function createMergeRuntime(deps) {
|
|
|
4176
5464
|
if (command.verb === "approve") {
|
|
4177
5465
|
const signal = new AbortController().signal;
|
|
4178
5466
|
const cards = await deps.cards(signal);
|
|
4179
|
-
|
|
5467
|
+
let candidate = (await loadMergeState(deps.io, deps.home)).state.rows.find((row) => row.id === command.id);
|
|
5468
|
+
if (candidate !== void 0 && candidate.stagingRef === void 0) {
|
|
5469
|
+
const entry = await readStagingIndex(deps.io, deps.home, candidate.id, deps.now());
|
|
5470
|
+
if (entry !== null) candidate = {
|
|
5471
|
+
...candidate,
|
|
5472
|
+
stagingRef: entry.ref,
|
|
5473
|
+
stagingExpiresAt: entry.expiresAt
|
|
5474
|
+
};
|
|
5475
|
+
}
|
|
4180
5476
|
if (candidate !== void 0 && candidate.state === "pending" && candidate.decision !== void 0) {
|
|
4181
5477
|
const missingSource = [candidate.a, candidate.b].find((name) => !cards.some((card) => card.name === name && card.managed === true));
|
|
4182
5478
|
if (missingSource !== void 0) {
|
|
4183
|
-
await
|
|
5479
|
+
if (!await dropRowV3(deps.io, deps.home, candidate.id, "superseded")) return {
|
|
5480
|
+
ok: false,
|
|
5481
|
+
text: "source-gone 但 row 已变化,未清理:" + candidate.id
|
|
5482
|
+
};
|
|
4184
5483
|
return {
|
|
4185
5484
|
ok: false,
|
|
4186
5485
|
text: "source-gone:" + missingSource + " 已不在 family-managed 技能库,已从 active 队列清除;可 merge skipped --discard 清理"
|
|
@@ -4188,11 +5487,16 @@ function createMergeRuntime(deps) {
|
|
|
4188
5487
|
}
|
|
4189
5488
|
const currentRevision = sourceRevisionOf(cards, candidate.a, candidate.b);
|
|
4190
5489
|
if (candidate.sourceRevision !== void 0 && currentRevision !== void 0 && currentRevision !== candidate.sourceRevision) {
|
|
4191
|
-
|
|
4192
|
-
|
|
4193
|
-
|
|
4194
|
-
|
|
4195
|
-
|
|
5490
|
+
if (!await transactRows(deps.io, deps.home, (tx) => {
|
|
5491
|
+
const current = tx.rows().find((item) => item.id === candidate.id);
|
|
5492
|
+
return current === void 0 ? false : tx.transition(candidate.id, {
|
|
5493
|
+
type: "expired",
|
|
5494
|
+
kind: "source-changed"
|
|
5495
|
+
}, current.version).ok;
|
|
5496
|
+
})) return {
|
|
5497
|
+
ok: false,
|
|
5498
|
+
text: "row changed before source-changed record: " + candidate.id
|
|
5499
|
+
};
|
|
4196
5500
|
return {
|
|
4197
5501
|
ok: false,
|
|
4198
5502
|
text: "source-changed:源技能内容在决策后变化,拒绝照跑;出口:重新 merge pair,或 merge skipped --discard"
|
|
@@ -4200,22 +5504,47 @@ function createMergeRuntime(deps) {
|
|
|
4200
5504
|
}
|
|
4201
5505
|
const currentTargetRevision = targetRevisionOf(cards, candidate.decision.target);
|
|
4202
5506
|
if (candidate.targetRevision !== void 0 && currentTargetRevision !== void 0 && currentTargetRevision !== candidate.targetRevision) {
|
|
4203
|
-
|
|
4204
|
-
|
|
4205
|
-
|
|
4206
|
-
|
|
4207
|
-
|
|
5507
|
+
if (!await transactRows(deps.io, deps.home, (tx) => {
|
|
5508
|
+
const current = tx.rows().find((item) => item.id === candidate.id);
|
|
5509
|
+
return current === void 0 ? false : tx.transition(candidate.id, {
|
|
5510
|
+
type: "expired",
|
|
5511
|
+
kind: "target-taken"
|
|
5512
|
+
}, current.version).ok;
|
|
5513
|
+
})) return {
|
|
5514
|
+
ok: false,
|
|
5515
|
+
text: "row changed before target-taken record: " + candidate.id
|
|
5516
|
+
};
|
|
4208
5517
|
return {
|
|
4209
5518
|
ok: false,
|
|
4210
5519
|
text: "target-taken:目标 " + candidate.decision.target + " 在决策后变化,拒绝照跑;可重新 merge pair 或 discard"
|
|
4211
5520
|
};
|
|
4212
5521
|
}
|
|
5522
|
+
if (candidate.decision.mode !== "create-umbrella") {
|
|
5523
|
+
const targetCard = cards.find((card) => card.name === candidate.decision?.target);
|
|
5524
|
+
if (targetCard !== void 0 && targetCard.managed !== true) return {
|
|
5525
|
+
ok: false,
|
|
5526
|
+
text: "target 不是 family-managed:" + candidate.decision.target
|
|
5527
|
+
};
|
|
5528
|
+
if (targetCard !== void 0 && (targetCard.protectionUnknown === true || targetCard.protectedBy != null)) return {
|
|
5529
|
+
ok: false,
|
|
5530
|
+
text: "target 受保护:" + candidate.decision.target
|
|
5531
|
+
};
|
|
5532
|
+
if (await deps.isBlocked?.(candidate.decision.target) === true) return {
|
|
5533
|
+
ok: false,
|
|
5534
|
+
text: "target 被范围门拦下:" + candidate.decision.target
|
|
5535
|
+
};
|
|
5536
|
+
}
|
|
4213
5537
|
if (candidate.decision.mode === "merge-into-existing" && !cards.some((card) => card.name === candidate.decision?.target)) {
|
|
4214
|
-
|
|
4215
|
-
|
|
4216
|
-
|
|
4217
|
-
|
|
4218
|
-
|
|
5538
|
+
if (!await transactRows(deps.io, deps.home, (tx) => {
|
|
5539
|
+
const current = tx.rows().find((item) => item.id === candidate.id);
|
|
5540
|
+
return current === void 0 ? false : tx.transition(candidate.id, {
|
|
5541
|
+
type: "expired",
|
|
5542
|
+
kind: "target-taken"
|
|
5543
|
+
}, current.version).ok;
|
|
5544
|
+
})) return {
|
|
5545
|
+
ok: false,
|
|
5546
|
+
text: "row changed before target-taken record: " + candidate.id
|
|
5547
|
+
};
|
|
4219
5548
|
return {
|
|
4220
5549
|
ok: false,
|
|
4221
5550
|
text: "target-taken:目标 " + candidate.decision.target + " 已不存在,行保留 pending --expired;出口:重新 merge pair,或 merge skipped --discard"
|
|
@@ -4225,6 +5554,8 @@ function createMergeRuntime(deps) {
|
|
|
4225
5554
|
const result = await runApproveCommand({
|
|
4226
5555
|
...common,
|
|
4227
5556
|
rowId: command.id,
|
|
5557
|
+
...detached ? { detached: true } : {},
|
|
5558
|
+
pairTimeoutMinutes: deps.params.mergePairTimeoutMinutes,
|
|
4228
5559
|
ports: { round2: createNewChildRound2({
|
|
4229
5560
|
io: deps.io,
|
|
4230
5561
|
home: deps.home,
|
|
@@ -4235,7 +5566,9 @@ function createMergeRuntime(deps) {
|
|
|
4235
5566
|
...deps.cwd === void 0 ? {} : { cwd: deps.cwd },
|
|
4236
5567
|
timeoutMs: deps.params.mergePairTimeoutMinutes * 6e4,
|
|
4237
5568
|
commitPort: skillLibraryPort(deps.skills),
|
|
4238
|
-
now: deps.now
|
|
5569
|
+
now: deps.now,
|
|
5570
|
+
...unitFor(deps) === void 0 ? {} : { unit: unitFor(deps) },
|
|
5571
|
+
...deps.isBlocked === void 0 ? {} : { isBlocked: deps.isBlocked }
|
|
4239
5572
|
}) }
|
|
4240
5573
|
});
|
|
4241
5574
|
if (!result.ok) return {
|
|
@@ -4256,7 +5589,9 @@ function createMergeRuntime(deps) {
|
|
|
4256
5589
|
};
|
|
4257
5590
|
}
|
|
4258
5591
|
const cards = command.verb === "pending" ? await deps.cards(new AbortController().signal) : void 0;
|
|
4259
|
-
const
|
|
5592
|
+
const registry = deps.registry;
|
|
5593
|
+
const runs = command.verb === "status" ? registry.runs("merge") : void 0;
|
|
5594
|
+
const text = await handleReadOnlyMergeCommand(command, deps.io, deps.home, cards, runs, deps.params.mergeApproval);
|
|
4260
5595
|
if (text !== null) return {
|
|
4261
5596
|
ok: true,
|
|
4262
5597
|
text
|
|
@@ -4635,6 +5970,143 @@ async function supersedeMergeRuns(registry) {
|
|
|
4635
5970
|
return ids.length;
|
|
4636
5971
|
}
|
|
4637
5972
|
//#endregion
|
|
5973
|
+
//#region lib/types/coordinator.js
|
|
5974
|
+
/**
|
|
5975
|
+
* Pure merge transition table. No io, no run shell, no subagents.
|
|
5976
|
+
*
|
|
5977
|
+
* Every transition returns the whole next row (or a drop), so the caller can persist it
|
|
5978
|
+
* through one state transaction. Invalid transitions are refused by name;
|
|
5979
|
+
* a caller never "repairs" a row by guessing which state the operator meant.
|
|
5980
|
+
*/
|
|
5981
|
+
function invalid(event, state) {
|
|
5982
|
+
return {
|
|
5983
|
+
ok: false,
|
|
5984
|
+
code: "invalid-transition",
|
|
5985
|
+
message: "cannot apply " + event.type + " to " + (state ?? "no row")
|
|
5986
|
+
};
|
|
5987
|
+
}
|
|
5988
|
+
/** Drop fields that must not survive a transition; optional fields stay absent. */
|
|
5989
|
+
function base(row, at) {
|
|
5990
|
+
return {
|
|
5991
|
+
id: row.id,
|
|
5992
|
+
pairKey: row.pairKey,
|
|
5993
|
+
a: row.a,
|
|
5994
|
+
b: row.b,
|
|
5995
|
+
state: row.state,
|
|
5996
|
+
queue: row.queue,
|
|
5997
|
+
createdAt: row.createdAt,
|
|
5998
|
+
updatedAt: at,
|
|
5999
|
+
...row.runId === void 0 ? {} : { runId: row.runId },
|
|
6000
|
+
...row.unitId === void 0 ? {} : { unitId: row.unitId },
|
|
6001
|
+
...row.origin === void 0 ? {} : { origin: row.origin },
|
|
6002
|
+
...row.decision === void 0 ? {} : { decision: row.decision },
|
|
6003
|
+
...row.sourceRevision === void 0 ? {} : { sourceRevision: row.sourceRevision },
|
|
6004
|
+
...row.targetRevision === void 0 ? {} : { targetRevision: row.targetRevision },
|
|
6005
|
+
...row.attempts === void 0 ? {} : { attempts: row.attempts },
|
|
6006
|
+
...row.stagingRef === void 0 ? {} : { stagingRef: row.stagingRef },
|
|
6007
|
+
...row.stagingExpiresAt === void 0 ? {} : { stagingExpiresAt: row.stagingExpiresAt },
|
|
6008
|
+
...row.expired === void 0 ? {} : { expired: row.expired },
|
|
6009
|
+
...row.reason === void 0 ? {} : { reason: row.reason },
|
|
6010
|
+
...row.target === void 0 ? {} : { target: row.target }
|
|
6011
|
+
};
|
|
6012
|
+
}
|
|
6013
|
+
function next(row, at, state, patch = {}) {
|
|
6014
|
+
return {
|
|
6015
|
+
ok: true,
|
|
6016
|
+
row: {
|
|
6017
|
+
...base(row, at),
|
|
6018
|
+
state,
|
|
6019
|
+
queue: queueOf(state),
|
|
6020
|
+
...patch
|
|
6021
|
+
}
|
|
6022
|
+
};
|
|
6023
|
+
}
|
|
6024
|
+
/** Legacy v2 projection only; runtime writes must use transitionV3/RowTx. */
|
|
6025
|
+
function transitionLegacyForProjection(current, event) {
|
|
6026
|
+
if (event.type === "candidate") {
|
|
6027
|
+
if (current !== void 0) return {
|
|
6028
|
+
ok: true,
|
|
6029
|
+
row: current
|
|
6030
|
+
};
|
|
6031
|
+
const key = pairKey(event.a, event.b);
|
|
6032
|
+
return {
|
|
6033
|
+
ok: true,
|
|
6034
|
+
row: {
|
|
6035
|
+
id: rowIdOf(key),
|
|
6036
|
+
pairKey: key,
|
|
6037
|
+
a: event.a,
|
|
6038
|
+
b: event.b,
|
|
6039
|
+
state: "verify",
|
|
6040
|
+
queue: "verify",
|
|
6041
|
+
createdAt: event.at,
|
|
6042
|
+
updatedAt: event.at
|
|
6043
|
+
}
|
|
6044
|
+
};
|
|
6045
|
+
}
|
|
6046
|
+
if (event.type === "source-gone") return {
|
|
6047
|
+
ok: true,
|
|
6048
|
+
row: null
|
|
6049
|
+
};
|
|
6050
|
+
if (event.type === "gate-keep") {
|
|
6051
|
+
if (current !== void 0) return {
|
|
6052
|
+
ok: true,
|
|
6053
|
+
row: current
|
|
6054
|
+
};
|
|
6055
|
+
const key = pairKey(event.a, event.b);
|
|
6056
|
+
return {
|
|
6057
|
+
ok: true,
|
|
6058
|
+
row: {
|
|
6059
|
+
id: rowIdOf(key),
|
|
6060
|
+
pairKey: key,
|
|
6061
|
+
a: event.a,
|
|
6062
|
+
b: event.b,
|
|
6063
|
+
state: "skipped",
|
|
6064
|
+
queue: "skipped",
|
|
6065
|
+
origin: "agent",
|
|
6066
|
+
createdAt: event.at,
|
|
6067
|
+
updatedAt: event.at
|
|
6068
|
+
}
|
|
6069
|
+
};
|
|
6070
|
+
}
|
|
6071
|
+
if (current === void 0) return invalid(event, void 0);
|
|
6072
|
+
switch (event.type) {
|
|
6073
|
+
case "round1-keep": return current.state === "verify" ? next(current, event.at, "skipped", { origin: "agent" }) : invalid(event, current.state);
|
|
6074
|
+
case "round1-merge":
|
|
6075
|
+
if (current.state !== "verify") return invalid(event, current.state);
|
|
6076
|
+
return event.approval === "on" ? next(current, event.at, "pending", {
|
|
6077
|
+
...event.decision === void 0 ? {} : { decision: event.decision },
|
|
6078
|
+
...event.stagingRef === void 0 ? {} : { stagingRef: event.stagingRef },
|
|
6079
|
+
...event.stagingExpiresAt === void 0 ? {} : { stagingExpiresAt: event.stagingExpiresAt },
|
|
6080
|
+
...event.sourceRevision === void 0 ? {} : { sourceRevision: event.sourceRevision },
|
|
6081
|
+
...event.targetRevision === void 0 ? {} : { targetRevision: event.targetRevision }
|
|
6082
|
+
}) : next(current, event.at, "approved", {
|
|
6083
|
+
...event.decision === void 0 ? {} : { decision: event.decision },
|
|
6084
|
+
...event.stagingRef === void 0 ? {} : { stagingRef: event.stagingRef },
|
|
6085
|
+
...event.stagingExpiresAt === void 0 ? {} : { stagingExpiresAt: event.stagingExpiresAt },
|
|
6086
|
+
...event.sourceRevision === void 0 ? {} : { sourceRevision: event.sourceRevision },
|
|
6087
|
+
...event.targetRevision === void 0 ? {} : { targetRevision: event.targetRevision }
|
|
6088
|
+
});
|
|
6089
|
+
case "round1-fail": return current.state === "verify" ? next(current, event.at, "verify", {
|
|
6090
|
+
failure: event.failure,
|
|
6091
|
+
attempts: (current.attempts ?? 0) + 1
|
|
6092
|
+
}) : invalid(event, current.state);
|
|
6093
|
+
case "approve": return current.state === "pending" ? next(current, event.at, "approved", event.runId === void 0 ? {} : { runId: event.runId }) : invalid(event, current.state);
|
|
6094
|
+
case "reject": return current.state === "pending" ? next(current, event.at, "skipped", { origin: "human" }) : invalid(event, current.state);
|
|
6095
|
+
case "round2-cancel": return current.state === "fuse" || current.state === "approved" ? next(current, event.at, "approved") : invalid(event, current.state);
|
|
6096
|
+
case "round2-fail": return current.state === "fuse" || current.state === "approved" ? next(current, event.at, "approved", {
|
|
6097
|
+
failure: event.failure,
|
|
6098
|
+
attempts: (current.attempts ?? 0) + 1
|
|
6099
|
+
}) : invalid(event, current.state);
|
|
6100
|
+
case "round2-success": return current.state === "fuse" || current.state === "approved" ? {
|
|
6101
|
+
ok: true,
|
|
6102
|
+
row: null
|
|
6103
|
+
} : invalid(event, current.state);
|
|
6104
|
+
case "target-taken":
|
|
6105
|
+
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);
|
|
6106
|
+
case "restore": return current.state === "skipped" ? next(current, event.at, "verify") : invalid(event, current.state);
|
|
6107
|
+
}
|
|
6108
|
+
}
|
|
6109
|
+
//#endregion
|
|
4638
6110
|
//#region lib/types/pair-command.js
|
|
4639
6111
|
async function runPairCommand(options) {
|
|
4640
6112
|
return await runScanCommand({
|
|
@@ -4664,14 +6136,10 @@ const Config = z.object({
|
|
|
4664
6136
|
mergePairTimeoutMinutes: z.number().min(1).default(15),
|
|
4665
6137
|
mergeModelsRoot: z.string().default("")
|
|
4666
6138
|
});
|
|
4667
|
-
function
|
|
4668
|
-
const root = configuredRoot
|
|
4669
|
-
if (root ===
|
|
6139
|
+
function engineConfigFromRoot(configuredRoot) {
|
|
6140
|
+
const root = configuredRoot?.trim() ?? "";
|
|
6141
|
+
if (root === "") return void 0;
|
|
4670
6142
|
return {
|
|
4671
|
-
models_dir: "",
|
|
4672
|
-
offline: true,
|
|
4673
|
-
auto_provision: false,
|
|
4674
|
-
embed_backend: "local",
|
|
4675
6143
|
embedder_model: join(root, "potion-multilingual-128M-int8"),
|
|
4676
6144
|
embedder_model_last: join(root, "potion-multilingual-128M-pca64-int8")
|
|
4677
6145
|
};
|
|
@@ -4715,7 +6183,6 @@ function apply(ctx, rawConfig = {}) {
|
|
|
4715
6183
|
ctx.logger.warn(message);
|
|
4716
6184
|
}
|
|
4717
6185
|
});
|
|
4718
|
-
runs.load();
|
|
4719
6186
|
ctx.effect(() => {
|
|
4720
6187
|
const host = ctx;
|
|
4721
6188
|
if (typeof host.on !== "function") return () => {};
|
|
@@ -4733,20 +6200,17 @@ function apply(ctx, rawConfig = {}) {
|
|
|
4733
6200
|
...policyStageLimits(policySnapshot?.get?.())
|
|
4734
6201
|
}
|
|
4735
6202
|
});
|
|
4736
|
-
|
|
4737
|
-
recoverCommitJournals(io, home, Date.now(), mergeSkillsRoot, async (name) => {
|
|
4738
|
-
return (await skills.restoreFromArchive(name)).ok;
|
|
4739
|
-
}, (runId) => {
|
|
6203
|
+
runs.load().then(() => recoverCommitJournals(io, home, Date.now(), (runId) => {
|
|
4740
6204
|
const run = runs.find(runId);
|
|
4741
6205
|
return run !== void 0 && run.state === "running";
|
|
4742
|
-
}).then((reports) => {
|
|
6206
|
+
})).then((reports) => {
|
|
4743
6207
|
if (reports.length > 0) ctx.logger.info("merge commit recovery: " + reports.map((report) => report.runId + "=" + report.action).join(", "));
|
|
4744
6208
|
}).catch((error) => {
|
|
4745
6209
|
ctx.logger.warn("merge commit recovery failed: " + (error instanceof Error ? error.message : String(error)));
|
|
4746
6210
|
});
|
|
4747
6211
|
const channel = subagentChannelAdapter(createSubagentChannel(ctx));
|
|
4748
6212
|
const stage = new StageToolRegistry();
|
|
4749
|
-
const engineConfig =
|
|
6213
|
+
const engineConfig = engineConfigFromRoot(params.mergeModelsRoot);
|
|
4750
6214
|
const llm = ctx.get("llm");
|
|
4751
6215
|
const defaultModel = ctx.get("agentDefaultModel");
|
|
4752
6216
|
const runner = llm === void 0 || defaultModel === void 0 ? void 0 : createEngineRunner(llm, () => defaultModel.currentSelection());
|
|
@@ -4756,7 +6220,7 @@ function apply(ctx, rawConfig = {}) {
|
|
|
4756
6220
|
engine: { screen },
|
|
4757
6221
|
select: selectWithErrors,
|
|
4758
6222
|
cfg: {
|
|
4759
|
-
...engineConfig
|
|
6223
|
+
...engineConfig,
|
|
4760
6224
|
judge: "deepseek"
|
|
4761
6225
|
},
|
|
4762
6226
|
...runner === void 0 ? {} : { runner },
|
|
@@ -4765,14 +6229,20 @@ function apply(ctx, rawConfig = {}) {
|
|
|
4765
6229
|
const at = (/* @__PURE__ */ new Date()).toISOString();
|
|
4766
6230
|
for (const skipped of selection.skipped) {
|
|
4767
6231
|
if (loaded.state.rows.some((row) => row.pairKey === skipped.pairKey)) continue;
|
|
4768
|
-
const
|
|
4769
|
-
|
|
6232
|
+
const key = pairKey(skipped.a, skipped.b);
|
|
6233
|
+
await transactRows(io, home, (tx) => tx.createSkipped({
|
|
6234
|
+
id: rowIdOf(key),
|
|
6235
|
+
pairKey: key,
|
|
4770
6236
|
a: skipped.a,
|
|
4771
6237
|
b: skipped.b,
|
|
4772
|
-
|
|
4773
|
-
at
|
|
4774
|
-
|
|
4775
|
-
|
|
6238
|
+
version: 0,
|
|
6239
|
+
createdAt: at,
|
|
6240
|
+
updatedAt: at,
|
|
6241
|
+
origin: "agent",
|
|
6242
|
+
state: "skipped",
|
|
6243
|
+
reason: skipped.reason ?? "gate-keep",
|
|
6244
|
+
attempts: 0
|
|
6245
|
+
}).ok);
|
|
4776
6246
|
}
|
|
4777
6247
|
}
|
|
4778
6248
|
});
|
|
@@ -4903,4 +6373,4 @@ function apply(ctx, rawConfig = {}) {
|
|
|
4903
6373
|
});
|
|
4904
6374
|
}
|
|
4905
6375
|
//#endregion
|
|
4906
|
-
export { Config, MERGE_DOCTOR_SCHEMA_VERSION, 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,
|
|
6376
|
+
export { Config, MERGE_DOCTOR_SCHEMA_VERSION, 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, canRetry, cleanupExpiredStaging, commitReceiptPath, commitStagedRun, committedPairs, configureMergePresentation, copyStagedFile, countRows, createChannelScanPorts, createEngineRunner, createEngineScreening, createMergeCommand, createMergeRuntime, createMergeStageTool, createNewChildRound2, createReadOnlyMergeService, createStaging, dropRow, dropRowV3, dropStaging, durableReceiptDir, durableReceiptPath, emptyMergeState, engineConfigFromRoot, ensureStaging, formatFacts, formatQueue, formatRunsView, formatStatus, handleReadOnlyMergeCommand, hasBlockingFinding, inject, isSkippedPair, listStagedFiles, loadMergeState, loadStateSchema, makeMergeRoutes, mergeCommandHelp, mergeDoctor, mergeDoctorAvailability, mergeReportDigestPath, mergeReportPath, mergeRunIdsToSupersede, mergeStatePath, mergeUsage, moveRow, mutateMergeState, pairKey, parseDecision, parseMergeCommand, queueOf, readCommitReceipt, readCommitReceipts, readMergeReport, readStagedFile, readStagedTree, readStaging, recordRound2Outcome, rejectPending, resolveMergeParams, resolveTargetName, rewriteFrontmatterName, rowIdOf, rowsOf, runApproveCommand, runAuthoring, runMergeCommand, runNewChildAuthoring, runPairCommand, runResumeCommand, runRound1OverChannel, runScan, runScanCommand, setStagingTarget, skillLibraryPort, skillLibraryUndoPort, skippedPairKeys, snapshotCards, stageKeyOf, stageOf, stagingRoot, subagentChannelAdapter, supersedeMergeRuns, transactRows, transitionLegacyForProjection, undoMergeReceipt, unitFor, upsertRow, validateStagedTree, withMergeRun, writeCommitReceipt, writeMergeReport, writeStagedFile };
|