@lmzhen/dsh-evolution-merge 0.28.0 → 0.30.0

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 (115) hide show
  1. package/README.md +21 -18
  2. package/lib/index.js +1583 -1209
  3. package/lib/types/adapters/channel.js +29 -0
  4. package/lib/types/adapters/engine-screen.d.ts +55 -0
  5. package/lib/types/adapters/engine-screen.js +45 -0
  6. package/lib/types/adapters/library.js +14 -0
  7. package/lib/types/adapters/model-runner.d.ts +28 -0
  8. package/lib/types/adapters/model-runner.js +34 -0
  9. package/lib/types/adapters/select.d.ts +3 -0
  10. package/lib/types/adapters/select.js +35 -0
  11. package/lib/types/adapters/undo.js +35 -0
  12. package/lib/types/approve-command.js +42 -0
  13. package/lib/types/authoring-flow.d.ts +7 -2
  14. package/lib/types/authoring-flow.js +48 -0
  15. package/lib/types/channel-port.d.ts +2 -1
  16. package/lib/types/channel-port.js +74 -0
  17. package/lib/types/commands.d.ts +9 -0
  18. package/lib/types/commands.js +79 -0
  19. package/lib/types/commit-assembly.d.ts +2 -0
  20. package/lib/types/commit-assembly.js +48 -0
  21. package/lib/types/commit-execute.js +52 -0
  22. package/lib/types/commit-plan.js +56 -0
  23. package/lib/types/commit-receipt.d.ts +6 -1
  24. package/lib/types/commit-receipt.js +85 -0
  25. package/lib/types/contracts.d.ts +21 -7
  26. package/lib/types/contracts.js +57 -0
  27. package/lib/types/coordinator.d.ts +11 -0
  28. package/lib/types/coordinator.js +131 -0
  29. package/lib/types/decision.js +61 -0
  30. package/lib/types/doctor.d.ts +1 -1
  31. package/lib/types/doctor.js +50 -0
  32. package/lib/types/domain/claim.d.ts +10 -0
  33. package/lib/types/domain/claim.js +12 -0
  34. package/lib/types/domain/commit-journal-store.d.ts +6 -0
  35. package/lib/types/domain/commit-journal-store.js +22 -0
  36. package/lib/types/domain/commit-journal.d.ts +16 -0
  37. package/lib/types/domain/commit-journal.js +25 -0
  38. package/lib/types/domain/errors.d.ts +10 -0
  39. package/lib/types/domain/errors.js +7 -0
  40. package/lib/types/domain/failure-route.d.ts +25 -0
  41. package/lib/types/domain/failure-route.js +22 -0
  42. package/lib/types/domain/indexes.d.ts +21 -0
  43. package/lib/types/domain/indexes.js +15 -0
  44. package/lib/types/domain/ledger.d.ts +17 -0
  45. package/lib/types/domain/ledger.js +65 -0
  46. package/lib/types/domain/markdown.d.ts +2 -0
  47. package/lib/types/domain/markdown.js +57 -0
  48. package/lib/types/domain/migrate-v1.d.ts +41 -0
  49. package/lib/types/domain/migrate-v1.js +23 -0
  50. package/lib/types/domain/pair-index-store.d.ts +6 -0
  51. package/lib/types/domain/pair-index-store.js +25 -0
  52. package/lib/types/domain/pair-index.d.ts +9 -0
  53. package/lib/types/domain/pair-index.js +28 -0
  54. package/lib/types/domain/resolve-pair.d.ts +11 -0
  55. package/lib/types/domain/resolve-pair.js +17 -0
  56. package/lib/types/domain/row.d.ts +113 -0
  57. package/lib/types/domain/row.js +111 -0
  58. package/lib/types/domain/run-record.d.ts +45 -0
  59. package/lib/types/domain/run-record.js +24 -0
  60. package/lib/types/domain/state.d.ts +57 -0
  61. package/lib/types/domain/state.js +65 -0
  62. package/lib/types/gate-cache.js +36 -0
  63. package/lib/types/index.d.ts +6 -7
  64. package/lib/types/index.js +241 -0
  65. package/lib/types/ladder.js +162 -0
  66. package/lib/types/merge-command.d.ts +3 -0
  67. package/lib/types/merge-command.js +43 -0
  68. package/lib/types/merge-routes.d.ts +15 -4
  69. package/lib/types/merge-routes.js +99 -0
  70. package/lib/types/merge-tool.js +69 -0
  71. package/lib/types/new-child-authoring.d.ts +1 -1
  72. package/lib/types/new-child-authoring.js +83 -0
  73. package/lib/types/pair-command.js +18 -0
  74. package/lib/types/params.d.ts +1 -0
  75. package/lib/types/params.js +49 -0
  76. package/lib/types/pending-actions.js +28 -0
  77. package/lib/types/prompts.d.ts +7 -5
  78. package/lib/types/prompts.js +60 -0
  79. package/lib/types/read-commands.js +32 -0
  80. package/lib/types/report.js +70 -0
  81. package/lib/types/resume-command.js +57 -0
  82. package/lib/types/round2-outcome.js +50 -0
  83. package/lib/types/routes.d.ts +20 -2
  84. package/lib/types/routes.js +75 -0
  85. package/lib/types/rules.js +55 -0
  86. package/lib/types/run-shell.d.ts +7 -0
  87. package/lib/types/run-shell.js +69 -0
  88. package/lib/types/runtime.d.ts +5 -4
  89. package/lib/types/runtime.js +269 -0
  90. package/lib/types/scan-command.d.ts +1 -0
  91. package/lib/types/scan-command.js +30 -0
  92. package/lib/types/scan-ports.d.ts +1 -1
  93. package/lib/types/scan-ports.js +93 -0
  94. package/lib/types/scan-run.d.ts +4 -0
  95. package/lib/types/scan-run.js +186 -0
  96. package/lib/types/skill-card.d.ts +12 -0
  97. package/lib/types/skill-card.js +2 -0
  98. package/lib/types/skipped.js +24 -0
  99. package/lib/types/stage-tool-registry.js +32 -0
  100. package/lib/types/stage-tool.js +31 -0
  101. package/lib/types/staged-tree.js +75 -0
  102. package/lib/types/staging-files.js +86 -0
  103. package/lib/types/staging.d.ts +13 -0
  104. package/lib/types/staging.js +149 -0
  105. package/lib/types/state.js +103 -0
  106. package/lib/types/status.d.ts +2 -1
  107. package/lib/types/status.js +28 -0
  108. package/lib/types/supersede.js +14 -0
  109. package/lib/types/undo-merge.js +23 -0
  110. package/package.json +4 -7
  111. package/lib/types/adapters/model.d.ts +0 -31
  112. package/lib/types/gate-filter.d.ts +0 -31
  113. package/lib/types/propose.d.ts +0 -26
  114. package/lib/types/screening-port.d.ts +0 -47
  115. package/lib/types/screening.d.ts +0 -30
