@lmzhen/dsh-evolution-merge 0.30.18 → 0.30.20

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (143) hide show
  1. package/README.md +21 -4
  2. package/data/minhash_params.json +1063 -0
  3. package/lib/index.js +7313 -3272
  4. package/lib/types/adapters/channel.d.ts +1 -0
  5. package/lib/types/adapters/channel.js +15 -1
  6. package/lib/types/adapters/engine-screen.d.ts +1 -1
  7. package/lib/types/adapters/engine-screen.js +5 -2
  8. package/lib/types/adapters/model-runner.d.ts +0 -5
  9. package/lib/types/adapters/model-runner.js +8 -2
  10. package/lib/types/adapters/model.js +2 -1
  11. package/lib/types/adapters/select.js +12 -8
  12. package/lib/types/approve-command.d.ts +12 -1
  13. package/lib/types/approve-command.js +39 -6
  14. package/lib/types/authoring-flow.d.ts +16 -0
  15. package/lib/types/authoring-flow.js +37 -13
  16. package/lib/types/channel-port.d.ts +3 -1
  17. package/lib/types/channel-port.js +8 -3
  18. package/lib/types/command-defs.d.ts +5 -0
  19. package/lib/types/command-defs.js +33 -0
  20. package/lib/types/command-surface.d.ts +1 -1
  21. package/lib/types/command-surface.js +10 -43
  22. package/lib/types/commands.d.ts +3 -4
  23. package/lib/types/commands.js +33 -72
  24. package/lib/types/commit-assembly.d.ts +14 -1
  25. package/lib/types/commit-assembly.js +65 -9
  26. package/lib/types/commit-execute.d.ts +7 -1
  27. package/lib/types/commit-execute.js +54 -22
  28. package/lib/types/commit-plan.d.ts +7 -1
  29. package/lib/types/commit-plan.js +4 -0
  30. package/lib/types/commit-receipt.d.ts +3 -0
  31. package/lib/types/contracts.d.ts +43 -0
  32. package/lib/types/doctor.d.ts +11 -1
  33. package/lib/types/doctor.js +81 -27
  34. package/lib/types/domain/authoring-manifest.d.ts +26 -0
  35. package/lib/types/domain/authoring-manifest.js +43 -0
  36. package/lib/types/domain/engine-criteria.d.ts +10 -0
  37. package/lib/types/domain/engine-criteria.js +11 -0
  38. package/lib/types/domain/engine-input.d.ts +10 -0
  39. package/lib/types/domain/engine-input.js +26 -0
  40. package/lib/types/domain/errors.d.ts +15 -1
  41. package/lib/types/domain/errors.js +23 -4
  42. package/lib/types/domain/evidence-hash.d.ts +3 -0
  43. package/lib/types/domain/evidence-hash.js +14 -0
  44. package/lib/types/domain/failure-route.d.ts +14 -0
  45. package/lib/types/domain/failure-route.js +12 -0
  46. package/lib/types/domain/live-facts.d.ts +31 -0
  47. package/lib/types/domain/live-facts.js +54 -0
  48. package/lib/types/domain/merge-limits.d.ts +18 -0
  49. package/lib/types/domain/merge-limits.js +22 -0
  50. package/lib/types/domain/merge-report.d.ts +41 -0
  51. package/lib/types/domain/merge-report.js +2 -0
  52. package/lib/types/domain/merge-snapshot.d.ts +99 -0
  53. package/lib/types/domain/merge-snapshot.js +82 -0
  54. package/lib/types/domain/pair-ledger.d.ts +34 -0
  55. package/lib/types/domain/pair-ledger.js +96 -0
  56. package/lib/types/domain/pre-1b-backup.d.ts +43 -0
  57. package/lib/types/domain/pre-1b-backup.js +140 -0
  58. package/lib/types/domain/provider-key.d.ts +2 -0
  59. package/lib/types/domain/provider-key.js +11 -0
  60. package/lib/types/domain/read-probe.d.ts +5 -0
  61. package/lib/types/domain/read-probe.js +11 -0
  62. package/lib/types/domain/reconcile.d.ts +47 -0
  63. package/lib/types/domain/reconcile.js +178 -0
  64. package/lib/types/domain/resolve-pair.d.ts +2 -9
  65. package/lib/types/domain/resolve-pair.js +2 -15
  66. package/lib/types/domain/row-v4.d.ts +148 -0
  67. package/lib/types/domain/row-v4.js +159 -0
  68. package/lib/types/domain/run-coordinator.d.ts +51 -0
  69. package/lib/types/domain/run-coordinator.js +45 -0
  70. package/lib/types/domain/schema4.d.ts +53 -0
  71. package/lib/types/domain/schema4.js +129 -0
  72. package/lib/types/domain/selection-policy.d.ts +14 -0
  73. package/lib/types/domain/selection-policy.js +6 -0
  74. package/lib/types/domain/skipped-expiry.d.ts +5 -0
  75. package/lib/types/domain/skipped-expiry.js +16 -0
  76. package/lib/types/domain/stage-aggregate.d.ts +96 -0
  77. package/lib/types/domain/stage-aggregate.js +293 -0
  78. package/lib/types/domain/staging-index.d.ts +1 -0
  79. package/lib/types/domain/state-bootstrap.d.ts +43 -0
  80. package/lib/types/domain/state-bootstrap.js +267 -0
  81. package/lib/types/domain/state-v3.d.ts +3 -14
  82. package/lib/types/domain/state-v3.js +3 -79
  83. package/lib/types/domain/undo-journal.d.ts +19 -0
  84. package/lib/types/domain/undo-journal.js +20 -0
  85. package/lib/types/domain/undo-receipt.d.ts +18 -0
  86. package/lib/types/domain/undo-receipt.js +22 -0
  87. package/lib/types/domain/validation-plan.d.ts +65 -0
  88. package/lib/types/domain/validation-plan.js +49 -0
  89. package/lib/types/facts.d.ts +3 -1
  90. package/lib/types/facts.js +13 -16
  91. package/lib/types/index.d.ts +2 -0
  92. package/lib/types/index.js +181 -68
  93. package/lib/types/merge-command.d.ts +3 -1
  94. package/lib/types/merge-command.js +10 -6
  95. package/lib/types/merge-routes.d.ts +10 -5
  96. package/lib/types/merge-routes.js +72 -7
  97. package/lib/types/merge-tool.d.ts +1 -1
  98. package/lib/types/merge-tool.js +2 -2
  99. package/lib/types/metrics.js +1 -1
  100. package/lib/types/new-child-authoring.d.ts +2 -0
  101. package/lib/types/new-child-authoring.js +57 -10
  102. package/lib/types/params.d.ts +2 -4
  103. package/lib/types/params.js +13 -0
  104. package/lib/types/pending-actions.d.ts +2 -2
  105. package/lib/types/pending-actions.js +4 -4
  106. package/lib/types/prompts.d.ts +2 -4
  107. package/lib/types/prompts.js +21 -8
  108. package/lib/types/read-commands.d.ts +3 -3
  109. package/lib/types/read-commands.js +19 -28
  110. package/lib/types/recovery-coordinator.d.ts +34 -0
  111. package/lib/types/recovery-coordinator.js +84 -0
  112. package/lib/types/recovery-projection.d.ts +31 -0
  113. package/lib/types/recovery-projection.js +96 -0
  114. package/lib/types/resume-command.d.ts +5 -1
  115. package/lib/types/resume-command.js +46 -13
  116. package/lib/types/revision.d.ts +17 -0
  117. package/lib/types/revision.js +39 -3
  118. package/lib/types/round2-outcome.d.ts +2 -1
  119. package/lib/types/round2-outcome.js +59 -11
  120. package/lib/types/run-shell.d.ts +16 -2
  121. package/lib/types/run-shell.js +49 -5
  122. package/lib/types/runtime.d.ts +0 -5
  123. package/lib/types/runtime.js +274 -89
  124. package/lib/types/scan-command.d.ts +1 -0
  125. package/lib/types/scan-command.js +2 -0
  126. package/lib/types/scan-ports.d.ts +18 -0
  127. package/lib/types/scan-ports.js +49 -23
  128. package/lib/types/scan-run.d.ts +17 -0
  129. package/lib/types/scan-run.js +143 -48
  130. package/lib/types/stage-tool-registry.d.ts +7 -0
  131. package/lib/types/stage-tool-registry.js +12 -1
  132. package/lib/types/state.d.ts +5 -5
  133. package/lib/types/state.js +22 -69
  134. package/lib/types/status.d.ts +6 -1
  135. package/lib/types/status.js +47 -0
  136. package/lib/types/surface/body.d.ts +4 -0
  137. package/lib/types/surface/body.js +16 -0
  138. package/lib/types/surface/merge-view.d.ts +14 -0
  139. package/lib/types/surface/merge-view.js +126 -0
  140. package/lib/types/undo-operation.d.ts +24 -0
  141. package/lib/types/undo-operation.js +135 -0
  142. package/lib/types/wire.d.ts +22 -3
  143. package/package.json +6 -4
