openmerit 0.1.3 → 0.1.5

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 (77) hide show
  1. package/CHANGELOG.md +26 -0
  2. package/README.md +83 -312
  3. package/dist/core/src/index.d.ts +90 -0
  4. package/dist/core/src/index.js +1137 -0
  5. package/dist/core/src/store.d.ts +35 -0
  6. package/dist/core/src/store.js +102 -0
  7. package/dist/pi/src/index.d.ts +15 -0
  8. package/dist/pi/src/index.js +423 -0
  9. package/dist/protocol/src/index.d.ts +402 -0
  10. package/dist/protocol/src/index.js +47 -0
  11. package/dist/protocol/src/schemas.d.ts +450 -0
  12. package/dist/protocol/src/schemas.js +224 -0
  13. package/docs/adapter-guide.md +172 -0
  14. package/docs/architecture.md +55 -0
  15. package/docs/automation.md +66 -0
  16. package/docs/getting-started.md +55 -0
  17. package/docs/lifecycle.md +30 -0
  18. package/docs/metrics-and-evidence.md +40 -0
  19. package/docs/operations.md +31 -0
  20. package/docs/pareto-spec.md +76 -0
  21. package/docs/pi-extension.md +44 -0
  22. package/docs/roadmap.md +26 -0
  23. package/docs/security.md +23 -0
  24. package/docs/testing.md +36 -0
  25. package/docs/ux-reference.md +32 -0
  26. package/package.json +45 -54
  27. package/benchmark/invoice_ocr/data/invoice_01_ground_truth.json +0 -38
  28. package/benchmark/invoice_ocr/data/invoice_01_row_2.jpg +0 -0
  29. package/benchmark/invoice_ocr/data/invoice_02_ground_truth.json +0 -32
  30. package/benchmark/invoice_ocr/data/invoice_02_row_5.jpg +0 -0
  31. package/benchmark/invoice_ocr/data/invoice_03_ground_truth.json +0 -26
  32. package/benchmark/invoice_ocr/data/invoice_03_row_6.jpg +0 -0
  33. package/benchmark/invoice_ocr/data/invoice_04_ground_truth.json +0 -26
  34. package/benchmark/invoice_ocr/data/invoice_04_row_7.jpg +0 -0
  35. package/benchmark/invoice_ocr/data/invoice_05_ground_truth.json +0 -38
  36. package/benchmark/invoice_ocr/data/invoice_05_row_947.jpg +0 -0
  37. package/benchmark/invoice_ocr/data/invoice_06_ground_truth.json +0 -38
  38. package/benchmark/invoice_ocr/data/invoice_06_row_948.jpg +0 -0
  39. package/benchmark/invoice_ocr/data/invoice_07_ground_truth.json +0 -20
  40. package/benchmark/invoice_ocr/data/invoice_07_row_949.jpg +0 -0
  41. package/benchmark/invoice_ocr/data/invoice_08_ground_truth.json +0 -38
  42. package/benchmark/invoice_ocr/data/invoice_08_row_1888.jpg +0 -0
  43. package/benchmark/invoice_ocr/data/invoice_09_ground_truth.json +0 -26
  44. package/benchmark/invoice_ocr/data/invoice_09_row_1890.jpg +0 -0
  45. package/benchmark/invoice_ocr/data/invoice_10_ground_truth.json +0 -20
  46. package/benchmark/invoice_ocr/data/invoice_10_row_1892.jpg +0 -0
  47. package/benchmark/invoice_ocr/data/manifest.json +0 -97
  48. package/dist/benchmarks.js +0 -98
  49. package/dist/catalog.js +0 -61
  50. package/dist/cli.js +0 -188
  51. package/dist/daemon.js +0 -388
  52. package/dist/diagnostics.js +0 -194
  53. package/dist/frontier.js +0 -56
  54. package/dist/harness.js +0 -1
  55. package/dist/integrations.js +0 -19
  56. package/dist/invoice-eval.js +0 -33
  57. package/dist/invoice-score.js +0 -124
  58. package/dist/judge.js +0 -43
  59. package/dist/llm.js +0 -203
  60. package/dist/pi-trials.js +0 -366
  61. package/dist/policy.js +0 -115
  62. package/dist/providers.js +0 -1
  63. package/dist/recommend.js +0 -76
  64. package/dist/routes.js +0 -59
  65. package/dist/standalone.js +0 -220
  66. package/dist/store.js +0 -89
  67. package/dist/strategist.js +0 -68
  68. package/dist/task-input.js +0 -54
  69. package/dist/traces.js +0 -127
  70. package/dist/trials.js +0 -140
  71. package/dist/types.js +0 -2
  72. package/examples/invoice-prompt.txt +0 -19
  73. package/examples/task.example.json +0 -7
  74. package/extension/openmerit.ts +0 -820
  75. package/instructions/OPENMERIT.md +0 -54
  76. package/instructions/openmerit.policy.json +0 -33
  77. package/rules.md +0 -39
