@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.
Files changed (90) hide show
  1. package/README.md +4 -3
  2. package/lib/index.js +2321 -854
  3. package/lib/types/adapters/library.js +4 -1
  4. package/lib/types/adapters/model.d.ts +31 -0
  5. package/lib/types/adapters/model.js +46 -0
  6. package/lib/types/approve-command.d.ts +2 -0
  7. package/lib/types/approve-command.js +41 -46
  8. package/lib/types/authoring-flow.d.ts +3 -1
  9. package/lib/types/authoring-flow.js +15 -5
  10. package/lib/types/command-surface.d.ts +5 -0
  11. package/lib/types/command-surface.js +44 -0
  12. package/lib/types/commit-assembly.d.ts +2 -0
  13. package/lib/types/commit-assembly.js +7 -2
  14. package/lib/types/commit-execute.d.ts +7 -1
  15. package/lib/types/commit-execute.js +40 -10
  16. package/lib/types/commit-recovery.d.ts +7 -2
  17. package/lib/types/commit-recovery.js +73 -96
  18. package/lib/types/contracts.d.ts +1 -1
  19. package/lib/types/contracts.js +1 -1
  20. package/lib/types/coordinator.d.ts +3 -2
  21. package/lib/types/coordinator.js +4 -3
  22. package/lib/types/decision.js +1 -1
  23. package/lib/types/doctor.d.ts +7 -0
  24. package/lib/types/doctor.js +104 -4
  25. package/lib/types/domain/claim.d.ts +4 -1
  26. package/lib/types/domain/claim.js +2 -2
  27. package/lib/types/domain/commit-journal-store.js +5 -1
  28. package/lib/types/domain/commit-journal.js +1 -1
  29. package/lib/types/domain/consistency.d.ts +22 -0
  30. package/lib/types/domain/consistency.js +50 -0
  31. package/lib/types/domain/ledger.js +10 -1
  32. package/lib/types/domain/migrate-v3.d.ts +20 -0
  33. package/lib/types/domain/migrate-v3.js +68 -0
  34. package/lib/types/domain/row-tx.d.ts +27 -0
  35. package/lib/types/domain/row-tx.js +31 -0
  36. package/lib/types/domain/row-v3-bridge.d.ts +6 -0
  37. package/lib/types/domain/row-v3-bridge.js +47 -0
  38. package/lib/types/domain/row-v3.d.ts +113 -0
  39. package/lib/types/domain/row-v3.js +107 -0
  40. package/lib/types/domain/row.d.ts +1 -0
  41. package/lib/types/domain/staging-index.d.ts +14 -0
  42. package/lib/types/domain/staging-index.js +41 -0
  43. package/lib/types/domain/state-v3.d.ts +15 -0
  44. package/lib/types/domain/state-v3.js +80 -0
  45. package/lib/types/facts.d.ts +24 -0
  46. package/lib/types/facts.js +68 -0
  47. package/lib/types/gate-filter.d.ts +31 -0
  48. package/lib/types/gate-filter.js +64 -0
  49. package/lib/types/index.d.ts +5 -2
  50. package/lib/types/index.js +16 -26
  51. package/lib/types/ladder.js +1 -1
  52. package/lib/types/merge-routes.d.ts +2 -2
  53. package/lib/types/merge-routes.js +21 -22
  54. package/lib/types/metrics.d.ts +16 -0
  55. package/lib/types/metrics.js +27 -0
  56. package/lib/types/new-child-authoring.d.ts +3 -1
  57. package/lib/types/new-child-authoring.js +14 -3
  58. package/lib/types/pending-actions.d.ts +2 -2
  59. package/lib/types/pending-actions.js +17 -17
  60. package/lib/types/propose.d.ts +26 -0
  61. package/lib/types/propose.js +69 -0
  62. package/lib/types/read-commands.d.ts +2 -2
  63. package/lib/types/read-commands.js +5 -3
  64. package/lib/types/resume-command.d.ts +2 -0
  65. package/lib/types/resume-command.js +4 -1
  66. package/lib/types/round2-outcome.js +17 -9
  67. package/lib/types/runtime.d.ts +4 -1
  68. package/lib/types/runtime.js +99 -29
  69. package/lib/types/scan-ports.d.ts +3 -1
  70. package/lib/types/scan-ports.js +2 -0
  71. package/lib/types/scan-run.js +114 -51
  72. package/lib/types/screening-port.d.ts +47 -0
  73. package/lib/types/screening-port.js +93 -0
  74. package/lib/types/screening.d.ts +30 -0
  75. package/lib/types/screening.js +75 -0
  76. package/lib/types/state.d.ts +14 -0
  77. package/lib/types/state.js +70 -1
  78. package/lib/types/status.d.ts +3 -0
  79. package/lib/types/status.js +17 -0
  80. package/lib/types/target-claim.d.ts +1 -0
  81. package/lib/types/target-claim.js +63 -18
  82. package/lib/types/unit-claim.d.ts +5 -0
  83. package/lib/types/unit-claim.js +15 -0
  84. package/lib/types/wire-projection.d.ts +36 -0
  85. package/lib/types/wire-projection.js +69 -0
  86. package/lib/types/wire.d.ts +254 -0
  87. package/lib/types/wire.js +3 -0
  88. package/package.json +8 -4
  89. package/lib/types/routes.d.ts +0 -70
  90. package/lib/types/routes.js +0 -78