@@ -19,6 +19,7 @@ export interface RunScanCommandOptions {
19
19
  readonly detached?: boolean;
20
20
  readonly notes?: readonly string[];
21
21
  readonly isOwnerActive?: (runId: string) => boolean | Promise<boolean>;
22
+ readonly cards?: readonly import('./skill-card.ts').SkillCard[];
22
23
  }
23
24
  export declare function runScanCommand(options: RunScanCommandOptions): Promise<WithMergeRunResult>;
24
25
  //# sourceMappingURL=scan-command.d.ts.map
@@ -9,6 +9,7 @@ export async function runScanCommand(options) {
9
9
  approval: { platformWrite: options.platformWrite, mergeApproval: options.approval },
10
10
  now: options.now,
11
11
  ...(options.detached === undefined ? {} : { detached: options.detached }),
12
+ ...(options.timeoutMinutes === undefined ? {} : { timeoutMinutes: options.timeoutMinutes }),
12
13
  async body(ctx) {
13
14
  const ports = options.makePorts === undefined ? options.ports : options.makePorts(ctx.runId);
14
15
  if (ports === undefined)
@@ -26,6 +27,7 @@ export async function runScanCommand(options) {
26
27
  ...(options.timeoutMinutes === undefined ? {} : { timeoutMinutes: options.timeoutMinutes }),
27
28
  ...(options.notes === undefined ? {} : { notes: options.notes }),
28
29
  ...(options.isOwnerActive === undefined ? {} : { isOwnerActive: options.isOwnerActive }),
30
+ ...(options.cards === undefined ? {} : { cards: options.cards }),
29
31
  });
30
32
  },
31
33
  });
@@ -6,10 +6,12 @@
6
6
  * approve run must open a new child (not handled by this factory).
7
7
  */
8
8
  import type { EvolutionIoLike, UnitTransaction } from '@lmzhen/dsh-evolution-core';
9
+ import type { MergeDecision, MergeRow } from './contracts.ts';
9
10
  import type { SkillCard } from './skill-card.ts';
10
11
  import { type ConversationChannelLike } from './channel-port.ts';
11
12
  import type { CommitPort } from './commit-execute.ts';
12
13
  import type { StageToolContext } from './stage-tool.ts';
14
+ import { type StageEntry } from './domain/stage-aggregate.ts';
13
15
  import type { ScanPorts } from './scan-run.ts';
