@lmzhen/dsh-evolution-merge 0.30.17 → 0.30.19

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 (187) hide show
  1. package/README.md +25 -7
  2. package/data/minhash_params.json +1063 -0
  3. package/lib/index.js +7308 -1800
  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/library.js +4 -1
  9. package/lib/types/adapters/model-runner.d.ts +0 -5
  10. package/lib/types/adapters/model-runner.js +8 -2
  11. package/lib/types/adapters/model.d.ts +31 -0
  12. package/lib/types/adapters/model.js +47 -0
  13. package/lib/types/adapters/select.js +12 -8
  14. package/lib/types/approve-command.d.ts +14 -1
  15. package/lib/types/approve-command.js +72 -44
  16. package/lib/types/authoring-flow.d.ts +19 -1
  17. package/lib/types/authoring-flow.js +49 -15
  18. package/lib/types/channel-port.d.ts +3 -1
  19. package/lib/types/channel-port.js +8 -3
  20. package/lib/types/command-defs.d.ts +5 -0
  21. package/lib/types/command-defs.js +33 -0
  22. package/lib/types/command-surface.d.ts +5 -0
  23. package/lib/types/command-surface.js +11 -0
  24. package/lib/types/commands.d.ts +3 -4
  25. package/lib/types/commands.js +33 -72
  26. package/lib/types/commit-assembly.d.ts +16 -1
  27. package/lib/types/commit-assembly.js +72 -11
  28. package/lib/types/commit-execute.d.ts +14 -2
  29. package/lib/types/commit-execute.js +76 -14
  30. package/lib/types/commit-plan.d.ts +7 -1
  31. package/lib/types/commit-plan.js +4 -0
  32. package/lib/types/commit-receipt.d.ts +3 -0
  33. package/lib/types/commit-recovery.d.ts +7 -2
  34. package/lib/types/commit-recovery.js +73 -96
  35. package/lib/types/contracts.d.ts +44 -1
  36. package/lib/types/contracts.js +1 -1
  37. package/lib/types/coordinator.d.ts +3 -2
  38. package/lib/types/coordinator.js +4 -3
  39. package/lib/types/decision.js +1 -1
  40. package/lib/types/doctor.d.ts +18 -1
  41. package/lib/types/doctor.js +164 -10
  42. package/lib/types/domain/authoring-manifest.d.ts +26 -0
  43. package/lib/types/domain/authoring-manifest.js +43 -0
  44. package/lib/types/domain/claim.d.ts +4 -1
  45. package/lib/types/domain/claim.js +2 -2
  46. package/lib/types/domain/commit-journal-store.js +5 -1
  47. package/lib/types/domain/commit-journal.js +1 -1
  48. package/lib/types/domain/consistency.d.ts +22 -0
  49. package/lib/types/domain/consistency.js +50 -0
  50. package/lib/types/domain/engine-criteria.d.ts +10 -0
  51. package/lib/types/domain/engine-criteria.js +11 -0
  52. package/lib/types/domain/engine-input.d.ts +10 -0
  53. package/lib/types/domain/engine-input.js +26 -0
  54. package/lib/types/domain/errors.d.ts +15 -1
  55. package/lib/types/domain/errors.js +23 -4
  56. package/lib/types/domain/evidence-hash.d.ts +3 -0
  57. package/lib/types/domain/evidence-hash.js +14 -0
  58. package/lib/types/domain/failure-route.d.ts +14 -0
  59. package/lib/types/domain/failure-route.js +12 -0
  60. package/lib/types/domain/ledger.js +10 -1
  61. package/lib/types/domain/live-facts.d.ts +31 -0
  62. package/lib/types/domain/live-facts.js +54 -0
  63. package/lib/types/domain/merge-limits.d.ts +18 -0
  64. package/lib/types/domain/merge-limits.js +22 -0
  65. package/lib/types/domain/merge-report.d.ts +41 -0
  66. package/lib/types/domain/merge-report.js +2 -0
  67. package/lib/types/domain/merge-snapshot.d.ts +99 -0
  68. package/lib/types/domain/merge-snapshot.js +82 -0
  69. package/lib/types/domain/migrate-v3.d.ts +20 -0
  70. package/lib/types/domain/migrate-v3.js +68 -0
  71. package/lib/types/domain/pair-ledger.d.ts +34 -0
  72. package/lib/types/domain/pair-ledger.js +96 -0
  73. package/lib/types/domain/pre-1b-backup.d.ts +43 -0
  74. package/lib/types/domain/pre-1b-backup.js +140 -0
  75. package/lib/types/domain/provider-key.d.ts +2 -0
  76. package/lib/types/domain/provider-key.js +11 -0
  77. package/lib/types/domain/read-probe.d.ts +5 -0
  78. package/lib/types/domain/read-probe.js +11 -0
  79. package/lib/types/domain/reconcile.d.ts +47 -0
  80. package/lib/types/domain/reconcile.js +178 -0
  81. package/lib/types/domain/resolve-pair.d.ts +2 -9
  82. package/lib/types/domain/resolve-pair.js +2 -15
  83. package/lib/types/domain/row-tx.d.ts +27 -0
  84. package/lib/types/domain/row-tx.js +31 -0
  85. package/lib/types/domain/row-v3-bridge.d.ts +6 -0
  86. package/lib/types/domain/row-v3-bridge.js +47 -0
  87. package/lib/types/domain/row-v3.d.ts +113 -0
  88. package/lib/types/domain/row-v3.js +107 -0
  89. package/lib/types/domain/row-v4.d.ts +148 -0
  90. package/lib/types/domain/row-v4.js +159 -0
  91. package/lib/types/domain/row.d.ts +1 -0
  92. package/lib/types/domain/run-coordinator.d.ts +51 -0
  93. package/lib/types/domain/run-coordinator.js +45 -0
  94. package/lib/types/domain/schema4.d.ts +53 -0
  95. package/lib/types/domain/schema4.js +129 -0
  96. package/lib/types/domain/selection-policy.d.ts +14 -0
  97. package/lib/types/domain/selection-policy.js +6 -0
  98. package/lib/types/domain/skipped-expiry.d.ts +5 -0
  99. package/lib/types/domain/skipped-expiry.js +16 -0
  100. package/lib/types/domain/stage-aggregate.d.ts +96 -0
  101. package/lib/types/domain/stage-aggregate.js +293 -0
  102. package/lib/types/domain/staging-index.d.ts +15 -0
  103. package/lib/types/domain/staging-index.js +41 -0
  104. package/lib/types/domain/state-bootstrap.d.ts +43 -0
  105. package/lib/types/domain/state-bootstrap.js +267 -0
  106. package/lib/types/domain/state-v3.d.ts +4 -0
  107. package/lib/types/domain/state-v3.js +4 -0
  108. package/lib/types/domain/undo-journal.d.ts +19 -0
  109. package/lib/types/domain/undo-journal.js +20 -0
  110. package/lib/types/domain/undo-receipt.d.ts +18 -0
  111. package/lib/types/domain/undo-receipt.js +22 -0
  112. package/lib/types/domain/validation-plan.d.ts +65 -0
  113. package/lib/types/domain/validation-plan.js +49 -0
  114. package/lib/types/facts.d.ts +26 -0
  115. package/lib/types/facts.js +65 -0
  116. package/lib/types/gate-filter.d.ts +31 -0
  117. package/lib/types/gate-filter.js +64 -0
  118. package/lib/types/index.d.ts +7 -2
  119. package/lib/types/index.js +185 -82
  120. package/lib/types/ladder.js +1 -1
  121. package/lib/types/merge-command.d.ts +3 -1
  122. package/lib/types/merge-command.js +10 -6
  123. package/lib/types/merge-routes.d.ts +12 -7
  124. package/lib/types/merge-routes.js +87 -23
  125. package/lib/types/merge-tool.d.ts +1 -1
  126. package/lib/types/merge-tool.js +2 -2
  127. package/lib/types/metrics.d.ts +16 -0
  128. package/lib/types/metrics.js +27 -0
  129. package/lib/types/new-child-authoring.d.ts +5 -1
  130. package/lib/types/new-child-authoring.js +71 -13
  131. package/lib/types/params.d.ts +2 -4
  132. package/lib/types/params.js +13 -0
  133. package/lib/types/pending-actions.d.ts +2 -2
  134. package/lib/types/pending-actions.js +17 -17
  135. package/lib/types/prompts.d.ts +2 -4
  136. package/lib/types/prompts.js +21 -8
  137. package/lib/types/propose.d.ts +26 -0
  138. package/lib/types/propose.js +69 -0
  139. package/lib/types/read-commands.d.ts +4 -4
  140. package/lib/types/read-commands.js +19 -26
  141. package/lib/types/recovery-coordinator.d.ts +34 -0
  142. package/lib/types/recovery-coordinator.js +84 -0
  143. package/lib/types/recovery-projection.d.ts +31 -0
  144. package/lib/types/recovery-projection.js +96 -0
  145. package/lib/types/resume-command.d.ts +7 -1
  146. package/lib/types/resume-command.js +50 -14
  147. package/lib/types/revision.d.ts +17 -0
  148. package/lib/types/revision.js +39 -3
  149. package/lib/types/round2-outcome.d.ts +2 -1
  150. package/lib/types/round2-outcome.js +68 -12
  151. package/lib/types/run-shell.d.ts +16 -2
  152. package/lib/types/run-shell.js +49 -5
  153. package/lib/types/runtime.d.ts +4 -6
  154. package/lib/types/runtime.js +341 -86
  155. package/lib/types/scan-command.d.ts +1 -0
  156. package/lib/types/scan-command.js +2 -0
  157. package/lib/types/scan-ports.d.ts +21 -1
  158. package/lib/types/scan-ports.js +51 -23
  159. package/lib/types/scan-run.d.ts +17 -0
  160. package/lib/types/scan-run.js +221 -63
  161. package/lib/types/screening-port.d.ts +47 -0
  162. package/lib/types/screening-port.js +93 -0
  163. package/lib/types/screening.d.ts +30 -0
  164. package/lib/types/screening.js +75 -0
  165. package/lib/types/stage-tool-registry.d.ts +7 -0
  166. package/lib/types/stage-tool-registry.js +12 -1
  167. package/lib/types/state.d.ts +15 -1
  168. package/lib/types/state.js +39 -17
  169. package/lib/types/status.d.ts +8 -0
  170. package/lib/types/status.js +64 -0
  171. package/lib/types/surface/body.d.ts +4 -0
  172. package/lib/types/surface/body.js +16 -0
  173. package/lib/types/surface/merge-view.d.ts +14 -0
  174. package/lib/types/surface/merge-view.js +126 -0
  175. package/lib/types/target-claim.d.ts +1 -0
  176. package/lib/types/target-claim.js +63 -18
  177. package/lib/types/undo-operation.d.ts +24 -0
  178. package/lib/types/undo-operation.js +135 -0
  179. package/lib/types/unit-claim.d.ts +5 -0
  180. package/lib/types/unit-claim.js +15 -0
  181. package/lib/types/wire-projection.d.ts +36 -0
  182. package/lib/types/wire-projection.js +69 -0
  183. package/lib/types/wire.d.ts +273 -0
  184. package/lib/types/wire.js +3 -0
  185. package/package.json +10 -4
  186. package/lib/types/routes.d.ts +0 -70
  187. package/lib/types/routes.js +0 -78