@@ -2,10 +2,11 @@
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";
5
+ import { pairKey, rowIdOf, } from "./contracts.js";
6
+ import { dropRowV3, loadMergeState, transactRows } from "./state.js";
8
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";
9
10
  const isAborted = (signal) => signal.aborted;
10
11
  function at(now) {
11
12
  return new Date(now()).toISOString();
@@ -63,28 +64,37 @@ export async function runScan(options) {
63
64
  await stage('decide', { done: index, total: candidates.length });
64
65
  let row;
65
66
  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 };
67
+ const key = pairKey(candidate.a, candidate.b);
68
+ const atNow = at(options.now);
69
+ 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
70
  const before = (await loadMergeState(options.io, options.home)).state.rows.find(item => item.pairKey === row.pairKey);
71
71
  if (before?.state === 'verify' && before.runId !== undefined && before.runId !== options.runId
72
72
  && (await options.isOwnerActive?.(before.runId)) === true) {
73
73
  continue;
74
74
  }
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
- }
75
+ const status = await transactRows(options.io, options.home, tx => {
76
+ const current = tx.rows().find(item => item.pairKey === row.pairKey);
77
+ if (current !== undefined && current.state !== 'verify' && current.state !== 'skipped')
78
+ return 'conflict:' + current.state;
79
+ if (current?.state === 'skipped') {
80
+ const removed = tx.transition(current.id, { type: 'restored' }, current.version);
81
+ if (!removed.ok)
82
+ return 'restore-failed';
83
+ }
84
+ if (current?.state === 'verify')
85
+ return 'existing';
86
+ const made = tx.createVerify({
87
+ id: current?.id ?? rowIdOf(row.pairKey), pairKey: row.pairKey, a: row.a, b: row.b,
88
+ version: 0, createdAt: at(options.now), updatedAt: at(options.now), origin: 'agent',
89
+ state: 'verify', verifySubState: 'unverified', attempts: 0,
90
+ ...(row.runId === undefined ? {} : { runId: row.runId }),
91
+ });
92
+ return made.ok ? 'created' : 'invalid:' + made.code;
93
+ });
94
+ if (status.startsWith('conflict:'))
95
+ throw new Error('pair already in ' + status.slice(9));
96
+ if (status === 'restore-failed' || status.startsWith('invalid:'))
97
+ throw new Error('cannot place verify row: ' + status);
88
98
  }
89
99
  catch (error) {
90
100
  totals.failed += 1;
@@ -107,22 +117,39 @@ export async function runScan(options) {
107
117
  break;
108
118
  }
109
119
  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);
120
+ const failure = round1.failure ?? failureOf('decide', 'round1 failed');
121
+ const projected = await transactRows(options.io, options.home, tx => {
122
+ const current = tx.rows().find(item => item.id === row.id);
123
+ if (current === undefined)
124
+ return null;
125
+ const moved = tx.transition(row.id, { type: 'failed', failure }, current.version);
126
+ return moved.ok && moved.row !== null ? v3ToLegacy(moved.row) : null;
127
+ });
128
+ if (projected === null) {
129
+ pairs.push(pairOf(row, candidate, 'failed', { failure: failureOf('decide', 'row changed before failure record') }));
130
+ totals.failed += 1;
131
+ continue;
114
132
  }
115
- pairs.push(pairOf(row, candidate, 'failed', { failure: round1.failure ?? failureOf('decide', 'round1 failed') }));
133
+ row = projected;
134
+ pairs.push(pairOf(row, candidate, 'failed', { failure }));
116
135
  totals.failed += 1;
