jiva-core 0.3.47 → 0.3.48-dev.3254e6a
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 +31 -1
- package/dist/code/agent.d.ts +13 -0
- package/dist/code/agent.d.ts.map +1 -1
- package/dist/code/agent.js +129 -13
- package/dist/code/agent.js.map +1 -1
- package/dist/code/benchmark/agent-factory.d.ts +18 -0
- package/dist/code/benchmark/agent-factory.d.ts.map +1 -0
- package/dist/code/benchmark/agent-factory.js +21 -0
- package/dist/code/benchmark/agent-factory.js.map +1 -0
- package/dist/code/benchmark/fixtures.d.ts +28 -0
- package/dist/code/benchmark/fixtures.d.ts.map +1 -0
- package/dist/code/benchmark/fixtures.js +177 -0
- package/dist/code/benchmark/fixtures.js.map +1 -0
- package/dist/code/benchmark/index.d.ts +16 -0
- package/dist/code/benchmark/index.d.ts.map +1 -0
- package/dist/code/benchmark/index.js +14 -0
- package/dist/code/benchmark/index.js.map +1 -0
- package/dist/code/benchmark/microcrm/assets/api.test.mjs +254 -0
- package/dist/code/benchmark/microcrm/assets/app.full.reference.mjs +267 -0
- package/dist/code/benchmark/microcrm/assets/app.reference.mjs +191 -0
- package/dist/code/benchmark/microcrm/assets/bulk.test.mjs +63 -0
- package/dist/code/benchmark/microcrm/assets/idempotency.test.mjs +64 -0
- package/dist/code/benchmark/microcrm/assets/query.test.mjs +96 -0
- package/dist/code/benchmark/microcrm/assets/stats.test.mjs +67 -0
- package/dist/code/benchmark/microcrm/index.d.ts +23 -0
- package/dist/code/benchmark/microcrm/index.d.ts.map +1 -0
- package/dist/code/benchmark/microcrm/index.js +72 -0
- package/dist/code/benchmark/microcrm/index.js.map +1 -0
- package/dist/code/benchmark/microcrm/spec.d.ts +13 -0
- package/dist/code/benchmark/microcrm/spec.d.ts.map +1 -0
- package/dist/code/benchmark/microcrm/spec.js +115 -0
- package/dist/code/benchmark/microcrm/spec.js.map +1 -0
- package/dist/code/benchmark/orchestrator-factory.d.ts +36 -0
- package/dist/code/benchmark/orchestrator-factory.d.ts.map +1 -0
- package/dist/code/benchmark/orchestrator-factory.js +57 -0
- package/dist/code/benchmark/orchestrator-factory.js.map +1 -0
- package/dist/code/benchmark/report.d.ts +8 -0
- package/dist/code/benchmark/report.d.ts.map +1 -0
- package/dist/code/benchmark/report.js +103 -0
- package/dist/code/benchmark/report.js.map +1 -0
- package/dist/code/benchmark/runner.d.ts +29 -0
- package/dist/code/benchmark/runner.d.ts.map +1 -0
- package/dist/code/benchmark/runner.js +219 -0
- package/dist/code/benchmark/runner.js.map +1 -0
- package/dist/code/benchmark/suites.d.ts +19 -0
- package/dist/code/benchmark/suites.d.ts.map +1 -0
- package/dist/code/benchmark/suites.js +48 -0
- package/dist/code/benchmark/suites.js.map +1 -0
- package/dist/code/benchmark/tasks.d.ts +16 -0
- package/dist/code/benchmark/tasks.d.ts.map +1 -0
- package/dist/code/benchmark/tasks.js +153 -0
- package/dist/code/benchmark/tasks.js.map +1 -0
- package/dist/code/benchmark/tests.d.ts +17 -0
- package/dist/code/benchmark/tests.d.ts.map +1 -0
- package/dist/code/benchmark/tests.js +234 -0
- package/dist/code/benchmark/tests.js.map +1 -0
- package/dist/code/benchmark/types.d.ts +161 -0
- package/dist/code/benchmark/types.d.ts.map +1 -0
- package/dist/code/benchmark/types.js +11 -0
- package/dist/code/benchmark/types.js.map +1 -0
- package/dist/code/benchmark/verify.d.ts +21 -0
- package/dist/code/benchmark/verify.d.ts.map +1 -0
- package/dist/code/benchmark/verify.js +108 -0
- package/dist/code/benchmark/verify.js.map +1 -0
- package/dist/code/tools/edit.d.ts.map +1 -1
- package/dist/code/tools/edit.js +12 -3
- package/dist/code/tools/edit.js.map +1 -1
- package/dist/code/tools/write.d.ts.map +1 -1
- package/dist/code/tools/write.js +13 -3
- package/dist/code/tools/write.js.map +1 -1
- package/dist/core/agent-interface.d.ts +5 -0
- package/dist/core/agent-interface.d.ts.map +1 -1
- package/dist/core/config.d.ts +48 -0
- package/dist/core/config.d.ts.map +1 -1
- package/dist/core/config.js +7 -0
- package/dist/core/config.js.map +1 -1
- package/dist/core/manager-agent.d.ts +5 -0
- package/dist/core/manager-agent.d.ts.map +1 -1
- package/dist/core/manager-agent.js +8 -8
- package/dist/core/manager-agent.js.map +1 -1
- package/dist/interfaces/cli/index.js +129 -0
- package/dist/interfaces/cli/index.js.map +1 -1
- package/dist/interfaces/cli/repl.d.ts.map +1 -1
- package/dist/interfaces/cli/repl.js +41 -15
- package/dist/interfaces/cli/repl.js.map +1 -1
- package/dist/interfaces/cli/setup-wizard.d.ts.map +1 -1
- package/dist/interfaces/cli/setup-wizard.js +2 -1
- package/dist/interfaces/cli/setup-wizard.js.map +1 -1
- package/dist/interfaces/http/index.d.ts.map +1 -1
- package/dist/interfaces/http/index.js +2 -0
- package/dist/interfaces/http/index.js.map +1 -1
- package/dist/interfaces/http/routes/benchmark.d.ts +14 -0
- package/dist/interfaces/http/routes/benchmark.d.ts.map +1 -0
- package/dist/interfaces/http/routes/benchmark.js +130 -0
- package/dist/interfaces/http/routes/benchmark.js.map +1 -0
- package/dist/interfaces/http/session-manager.d.ts.map +1 -1
- package/dist/interfaces/http/session-manager.js +1 -0
- package/dist/interfaces/http/session-manager.js.map +1 -1
- package/dist/models/base.d.ts +8 -0
- package/dist/models/base.d.ts.map +1 -1
- package/dist/models/model-client.d.ts +10 -1
- package/dist/models/model-client.d.ts.map +1 -1
- package/dist/models/model-client.js +3 -1
- package/dist/models/model-client.js.map +1 -1
- package/dist/models/orchestrator.d.ts +16 -3
- package/dist/models/orchestrator.d.ts.map +1 -1
- package/dist/models/orchestrator.js +32 -5
- package/dist/models/orchestrator.js.map +1 -1
- package/package.json +2 -2
- package/scripts/bench-selftest.mjs +112 -0
- package/scripts/copy-benchmark-assets.mjs +17 -0
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import { test } from 'node:test';
|
|
2
|
+
import assert from 'node:assert/strict';
|
|
3
|
+
import { createApp } from '../src/app.js';
|
|
4
|
+
|
|
5
|
+
async function withServer(run) {
|
|
6
|
+
const server = createApp(':memory:');
|
|
7
|
+
await new Promise((res) => server.listen(0, res));
|
|
8
|
+
const { port } = server.address();
|
|
9
|
+
try { await run(`http://127.0.0.1:${port}`); } finally { await new Promise((res) => server.close(res)); }
|
|
10
|
+
}
|
|
11
|
+
async function req(base, method, path, body) {
|
|
12
|
+
const init = { method, headers: {} };
|
|
13
|
+
if (body !== undefined) { init.headers['content-type'] = 'application/json'; init.body = JSON.stringify(body); }
|
|
14
|
+
const res = await fetch(base + path, init);
|
|
15
|
+
const text = await res.text();
|
|
16
|
+
let json = null; try { json = text ? JSON.parse(text) : null; } catch { json = null; }
|
|
17
|
+
return { status: res.status, body: json };
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
// Seed one contact with deals of known amounts/stages.
|
|
21
|
+
async function seed(base) {
|
|
22
|
+
const c = await req(base, 'POST', '/contacts', { name: 'A', email: `a${Math.random().toString(36).slice(2)}@x.com` });
|
|
23
|
+
const id = c.body.id;
|
|
24
|
+
const deals = [
|
|
25
|
+
{ title: 'd1', amount: 100, stage: 'lead' },
|
|
26
|
+
{ title: 'd2', amount: 200, stage: 'qualified' },
|
|
27
|
+
{ title: 'd3', amount: 300, stage: 'won' },
|
|
28
|
+
{ title: 'd4', amount: 400, stage: 'won' },
|
|
29
|
+
];
|
|
30
|
+
for (const d of deals) await req(base, 'POST', `/contacts/${id}/deals`, d);
|
|
31
|
+
return id;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
test('minAmount filters deals', () =>
|
|
35
|
+
withServer(async (base) => {
|
|
36
|
+
await seed(base);
|
|
37
|
+
const r = await req(base, 'GET', '/deals?minAmount=300');
|
|
38
|
+
assert.equal(r.status, 200);
|
|
39
|
+
assert.equal(r.body.total, 2);
|
|
40
|
+
assert.ok(r.body.data.every((d) => d.amount >= 300));
|
|
41
|
+
}));
|
|
42
|
+
|
|
43
|
+
test('maxAmount filters deals', () =>
|
|
44
|
+
withServer(async (base) => {
|
|
45
|
+
await seed(base);
|
|
46
|
+
const r = await req(base, 'GET', '/deals?maxAmount=200');
|
|
47
|
+
assert.equal(r.body.total, 2);
|
|
48
|
+
assert.ok(r.body.data.every((d) => d.amount <= 200));
|
|
49
|
+
}));
|
|
50
|
+
|
|
51
|
+
test('minAmount + maxAmount form a range', () =>
|
|
52
|
+
withServer(async (base) => {
|
|
53
|
+
await seed(base);
|
|
54
|
+
const r = await req(base, 'GET', '/deals?minAmount=200&maxAmount=300');
|
|
55
|
+
assert.deepEqual(r.body.data.map((d) => d.amount).sort((a, b) => a - b), [200, 300]);
|
|
56
|
+
}));
|
|
57
|
+
|
|
58
|
+
test('sort=amount:desc orders descending', () =>
|
|
59
|
+
withServer(async (base) => {
|
|
60
|
+
await seed(base);
|
|
61
|
+
const r = await req(base, 'GET', '/deals?sort=amount:desc');
|
|
62
|
+
assert.deepEqual(r.body.data.map((d) => d.amount), [400, 300, 200, 100]);
|
|
63
|
+
}));
|
|
64
|
+
|
|
65
|
+
test('sort=amount:asc orders ascending', () =>
|
|
66
|
+
withServer(async (base) => {
|
|
67
|
+
await seed(base);
|
|
68
|
+
const r = await req(base, 'GET', '/deals?sort=amount:asc');
|
|
69
|
+
assert.deepEqual(r.body.data.map((d) => d.amount), [100, 200, 300, 400]);
|
|
70
|
+
}));
|
|
71
|
+
|
|
72
|
+
test('invalid sort field → 400', () =>
|
|
73
|
+
withServer(async (base) => {
|
|
74
|
+
await seed(base);
|
|
75
|
+
const r = await req(base, 'GET', '/deals?sort=banana:asc');
|
|
76
|
+
assert.equal(r.status, 400);
|
|
77
|
+
}));
|
|
78
|
+
|
|
79
|
+
test('filters combine with stage', () =>
|
|
80
|
+
withServer(async (base) => {
|
|
81
|
+
await seed(base);
|
|
82
|
+
const r = await req(base, 'GET', '/deals?stage=won&minAmount=350');
|
|
83
|
+
assert.equal(r.body.total, 1);
|
|
84
|
+
assert.equal(r.body.data[0].amount, 400);
|
|
85
|
+
}));
|
|
86
|
+
|
|
87
|
+
test('pagination returns hasMore and a full filtered total', () =>
|
|
88
|
+
withServer(async (base) => {
|
|
89
|
+
await seed(base);
|
|
90
|
+
const page1 = await req(base, 'GET', '/deals?limit=2&offset=0&sort=amount:asc');
|
|
91
|
+
assert.equal(page1.body.total, 4);
|
|
92
|
+
assert.equal(page1.body.data.length, 2);
|
|
93
|
+
assert.equal(page1.body.hasMore, true);
|
|
94
|
+
const page2 = await req(base, 'GET', '/deals?limit=2&offset=2&sort=amount:asc');
|
|
95
|
+
assert.equal(page2.body.hasMore, false);
|
|
96
|
+
}));
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { test } from 'node:test';
|
|
2
|
+
import assert from 'node:assert/strict';
|
|
3
|
+
import { createApp } from '../src/app.js';
|
|
4
|
+
|
|
5
|
+
async function withServer(run) {
|
|
6
|
+
const server = createApp(':memory:');
|
|
7
|
+
await new Promise((res) => server.listen(0, res));
|
|
8
|
+
const { port } = server.address();
|
|
9
|
+
try { await run(`http://127.0.0.1:${port}`); } finally { await new Promise((res) => server.close(res)); }
|
|
10
|
+
}
|
|
11
|
+
async function req(base, method, path, body) {
|
|
12
|
+
const init = { method, headers: {} };
|
|
13
|
+
if (body !== undefined) { init.headers['content-type'] = 'application/json'; init.body = JSON.stringify(body); }
|
|
14
|
+
const res = await fetch(base + path, init);
|
|
15
|
+
const text = await res.text();
|
|
16
|
+
let json = null; try { json = text ? JSON.parse(text) : null; } catch { json = null; }
|
|
17
|
+
return { status: res.status, body: json };
|
|
18
|
+
}
|
|
19
|
+
const mkContact = () => ({ name: 'A', email: `a${Math.random().toString(36).slice(2)}@x.com` });
|
|
20
|
+
|
|
21
|
+
test('stats for unknown contact → 404', () =>
|
|
22
|
+
withServer(async (base) => {
|
|
23
|
+
const r = await req(base, 'GET', '/contacts/99999/stats');
|
|
24
|
+
assert.equal(r.status, 404);
|
|
25
|
+
}));
|
|
26
|
+
|
|
27
|
+
test('stats for a contact with no deals are all zero', () =>
|
|
28
|
+
withServer(async (base) => {
|
|
29
|
+
const c = await req(base, 'POST', '/contacts', mkContact());
|
|
30
|
+
const r = await req(base, 'GET', `/contacts/${c.body.id}/stats`);
|
|
31
|
+
assert.equal(r.status, 200);
|
|
32
|
+
assert.deepEqual(r.body, { totalDeals: 0, totalValue: 0, wonValue: 0, winRate: 0, weightedPipeline: 0 });
|
|
33
|
+
}));
|
|
34
|
+
|
|
35
|
+
test('totalValue and wonValue sum the right deals', () =>
|
|
36
|
+
withServer(async (base) => {
|
|
37
|
+
const c = await req(base, 'POST', '/contacts', mkContact());
|
|
38
|
+
const id = c.body.id;
|
|
39
|
+
await req(base, 'POST', `/contacts/${id}/deals`, { title: 'a', amount: 100, stage: 'won' });
|
|
40
|
+
await req(base, 'POST', `/contacts/${id}/deals`, { title: 'b', amount: 50, stage: 'lead' });
|
|
41
|
+
const r = await req(base, 'GET', `/contacts/${id}/stats`);
|
|
42
|
+
assert.equal(r.body.totalDeals, 2);
|
|
43
|
+
assert.equal(r.body.totalValue, 150);
|
|
44
|
+
assert.equal(r.body.wonValue, 100);
|
|
45
|
+
}));
|
|
46
|
+
|
|
47
|
+
test('winRate is won deals over total deals', () =>
|
|
48
|
+
withServer(async (base) => {
|
|
49
|
+
const c = await req(base, 'POST', '/contacts', mkContact());
|
|
50
|
+
const id = c.body.id;
|
|
51
|
+
await req(base, 'POST', `/contacts/${id}/deals`, { title: 'a', amount: 10, stage: 'won' });
|
|
52
|
+
for (const s of ['lead', 'qualified', 'lost']) await req(base, 'POST', `/contacts/${id}/deals`, { title: s, amount: 10, stage: s });
|
|
53
|
+
const r = await req(base, 'GET', `/contacts/${id}/stats`);
|
|
54
|
+
assert.equal(r.body.winRate, 0.25); // 1 of 4
|
|
55
|
+
}));
|
|
56
|
+
|
|
57
|
+
test('weightedPipeline applies per-stage probabilities (lead .1, qualified .4, won 1, lost 0)', () =>
|
|
58
|
+
withServer(async (base) => {
|
|
59
|
+
const c = await req(base, 'POST', '/contacts', mkContact());
|
|
60
|
+
const id = c.body.id;
|
|
61
|
+
await req(base, 'POST', `/contacts/${id}/deals`, { title: 'l', amount: 100, stage: 'lead' }); // 10
|
|
62
|
+
await req(base, 'POST', `/contacts/${id}/deals`, { title: 'q', amount: 100, stage: 'qualified' }); // 40
|
|
63
|
+
await req(base, 'POST', `/contacts/${id}/deals`, { title: 'w', amount: 100, stage: 'won' }); // 100
|
|
64
|
+
await req(base, 'POST', `/contacts/${id}/deals`, { title: 'x', amount: 100, stage: 'lost' }); // 0
|
|
65
|
+
const r = await req(base, 'GET', `/contacts/${id}/stats`);
|
|
66
|
+
assert.equal(r.body.weightedPipeline, 150);
|
|
67
|
+
}));
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The micro-CRM benchmark suite — a building, scored suite.
|
|
3
|
+
*
|
|
4
|
+
* Tier 1 (`crm-build`) builds the base Node + SQLite REST API from scratch (large single
|
|
5
|
+
* output — a genuine test of whether the model can emit a sizeable file). Tiers 2-5 each
|
|
6
|
+
* scaffold the WORKING base implementation and ask the agent to add one harder feature
|
|
7
|
+
* (atomic bulk insert, advanced querying, weighted analytics, idempotency). Each task is
|
|
8
|
+
* graded by its own hidden test file; the suite score is the fraction of all spec tests
|
|
9
|
+
* passed.
|
|
10
|
+
*
|
|
11
|
+
* Test files and reference implementations live as plain `.mjs` assets (read verbatim at
|
|
12
|
+
* runtime; copied into dist by scripts/copy-benchmark-assets.mjs).
|
|
13
|
+
*/
|
|
14
|
+
import type { BenchmarkTask, TaskFile } from '../types.js';
|
|
15
|
+
export declare const MICROCRM_PACKAGE_JSON: string;
|
|
16
|
+
/** Base API the agent builds in tier 1 and that scaffolds the extend tasks. */
|
|
17
|
+
export declare const MICROCRM_BASE_APP: string;
|
|
18
|
+
/** Reference with every extension implemented — used only by the harness self-test. */
|
|
19
|
+
export declare const MICROCRM_FULL_APP: string;
|
|
20
|
+
export declare const MICROCRM_TASKS: BenchmarkTask[];
|
|
21
|
+
/** Self-test helper: the known-good files that should pass a given task's tests. */
|
|
22
|
+
export declare function microcrmReferenceFor(task: BenchmarkTask): TaskFile;
|
|
23
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../src/code/benchmark/microcrm/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAKH,OAAO,KAAK,EAAE,aAAa,EAAE,QAAQ,EAAE,MAAM,aAAa,CAAC;AAY3D,eAAO,MAAM,qBAAqB,QAAsF,CAAC;AACzH,+EAA+E;AAC/E,eAAO,MAAM,iBAAiB,QAAiC,CAAC;AAChE,uFAAuF;AACvF,eAAO,MAAM,iBAAiB,QAAsC,CAAC;AA6CrE,eAAO,MAAM,cAAc,EAAE,aAAa,EAMzC,CAAC;AAEF,oFAAoF;AACpF,wBAAgB,oBAAoB,CAAC,IAAI,EAAE,aAAa,GAAG,QAAQ,CAIlE"}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The micro-CRM benchmark suite — a building, scored suite.
|
|
3
|
+
*
|
|
4
|
+
* Tier 1 (`crm-build`) builds the base Node + SQLite REST API from scratch (large single
|
|
5
|
+
* output — a genuine test of whether the model can emit a sizeable file). Tiers 2-5 each
|
|
6
|
+
* scaffold the WORKING base implementation and ask the agent to add one harder feature
|
|
7
|
+
* (atomic bulk insert, advanced querying, weighted analytics, idempotency). Each task is
|
|
8
|
+
* graded by its own hidden test file; the suite score is the fraction of all spec tests
|
|
9
|
+
* passed.
|
|
10
|
+
*
|
|
11
|
+
* Test files and reference implementations live as plain `.mjs` assets (read verbatim at
|
|
12
|
+
* runtime; copied into dist by scripts/copy-benchmark-assets.mjs).
|
|
13
|
+
*/
|
|
14
|
+
import { readFileSync } from 'fs';
|
|
15
|
+
import { fileURLToPath } from 'url';
|
|
16
|
+
import { dirname, join } from 'path';
|
|
17
|
+
import { MICROCRM_SPEC, MICROCRM_BULK_SPEC, MICROCRM_QUERY_SPEC, MICROCRM_STATS_SPEC, MICROCRM_IDEMPOTENCY_SPEC, } from './spec.js';
|
|
18
|
+
const assetsDir = join(dirname(fileURLToPath(import.meta.url)), 'assets');
|
|
19
|
+
const readAsset = (name) => readFileSync(join(assetsDir, name), 'utf-8');
|
|
20
|
+
export const MICROCRM_PACKAGE_JSON = JSON.stringify({ name: 'microcrm', type: 'module', private: true }, null, 2) + '\n';
|
|
21
|
+
/** Base API the agent builds in tier 1 and that scaffolds the extend tasks. */
|
|
22
|
+
export const MICROCRM_BASE_APP = readAsset('app.reference.mjs');
|
|
23
|
+
/** Reference with every extension implemented — used only by the harness self-test. */
|
|
24
|
+
export const MICROCRM_FULL_APP = readAsset('app.full.reference.mjs');
|
|
25
|
+
/** A base-build task (from scratch) — no src provided. */
|
|
26
|
+
function buildTask() {
|
|
27
|
+
return {
|
|
28
|
+
id: 'crm-build',
|
|
29
|
+
tier: 1,
|
|
30
|
+
title: 'Build the micro-CRM REST API',
|
|
31
|
+
capability: 'Build a spec-defined Node + SQLite REST API (large output)',
|
|
32
|
+
kind: 'scratch',
|
|
33
|
+
prompt: MICROCRM_SPEC,
|
|
34
|
+
scaffold: { 'package.json': MICROCRM_PACKAGE_JSON, 'test/api.test.mjs': readAsset('api.test.mjs') },
|
|
35
|
+
protectedPaths: ['test/api.test.mjs'],
|
|
36
|
+
maxIterations: 60,
|
|
37
|
+
timeoutMs: 600_000,
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
/** An extend task — scaffolds the working base app and adds one feature. */
|
|
41
|
+
function extendTask(id, tier, title, capability, prompt, testFile) {
|
|
42
|
+
return {
|
|
43
|
+
id,
|
|
44
|
+
tier,
|
|
45
|
+
title,
|
|
46
|
+
capability,
|
|
47
|
+
kind: 'extend',
|
|
48
|
+
prompt,
|
|
49
|
+
scaffold: {
|
|
50
|
+
'package.json': MICROCRM_PACKAGE_JSON,
|
|
51
|
+
'src/app.js': MICROCRM_BASE_APP,
|
|
52
|
+
[`test/${testFile}`]: readAsset(testFile),
|
|
53
|
+
},
|
|
54
|
+
protectedPaths: [`test/${testFile}`],
|
|
55
|
+
maxIterations: 40,
|
|
56
|
+
timeoutMs: 360_000,
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
export const MICROCRM_TASKS = [
|
|
60
|
+
buildTask(),
|
|
61
|
+
extendTask('crm-bulk', 2, 'Atomic bulk deal insert', 'Transactions / atomicity', MICROCRM_BULK_SPEC, 'bulk.test.mjs'),
|
|
62
|
+
extendTask('crm-query', 3, 'Advanced deal querying', 'Filtering, sorting & pagination', MICROCRM_QUERY_SPEC, 'query.test.mjs'),
|
|
63
|
+
extendTask('crm-stats', 4, 'Pipeline analytics', 'Aggregation & weighted arithmetic', MICROCRM_STATS_SPEC, 'stats.test.mjs'),
|
|
64
|
+
extendTask('crm-idempotency', 5, 'Idempotency-Key on create', 'Idempotency / dedup reasoning', MICROCRM_IDEMPOTENCY_SPEC, 'idempotency.test.mjs'),
|
|
65
|
+
];
|
|
66
|
+
/** Self-test helper: the known-good files that should pass a given task's tests. */
|
|
67
|
+
export function microcrmReferenceFor(task) {
|
|
68
|
+
const src = task.id === 'crm-build' ? MICROCRM_BASE_APP : MICROCRM_FULL_APP;
|
|
69
|
+
// Keep the task's test files, but override src/app.js with the reference implementation.
|
|
70
|
+
return { ...task.scaffold, 'package.json': MICROCRM_PACKAGE_JSON, 'src/app.js': src };
|
|
71
|
+
}
|
|
72
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../../../src/code/benchmark/microcrm/index.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The micro-CRM specification handed to the agent. It pins the exact module
|
|
3
|
+
* contract the hidden test suite depends on (entry file, exported factory, the
|
|
4
|
+
* built-in `node:http` + `node:sqlite` stack) and then enumerates every endpoint
|
|
5
|
+
* with its status codes and response shapes. Ambiguity here would unfairly fail a
|
|
6
|
+
* competent model, so the spec mirrors the test expectations precisely.
|
|
7
|
+
*/
|
|
8
|
+
export declare const MICROCRM_SPEC = "Build a small REST API for a \"micro CRM\" using ONLY Node.js built-in modules \u2014 no npm install, no external packages.\n\nSTACK (mandatory):\n- HTTP server: the built-in `node:http` module.\n- Database: the built-in `node:sqlite` module (import { DatabaseSync } from 'node:sqlite').\n- The project is an ES module (package.json has \"type\": \"module\").\n\nMODULE CONTRACT (the test suite imports exactly this):\n- Create the file src/app.js.\n- Export a function: `export function createApp(dbPath = ':memory:')`.\n- createApp must create the database (initialise the schema if needed) and return a\n Node http.Server created with http.createServer(handler). DO NOT call .listen() \u2014\n the caller starts it. Each call to createApp uses its own database connection.\n- All request and response bodies are JSON. Always set Content-Type: application/json\n (except for 204 responses, which have no body).\n\nDATA MODEL:\n- contact: { id (auto int), name (string, required, non-empty), email (string, required,\n unique, must look like an email), company (string or null), created_at (ISO string) }\n- deal: { id (auto int), contact_id (int), title (string, required, non-empty),\n amount (number >= 0, default 0), stage (one of \"lead\" | \"qualified\" | \"won\" | \"lost\",\n default \"lead\"), created_at (ISO string) }\n\nENDPOINTS:\n\nContacts:\n- POST /contacts \u2192 create. 201 with the created contact. 400 if name or email missing/invalid.\n 409 if the email already exists. 400 if the request body is not valid JSON.\n- GET /contacts \u2192 200 with { data: [...contacts], total }. Supports:\n ?q=<text> case-insensitive substring match on name OR email\n ?limit=&?offset= pagination; \"total\" is the FULL count ignoring limit/offset.\n- GET /contacts/:id \u2192 200 with the contact, or 404.\n- PATCH /contacts/:id \u2192 partial update (any subset of name/email/company). 200 with the\n updated contact. 404 if not found. 400 if a provided field is invalid. 409 if the new\n email belongs to another contact.\n- DELETE /contacts/:id \u2192 204 (no body), or 404. Deleting a contact also deletes its deals.\n\nDeals:\n- POST /contacts/:id/deals \u2192 create a deal for that contact. 201 with the created deal\n (stage defaults to \"lead\", amount defaults to 0). 404 if the contact does not exist.\n 400 if title is missing, amount is negative, or stage is not one of the four allowed values.\n- GET /contacts/:id/deals \u2192 200 with { data: [...deals], total } for that contact only.\n- GET /deals \u2192 200 with { data: [...deals], total }. Supports ?stage=<stage> to filter.\n- GET /deals/summary \u2192 200 with { summary: { lead: { count, totalAmount }, qualified: {...},\n won: {...}, lost: {...} } } \u2014 every stage present, zeros when there are no deals.\n- PATCH /deals/:id \u2192 partial update (title/amount/stage). 200 with the updated deal, or 404.\n 400 on invalid amount/stage.\n\nAnything else (unknown path) \u2192 404 with a JSON body.\n\nGOAL: make the hidden test suite in test/api.test.mjs pass. Run it with: node --test\nDo NOT modify anything under test/. Implement everything in src/.";
|
|
9
|
+
export declare const MICROCRM_BULK_SPEC = "src/app.js already contains a working micro-CRM REST API (contacts + deals) built on node:http + node:sqlite, exporting createApp(dbPath). Extend it \u2014 do not rewrite it, and keep all existing behaviour working. Run the tests with: node --test. Do NOT modify test/.\n\nAdd an ATOMIC bulk deal endpoint:\n- POST /contacts/:id/deals/bulk with body { deals: [ { title, amount?, stage? }, ... ] }.\n- 404 if the contact does not exist.\n- 400 if \"deals\" is missing, not an array, or empty.\n- Validate every deal with the same rules as a single deal (title required & non-empty,\n amount a number >= 0, stage one of lead/qualified/won/lost). If ANY item is invalid,\n respond 400 and insert NOTHING (the operation must be all-or-nothing / atomic).\n- On success \u2192 201 with { data: [...createdDeals], total }. Each created deal applies the\n same defaults as the single-deal endpoint (stage \"lead\", amount 0).\nMake test/bulk.test.mjs pass.";
|
|
10
|
+
export declare const MICROCRM_QUERY_SPEC = "src/app.js already contains a working micro-CRM REST API (contacts + deals) built on node:http + node:sqlite, exporting createApp(dbPath). Extend it \u2014 do not rewrite it, and keep all existing behaviour working. Run the tests with: node --test. Do NOT modify test/.\n\nUpgrade GET /deals with filtering, sorting and pagination metadata:\n- ?minAmount= and ?maxAmount= filter by amount (inclusive). A non-numeric value \u2192 400.\n- ?stage= filters by stage (already supported); all filters combine with AND.\n- ?sort=<field>:<dir> where field is one of amount | created_at | id and dir is asc | desc.\n Any other field or direction \u2192 400. Default order is by id ascending.\n- ?limit= and ?offset= paginate. The response is { data, total, hasMore } where \"total\" is\n the FULL count of rows matching the filters (ignoring limit/offset) and \"hasMore\" is true\n when there are more rows after the current page (offset + data.length < total).\nMake test/query.test.mjs pass.";
|
|
11
|
+
export declare const MICROCRM_STATS_SPEC = "src/app.js already contains a working micro-CRM REST API (contacts + deals) built on node:http + node:sqlite, exporting createApp(dbPath). Extend it \u2014 do not rewrite it, and keep all existing behaviour working. Run the tests with: node --test. Do NOT modify test/.\n\nAdd a per-contact analytics endpoint:\n- GET /contacts/:id/stats \u2192 404 if the contact does not exist, else 200 with:\n { totalDeals, totalValue, wonValue, winRate, weightedPipeline }\n where:\n - totalDeals = number of deals for the contact\n - totalValue = sum of all deal amounts\n - wonValue = sum of amounts of deals whose stage is \"won\"\n - winRate = wonDeals / totalDeals (0 when there are no deals), rounded to 4 decimals\n - weightedPipeline = sum over deals of amount * P(stage), rounded to 2 decimals, where\n P = { lead: 0.1, qualified: 0.4, won: 1.0, lost: 0.0 }\n A contact with no deals returns all zeros.\nMake test/stats.test.mjs pass.";
|
|
12
|
+
export declare const MICROCRM_IDEMPOTENCY_SPEC = "src/app.js already contains a working micro-CRM REST API (contacts + deals) built on node:http + node:sqlite, exporting createApp(dbPath). Extend it \u2014 do not rewrite it, and keep all existing behaviour working. Run the tests with: node --test. Do NOT modify test/.\n\nAdd idempotency to contact creation via the \"Idempotency-Key\" request header:\n- POST /contacts with an Idempotency-Key header: the FIRST request with a given key creates\n the contact as normal (201) and remembers the key.\n- A REPEAT request with the SAME key must NOT create a duplicate \u2014 return the originally\n created contact with status 200 (even if the new request body differs; do not validate or\n conflict-check it).\n- Different keys create different contacts. Requests with no Idempotency-Key behave exactly\n as before (each creates a contact). The key store may be in-memory (per createApp instance).\nMake test/idempotency.test.mjs pass.";
|
|
13
|
+
//# sourceMappingURL=spec.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"spec.d.ts","sourceRoot":"","sources":["../../../../src/code/benchmark/microcrm/spec.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,eAAO,MAAM,aAAa,4lGAmDwC,CAAC;AAWnE,eAAO,MAAM,kBAAkB,47BAWD,CAAC;AAE/B,eAAO,MAAM,mBAAmB,g+BAUD,CAAC;AAEhC,eAAO,MAAM,mBAAmB,w8BAaD,CAAC;AAEhC,eAAO,MAAM,yBAAyB,86BAUD,CAAC"}
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The micro-CRM specification handed to the agent. It pins the exact module
|
|
3
|
+
* contract the hidden test suite depends on (entry file, exported factory, the
|
|
4
|
+
* built-in `node:http` + `node:sqlite` stack) and then enumerates every endpoint
|
|
5
|
+
* with its status codes and response shapes. Ambiguity here would unfairly fail a
|
|
6
|
+
* competent model, so the spec mirrors the test expectations precisely.
|
|
7
|
+
*/
|
|
8
|
+
export const MICROCRM_SPEC = `Build a small REST API for a "micro CRM" using ONLY Node.js built-in modules — no npm install, no external packages.
|
|
9
|
+
|
|
10
|
+
STACK (mandatory):
|
|
11
|
+
- HTTP server: the built-in \`node:http\` module.
|
|
12
|
+
- Database: the built-in \`node:sqlite\` module (import { DatabaseSync } from 'node:sqlite').
|
|
13
|
+
- The project is an ES module (package.json has "type": "module").
|
|
14
|
+
|
|
15
|
+
MODULE CONTRACT (the test suite imports exactly this):
|
|
16
|
+
- Create the file src/app.js.
|
|
17
|
+
- Export a function: \`export function createApp(dbPath = ':memory:')\`.
|
|
18
|
+
- createApp must create the database (initialise the schema if needed) and return a
|
|
19
|
+
Node http.Server created with http.createServer(handler). DO NOT call .listen() —
|
|
20
|
+
the caller starts it. Each call to createApp uses its own database connection.
|
|
21
|
+
- All request and response bodies are JSON. Always set Content-Type: application/json
|
|
22
|
+
(except for 204 responses, which have no body).
|
|
23
|
+
|
|
24
|
+
DATA MODEL:
|
|
25
|
+
- contact: { id (auto int), name (string, required, non-empty), email (string, required,
|
|
26
|
+
unique, must look like an email), company (string or null), created_at (ISO string) }
|
|
27
|
+
- deal: { id (auto int), contact_id (int), title (string, required, non-empty),
|
|
28
|
+
amount (number >= 0, default 0), stage (one of "lead" | "qualified" | "won" | "lost",
|
|
29
|
+
default "lead"), created_at (ISO string) }
|
|
30
|
+
|
|
31
|
+
ENDPOINTS:
|
|
32
|
+
|
|
33
|
+
Contacts:
|
|
34
|
+
- POST /contacts → create. 201 with the created contact. 400 if name or email missing/invalid.
|
|
35
|
+
409 if the email already exists. 400 if the request body is not valid JSON.
|
|
36
|
+
- GET /contacts → 200 with { data: [...contacts], total }. Supports:
|
|
37
|
+
?q=<text> case-insensitive substring match on name OR email
|
|
38
|
+
?limit=&?offset= pagination; "total" is the FULL count ignoring limit/offset.
|
|
39
|
+
- GET /contacts/:id → 200 with the contact, or 404.
|
|
40
|
+
- PATCH /contacts/:id → partial update (any subset of name/email/company). 200 with the
|
|
41
|
+
updated contact. 404 if not found. 400 if a provided field is invalid. 409 if the new
|
|
42
|
+
email belongs to another contact.
|
|
43
|
+
- DELETE /contacts/:id → 204 (no body), or 404. Deleting a contact also deletes its deals.
|
|
44
|
+
|
|
45
|
+
Deals:
|
|
46
|
+
- POST /contacts/:id/deals → create a deal for that contact. 201 with the created deal
|
|
47
|
+
(stage defaults to "lead", amount defaults to 0). 404 if the contact does not exist.
|
|
48
|
+
400 if title is missing, amount is negative, or stage is not one of the four allowed values.
|
|
49
|
+
- GET /contacts/:id/deals → 200 with { data: [...deals], total } for that contact only.
|
|
50
|
+
- GET /deals → 200 with { data: [...deals], total }. Supports ?stage=<stage> to filter.
|
|
51
|
+
- GET /deals/summary → 200 with { summary: { lead: { count, totalAmount }, qualified: {...},
|
|
52
|
+
won: {...}, lost: {...} } } — every stage present, zeros when there are no deals.
|
|
53
|
+
- PATCH /deals/:id → partial update (title/amount/stage). 200 with the updated deal, or 404.
|
|
54
|
+
400 on invalid amount/stage.
|
|
55
|
+
|
|
56
|
+
Anything else (unknown path) → 404 with a JSON body.
|
|
57
|
+
|
|
58
|
+
GOAL: make the hidden test suite in test/api.test.mjs pass. Run it with: node --test
|
|
59
|
+
Do NOT modify anything under test/. Implement everything in src/.`;
|
|
60
|
+
/**
|
|
61
|
+
* The extend tasks scaffold a WORKING base API (src/app.js) and ask the agent to add
|
|
62
|
+
* one harder feature. Each prompt pins the precise contract its hidden test depends on.
|
|
63
|
+
*/
|
|
64
|
+
const EXTEND_PREAMBLE = 'src/app.js already contains a working micro-CRM REST API (contacts + deals) built on ' +
|
|
65
|
+
'node:http + node:sqlite, exporting createApp(dbPath). Extend it — do not rewrite it, and ' +
|
|
66
|
+
'keep all existing behaviour working. Run the tests with: node --test. Do NOT modify test/.';
|
|
67
|
+
export const MICROCRM_BULK_SPEC = `${EXTEND_PREAMBLE}
|
|
68
|
+
|
|
69
|
+
Add an ATOMIC bulk deal endpoint:
|
|
70
|
+
- POST /contacts/:id/deals/bulk with body { deals: [ { title, amount?, stage? }, ... ] }.
|
|
71
|
+
- 404 if the contact does not exist.
|
|
72
|
+
- 400 if "deals" is missing, not an array, or empty.
|
|
73
|
+
- Validate every deal with the same rules as a single deal (title required & non-empty,
|
|
74
|
+
amount a number >= 0, stage one of lead/qualified/won/lost). If ANY item is invalid,
|
|
75
|
+
respond 400 and insert NOTHING (the operation must be all-or-nothing / atomic).
|
|
76
|
+
- On success → 201 with { data: [...createdDeals], total }. Each created deal applies the
|
|
77
|
+
same defaults as the single-deal endpoint (stage "lead", amount 0).
|
|
78
|
+
Make test/bulk.test.mjs pass.`;
|
|
79
|
+
export const MICROCRM_QUERY_SPEC = `${EXTEND_PREAMBLE}
|
|
80
|
+
|
|
81
|
+
Upgrade GET /deals with filtering, sorting and pagination metadata:
|
|
82
|
+
- ?minAmount= and ?maxAmount= filter by amount (inclusive). A non-numeric value → 400.
|
|
83
|
+
- ?stage= filters by stage (already supported); all filters combine with AND.
|
|
84
|
+
- ?sort=<field>:<dir> where field is one of amount | created_at | id and dir is asc | desc.
|
|
85
|
+
Any other field or direction → 400. Default order is by id ascending.
|
|
86
|
+
- ?limit= and ?offset= paginate. The response is { data, total, hasMore } where "total" is
|
|
87
|
+
the FULL count of rows matching the filters (ignoring limit/offset) and "hasMore" is true
|
|
88
|
+
when there are more rows after the current page (offset + data.length < total).
|
|
89
|
+
Make test/query.test.mjs pass.`;
|
|
90
|
+
export const MICROCRM_STATS_SPEC = `${EXTEND_PREAMBLE}
|
|
91
|
+
|
|
92
|
+
Add a per-contact analytics endpoint:
|
|
93
|
+
- GET /contacts/:id/stats → 404 if the contact does not exist, else 200 with:
|
|
94
|
+
{ totalDeals, totalValue, wonValue, winRate, weightedPipeline }
|
|
95
|
+
where:
|
|
96
|
+
- totalDeals = number of deals for the contact
|
|
97
|
+
- totalValue = sum of all deal amounts
|
|
98
|
+
- wonValue = sum of amounts of deals whose stage is "won"
|
|
99
|
+
- winRate = wonDeals / totalDeals (0 when there are no deals), rounded to 4 decimals
|
|
100
|
+
- weightedPipeline = sum over deals of amount * P(stage), rounded to 2 decimals, where
|
|
101
|
+
P = { lead: 0.1, qualified: 0.4, won: 1.0, lost: 0.0 }
|
|
102
|
+
A contact with no deals returns all zeros.
|
|
103
|
+
Make test/stats.test.mjs pass.`;
|
|
104
|
+
export const MICROCRM_IDEMPOTENCY_SPEC = `${EXTEND_PREAMBLE}
|
|
105
|
+
|
|
106
|
+
Add idempotency to contact creation via the "Idempotency-Key" request header:
|
|
107
|
+
- POST /contacts with an Idempotency-Key header: the FIRST request with a given key creates
|
|
108
|
+
the contact as normal (201) and remembers the key.
|
|
109
|
+
- A REPEAT request with the SAME key must NOT create a duplicate — return the originally
|
|
110
|
+
created contact with status 200 (even if the new request body differs; do not validate or
|
|
111
|
+
conflict-check it).
|
|
112
|
+
- Different keys create different contacts. Requests with no Idempotency-Key behave exactly
|
|
113
|
+
as before (each creates a contact). The key store may be in-memory (per createApp instance).
|
|
114
|
+
Make test/idempotency.test.mjs pass.`;
|
|
115
|
+
//# sourceMappingURL=spec.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"spec.js","sourceRoot":"","sources":["../../../../src/code/benchmark/microcrm/spec.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,MAAM,CAAC,MAAM,aAAa,GAAG;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;kEAmDqC,CAAC;AAEnE;;;GAGG;AACH,MAAM,eAAe,GACnB,uFAAuF;IACvF,2FAA2F;IAC3F,4FAA4F,CAAC;AAE/F,MAAM,CAAC,MAAM,kBAAkB,GAAG,GAAG,eAAe;;;;;;;;;;;8BAWtB,CAAC;AAE/B,MAAM,CAAC,MAAM,mBAAmB,GAAG,GAAG,eAAe;;;;;;;;;;+BAUtB,CAAC;AAEhC,MAAM,CAAC,MAAM,mBAAmB,GAAG,GAAG,eAAe;;;;;;;;;;;;;+BAatB,CAAC;AAEhC,MAAM,CAAC,MAAM,yBAAyB,GAAG,GAAG,eAAe;;;;;;;;;;qCAUtB,CAAC"}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared ModelOrchestrator builder for the benchmark, used by both the CLI
|
|
3
|
+
* command and the HTTP route. Mirrors the model-construction logic in the CLI
|
|
4
|
+
* `run` command so benchmarks exercise exactly the same model stack a real
|
|
5
|
+
* code-mode session would.
|
|
6
|
+
*/
|
|
7
|
+
import { ModelOrchestrator } from '../../models/orchestrator.js';
|
|
8
|
+
/** Subset of the stored ModelConfig shape we need to build a client. */
|
|
9
|
+
export interface ModelConfigInput {
|
|
10
|
+
endpoint: string;
|
|
11
|
+
apiKey?: string;
|
|
12
|
+
/** Preferred model id; `defaultModel` is the config-store alias. */
|
|
13
|
+
model?: string;
|
|
14
|
+
defaultModel?: string;
|
|
15
|
+
useHarmonyFormat?: boolean;
|
|
16
|
+
defaultReasoningEffort?: 'low' | 'medium' | 'high';
|
|
17
|
+
reasoningEffortStrategy?: 'api_param' | 'system_prompt' | 'both';
|
|
18
|
+
defaultMaxTokens?: number;
|
|
19
|
+
useGoogleADC?: boolean;
|
|
20
|
+
}
|
|
21
|
+
export interface OrchestratorInput {
|
|
22
|
+
reasoning: ModelConfigInput;
|
|
23
|
+
multimodal?: ModelConfigInput;
|
|
24
|
+
toolCalling?: ModelConfigInput;
|
|
25
|
+
/** When true, ping each model first; throws if the reasoning model is unreachable. */
|
|
26
|
+
testConnectivity?: boolean;
|
|
27
|
+
/** Optional progress logger. */
|
|
28
|
+
log?: (msg: string) => void;
|
|
29
|
+
}
|
|
30
|
+
export interface OrchestratorBundle {
|
|
31
|
+
orchestrator: ModelOrchestrator;
|
|
32
|
+
/** A short label for reports, e.g. the reasoning model id. */
|
|
33
|
+
modelLabel: string;
|
|
34
|
+
}
|
|
35
|
+
export declare function createOrchestrator(input: OrchestratorInput): Promise<OrchestratorBundle>;
|
|
36
|
+
//# sourceMappingURL=orchestrator-factory.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"orchestrator-factory.d.ts","sourceRoot":"","sources":["../../../src/code/benchmark/orchestrator-factory.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAGH,OAAO,EAAE,iBAAiB,EAAE,MAAM,8BAA8B,CAAC;AAEjE,wEAAwE;AACxE,MAAM,WAAW,gBAAgB;IAC/B,QAAQ,EAAE,MAAM,CAAC;IACjB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,oEAAoE;IACpE,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,gBAAgB,CAAC,EAAE,OAAO,CAAC;IAC3B,sBAAsB,CAAC,EAAE,KAAK,GAAG,QAAQ,GAAG,MAAM,CAAC;IACnD,uBAAuB,CAAC,EAAE,WAAW,GAAG,eAAe,GAAG,MAAM,CAAC;IACjE,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,YAAY,CAAC,EAAE,OAAO,CAAC;CACxB;AAED,MAAM,WAAW,iBAAiB;IAChC,SAAS,EAAE,gBAAgB,CAAC;IAC5B,UAAU,CAAC,EAAE,gBAAgB,CAAC;IAC9B,WAAW,CAAC,EAAE,gBAAgB,CAAC;IAC/B,sFAAsF;IACtF,gBAAgB,CAAC,EAAE,OAAO,CAAC;IAC3B,gCAAgC;IAChC,GAAG,CAAC,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,IAAI,CAAC;CAC7B;AAED,MAAM,WAAW,kBAAkB;IACjC,YAAY,EAAE,iBAAiB,CAAC;IAChC,8DAA8D;IAC9D,UAAU,EAAE,MAAM,CAAC;CACpB;AAoBD,wBAAsB,kBAAkB,CAAC,KAAK,EAAE,iBAAiB,GAAG,OAAO,CAAC,kBAAkB,CAAC,CAoC9F"}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared ModelOrchestrator builder for the benchmark, used by both the CLI
|
|
3
|
+
* command and the HTTP route. Mirrors the model-construction logic in the CLI
|
|
4
|
+
* `run` command so benchmarks exercise exactly the same model stack a real
|
|
5
|
+
* code-mode session would.
|
|
6
|
+
*/
|
|
7
|
+
import { createModelClient } from '../../models/model-client.js';
|
|
8
|
+
import { ModelOrchestrator } from '../../models/orchestrator.js';
|
|
9
|
+
function resolveModelId(cfg) {
|
|
10
|
+
return cfg.model || cfg.defaultModel || '';
|
|
11
|
+
}
|
|
12
|
+
function buildClient(cfg, type) {
|
|
13
|
+
return createModelClient({
|
|
14
|
+
endpoint: cfg.endpoint,
|
|
15
|
+
apiKey: cfg.apiKey ?? '',
|
|
16
|
+
model: resolveModelId(cfg),
|
|
17
|
+
type,
|
|
18
|
+
useHarmonyFormat: cfg.useHarmonyFormat,
|
|
19
|
+
defaultReasoningEffort: cfg.defaultReasoningEffort,
|
|
20
|
+
reasoningEffortStrategy: cfg.reasoningEffortStrategy,
|
|
21
|
+
defaultMaxTokens: cfg.defaultMaxTokens,
|
|
22
|
+
useGoogleADC: cfg.useGoogleADC,
|
|
23
|
+
});
|
|
24
|
+
}
|
|
25
|
+
export async function createOrchestrator(input) {
|
|
26
|
+
const log = input.log ?? (() => { });
|
|
27
|
+
if (!input.reasoning?.endpoint) {
|
|
28
|
+
throw new Error('A reasoning model configuration is required for the benchmark');
|
|
29
|
+
}
|
|
30
|
+
const reasoningModel = buildClient(input.reasoning, 'reasoning');
|
|
31
|
+
if (input.testConnectivity) {
|
|
32
|
+
const t = await reasoningModel.testConnectivity();
|
|
33
|
+
if (!t.success) {
|
|
34
|
+
throw new Error(`Reasoning model connection failed: ${t.error}`);
|
|
35
|
+
}
|
|
36
|
+
log(`Reasoning model connected (${t.latency}ms)`);
|
|
37
|
+
}
|
|
38
|
+
let multimodalModel = input.multimodal ? buildClient(input.multimodal, 'multimodal') : undefined;
|
|
39
|
+
if (multimodalModel && input.testConnectivity) {
|
|
40
|
+
const t = await multimodalModel.testConnectivity();
|
|
41
|
+
if (!t.success) {
|
|
42
|
+
log(`Multimodal model unreachable, continuing without it: ${t.error}`);
|
|
43
|
+
multimodalModel = undefined;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
let toolCallingModel = input.toolCalling ? buildClient(input.toolCalling, 'tool-calling') : undefined;
|
|
47
|
+
if (toolCallingModel && input.testConnectivity) {
|
|
48
|
+
const t = await toolCallingModel.testConnectivity();
|
|
49
|
+
if (!t.success) {
|
|
50
|
+
log(`Tool-calling model unreachable, continuing without it: ${t.error}`);
|
|
51
|
+
toolCallingModel = undefined;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
const orchestrator = new ModelOrchestrator({ reasoningModel, multimodalModel, toolCallingModel });
|
|
55
|
+
return { orchestrator, modelLabel: resolveModelId(input.reasoning) || 'unknown' };
|
|
56
|
+
}
|
|
57
|
+
//# sourceMappingURL=orchestrator-factory.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"orchestrator-factory.js","sourceRoot":"","sources":["../../../src/code/benchmark/orchestrator-factory.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,EAAE,iBAAiB,EAAE,MAAM,8BAA8B,CAAC;AACjE,OAAO,EAAE,iBAAiB,EAAE,MAAM,8BAA8B,CAAC;AAgCjE,SAAS,cAAc,CAAC,GAAqB;IAC3C,OAAO,GAAG,CAAC,KAAK,IAAI,GAAG,CAAC,YAAY,IAAI,EAAE,CAAC;AAC7C,CAAC;AAED,SAAS,WAAW,CAAC,GAAqB,EAAE,IAAiD;IAC3F,OAAO,iBAAiB,CAAC;QACvB,QAAQ,EAAE,GAAG,CAAC,QAAQ;QACtB,MAAM,EAAE,GAAG,CAAC,MAAM,IAAI,EAAE;QACxB,KAAK,EAAE,cAAc,CAAC,GAAG,CAAC;QAC1B,IAAI;QACJ,gBAAgB,EAAE,GAAG,CAAC,gBAAgB;QACtC,sBAAsB,EAAE,GAAG,CAAC,sBAAsB;QAClD,uBAAuB,EAAE,GAAG,CAAC,uBAAuB;QACpD,gBAAgB,EAAE,GAAG,CAAC,gBAAgB;QACtC,YAAY,EAAE,GAAG,CAAC,YAAY;KAC/B,CAAC,CAAC;AACL,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,kBAAkB,CAAC,KAAwB;IAC/D,MAAM,GAAG,GAAG,KAAK,CAAC,GAAG,IAAI,CAAC,GAAG,EAAE,GAAE,CAAC,CAAC,CAAC;IAEpC,IAAI,CAAC,KAAK,CAAC,SAAS,EAAE,QAAQ,EAAE,CAAC;QAC/B,MAAM,IAAI,KAAK,CAAC,+DAA+D,CAAC,CAAC;IACnF,CAAC;IAED,MAAM,cAAc,GAAG,WAAW,CAAC,KAAK,CAAC,SAAS,EAAE,WAAW,CAAC,CAAC;IACjE,IAAI,KAAK,CAAC,gBAAgB,EAAE,CAAC;QAC3B,MAAM,CAAC,GAAG,MAAM,cAAc,CAAC,gBAAgB,EAAE,CAAC;QAClD,IAAI,CAAC,CAAC,CAAC,OAAO,EAAE,CAAC;YACf,MAAM,IAAI,KAAK,CAAC,sCAAsC,CAAC,CAAC,KAAK,EAAE,CAAC,CAAC;QACnE,CAAC;QACD,GAAG,CAAC,8BAA8B,CAAC,CAAC,OAAO,KAAK,CAAC,CAAC;IACpD,CAAC;IAED,IAAI,eAAe,GAAG,KAAK,CAAC,UAAU,CAAC,CAAC,CAAC,WAAW,CAAC,KAAK,CAAC,UAAU,EAAE,YAAY,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC;IACjG,IAAI,eAAe,IAAI,KAAK,CAAC,gBAAgB,EAAE,CAAC;QAC9C,MAAM,CAAC,GAAG,MAAM,eAAe,CAAC,gBAAgB,EAAE,CAAC;QACnD,IAAI,CAAC,CAAC,CAAC,OAAO,EAAE,CAAC;YACf,GAAG,CAAC,wDAAwD,CAAC,CAAC,KAAK,EAAE,CAAC,CAAC;YACvE,eAAe,GAAG,SAAS,CAAC;QAC9B,CAAC;IACH,CAAC;IAED,IAAI,gBAAgB,GAAG,KAAK,CAAC,WAAW,CAAC,CAAC,CAAC,WAAW,CAAC,KAAK,CAAC,WAAW,EAAE,cAAc,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC;IACtG,IAAI,gBAAgB,IAAI,KAAK,CAAC,gBAAgB,EAAE,CAAC;QAC/C,MAAM,CAAC,GAAG,MAAM,gBAAgB,CAAC,gBAAgB,EAAE,CAAC;QACpD,IAAI,CAAC,CAAC,CAAC,OAAO,EAAE,CAAC;YACf,GAAG,CAAC,0DAA0D,CAAC,CAAC,KAAK,EAAE,CAAC,CAAC;YACzE,gBAAgB,GAAG,SAAS,CAAC;QAC/B,CAAC;IACH,CAAC;IAED,MAAM,YAAY,GAAG,IAAI,iBAAiB,CAAC,EAAE,cAAc,EAAE,eAAe,EAAE,gBAAgB,EAAE,CAAC,CAAC;IAClG,OAAO,EAAE,YAAY,EAAE,UAAU,EAAE,cAAc,CAAC,KAAK,CAAC,SAAS,CAAC,IAAI,SAAS,EAAE,CAAC;AACpF,CAAC"}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Report formatting for benchmark results — a chalk table for the CLI and a
|
|
3
|
+
* plain JSON serializer for files / HTTP.
|
|
4
|
+
*/
|
|
5
|
+
import type { SuiteResult } from './types.js';
|
|
6
|
+
export declare function formatReportForCLI(suite: SuiteResult): string;
|
|
7
|
+
export declare function toReportJSON(suite: SuiteResult): string;
|
|
8
|
+
//# sourceMappingURL=report.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"report.d.ts","sourceRoot":"","sources":["../../../src/code/benchmark/report.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAGH,OAAO,KAAK,EAAE,WAAW,EAAc,MAAM,YAAY,CAAC;AAgB1D,wBAAgB,kBAAkB,CAAC,KAAK,EAAE,WAAW,GAAG,MAAM,CAgG7D;AAED,wBAAgB,YAAY,CAAC,KAAK,EAAE,WAAW,GAAG,MAAM,CAEvD"}
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Report formatting for benchmark results — a chalk table for the CLI and a
|
|
3
|
+
* plain JSON serializer for files / HTTP.
|
|
4
|
+
*/
|
|
5
|
+
import chalk from 'chalk';
|
|
6
|
+
function fmtMs(ms) {
|
|
7
|
+
return ms < 1000 ? `${ms}ms` : `${(ms / 1000).toFixed(1)}s`;
|
|
8
|
+
}
|
|
9
|
+
function pad(s, n) {
|
|
10
|
+
return s.length >= n ? s.slice(0, n) : s + ' '.repeat(n - s.length);
|
|
11
|
+
}
|
|
12
|
+
function statusCell(r) {
|
|
13
|
+
if (r.passed)
|
|
14
|
+
return chalk.green('PASS');
|
|
15
|
+
const reason = r.reason ?? 'fail';
|
|
16
|
+
return chalk.red('FAIL') + chalk.gray(` ${reason}`);
|
|
17
|
+
}
|
|
18
|
+
export function formatReportForCLI(suite) {
|
|
19
|
+
const lines = [];
|
|
20
|
+
lines.push('');
|
|
21
|
+
const scored = suite.scoring === 'scored';
|
|
22
|
+
lines.push(chalk.bold(` Jiva Code-Mode Benchmark — ${suite.suiteName ?? 'taskstore'}`));
|
|
23
|
+
if (suite.model)
|
|
24
|
+
lines.push(chalk.gray(` Model: ${suite.model}`));
|
|
25
|
+
lines.push(chalk.gray(` ${suite.startedAt} → ${suite.finishedAt}`));
|
|
26
|
+
lines.push('');
|
|
27
|
+
// Header
|
|
28
|
+
lines.push(chalk.gray(' ' +
|
|
29
|
+
pad('Tier', 5) +
|
|
30
|
+
pad('Task', 26) +
|
|
31
|
+
pad('Status', 18) +
|
|
32
|
+
pad('Iters', 7) +
|
|
33
|
+
pad('Tests', 9) +
|
|
34
|
+
pad('Tokens', 10) +
|
|
35
|
+
'Time'));
|
|
36
|
+
lines.push(chalk.gray(' ' + '─'.repeat(86)));
|
|
37
|
+
for (const r of suite.tasks) {
|
|
38
|
+
const iters = `${r.iterations}${r.hitMaxIterations ? '!' : ''}`;
|
|
39
|
+
const tests = `${r.testsPassed}/${r.testsPassed + r.testsFailed}`;
|
|
40
|
+
const tokens = r.tokenUsage?.totalTokens ? String(r.tokenUsage.totalTokens) : '-';
|
|
41
|
+
lines.push(' ' +
|
|
42
|
+
pad(String(r.tier), 5) +
|
|
43
|
+
pad(r.title, 26) +
|
|
44
|
+
pad(statusCell(r), 18 + 10) + // +10 to absorb chalk color codes width
|
|
45
|
+
pad(iters, 7) +
|
|
46
|
+
pad(tests, 9) +
|
|
47
|
+
pad(tokens, 10) +
|
|
48
|
+
fmtMs(r.wallTimeMs));
|
|
49
|
+
}
|
|
50
|
+
lines.push(chalk.gray(' ' + '─'.repeat(86)));
|
|
51
|
+
if (scored) {
|
|
52
|
+
// Headline for scored suites is the spec-test pass-rate, not all-or-nothing.
|
|
53
|
+
const score = `${suite.totalTestsPassed}/${suite.totalTestsRun} tests (${suite.scorePct}%)`;
|
|
54
|
+
const colour = suite.scorePct >= 90 ? chalk.green : suite.scorePct >= 50 ? chalk.yellow : chalk.red;
|
|
55
|
+
lines.push(colour(` Score: ${score}`));
|
|
56
|
+
}
|
|
57
|
+
else {
|
|
58
|
+
const ratio = `${suite.passed}/${suite.totalTasks}`;
|
|
59
|
+
const summary = suite.passed === suite.totalTasks ? chalk.green(` ${ratio} passed`) : chalk.yellow(` ${ratio} passed`);
|
|
60
|
+
lines.push(summary + chalk.gray(` · highest tier passed: ${suite.highestTierPassed}`));
|
|
61
|
+
}
|
|
62
|
+
lines.push(chalk.gray(` total time: ${fmtMs(suite.totalWallTimeMs)}` +
|
|
63
|
+
(suite.totalTokens ? ` · total tokens: ${suite.totalTokens}` : '')));
|
|
64
|
+
// Scored suites: list the specific spec tests the model missed (the capability gaps).
|
|
65
|
+
if (scored) {
|
|
66
|
+
const missed = suite.tasks.flatMap((t) => t.failingTests ?? []);
|
|
67
|
+
if (missed.length > 0) {
|
|
68
|
+
lines.push('');
|
|
69
|
+
lines.push(chalk.bold(` Missed (${missed.length})`));
|
|
70
|
+
for (const name of missed)
|
|
71
|
+
lines.push(chalk.red(' ✗ ') + chalk.gray(name));
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
// Output-length limitation flag: failures driven by output-token truncation are a
|
|
75
|
+
// model capacity limit (e.g. a hard 4096-token cap), distinct from a logic gap.
|
|
76
|
+
const outputLimited = suite.tasks.filter((t) => t.outputLimited);
|
|
77
|
+
if (outputLimited.length > 0) {
|
|
78
|
+
lines.push('');
|
|
79
|
+
lines.push(chalk.yellow(' ⚠ Output-length limited') +
|
|
80
|
+
chalk.gray(` — ${outputLimited.length} task(s) failed after hitting the model's output-token limit:`));
|
|
81
|
+
for (const t of outputLimited) {
|
|
82
|
+
lines.push(chalk.gray(` ${t.id} (${t.truncationEvents}× truncation) — couldn't emit a large enough response`));
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
// Surface failures with their diagnostic notes (infra/agent errors, not per-test).
|
|
86
|
+
const failures = suite.tasks.filter((t) => !t.passed);
|
|
87
|
+
if (failures.length > 0) {
|
|
88
|
+
lines.push('');
|
|
89
|
+
lines.push(chalk.bold(' Failures'));
|
|
90
|
+
for (const f of failures) {
|
|
91
|
+
const tag = f.outputLimited ? chalk.yellow(' [output-limited]') : '';
|
|
92
|
+
lines.push(chalk.red(` • ${f.id}`) + chalk.gray(` (${f.reason ?? 'fail'})`) + tag);
|
|
93
|
+
if (f.notes)
|
|
94
|
+
lines.push(chalk.gray(` ${f.notes}`));
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
lines.push('');
|
|
98
|
+
return lines.join('\n');
|
|
99
|
+
}
|
|
100
|
+
export function toReportJSON(suite) {
|
|
101
|
+
return JSON.stringify(suite, null, 2);
|
|
102
|
+
}
|
|
103
|
+
//# sourceMappingURL=report.js.map
|