14
16
  export interface CreateScanPortsOptions {
15
17
  readonly io: EvolutionIoLike;
@@ -17,6 +19,7 @@ export interface CreateScanPortsOptions {
17
19
  readonly runId: string;
18
20
  readonly cards: readonly SkillCard[];
19
21
  readonly screening: ScanPorts['screening'];
22
+ readonly resolve?: ScanPorts['resolve'];
20
23
  readonly channel: ConversationChannelLike;
21
24
  readonly parent: unknown;
22
25
  readonly allow: readonly string[];
@@ -25,10 +28,25 @@ export interface CreateScanPortsOptions {
25
28
  readonly approval: boolean;
26
29
  readonly commitPort: CommitPort;
27
30
  readonly unit?: UnitTransaction | undefined;
31
+ readonly limits?: import('@lmzhen/dsh-evolution-core').SkillLimits | undefined;
28
32
  readonly now: () => number;
29
33
  readonly isBlocked?: (name: string) => boolean | Promise<boolean>;
30
34
  readonly onChild?: (context: StageToolContext) => void;
31
35
  readonly onChildClosed?: (childId: string) => void;
32
36
  }
37
+ export interface StageForCardsOptions {
38
+ readonly io: EvolutionIoLike;
39
+ readonly home: string;
40
+ readonly runId: string;
41
+ readonly cards: readonly SkillCard[];
42
+ readonly now: () => number;
43
+ }
44
+ export declare function createStageForCards(options: StageForCardsOptions): (row: MergeRow, decision: MergeDecision) => Promise<{
45
+ readonly ok: true;
46
+ readonly entry: StageEntry;
47
+ } | {
48
+ readonly ok: false;
49
+ readonly message: string;
50
+ }>;
33
51
  export declare function createChannelScanPorts(options: CreateScanPortsOptions): ScanPorts;
34
52
  //# sourceMappingURL=scan-ports.d.ts.map
@@ -1,39 +1,49 @@
1
1
  import { pairKey } from "./contracts.js";
2
+ function classifyCommitFailure(message) {
3
+ if (message.includes('目标技能') && (message.includes('被改过') || message.includes('不存在')))
4
+ return message.includes('被改过') ? 'target-changed' : 'target-taken';
5
+ if (message.includes('来源技能') && message.includes('被改过'))
6
+ return 'source-changed';
7
+ if (message.includes('来源技能') && message.includes('不存在'))
8
+ return 'source-gone';
9
+ return 'authoring-failed';
10
+ }
2
11
  import { runRound1OverChannel } from "./channel-port.js";
3
12
  import { runAuthoring } from "./authoring-flow.js";
4
- import { dropStaging, ensureStaging, stageKeyOf, stagingRoot } from "./staging.js";
13
+ import { buildRound2Prompt, MERGE_PROMPT_VERSION, promptHashOf } from "./prompts.js";
14
+ import { stageKeyOf } from "./staging.js";
15
+ import { createStageTree, reserveStage } from "./domain/stage-aggregate.js";
5
16
  import { sourceRevisionOf, targetRevisionOf } from "./revision.js";
17
+ export function createStageForCards(options) {
18
+ return async (row, decision) => {
19
+ const stageKey = stageKeyOf(options.runId, pairKey(row.a, row.b));
20
+ const entry = { version: 1, rowId: row.id, pairKey: row.pairKey, runId: options.runId, ref: stageKey, state: 'reserved', createdAt: options.now(), expiresAt: options.now() + 7 * 86_400_000, updatedAt: options.now() };
21
+ await reserveStage(options.io, options.home, entry);
22
+ const staged = await createStageTree(options.io, options.home, {
23
+ runId: stageKey, target: decision.target,
24
+ sources: options.cards.filter(card => card.name === row.a || card.name === row.b).map(card => ({ name: card.name, body: card.body, files: card.files ?? [] })),
25
+ promptVersion: 'merge-b/v1', ttlDays: 7, now: options.now,
26
+ });
27
+ return staged.ok ? { ok: true, entry } : { ok: false, message: staged.message };
28
+ };
29
+ }
6
30
  export function createChannelScanPorts(options) {
7
31
  const sessions = new Map();
8
32
  return {
9
33
  screening: options.screening,
34
+ ...(options.resolve === undefined ? {} : { resolve: options.resolve }),
10
35
  async round1(candidate, signal) {
11
36
  const stageKey = stageKeyOf(options.runId, pairKey(candidate.a, candidate.b));
12
- const staged = await ensureStaging(options.io, options.home, {
13
- runId: stageKey,
14
- target: candidate.a + '-' + candidate.b,
15
- sources: options.cards
16
- .filter(card => card.name === candidate.a || card.name === candidate.b)
17
- .map(card => ({ name: card.name, body: card.body, files: card.files ?? [] })),
18
- promptVersion: 'merge-b/v1',
19
- ttlDays: 7,
20
- now: options.now,
21
- });
22
- if (!staged.ok)
23
- return { kind: 'failed', failure: { stage: 'decide', code: 'staging-create', message: staged.message, retryable: true, retryFrom: 'agent1' } };
24
37
  const result = await runRound1OverChannel({
25
38
  channel: options.channel,
26
39
  parent: options.parent,
27
40
  cards: options.cards,
28
41
  allow: options.allow,
29
- cwd: stagingRoot(options.home, stageKey),
42
+ cwd: options.cwd ?? options.home,
30
43
  timeoutMs: options.timeoutMs,
31
44
  onChildOpened: childId => options.onChild?.({ runId: stageKey, childId, expectedChildId: childId, phase: 'decide' }),
32
45
  }, candidate, signal);
33
- if (result.outcome.kind !== 'merge') {
34
- await dropStaging(options.io, options.home, stageKey);
35
- }
36
- else {
46
+ if (result.outcome.kind === 'merge') {
37
47
  if (options.approval)
38
48
  await result.dispose();
39
49
  else {
@@ -43,6 +53,7 @@ export function createChannelScanPorts(options) {
43
53
  }
44
54
  }
45
55
  }
56
+ const round1Usage = result.usage?.();
46
57
  const sourceRevision = result.outcome.kind === 'merge' ? sourceRevisionOf(options.cards, candidate.a, candidate.b) : undefined;
47
58
  const target = result.outcome.kind === 'merge' ? result.outcome.decision?.target : undefined;
48
59
  const targetRevision = target === undefined ? undefined : targetRevisionOf(options.cards, target);
@@ -51,9 +62,13 @@ export function createChannelScanPorts(options) {
51
62
  ...result.outcome, stagingRef: stageKey, stagingExpiresAt: options.now() + 7 * 86_400_000,
52
63
  ...(sourceRevision === undefined ? {} : { sourceRevision }),
53
64
  ...(targetRevision === undefined ? {} : { targetRevision }),
65
+ ...(round1Usage === undefined ? {} : { usage: round1Usage }),
54
66
  }
55
- : result.outcome;
67
+ : result.outcome.kind === 'failed'
68
+ ? { ...result.outcome, degraded: ['round1 child failed: ' + (result.outcome.failure?.code ?? 'unknown')], ...(round1Usage === undefined ? {} : { usage: round1Usage }) }
69
+ : { ...result.outcome, ...(round1Usage === undefined ? {} : { usage: round1Usage }) };
56
70
  },
71
+ stage: createStageForCards({ io: options.io, home: options.home, runId: options.runId, cards: options.cards, now: options.now }),
57
72
  async round2(row, decision, signal) {
58
73
  const key = pairKey(row.a, row.b);
59
74
  const entry = sessions.get(key);
@@ -76,18 +91,29 @@ export function createChannelScanPorts(options) {
76
91
  now: options.now,
77
92
  ...(options.isBlocked === undefined ? {} : { isBlocked: options.isBlocked }),
78
93
  summary: 'merge ' + row.a + ' + ' + row.b,
79
- author: async () => {
80
- await entry.result.author?.(row, decision, signal);
81
- return 'authored';
94
+ ...(options.limits === undefined ? {} : { limits: options.limits }),
95
+ promptVersion: MERGE_PROMPT_VERSION,
96
+ promptHash: promptHashOf(buildRound2Prompt(row, decision)),
97
+ revisions: {
98
+ ...(decision.targetRevisionRef === undefined ? {} : { targetRevision: decision.targetRevisionRef }),
99
+ ...(decision.sourceRevisions === undefined ? {} : { sourceRevisions: decision.sourceRevisions }),
100
+ ...(decision.supportDigest === undefined ? {} : { supportDigest: decision.supportDigest }),
82
101
  },
102
+ author: async () => (await entry.result.author?.(row, decision, signal)) ?? 'authored',
83
103
  });
104
+ const usage = entry.result.usage?.();
84
105
  if (!authored.ok) {
85
- return { kind: 'failed', failure: { stage: 'author', code: 'authoring-failed', message: authored.message, retryable: true, retryFrom: 'agent2' } };
106
+ const limit = authored.findings?.some(finding => finding.rule === 'limit') === true;
107
+ const code = authored.code ?? (limit ? 'merge-over-limit' : authored.findings === undefined ? classifyCommitFailure(authored.message) : 'merge-validation');
108
+ return { kind: 'failed', failure: { stage: 'author', code, message: authored.message, retryable: code !== 'target-changed' && code !== 'source-changed' && code !== 'support-changed', retryFrom: code === 'target-changed' || code === 'source-changed' || code === 'support-changed' ? 'none' : 'agent2', ...(authored.lengths === undefined ? {} : { lengths: authored.lengths }), ...(usage === undefined ? {} : { usage }) }, };
86
109
  }
87
110
  const receipt = authored.commit.receipt;
88
111
  return {
89
112
  kind: 'merged',
90
113
  ...(receipt?.unitId === undefined ? {} : { unitId: receipt.unitId }),
114
+ ...(receipt?.evidenceSha256 === undefined ? {} : { evidenceSha256: receipt.evidenceSha256 }),
115
+ ...(receipt?.validationPlan === undefined ? {} : { validationPlan: receipt.validationPlan }),
116
+ ...(usage === undefined ? {} : { usage }),
91
117
  archived: receipt?.archived ?? [],
92
118
  target: { requested: decision.target, actual: authored.commit.plan.target, created: receipt?.create ?? false },
93
119
  };
@@ -3,7 +3,9 @@
3
3
  * candidate → round1 → pending/fuse → commit transitions and returns the report draft.
4
4
  */
5
5
  import { type MergeCandidate, type MergeDecision, type MergeFailure, type MergeRow } from './contracts.ts';
6
+ import { type StageEntry } from './domain/stage-aggregate.ts';
6
7
  import type { EvolutionIoLike, RunStage } from '@lmzhen/dsh-evolution-core';
8
+ import type { SkillCard } from './skill-card.ts';
7
9
  import type { MergeRunReportDraft } from './run-shell.ts';
8
10
  export interface ScanRound1Outcome {
9
11
  readonly kind: 'keep' | 'merge' | 'failed';
@@ -13,10 +15,14 @@ export interface ScanRound1Outcome {
13
15
  readonly stagingExpiresAt?: number;
14
16
  readonly sourceRevision?: string;
15
17
  readonly targetRevision?: string;
18
+ readonly degraded?: readonly string[];
19
+ readonly usage?: import('./contracts.ts').MergeUsage;
16
20
  }
17
21
  export interface ScanRound2Outcome {
18
22
  readonly kind: 'merged' | 'failed';
19
23
  readonly unitId?: string;
24
+ readonly evidenceSha256?: string;
25
+ readonly validationPlan?: import('./contracts.ts').MergeValidationPlanRef;
20
26
  readonly archived?: readonly string[];
21
27
  readonly target?: {
22
28
  readonly requested: string;
@@ -24,10 +30,20 @@ export interface ScanRound2Outcome {
24
30
  readonly created: boolean;
25
31
  };
26
32
  readonly failure?: MergeFailure;
33
+ readonly usage?: import('./contracts.ts').MergeUsage;
34
+ readonly lengths?: Readonly<Record<string, number>>;
27
35
  }
28
36
  export interface ScanPorts {
29
37
  screening(signal: AbortSignal): Promise<readonly MergeCandidate[]>;
38
+ resolve?(candidate: MergeCandidate): Promise<boolean>;
30
39
  round1(candidate: MergeCandidate, signal: AbortSignal): Promise<ScanRound1Outcome>;
40
+ stage?(row: MergeRow, decision: MergeDecision): Promise<{
41
+ readonly ok: true;
42
+ readonly entry: StageEntry;
43
+ } | {
44
+ readonly ok: false;
45
+ readonly message: string;
46
+ }>;
31
47
  round2(row: MergeRow, decision: MergeDecision, signal: AbortSignal): Promise<ScanRound2Outcome>;
32
48
  }
33
49
  export interface RunScanOptions {
@@ -46,6 +62,7 @@ export interface RunScanOptions {
46
62
  readonly notes?: readonly string[];
47
63
  readonly runId?: string;
48
64
  readonly isOwnerActive?: (runId: string) => boolean | Promise<boolean>;
65
+ readonly cards?: readonly SkillCard[];
49
66
  }
50
67
  export declare function runScan(options: RunScanOptions): Promise<MergeRunReportDraft>;
51
68
  //# sourceMappingURL=scan-run.d.ts.map
@@ -3,10 +3,13 @@
3
3
  * candidate → round1 → pending/fuse → commit transitions and returns the report draft.
4
4
  */
5
5
  import { pairKey, rowIdOf, } from "./contracts.js";
6
- import { dropRowV3, loadMergeState, transactRows } from "./state.js";
7
- import { recordPair } from "./domain/pair-index-store.js";
8
- import { writeStagingIndex } from "./domain/staging-index.js";
9
- import { v3ToLegacy } from "./domain/row-v3-bridge.js";
6
+ import { loadMergeState } from "./state.js";
7
+ import { pairLedger } from "./domain/pair-ledger.js";
8
+ import { presentStage, dropStage } from "./domain/stage-aggregate.js";
9
+ import { recordPair } from "./domain/run-coordinator.js";
10
+ import { v4ToLegacy } from "./domain/row-v4.js";
11
+ import { criteriaFrom } from "./domain/engine-criteria.js";
12
+ import { withDecisionRevisions } from "./revision.js";
10
13
  const isAborted = (signal) => signal.aborted;
11
14
  function at(now) {
12
15
  return new Date(now()).toISOString();
@@ -46,8 +49,10 @@ export async function runScan(options) {
46
49
  const startedAt = options.startedAt ?? options.now();
47
50
  const timeoutMs = (options.timeoutMinutes ?? 0) > 0 ? options.timeoutMinutes * 60_000 : 0;
48
51
  const pairs = [];
52
+ const degradedNotes = [];
49
53
  const totals = { merged: 0, kept: 0, pending: 0, failed: 0 };
50
54
  const remaining = [];
55
+ const finalizeRows = [];
51
56
  let stopReason = 'completed';
52
57
  for (let index = 0; index < candidates.length; index += 1) {
53
58
  if (isAborted(options.signal)) {
@@ -72,7 +77,7 @@ export async function runScan(options) {
72
77
  && (await options.isOwnerActive?.(before.runId)) === true) {
73
78
  continue;
74
79
  }
75
- const status = await transactRows(options.io, options.home, tx => {
80
+ const status = await pairLedger(options.io, options.home).transact(tx => {
76
81
  const current = tx.rows().find(item => item.pairKey === row.pairKey);
77
82
  if (current !== undefined && current.state !== 'verify' && current.state !== 'skipped')
78
83
  return 'conflict:' + current.state;
@@ -104,6 +109,15 @@ export async function runScan(options) {
104
109
  });
105
110
  continue;
106
111
  }
112
+ if (options.ports.resolve !== undefined && !(await options.ports.resolve(candidate))) {
113
+ await pairLedger(options.io, options.home).transact(tx => {
114
+ const current = tx.rows().find(item => item.id === row.id);
115
+ return current === undefined ? false : tx.remove(row.id, 'superseded', undefined, current.version).ok;
116
+ });
117
+ pairs.push(pairOf(row, candidate, 'failed', { failure: { stage: 'decide', code: 'source-gone', message: 'source skill disappeared before round1', retryable: false, retryFrom: 'none' } }));
118
+ totals.failed += 1;
119
+ continue;
120
+ }
107
121
  let round1;
108
122
  try {
109
123
  round1 = await options.ports.round1(candidate, options.signal);
@@ -117,13 +131,15 @@ export async function runScan(options) {
117
131
  break;
118
132
  }
119
133
  if (round1.kind === 'failed') {
134
+ if (round1.degraded !== undefined)
135
+ degradedNotes.push(...round1.degraded);
120
136
  const failure = round1.failure ?? failureOf('decide', 'round1 failed');
121
- const projected = await transactRows(options.io, options.home, tx => {
137
+ const projected = await pairLedger(options.io, options.home).transact(tx => {
122
138
  const current = tx.rows().find(item => item.id === row.id);
123
139
  if (current === undefined)
124
140
  return null;
125
141
  const moved = tx.transition(row.id, { type: 'failed', failure }, current.version);
126
- return moved.ok && moved.row !== null ? v3ToLegacy(moved.row) : null;
142
+ return moved.ok && moved.row !== null ? v4ToLegacy(moved.row) : null;
127
143
  });
128
144
  if (projected === null) {
129
145
  pairs.push(pairOf(row, candidate, 'failed', { failure: failureOf('decide', 'row changed before failure record') }));
@@ -136,12 +152,13 @@ export async function runScan(options) {
136
152
  continue;
137
153
  }
138
154
  if (round1.kind === 'keep') {
139
- const projected = await transactRows(options.io, options.home, tx => {
155
+ const projected = await pairLedger(options.io, options.home).transact(tx => {
140
156
  const current = tx.rows().find(item => item.id === row.id);
141
157
  if (current === undefined)
142
158
  return null;
143
- const moved = tx.transition(row.id, { type: 'skipped', reason: 'kept' }, current.version);
144
- return moved.ok && moved.row !== null ? v3ToLegacy(moved.row) : null;
159
+ const keepReason = round1.decision?.rationale?.trim();
160
+ const moved = tx.transition(row.id, { type: 'skipped', ...(keepReason === undefined || keepReason === '' ? { reason: 'kept' } : { reason: keepReason }), origin: 'agent' }, current.version);
161
+ return moved.ok && moved.row !== null ? v4ToLegacy(moved.row) : null;
145
162
  });
146
163
  if (projected === null) {
147
164
  pairs.push(pairOf(row, candidate, 'failed', { failure: failureOf('decide', 'row changed before keep record') }));
@@ -150,7 +167,7 @@ export async function runScan(options) {
150
167
  }
151
168
  row = projected;
152
169
  await recordPair(options.io, options.home, { pairKey: row.pairKey, runId: row.runId ?? 'merge', result: 'kept' });
153
- pairs.push(pairOf(row, candidate, 'kept', round1.decision === undefined ? {} : { decision: round1.decision }));
170
+ pairs.push(pairOf(row, candidate, 'kept', { ...(round1.decision === undefined ? {} : { decision: round1.decision }), ...(round1.usage === undefined ? {} : { usage: round1.usage }) }));
154
171
  totals.kept += 1;
155
172
  continue;
156
173
  }
@@ -159,52 +176,71 @@ export async function runScan(options) {
159
176
  totals.failed += 1;
160
177
  continue;
161
178
  }
179
+ const mergeDecision = options.cards !== undefined ? withDecisionRevisions(round1.decision, options.cards, row.a, row.b) : round1.decision;
162
180
  if (options.approval) {
163
- const event = { type: 'verify-passed', decision: round1.decision, target: round1.decision.target, ...(round1.sourceRevision === undefined ? {} : { sourceRevision: round1.sourceRevision }) };
164
- const projected = await transactRows(options.io, options.home, tx => {
181
+ const event = { type: 'verify-passed', decision: mergeDecision, target: mergeDecision.target, ...(round1.sourceRevision === undefined ? {} : { sourceRevision: round1.sourceRevision }), ...(mergeDecision.promptVersion === undefined ? {} : { promptVersion: mergeDecision.promptVersion }), ...(mergeDecision.promptHash === undefined ? {} : { promptHash: mergeDecision.promptHash }) };
182
+ const projected = await pairLedger(options.io, options.home).transact(tx => {
165
183
  const current = tx.rows().find(item => item.id === row.id);
166
184
  if (current === undefined)
167
185
  return null;
168
186
  const moved = tx.transition(row.id, event, current.version);
169
- return moved.ok && moved.row !== null ? v3ToLegacy(moved.row) : null;
187
+ return moved.ok && moved.row !== null ? v4ToLegacy(moved.row) : null;
170
188
  });
171
189
  if (projected === null) {
172
- pairs.push(pairOf(row, candidate, 'failed', { decision: round1.decision, failure: failureOf('decide', 'row changed before pending record') }));
190
+ pairs.push(pairOf(row, candidate, 'failed', { decision: mergeDecision, failure: failureOf('decide', 'row changed before pending record') }));
173
191
  totals.failed += 1;
174
192
  continue;
175
193
  }
176
194
  row = projected;
177
- pairs.push(pairOf(row, candidate, 'pending', { decision: round1.decision }));
195
+ pairs.push(pairOf(row, candidate, 'pending', { decision: mergeDecision }));
178
196
  totals.pending += 1;
179
197
  continue;
180
198
  }
181
- const staged = await transactRows(options.io, options.home, tx => {
199
+ const approved = await pairLedger(options.io, options.home).transact(tx => {
182
200
  const current = tx.rows().find(item => item.id === row.id);
183
201
  if (current === undefined)
184
202
  return null;
185
- const approved = tx.transition(row.id, { type: 'auto-approved', decision: round1.decision, target: round1.decision.target, ...(row.runId === undefined ? {} : { runId: row.runId }) }, current.version);
186
- if (!approved.ok || approved.row === null)
187
- return null;
188
- const moved = tx.transition(row.id, { type: 'staged', ...(round1.stagingRef === undefined ? {} : { ref: round1.stagingRef }), ...(round1.stagingExpiresAt === undefined ? {} : { expiresAt: round1.stagingExpiresAt }) }, approved.row.version);
189
- return moved.ok && moved.row !== null ? v3ToLegacy(moved.row) : null;
203
+ const moved = tx.transition(row.id, { type: 'auto-approved', decision: mergeDecision, target: mergeDecision.target, ...(row.runId === undefined ? {} : { runId: row.runId }) }, current.version);
204
+ return moved.ok && moved.row !== null ? v4ToLegacy(moved.row) : null;
190
205
  });
191
- if (staged === null) {
192
- pairs.push(pairOf(row, candidate, 'failed', { decision: round1.decision, failure: failureOf('author', 'row changed before staging record') }));
206
+ if (approved === null) {
207
+ pairs.push(pairOf(row, candidate, 'failed', { decision: mergeDecision, failure: failureOf('author', 'row changed before approve record') }));
193
208
  totals.failed += 1;
194
209
  continue;
195
210
  }
196
- row = staged;
197
- if (round1.stagingRef !== undefined) {
198
- const expiresAt = round1.stagingExpiresAt ?? options.now() + 7 * 86_400_000;
199
- await writeStagingIndex(options.io, options.home, { rowId: row.id, pairKey: row.pairKey, ref: round1.stagingRef, expiresAt, ...(row.runId === undefined ? {} : { runId: row.runId }), createdAt: options.now() });
211
+ row = approved;
212
+ let stageEntry;
213
+ if (options.ports.stage !== undefined) {
214
+ const staged = await options.ports.stage(row, mergeDecision);
215
+ if (!staged.ok) {
216
+ pairs.push(pairOf(row, candidate, 'failed', { decision: mergeDecision, failure: failureOf('author', staged.message) }));
217
+ totals.failed += 1;
218
+ continue;
219
+ }
220
+ stageEntry = staged.entry;
221
+ }
222
+ const fused = await pairLedger(options.io, options.home).transact(tx => {
223
+ const current = tx.rows().find(item => item.id === row.id);
224
+ if (current === undefined)
225
+ return null;
226
+ const moved = tx.transition(row.id, { type: 'staged' }, current.version);
227
+ return moved.ok && moved.row !== null ? v4ToLegacy(moved.row) : null;
228
+ });
229
+ if (fused === null) {
230
+ pairs.push(pairOf(row, candidate, 'failed', { decision: mergeDecision, failure: failureOf('author', 'row changed before staging record') }));
231
+ totals.failed += 1;
232
+ continue;
200
233
  }
234
+ row = fused;
235
+ if (stageEntry !== undefined)
236
+ await presentStage(options.io, options.home, stageEntry);
201
237
  if (isAborted(options.signal)) {
202
- const projected = await transactRows(options.io, options.home, tx => {
238
+ const projected = await pairLedger(options.io, options.home).transact(tx => {
203
239
  const current = tx.rows().find(item => item.id === row.id);
204
240
  if (current === undefined)
205
241
  return null;
206
242
  const moved = tx.transition(row.id, { type: 'skipped', reason: 'cancelled' }, current.version);
207
- return moved.ok && moved.row !== null ? v3ToLegacy(moved.row) : null;
243
+ return moved.ok && moved.row !== null ? v4ToLegacy(moved.row) : null;
208
244
  });
209
245
  if (projected !== null)
210
246
  row = projected;
@@ -215,44 +251,85 @@ export async function runScan(options) {
215
251
  await stage('author', { done: index, total: candidates.length });
216
252
  let round2;
217
253
  try {
218
- round2 = await options.ports.round2(row, round1.decision, options.signal);
254
+ round2 = await options.ports.round2(row, mergeDecision, options.signal);
219
255
  }
220
256
  catch (error) {
221
257
  round2 = { kind: 'failed', failure: failureOf('author', error) };
222
258
  }
223
259
  if (round2.kind === 'merged') {
224
- const dropped = await dropRowV3(options.io, options.home, row.id, 'committed', round2.unitId ?? 'unknown');
225
- if (!dropped) {
226
- pairs.push(pairOf(row, candidate, 'failed', { decision: round1.decision, failure: failureOf('commit', 'row changed before commit record') }));
227
- totals.failed += 1;
228
- continue;
229
- }
230
- pairs.push(pairOf(row, candidate, 'merged', {
231
- decision: round1.decision,
232
- ...(round2.unitId === undefined ? {} : { unitId: round2.unitId }),
233
- ...(round2.target === undefined ? {} : { target: round2.target }),
234
- archived: round2.archived ?? [],
235
- }));
260
+ finalizeRows.push({ id: row.id, unitId: round2.unitId ?? 'unknown', evidenceSha256: round2.evidenceSha256 ?? 'unknown' });
261
+ pairs.push({ ...pairOf(row, candidate, 'merged', {
262
+ decision: mergeDecision,
263
+ ...(round2.unitId === undefined ? {} : { unitId: round2.unitId }),
264
+ ...(round2.target === undefined ? {} : { target: round2.target }),
265
+ ...(round2.usage === undefined ? {} : { usage: round2.usage }),
266
+ ...(round2.lengths === undefined ? {} : { lengths: round2.lengths }),
267
+ archived: round2.archived ?? [],
268
+ }), ...(round2.validationPlan === undefined ? {} : { validationPlan: round2.validationPlan }) });
236
269
  totals.merged += 1;
237
270
  continue;
238
271
  }
239
272
  const failed = round2.failure ?? failureOf('author', 'round2 failed');
240
- const projected = await transactRows(options.io, options.home, tx => {
273
+ if (failed.code === 'merge-over-limit' || failed.code === 'limit' || failed.code === 'merge-validation') {
274
+ const reason = failed.code === 'merge-validation' ? 'validation-failed' : 'merge-over-limit';
275
+ const skipped = await pairLedger(options.io, options.home).transact(tx => {
276
+ const current = tx.rows().find(item => item.id === row.id);
277
+ return current === undefined ? false : tx.transition(row.id, { type: 'skipped', reason, origin: 'system', skipSource: 'system' }, current.version).ok;
278
+ });
279
+ if (skipped)
280
+ await dropStage(options.io, options.home, row.id);
281
+ pairs.push(pairOf(row, candidate, 'failed', { decision: mergeDecision, failure: failed }));
282
+ totals.failed += 1;
283
+ continue;
284
+ }
285
+ if (failed.code === 'target-changed' || failed.code === 'target-taken' || failed.code === 'source-changed' || failed.code === 'support-changed' || failed.code === 'source-gone') {
286
+ const code = failed.code === 'target-changed' ? 'target-taken' : failed.code === 'support-changed' ? 'source-changed' : failed.code;
287
+ await pairLedger(options.io, options.home).transact(tx => {
288
+ const current = tx.rows().find(item => item.id === row.id);
289
+ if (current === undefined)
290
+ return false;
291
+ if (code === 'source-gone')
292
+ return tx.remove(row.id, 'superseded', undefined, current.version).ok;
293
+ return tx.transition(row.id, { type: 'skipped', reason: code, origin: 'system', skipSource: 'system' }, current.version).ok;
294
+ });
295
+ await dropStage(options.io, options.home, row.id);
296
+ pairs.push(pairOf(row, candidate, 'failed', { decision: mergeDecision, failure: { ...failed, code, retryable: false, retryFrom: 'none' } }));
297
+ totals.failed += 1;
298
+ continue;
299
+ }
300
+ const terminal = failed.code === 'commit-failed' || failed.code === 'validation-failed' || failed.code === 'merge-validation';
301
+ const projected = terminal ? null : await pairLedger(options.io, options.home).transact(tx => {
241
302
  const current = tx.rows().find(item => item.id === row.id);
242
303
  if (current === undefined)
243
304
  return null;
244
305
  const moved = tx.transition(row.id, { type: 'failed', failure: failed }, current.version);
245
- return moved.ok && moved.row !== null ? v3ToLegacy(moved.row) : null;
306
+ return moved.ok && moved.row !== null ? v4ToLegacy(moved.row) : null;
246
307
  });
308
+ if (projected === null && terminal) {
309
+ await pairLedger(options.io, options.home).transact(tx => {
310
+ const current = tx.rows().find(item => item.id === row.id);
311
+ return current === undefined ? false : tx.remove(row.id, 'superseded', undefined, current.version).ok;
312
+ });
313
+ pairs.push(pairOf(row, candidate, 'failed', { decision: mergeDecision, failure: failed }));
314
+ totals.failed += 1;
315
+ continue;
316
+ }
247
317
  if (projected === null) {
248
- pairs.push(pairOf(row, candidate, 'failed', { decision: round1.decision, failure: failureOf('author', 'row changed before failure record') }));
318
+ pairs.push(pairOf(row, candidate, 'failed', { decision: mergeDecision, failure: failureOf('author', 'row changed before failure record') }));
249
319
  totals.failed += 1;
250
320
  continue;
251
321
  }
252
322
  row = projected;
253
- pairs.push(pairOf(row, candidate, 'failed', { decision: round1.decision, failure: failed }));
323
+ pairs.push(pairOf(row, candidate, 'failed', { decision: mergeDecision, failure: failed }));
254
324
  totals.failed += 1;
255
325
  }
326
+ const selectionReportOnly = candidates.reportOnly ?? [];
327
+ const selectionReportOnlyPairs = selectionReportOnly.flatMap(raw => {
328
+ const value = raw;
329
+ if (typeof value.a !== 'string' || typeof value.b !== 'string')
330
+ return [];
331
+ return [{ id: '', pairKey: typeof value.pairKey === 'string' ? value.pairKey : pairKey(value.a, value.b), a: value.a, b: value.b, queue: 'verify', candidate: { a: value.a, b: value.b }, outcome: 'kept', archived: [], ...(value.gate === undefined ? {} : { gate: value.gate }) }];
332
+ });
256
333
  return {
257
334
  stopReason,
258
335
  scope: {
@@ -261,8 +338,26 @@ export async function runScan(options) {
261
338
  },
262
339
  pairs,
263
340
  totals,
264
- notes: [...(options.notes ?? [])],
341
+ notes: [...(options.notes ?? []), ...degradedNotes],
342
+ reportOnly: [...pairs.filter(pair => pair.outcome === 'kept' || pair.gate?.level === 'L0' || pair.gate?.level === 'L1'), ...selectionReportOnlyPairs],
343
+ validationFailures: pairs.filter(pair => pair.failure !== undefined && (pair.failure.stage === 'validate' || pair.failure.code === 'merge-validation' || pair.failure.code === 'merge-over-limit')),
344
+ archived: [...new Set(pairs.flatMap(pair => pair.archived))],
345
+ ...(pairs.find(pair => pair.outcome === 'merged' && pair.unitId !== undefined) === undefined ? {} : { commit: (() => { const pair = pairs.find(item => item.outcome === 'merged' && item.unitId !== undefined); return { ...(pair.unitId === undefined ? {} : { unitId: pair.unitId }), ...(pair.target === undefined ? {} : { target: pair.target.actual ?? pair.target.requested }), ...(pair.target === undefined ? {} : { created: pair.target.created }) }; })() }),
346
+ ...(pairs.every(pair => pair.usage === undefined) ? {} : { usage: { inputTokens: pairs.reduce((sum, pair) => sum + (pair.usage?.inputTokens ?? 0), 0), outputTokens: pairs.reduce((sum, pair) => sum + (pair.usage?.outputTokens ?? 0), 0), totalTokens: pairs.reduce((sum, pair) => sum + (pair.usage?.totalTokens ?? 0), 0), costCny: pairs.reduce((sum, pair) => sum + (pair.usage?.costCny ?? 0), 0) } }),
347
+ timing: { screeningMs: 0, gateMs: 0, agentMs: Math.max(0, options.now() - startedAt), commitMs: 0 },
348
+ criteria: pairs.map(pair => pair.criteria ?? criteriaFrom(pair.gate?.level ?? 'L2', pair.gate?.verdict ?? 0, pair.decision?.shouldMerge === true ? 'merge' : 'keep')),
265
349
  ...(remaining.length === 0 ? {} : { remaining }),
350
+ ...(finalizeRows.length === 0 ? {} : { finalize: async () => {
351
+ for (const item of finalizeRows) {
352
+ const removed = await pairLedger(options.io, options.home).transact(tx => {
353
+ const current = tx.rows().find(entry => entry.id === item.id);
354
+ return current === undefined ? true : tx.remove(item.id, 'committed', { unitId: item.unitId, evidenceSha256: item.evidenceSha256 }, current.version).ok;
355
+ });
356
+ if (!removed)
357
+ throw new Error('row changed before commit record: ' + item.id);
358
+ await dropStage(options.io, options.home, item.id);
359
+ }
360
+ } })
266
361
  };
267
362
  }
268
363
  //# sourceMappingURL=scan-run.js.map
@@ -9,6 +9,13 @@ export declare class StageToolRegistry {
9
9
  bind(childId: string, context: StageToolContext): void;
10
10
  unbind(childId: string): void;
11
11
  clear(): void;
12
+ /** Diagnostic view for a refused tool call; names only, no content. */
13
+ snapshot(exec: unknown): {
14
+ readonly childId?: string;
15
+ readonly bound: number;
16
+ readonly authors: number;
17
+ readonly keys: readonly string[];
18
+ };
12
19
  resolve(exec: unknown): StageToolContext | undefined;
13
20
  }
14
21
  //# sourceMappingURL=stage-tool-registry.d.ts.map
@@ -9,9 +9,20 @@ export class StageToolRegistry {
9
9
  clear() {
10
10
  this.bindings.clear();
11
11
  }
12
+ /** Diagnostic view for a refused tool call; names only, no content. */
13
+ snapshot(exec) {
14
+ const childId = childIdOf(exec);
15
+ return { ...(childId === undefined ? {} : { childId }), bound: this.bindings.size, authors: [...this.bindings.values()].filter(context => context.phase === 'author').length, keys: [...this.bindings.keys()] };
16
+ }
12
17
  resolve(exec) {
13
18
  const childId = childIdOf(exec);
14
- return childId === undefined ? undefined : this.bindings.get(childId);
19
+ const direct = childId === undefined ? undefined : this.bindings.get(childId);
20
+ if (direct !== undefined)
21
+ return direct;
22
+ // New-child sessions may expose a session id that differs from the channel's childId.
23
+ // Only a single author-phase binding is unambiguous; never guess with concurrent bindings.
24
+ const authors = [...this.bindings.values()].filter(context => context.phase === 'author');
25
+ return authors.length === 1 ? authors[0] : undefined;
15
26
  }
16
27
  }
17
28
  function childIdOf(exec) {