package/dist/daemon.js DELETED
@@ -1,388 +0,0 @@
1
- /**
2
- * Trial engine shared by the pi extension's session-bound jobs and the optional
3
- * CLI watcher. It reads traces, runs pi candidates, and writes recommendations
4
- * for the extension to apply in the active session.
5
- */
6
- import { fetchCatalog, diffNewModels, loadSnapshot, saveSnapshot } from "./catalog.js";
7
- import { categorizeTask, loadDigest, relevantBenchmarks, benchmarkPrior, openRouterBenchmarkCandidates } from "./benchmarks.js";
8
- import { loadKey } from "./llm.js";
9
- import { providerAllowed } from "./policy.js";
10
- import { buildRecommendation } from "./recommend.js";
11
- import { appendJsonl, paths, readJson, readJsonl, writeJson, } from "./store.js";
12
- import { budgetOk, ensureRubric, recordMeritSpend, recordTrialSpend, runTrial, trialBudgetOk } from "./trials.js";
13
- import { JUDGE_PREFS } from "./judge.js";
14
- import { runPiTrial, scorePiRun, settledActiveTask } from "./pi-trials.js";
15
- import { taskInputKey } from "./task-input.js";
16
- import { pickNext, STRAT_PREFS } from "./strategist.js";
17
- import { enrichRouteEntry, routeCatalog, routeKey, routeLabel } from "./routes.js";
18
- import { PiCliChatClient } from "./llm.js";
19
- import { LocalJsonlEventSink, PiTraceObservationSource, meritEvent } from "./integrations.js";
20
- function resolveRoutePref(cat, prefs, label, requireImages = false) {
21
- const available = [...cat.values()].filter((entry) => !!entry.route && entry.priceKnown !== false &&
22
- (!requireImages || entry.inputModalities?.includes("image")));
23
- for (const pref of prefs) {
24
- if (!pref)
25
- continue;
26
- const routed = cat.get(pref);
27
- if (routed && available.includes(routed))
28
- return routed;
29
- const exact = available.find((entry) => entry.id === pref);
30
- if (exact)
31
- return exact;
32
- }
33
- for (const pref of prefs) {
34
- if (!pref)
35
- continue;
36
- const fuzzy = available.find((entry) => entry.id.includes(pref));
37
- if (fuzzy)
38
- return fuzzy;
39
- }
40
- const fallback = available.sort((a, b) => (a.priceKnown === false ? 1 : 0) - (b.priceKnown === false ? 1 : 0) || a.price - b.price)[0];
41
- if (fallback)
42
- return fallback;
43
- throw new Error(`could not resolve ${label} route from Pi's eligible models`);
44
- }
45
- function processIsAlive(pid) {
46
- if (!Number.isInteger(pid) || pid <= 0)
47
- return false;
48
- try {
49
- process.kill(pid, 0);
50
- return true;
51
- }
52
- catch (error) {
53
- return error.code === "EPERM";
54
- }
55
- }
56
- /** Completed jobs stay final; live owners retain their claim; abandoned claims can retry. */
57
- export function taskJobIsClaimed(prior, marker, now = Date.now()) {
58
- if (!prior || prior.marker !== marker || prior.status === "failed")
59
- return false;
60
- if (prior.status !== "running")
61
- return true;
62
- if (prior.ownerPid !== undefined)
63
- return processIsAlive(prior.ownerPid);
64
- const updated = Date.parse(prior.updatedAt);
65
- return Number.isFinite(updated) && now - updated < 15 * 60_000;
66
- }
67
- function emitTrialProgress(progress) {
68
- console.log(`[openmerit/progress] ${JSON.stringify(progress)}`);
69
- }
70
- function persistTrial(point, sink) {
71
- appendJsonl(paths.trials(), point);
72
- sink.emit(meritEvent("trial.completed", point));
73
- }
74
- function resolvePref(cat, prefs, label) {
75
- for (const p of prefs) {
76
- if (p && cat.has(p))
77
- return p;
78
- }
79
- for (const p of prefs) {
80
- if (!p)
81
- continue;
82
- for (const id of [...cat.keys()].sort()) {
83
- if (id.includes(p))
84
- return id;
85
- }
86
- }
87
- throw new Error(`could not resolve ${label} model from preferences`);
88
- }
89
- /** Ingest new session-trace bytes into the observation log. */
90
- export function tracesTick(source = new PiTraceObservationSource(), sink = new LocalJsonlEventSink()) {
91
- const fresh = source.read();
92
- for (const o of fresh) {
93
- appendJsonl(paths.observations(), o);
94
- sink.emit(meritEvent("observation.recorded", o, source.id));
95
- }
96
- if (fresh.length > 0)
97
- console.log(`[openmerit] traces: ${fresh.length} new observation(s)`);
98
- return fresh;
99
- }
100
- /** Diff the provider catalog; queue newly released models for trials. */
101
- export async function catalogTick(key, policy) {
102
- const cat = await fetchCatalog(key);
103
- const snapshot = loadSnapshot();
104
- const fresh = snapshot.ids.length === 0 ? [] : diffNewModels(cat, snapshot);
105
- saveSnapshot(cat);
106
- const allowed = fresh.filter((e) => e.price <= policy.max_usd_per_m && providerAllowed(policy, e.id));
107
- if (allowed.length > 0) {
108
- const queue = readJson(paths.candidates(), { newModels: [] });
109
- const known = new Set(queue.newModels.map((m) => m.id));
110
- for (const e of allowed) {
111
- if (!known.has(e.id)) {
112
- queue.newModels.push({ id: e.id, firstSeen: new Date().toISOString(), price: e.price });
113
- }
114
- }
115
- writeJson(paths.candidates(), queue);
116
- console.log(`[openmerit] catalog: ${allowed.length} new model(s) queued: ${allowed.map((e) => e.id).join(", ")}`);
117
- }
118
- return allowed.map((e) => e.id);
119
- }
120
- /**
121
- * Run due shadow trials: for each recently observed task, trial queued new
122
- * models (or a strategist pick when the queue is empty and the frontier is
123
- * thin). Budget-capped per policy.
124
- */
125
- export async function trialTick(key, policy) {
126
- const sink = new LocalJsonlEventSink();
127
- const budget = budgetOk(policy);
128
- if (!budget.ok) {
129
- console.log(`[openmerit] trials paused: ${budget.reason}`);
130
- return;
131
- }
132
- const observations = readJsonl(paths.observations());
133
- const queue = readJson(paths.candidates(), { newModels: [] });
134
- if (observations.length === 0 || queue.newModels.length === 0)
135
- return;
136
- const cat = await fetchCatalog(key);
137
- const judge = resolvePref(cat, [policy.judge_model, ...JUDGE_PREFS], "judge");
138
- // Most recent observation per task.
139
- const latestByTask = new Map();
140
- for (const o of observations)
141
- latestByTask.set(o.taskKey, o);
142
- const trials = readJsonl(paths.trials());
143
- const rubricCache = new Map();
144
- const digest = loadDigest();
145
- for (const [tKey, obs] of latestByTask) {
146
- const due = queue.newModels.filter((m) => !trials.some((t) => t.taskKey === tKey && t.model === m.id));
147
- if (due.length === 0)
148
- continue;
149
- const { benchmarks } = relevantBenchmarks(obs.taskLabel);
150
- const category = categorizeTask(obs.taskLabel);
151
- // Rank due candidates by benchmark prior; cheapest first when unknown.
152
- due.sort((a, b) => {
153
- const pa = benchmarkPrior(category, a.id, digest);
154
- const pb = benchmarkPrior(category, b.id, digest);
155
- return (pb ?? -1) - (pa ?? -1) || a.price - b.price;
156
- });
157
- const candidate = due[0];
158
- const b = budgetOk(policy);
159
- if (!b.ok)
160
- break;
161
- console.log(`[openmerit] shadow trial: ${candidate.id} on task ${tKey} (${category}; benchmarks: ${benchmarks.join(", ")})`);
162
- const rubric = await ensureRubric(key, judge, obs.taskLabel, rubricCache);
163
- const { point, costUsd, error } = await runTrial(key, judge, obs.taskLabel, rubric, candidate.id, cat.get(candidate.id));
164
- recordTrialSpend(costUsd);
165
- persistTrial({ ...point, taskKey: tKey }, sink);
166
- if (error)
167
- console.log(`[openmerit] trial failed: ${error.slice(0, 120)}`);
168
- else
169
- console.log(`[openmerit] trial: ${candidate.id} score=${point.score.toFixed(2)} $${point.price.toFixed(2)}/M`);
170
- }
171
- }
172
- /** Compare the one completed active pi task, sequentially, then target its session. */
173
- export async function autoTaskTick(key, policy, settledTask) {
174
- const sink = new LocalJsonlEventSink();
175
- const task = settledTask === undefined ? settledActiveTask() : settledTask;
176
- if (!task)
177
- return null;
178
- const marker = settledTask === undefined
179
- ? `${task.sessionFile}:${task.settledAt}`
180
- : `${task.sessionFile}:${task.sessionBytes ?? task.settledAt}`;
181
- const processedPath = settledTask === undefined ? paths.watchProcessed() : paths.sessionJob(marker);
182
- const prior = readJson(processedPath, null);
183
- if (taskJobIsClaimed(prior, marker))
184
- return null;
185
- if (!budgetOk(policy).ok)
186
- return null;
187
- // Claim the settled turn before provider calls so the next poll cannot duplicate it.
188
- writeJson(processedPath, { marker, status: "running", ownerPid: process.pid,
189
- updatedAt: new Date().toISOString() });
190
- try {
191
- let cat = routeCatalog(task.routes);
192
- if (!cat.has(routeKey(task.route)))
193
- cat.set(routeKey(task.route), routeCatalog([task.route]).values().next().value);
194
- if (key) {
195
- try {
196
- const live = await fetchCatalog(key);
197
- saveSnapshot(live);
198
- cat = new Map([...cat].map(([id, entry]) => [id,
199
- entry.route?.provider === "openrouter" ? enrichRouteEntry(entry, live.get(entry.id)) : entry]));
200
- }
201
- catch (error) {
202
- console.log(`[openmerit] OpenRouter enrichment unavailable: ${error.message}`);
203
- }
204
- }
205
- for (const [id, entry] of [...cat]) {
206
- if ((entry.priceKnown !== false && entry.price > policy.max_usd_per_m) ||
207
- !providerAllowed(policy, entry.id, entry.route?.provider) ||
208
- (task.images.length > 0 && !entry.inputModalities?.includes("image")) ||
209
- (id !== routeKey(task.route) && !trialBudgetOk(policy, entry, task.task.length).ok))
210
- cat.delete(id);
211
- }
212
- const activeEntry = cat.get(routeKey(task.route));
213
- if (!activeEntry)
214
- throw new Error(`active route ${task.route.provider}/${task.route.modelId} is excluded by policy or task capabilities`);
215
- if (cat.size < 2)
216
- throw new Error("Pi exposes no eligible alternate model route for this task");
217
- const judge = resolveRoutePref(cat, [policy.judge_model, ...JUDGE_PREFS, task.model], "judge", task.images.length > 0);
218
- const strategist = resolveRoutePref(cat, [policy.strategist_model, ...STRAT_PREFS, task.model], "strategist");
219
- const client = new PiCliChatClient(undefined, recordMeritSpend);
220
- const { category, benchmarks } = relevantBenchmarks(task.task);
221
- let benchmarkCandidates = [];
222
- try {
223
- if (key)
224
- benchmarkCandidates = await openRouterBenchmarkCandidates(key, category, new Map([...cat.values()].map((entry) => [entry.id, entry])));
225
- }
226
- catch (e) {
227
- console.log(`[openmerit] benchmark shortlist unavailable: ${e.message}`);
228
- }
229
- const rubric = await ensureRubric(key ?? "", judge.route, task.task, new Map(), client);
230
- const tKey = taskInputKey(task.task, task.images, task.files);
231
- const points = [];
232
- const tried = new Set();
233
- const failedVendors = new Set();
234
- const total = Math.max(1, Math.floor(policy.watch.models_per_task));
235
- if (settledTask !== undefined)
236
- emitTrialProgress({ phase: "start", taskKey: tKey,
237
- model: routeLabel(task.route), index: 1, total });
238
- const baseline = await scorePiRun(key ?? "", judge.route, task.task, rubric, task.model, activeEntry, task.run, "trace", task.images, client);
239
- points.push(baseline.point);
240
- tried.add(routeKey(task.route));
241
- persistTrial({ ...baseline.point, taskKey: tKey, sessionFile: task.sessionFile }, sink);
242
- console.log(`[openmerit] task ${tKey}: A=${task.model} score=${baseline.point.score.toFixed(2)} from pi trace`);
243
- if (settledTask !== undefined)
244
- emitTrialProgress({ phase: "complete", taskKey: tKey,
245
- model: routeLabel(task.route), index: 1, total, score: baseline.point.score,
246
- price: baseline.point.price, latencyMs: baseline.point.latencyMs,
247
- costUsd: baseline.costUsd, toolCalls: baseline.point.toolCalls,
248
- toolErrors: baseline.point.toolErrors, changedFiles: baseline.point.changedFiles });
249
- for (let i = 1; i < total; i++) {
250
- if (!budgetOk(policy).ok)
251
- break;
252
- const pick = await pickNext(key ?? "", strategist.route, task.task, rubric, benchmarks, points, cat, tried, policy.max_usd_per_m, failedVendors, benchmarkCandidates, client);
253
- if (!pick)
254
- break;
255
- if (settledTask !== undefined)
256
- emitTrialProgress({ phase: "start", taskKey: tKey,
257
- model: pick.route ? routeLabel(pick.route) : pick.model, index: i + 1, total });
258
- const entry = pick.route ? cat.get(routeKey(pick.route)) : cat.get(pick.model);
259
- const { point, costUsd, error } = await runPiTrial(key ?? "", judge.route, task.task, rubric, pick.model, entry, task.cwd, task.images, task.files, undefined, client);
260
- tried.add(pick.route ? routeKey(pick.route) : pick.model);
261
- points.push(point);
262
- recordTrialSpend(costUsd);
263
- persistTrial({ ...point, taskKey: tKey, sessionFile: task.sessionFile }, sink);
264
- if (error)
265
- failedVendors.add(pick.route?.provider ?? pick.model.split("/")[0]);
266
- console.log(`[openmerit] task ${tKey}: ${pick.model} score=${point.score.toFixed(2)} cost=$${costUsd.toFixed(4)} ` +
267
- `tools=${point.toolCalls ?? 0} toolErrors=${point.toolErrors ?? 0} changedFiles=${point.changedFiles ?? 0}` +
268
- `${error ? ` error=${error.slice(0, 80)}` : ""}`);
269
- if (settledTask !== undefined)
270
- emitTrialProgress({ phase: "complete", taskKey: tKey,
271
- model: pick.route ? routeLabel(pick.route) : pick.model, index: i + 1, total, score: point.score, price: point.price,
272
- latencyMs: point.latencyMs, costUsd, error: error?.slice(0, 120),
273
- toolCalls: point.toolCalls, toolErrors: point.toolErrors, changedFiles: point.changedFiles });
274
- }
275
- const rec = buildRecommendation(tKey, task.task.slice(0, 120), task.model, points, policy, task.sessionFile, task.route);
276
- if (rec) {
277
- appendJsonl(paths.recommendations(), rec);
278
- sink.emit(meritEvent("recommendation.created", rec));
279
- console.log(`[openmerit] task ${tKey}: selected ${rec.recommended.model}; auto=${rec.policy.autoApply}`);
280
- }
281
- else
282
- console.log(`[openmerit] task ${tKey}: active model remains best`);
283
- writeJson(processedPath, { marker, status: rec ? "complete" : "no_swap",
284
- ownerPid: process.pid, updatedAt: new Date().toISOString() });
285
- return rec;
286
- }
287
- catch (error) {
288
- writeJson(processedPath, { marker, status: "failed",
289
- ownerPid: process.pid, updatedAt: new Date().toISOString() });
290
- throw error;
291
- }
292
- }
293
- /** Rebuild frontiers from trials and emit recommendations for the harness's current model. */
294
- export function recommendTick(policy) {
295
- const sink = new LocalJsonlEventSink();
296
- const harness = readJson(paths.harnessState(), {});
297
- const currentModel = harness.currentModel ?? null;
298
- const currentRoute = harness.currentRoute ?? null;
299
- const trials = readJsonl(paths.trials());
300
- const observations = readJsonl(paths.observations());
301
- const labelByTask = new Map();
302
- for (const o of observations)
303
- if (!labelByTask.has(o.taskKey))
304
- labelByTask.set(o.taskKey, o.taskLabel);
305
- const byTask = new Map();
306
- for (const t of trials) {
307
- if (!t.taskKey)
308
- continue;
309
- const arr = byTask.get(t.taskKey) ?? [];
310
- arr.push(t);
311
- byTask.set(t.taskKey, arr);
312
- }
313
- // Dedupe on latest status per id: don't re-emit what is pending or was dismissed.
314
- const all = readJsonl(paths.recommendations());
315
- const latestById = new Map();
316
- for (const r of all)
317
- latestById.set(r.id, r);
318
- const existing = new Set([...latestById.values()]
319
- .filter((r) => r.status === "pending" || r.status === "dismissed")
320
- .map((r) => `${r.taskKey}:${r.recommended.route ? routeKey(r.recommended.route) : `openrouter:${r.recommended.model}`}`));
321
- const emitted = [];
322
- for (const [tKey, points] of byTask) {
323
- const rec = buildRecommendation(tKey, labelByTask.get(tKey) ?? tKey, currentModel, points, policy, null, currentRoute);
324
- if (!rec)
325
- continue;
326
- const dedupeKey = `${rec.taskKey}:${rec.recommended.route ? routeKey(rec.recommended.route) : `openrouter:${rec.recommended.model}`}`;
327
- if (existing.has(dedupeKey))
328
- continue;
329
- appendJsonl(paths.recommendations(), rec);
330
- sink.emit(meritEvent("recommendation.created", rec));
331
- emitted.push(rec);
332
- console.log(`[openmerit] recommendation: ${rec.recommended.model} <- ${currentModel ?? "unknown"} ` +
333
- `(gain ${rec.evidence.scoreGain}, auto=${rec.policy.autoApply})`);
334
- }
335
- return emitted;
336
- }
337
- /** One full pass. */
338
- export async function tickOnce(policy) {
339
- tracesTick();
340
- let key = null;
341
- try {
342
- key = loadKey();
343
- }
344
- catch (e) {
345
- console.log(`[openmerit] ${e.message}; OpenRouter enrichment disabled`);
346
- }
347
- if (key) {
348
- await catalogTick(key, policy);
349
- }
350
- await autoTaskTick(key, policy);
351
- }
352
- /** Run forever, honoring policy watch intervals. */
353
- export async function runDaemon(policy) {
354
- console.log("[openmerit] background track started");
355
- const tracesMs = Math.max(5, policy.watch.traces_interval_sec) * 1000;
356
- let lastCatalog = 0;
357
- // eslint-disable-next-line no-constant-condition
358
- while (true) {
359
- try {
360
- tracesTick();
361
- }
362
- catch (e) {
363
- console.log(`[openmerit] traces tick failed: ${e.message}`);
364
- }
365
- const now = Date.now();
366
- const wantCatalog = now - lastCatalog >= policy.watch.catalog_interval_min * 60_000;
367
- {
368
- let key = null;
369
- try {
370
- key = loadKey();
371
- }
372
- catch {
373
- /* no key; skip provider ticks */
374
- }
375
- try {
376
- if (key && wantCatalog) {
377
- await catalogTick(key, policy);
378
- lastCatalog = now;
379
- }
380
- await autoTaskTick(key, policy);
381
- }
382
- catch (e) {
383
- console.log(`[openmerit] provider tick failed: ${e.message}`);
384
- }
385
- }
386
- await new Promise((r) => setTimeout(r, tracesMs));
387
- }
388
- }
@@ -1,194 +0,0 @@
1
- /** Read-only diagnostics plus a provider-free self-test for first-run support. */
2
- import { spawnSync } from "node:child_process";
3
- import { appendFileSync, existsSync, mkdtempSync, readFileSync, rmSync } from "node:fs";
4
- import { tmpdir } from "node:os";
5
- import { basename, join } from "node:path";
6
- import { LocalJsonlEventSink, meritEvent } from "./integrations.js";
7
- import { piExecutable, availablePiRoutes } from "./pi-trials.js";
8
- import { DEFAULT_POLICY, loadPolicy, providerAllowed } from "./policy.js";
9
- import { buildRecommendation } from "./recommend.js";
10
- import { modelRoute, routeCatalog, routeKey, routeLabel } from "./routes.js";
11
- import { appendJsonl, paths, readJsonReport, readJsonlReport, writeJson, } from "./store.js";
12
- function check(id, status, message, details) {
13
- return { id, status, message, ...(details?.length ? { details } : {}) };
14
- }
15
- function packageSources(output) {
16
- return [...new Set(output.split("\n").filter((line) => /^ {2}\S/.test(line)).map((line) => line.trim())
17
- .filter((line) => /openmerit/i.test(line) && /^(npm:|git:|https?:|\.?\.?\/|\/)/.test(line)))];
18
- }
19
- function safePackageSource(source) {
20
- if (source.startsWith("npm:/"))
21
- return `npm:local/${basename(source)}`;
22
- if (source.startsWith("/"))
23
- return `local:${basename(source)}`;
24
- return source;
25
- }
26
- /** No credentials, prompts, or trace contents are included in this report. */
27
- export function collectDoctorReport() {
28
- const checks = [];
29
- const executable = piExecutable();
30
- const versionResult = spawnSync(executable, ["--version"], { encoding: "utf8" });
31
- const piVersion = versionResult.status === 0 ? versionResult.stdout.trim() : null;
32
- checks.push(piVersion
33
- ? check("pi", "pass", `Pi detected (${piVersion})`)
34
- : check("pi", "fail", "Pi is not executable"));
35
- try {
36
- const policy = loadPolicy(paths.policy());
37
- checks.push(check("policy", "pass", `Policy is valid (v${policy.version}, ${policy.mode})`));
38
- }
39
- catch (error) {
40
- checks.push(check("policy", "fail", `Policy is invalid: ${error.message}`));
41
- }
42
- const stateRead = readJsonReport(paths.harnessState(), {});
43
- checks.push(!stateRead.valid
44
- ? check("harness-state", "fail", "harness-state.json is malformed; start Pi to replace it atomically")
45
- : stateRead.exists
46
- ? check("harness-state", "pass", "Pi extension state snapshot is readable")
47
- : check("harness-state", "warn", "No Pi extension state yet; start Pi once with OpenMerit installed"));
48
- let routes = stateRead.value.routes ?? [];
49
- const liveSnapshot = routes.length > 0;
50
- if (!routes.length && piVersion) {
51
- try {
52
- routes = availablePiRoutes();
53
- }
54
- catch { /* the check below explains it */ }
55
- }
56
- let policy = DEFAULT_POLICY;
57
- try {
58
- policy = loadPolicy(paths.policy());
59
- }
60
- catch { /* already reported */ }
61
- routes = routes.filter((route) => providerAllowed(policy, route.meritId, route.provider));
62
- const priced = routes.filter((route) => !!route.cost);
63
- checks.push(routes.length >= 2
64
- ? check("routes", "pass", `${routes.length} Pi model routes are visible`, routes.slice(0, 8).map((route) => `${routeLabel(route)}${route.cost ? "" : " (price unknown)"}`))
65
- : check("routes", "fail", `Only ${routes.length} policy-eligible Pi route${routes.length === 1 ? " is" : "s are"} visible; at least two are required`));
66
- checks.push(liveSnapshot
67
- ? priced.length >= 2
68
- ? check("pricing", "pass", `${priced.length} eligible routes expose pricing`)
69
- : check("pricing", "warn", "Fewer than two eligible routes expose pricing; unknown-price routes cannot auto-apply")
70
- : check("pricing", "warn", "Route prices are unavailable until the extension reports a live Pi snapshot"));
71
- const session = stateRead.value.sessionFile;
72
- checks.push(!session
73
- ? check("session", "warn", "No active saved Pi session is reported yet")
74
- : existsSync(session)
75
- ? check("session", "pass", "The reported Pi session file is readable")
76
- : check("session", "warn", "The reported Pi session file no longer exists"));
77
- if (piVersion) {
78
- const listed = spawnSync(executable, ["list"], { encoding: "utf8" });
79
- const sources = listed.status === 0 ? packageSources(listed.stdout) : [];
80
- const safeSources = sources.map(safePackageSource);
81
- checks.push(sources.length > 1
82
- ? check("installation", "fail", "OpenMerit is installed from multiple Pi package sources", safeSources)
83
- : sources.length === 1
84
- ? check("installation", "pass", `One Pi package source is installed`, safeSources)
85
- : check("installation", "warn", "OpenMerit is not listed as an installed Pi package; use `pi install npm:openmerit`"));
86
- }
87
- const jsonlFiles = [
88
- ["recommendations.jsonl", paths.recommendations()],
89
- ["trials.jsonl", paths.trials()],
90
- ["traces/observations.jsonl", paths.observations()],
91
- ["events.jsonl", paths.events()],
92
- ];
93
- const malformed = jsonlFiles.flatMap(([label, file]) => {
94
- const invalid = readJsonlReport(file).invalidLines;
95
- return invalid.length ? [`${label}: lines ${invalid.join(", ")}`] : [];
96
- });
97
- checks.push(malformed.length
98
- ? check("jsonl", "warn", "Malformed JSONL lines were skipped; valid later records remain readable", malformed)
99
- : check("jsonl", "pass", "Append-only JSONL state is readable"));
100
- const ledger = readJsonReport(paths.ledger(), { date: "", trials: 0, usd: 0 });
101
- checks.push(ledger.valid
102
- ? check("ledger", "pass", "Trial-spend ledger is readable")
103
- : check("ledger", "fail", "Trial-spend ledger is malformed; spending remains disabled until repaired"));
104
- let openRouter = false;
105
- try {
106
- const env = process.env.OPENROUTER_API_KEY;
107
- if (env?.trim())
108
- openRouter = true;
109
- else if (existsSync(paths.envFile()))
110
- openRouter = /^\s*OPENROUTER_API_KEY\s*=\s*\S+/m
111
- .test(readFileSync(paths.envFile(), "utf8"));
112
- }
113
- catch { /* optional */ }
114
- checks.push(check("openrouter-enrichment", "pass", openRouter ? "Optional OpenRouter enrichment is configured" : "Optional OpenRouter enrichment is not configured"));
115
- const activeRoute = stateRead.value.currentRoute ? routeLabel(stateRead.value.currentRoute) : null;
116
- return {
117
- schemaVersion: 1,
118
- ok: !checks.some((item) => item.status === "fail"),
119
- checks,
120
- summary: { piVersion, routeCount: routes.length, pricedRouteCount: priced.length, activeRoute },
121
- };
122
- }
123
- export function renderDiagnosticReport(report) {
124
- const icon = { pass: "PASS", warn: "WARN", fail: "FAIL" };
125
- return report.checks.flatMap((item) => [
126
- `${icon[item.status].padEnd(4)} ${item.id.padEnd(22)} ${item.message}`,
127
- ...(item.details ?? []).map((detail) => ` - ${detail}`),
128
- ]).join("\n");
129
- }
130
- /** Exercise core state, routing, recommendation, and event paths without provider calls. */
131
- export function runOfflineVerify() {
132
- const previousHome = process.env.OPENMERIT_HOME;
133
- const root = mkdtempSync(join(tmpdir(), "openmerit-verify-"));
134
- const checks = [];
135
- try {
136
- process.env.OPENMERIT_HOME = root;
137
- const active = modelRoute("native", "model-a", {
138
- input: ["text"], cost: { input: 1, output: 2 }, contextWindow: 8192, maxTokens: 4096,
139
- });
140
- const candidate = modelRoute("local", "model-b", {
141
- input: ["text"], cost: { input: 0.2, output: 0.4 }, contextWindow: 8192, maxTokens: 4096,
142
- });
143
- writeJson(paths.harnessState(), { schemaVersion: 1, currentRoute: active, routes: [active, candidate] });
144
- const state = readJsonReport(paths.harnessState(), {});
145
- checks.push(state.valid && state.value.routes?.length === 2
146
- ? check("atomic-json", "pass", "Atomic JSON state round-trip passed")
147
- : check("atomic-json", "fail", "Atomic JSON state round-trip failed"));
148
- const entries = routeCatalog([active, candidate]);
149
- const point = (route, score) => {
150
- const entry = entries.get(routeKey(route));
151
- return { schemaVersion: 1, model: route.meritId, route, score, price: entry.price,
152
- priceKnown: entry.priceKnown, ts: new Date().toISOString(), source: "pi_trial" };
153
- };
154
- const recommendation = buildRecommendation("verify-task", "offline verification", active.meritId, [point(active, 0.6), point(candidate, 0.9)], DEFAULT_POLICY, "/verify/session.jsonl", active);
155
- checks.push(recommendation?.recommended.route?.provider === "local" &&
156
- recommendation.policy.reasons.length > 0
157
- ? check("provider-neutral", "pass", "Exact provider routes and policy reasons survive recommendation")
158
- : check("provider-neutral", "fail", "Provider route or policy evidence was lost"));
159
- if (recommendation)
160
- appendJsonl(paths.recommendations(), recommendation);
161
- appendJsonl(paths.recommendations(), { schemaVersion: 0, id: "legacy", status: "dismissed" });
162
- appendFileSync(paths.recommendations(), "{interrupted");
163
- const history = readJsonlReport(paths.recommendations());
164
- checks.push(history.records.length === 2 && history.invalidLines.length === 1
165
- ? check("jsonl-recovery", "pass", "Valid records survive legacy and interrupted JSONL lines")
166
- : check("jsonl-recovery", "fail", "JSONL recovery did not preserve valid history"));
167
- const sink = new LocalJsonlEventSink();
168
- if (recommendation)
169
- sink.emit(meritEvent("recommendation.created", recommendation));
170
- const events = readJsonlReport(paths.events());
171
- checks.push(events.records.length === 1
172
- ? check("events", "pass", "Versioned neutral event sink round-trip passed")
173
- : check("events", "fail", "Neutral event sink round-trip failed"));
174
- checks.push(loadPolicy(paths.policy()).mode === "recommend"
175
- ? check("compatibility", "pass", "Missing 0.1.x policy fields normalize to safe defaults")
176
- : check("compatibility", "fail", "Policy defaults are unsafe"));
177
- }
178
- catch (error) {
179
- checks.push(check("verify", "fail", error.message));
180
- }
181
- finally {
182
- if (previousHome === undefined)
183
- delete process.env.OPENMERIT_HOME;
184
- else
185
- process.env.OPENMERIT_HOME = previousHome;
186
- rmSync(root, { recursive: true, force: true });
187
- }
188
- return {
189
- schemaVersion: 1,
190
- ok: !checks.some((item) => item.status === "fail"),
191
- checks,
192
- summary: { piVersion: null, routeCount: 2, pricedRouteCount: 2, activeRoute: "native/model-a via native" },
193
- };
194
- }
package/dist/frontier.js DELETED
@@ -1,56 +0,0 @@
1
- /** Pareto frontier over (score up, price down, latency down) + best-fit/fallback picks. */
2
- import { pointKey } from "./routes.js";
3
- function effectivePrice(point) {
4
- return point.priceKnown === false ? Number.POSITIVE_INFINITY : point.price;
5
- }
6
- /** a dominates b if a is no worse on every objective and strictly better on one. */
7
- export function dominates(a, b) {
8
- const aLat = a.latencyMs ?? Number.POSITIVE_INFINITY;
9
- const bLat = b.latencyMs ?? Number.POSITIVE_INFINITY;
10
- const aPrice = effectivePrice(a);
11
- const bPrice = effectivePrice(b);
12
- const noWorse = a.score >= b.score && aPrice <= bPrice && aLat <= bLat;
13
- const better = a.score > b.score || aPrice < bPrice || aLat < bLat;
14
- return noWorse && better;
15
- }
16
- /**
17
- * Non-dominated set, cheapest first. When no point carries latency this
18
- * reduces to the classic "strictly increasing score as price rises" chain
19
- * from the original model-search prototype.
20
- */
21
- export function paretoFrontier(points) {
22
- const fr = points.filter((p) => !points.some((q) => q !== p && dominates(q, p)));
23
- return fr.sort((a, b) => effectivePrice(a) - effectivePrice(b) || b.score - a.score);
24
- }
25
- /** Simple 2-objective chain (score vs price only), cheapest first. */
26
- export function scorePriceChain(points) {
27
- const pts = [...points].sort((a, b) => effectivePrice(a) - effectivePrice(b) || b.score - a.score);
28
- const chain = [];
29
- let best = -1;
30
- for (const p of pts) {
31
- if (p.score > best) {
32
- chain.push(p);
33
- best = p.score;
34
- }
35
- }
36
- return chain;
37
- }
38
- /** Highest score; ties broken by lower price then lower latency. */
39
- export function pickBest(points) {
40
- return [...points].sort((a, b) => b.score - a.score || effectivePrice(a) - effectivePrice(b) ||
41
- (a.latencyMs ?? 1e18) - (b.latencyMs ?? 1e18))[0];
42
- }
43
- /**
44
- * Fallback for `best`: prefer the cheapest frontier point that still clears
45
- * `minScore`; otherwise the next-best scoring point overall. Never returns
46
- * `best` itself.
47
- */
48
- export function pickFallback(points, best, minScore) {
49
- const others = points.filter((p) => pointKey(p) !== pointKey(best));
50
- if (others.length === 0)
51
- return undefined;
52
- const fr = paretoFrontier(others).filter((p) => p.score >= minScore);
53
- if (fr.length > 0)
54
- return fr[0]; // cheapest adequate frontier point
55
- return pickBest(others);
56
- }
package/dist/harness.js DELETED
@@ -1 +0,0 @@
1
- export {};
@@ -1,19 +0,0 @@
1
- /** Stable boundaries for trace inputs and telemetry outputs. */
2
- import { randomUUID } from "node:crypto";
3
- import { appendJsonl, paths } from "./store.js";
4
- import { ingestNewTraces } from "./traces.js";
5
- export class PiTraceObservationSource {
6
- sessionsDir;
7
- id = "pi-jsonl";
8
- constructor(sessionsDir) {
9
- this.sessionsDir = sessionsDir;
10
- }
11
- read() { return ingestNewTraces(this.sessionsDir); }
12
- }
13
- export class LocalJsonlEventSink {
14
- id = "local-jsonl";
15
- emit(event) { appendJsonl(paths.events(), event); }
16
- }
17
- export function meritEvent(type, payload, source = "openmerit") {
18
- return { schemaVersion: 1, id: randomUUID(), ts: new Date().toISOString(), source, type, payload };
19
- }