@lmzhen/dsh-evolution-merge 0.30.17 → 0.30.18
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +4 -3
- package/lib/index.js +2321 -854
- 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 +114 -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 +70 -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,12 +817,68 @@ 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;
|
|
@@ -349,9 +887,11 @@ async function mutateMergeState(io, home, now, fn) {
|
|
|
349
887
|
if (!parsed.ok || parsed.note === "merge-state-forward-only") throw new Error(parsed.note ?? "merge-state-invalid");
|
|
350
888
|
const out = fn(parsed.rows);
|
|
351
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");
|
|
352
892
|
return JSON.stringify({
|
|
353
|
-
schemaVersion:
|
|
354
|
-
rows
|
|
893
|
+
schemaVersion: 3,
|
|
894
|
+
rows
|
|
355
895
|
}, null, 2);
|
|
356
896
|
});
|
|
357
897
|
return result;
|
|
@@ -425,6 +965,19 @@ async function moveRow(io, home, now, id, patch) {
|
|
|
425
965
|
};
|
|
426
966
|
});
|
|
427
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
|
+
}
|
|
428
981
|
async function dropRow(io, home, now, id) {
|
|
429
982
|
return await mutateMergeState(io, home, now, (rows) => {
|
|
430
983
|
const next = rows.filter((item) => item.id !== id);
|
|
@@ -596,20 +1149,6 @@ async function cleanupExpiredStaging(io, home, now, keep = /* @__PURE__ */ new S
|
|
|
596
1149
|
};
|
|
597
1150
|
}
|
|
598
1151
|
//#endregion
|
|
599
|
-
//#region lib/types/domain/commit-journal.js
|
|
600
|
-
/** What a restart may do with the last durable phase. */
|
|
601
|
-
function recoveryFor(journal) {
|
|
602
|
-
switch (journal.phase) {
|
|
603
|
-
case "prepared": return "retry-commit";
|
|
604
|
-
case "target-written": return "rollback-target";
|
|
605
|
-
case "sources-archived": return "rollback-sources";
|
|
606
|
-
case "committed":
|
|
607
|
-
case "rolled-back":
|
|
608
|
-
case "failed": return "none";
|
|
609
|
-
default: return "refuse";
|
|
610
|
-
}
|
|
611
|
-
}
|
|
612
|
-
//#endregion
|
|
613
1152
|
//#region lib/types/domain/commit-journal-store.js
|
|
614
1153
|
/** Durable commit journal: <home>/merge/journals/<run>.json. */
|
|
615
1154
|
function journalPath(home, runId) {
|
|
@@ -617,9 +1156,20 @@ function journalPath(home, runId) {
|
|
|
617
1156
|
}
|
|
618
1157
|
async function writeJournal(io, home, journal) {
|
|
619
1158
|
const path = journalPath(home, journal.runId);
|
|
620
|
-
|
|
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);
|
|
621
1162
|
return path;
|
|
622
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
|
+
}
|
|
623
1173
|
//#endregion
|
|
624
1174
|
//#region lib/types/commit-snapshot.js
|
|
625
1175
|
/** Pre-commit snapshot: enough to restore the target and archived sources after a crash. */
|
|
@@ -654,32 +1204,9 @@ async function writeCommitSnapshot(io, home, runId, plan, now, port) {
|
|
|
654
1204
|
await io.writeText(snapshotPath(home, runId), JSON.stringify(snapshot, null, 2));
|
|
655
1205
|
return snapshot;
|
|
656
1206
|
}
|
|
657
|
-
async function readCommitSnapshot(io, home, runId) {
|
|
658
|
-
const raw = await io.readText(snapshotPath(home, runId));
|
|
659
|
-
if (raw === null) return null;
|
|
660
|
-
try {
|
|
661
|
-
return JSON.parse(raw);
|
|
662
|
-
} catch {
|
|
663
|
-
return null;
|
|
664
|
-
}
|
|
665
|
-
}
|
|
666
1207
|
function isSafeSkillName(name) {
|
|
667
1208
|
return /^[A-Za-z0-9][A-Za-z0-9._-]*$/.test(name) && name !== "." && name !== "..";
|
|
668
1209
|
}
|
|
669
|
-
function skillFilePath(root, name, path) {
|
|
670
|
-
if (!isSafeSkillName(name) || path.startsWith("/") || path.split("/").includes("..")) throw new Error("unsafe skill file: " + name + "/" + path);
|
|
671
|
-
const base = resolve(root, name);
|
|
672
|
-
const file = resolve(base, path);
|
|
673
|
-
if (!file.startsWith(base + sep)) throw new Error("skill file escapes skill: " + name + "/" + path);
|
|
674
|
-
return file;
|
|
675
|
-
}
|
|
676
|
-
function skillBodyPath(root, name) {
|
|
677
|
-
if (!isSafeSkillName(name)) throw new Error("unsafe skill name: " + name);
|
|
678
|
-
const base = resolve(root);
|
|
679
|
-
const path = resolve(base, name, "SKILL.md");
|
|
680
|
-
if (!path.startsWith(base + sep)) throw new Error("skill path escapes root: " + name);
|
|
681
|
-
return path;
|
|
682
|
-
}
|
|
683
1210
|
//#endregion
|
|
684
1211
|
//#region lib/types/commit-receipt.js
|
|
685
1212
|
/**
|
|
@@ -757,448 +1284,488 @@ async function readCommitReceipt(io, home, runId, target) {
|
|
|
757
1284
|
}
|
|
758
1285
|
//#endregion
|
|
759
1286
|
//#region lib/types/commit-recovery.js
|
|
760
|
-
/** Startup replay for non-terminal commit journals. */
|
|
761
|
-
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) {
|
|
762
1289
|
const dir = join(home, "merge", "journals");
|
|
763
|
-
if (!await io.exists(dir)) return [];
|
|
764
|
-
const files = (await io.list(dir)).filter((name) => name.endsWith(".json"));
|
|
765
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
|
+
}
|
|
766
1303
|
for (const file of files) {
|
|
767
|
-
|
|
768
|
-
try {
|
|
769
|
-
journal = JSON.parse(await io.readText(join(dir, file)) ?? "");
|
|
770
|
-
} catch {
|
|
771
|
-
continue;
|
|
772
|
-
}
|
|
773
|
-
if (isActive?.(journal.runId) === true) {
|
|
774
|
-
reports.push({
|
|
775
|
-
runId: journal.runId,
|
|
776
|
-
phase: journal.phase,
|
|
777
|
-
action: "skipped-active-run",
|
|
778
|
-
ok: true
|
|
779
|
-
});
|
|
780
|
-
continue;
|
|
781
|
-
}
|
|
782
|
-
const recovery = recoveryFor(journal);
|
|
783
|
-
if (recovery === "none") continue;
|
|
784
|
-
if (recovery === "refuse") {
|
|
785
|
-
reports.push({
|
|
786
|
-
runId: journal.runId,
|
|
787
|
-
phase: journal.phase,
|
|
788
|
-
action: "unknown-phase-refused",
|
|
789
|
-
ok: false
|
|
790
|
-
});
|
|
791
|
-
continue;
|
|
792
|
-
}
|
|
793
|
-
let receipt = null;
|
|
1304
|
+
const path = join(dir, file);
|
|
794
1305
|
try {
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
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;
|
|
812
1378
|
reports.push({
|
|
813
1379
|
runId: journal.runId,
|
|
814
1380
|
phase: journal.phase,
|
|
815
|
-
action: "
|
|
816
|
-
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)
|
|
817
1387
|
});
|
|
818
|
-
continue;
|
|
819
|
-
}
|
|
820
|
-
let snapshot;
|
|
821
|
-
try {
|
|
822
|
-
snapshot = await readCommitSnapshot(io, home, journal.runId);
|
|
823
1388
|
} catch (error) {
|
|
824
1389
|
reports.push({
|
|
825
|
-
runId:
|
|
826
|
-
phase:
|
|
827
|
-
action: "
|
|
828
|
-
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
|
|
829
1395
|
});
|
|
830
|
-
continue;
|
|
831
1396
|
}
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
}
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
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;
|
|
843
1420
|
}
|
|
844
|
-
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
|
+
});
|
|
845
1453
|
}
|
|
846
|
-
if (snapshot.create && snapshot.targetBody === null) {
|
|
847
|
-
try {
|
|
848
|
-
await io.remove(skillBodyPath(skillRoot, snapshot.target));
|
|
849
|
-
} catch {}
|
|
850
|
-
try {
|
|
851
|
-
const dir = join(skillRoot, snapshot.target);
|
|
852
|
-
if ((await io.list(dir)).length === 0) await io.remove(dir);
|
|
853
|
-
} catch {}
|
|
854
|
-
} else if (snapshot.targetBody !== null) await io.writeText(skillBodyPath(skillRoot, snapshot.target), snapshot.targetBody);
|
|
855
|
-
const oldPaths = new Set((snapshot.targetSupport ?? []).map((file) => file.path));
|
|
856
|
-
for (const file of snapshot.targetSupport ?? []) await io.writeText(skillFilePath(skillRoot, snapshot.target, file.path), file.content);
|
|
857
|
-
for (const file of snapshot.targetSupportAfter ?? []) if (!oldPaths.has(file.path)) try {
|
|
858
|
-
await io.remove(skillFilePath(skillRoot, snapshot.target, file.path));
|
|
859
|
-
} catch {}
|
|
860
|
-
for (const file of snapshot.sourceSupport ?? []) await io.writeText(skillFilePath(skillRoot, file.name, file.path), file.content);
|
|
861
|
-
action = recovery === "rollback-sources" ? "restored-sources-and-target" : "restored-target";
|
|
862
|
-
} catch (error) {
|
|
863
|
-
action = "restore-failed: " + (error instanceof Error ? error.message : String(error));
|
|
864
|
-
ok = false;
|
|
865
1454
|
}
|
|
866
|
-
if (ok) await writeJournal(io, home, {
|
|
867
|
-
...journal,
|
|
868
|
-
phase: "rolled-back",
|
|
869
|
-
updatedAt: new Date(now).toISOString()
|
|
870
|
-
});
|
|
871
|
-
reports.push({
|
|
872
|
-
runId: journal.runId,
|
|
873
|
-
phase: journal.phase,
|
|
874
|
-
action,
|
|
875
|
-
ok
|
|
876
|
-
});
|
|
877
1455
|
}
|
|
878
|
-
|
|
1456
|
+
try {
|
|
1457
|
+
await io.writeText(join(home, "merge", "recovery.json"), JSON.stringify(reports, null, 2));
|
|
1458
|
+
} catch {}
|
|
879
1459
|
return reports;
|
|
880
1460
|
}
|
|
881
1461
|
//#endregion
|
|
882
|
-
//#region lib/types/
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
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) {
|
|
899
1479
|
return {
|
|
900
|
-
id
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
...row.origin === void 0 ? {} : { origin: row.origin },
|
|
911
|
-
...row.decision === void 0 ? {} : { decision: row.decision },
|
|
912
|
-
...row.sourceRevision === void 0 ? {} : { sourceRevision: row.sourceRevision },
|
|
913
|
-
...row.targetRevision === void 0 ? {} : { targetRevision: row.targetRevision },
|
|
914
|
-
...row.attempts === void 0 ? {} : { attempts: row.attempts },
|
|
915
|
-
...row.stagingRef === void 0 ? {} : { stagingRef: row.stagingRef },
|
|
916
|
-
...row.stagingExpiresAt === void 0 ? {} : { stagingExpiresAt: row.stagingExpiresAt },
|
|
917
|
-
...row.expired === void 0 ? {} : { expired: row.expired },
|
|
918
|
-
...row.reason === void 0 ? {} : { reason: row.reason },
|
|
919
|
-
...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
|
|
920
1490
|
};
|
|
921
1491
|
}
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
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"
|
|
930
1552
|
}
|
|
931
|
-
|
|
932
|
-
}
|
|
933
|
-
function transition(current, event) {
|
|
934
|
-
if (event.type === "candidate") {
|
|
935
|
-
if (current !== void 0) return {
|
|
936
|
-
ok: true,
|
|
937
|
-
row: current
|
|
938
|
-
};
|
|
939
|
-
const key = pairKey(event.a, event.b);
|
|
940
|
-
return {
|
|
941
|
-
ok: true,
|
|
942
|
-
row: {
|
|
943
|
-
id: rowIdOf(key),
|
|
944
|
-
pairKey: key,
|
|
945
|
-
a: event.a,
|
|
946
|
-
b: event.b,
|
|
947
|
-
state: "verify",
|
|
948
|
-
queue: "verify",
|
|
949
|
-
createdAt: event.at,
|
|
950
|
-
updatedAt: event.at
|
|
951
|
-
}
|
|
952
|
-
};
|
|
953
|
-
}
|
|
954
|
-
if (event.type === "source-gone") return {
|
|
955
|
-
ok: true,
|
|
956
|
-
row: null
|
|
957
|
-
};
|
|
958
|
-
if (event.type === "gate-keep") {
|
|
959
|
-
if (current !== void 0) return {
|
|
960
|
-
ok: true,
|
|
961
|
-
row: current
|
|
962
|
-
};
|
|
963
|
-
const key = pairKey(event.a, event.b);
|
|
964
|
-
return {
|
|
965
|
-
ok: true,
|
|
966
|
-
row: {
|
|
967
|
-
id: rowIdOf(key),
|
|
968
|
-
pairKey: key,
|
|
969
|
-
a: event.a,
|
|
970
|
-
b: event.b,
|
|
971
|
-
state: "skipped",
|
|
972
|
-
queue: "skipped",
|
|
973
|
-
origin: "agent",
|
|
974
|
-
createdAt: event.at,
|
|
975
|
-
updatedAt: event.at
|
|
976
|
-
}
|
|
977
|
-
};
|
|
978
|
-
}
|
|
979
|
-
if (current === void 0) return invalid(event, void 0);
|
|
980
|
-
switch (event.type) {
|
|
981
|
-
case "round1-keep": return current.state === "verify" ? next(current, event.at, "skipped", { origin: "agent" }) : invalid(event, current.state);
|
|
982
|
-
case "round1-merge":
|
|
983
|
-
if (current.state !== "verify") return invalid(event, current.state);
|
|
984
|
-
return event.approval === "on" ? next(current, event.at, "pending", {
|
|
985
|
-
...event.decision === void 0 ? {} : { decision: event.decision },
|
|
986
|
-
...event.stagingRef === void 0 ? {} : { stagingRef: event.stagingRef },
|
|
987
|
-
...event.stagingExpiresAt === void 0 ? {} : { stagingExpiresAt: event.stagingExpiresAt },
|
|
988
|
-
...event.sourceRevision === void 0 ? {} : { sourceRevision: event.sourceRevision },
|
|
989
|
-
...event.targetRevision === void 0 ? {} : { targetRevision: event.targetRevision }
|
|
990
|
-
}) : next(current, event.at, "approved", {
|
|
991
|
-
...event.decision === void 0 ? {} : { decision: event.decision },
|
|
992
|
-
...event.stagingRef === void 0 ? {} : { stagingRef: event.stagingRef },
|
|
993
|
-
...event.stagingExpiresAt === void 0 ? {} : { stagingExpiresAt: event.stagingExpiresAt },
|
|
994
|
-
...event.sourceRevision === void 0 ? {} : { sourceRevision: event.sourceRevision },
|
|
995
|
-
...event.targetRevision === void 0 ? {} : { targetRevision: event.targetRevision }
|
|
996
|
-
});
|
|
997
|
-
case "round1-fail": return current.state === "verify" ? next(current, event.at, "verify", {
|
|
998
|
-
failure: event.failure,
|
|
999
|
-
attempts: (current.attempts ?? 0) + 1
|
|
1000
|
-
}) : invalid(event, current.state);
|
|
1001
|
-
case "approve": return current.state === "pending" ? next(current, event.at, "approved", { ...event.runId === void 0 ? {} : { runId: event.runId } }) : invalid(event, current.state);
|
|
1002
|
-
case "reject": return current.state === "pending" ? next(current, event.at, "skipped", { origin: "human" }) : invalid(event, current.state);
|
|
1003
|
-
case "round2-cancel": return current.state === "fuse" || current.state === "approved" ? next(current, event.at, "approved") : invalid(event, current.state);
|
|
1004
|
-
case "round2-fail": return current.state === "fuse" || current.state === "approved" ? next(current, event.at, "approved", {
|
|
1005
|
-
failure: event.failure,
|
|
1006
|
-
attempts: (current.attempts ?? 0) + 1
|
|
1007
|
-
}) : invalid(event, current.state);
|
|
1008
|
-
case "round2-success": return current.state === "fuse" || current.state === "approved" ? {
|
|
1009
|
-
ok: true,
|
|
1010
|
-
row: null
|
|
1011
|
-
} : invalid(event, current.state);
|
|
1012
|
-
case "target-taken":
|
|
1013
|
-
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);
|
|
1014
|
-
case "restore": return current.state === "skipped" ? next(current, event.at, "verify") : invalid(event, current.state);
|
|
1015
|
-
}
|
|
1016
|
-
}
|
|
1017
|
-
//#endregion
|
|
1018
|
-
//#region lib/types/presentation-budget.js
|
|
1019
|
-
/** Tool-result budget: keep the head and tail, mark what was omitted. */
|
|
1020
|
-
function fitToolText(text, max = 8e3) {
|
|
1021
|
-
if (text.length <= max) return text;
|
|
1022
|
-
const marker = "\n... [truncated " + String(text.length - max) + " chars] ...\n";
|
|
1023
|
-
const keep = Math.max(0, max - marker.length);
|
|
1024
|
-
const head = Math.ceil(keep * .7);
|
|
1025
|
-
const tail = keep - head;
|
|
1026
|
-
return text.slice(0, head) + marker + (tail === 0 ? "" : text.slice(-tail));
|
|
1027
|
-
}
|
|
1553
|
+
]
|
|
1554
|
+
};
|
|
1028
1555
|
//#endregion
|
|
1029
|
-
//#region lib/types/
|
|
1030
|
-
|
|
1031
|
-
|
|
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 = {
|
|
1032
1561
|
verify: 0,
|
|
1033
1562
|
pending: 0,
|
|
1034
1563
|
fuse: 0,
|
|
1035
1564
|
skipped: 0
|
|
1036
1565
|
};
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
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
|
+
};
|
|
1044
1578
|
}
|
|
1045
|
-
function
|
|
1046
|
-
const
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
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
|
+
};
|
|
1050
1624
|
}
|
|
1051
|
-
function
|
|
1052
|
-
const
|
|
1053
|
-
|
|
1054
|
-
|
|
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
|
+
};
|
|
1055
1645
|
}
|
|
1056
1646
|
//#endregion
|
|
1057
|
-
//#region lib/types/
|
|
1058
|
-
|
|
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)) {
|
|
1059
1682
|
return {
|
|
1060
1683
|
id: row.id,
|
|
1061
|
-
|
|
1684
|
+
state: row.state,
|
|
1685
|
+
queue: queueOf$1(row),
|
|
1686
|
+
...row.state === "verify" ? { verifySubState: "queued" } : {},
|
|
1062
1687
|
a: row.a,
|
|
1063
1688
|
b: row.b,
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
...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" } : {},
|
|
1075
1701
|
...row.failure === void 0 ? {} : { failure: {
|
|
1076
|
-
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",
|
|
1077
1703
|
code: row.failure.code,
|
|
1078
|
-
|
|
1079
|
-
|
|
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)
|
|
1080
1714
|
} },
|
|
1081
1715
|
...row.attempts === void 0 ? {} : { attempts: row.attempts },
|
|
1082
|
-
...row.
|
|
1083
|
-
...row.
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
row.decision?.aSummary ?? "未提供技能1摘要",
|
|
1088
|
-
row.decision?.bSummary ?? "未提供技能2摘要",
|
|
1089
|
-
row.decision?.rationale ?? row.reason ?? "未提供融合原因"
|
|
1090
|
-
],
|
|
1091
|
-
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
|
|
1092
1721
|
};
|
|
1093
1722
|
}
|
|
1094
|
-
function
|
|
1095
|
-
const
|
|
1096
|
-
|
|
1097
|
-
schemaVersion: 2,
|
|
1098
|
-
counts,
|
|
1099
|
-
tabs: [
|
|
1100
|
-
{
|
|
1101
|
-
queue: "verify",
|
|
1102
|
-
count: counts.verify
|
|
1103
|
-
},
|
|
1104
|
-
{
|
|
1105
|
-
queue: "pending",
|
|
1106
|
-
count: counts.pending
|
|
1107
|
-
},
|
|
1108
|
-
{
|
|
1109
|
-
queue: "fuse",
|
|
1110
|
-
count: counts.fuse
|
|
1111
|
-
},
|
|
1112
|
-
{
|
|
1113
|
-
queue: "skipped",
|
|
1114
|
-
count: counts.skipped
|
|
1115
|
-
}
|
|
1116
|
-
],
|
|
1117
|
-
commands,
|
|
1118
|
-
...latestRun === void 0 ? {} : { latestRun },
|
|
1119
|
-
...approval === void 0 ? {} : { approval }
|
|
1120
|
-
};
|
|
1121
|
-
}
|
|
1122
|
-
function pageQueue(rows, queue, cursor = 0, limit = 50) {
|
|
1123
|
-
const filtered = rows.filter((row) => queueOf(row.state) === queue);
|
|
1124
|
-
const size = Math.max(1, Math.min(200, limit));
|
|
1125
|
-
const start = Math.max(0, cursor);
|
|
1126
|
-
const items = filtered.slice(start, start + size).map(toRowView);
|
|
1127
|
-
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)));
|
|
1128
1726
|
return {
|
|
1727
|
+
wireVersion: 1,
|
|
1728
|
+
readAt,
|
|
1129
1729
|
queue,
|
|
1130
|
-
|
|
1131
|
-
|
|
1730
|
+
expiredOnly,
|
|
1731
|
+
counts: countsOf(rows),
|
|
1732
|
+
rows: page.map((row) => toRowFact(row)),
|
|
1733
|
+
...filtered.length > page.length ? { nextCursor: String(page.length) } : {},
|
|
1734
|
+
notes: []
|
|
1132
1735
|
};
|
|
1133
1736
|
}
|
|
1134
|
-
function
|
|
1737
|
+
function toRunsView(runs, readAt) {
|
|
1135
1738
|
return {
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
...
|
|
1139
|
-
...run.stage === void 0 ? {} : { stage: run.stage },
|
|
1140
|
-
...run.progress === void 0 ? {} : { progress: run.progress },
|
|
1141
|
-
startedAt: run.startedAt,
|
|
1142
|
-
...run.endedAt === void 0 ? {} : { endedAt: run.endedAt },
|
|
1143
|
-
...run.resultRef === void 0 ? {} : { resultRef: run.resultRef },
|
|
1144
|
-
...run.failure === void 0 ? {} : { failure: run.failure },
|
|
1145
|
-
...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))
|
|
1146
1742
|
};
|
|
1147
1743
|
}
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
continue;
|
|
1174
|
-
}
|
|
1175
|
-
const heading = /^(#{1,6})\s+(.*)$/.exec(line);
|
|
1176
|
-
if (heading !== null) {
|
|
1177
|
-
if (inList) {
|
|
1178
|
-
out.push("</ul>");
|
|
1179
|
-
inList = false;
|
|
1180
|
-
}
|
|
1181
|
-
const level = heading[1]?.length ?? 1;
|
|
1182
|
-
out.push("<h" + String(level) + ">" + escapeHtml(heading[2] ?? "") + "</h" + String(level) + ">");
|
|
1183
|
-
continue;
|
|
1184
|
-
}
|
|
1185
|
-
if (/^[-*]\s+/.test(line)) {
|
|
1186
|
-
if (!inList) {
|
|
1187
|
-
out.push("<ul>");
|
|
1188
|
-
inList = true;
|
|
1189
|
-
}
|
|
1190
|
-
out.push("<li>" + escapeHtml(line.replace(/^[-*]\s+/, "")) + "</li>");
|
|
1191
|
-
continue;
|
|
1192
|
-
}
|
|
1193
|
-
if (inList) {
|
|
1194
|
-
out.push("</ul>");
|
|
1195
|
-
inList = false;
|
|
1196
|
-
}
|
|
1197
|
-
out.push(line.trim() === "" ? "" : "<p>" + escapeHtml(line) + "</p>");
|
|
1198
|
-
}
|
|
1199
|
-
if (inList) out.push("</ul>");
|
|
1200
|
-
if (inCode) out.push("</code></pre>");
|
|
1201
|
-
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
|
+
};
|
|
1202
1769
|
}
|
|
1203
1770
|
//#endregion
|
|
1204
1771
|
//#region lib/types/merge-routes.js
|
|
@@ -1211,12 +1778,15 @@ const MERGE_ROUTES = {
|
|
|
1211
1778
|
history: MERGE_PATHS.history ?? MERGE_BASE + "history"
|
|
1212
1779
|
};
|
|
1213
1780
|
function makeMergeRoutes(deps, _audit) {
|
|
1214
|
-
const absent = () => json(
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
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
|
|
1220
1790
|
});
|
|
1221
1791
|
const get = (request) => request.method === "GET" ? null : methodNotAllowed(["GET"]);
|
|
1222
1792
|
const url = (request) => new URL(request.url, "http://local");
|
|
@@ -1230,11 +1800,13 @@ function makeMergeRoutes(deps, _audit) {
|
|
|
1230
1800
|
if (denied !== null) return denied;
|
|
1231
1801
|
const source = deps.getSource();
|
|
1232
1802
|
if (source === void 0) return absent();
|
|
1233
|
-
const
|
|
1234
|
-
return json(200, {
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
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
|
+
}));
|
|
1238
1810
|
}
|
|
1239
1811
|
},
|
|
1240
1812
|
{
|
|
@@ -1248,19 +1820,16 @@ function makeMergeRoutes(deps, _audit) {
|
|
|
1248
1820
|
if (source === void 0) return absent();
|
|
1249
1821
|
const query = url(request).searchParams;
|
|
1250
1822
|
const queue = query.get("queue") ?? "verify";
|
|
1251
|
-
if (queue !== "verify" && queue !== "pending" && queue !== "fuse" && queue !== "skipped") return
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
code: "bad-queue",
|
|
1255
|
-
message: "queue must be verify/pending/fuse/skipped"
|
|
1256
|
-
}
|
|
1257
|
-
});
|
|
1258
|
-
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");
|
|
1259
1826
|
const limit = Number.parseInt(query.get("limit") ?? "50", 10);
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
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);
|
|
1264
1833
|
}
|
|
1265
1834
|
},
|
|
1266
1835
|
{
|
|
@@ -1275,28 +1844,13 @@ function makeMergeRoutes(deps, _audit) {
|
|
|
1275
1844
|
const query = url(request).searchParams;
|
|
1276
1845
|
const id = query.get("id");
|
|
1277
1846
|
const side = query.get("side");
|
|
1278
|
-
if (id === null || id === "" || side !== "a" && side !== "b") return
|
|
1279
|
-
ok: false,
|
|
1280
|
-
error: {
|
|
1281
|
-
code: "bad-body-query",
|
|
1282
|
-
message: "id and side=a|b are required"
|
|
1283
|
-
}
|
|
1284
|
-
});
|
|
1847
|
+
if (id === null || id === "" || side !== "a" && side !== "b") return bad("id and side=a|b are required");
|
|
1285
1848
|
const content = await source.body(id, side);
|
|
1286
|
-
return content === null ? json(
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
}
|
|
1292
|
-
}) : json(200, {
|
|
1293
|
-
ok: true,
|
|
1294
|
-
data: {
|
|
1295
|
-
content,
|
|
1296
|
-
markdown: true,
|
|
1297
|
-
html: renderMarkdown(content)
|
|
1298
|
-
}
|
|
1299
|
-
});
|
|
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()));
|
|
1300
1854
|
}
|
|
1301
1855
|
},
|
|
1302
1856
|
{
|
|
@@ -1308,10 +1862,7 @@ function makeMergeRoutes(deps, _audit) {
|
|
|
1308
1862
|
if (denied !== null) return denied;
|
|
1309
1863
|
const source = deps.getSource();
|
|
1310
1864
|
if (source === void 0) return absent();
|
|
1311
|
-
return json(200,
|
|
1312
|
-
ok: true,
|
|
1313
|
-
data: { items: await source.history() }
|
|
1314
|
-
});
|
|
1865
|
+
return json(200, toHistoryView(await source.history(), Date.now()));
|
|
1315
1866
|
}
|
|
1316
1867
|
}
|
|
1317
1868
|
];
|
|
@@ -1559,8 +2110,58 @@ function mergeUsage() {
|
|
|
1559
2110
|
return USAGE;
|
|
1560
2111
|
}
|
|
1561
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
|
|
1562
2163
|
//#region lib/types/read-commands.js
|
|
1563
|
-
async function handleReadOnlyMergeCommand(command, io, home, cards) {
|
|
2164
|
+
async function handleReadOnlyMergeCommand(command, io, home, cards, runs, approval = false) {
|
|
1564
2165
|
const withNote = (text, note) => note === void 0 ? text : "note: " + note + "\n" + text;
|
|
1565
2166
|
const load = async () => {
|
|
1566
2167
|
const result = await loadMergeState(io, home);
|
|
@@ -1573,7 +2174,13 @@ async function handleReadOnlyMergeCommand(command, io, home, cards) {
|
|
|
1573
2174
|
const row = loaded.state.rows.find((item) => item.id === command.id);
|
|
1574
2175
|
return withNote(row === void 0 ? "merge row not found: " + command.id : formatStatus([row]), loaded.note);
|
|
1575
2176
|
}
|
|
1576
|
-
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);
|
|
1577
2184
|
}
|
|
1578
2185
|
if (command.verb === "pending") {
|
|
1579
2186
|
const loaded = await load();
|
|
@@ -1659,6 +2266,115 @@ function createMergeCommand(service) {
|
|
|
1659
2266
|
};
|
|
1660
2267
|
}
|
|
1661
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
|
|
1662
2378
|
//#region lib/types/skipped.js
|
|
1663
2379
|
/**
|
|
1664
2380
|
* Skipped filtering: a pair the operator marked as not-worth-merging stays out of automatic
|
|
@@ -1806,29 +2522,46 @@ async function runScan(options) {
|
|
|
1806
2522
|
});
|
|
1807
2523
|
let row;
|
|
1808
2524
|
try {
|
|
1809
|
-
const
|
|
1810
|
-
|
|
2525
|
+
const key = pairKey(candidate.a, candidate.b);
|
|
2526
|
+
const atNow = at(options.now);
|
|
2527
|
+
row = {
|
|
2528
|
+
id: rowIdOf(key),
|
|
2529
|
+
pairKey: key,
|
|
1811
2530
|
a: candidate.a,
|
|
1812
2531
|
b: candidate.b,
|
|
1813
|
-
|
|
1814
|
-
|
|
1815
|
-
|
|
1816
|
-
|
|
1817
|
-
...
|
|
1818
|
-
runId: options.runId
|
|
2532
|
+
state: "verify",
|
|
2533
|
+
queue: "verify",
|
|
2534
|
+
createdAt: atNow,
|
|
2535
|
+
updatedAt: atNow,
|
|
2536
|
+
...options.runId === void 0 ? {} : { runId: options.runId }
|
|
1819
2537
|
};
|
|
1820
2538
|
const before = (await loadMergeState(options.io, options.home)).state.rows.find((item) => item.pairKey === row.pairKey);
|
|
1821
2539
|
if (before?.state === "verify" && before.runId !== void 0 && before.runId !== options.runId && await options.isOwnerActive?.(before.runId) === true) continue;
|
|
1822
|
-
const
|
|
1823
|
-
|
|
1824
|
-
|
|
1825
|
-
|
|
1826
|
-
|
|
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 }
|
|
1827
2560
|
});
|
|
1828
|
-
|
|
1829
|
-
|
|
1830
|
-
|
|
1831
|
-
|
|
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);
|
|
1832
2565
|
} catch (error) {
|
|
1833
2566
|
totals.failed += 1;
|
|
1834
2567
|
pairs.push({
|
|
@@ -1862,33 +2595,47 @@ async function runScan(options) {
|
|
|
1862
2595
|
break;
|
|
1863
2596
|
}
|
|
1864
2597
|
if (round1.kind === "failed") {
|
|
1865
|
-
const
|
|
1866
|
-
|
|
1867
|
-
|
|
1868
|
-
|
|
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;
|
|
1869
2607
|
});
|
|
1870
|
-
if (
|
|
1871
|
-
row
|
|
1872
|
-
|
|
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;
|
|
1873
2612
|
}
|
|
1874
|
-
|
|
2613
|
+
row = projected;
|
|
2614
|
+
pairs.push(pairOf(row, candidate, "failed", { failure }));
|
|
1875
2615
|
totals.failed += 1;
|
|
1876
2616
|
continue;
|
|
1877
2617
|
}
|
|
1878
2618
|
if (round1.kind === "keep") {
|
|
1879
|
-
const
|
|
1880
|
-
|
|
1881
|
-
|
|
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;
|
|
1882
2627
|
});
|
|
1883
|
-
if (
|
|
1884
|
-
row
|
|
1885
|
-
|
|
1886
|
-
|
|
1887
|
-
pairKey: row.pairKey,
|
|
1888
|
-
runId: row.runId ?? "merge",
|
|
1889
|
-
result: "kept"
|
|
1890
|
-
});
|
|
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;
|
|
1891
2632
|
}
|
|
2633
|
+
row = projected;
|
|
2634
|
+
await recordPair(options.io, options.home, {
|
|
2635
|
+
pairKey: row.pairKey,
|
|
2636
|
+
runId: row.runId ?? "merge",
|
|
2637
|
+
result: "kept"
|
|
2638
|
+
});
|
|
1892
2639
|
pairs.push(pairOf(row, candidate, "kept", round1.decision === void 0 ? {} : { decision: round1.decision }));
|
|
1893
2640
|
totals.kept += 1;
|
|
1894
2641
|
continue;
|
|
@@ -1899,49 +2646,79 @@ async function runScan(options) {
|
|
|
1899
2646
|
continue;
|
|
1900
2647
|
}
|
|
1901
2648
|
if (options.approval) {
|
|
1902
|
-
const
|
|
1903
|
-
type: "
|
|
1904
|
-
approval: "on",
|
|
2649
|
+
const event = {
|
|
2650
|
+
type: "verify-passed",
|
|
1905
2651
|
decision: round1.decision,
|
|
1906
|
-
|
|
1907
|
-
...round1.
|
|
1908
|
-
|
|
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;
|
|
1909
2660
|
});
|
|
1910
|
-
if (
|
|
1911
|
-
row
|
|
1912
|
-
|
|
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;
|
|
1913
2668
|
}
|
|
2669
|
+
row = projected;
|
|
1914
2670
|
pairs.push(pairOf(row, candidate, "pending", { decision: round1.decision }));
|
|
1915
2671
|
totals.pending += 1;
|
|
1916
2672
|
continue;
|
|
1917
2673
|
}
|
|
1918
|
-
const
|
|
1919
|
-
|
|
1920
|
-
|
|
1921
|
-
|
|
1922
|
-
|
|
1923
|
-
|
|
1924
|
-
|
|
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;
|
|
1925
2690
|
});
|
|
1926
|
-
if (
|
|
2691
|
+
if (staged === null) {
|
|
1927
2692
|
pairs.push(pairOf(row, candidate, "failed", {
|
|
1928
2693
|
decision: round1.decision,
|
|
1929
|
-
failure: failureOf("
|
|
2694
|
+
failure: failureOf("author", "row changed before staging record")
|
|
1930
2695
|
}));
|
|
1931
2696
|
totals.failed += 1;
|
|
1932
2697
|
continue;
|
|
1933
2698
|
}
|
|
1934
|
-
row =
|
|
1935
|
-
|
|
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
|
+
}
|
|
1936
2711
|
if (isAborted(options.signal)) {
|
|
1937
|
-
const
|
|
1938
|
-
|
|
1939
|
-
|
|
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;
|
|
1940
2720
|
});
|
|
1941
|
-
if (
|
|
1942
|
-
row = cancelled.row;
|
|
1943
|
-
await upsertRow(options.io, options.home, options.now, row);
|
|
1944
|
-
}
|
|
2721
|
+
if (projected !== null) row = projected;
|
|
1945
2722
|
stopReason = "cancelled";
|
|
1946
2723
|
remaining.push(...candidates.slice(index).map((candidate) => ({
|
|
1947
2724
|
pairKey: pairKey(candidate.a, candidate.b),
|
|
@@ -1963,10 +2740,14 @@ async function runScan(options) {
|
|
|
1963
2740
|
};
|
|
1964
2741
|
}
|
|
1965
2742
|
if (round2.kind === "merged") {
|
|
1966
|
-
if (
|
|
1967
|
-
|
|
1968
|
-
|
|
1969
|
-
|
|
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
|
+
}
|
|
1970
2751
|
pairs.push(pairOf(row, candidate, "merged", {
|
|
1971
2752
|
decision: round1.decision,
|
|
1972
2753
|
...round2.unitId === void 0 ? {} : { unitId: round2.unitId },
|
|
@@ -1977,15 +2758,24 @@ async function runScan(options) {
|
|
|
1977
2758
|
continue;
|
|
1978
2759
|
}
|
|
1979
2760
|
const failed = round2.failure ?? failureOf("author", "round2 failed");
|
|
1980
|
-
const
|
|
1981
|
-
|
|
1982
|
-
|
|
1983
|
-
|
|
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;
|
|
1984
2769
|
});
|
|
1985
|
-
if (
|
|
1986
|
-
row
|
|
1987
|
-
|
|
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;
|
|
1988
2777
|
}
|
|
2778
|
+
row = projected;
|
|
1989
2779
|
pairs.push(pairOf(row, candidate, "failed", {
|
|
1990
2780
|
decision: round1.decision,
|
|
1991
2781
|
failure: failed
|
|
@@ -2205,48 +2995,40 @@ async function runScanCommand(options) {
|
|
|
2205
2995
|
//#endregion
|
|
2206
2996
|
//#region lib/types/pending-actions.js
|
|
2207
2997
|
async function act(io, home, now, id, type) {
|
|
2208
|
-
return await
|
|
2209
|
-
const
|
|
2210
|
-
if (
|
|
2211
|
-
|
|
2212
|
-
|
|
2213
|
-
|
|
2214
|
-
reason: "not-found",
|
|
2215
|
-
message: "pending row not found: " + id
|
|
2216
|
-
}
|
|
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
|
|
2217
3004
|
};
|
|
2218
|
-
|
|
2219
|
-
|
|
2220
|
-
|
|
2221
|
-
|
|
2222
|
-
ok: false,
|
|
2223
|
-
reason: "not-pending",
|
|
2224
|
-
message: "row is not pending: " + current.state
|
|
2225
|
-
}
|
|
3005
|
+
if (row.state !== "pending") return {
|
|
3006
|
+
ok: false,
|
|
3007
|
+
reason: "not-pending",
|
|
3008
|
+
message: "row is not pending: " + row.state
|
|
2226
3009
|
};
|
|
2227
|
-
const
|
|
2228
|
-
type: "
|
|
2229
|
-
|
|
3010
|
+
const event = type === "approve" ? {
|
|
3011
|
+
type: "approved",
|
|
3012
|
+
runId: row.runId ?? "approve:" + id
|
|
2230
3013
|
} : {
|
|
2231
|
-
type: "
|
|
2232
|
-
|
|
2233
|
-
|
|
2234
|
-
|
|
2235
|
-
|
|
2236
|
-
|
|
2237
|
-
|
|
2238
|
-
|
|
2239
|
-
|
|
2240
|
-
|
|
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"
|
|
2241
3028
|
};
|
|
2242
|
-
const copy = [...rows];
|
|
2243
|
-
copy[index] = next.row;
|
|
2244
3029
|
return {
|
|
2245
|
-
|
|
2246
|
-
|
|
2247
|
-
ok: true,
|
|
2248
|
-
row: next.row
|
|
2249
|
-
}
|
|
3030
|
+
ok: true,
|
|
3031
|
+
row: v3ToLegacy(moved.row)
|
|
2250
3032
|
};
|
|
2251
3033
|
});
|
|
2252
3034
|
}
|
|
@@ -2257,79 +3039,29 @@ async function rejectPending(io, home, now, id) {
|
|
|
2257
3039
|
return await act(io, home, now, id, "reject");
|
|
2258
3040
|
}
|
|
2259
3041
|
//#endregion
|
|
2260
|
-
//#region lib/types/domain/claim.js
|
|
2261
|
-
function acquireClaim(current, now, leaseMs, token) {
|
|
2262
|
-
if (current !== void 0 && current.expiresAt > now) return void 0;
|
|
2263
|
-
return {
|
|
2264
|
-
id: "claim",
|
|
2265
|
-
token,
|
|
2266
|
-
expiresAt: now + Math.max(1, leaseMs)
|
|
2267
|
-
};
|
|
2268
|
-
}
|
|
2269
|
-
function releaseClaim(current, token) {
|
|
2270
|
-
return current?.token === token ? void 0 : current;
|
|
2271
|
-
}
|
|
2272
|
-
//#endregion
|
|
2273
|
-
//#region lib/types/target-claim.js
|
|
2274
|
-
/** Durable target claim: one approve writer per merge target at a time. */
|
|
2275
|
-
const LEASE_MS = 30 * 6e4;
|
|
2276
|
-
function claimPath(home, target) {
|
|
2277
|
-
return join(home, "merge", "claims", contentHash(target) + ".json");
|
|
2278
|
-
}
|
|
2279
|
-
function parseClaim(raw) {
|
|
2280
|
-
if (raw === null) return void 0;
|
|
2281
|
-
const parsed = JSON.parse(raw);
|
|
2282
|
-
if (typeof parsed.token !== "string" || typeof parsed.expiresAt !== "number") throw new Error("corrupt target claim");
|
|
2283
|
-
return {
|
|
2284
|
-
id: "claim",
|
|
2285
|
-
token: parsed.token,
|
|
2286
|
-
expiresAt: parsed.expiresAt
|
|
2287
|
-
};
|
|
2288
|
-
}
|
|
2289
|
-
async function compareAndSwap(io, path, task) {
|
|
2290
|
-
if (io.transact !== void 0) {
|
|
2291
|
-
await io.transact(path, (current) => task(current));
|
|
2292
|
-
return;
|
|
2293
|
-
}
|
|
2294
|
-
const current = await io.exists(path) ? await io.readText(path) : null;
|
|
2295
|
-
const next = task(current);
|
|
2296
|
-
if (next === null) {
|
|
2297
|
-
if (current !== null) try {
|
|
2298
|
-
await io.remove(path);
|
|
2299
|
-
} catch {}
|
|
2300
|
-
} else if (next !== current) await io.writeText(path, next);
|
|
2301
|
-
}
|
|
2302
|
-
async function acquireTargetClaim(io, home, target, now) {
|
|
2303
|
-
const path = claimPath(home, target);
|
|
2304
|
-
const token = randomUUID();
|
|
2305
|
-
let granted;
|
|
2306
|
-
await compareAndSwap(io, path, (raw) => {
|
|
2307
|
-
const claim = acquireClaim(parseClaim(raw), now, LEASE_MS, token);
|
|
2308
|
-
if (claim === void 0) return raw;
|
|
2309
|
-
granted = claim;
|
|
2310
|
-
return JSON.stringify(claim) + "\n";
|
|
2311
|
-
});
|
|
2312
|
-
if (granted === void 0) return null;
|
|
2313
|
-
return {
|
|
2314
|
-
token,
|
|
2315
|
-
async release() {
|
|
2316
|
-
await compareAndSwap(io, path, (raw) => {
|
|
2317
|
-
const next = releaseClaim(parseClaim(raw), token);
|
|
2318
|
-
return next === void 0 ? null : JSON.stringify(next) + "\n";
|
|
2319
|
-
});
|
|
2320
|
-
}
|
|
2321
|
-
};
|
|
2322
|
-
}
|
|
2323
|
-
//#endregion
|
|
2324
3042
|
//#region lib/types/round2-outcome.js
|
|
2325
3043
|
async function recordRound2Outcome(io, home, now, row, outcome) {
|
|
2326
|
-
const at = new Date(now()).toISOString();
|
|
2327
3044
|
if (outcome.kind === "merged") {
|
|
2328
|
-
|
|
2329
|
-
|
|
2330
|
-
|
|
2331
|
-
|
|
2332
|
-
|
|
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
|
+
};
|
|
2333
3065
|
const target = outcome.target?.actual ?? outcome.target?.requested ?? row.decision?.target;
|
|
2334
3066
|
await recordPair(io, home, {
|
|
2335
3067
|
pairKey: row.pairKey,
|
|
@@ -2362,20 +3094,37 @@ async function recordRound2Outcome(io, home, now, row, outcome) {
|
|
|
2362
3094
|
retryable: true,
|
|
2363
3095
|
retryFrom: "agent2"
|
|
2364
3096
|
};
|
|
2365
|
-
|
|
2366
|
-
|
|
2367
|
-
|
|
2368
|
-
|
|
2369
|
-
|
|
2370
|
-
|
|
2371
|
-
|
|
2372
|
-
|
|
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,
|
|
2373
3109
|
pairKey: row.pairKey,
|
|
2374
|
-
|
|
2375
|
-
|
|
2376
|
-
|
|
2377
|
-
|
|
2378
|
-
|
|
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
|
+
});
|
|
2379
3128
|
return {
|
|
2380
3129
|
merged: false,
|
|
2381
3130
|
pair: {
|
|
@@ -2394,89 +3143,89 @@ async function recordRound2Outcome(io, home, now, row, outcome) {
|
|
|
2394
3143
|
//#endregion
|
|
2395
3144
|
//#region lib/types/approve-command.js
|
|
2396
3145
|
async function runApproveCommand(options) {
|
|
2397
|
-
const
|
|
2398
|
-
|
|
2399
|
-
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 {
|
|
2400
3148
|
ok: false,
|
|
2401
|
-
message: "
|
|
3149
|
+
message: "pending row not found: " + options.rowId
|
|
2402
3150
|
};
|
|
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
|
-
|
|
2445
|
-
|
|
2446
|
-
|
|
2447
|
-
scope: {
|
|
2448
|
-
candidatePairs: 1,
|
|
2449
|
-
queueBefore: {
|
|
2450
|
-
verify: 0,
|
|
2451
|
-
pending: 0,
|
|
2452
|
-
fuse: 1,
|
|
2453
|
-
skipped: 0
|
|
2454
|
-
}
|
|
2455
|
-
},
|
|
2456
|
-
notes: []
|
|
2457
|
-
};
|
|
2458
|
-
const record = await recordRound2Outcome(options.io, options.home, options.now, row, outcome);
|
|
2459
|
-
return {
|
|
2460
|
-
...base,
|
|
2461
|
-
stopReason: record.merged ? "completed" : "failed",
|
|
2462
|
-
pairs: [record.pair],
|
|
2463
|
-
totals: record.merged ? {
|
|
2464
|
-
merged: 1,
|
|
2465
|
-
kept: 0,
|
|
2466
|
-
pending: 0,
|
|
2467
|
-
failed: 0
|
|
2468
|
-
} : {
|
|
2469
|
-
merged: 0,
|
|
2470
|
-
kept: 0,
|
|
2471
|
-
pending: 0,
|
|
2472
|
-
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"
|
|
2473
3195
|
}
|
|
2474
3196
|
};
|
|
2475
3197
|
}
|
|
2476
|
-
|
|
2477
|
-
|
|
2478
|
-
|
|
2479
|
-
|
|
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
|
+
});
|
|
2480
3229
|
}
|
|
2481
3230
|
//#endregion
|
|
2482
3231
|
//#region lib/types/resume-command.js
|
|
@@ -2498,6 +3247,7 @@ async function runResumeCommand(options) {
|
|
|
2498
3247
|
approval: true,
|
|
2499
3248
|
platformWrite: options.platformWrite,
|
|
2500
3249
|
now: options.now,
|
|
3250
|
+
...options.detached === true ? { detached: true } : {},
|
|
2501
3251
|
...options.makePorts === void 0 ? { ports: {
|
|
2502
3252
|
...options.ports,
|
|
2503
3253
|
screening: () => Promise.resolve([{
|
|
@@ -2542,6 +3292,7 @@ async function runResumeCommand(options) {
|
|
|
2542
3292
|
mergeApproval: true
|
|
2543
3293
|
},
|
|
2544
3294
|
now: options.now,
|
|
3295
|
+
...options.detached === true ? { detached: true } : {},
|
|
2545
3296
|
async body(ctx) {
|
|
2546
3297
|
await ctx.stage("author", {
|
|
2547
3298
|
done: 0,
|
|
@@ -2658,6 +3409,245 @@ function skillLibraryUndoPort(skills) {
|
|
|
2658
3409
|
};
|
|
2659
3410
|
}
|
|
2660
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
|
|
2661
3651
|
//#region lib/types/doctor.js
|
|
2662
3652
|
/**
|
|
2663
3653
|
* Merge doctor rows: state, pending, gate cache and staging are each probed read-only.
|
|
@@ -2665,6 +3655,13 @@ function skillLibraryUndoPort(skills) {
|
|
|
2665
3655
|
* operator can act on. This module never repairs or writes.
|
|
2666
3656
|
*/
|
|
2667
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
|
+
}
|
|
2668
3665
|
async function mergeDoctor(io, home, configuredRoot) {
|
|
2669
3666
|
const rows = [];
|
|
2670
3667
|
const loaded = await loadMergeState(io, home);
|
|
@@ -2675,12 +3672,120 @@ async function mergeDoctor(io, home, configuredRoot) {
|
|
|
2675
3672
|
status: loaded.note === void 0 ? "ok" : "warn",
|
|
2676
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)
|
|
2677
3674
|
});
|
|
2678
|
-
const
|
|
2679
|
-
|
|
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({
|
|
2680
3785
|
key: "models",
|
|
2681
3786
|
label: "合并模型",
|
|
2682
3787
|
status: "warn",
|
|
2683
|
-
detail: "
|
|
3788
|
+
detail: "未配置 mergeModelsRoot;合并扫描会 fail loud"
|
|
2684
3789
|
});
|
|
2685
3790
|
else {
|
|
2686
3791
|
const int8 = join(modelRoot, "potion-multilingual-128M-int8");
|
|
@@ -2707,12 +3812,32 @@ async function mergeDoctor(io, home, configuredRoot) {
|
|
|
2707
3812
|
status: "ok",
|
|
2708
3813
|
detail: stagingRoot(home, "<runId>")
|
|
2709
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
|
+
}
|
|
2710
3835
|
return rows;
|
|
2711
3836
|
}
|
|
2712
3837
|
//#endregion
|
|
2713
3838
|
//#region lib/types/decision.js
|
|
2714
3839
|
function extractJson(text) {
|
|
2715
|
-
const body =
|
|
3840
|
+
const body = /`\``(?:json)?\s*([\s\S]*?)`\``/i.exec(text)?.[1] ?? text;
|
|
2716
3841
|
const start = body.indexOf("{");
|
|
2717
3842
|
if (start < 0) return void 0;
|
|
2718
3843
|
let depth = 0;
|
|
@@ -3239,12 +4364,17 @@ async function assembleStagedCommit(io, home, runId, port, options) {
|
|
|
3239
4364
|
const requested = staging.value.manifest.target;
|
|
3240
4365
|
let target = requested;
|
|
3241
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
|
+
};
|
|
3242
4371
|
const requestedArchived = targetCurrent === null && await port.isArchived?.(requested) === true;
|
|
3243
|
-
|
|
4372
|
+
const requestedOccupied = targetCurrent === null && await port.isNameOccupied?.(requested) === true;
|
|
4373
|
+
if (staging.value.manifest.mode === "create-umbrella" && (targetCurrent !== null || requestedArchived || requestedOccupied)) {
|
|
3244
4374
|
target = requested;
|
|
3245
4375
|
for (let suffix = 2; suffix <= 99; suffix += 1) {
|
|
3246
4376
|
const candidate = requested + "-v" + String(suffix);
|
|
3247
|
-
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) {
|
|
3248
4378
|
target = candidate;
|
|
3249
4379
|
targetCurrent = null;
|
|
3250
4380
|
break;
|
|
@@ -3289,27 +4419,87 @@ async function commitStagedRun(io, home, runId, port, options) {
|
|
|
3289
4419
|
ok: false,
|
|
3290
4420
|
message: assembled.message
|
|
3291
4421
|
};
|
|
3292
|
-
|
|
3293
|
-
|
|
3294
|
-
|
|
3295
|
-
|
|
3296
|
-
|
|
3297
|
-
message: "commit snapshot failed; refusing to write: " + (error instanceof Error ? error.message : String(error)),
|
|
3298
|
-
plan: assembled.plan
|
|
3299
|
-
};
|
|
3300
|
-
}
|
|
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
|
+
};
|
|
3301
4427
|
const journal = {
|
|
3302
4428
|
runId,
|
|
3303
4429
|
target: assembled.plan.target,
|
|
3304
4430
|
create: !assembled.plan.targetExists,
|
|
3305
4431
|
sources: assembled.plan.sources,
|
|
3306
|
-
phase: "
|
|
4432
|
+
phase: "prepared",
|
|
3307
4433
|
updatedAt: new Date(options.now()).toISOString()
|
|
3308
4434
|
};
|
|
3309
|
-
|
|
3310
|
-
|
|
3311
|
-
|
|
3312
|
-
|
|
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
|
+
}
|
|
3313
4503
|
if (!result.ok) {
|
|
3314
4504
|
try {
|
|
3315
4505
|
await writeJournal(io, home, {
|
|
@@ -3327,7 +4517,8 @@ async function commitStagedRun(io, home, runId, port, options) {
|
|
|
3327
4517
|
const committed = {
|
|
3328
4518
|
...journal,
|
|
3329
4519
|
phase: "committed",
|
|
3330
|
-
updatedAt: new Date(options.now()).toISOString()
|
|
4520
|
+
updatedAt: new Date(options.now()).toISOString(),
|
|
4521
|
+
...result.unitId === void 0 ? {} : { unitId: result.unitId }
|
|
3331
4522
|
};
|
|
3332
4523
|
try {
|
|
3333
4524
|
await writeJournal(io, home, committed);
|
|
@@ -3373,13 +4564,25 @@ async function commitStagedRun(io, home, runId, port, options) {
|
|
|
3373
4564
|
//#region lib/types/authoring-flow.js
|
|
3374
4565
|
async function runAuthoring(options) {
|
|
3375
4566
|
const targetCard = options.cards.find((card) => card.name === options.target);
|
|
3376
|
-
|
|
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 {
|
|
3377
4580
|
ok: false,
|
|
3378
|
-
message: "target is
|
|
4581
|
+
message: "target is blocked by curator: " + options.target
|
|
3379
4582
|
};
|
|
3380
4583
|
const stageKey = options.stageKey ?? options.runId;
|
|
3381
4584
|
if (options.reuseStaging === true) {
|
|
3382
|
-
if (!await setStagingTarget(options.io, options.home, stageKey, options.target,
|
|
4585
|
+
if (!await setStagingTarget(options.io, options.home, stageKey, options.target, mode)) return {
|
|
3383
4586
|
ok: false,
|
|
3384
4587
|
message: "staging missing or unreadable: " + stageKey
|
|
3385
4588
|
};
|
|
@@ -3387,7 +4590,7 @@ async function runAuthoring(options) {
|
|
|
3387
4590
|
const created = await createStaging(options.io, options.home, {
|
|
3388
4591
|
runId: stageKey,
|
|
3389
4592
|
target: options.target,
|
|
3390
|
-
|
|
4593
|
+
mode,
|
|
3391
4594
|
sources: options.cards.map((card) => ({
|
|
3392
4595
|
name: card.name,
|
|
3393
4596
|
body: card.body,
|
|
@@ -3425,9 +4628,11 @@ async function runAuthoring(options) {
|
|
|
3425
4628
|
findings
|
|
3426
4629
|
};
|
|
3427
4630
|
const commit = await commitStagedRun(options.io, options.home, stageKey, options.commitPort, {
|
|
3428
|
-
origin: "
|
|
4631
|
+
origin: "foreground",
|
|
3429
4632
|
summary: options.summary,
|
|
3430
|
-
now: options.now
|
|
4633
|
+
now: options.now,
|
|
4634
|
+
...options.isBlocked === void 0 ? {} : { isBlocked: options.isBlocked },
|
|
4635
|
+
...options.unit === void 0 ? {} : { unit: options.unit }
|
|
3431
4636
|
});
|
|
3432
4637
|
if (!commit.ok) return {
|
|
3433
4638
|
ok: false,
|
|
@@ -3553,7 +4758,9 @@ function createChannelScanPorts(options) {
|
|
|
3553
4758
|
mode: decision.mode,
|
|
3554
4759
|
cards: options.cards,
|
|
3555
4760
|
commitPort: options.commitPort,
|
|
4761
|
+
...options.unit === void 0 ? {} : { unit: options.unit },
|
|
3556
4762
|
now: options.now,
|
|
4763
|
+
...options.isBlocked === void 0 ? {} : { isBlocked: options.isBlocked },
|
|
3557
4764
|
summary: "merge " + row.a + " + " + row.b,
|
|
3558
4765
|
author: async () => {
|
|
3559
4766
|
await entry.result.author?.(row, decision, signal);
|
|
@@ -3592,12 +4799,35 @@ function createChannelScanPorts(options) {
|
|
|
3592
4799
|
//#region lib/types/new-child-authoring.js
|
|
3593
4800
|
async function runNewChildAuthoring(options, row, decision, signal) {
|
|
3594
4801
|
const targetCard = options.cards.find((card) => card.name === decision.target);
|
|
3595
|
-
|
|
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 {
|
|
3596
4826
|
kind: "failed",
|
|
3597
4827
|
failure: {
|
|
3598
4828
|
stage: "author",
|
|
3599
|
-
code: "target-
|
|
3600
|
-
message: "target is
|
|
4829
|
+
code: "target-blocked",
|
|
4830
|
+
message: "target is blocked by curator: " + decision.target,
|
|
3601
4831
|
retryable: false,
|
|
3602
4832
|
retryFrom: "none"
|
|
3603
4833
|
}
|
|
@@ -3712,9 +4942,11 @@ async function runNewChildAuthoring(options, row, decision, signal) {
|
|
|
3712
4942
|
}
|
|
3713
4943
|
};
|
|
3714
4944
|
const commit = await commitStagedRun(options.io, options.home, stageKey, options.commitPort, {
|
|
3715
|
-
origin: "
|
|
4945
|
+
origin: "foreground",
|
|
3716
4946
|
summary: "merge " + row.a + " + " + row.b,
|
|
3717
|
-
now: options.now
|
|
4947
|
+
now: options.now,
|
|
4948
|
+
...options.isBlocked === void 0 ? {} : { isBlocked: options.isBlocked },
|
|
4949
|
+
...options.unit === void 0 ? {} : { unit: options.unit }
|
|
3718
4950
|
});
|
|
3719
4951
|
if (!commit.ok) return {
|
|
3720
4952
|
kind: "failed",
|
|
@@ -3761,7 +4993,10 @@ function skillLibraryPort(skills) {
|
|
|
3761
4993
|
const names = await archivedNames.call(skills);
|
|
3762
4994
|
return names === null ? true : names.includes(name);
|
|
3763
4995
|
},
|
|
3764
|
-
|
|
4996
|
+
isNameOccupied: async (name) => {
|
|
4997
|
+
const probe = skills.skillDirExists;
|
|
4998
|
+
return typeof probe === "function" ? await probe.call(skills, name) : false;
|
|
4999
|
+
},
|
|
3765
5000
|
async readSupportFiles(name) {
|
|
3766
5001
|
if (typeof skills.listSupportFiles !== "function") return [];
|
|
3767
5002
|
const probe = await skills.listSupportFiles(name);
|
|
@@ -3795,6 +5030,17 @@ function forcedDecisionFor(a, b) {
|
|
|
3795
5030
|
rationale: "forced pair merge of " + sorted.join(" and ")
|
|
3796
5031
|
};
|
|
3797
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
|
+
}
|
|
3798
5044
|
function createMergeRuntime(deps) {
|
|
3799
5045
|
const readOnly = createReadOnlyMergeService(deps.io, deps.home);
|
|
3800
5046
|
const timeout = deps.timeoutMinutes > 0 ? deps.timeoutMinutes : void 0;
|
|
@@ -3845,7 +5091,12 @@ function createMergeRuntime(deps) {
|
|
|
3845
5091
|
b: command.b
|
|
3846
5092
|
}];
|
|
3847
5093
|
const existing = (await loadMergeState(deps.io, deps.home)).state.rows.find((row) => row.pairKey === pairKey(command.a, command.b));
|
|
3848
|
-
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
|
+
}
|
|
3849
5100
|
if (existing?.state === "pending") return {
|
|
3850
5101
|
ok: false,
|
|
3851
5102
|
text: "该 pair 已在待批准队列,先 approve/reject/discard: " + existing.id
|
|
@@ -3875,6 +5126,8 @@ function createMergeRuntime(deps) {
|
|
|
3875
5126
|
approval,
|
|
3876
5127
|
commitPort: skillLibraryPort(deps.skills),
|
|
3877
5128
|
now: deps.now,
|
|
5129
|
+
...unitFor(deps) === void 0 ? {} : { unit: unitFor(deps) },
|
|
5130
|
+
...deps.isBlocked === void 0 ? {} : { isBlocked: deps.isBlocked },
|
|
3878
5131
|
...stageHooks
|
|
3879
5132
|
});
|
|
3880
5133
|
return forced === void 0 ? ports : {
|
|
@@ -3940,6 +5193,8 @@ function createMergeRuntime(deps) {
|
|
|
3940
5193
|
approval: deps.params.mergeApproval,
|
|
3941
5194
|
commitPort: skillLibraryPort(deps.skills),
|
|
3942
5195
|
now: deps.now,
|
|
5196
|
+
...unitFor(deps) === void 0 ? {} : { unit: unitFor(deps) },
|
|
5197
|
+
...deps.isBlocked === void 0 ? {} : { isBlocked: deps.isBlocked },
|
|
3943
5198
|
...stageHooks
|
|
3944
5199
|
})
|
|
3945
5200
|
});
|
|
@@ -3966,15 +5221,18 @@ function createMergeRuntime(deps) {
|
|
|
3966
5221
|
ok: false,
|
|
3967
5222
|
text: "merge row not found: " + command.id
|
|
3968
5223
|
};
|
|
3969
|
-
|
|
3970
|
-
|
|
3971
|
-
|
|
3972
|
-
|
|
3973
|
-
|
|
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 {
|
|
3974
5233
|
ok: false,
|
|
3975
|
-
text: "merge row is not pending: " + command.id
|
|
5234
|
+
text: "merge row is not pending or changed: " + command.id
|
|
3976
5235
|
};
|
|
3977
|
-
await upsertRow(deps.io, deps.home, deps.now, next.row);
|
|
3978
5236
|
return {
|
|
3979
5237
|
ok: true,
|
|
3980
5238
|
text: "已拒绝合并:" + command.id
|
|
@@ -3986,13 +5244,10 @@ function createMergeRuntime(deps) {
|
|
|
3986
5244
|
};
|
|
3987
5245
|
if (command.verb === "report" || command.verb === "runs" || command.verb === "cancel") {
|
|
3988
5246
|
const registry = deps.registry;
|
|
3989
|
-
if (command.verb === "runs") {
|
|
3990
|
-
|
|
3991
|
-
|
|
3992
|
-
|
|
3993
|
-
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")
|
|
3994
|
-
};
|
|
3995
|
-
}
|
|
5247
|
+
if (command.verb === "runs") return {
|
|
5248
|
+
ok: true,
|
|
5249
|
+
text: formatRunsView(toRunsView(registry.runs("merge"), deps.now()))
|
|
5250
|
+
};
|
|
3996
5251
|
if (command.verb === "cancel") {
|
|
3997
5252
|
if (command.id !== void 0) {
|
|
3998
5253
|
const cancelled = registry.cancel(command.id);
|
|
@@ -4039,7 +5294,10 @@ function createMergeRuntime(deps) {
|
|
|
4039
5294
|
ok: false,
|
|
4040
5295
|
text: "merge row is not discardable: " + command.id
|
|
4041
5296
|
};
|
|
4042
|
-
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
|
+
};
|
|
4043
5301
|
return {
|
|
4044
5302
|
ok: true,
|
|
4045
5303
|
text: "已丢弃合并行:" + command.id
|
|
@@ -4073,7 +5331,10 @@ function createMergeRuntime(deps) {
|
|
|
4073
5331
|
ok: false,
|
|
4074
5332
|
text: "skipped row not found: " + command.restore
|
|
4075
5333
|
};
|
|
4076
|
-
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
|
+
};
|
|
4077
5338
|
return {
|
|
4078
5339
|
ok: true,
|
|
4079
5340
|
text: "已恢复参与筛选:" + row.id
|
|
@@ -4086,7 +5347,10 @@ function createMergeRuntime(deps) {
|
|
|
4086
5347
|
ok: false,
|
|
4087
5348
|
text: "merge row not found: " + command.clear
|
|
4088
5349
|
};
|
|
4089
|
-
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
|
+
};
|
|
4090
5354
|
return {
|
|
4091
5355
|
ok: true,
|
|
4092
5356
|
text: "已清除合并行:" + cleared.id
|
|
@@ -4106,6 +5370,21 @@ function createMergeRuntime(deps) {
|
|
|
4106
5370
|
};
|
|
4107
5371
|
const signal = new AbortController().signal;
|
|
4108
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
|
+
}
|
|
4109
5388
|
const pair = () => Promise.resolve([{
|
|
4110
5389
|
a: row.a,
|
|
4111
5390
|
b: row.b
|
|
@@ -4129,6 +5408,8 @@ function createMergeRuntime(deps) {
|
|
|
4129
5408
|
...common,
|
|
4130
5409
|
rowId: row.id,
|
|
4131
5410
|
ports: basePorts,
|
|
5411
|
+
...detached ? { detached: true } : {},
|
|
5412
|
+
pairTimeoutMinutes: deps.params.mergePairTimeoutMinutes,
|
|
4132
5413
|
...deps.isOwnerActive === void 0 ? {} : { isOwnerActive: deps.isOwnerActive },
|
|
4133
5414
|
makePorts: (runId) => createChannelScanPorts({
|
|
4134
5415
|
io: deps.io,
|
|
@@ -4143,7 +5424,9 @@ function createMergeRuntime(deps) {
|
|
|
4143
5424
|
timeoutMs: deps.params.mergePairTimeoutMinutes * 6e4,
|
|
4144
5425
|
approval: deps.params.mergeApproval,
|
|
4145
5426
|
commitPort: skillLibraryPort(deps.skills),
|
|
4146
|
-
now: deps.now
|
|
5427
|
+
now: deps.now,
|
|
5428
|
+
...unitFor(deps) === void 0 ? {} : { unit: unitFor(deps) },
|
|
5429
|
+
...deps.isBlocked === void 0 ? {} : { isBlocked: deps.isBlocked }
|
|
4147
5430
|
}),
|
|
4148
5431
|
round2WithRun: createNewChildRound2({
|
|
4149
5432
|
io: deps.io,
|
|
@@ -4156,6 +5439,8 @@ function createMergeRuntime(deps) {
|
|
|
4156
5439
|
timeoutMs: deps.params.mergePairTimeoutMinutes * 6e4,
|
|
4157
5440
|
commitPort: skillLibraryPort(deps.skills),
|
|
4158
5441
|
now: deps.now,
|
|
5442
|
+
...unitFor(deps) === void 0 ? {} : { unit: unitFor(deps) },
|
|
5443
|
+
...deps.isBlocked === void 0 ? {} : { isBlocked: deps.isBlocked },
|
|
4159
5444
|
...stageHooks
|
|
4160
5445
|
})
|
|
4161
5446
|
});
|
|
@@ -4179,11 +5464,22 @@ function createMergeRuntime(deps) {
|
|
|
4179
5464
|
if (command.verb === "approve") {
|
|
4180
5465
|
const signal = new AbortController().signal;
|
|
4181
5466
|
const cards = await deps.cards(signal);
|
|
4182
|
-
|
|
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
|
+
}
|
|
4183
5476
|
if (candidate !== void 0 && candidate.state === "pending" && candidate.decision !== void 0) {
|
|
4184
5477
|
const missingSource = [candidate.a, candidate.b].find((name) => !cards.some((card) => card.name === name && card.managed === true));
|
|
4185
5478
|
if (missingSource !== void 0) {
|
|
4186
|
-
await
|
|
5479
|
+
if (!await dropRowV3(deps.io, deps.home, candidate.id, "superseded")) return {
|
|
5480
|
+
ok: false,
|
|
5481
|
+
text: "source-gone 但 row 已变化,未清理:" + candidate.id
|
|
5482
|
+
};
|
|
4187
5483
|
return {
|
|
4188
5484
|
ok: false,
|
|
4189
5485
|
text: "source-gone:" + missingSource + " 已不在 family-managed 技能库,已从 active 队列清除;可 merge skipped --discard 清理"
|
|
@@ -4191,11 +5487,16 @@ function createMergeRuntime(deps) {
|
|
|
4191
5487
|
}
|
|
4192
5488
|
const currentRevision = sourceRevisionOf(cards, candidate.a, candidate.b);
|
|
4193
5489
|
if (candidate.sourceRevision !== void 0 && currentRevision !== void 0 && currentRevision !== candidate.sourceRevision) {
|
|
4194
|
-
|
|
4195
|
-
|
|
4196
|
-
|
|
4197
|
-
|
|
4198
|
-
|
|
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
|
+
};
|
|
4199
5500
|
return {
|
|
4200
5501
|
ok: false,
|
|
4201
5502
|
text: "source-changed:源技能内容在决策后变化,拒绝照跑;出口:重新 merge pair,或 merge skipped --discard"
|
|
@@ -4203,22 +5504,47 @@ function createMergeRuntime(deps) {
|
|
|
4203
5504
|
}
|
|
4204
5505
|
const currentTargetRevision = targetRevisionOf(cards, candidate.decision.target);
|
|
4205
5506
|
if (candidate.targetRevision !== void 0 && currentTargetRevision !== void 0 && currentTargetRevision !== candidate.targetRevision) {
|
|
4206
|
-
|
|
4207
|
-
|
|
4208
|
-
|
|
4209
|
-
|
|
4210
|
-
|
|
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
|
+
};
|
|
4211
5517
|
return {
|
|
4212
5518
|
ok: false,
|
|
4213
5519
|
text: "target-taken:目标 " + candidate.decision.target + " 在决策后变化,拒绝照跑;可重新 merge pair 或 discard"
|
|
4214
5520
|
};
|
|
4215
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
|
+
}
|
|
4216
5537
|
if (candidate.decision.mode === "merge-into-existing" && !cards.some((card) => card.name === candidate.decision?.target)) {
|
|
4217
|
-
|
|
4218
|
-
|
|
4219
|
-
|
|
4220
|
-
|
|
4221
|
-
|
|
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
|
+
};
|
|
4222
5548
|
return {
|
|
4223
5549
|
ok: false,
|
|
4224
5550
|
text: "target-taken:目标 " + candidate.decision.target + " 已不存在,行保留 pending --expired;出口:重新 merge pair,或 merge skipped --discard"
|
|
@@ -4228,6 +5554,8 @@ function createMergeRuntime(deps) {
|
|
|
4228
5554
|
const result = await runApproveCommand({
|
|
4229
5555
|
...common,
|
|
4230
5556
|
rowId: command.id,
|
|
5557
|
+
...detached ? { detached: true } : {},
|
|
5558
|
+
pairTimeoutMinutes: deps.params.mergePairTimeoutMinutes,
|
|
4231
5559
|
ports: { round2: createNewChildRound2({
|
|
4232
5560
|
io: deps.io,
|
|
4233
5561
|
home: deps.home,
|
|
@@ -4238,7 +5566,9 @@ function createMergeRuntime(deps) {
|
|
|
4238
5566
|
...deps.cwd === void 0 ? {} : { cwd: deps.cwd },
|
|
4239
5567
|
timeoutMs: deps.params.mergePairTimeoutMinutes * 6e4,
|
|
4240
5568
|
commitPort: skillLibraryPort(deps.skills),
|
|
4241
|
-
now: deps.now
|
|
5569
|
+
now: deps.now,
|
|
5570
|
+
...unitFor(deps) === void 0 ? {} : { unit: unitFor(deps) },
|
|
5571
|
+
...deps.isBlocked === void 0 ? {} : { isBlocked: deps.isBlocked }
|
|
4242
5572
|
}) }
|
|
4243
5573
|
});
|
|
4244
5574
|
if (!result.ok) return {
|
|
@@ -4259,7 +5589,9 @@ function createMergeRuntime(deps) {
|
|
|
4259
5589
|
};
|
|
4260
5590
|
}
|
|
4261
5591
|
const cards = command.verb === "pending" ? await deps.cards(new AbortController().signal) : void 0;
|
|
4262
|
-
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);
|
|
4263
5595
|
if (text !== null) return {
|
|
4264
5596
|
ok: true,
|
|
4265
5597
|
text
|
|
@@ -4638,6 +5970,143 @@ async function supersedeMergeRuns(registry) {
|
|
|
4638
5970
|
return ids.length;
|
|
4639
5971
|
}
|
|
4640
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
|
|
4641
6110
|
//#region lib/types/pair-command.js
|
|
4642
6111
|
async function runPairCommand(options) {
|
|
4643
6112
|
return await runScanCommand({
|
|
@@ -4667,14 +6136,10 @@ const Config = z.object({
|
|
|
4667
6136
|
mergePairTimeoutMinutes: z.number().min(1).default(15),
|
|
4668
6137
|
mergeModelsRoot: z.string().default("")
|
|
4669
6138
|
});
|
|
4670
|
-
function
|
|
4671
|
-
const root = configuredRoot
|
|
4672
|
-
if (root ===
|
|
6139
|
+
function engineConfigFromRoot(configuredRoot) {
|
|
6140
|
+
const root = configuredRoot?.trim() ?? "";
|
|
6141
|
+
if (root === "") return void 0;
|
|
4673
6142
|
return {
|
|
4674
|
-
models_dir: "",
|
|
4675
|
-
offline: true,
|
|
4676
|
-
auto_provision: false,
|
|
4677
|
-
embed_backend: "local",
|
|
4678
6143
|
embedder_model: join(root, "potion-multilingual-128M-int8"),
|
|
4679
6144
|
embedder_model_last: join(root, "potion-multilingual-128M-pca64-int8")
|
|
4680
6145
|
};
|
|
@@ -4718,7 +6183,6 @@ function apply(ctx, rawConfig = {}) {
|
|
|
4718
6183
|
ctx.logger.warn(message);
|
|
4719
6184
|
}
|
|
4720
6185
|
});
|
|
4721
|
-
runs.load();
|
|
4722
6186
|
ctx.effect(() => {
|
|
4723
6187
|
const host = ctx;
|
|
4724
6188
|
if (typeof host.on !== "function") return () => {};
|
|
@@ -4736,20 +6200,17 @@ function apply(ctx, rawConfig = {}) {
|
|
|
4736
6200
|
...policyStageLimits(policySnapshot?.get?.())
|
|
4737
6201
|
}
|
|
4738
6202
|
});
|
|
4739
|
-
|
|
4740
|
-
recoverCommitJournals(io, home, Date.now(), mergeSkillsRoot, async (name) => {
|
|
4741
|
-
return (await skills.restoreFromArchive(name)).ok;
|
|
4742
|
-
}, (runId) => {
|
|
6203
|
+
runs.load().then(() => recoverCommitJournals(io, home, Date.now(), (runId) => {
|
|
4743
6204
|
const run = runs.find(runId);
|
|
4744
6205
|
return run !== void 0 && run.state === "running";
|
|
4745
|
-
}).then((reports) => {
|
|
6206
|
+
})).then((reports) => {
|
|
4746
6207
|
if (reports.length > 0) ctx.logger.info("merge commit recovery: " + reports.map((report) => report.runId + "=" + report.action).join(", "));
|
|
4747
6208
|
}).catch((error) => {
|
|
4748
6209
|
ctx.logger.warn("merge commit recovery failed: " + (error instanceof Error ? error.message : String(error)));
|
|
4749
6210
|
});
|
|
4750
6211
|
const channel = subagentChannelAdapter(createSubagentChannel(ctx));
|
|
4751
6212
|
const stage = new StageToolRegistry();
|
|
4752
|
-
const engineConfig =
|
|
6213
|
+
const engineConfig = engineConfigFromRoot(params.mergeModelsRoot);
|
|
4753
6214
|
const llm = ctx.get("llm");
|
|
4754
6215
|
const defaultModel = ctx.get("agentDefaultModel");
|
|
4755
6216
|
const runner = llm === void 0 || defaultModel === void 0 ? void 0 : createEngineRunner(llm, () => defaultModel.currentSelection());
|
|
@@ -4759,7 +6220,7 @@ function apply(ctx, rawConfig = {}) {
|
|
|
4759
6220
|
engine: { screen },
|
|
4760
6221
|
select: selectWithErrors,
|
|
4761
6222
|
cfg: {
|
|
4762
|
-
...engineConfig
|
|
6223
|
+
...engineConfig,
|
|
4763
6224
|
judge: "deepseek"
|
|
4764
6225
|
},
|
|
4765
6226
|
...runner === void 0 ? {} : { runner },
|
|
@@ -4768,14 +6229,20 @@ function apply(ctx, rawConfig = {}) {
|
|
|
4768
6229
|
const at = (/* @__PURE__ */ new Date()).toISOString();
|
|
4769
6230
|
for (const skipped of selection.skipped) {
|
|
4770
6231
|
if (loaded.state.rows.some((row) => row.pairKey === skipped.pairKey)) continue;
|
|
4771
|
-
const
|
|
4772
|
-
|
|
6232
|
+
const key = pairKey(skipped.a, skipped.b);
|
|
6233
|
+
await transactRows(io, home, (tx) => tx.createSkipped({
|
|
6234
|
+
id: rowIdOf(key),
|
|
6235
|
+
pairKey: key,
|
|
4773
6236
|
a: skipped.a,
|
|
4774
6237
|
b: skipped.b,
|
|
4775
|
-
|
|
4776
|
-
at
|
|
4777
|
-
|
|
4778
|
-
|
|
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);
|
|
4779
6246
|
}
|
|
4780
6247
|
}
|
|
4781
6248
|
});
|
|
@@ -4906,4 +6373,4 @@ function apply(ctx, rawConfig = {}) {
|
|
|
4906
6373
|
});
|
|
4907
6374
|
}
|
|
4908
6375
|
//#endregion
|
|
4909
|
-
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 };
|