117
136
  continue;
118
137
  }
119
138
  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' });
139
+ const projected = await transactRows(options.io, options.home, tx => {
140
+ const current = tx.rows().find(item => item.id === row.id);
141
+ if (current === undefined)
142
+ 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;
145
+ });
146
+ if (projected === null) {
147
+ pairs.push(pairOf(row, candidate, 'failed', { failure: failureOf('decide', 'row changed before keep record') }));
148
+ totals.failed += 1;
149
+ continue;
125
150
  }
151
+ row = projected;
152
+ await recordPair(options.io, options.home, { pairKey: row.pairKey, runId: row.runId ?? 'merge', result: 'kept' });
126
153
  pairs.push(pairOf(row, candidate, 'kept', round1.decision === undefined ? {} : { decision: round1.decision }));
127
154
  totals.kept += 1;
128
155
  continue;
@@ -133,29 +160,54 @@ export async function runScan(options) {
133
160
  continue;
134
161
  }
135
162
  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);
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 => {
165
+ const current = tx.rows().find(item => item.id === row.id);
166
+ if (current === undefined)
167
+ return null;
168
+ const moved = tx.transition(row.id, event, current.version);
169
+ return moved.ok && moved.row !== null ? v3ToLegacy(moved.row) : null;
170
+ });
171
+ if (projected === null) {
172
+ pairs.push(pairOf(row, candidate, 'failed', { decision: round1.decision, failure: failureOf('decide', 'row changed before pending record') }));
173
+ totals.failed += 1;
174
+ continue;
140
175
  }
176
+ row = projected;
141
177
  pairs.push(pairOf(row, candidate, 'pending', { decision: round1.decision }));
142
178
  totals.pending += 1;
143
179
  continue;
144
180
  }
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') }));
181
+ const staged = await transactRows(options.io, options.home, tx => {
182
+ const current = tx.rows().find(item => item.id === row.id);
183
+ if (current === undefined)
184
+ 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;
190
+ });
191
+ if (staged === null) {
192
+ pairs.push(pairOf(row, candidate, 'failed', { decision: round1.decision, failure: failureOf('author', 'row changed before staging record') }));
148
193
  totals.failed += 1;
149
194
  continue;
150
195
  }
151
- row = fuse.row;
152
- await upsertRow(options.io, options.home, options.now, row);
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() });
200
+ }
153
201
  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);
158
- }
202
+ const projected = await transactRows(options.io, options.home, tx => {
203
+ const current = tx.rows().find(item => item.id === row.id);
204
+ if (current === undefined)
205
+ return null;
206
+ const moved = tx.transition(row.id, { type: 'skipped', reason: 'cancelled' }, current.version);
207
+ return moved.ok && moved.row !== null ? v3ToLegacy(moved.row) : null;
208
+ });
209
+ if (projected !== null)
210
+ row = projected;
159
211
  stopReason = 'cancelled';
160
212
  remaining.push(...candidates.slice(index).map(candidate => ({ pairKey: pairKey(candidate.a, candidate.b), queue: 'fuse' })));
161
213
  break;
@@ -169,9 +221,12 @@ export async function runScan(options) {
169
221
  round2 = { kind: 'failed', failure: failureOf('author', error) };
170
222
  }