@@ -0,0 +1,241 @@
1
+ import { join } from 'node:path';
2
+ import z from '@deepseek-ai/schemastery';
3
+ import { screen as engineScreen } from '@lmzhen/dsh-evolution-merge-engine';
4
+ import { selectWithErrors } from "./adapters/select.js";
5
+ import { createEngineScreening } from "./adapters/engine-screen.js";
6
+ import { createEngineRunner } from "./adapters/model-runner.js";
7
+ import { authorityFrom, createSubagentChannel, DEFAULT_SKILL_LIMITS, evolutionHome, evolutionIoAdapter, newRunRegistry, newSkillLibrary, policyStageLimits } from '@lmzhen/dsh-evolution-core';
8
+ import { registerHostRoutes } from '@lmzhen/dsh-evolution-core/http-routes';
9
+ import { loadMergeState, upsertRow } from "./state.js";
10
+ import { cleanupExpiredStaging } from "./staging.js";
11
+ import { readCommitReceipts } from "./commit-receipt.js";
12
+ import { transition } from "./coordinator.js";
13
+ import { makeMergeRoutes } from "./merge-routes.js";
14
+ import { createMergeCommand, runMergeCommand } from "./merge-command.js";
15
+ import { createMergeRuntime } from "./runtime.js";
16
+ import { resolveMergeParams } from "./params.js";
17
+ import { subagentChannelAdapter } from "./adapters/channel.js";
18
+ import { StageToolRegistry } from "./stage-tool-registry.js";
19
+ import { createMergeStageTool } from "./merge-tool.js";
20
+ import { supersedeMergeRuns } from "./supersede.js";
21
+ /** E3 knobs are volatile (user layer); the E2 pair timeout is deployment-only. */
22
+ export const Config = z.object({
23
+ mergeApproval: z.boolean().default(false).volatile(),
24
+ mergeSkippedTtlDays: z.number().min(0).default(0).volatile(),
25
+ mergeRunTimeoutMinutes: z.number().min(0).default(0).volatile(),
26
+ mergePairTimeoutMinutes: z.number().min(1).default(15),
27
+ mergeModelsRoot: z.string().default(''),
28
+ });
29
+ export function engineConfigFromEnv(configuredRoot) {
30
+ const root = configuredRoot !== undefined && configuredRoot !== '' ? configuredRoot : process.env.SKILL_MERGE_MODELS_ROOT;
31
+ if (root === undefined || root === '')
32
+ return undefined;
33
+ return {
34
+ models_dir: '',
35
+ offline: true,
36
+ auto_provision: false,
37
+ embed_backend: 'local',
38
+ embedder_model: join(root, 'potion-multilingual-128M-int8'),
39
+ embedder_model_last: join(root, 'potion-multilingual-128M-pca64-int8'),
40
+ };
41
+ }
42
+ /** Suppress the platform auto form; the registry renders the merge card instead. */
43
+ export function configureMergePresentation(ctx) {
44
+ ctx.inject(['settings'], (scope) => {
45
+ const settings = scope.settings;
46
+ if (typeof settings?.configure !== 'function')
47
+ return;
48
+ const disposer = settings.configure({ auto: false }, ctx.fiber);
49
+ if (typeof disposer === 'function')
50
+ scope.effect(() => disposer, 'evolution-merge: settings presentation');
51
+ });
52
+ }
53
+ export * from "./contracts.js";
54
+ export * from "./skill-card.js";
55
+ export * from "./state.js";
56
+ export * from "./coordinator.js";
57
+ export * from "./report.js";
58
+ export * from "./staging.js";
59
+ export * from "./staging-files.js";
60
+ export * from "./rules.js";
61
+ export * from "./commit-plan.js";
62
+ export * from "./stage-tool.js";
63
+ export * from "./stage-tool-registry.js";
64
+ export * from "./staged-tree.js";
65
+ export * from "./commit-assembly.js";
66
+ export * from "./commit-receipt.js";
67
+ export * from "./commit-execute.js";
68
+ export * from "./undo-merge.js";
69
+ export * from "./adapters/library.js";
70
+ export * from "./adapters/model-runner.js";
71
+ export * from "./adapters/channel.js";
72
+ export * from "./adapters/engine-screen.js";
73
+ export * from "./adapters/undo.js";
74
+ export * from "./run-shell.js";
75
+ export * from "./scan-run.js";
76
+ export * from "./pending-actions.js";
77
+ export * from "./scan-command.js";
78
+ export * from "./round2-outcome.js";
79
+ export * from "./approve-command.js";
80
+ export * from "./pair-command.js";
81
+ export * from "./resume-command.js";
82
+ export * from "./decision.js";
83
+ export * from "./prompts.js";
84
+ export * from "./channel-port.js";
85
+ export * from "./authoring-flow.js";
86
+ export * from "./merge-tool.js";
87
+ export * from "./commands.js";
88
+ export * from "./scan-ports.js";
89
+ export * from "./new-child-authoring.js";
90
+ export * from "./status.js";
91
+ export * from "./skipped.js";
92
+ export * from "./read-commands.js";
93
+ export * from "./routes.js";
94
+ export * from "./merge-routes.js";
95
+ export * from "./merge-command.js";
96
+ export * from "./params.js";
97
+ export * from "./doctor.js";
98
+ export * from "./supersede.js";
99
+ export * from "./runtime.js";
100
+ export const inject = [];
101
+ export function apply(ctx, rawConfig = {}) {
102
+ configureMergePresentation(ctx);
103
+ const params = resolveMergeParams(rawConfig);
104
+ let mergeRun;
105
+ const mergeService = {
106
+ async run(rawInput, invocation) {
107
+ return mergeRun === undefined ? { ok: false, text: 'merge 未就绪:evolutionIo 尚未挂载' } : mergeRun(rawInput, invocation);
108
+ },
109
+ };
110
+ const mergeContext = ctx;
111
+ if (typeof mergeContext.provide === 'function') {
112
+ ;
113
+ mergeContext.provide('evolutionMerge', mergeService);
114
+ }
115
+ let source;
116
+ registerHostRoutes(ctx, audit => makeMergeRoutes({ getSource: () => source }, audit));
117
+ ctx.inject(['commands', 'evolutionIo'], (scope) => {
118
+ const registry = scope.get('evolutionIo');
119
+ if (registry === undefined)
120
+ return;
121
+ const io = evolutionIoAdapter(() => registry.provider());
122
+ const home = evolutionHome();
123
+ void loadMergeState(io, home).then(loaded => cleanupExpiredStaging(io, home, Date.now(), new Set(loaded.state.rows.map(row => row.runId).filter((id) => id !== undefined)))).catch((error) => { ctx.logger.warn('merge staging cleanup failed: ' + (error instanceof Error ? error.message : String(error))); });
124
+ const runs = newRunRegistry({ io, home, warn: (message) => { ctx.logger.warn(message); } });
125
+ void runs.load();
126
+ ctx.effect(() => {
127
+ const host = ctx;
128
+ if (typeof host.on !== 'function')
129
+ return () => { };
130
+ return host.on('evolution/skill-mutated', (event) => {
131
+ if (event.action === 'restore')
132
+ void supersedeMergeRuns(runs);
133
+ });
134
+ }, 'evolution-merge: restore supersede');
135
+ const policySnapshot = ctx.get('evolutionPolicy');
136
+ const skills = newSkillLibrary({
137
+ io, ctx, authority: authorityFrom({ origin: 'subagent' }),
138
+ limits: { ...DEFAULT_SKILL_LIMITS, ...policyStageLimits(policySnapshot?.get?.()) },
139
+ });
140
+ const channel = subagentChannelAdapter(createSubagentChannel(ctx));
141
+ const stage = new StageToolRegistry();
142
+ const engineConfig = engineConfigFromEnv(params.mergeModelsRoot);
143
+ const llm = ctx.get('llm');
144
+ const defaultModel = ctx.get('agentDefaultModel');
145
+ const runner = llm === undefined || defaultModel === undefined
146
+ ? undefined
147
+ : createEngineRunner(llm, () => defaultModel.currentSelection());
148
+ const engineScreening = createEngineScreening({
149
+ io,
150
+ home,
151
+ engine: { screen: engineScreen },
152
+ select: selectWithErrors,
153
+ cfg: { ...(engineConfig ?? {}), judge: 'deepseek' },
154
+ ...(runner === undefined ? {} : { runner }),
155
+ async onSelection(selection) {
156
+ const loaded = await loadMergeState(io, home);
157
+ const at = new Date().toISOString();
158
+ for (const skipped of selection.skipped) {
159
+ if (loaded.state.rows.some(row => row.pairKey === skipped.pairKey))
160
+ continue;
161
+ const next = transition(undefined, {
162
+ type: 'gate-keep', a: skipped.a, b: skipped.b,
163
+ ...(skipped.reason === undefined ? {} : { reason: skipped.reason }), at,
164
+ });
165
+ if (next.ok && next.row !== null)
166
+ await upsertRow(io, home, () => Date.now(), next.row);
167
+ }
168
+ },
169
+ });
170
+ source = {
171
+ rows() { return loadMergeState(io, home).then(loaded => loaded.state.rows); },
172
+ run(id) { return Promise.resolve(runs.runs().find(record => record.id === id)); },
173
+ runs() { return Promise.resolve(runs.runs()); },
174
+ async body(id, side) {
175
+ const row = (await loadMergeState(io, home)).state.rows.find(item => item.id === id);
176
+ if (row === undefined)
177
+ return null;
178
+ return await skills.read(side === 'a' ? row.a : row.b);
179
+ },
180
+ async history() {
181
+ const receipts = await readCommitReceipts(io, home);
182
+ const items = new Map();
183
+ for (const receipt of receipts) {
184
+ for (const source of receipt.sources) {
185
+ const key = source + ':' + receipt.runId;
186
+ if (items.has(key))
187
+ continue;
188
+ items.set(key, { name: source, kind: 'merged', reason: 'Merged into ' + receipt.target, lastVersion: 1, archivedAt: new Date(receipt.at).toISOString(), sourceRunId: receipt.runId });
189
+ }
190
+ }
191
+ const archived = await skills.archivedNames?.() ?? [];
192
+ const seen = new Set([...items.values()].map(item => item.name));
193
+ for (const name of archived) {
194
+ if (seen.has(name))
195
+ continue;
196
+ items.set('archived:' + name, { name, kind: 'other', reason: 'archived', lastVersion: 1, archivedAt: new Date(Date.now()).toISOString() });
197
+ }
198
+ return [...items.values()];
199
+ },
200
+ approval() { return params.mergeApproval; },
201
+ commands() {
202
+ return ['merge scan', 'merge doctor', 'merge status', 'merge pending', 'merge skipped', 'merge report', 'merge runs', 'merge cancel'];
203
+ },
204
+ };
205
+ const commands = scope.get('commands');
206
+ if (commands === undefined)
207
+ return;
208
+ const runtime = createMergeRuntime({
209
+ io, home, registry: runs, skills, now: () => Date.now(), params, platformWrite: true,
210
+ channel, parent: undefined, allow: ['merge_stage'], timeoutMinutes: params.mergeRunTimeoutMinutes,
211
+ async cards(signal) {
212
+ const listed = await skills.list({ withContent: true });
213
+ if (signal.aborted)
214
+ return [];
215
+ return Promise.all(listed.map(async (item) => {
216
+ const probe = await skills.listSupportFiles(item.name);
217
+ const paths = probe.kind === 'present' ? probe.value : [];
218
+ const files = await Promise.all(paths.map(async (path) => ({ path, content: await skills.readSupportFile(item.name, path) ?? '' })));
219
+ return { name: item.name, description: item.description, body: item.content ?? '', files, protectedBy: item.protectedBy, protectionUnknown: item.protectionUnknown };
220
+ }));
221
+ },
222
+ screeningForRun: engineScreening,
223
+ stage: {
224
+ bind: (context) => { stage.bind(context.childId, context); },
225
+ unbind: (childId) => { stage.unbind(childId); },
226
+ },
227
+ });
228
+ mergeRun = (rawInput, invocation) => runMergeCommand(runtime, rawInput, invocation);
229
+ const command = createMergeCommand(runtime);
230
+ scope.effect(() => commands.register(command), 'evolution-merge: command');
231
+ ctx.inject(['tools'], (toolScope) => {
232
+ const tools = toolScope.get('tools');
233
+ if (tools === undefined)
234
+ return;
235
+ const tool = createMergeStageTool(io, home, exec => stage.resolve(exec));
236
+ toolScope.effect(() => tools.register(tool), 'evolution-merge: merge_stage');
237
+ });
238
+ scope.effect(() => () => { runs.cancelAll(); }, 'evolution-merge: runs');
239
+ });
240
+ }
241
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1,162 @@
1
+ /**
2
+ * L0–L2 candidate gate. This is a CANDIDATE classifier, not a content quality judge.
3
+ * Routing follows the frozen table: L0 -1 stops; L0 1/0 → L1; L1 -1 stops; L1 0/1 → L2;
4
+ * L2 1 → agent1; L2 0 → agent1 (undecided); L2 -1 → skipped.
5
+ */
6
+ import { createHash } from 'node:crypto';
7
+ export class GateCache {
8
+ entries = new Map();
9
+ get(key) { return this.entries.get(key); }
10
+ put(key, value) { this.entries.set(key, value); }
11
+ get size() { return this.entries.size; }
12
+ toObject() { return Object.fromEntries(this.entries); }
13
+ static fromObject(value) {
14
+ const cache = new GateCache();
15
+ if (typeof value !== 'object' || value === null)
16
+ return cache;
17
+ for (const [key, entry] of Object.entries(value)) {
18
+ if (typeof entry !== 'object' || entry === null)
19
+ continue;
20
+ const verdict = entry;
21
+ if (verdict.verdict === 1 || verdict.verdict === 0 || verdict.verdict === -1) {
22
+ cache.put(key, { level: verdict.level === 'L0' || verdict.level === 'L1' || verdict.level === 'L2' ? verdict.level : 'L2', verdict: verdict.verdict, reason: typeof verdict.reason === 'string' ? verdict.reason : '', evidence: Array.isArray(verdict.evidence) ? verdict.evidence : [], cached: true });
23
+ }
24
+ }
25
+ return cache;
26
+ }
27
+ }
28
+ export function gateCacheKey(level, payload, model) {
29
+ return createHash('sha256').update(JSON.stringify({ level, payload, model })).digest('hex');
30
+ }
31
+ function payloadFor(level, candidate, facts) {
32
+ const a = facts.get(candidate.a);
33
+ const b = facts.get(candidate.b);
34
+ if (a === undefined || b === undefined)
35
+ throw new Error('candidate names are not in the fact set');
36
+ const base = {
37
+ level,
38
+ skill_a: { name: a.name, description: a.description },
39
+ skill_b: { name: b.name, description: b.description },
40
+ scores: { jaccard_summary: candidate.score ?? null },
41
+ schema: { verdict: '1 | 0 | -1', reason: '<=80 chars' },
42
+ };
43
+ if (level === 'L0')
44
+ return JSON.stringify(base);
45
+ const withHead = {
46
+ ...base,
47
+ skill_a: { ...base.skill_a, head: a.body.slice(0, 2400) },
48
+ skill_b: { ...base.skill_b, head: b.body.slice(0, 2400) },
49
+ };
50
+ if (level === 'L1')
51
+ return JSON.stringify(withHead);
52
+ return JSON.stringify({
53
+ ...withHead,
54
+ skill_a: { ...withHead.skill_a, body_excerpt: a.body.slice(0, 4000) },
55
+ skill_b: { ...withHead.skill_b, body_excerpt: b.body.slice(0, 4000) },
56
+ });
57
+ }
58
+ const SYSTEM = 'Judge whether two skill documents describe the same reusable capability. Shared vocabulary or a shared preamble is not duplication. Return only strict JSON: {"verdict":1|0|-1,"reason":"..."}';
59
+ function extractJson(text) {
60
+ const fenced = /\`\`\`(?:json)?\s*([\s\S]*?)\`\`\`/i.exec(text);
61
+ const body = fenced?.[1] ?? text;
62
+ const start = body.indexOf('{');
63
+ if (start < 0)
64
+ return undefined;
65
+ let depth = 0;
66
+ for (let index = start; index < body.length; index += 1) {
67
+ if (body[index] === '{')
68
+ depth += 1;
69
+ else if (body[index] === '}') {
70
+ depth -= 1;
71
+ if (depth === 0) {
72
+ try {
73
+ return JSON.parse(body.slice(start, index + 1));
74
+ }
75
+ catch {
76
+ return undefined;
77
+ }
78
+ }
79
+ }
80
+ }
81
+ return undefined;
82
+ }
83
+ export function parseVerdict(level, raw, cached = false) {
84
+ const value = typeof raw === 'string' ? extractJson(raw) : raw;
85
+ if (typeof value !== 'object' || value === null)
86
+ return undefined;
87
+ const record = value;
88
+ const verdict = record.verdict === 1 || record.verdict === '1' ? 1
89
+ : record.verdict === 0 || record.verdict === '0' ? 0
90
+ : record.verdict === -1 || record.verdict === '-1' ? -1
91
+ : undefined;
92
+ if (verdict === undefined)
93
+ return undefined;
94
+ const reason = typeof record.reason === 'string' ? record.reason.slice(0, 80) : '';
95
+ const evidence = Array.isArray(record.evidence)
96
+ ? record.evidence
97
+ .filter(item => typeof item === 'object' && item !== null)
98
+ .map((item) => {
99
+ const entry = item;
100
+ return { path: typeof entry.path === 'string' ? entry.path : '', line: typeof entry.line === 'number' ? entry.line : -1, quote: typeof entry.quote === 'string' ? entry.quote.slice(0, 120) : '' };
101
+ })
102
+ : [];
103
+ return { level, verdict, reason, evidence, cached };
104
+ }
105
+ /** Deterministic 10% L0 audit selection from the run seed + pair key. */
106
+ export function selectedForAudit(pairKey, seed, rate = 0.1) {
107
+ const digest = createHash('sha256').update(seed + '\u0000' + pairKey).digest();
108
+ return digest[0] !== undefined && digest[0] < Math.round(Math.max(0, Math.min(1, rate)) * 256);
109
+ }
110
+ export async function runLadder(options) {
111
+ const facts = new Map(options.facts.map(fact => [fact.name, fact]));
112
+ const verdicts = [];
113
+ let calls = 0;
114
+ let level = options.startLevel ?? 'L0';
115
+ for (;;) {
116
+ let payload;
117
+ try {
118
+ payload = payloadFor(level, options.candidate, facts);
119
+ }
120
+ catch (error) {
121
+ return { route: 'error', verdicts, calls, error: error instanceof Error ? error.message : String(error) };
122
+ }
123
+ const key = gateCacheKey(level, payload, options.modelName);
124
+ const cached = options.cache?.get(key);
125
+ let verdict;
126
+ if (cached !== undefined)
127
+ verdict = { ...cached, level, cached: true };
128
+ else {
129
+ try {
130
+ calls += 1;
131
+ const raw = await options.model.callJson({ system: SYSTEM, user: payload, maxTokens: 500, signal: options.signal });
132
+ verdict = parseVerdict(level, raw);
133
+ if (verdict !== undefined)
134
+ options.cache?.put(key, verdict);
135
+ }
136
+ catch (error) {
137
+ return { route: 'error', verdicts, calls, error: error instanceof Error ? error.message : String(error) };
138
+ }
139
+ }
140
+ if (verdict === undefined)
141
+ return { route: 'error', verdicts, calls, error: 'invalid gate JSON' };
142
+ verdicts.push(verdict);
143
+ if (level === 'L0') {
144
+ if (verdict.verdict === -1)
145
+ return { route: 'keep', verdicts, calls, droppedAt: 'L0' };
146
+ level = 'L1';
147
+ continue;
148
+ }
149
+ if (level === 'L1') {
150
+ if (verdict.verdict === -1)
151
+ return { route: 'keep', verdicts, calls, droppedAt: 'L1' };
152
+ level = 'L2';
153
+ continue;
154
+ }
155
+ if (verdict.verdict === 1)
156
+ return { route: 'merge', verdicts, calls };
157
+ if (verdict.verdict === 0)
158
+ return { route: 'undecided', verdicts, calls };
159
+ return { route: 'keep', verdicts, calls, droppedAt: 'L2' };
160
+ }
161
+ }
162
+ //# sourceMappingURL=ladder.js.map
@@ -32,5 +32,8 @@ export interface MergeCommandDefinitionLike {
32
32
  readonly handler: (invocation: CommandInvocationLike) => Promise<CommandResultLike>;
33
33
  }
34
34
  export declare function createReadOnlyMergeService(io: EvolutionIoLike, home: string): MergeCommandService;
35
+ export declare function runMergeCommand(service: MergeCommandService, rawInput: string, invocation?: {
36
+ readonly agent?: unknown;
37
+ }): Promise<MergeCommandResult>;
35
38
  export declare function createMergeCommand(service: MergeCommandService): MergeCommandDefinitionLike;
36
39
  //# sourceMappingURL=merge-command.d.ts.map
@@ -0,0 +1,43 @@
1
+ import { parseMergeCommand, mergeUsage } from "./commands.js";
2
+ import { handleReadOnlyMergeCommand } from "./read-commands.js";
3
+ import { formatMergeError } from "./domain/errors.js";
4
+ export function createReadOnlyMergeService(io, home) {
5
+ return {
6
+ async execute(command) {
7
+ const text = await handleReadOnlyMergeCommand(command, io, home);
8
+ return text === null
9
+ ? { ok: false, text: 'merge ' + command.verb + ' 需要运行引擎(当前只挂载了只读命令)' }
10
+ : { ok: true, text };
11
+ },
12
+ };
13
+ }
14
+ export async function runMergeCommand(service, rawInput, invocation) {
15
+ const tokens = rawInput.trim().split(/\s+/).filter(Boolean);
16
+ if (tokens[0] === 'merge' || tokens[0] === '/merge' || tokens[0] === 'evolution' || tokens[0] === '/evolution')
17
+ tokens.shift();
18
+ if (tokens[0] === 'merge')
19
+ tokens.shift();
20
+ const parsed = parseMergeCommand(tokens);
21
+ if (!parsed.ok)
22
+ return { ok: false, text: parsed.message };
23
+ if (parsed.help === true)
24
+ return { ok: true, text: mergeUsage() };
25
+ try {
26
+ return await service.execute(parsed.command, invocation?.agent === undefined ? {} : { parent: invocation.agent });
27
+ }
28
+ catch (error) {
29
+ return { ok: false, text: formatMergeError({ kind: 'runtime', code: 'merge-command-failed', message: error instanceof Error ? error.message : String(error) }) };
30
+ }
31
+ }
32
+ export function createMergeCommand(service) {
33
+ return {
34
+ name: 'merge',
35
+ description: '技能自动合并:扫描、待批准、重跑、报告与 undo(兼容别名;请用 /evolution merge)',
36
+ input: { hint: mergeUsage() },
37
+ async handler(invocation) {
38
+ const result = await runMergeCommand(service, invocation.rawInput, invocation);
39
+ return { kind: result.ok ? 'success' : 'error', text: result.text };
40
+ },
41
+ };
42
+ }
43
+ //# sourceMappingURL=merge-command.js.map
@@ -6,16 +6,27 @@ import type { RunRecord } from '@lmzhen/dsh-evolution-core';
6
6
  import { type ConnectionFetchRouteLike, type DecisionAudit } from '@lmzhen/dsh-evolution-core/http-routes';
7
7
  import type { MergeRow } from './contracts.ts';
8
8
  export declare const MERGE_ROUTES: {
9
- readonly view: "/api/dsh-evolution/merge/view";
10
- readonly rows: "/api/dsh-evolution/merge/rows";
11
- readonly run: "/api/dsh-evolution/merge/run";
12
- readonly runs: "/api/dsh-evolution/merge/runs";
9
+ view: `/api/dsh-evolution/${string}`;
10
+ rows: `/api/dsh-evolution/${string}`;
11
+ body: `/api/dsh-evolution/${string}`;
12
+ history: `/api/dsh-evolution/${string}`;
13
13
  };
14
+ export interface MergeHistoryItem {
15
+ readonly name: string;
16
+ readonly kind: 'merged' | 'deleted' | 'split' | 'other';
17
+ readonly reason: string;
18
+ readonly lastVersion: number;
19
+ readonly archivedAt: string;
20
+ readonly sourceRunId?: string;
21
+ }
14
22
  export interface MergeRouteSource {
15
23
  rows(): Promise<readonly MergeRow[]>;
16
24
  run(id: string): Promise<RunRecord | undefined>;
17
25
  runs(cursor?: string, limit?: number): Promise<readonly RunRecord[]>;
26
+ body(id: string, side: 'a' | 'b'): Promise<string | null>;
27
+ history(): Promise<readonly MergeHistoryItem[]>;
18
28
  commands(): readonly string[];
29
+ approval?(): boolean;
19
30
  }
20
31
  export declare function makeMergeRoutes(deps: {
21
32
  getSource: () => MergeRouteSource | undefined;
@@ -0,0 +1,99 @@
1
+ import { HOST_ROUTES, json, methodNotAllowed } from '@lmzhen/dsh-evolution-core/http-routes';
2
+ import { buildMergeView, pageQueue, toRunFact } from "./routes.js";
3
+ import { renderMarkdown } from "./domain/markdown.js";
4
+ const MERGE_PATHS = HOST_ROUTES.merge;
5
+ const MERGE_BASE = MERGE_PATHS.view.slice(0, -'view'.length);
6
+ export const MERGE_ROUTES = {
7
+ view: MERGE_PATHS.view,
8
+ rows: MERGE_PATHS.rows,
9
+ body: (MERGE_PATHS.body ?? MERGE_BASE + 'body'),
10
+ history: (MERGE_PATHS.history ?? MERGE_BASE + 'history'),
11
+ };
12
+ export function makeMergeRoutes(deps, _audit) {
13
+ const absent = () => json(503, { ok: false, error: { code: 'merge-service-absent', message: 'merge service is not mounted' } });
14
+ const get = (request) => (request.method === 'GET' ? null : methodNotAllowed(['GET']));
15
+ const url = (request) => new URL(request.url, 'http://local');
16
+ return [
17
+ {
18
+ path: MERGE_ROUTES.view,
19
+ methods: ['GET'],
20
+ requestBody: 'buffered',
21
+ fetch: async (request) => {
22
+ const denied = get(request);
23
+ if (denied !== null)
24
+ return denied;
25
+ const source = deps.getSource();
26
+ if (source === undefined)
27
+ return absent();
28
+ const runs = await source.runs(undefined, 20);
29
+ const latestMerge = runs.find(run => run.kind === 'merge');
30
+ return json(200, {
31
+ ok: true,
32
+ data: buildMergeView(await source.rows(), source.commands(), latestMerge === undefined ? undefined : toRunFact(latestMerge), source.approval?.()),
33
+ });
34
+ },
35
+ },
36
+ {
37
+ path: MERGE_ROUTES.rows,
38
+ methods: ['GET'],
39
+ requestBody: 'buffered',
40
+ fetch: async (request) => {
41
+ const denied = get(request);
42
+ if (denied !== null)
43
+ return denied;
44
+ const source = deps.getSource();
45
+ if (source === undefined)
46
+ return absent();
47
+ const query = url(request).searchParams;
48
+ const queue = query.get('queue') ?? 'verify';
49
+ if (queue !== 'verify' && queue !== 'pending' && queue !== 'fuse' && queue !== 'skipped') {
50
+ return json(400, { ok: false, error: { code: 'bad-queue', message: 'queue must be verify/pending/fuse/skipped' } });
51
+ }
52
+ const cursor = Number.parseInt(query.get('cursor') ?? '0', 10);
53
+ const limit = Number.parseInt(query.get('limit') ?? '50', 10);
54
+ return json(200, {
55
+ ok: true,
56
+ data: pageQueue(await source.rows(), queue, Number.isFinite(cursor) ? cursor : 0, Number.isFinite(limit) ? limit : 50),
57
+ });
58
+ },
59
+ },
60
+ {
61
+ path: MERGE_ROUTES.body,
62
+ methods: ['GET'],
63
+ requestBody: 'buffered',
64
+ fetch: async (request) => {
65
+ const denied = get(request);
66
+ if (denied !== null)
67
+ return denied;
68
+ const source = deps.getSource();
69
+ if (source === undefined)
70
+ return absent();
71
+ const query = url(request).searchParams;
72
+ const id = query.get('id');
73
+ const side = query.get('side');
74
+ if (id === null || id === '' || (side !== 'a' && side !== 'b')) {
75
+ return json(400, { ok: false, error: { code: 'bad-body-query', message: 'id and side=a|b are required' } });
76
+ }
77
+ const content = await source.body(id, side);
78
+ return content === null
79
+ ? json(404, { ok: false, error: { code: 'body-not-found', message: 'merge body not found: ' + id } })
80
+ : json(200, { ok: true, data: { content, markdown: true, html: renderMarkdown(content) } });
81
+ },
82
+ },
83
+ {
84
+ path: MERGE_ROUTES.history,
85
+ methods: ['GET'],
86
+ requestBody: 'buffered',
87
+ fetch: async (request) => {
88
+ const denied = get(request);
89
+ if (denied !== null)
90
+ return denied;
91
+ const source = deps.getSource();
92
+ if (source === undefined)
93
+ return absent();
94
+ return json(200, { ok: true, data: { items: await source.history() } });
95
+ },
96
+ },
97
+ ];
98
+ }
99
+ //# sourceMappingURL=merge-routes.js.map
@@ -0,0 +1,69 @@
1
+ import { applyStageTool } from "./stage-tool.js";
2
+ export const MERGE_STAGE_TOOL_NAME = 'merge_stage';
3
+ function shapeError(message) {
4
+ return { ok: false, code: 'bad-path', message };
5
+ }
6
+ function operation(args) {
7
+ const op = args.op;
8
+ if (op === 'list') {
9
+ const area = args.area;
10
+ if (area !== 'sources' && area !== 'merged')
11
+ return shapeError('list.area must be sources or merged');
12
+ return { op, area, ...(typeof args.path === 'string' ? { path: args.path } : {}), ...(typeof args.source === 'string' ? { source: args.source } : {}) };
13
+ }
14
+ if (op === 'read') {
15
+ const area = args.area;
16
+ if (area !== 'sources' && area !== 'merged')
17
+ return shapeError('read.area must be sources or merged');
18
+ if (typeof args.path !== 'string' || args.path === '')
19
+ return shapeError('read.path is required');
20
+ return { op, area, path: args.path, ...(typeof args.source === 'string' ? { source: args.source } : {}) };
21
+ }
22
+ if (op === 'write') {
23
+ if (typeof args.path !== 'string' || args.path === '')
24
+ return shapeError('write.path is required');
25
+ if (typeof args.content !== 'string')
26
+ return shapeError('write.content must be a string');
27
+ return { op, path: args.path, content: args.content };
28
+ }
29
+ if (op === 'copy') {
30
+ if (typeof args.source !== 'string' || typeof args.from !== 'string' || typeof args.to !== 'string')
31
+ return shapeError('copy requires source/from/to');
32
+ return { op, source: args.source, from: args.from, to: args.to };
33
+ }
34
+ return shapeError('op must be list/read/write/copy');
35
+ }
36
+ export function createMergeStageTool(io, home, context) {
37
+ return {
38
+ name: MERGE_STAGE_TOOL_NAME,
39
+ description: 'Read source copies and write the merged skill into the staging tree. list/read use area=sources|merged plus source (for sources); write/copy paths are relative to merged/<target> (for example path=SKILL.md, from=SKILL.md, to=references/x.md). Write/copy are only allowed in the author phase.',
40
+ parameters: {
41
+ type: 'object',
42
+ properties: {
43
+ op: { type: 'string', enum: ['list', 'read', 'write', 'copy'] },
44
+ area: { type: 'string', enum: ['sources', 'merged'] },
45
+ path: { type: 'string' },
46
+ source: { type: 'string' },
47
+ content: { type: 'string' },
48
+ from: { type: 'string' },
49
+ to: { type: 'string' },
50
+ },
51
+ required: ['op'],
52
+ additionalProperties: false,
53
+ },
54
+ output: {
55
+ schema: { type: 'string' },
56
+ render: (_args, value) => [{ type: 'text', text: value }],
57
+ },
58
+ async execute(args, exec) {
59
+ const op = operation(args);
60
+ if ('ok' in op)
61
+ return JSON.stringify(op);
62
+ const ctx = context(exec);
63
+ if (ctx === undefined)
64
+ return JSON.stringify({ ok: false, code: 'not-merge-child', message: 'merge_stage is only available to an active merge child' });
65
+ return JSON.stringify(await applyStageTool(io, home, ctx, op));
66
+ },
67
+ };
68
+ }
69
+ //# sourceMappingURL=merge-tool.js.map
@@ -4,7 +4,7 @@
4
4
  */
5
5
  import type { EvolutionIoLike } from '@lmzhen/dsh-evolution-core';
6
6
  import type { MergeDecision, MergeRow } from './contracts.ts';
7
- import type { SkillCard } from './screening.ts';
7
+ import type { SkillCard } from './skill-card.ts';
8
8
  import type { ConversationChannelLike } from './channel-port.ts';
9
9
  import { type CommitPort } from './commit-execute.ts';
10
10
  import type { StageToolContext } from './stage-tool.ts';