@lmzhen/dsh-evolution-merge 0.30.17 → 0.30.18
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +4 -3
- package/lib/index.js +2321 -854
- package/lib/types/adapters/library.js +4 -1
- package/lib/types/adapters/model.d.ts +31 -0
- package/lib/types/adapters/model.js +46 -0
- package/lib/types/approve-command.d.ts +2 -0
- package/lib/types/approve-command.js +41 -46
- package/lib/types/authoring-flow.d.ts +3 -1
- package/lib/types/authoring-flow.js +15 -5
- package/lib/types/command-surface.d.ts +5 -0
- package/lib/types/command-surface.js +44 -0
- package/lib/types/commit-assembly.d.ts +2 -0
- package/lib/types/commit-assembly.js +7 -2
- package/lib/types/commit-execute.d.ts +7 -1
- package/lib/types/commit-execute.js +40 -10
- package/lib/types/commit-recovery.d.ts +7 -2
- package/lib/types/commit-recovery.js +73 -96
- package/lib/types/contracts.d.ts +1 -1
- package/lib/types/contracts.js +1 -1
- package/lib/types/coordinator.d.ts +3 -2
- package/lib/types/coordinator.js +4 -3
- package/lib/types/decision.js +1 -1
- package/lib/types/doctor.d.ts +7 -0
- package/lib/types/doctor.js +104 -4
- package/lib/types/domain/claim.d.ts +4 -1
- package/lib/types/domain/claim.js +2 -2
- package/lib/types/domain/commit-journal-store.js +5 -1
- package/lib/types/domain/commit-journal.js +1 -1
- package/lib/types/domain/consistency.d.ts +22 -0
- package/lib/types/domain/consistency.js +50 -0
- package/lib/types/domain/ledger.js +10 -1
- package/lib/types/domain/migrate-v3.d.ts +20 -0
- package/lib/types/domain/migrate-v3.js +68 -0
- package/lib/types/domain/row-tx.d.ts +27 -0
- package/lib/types/domain/row-tx.js +31 -0
- package/lib/types/domain/row-v3-bridge.d.ts +6 -0
- package/lib/types/domain/row-v3-bridge.js +47 -0
- package/lib/types/domain/row-v3.d.ts +113 -0
- package/lib/types/domain/row-v3.js +107 -0
- package/lib/types/domain/row.d.ts +1 -0
- package/lib/types/domain/staging-index.d.ts +14 -0
- package/lib/types/domain/staging-index.js +41 -0
- package/lib/types/domain/state-v3.d.ts +15 -0
- package/lib/types/domain/state-v3.js +80 -0
- package/lib/types/facts.d.ts +24 -0
- package/lib/types/facts.js +68 -0
- package/lib/types/gate-filter.d.ts +31 -0
- package/lib/types/gate-filter.js +64 -0
- package/lib/types/index.d.ts +5 -2
- package/lib/types/index.js +16 -26
- package/lib/types/ladder.js +1 -1
- package/lib/types/merge-routes.d.ts +2 -2
- package/lib/types/merge-routes.js +21 -22
- package/lib/types/metrics.d.ts +16 -0
- package/lib/types/metrics.js +27 -0
- package/lib/types/new-child-authoring.d.ts +3 -1
- package/lib/types/new-child-authoring.js +14 -3
- package/lib/types/pending-actions.d.ts +2 -2
- package/lib/types/pending-actions.js +17 -17
- package/lib/types/propose.d.ts +26 -0
- package/lib/types/propose.js +69 -0
- package/lib/types/read-commands.d.ts +2 -2
- package/lib/types/read-commands.js +5 -3
- package/lib/types/resume-command.d.ts +2 -0
- package/lib/types/resume-command.js +4 -1
- package/lib/types/round2-outcome.js +17 -9
- package/lib/types/runtime.d.ts +4 -1
- package/lib/types/runtime.js +99 -29
- package/lib/types/scan-ports.d.ts +3 -1
- package/lib/types/scan-ports.js +2 -0
- package/lib/types/scan-run.js +114 -51
- package/lib/types/screening-port.d.ts +47 -0
- package/lib/types/screening-port.js +93 -0
- package/lib/types/screening.d.ts +30 -0
- package/lib/types/screening.js +75 -0
- package/lib/types/state.d.ts +14 -0
- package/lib/types/state.js +70 -1
- package/lib/types/status.d.ts +3 -0
- package/lib/types/status.js +17 -0
- package/lib/types/target-claim.d.ts +1 -0
- package/lib/types/target-claim.js +63 -18
- package/lib/types/unit-claim.d.ts +5 -0
- package/lib/types/unit-claim.js +15 -0
- package/lib/types/wire-projection.d.ts +36 -0
- package/lib/types/wire-projection.js +69 -0
- package/lib/types/wire.d.ts +254 -0
- package/lib/types/wire.js +3 -0
- package/package.json +8 -4
- package/lib/types/routes.d.ts +0 -70
- package/lib/types/routes.js +0 -78
package/lib/types/scan-run.js
CHANGED
|
@@ -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 {
|
|
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
|
|
67
|
-
|
|
68
|
-
|
|
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
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
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
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
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
|
-
|
|
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
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
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
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
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
|
|
146
|
-
|
|
147
|
-
|
|
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 =
|
|
152
|
-
|
|
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
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
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
|
|
173
|
-
if (
|
|
174
|
-
|
|
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
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
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
|
package/lib/types/state.d.ts
CHANGED
|
@@ -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
|
package/lib/types/state.js
CHANGED
|
@@ -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
|
-
|
|
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);
|
package/lib/types/status.d.ts
CHANGED
|
@@ -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
|