171
223
  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);
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
+ }
175
230
  pairs.push(pairOf(row, candidate, 'merged', {
176
231
  decision: round1.decision,
177
232
  ...(round2.unitId === undefined ? {} : { unitId: round2.unitId }),
@@ -182,11 +237,19 @@ export async function runScan(options) {
182
237
  continue;
183
238
  }
184
239
  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);
240
+ const projected = await transactRows(options.io, options.home, tx => {
241
+ const current = tx.rows().find(item => item.id === row.id);
242
+ if (current === undefined)
243
+ return null;
244
+ const moved = tx.transition(row.id, { type: 'failed', failure: failed }, current.version);
245
+ return moved.ok && moved.row !== null ? v3ToLegacy(moved.row) : null;
246
+ });
247
+ if (projected === null) {
248
+ pairs.push(pairOf(row, candidate, 'failed', { decision: round1.decision, failure: failureOf('author', 'row changed before failure record') }));
249
+ totals.failed += 1;
250
+ continue;
189
251
  }
252
+ row = projected;
190
253
  pairs.push(pairOf(row, candidate, 'failed', { decision: round1.decision, failure: failed }));
191
254
  totals.failed += 1;
192
255
  }
@@ -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
@@ -1,5 +1,7 @@
1
1
  import type { EvolutionIoLike } from '@lmzhen/dsh-evolution-core';
2
2
  import { type MergePairState, type MergeQueue, type MergeRow, type MergeState } from './contracts.ts';
3
+ import { type RowWriteResult } from './domain/row-tx.ts';
4
+ import { type SkippedRowV3, type V3Event, type V3Row, type VerifyRowV3 } from './domain/row-v3.ts';
3
5
  export declare function mergeStatePath(home: string): string;
4
6
  export interface LoadMergeStateResult {
5
7
  readonly ok: boolean;
@@ -8,6 +10,16 @@ export interface LoadMergeStateResult {
8
10
  }
9
11
  /** Pure read: never writes, never deletes a row. */
10
12
  export declare function loadMergeState(io: EvolutionIoLike, home: string): Promise<LoadMergeStateResult>;
13
+ /** Schema probe for doctor; the only other reader of mergeStatePath. */
14
+ export declare function loadStateSchema(io: EvolutionIoLike, home: string): Promise<number | null>;
15
+ export interface RowTx {
16
+ rows(): readonly V3Row[];
17
+ createVerify(row: VerifyRowV3): RowWriteResult;
18
+ createSkipped(row: SkippedRowV3): RowWriteResult;
19
+ transition(id: string, event: V3Event, expectedVersion: number): RowWriteResult;
20
+ }
21
+ /** G3-5 runtime write API: only createVerify/createSkipped/transition. v2 writes are rejected. */
22
+ export declare function transactRows<T>(io: EvolutionIoLike, home: string, fn: (tx: RowTx) => T): Promise<T>;
11
23
  /** The ONE write point. The callback returns the whole row set, not a delta. */
12
24
  export declare function mutateMergeState<T>(io: EvolutionIoLike, home: string, now: () => number, fn: (rows: readonly MergeRow[]) => {
13
25
  rows: readonly MergeRow[];
@@ -31,6 +43,8 @@ export type UpsertResult = {
31
43
  };
32
44
  export declare function upsertRow(io: EvolutionIoLike, home: string, now: () => number, row: MergeRow, expect?: UpsertExpect, cas?: string | null): Promise<UpsertResult>;
33
45
  export declare function moveRow(io: EvolutionIoLike, home: string, now: () => number, id: string, patch: MoveRowPatch): Promise<MergeRow | undefined>;
46
+ /** G3-5 v3-aware drop: only terminal events. */
47
+ export declare function dropRowV3(io: EvolutionIoLike, home: string, id: string, event: 'committed' | 'superseded' | 'restored', unitId?: string): Promise<boolean>;
34
48
  export declare function dropRow(io: EvolutionIoLike, home: string, now: () => number, id: string): Promise<boolean>;
35
49
  export declare function rowsOf(io: EvolutionIoLike, home: string, queue?: MergeQueue): Promise<readonly MergeRow[]>;
36
50
  //# sourceMappingURL=state.d.ts.map
@@ -10,6 +10,10 @@ import { join } from 'node:path';
10
10
  import { transactIo } from '@lmzhen/dsh-evolution-core';
11
11
  import { MERGE_STATE_VERSION, queueOf, } from "./contracts.js";
12
12
  import { parseLedger } from "./domain/ledger.js";
13
+ import { legacyToV3 } from "./domain/row-v3-bridge.js";
14
+ import { applyRowWrite } from "./domain/row-tx.js";
15
+ import { isV3Row } from "./domain/row-v3.js";
16
+ import { deleteStagingIndex } from "./domain/staging-index.js";
13
17
  export function mergeStatePath(home) {
14
18
  return join(home, 'merge', 'merge-state.json');
15
19
  }
@@ -39,6 +43,55 @@ export async function loadMergeState(io, home) {
39
43
  ...(parsed.note === undefined ? {} : { note: parsed.note }),
40
44
  };
41
45
  }
46
+ /** Schema probe for doctor; the only other reader of mergeStatePath. */
47
+ export async function loadStateSchema(io, home) {
48
+ try {
49
+ const raw = await io.readText(mergeStatePath(home));
50
+ if (raw === null)
51
+ return null;
52
+ const parsed = JSON.parse(raw);
53
+ return typeof parsed.schemaVersion === 'number' ? parsed.schemaVersion : null;
54
+ }
55
+ catch {
56
+ return null;
57
+ }
58
+ }
59
+ /** G3-5 runtime write API: only createVerify/createSkipped/transition. v2 writes are rejected. */
60
+ export async function transactRows(io, home, fn) {
61
+ let result;
62
+ await transactIo(io, mergeStatePath(home), (current) => {
63
+ let rows = [];
64
+ if (current !== null) {
65
+ let parsed;
66
+ try {
67
+ parsed = JSON.parse(current);
68
+ }
69
+ catch {
70
+ throw new Error('merge-state-needs-repair');
71
+ }
72
+ const body = parsed;
73
+ if (body.schemaVersion !== 3 || !Array.isArray(body.rows) || !body.rows.every(isV3Row)) {
74
+ throw new Error(body.schemaVersion === 2 ? 'merge-state-migration-required' : 'merge-state-needs-repair');
75
+ }
76
+ rows = body.rows;
77
+ }
78
+ const apply = (write) => {
79
+ const next = applyRowWrite(rows, write, new Date().toISOString());
80
+ if (next.ok)
81
+ rows = next.rows;
82
+ return next;
83
+ };
84
+ const tx = {
85
+ rows: () => rows,
86
+ createVerify: row => apply({ kind: 'createVerify', row }),
87
+ createSkipped: row => apply({ kind: 'createSkipped', row }),
88
+ transition: (id, event, expectedVersion) => apply({ kind: 'transition', id, event, expectedVersion }),
89
+ };
90
+ result = fn(tx);
91
+ return JSON.stringify({ schemaVersion: 3, rows }, null, 2);
92
+ });
93
+ return result;
94
+ }
42
95
  /** The ONE write point. The callback returns the whole row set, not a delta. */
43
96
  export async function mutateMergeState(io, home, now, fn) {
44
97
  let result;
@@ -50,7 +103,10 @@ export async function mutateMergeState(io, home, now, fn) {
50
103
  throw new Error(parsed.note ?? 'merge-state-invalid');
51
104
  const out = fn(parsed.rows);
52
105
  result = out.result;
53
- return JSON.stringify({ schemaVersion: MERGE_STATE_VERSION, rows: out.rows }, null, 2);
106
+ const rows = out.rows.map(row => legacyToV3(row));
107
+ if (rows.some(row => row === null))
108
+ throw new Error('merge-state-v3-row-invalid');
109
+ return JSON.stringify({ schemaVersion: 3, rows }, null, 2);
54
110
  });
55
111
  // transactIo only returns after fn ran; a skipped write is not a state outcome.
56
112
  void ran;
@@ -102,6 +158,19 @@ export async function moveRow(io, home, now, id, patch) {
102
158
  return { rows: copy, result: next };
103
159
  });
104
160
  }
161
+ /** G3-5 v3-aware drop: only terminal events. */
162
+ export async function dropRowV3(io, home, id, event, unitId = 'unknown') {
163
+ const dropped = await transactRows(io, home, tx => {
164
+ const row = tx.rows().find(item => item.id === id);
165
+ if (row === undefined)
166
+ return false;
167
+ const moved = tx.transition(id, event === 'committed' ? { type: 'committed', unitId } : { type: event }, row.version);
168
+ return moved.ok;
169
+ });
170
+ if (dropped)
171
+ await deleteStagingIndex(io, home, id);
172
+ return dropped;
173
+ }
105
174
  export async function dropRow(io, home, now, id) {
106
175
  return await mutateMergeState(io, home, now, (rows) => {
107
176
  const next = rows.filter(item => item.id !== id);
@@ -3,6 +3,7 @@
3
3
  * writes, no report reads. The UI and command parser share this vocabulary.
4
4
  */
5
5
  import type { MergeQueue, MergeRow } from './contracts.ts';
6
+ import type { Facts, RunsView } from './wire.ts';
6
7
  export interface MergeCounts {
7
8
  readonly verify: number;
8
9
  readonly pending: number;
@@ -11,5 +12,7 @@ export interface MergeCounts {
11
12
  }
12
13
  export declare function countRows(rows: readonly MergeRow[]): MergeCounts;
13
14
  export declare function formatStatus(rows: readonly MergeRow[]): string;
15
+ export declare function formatFacts(facts: Facts): string;
16
+ export declare function formatRunsView(view: RunsView): string;
14
17
  export declare function formatQueue(rows: readonly MergeRow[], queue: MergeQueue): string;
15
18
  //# sourceMappingURL=status.d.ts.map