@@ -2,10 +2,14 @@
2
2
  * The scan pair loop. Registry/report lifecycle stays in withMergeRun; this module owns
3
3
  * candidate → round1 → pending/fuse → commit transitions and returns the report draft.
4
4
  */
5
- import { pairKey, } from "./contracts.js";
6
- import { transition } from "./coordinator.js";
7
- import { dropRow, loadMergeState, upsertRow } from "./state.js";
8
- import { recordPair } from "./domain/pair-index-store.js";
5
+ import { pairKey, rowIdOf, } from "./contracts.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";
9
13
  const isAborted = (signal) => signal.aborted;
10
14
  function at(now) {
11
15
  return new Date(now()).toISOString();
@@ -45,8 +49,10 @@ export async function runScan(options) {
45
49
  const startedAt = options.startedAt ?? options.now();
46
50
  const timeoutMs = (options.timeoutMinutes ?? 0) > 0 ? options.timeoutMinutes * 60_000 : 0;
47
51
  const pairs = [];
52
+ const degradedNotes = [];
48
53
  const totals = { merged: 0, kept: 0, pending: 0, failed: 0 };
49
54
  const remaining = [];
55
+ const finalizeRows = [];
50
56
  let stopReason = 'completed';
51
57
  for (let index = 0; index < candidates.length; index += 1) {
52
58
  if (isAborted(options.signal)) {
@@ -63,28 +69,37 @@ export async function runScan(options) {
63
69
  await stage('decide', { done: index, total: candidates.length });
64
70
  let row;
65
71
  try {
66
- const created = transition(undefined, { type: 'candidate', a: candidate.a, b: candidate.b, at: at(options.now) });
67
- if (!created.ok || created.row === null)
68
- throw new Error(created.ok ? 'candidate row missing' : created.message);
69
- row = options.runId === undefined ? created.row : { ...created.row, runId: options.runId };
72
+ const key = pairKey(candidate.a, candidate.b);
73
+ const atNow = at(options.now);
74
+ row = { id: rowIdOf(key), pairKey: key, a: candidate.a, b: candidate.b, state: 'verify', queue: 'verify', createdAt: atNow, updatedAt: atNow, ...(options.runId === undefined ? {} : { runId: options.runId }) };
70
75
  const before = (await loadMergeState(options.io, options.home)).state.rows.find(item => item.pairKey === row.pairKey);
71
76
  if (before?.state === 'verify' && before.runId !== undefined && before.runId !== options.runId
72
77
  && (await options.isOwnerActive?.(before.runId)) === true) {
73
78
  continue;
74
79
  }
75
- const placed = await upsertRow(options.io, options.home, options.now, row, 'verify', before?.updatedAt ?? null);
76
- if (!placed.ok && before?.state === 'skipped') {
77
- const restored = transition(before, { type: 'restore', at: at(options.now) });
78
- if (!restored.ok || restored.row === null)
79
- throw new Error('cannot restore skipped pair');
80
- const persisted = await upsertRow(options.io, options.home, options.now, restored.row, 'any', before.updatedAt);
81
- if (!persisted.ok)
82
- throw new Error('pair changed while restoring skipped row');
83
- row = restored.row;
84
- }
85
- else if (!placed.ok) {
86
- throw new Error('pair already in ' + placed.current.state);
87
- }
80
+ const status = await pairLedger(options.io, options.home).transact(tx => {
81
+ const current = tx.rows().find(item => item.pairKey === row.pairKey);
82
+ if (current !== undefined && current.state !== 'verify' && current.state !== 'skipped')
83
+ return 'conflict:' + current.state;
84
+ if (current?.state === 'skipped') {
85
+ const removed = tx.transition(current.id, { type: 'restored' }, current.version);
86
+ if (!removed.ok)
87
+ return 'restore-failed';
88
+ }
89
+ if (current?.state === 'verify')
90
+ return 'existing';
91
+ const made = tx.createVerify({
92
+ id: current?.id ?? rowIdOf(row.pairKey), pairKey: row.pairKey, a: row.a, b: row.b,
93
+ version: 0, createdAt: at(options.now), updatedAt: at(options.now), origin: 'agent',
94
+ state: 'verify', verifySubState: 'unverified', attempts: 0,
95
+ ...(row.runId === undefined ? {} : { runId: row.runId }),
96
+ });
97
+ return made.ok ? 'created' : 'invalid:' + made.code;
98
+ });
99
+ if (status.startsWith('conflict:'))
100
+ throw new Error('pair already in ' + status.slice(9));
101
+ if (status === 'restore-failed' || status.startsWith('invalid:'))
102
+ throw new Error('cannot place verify row: ' + status);
88
103
  }
89
104
  catch (error) {
90
105
  totals.failed += 1;
@@ -94,6 +109,15 @@ export async function runScan(options) {
94
109
  });
95
110
  continue;
96
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
+ }
97
121
  let round1;
98
122
  try {
99
123
  round1 = await options.ports.round1(candidate, options.signal);
@@ -107,23 +131,43 @@ export async function runScan(options) {
107
131
  break;
108
132
  }
109
133
  if (round1.kind === 'failed') {
110
- const next = transition(row, { type: 'round1-fail', failure: round1.failure ?? failureOf('decide', 'round1 failed'), at: at(options.now) });
111
- if (next.ok && next.row !== null) {
112
- row = next.row;
113
- await upsertRow(options.io, options.home, options.now, row);
134
+ if (round1.degraded !== undefined)
135
+ degradedNotes.push(...round1.degraded);
136
+ const failure = round1.failure ?? failureOf('decide', 'round1 failed');
137
+ const projected = await pairLedger(options.io, options.home).transact(tx => {
138
+ const current = tx.rows().find(item => item.id === row.id);
139
+ if (current === undefined)
140
+ return null;
141
+ const moved = tx.transition(row.id, { type: 'failed', failure }, current.version);
142
+ return moved.ok && moved.row !== null ? v4ToLegacy(moved.row) : null;
143
+ });
144
+ if (projected === null) {
145
+ pairs.push(pairOf(row, candidate, 'failed', { failure: failureOf('decide', 'row changed before failure record') }));
146
+ totals.failed += 1;
147
+ continue;
114
148
  }
115
- pairs.push(pairOf(row, candidate, 'failed', { failure: round1.failure ?? failureOf('decide', 'round1 failed') }));
149
+ row = projected;
150
+ pairs.push(pairOf(row, candidate, 'failed', { failure }));
116
151
  totals.failed += 1;
117
152
  continue;
118
153
  }
119
154
  if (round1.kind === 'keep') {
120
- const next = transition(row, { type: 'round1-keep', at: at(options.now) });
121
- if (next.ok && next.row !== null) {
122
- row = next.row;
123
- await upsertRow(options.io, options.home, options.now, row);
124
- await recordPair(options.io, options.home, { pairKey: row.pairKey, runId: row.runId ?? 'merge', result: 'kept' });
155
+ const projected = await pairLedger(options.io, options.home).transact(tx => {
156
+ const current = tx.rows().find(item => item.id === row.id);
157
+ if (current === undefined)
158
+ return 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;
162
+ });
163
+ if (projected === null) {
164
+ pairs.push(pairOf(row, candidate, 'failed', { failure: failureOf('decide', 'row changed before keep record') }));
165
+ totals.failed += 1;
166
+ continue;
125
167
  }
126
- pairs.push(pairOf(row, candidate, 'kept', round1.decision === undefined ? {} : { decision: round1.decision }));
168
+ row = projected;
169
+ await recordPair(options.io, options.home, { pairKey: row.pairKey, runId: row.runId ?? 'merge', result: 'kept' });
170
+ pairs.push(pairOf(row, candidate, 'kept', { ...(round1.decision === undefined ? {} : { decision: round1.decision }), ...(round1.usage === undefined ? {} : { usage: round1.usage }) }));
127
171
  totals.kept += 1;
128
172
  continue;
129
173
  }
@@ -132,30 +176,74 @@ export async function runScan(options) {
132
176
  totals.failed += 1;
133
177
  continue;
134
178
  }
179
+ const mergeDecision = options.cards !== undefined ? withDecisionRevisions(round1.decision, options.cards, row.a, row.b) : round1.decision;
135
180
  if (options.approval) {
136
- const next = transition(row, { type: 'round1-merge', approval: 'on', decision: round1.decision, ...(round1.sourceRevision === undefined ? {} : { sourceRevision: round1.sourceRevision }), ...(round1.targetRevision === undefined ? {} : { targetRevision: round1.targetRevision }), at: at(options.now) });
137
- if (next.ok && next.row !== null) {
138
- row = next.row;
139
- await upsertRow(options.io, options.home, options.now, row);
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 => {
183
+ const current = tx.rows().find(item => item.id === row.id);
184
+ if (current === undefined)
185
+ return null;
186
+ const moved = tx.transition(row.id, event, current.version);
187
+ return moved.ok && moved.row !== null ? v4ToLegacy(moved.row) : null;
188
+ });
189
+ if (projected === null) {
190
+ pairs.push(pairOf(row, candidate, 'failed', { decision: mergeDecision, failure: failureOf('decide', 'row changed before pending record') }));
191
+ totals.failed += 1;
192
+ continue;
140
193
  }
141
- pairs.push(pairOf(row, candidate, 'pending', { decision: round1.decision }));
194
+ row = projected;
195
+ pairs.push(pairOf(row, candidate, 'pending', { decision: mergeDecision }));
142
196
  totals.pending += 1;
143
197
  continue;
144
198
  }
145
- const fuse = transition(row, { type: 'round1-merge', approval: 'off', decision: round1.decision, ...(round1.stagingRef === undefined ? {} : { stagingRef: round1.stagingRef }), ...(round1.stagingExpiresAt === undefined ? {} : { stagingExpiresAt: round1.stagingExpiresAt }), at: at(options.now) });
146
- if (!fuse.ok || fuse.row === null) {
147
- pairs.push(pairOf(row, candidate, 'failed', { decision: round1.decision, failure: failureOf('decide', 'cannot enter fuse') }));
199
+ const approved = await pairLedger(options.io, options.home).transact(tx => {
200
+ const current = tx.rows().find(item => item.id === row.id);
201
+ if (current === undefined)
202
+ return 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;
205
+ });
206
+ if (approved === null) {
207
+ pairs.push(pairOf(row, candidate, 'failed', { decision: mergeDecision, failure: failureOf('author', 'row changed before approve record') }));
148
208
  totals.failed += 1;
149
209
  continue;
150
210
  }
151
- row = fuse.row;
152
- await upsertRow(options.io, options.home, options.now, row);
153
- if (isAborted(options.signal)) {
154
- const cancelled = transition(row, { type: 'round2-cancel', at: at(options.now) });
155
- if (cancelled.ok && cancelled.row !== null) {
156
- row = cancelled.row;
157
- await upsertRow(options.io, options.home, options.now, row);
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;
158
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;
233
+ }
234
+ row = fused;
235
+ if (stageEntry !== undefined)
236
+ await presentStage(options.io, options.home, stageEntry);
237
+ if (isAborted(options.signal)) {
238
+ const projected = await pairLedger(options.io, options.home).transact(tx => {
239
+ const current = tx.rows().find(item => item.id === row.id);
240
+ if (current === undefined)
241
+ return null;
242
+ const moved = tx.transition(row.id, { type: 'skipped', reason: 'cancelled' }, current.version);
243
+ return moved.ok && moved.row !== null ? v4ToLegacy(moved.row) : null;
244
+ });
245
+ if (projected !== null)
246
+ row = projected;
159
247
  stopReason = 'cancelled';
160
248
  remaining.push(...candidates.slice(index).map(candidate => ({ pairKey: pairKey(candidate.a, candidate.b), queue: 'fuse' })));
161
249
  break;
@@ -163,33 +251,85 @@ export async function runScan(options) {
163
251
  await stage('author', { done: index, total: candidates.length });
164
252
  let round2;
165
253
  try {
166
- round2 = await options.ports.round2(row, round1.decision, options.signal);
254
+ round2 = await options.ports.round2(row, mergeDecision, options.signal);
167
255
  }
168
256
  catch (error) {
169
257
  round2 = { kind: 'failed', failure: failureOf('author', error) };
170
258
  }
171
259
  if (round2.kind === 'merged') {
172
- const success = transition(row, { type: 'round2-success', at: at(options.now) });
173
- if (success.ok)
174
- await dropRow(options.io, options.home, options.now, row.id);
175
- pairs.push(pairOf(row, candidate, 'merged', {
176
- decision: round1.decision,
177
- ...(round2.unitId === undefined ? {} : { unitId: round2.unitId }),
178
- ...(round2.target === undefined ? {} : { target: round2.target }),
179
- archived: round2.archived ?? [],
180
- }));
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 }) });
181
269
  totals.merged += 1;
182
270
  continue;
183
271
  }
184
272
  const failed = round2.failure ?? failureOf('author', 'round2 failed');
185
- const next = transition(row, { type: 'round2-fail', failure: failed, at: at(options.now) });
186
- if (next.ok && next.row !== null) {
187
- row = next.row;
188
- await upsertRow(options.io, options.home, options.now, row);
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 => {
302
+ const current = tx.rows().find(item => item.id === row.id);
303
+ if (current === undefined)
304
+ return null;
305
+ const moved = tx.transition(row.id, { type: 'failed', failure: failed }, current.version);
306
+ return moved.ok && moved.row !== null ? v4ToLegacy(moved.row) : null;
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
+ }
317
+ if (projected === null) {
318
+ pairs.push(pairOf(row, candidate, 'failed', { decision: mergeDecision, failure: failureOf('author', 'row changed before failure record') }));
319
+ totals.failed += 1;
320
+ continue;
189
321
  }
190
- pairs.push(pairOf(row, candidate, 'failed', { decision: round1.decision, failure: failed }));
322
+ row = projected;
323
+ pairs.push(pairOf(row, candidate, 'failed', { decision: mergeDecision, failure: failed }));
191
324
  totals.failed += 1;
192
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
+ });
193
333
  return {
194
334
  stopReason,
195
335
  scope: {
@@ -198,8 +338,26 @@ export async function runScan(options) {
198
338
  },
199
339
  pairs,
200
340
  totals,
201
- 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')),
202
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
+ } })
203
361
  };
204
362
  }
205
363
  //# sourceMappingURL=scan-run.js.map
@@ -0,0 +1,47 @@
1
+ /**
2
+ * The real screening port: lexical floor + optional C3 proposal + optional L0–L2 gate.
3
+ * Each stage is independently testable; this file only composes their frozen contracts.
4
+ */
5
+ import { type MergeCandidate } from './contracts.ts';
6
+ import { type SkillCard, type ScreeningOptions } from './screening.ts';
7
+ import { type ModelPort } from './propose.ts';
8
+ import type { GateCache, GateModelPort } from './ladder.ts';
9
+ export interface ScreeningForRunOptions extends Omit<BuildScreeningOptions, 'signal'> {
10
+ readonly runSeed: string;
11
+ }
12
+ export interface BuildScreeningOptions {
13
+ readonly cards: readonly SkillCard[];
14
+ readonly signal: AbortSignal;
15
+ readonly lexical?: ScreeningOptions;
16
+ readonly propose?: {
17
+ readonly model: ModelPort;
18
+ readonly shardSize?: number;
19
+ readonly maxPairs?: number;
20
+ };
21
+ readonly gate?: {
22
+ readonly model: GateModelPort;
23
+ readonly modelName: string;
24
+ readonly runSeed: string;
25
+ readonly cache?: GateCache;
26
+ readonly auditRate?: number;
27
+ };
28
+ }
29
+ export interface ScreeningPortStats {
30
+ readonly lexical: number;
31
+ readonly proposed: number;
32
+ readonly candidates: number;
33
+ readonly kept: number;
34
+ readonly dropped: number;
35
+ readonly gateCalls: number;
36
+ readonly gateErrors: number;
37
+ readonly shards: number;
38
+ }
39
+ export interface ScreeningPortResult {
40
+ readonly candidates: readonly MergeCandidate[];
41
+ readonly stats: ScreeningPortStats;
42
+ readonly degraded: readonly string[];
43
+ }
44
+ export declare function buildScreeningCandidates(options: BuildScreeningOptions): Promise<ScreeningPortResult>;
45
+ /** Build the run-scoped screening function; the run id seeds the 10% audit. */
46
+ export declare function createScreeningForRun(options: ScreeningForRunOptions): (signal: AbortSignal) => Promise<readonly MergeCandidate[]>;
47
+ //# sourceMappingURL=screening-port.d.ts.map
@@ -0,0 +1,93 @@
1
+ /**
2
+ * The real screening port: lexical floor + optional C3 proposal + optional L0–L2 gate.
3
+ * Each stage is independently testable; this file only composes their frozen contracts.
4
+ */
5
+ import { pairKey } from "./contracts.js";
6
+ import { lexicalCandidates } from "./screening.js";
7
+ import { proposePairs } from "./propose.js";
8
+ import { filterCandidates } from "./gate-filter.js";
9
+ export async function buildScreeningCandidates(options) {
10
+ const lexical = lexicalCandidates(options.cards, options.lexical);
11
+ const merged = new Map();
12
+ for (const candidate of lexical.candidates)
13
+ merged.set(pairKey(candidate.a, candidate.b), candidate);
14
+ const degraded = [];
15
+ let proposed = 0;
16
+ let shards = 0;
17
+ if (options.propose !== undefined) {
18
+ const proposal = await proposePairs(options.cards, options.propose.model, {
19
+ signal: options.signal,
20
+ ...(options.propose.shardSize === undefined ? {} : { shardSize: options.propose.shardSize }),
21
+ ...(options.propose.maxPairs === undefined ? {} : { maxPairs: options.propose.maxPairs }),
22
+ });
23
+ proposed = proposal.candidates.length;
24
+ shards = proposal.shards;
25
+ degraded.push(...proposal.degraded);
26
+ for (const candidate of proposal.candidates) {
27
+ const key = pairKey(candidate.a, candidate.b);
28
+ const existing = merged.get(key);
29
+ if (existing === undefined) {
30
+ merged.set(key, candidate);
31
+ continue;
32
+ }
33
+ const arms = [...new Set([...(existing.arms ?? []), ...(candidate.arms ?? [])])];
34
+ merged.set(key, {
35
+ ...existing,
36
+ ...(candidate.reason === undefined || existing.reason !== undefined ? {} : { reason: candidate.reason }),
37
+ ...(arms.length === 0 ? {} : { arms }),
38
+ });
39
+ }
40
+ }
41
+ let candidates = [...merged.values()].sort((left, right) => (right.score ?? 0) - (left.score ?? 0)
42
+ || left.a.localeCompare(right.a)
43
+ || left.b.localeCompare(right.b));
44
+ let kept = candidates.length;
45
+ let dropped = 0;
46
+ let gateCalls = 0;
47
+ let gateErrors = 0;
48
+ if (options.gate !== undefined) {
49
+ const facts = options.cards.map(card => ({ name: card.name, description: card.description, body: card.body }));
50
+ const filtered = await filterCandidates(candidates, {
51
+ facts,
52
+ model: options.gate.model,
53
+ modelName: options.gate.modelName,
54
+ signal: options.signal,
55
+ runSeed: options.gate.runSeed,
56
+ ...(options.gate.cache === undefined ? {} : { cache: options.gate.cache }),
57
+ ...(options.gate.auditRate === undefined ? {} : { auditRate: options.gate.auditRate }),
58
+ });
59
+ candidates = filtered.kept.map(decision => decision.candidate);
60
+ kept = filtered.kept.length;
61
+ dropped = filtered.dropped.length;
62
+ gateCalls = filtered.calls;
63
+ gateErrors = filtered.errors.length;
64
+ for (const error of filtered.errors)
65
+ degraded.push('gate error: ' + error.candidate.a + ' + ' + error.candidate.b + ': ' + error.message);
66
+ }
67
+ return {
68
+ candidates,
69
+ stats: {
70
+ lexical: lexical.candidates.length,
71
+ proposed,
72
+ candidates: merged.size,
73
+ kept,
74
+ dropped,
75
+ gateCalls,
76
+ gateErrors,
77
+ shards,
78
+ },
79
+ degraded,
80
+ };
81
+ }
82
+ /** Build the run-scoped screening function; the run id seeds the 10% audit. */
83
+ export function createScreeningForRun(options) {
84
+ const gate = options.gate === undefined ? undefined : { ...options.gate, runSeed: options.runSeed };
85
+ return async (signal) => (await buildScreeningCandidates({
86
+ cards: options.cards,
87
+ signal,
88
+ ...(options.lexical === undefined ? {} : { lexical: options.lexical }),
89
+ ...(options.propose === undefined ? {} : { propose: options.propose }),
90
+ ...(gate === undefined ? {} : { gate }),
91
+ })).candidates;
92
+ }
93
+ //# sourceMappingURL=screening-port.js.map
@@ -0,0 +1,30 @@
1
+ import type { MergeCandidate } from './contracts.ts';
2
+ export interface SkillCard {
3
+ readonly name: string;
4
+ readonly description: string;
5
+ readonly body: string;
6
+ }
7
+ export interface ScreeningOptions {
8
+ /** Body Jaccard at/above this is a candidate on its own. */
9
+ readonly bodyGate?: number;
10
+ /** Description Jaccard at/above this is a candidate when the body also clears minAffinity. */
11
+ readonly summaryGate?: number;
12
+ /** Minimum body affinity for a summary-led candidate. */
13
+ readonly minAffinity?: number;
14
+ /** Upper bound after sorting by score; absent = no cap. */
15
+ readonly maxCandidates?: number;
16
+ }
17
+ export interface ScreeningStats {
18
+ readonly skills: number;
19
+ readonly filtered: number;
20
+ readonly candidates: number;
21
+ }
22
+ export interface ScreeningResult {
23
+ readonly candidates: readonly MergeCandidate[];
24
+ readonly stats: ScreeningStats;
25
+ }
26
+ /** Shannon entropy over characters; a cheap prefilter signal, not a name-quality judge. */
27
+ export declare function nameEntropy(name: string): number;
28
+ export declare function prefilter(cards: readonly SkillCard[]): readonly SkillCard[];
29
+ export declare function lexicalCandidates(cards: readonly SkillCard[], options?: ScreeningOptions): ScreeningResult;
30
+ //# sourceMappingURL=screening.d.ts.map
@@ -0,0 +1,75 @@
1
+ /**
2
+ * Deterministic candidate screening: name prefilter + lexical affinity.
3
+ *
4
+ * The semantic proposer (C3) is a separate port; this module is the zero-cost floor that
5
+ * must work without a model. It reuses core `vocabulary`/`affinity` so drift/dedup and
6
+ * merge screening share one tokenizer.
7
+ */
8
+ import { affinity, vocabulary } from '@lmzhen/dsh-evolution-core';
9
+ /** Shannon entropy over characters; a cheap prefilter signal, not a name-quality judge. */
10
+ export function nameEntropy(name) {
11
+ if (name === '')
12
+ return 0;
13
+ const counts = new Map();
14
+ for (const char of name)
15
+ counts.set(char, (counts.get(char) ?? 0) + 1);
16
+ let entropy = 0;
17
+ for (const count of counts.values()) {
18
+ const p = count / name.length;
19
+ entropy -= p * Math.log2(p);
20
+ }
21
+ return entropy;
22
+ }
23
+ const MIN_NAME_ENTROPY = 1.5;
24
+ const MIN_NAME_LENGTH = 6;
25
+ export function prefilter(cards) {
26
+ return cards.filter((card) => {
27
+ const name = card.name.trim();
28
+ if (name.length < MIN_NAME_LENGTH)
29
+ return false;
30
+ if (nameEntropy(name) < MIN_NAME_ENTROPY)
31
+ return false;
32
+ return card.body.trim() !== '' || card.description.trim() !== '';
33
+ });
34
+ }
35
+ function sortedPair(a, b) {
36
+ return a < b ? [a, b] : [b, a];
37
+ }
38
+ export function lexicalCandidates(cards, options = {}) {
39
+ const bodyGate = options.bodyGate ?? 0.9;
40
+ const summaryGate = options.summaryGate ?? 0.5;
41
+ const minAffinity = options.minAffinity ?? 0.3;
42
+ const eligible = prefilter(cards);
43
+ const vectors = eligible.map(card => ({
44
+ card,
45
+ summary: vocabulary(card.description),
46
+ body: vocabulary(card.body),
47
+ }));
48
+ const candidates = [];
49
+ for (let i = 0; i < vectors.length; i += 1) {
50
+ for (let j = i + 1; j < vectors.length; j += 1) {
51
+ const left = vectors[i];
52
+ const right = vectors[j];
53
+ if (left === undefined || right === undefined)
54
+ continue;
55
+ const summary = affinity(left.summary, right.summary);
56
+ const body = affinity(left.body, right.body);
57
+ const arms = [];
58
+ if (body >= bodyGate)
59
+ arms.push('body');
60
+ if (summary >= summaryGate && body >= minAffinity)
61
+ arms.push('summary');
62
+ if (arms.length === 0)
63
+ continue;
64
+ const [a, b] = sortedPair(left.card.name, right.card.name);
65
+ candidates.push({ a, b, score: Math.max(body, summary), arms, source: 'lexical' });
66
+ }
67
+ }
68
+ candidates.sort((left, right) => (right.score ?? 0) - (left.score ?? 0) || left.a.localeCompare(right.a) || left.b.localeCompare(right.b));
69
+ const capped = options.maxCandidates === undefined ? candidates : candidates.slice(0, options.maxCandidates);
70
+ return {
71
+ candidates: capped,
72
+ stats: { skills: cards.length, filtered: cards.length - eligible.length, candidates: capped.length },
73
+ };
74
+ }
75
+ //# sourceMappingURL=screening.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