@polderlabs/bizar 4.8.0 → 5.0.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.
- package/bizar-dash/dist/assets/icons-CFqu2M-c.js +656 -0
- package/bizar-dash/dist/assets/icons-CFqu2M-c.js.map +1 -0
- package/bizar-dash/dist/assets/index-DmpSFPJY.js +9 -0
- package/bizar-dash/dist/assets/index-DmpSFPJY.js.map +1 -0
- package/bizar-dash/dist/assets/main-Dl8yY5_H.js +16 -0
- package/bizar-dash/dist/assets/main-Dl8yY5_H.js.map +1 -0
- package/bizar-dash/dist/assets/{main-DX_Jh8Wc.css → main-ZAfGKENE.css} +1 -1
- package/bizar-dash/dist/assets/markdown-DIquRulQ.js +29 -0
- package/bizar-dash/dist/assets/markdown-DIquRulQ.js.map +1 -0
- package/bizar-dash/dist/assets/mobile-C2gysFOZ.js +2 -0
- package/bizar-dash/dist/assets/mobile-C2gysFOZ.js.map +1 -0
- package/bizar-dash/dist/assets/mobile-DHXXbn1A.js +1 -0
- package/bizar-dash/dist/assets/{mobile-Chvf9u_B.js.map → mobile-DHXXbn1A.js.map} +1 -1
- package/bizar-dash/dist/assets/react-vendor-DZRUXSPQ.js +40 -0
- package/bizar-dash/dist/assets/react-vendor-DZRUXSPQ.js.map +1 -0
- package/bizar-dash/dist/index.html +6 -3
- package/bizar-dash/dist/mobile.html +5 -2
- package/bizar-dash/node_modules/.vite/vitest/da39a3ee5e6b4b0d3255bfef95601890afd80709/results.json +1 -1
- package/bizar-dash/skills/eval/SKILL.md +237 -0
- package/bizar-dash/src/server/api.mjs +28 -0
- package/bizar-dash/src/server/auth.mjs +155 -1
- package/bizar-dash/src/server/eval-store.mjs +226 -0
- package/bizar-dash/src/server/eval.mjs +347 -0
- package/bizar-dash/src/server/memory-lightrag.mjs +109 -0
- package/bizar-dash/src/server/memory-store.mjs +121 -0
- package/bizar-dash/src/server/ocr.mjs +55 -0
- package/bizar-dash/src/server/otel.mjs +133 -0
- package/bizar-dash/src/server/plugins/registry.mjs +363 -0
- package/bizar-dash/src/server/plugins/sandbox.mjs +655 -0
- package/bizar-dash/src/server/plugins/store.mjs +659 -0
- package/bizar-dash/src/server/routes/chat.mjs +246 -170
- package/bizar-dash/src/server/routes/clipboard.mjs +173 -0
- package/bizar-dash/src/server/routes/eval.mjs +147 -0
- package/bizar-dash/src/server/routes/memory.mjs +46 -0
- package/bizar-dash/src/server/routes/ocr.mjs +182 -0
- package/bizar-dash/src/server/routes/opencode-sessions.mjs +82 -48
- package/bizar-dash/src/server/routes/plugins.mjs +220 -0
- package/bizar-dash/src/server/routes/users.mjs +84 -0
- package/bizar-dash/src/server/routes/voice.mjs +131 -0
- package/bizar-dash/src/server/routes/workspaces.mjs +204 -0
- package/bizar-dash/src/server/server.mjs +40 -0
- package/bizar-dash/src/server/voice-store.mjs +202 -0
- package/bizar-dash/src/server/voice-transcribe.mjs +72 -0
- package/bizar-dash/src/server/workspaces.mjs +626 -0
- package/bizar-dash/src/web/components/InviteDialog.tsx +205 -0
- package/bizar-dash/src/web/components/ScreenshotCapture.tsx +138 -0
- package/bizar-dash/src/web/components/ScreenshotOCR.tsx +42 -0
- package/bizar-dash/src/web/components/SettingsSearch.tsx +204 -89
- package/bizar-dash/src/web/components/VoiceNotesPanel.tsx +182 -0
- package/bizar-dash/src/web/components/VoiceRecorder.tsx +104 -0
- package/bizar-dash/src/web/components/WorkspaceSelector.tsx +158 -0
- package/bizar-dash/src/web/lib/search.ts +115 -0
- package/bizar-dash/src/web/mobile/views/MobileSettings.tsx +10 -35
- package/bizar-dash/src/web/mobile/views/QrCodePanel.tsx +69 -0
- package/bizar-dash/src/web/styles/memory.css +166 -1
- package/bizar-dash/src/web/styles/settings.css +80 -0
- package/bizar-dash/src/web/views/Memory.tsx +22 -2
- package/bizar-dash/src/web/views/Settings.tsx +99 -0
- package/bizar-dash/src/web/views/Workspace.tsx +294 -0
- package/bizar-dash/src/web/views/memory/FromScreenshotPanel.tsx +23 -0
- package/bizar-dash/src/web/views/memory/MemoryGraphLegend.tsx +29 -0
- package/bizar-dash/src/web/views/memory/MemoryGraphPanel.tsx +192 -0
- package/bizar-dash/src/web/views/memory/MemoryGraphView.tsx +336 -0
- package/bizar-dash/src/web/views/memory/VaultFromClipboardPanel.tsx +101 -0
- package/bizar-dash/src/web/views/settings/WorkspacesSection.tsx +189 -0
- package/bizar-dash/tests/bundle-analysis.test.mjs +73 -0
- package/bizar-dash/tests/clipboard.test.mjs +147 -0
- package/bizar-dash/tests/components/screenshot-ocr.test.tsx +75 -0
- package/bizar-dash/tests/components/settings-search.test.tsx +180 -0
- package/bizar-dash/tests/components/workspace-selector.test.tsx +73 -0
- package/bizar-dash/tests/deploy-templates.test.mjs +100 -0
- package/bizar-dash/tests/docker-build.test.mjs +96 -0
- package/bizar-dash/tests/eval/fixtures.test.mjs +141 -0
- package/bizar-dash/tests/eval/report.test.mjs +284 -0
- package/bizar-dash/tests/eval/runner.test.mjs +471 -0
- package/bizar-dash/tests/lib/search-fuzzy.test.ts +149 -0
- package/bizar-dash/tests/memory-graph-view.test.tsx +69 -0
- package/bizar-dash/tests/memory-graph.test.mjs +95 -0
- package/bizar-dash/tests/ocr.test.mjs +87 -0
- package/bizar-dash/tests/otel.test.mjs +188 -0
- package/bizar-dash/tests/plugins-registry.test.mjs +387 -0
- package/bizar-dash/tests/plugins-sandbox.test.mjs +374 -0
- package/bizar-dash/tests/plugins-store.test.mjs +455 -0
- package/bizar-dash/tests/users.test.mjs +108 -0
- package/bizar-dash/tests/voice-recorder.test.tsx +95 -0
- package/bizar-dash/tests/voice-store.test.mjs +148 -0
- package/bizar-dash/tests/voice-transcribe.test.mjs +87 -0
- package/bizar-dash/tests/workspaces.test.mjs +527 -0
- package/cli/bin.mjs +72 -2
- package/cli/commands/clip.mjs +146 -0
- package/cli/commands/dash.mjs +6 -0
- package/cli/commands/deploy/cloudflare.mjs +250 -0
- package/cli/commands/deploy/docker.mjs +221 -0
- package/cli/commands/deploy/fly.mjs +161 -0
- package/cli/commands/deploy/vercel.mjs +225 -0
- package/cli/commands/deploy.mjs +240 -0
- package/cli/commands/eval.mjs +378 -0
- package/cli/commands/marketplace.mjs +64 -0
- package/cli/commands/ocr.mjs +165 -0
- package/cli/commands/plugin.mjs +358 -0
- package/cli/commands/voice.mjs +211 -0
- package/cli/commands/workspace.mjs +247 -0
- package/package.json +12 -2
- package/templates/deploy/cloudflare/README.md +32 -0
- package/templates/deploy/cloudflare/functions-index.template.js +15 -0
- package/templates/deploy/cloudflare/wrangler.toml.template +9 -0
- package/templates/deploy/docker/.env.template +16 -0
- package/templates/deploy/docker/README.md +58 -0
- package/templates/deploy/docker/docker-compose.template.yml +23 -0
- package/templates/deploy/fly/README.md +35 -0
- package/templates/deploy/fly/fly.toml.template +28 -0
- package/templates/deploy/vercel/README.md +29 -0
- package/templates/deploy/vercel/api-index.template.js +18 -0
- package/templates/deploy/vercel/vercel.json.template +16 -0
- package/templates/eval-fixtures/README.md +58 -0
- package/templates/eval-fixtures/code-search-basic.json +28 -0
- package/templates/eval-fixtures/latency-bounds.json +16 -0
- package/templates/eval-fixtures/regression-suite.json +79 -0
- package/templates/eval-fixtures/response-format.json +30 -0
- package/templates/eval-fixtures/tool-call-correctness.json +24 -0
- package/templates/plugin-template/README.md +121 -0
- package/templates/plugin-template/index.js +66 -0
- package/templates/plugin-template/plugin.json +42 -0
- package/templates/plugin-template/tests/plugin.test.js +83 -0
- package/bizar-dash/dist/assets/main-DHZmbnxQ.js +0 -361
- package/bizar-dash/dist/assets/main-DHZmbnxQ.js.map +0 -1
- package/bizar-dash/dist/assets/mobile-BK8-ythT.js +0 -351
- package/bizar-dash/dist/assets/mobile-BK8-ythT.js.map +0 -1
- package/bizar-dash/dist/assets/mobile-Chvf9u_B.js +0 -1
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* src/server/eval-store.mjs
|
|
3
|
+
*
|
|
4
|
+
* v5.0.0 — Persistent storage for eval runs and results.
|
|
5
|
+
*
|
|
6
|
+
* Storage: ~/.local/share/bizar/eval/
|
|
7
|
+
*
|
|
8
|
+
* Index: ~/.local/share/bizar/eval/.index.json
|
|
9
|
+
* { "runs": [{ id, startedAt, finishedAt, suitePath, total, passed, failed }] }
|
|
10
|
+
*
|
|
11
|
+
* Per-run: ~/.local/share/bizar/eval/<run-id>.json
|
|
12
|
+
* Full run object with results array.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import { existsSync, mkdirSync, readFileSync, readdirSync, unlinkSync, writeFileSync, renameSync, statSync } from 'node:fs';
|
|
16
|
+
import { join, basename } from 'node:path';
|
|
17
|
+
import { homedir } from 'node:os';
|
|
18
|
+
|
|
19
|
+
// Allow test override
|
|
20
|
+
const STORE_HOME = process.env.BIZAR_STORE_HOME
|
|
21
|
+
? process.env.BIZAR_STORE_HOME
|
|
22
|
+
: join(homedir(), '.local', 'share', 'bizar');
|
|
23
|
+
const EVAL_DIR = join(STORE_HOME, 'eval');
|
|
24
|
+
const INDEX_FILE = join(EVAL_DIR, '.index.json');
|
|
25
|
+
|
|
26
|
+
// ── Helpers ───────────────────────────────────────────────────────────────────
|
|
27
|
+
|
|
28
|
+
function ensureDir(dir) {
|
|
29
|
+
if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function atomicWriteJson(filePath, data) {
|
|
33
|
+
const tmp = `${filePath}.tmp.${process.pid}`;
|
|
34
|
+
writeFileSync(tmp, JSON.stringify(data, null, 2) + '\n', 'utf8');
|
|
35
|
+
renameSync(tmp, filePath);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function safeReadJSON(file, fallback = null) {
|
|
39
|
+
try {
|
|
40
|
+
if (!existsSync(file)) return fallback;
|
|
41
|
+
const text = readFileSync(file, 'utf8');
|
|
42
|
+
if (!text.trim()) return fallback;
|
|
43
|
+
return JSON.parse(text);
|
|
44
|
+
} catch {
|
|
45
|
+
return fallback;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function loadIndex() {
|
|
50
|
+
return safeReadJSON(INDEX_FILE, { runs: [] });
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function saveIndex(index) {
|
|
54
|
+
ensureDir(EVAL_DIR);
|
|
55
|
+
atomicWriteJson(INDEX_FILE, index);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// ── Public API ────────────────────────────────────────────────────────────────
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Save a completed run to disk.
|
|
62
|
+
*
|
|
63
|
+
* @param {object} run
|
|
64
|
+
* @param {string} run.id
|
|
65
|
+
* @param {string} run.startedAt
|
|
66
|
+
* @param {string} run.finishedAt
|
|
67
|
+
* @param {string} run.suitePath
|
|
68
|
+
* @param {number} run.total
|
|
69
|
+
* @param {number} run.passed
|
|
70
|
+
* @param {number} run.failed
|
|
71
|
+
* @param {object[]} run.results
|
|
72
|
+
* @returns {{ ok: boolean, path: string }}
|
|
73
|
+
*/
|
|
74
|
+
export async function saveRun(run) {
|
|
75
|
+
ensureDir(EVAL_DIR);
|
|
76
|
+
|
|
77
|
+
// Write the run file
|
|
78
|
+
const runPath = join(EVAL_DIR, `${run.id}.json`);
|
|
79
|
+
atomicWriteJson(runPath, run);
|
|
80
|
+
|
|
81
|
+
// Update the index
|
|
82
|
+
const index = loadIndex();
|
|
83
|
+
const existingIdx = index.runs.findIndex((r) => r.id === run.id);
|
|
84
|
+
const indexEntry = {
|
|
85
|
+
id: run.id,
|
|
86
|
+
startedAt: run.startedAt,
|
|
87
|
+
finishedAt: run.finishedAt,
|
|
88
|
+
suitePath: run.suitePath,
|
|
89
|
+
total: run.total,
|
|
90
|
+
passed: run.passed,
|
|
91
|
+
failed: run.failed,
|
|
92
|
+
};
|
|
93
|
+
if (existingIdx >= 0) {
|
|
94
|
+
index.runs[existingIdx] = indexEntry;
|
|
95
|
+
} else {
|
|
96
|
+
index.runs.unshift(indexEntry); // newest first
|
|
97
|
+
}
|
|
98
|
+
saveIndex(index);
|
|
99
|
+
|
|
100
|
+
return { ok: true, path: runPath };
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* List recent runs, newest first.
|
|
105
|
+
*
|
|
106
|
+
* @param {{ limit?: number }} opts
|
|
107
|
+
* @returns {Promise<object[]>}
|
|
108
|
+
*/
|
|
109
|
+
export async function listRuns({ limit = 20 } = {}) {
|
|
110
|
+
const index = loadIndex();
|
|
111
|
+
return index.runs.slice(0, Math.max(1, limit));
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Get a full run by id.
|
|
116
|
+
*
|
|
117
|
+
* @param {string} id
|
|
118
|
+
* @returns {Promise<object | null>}
|
|
119
|
+
*/
|
|
120
|
+
export async function getRun(id) {
|
|
121
|
+
if (!id) return null;
|
|
122
|
+
const runPath = join(EVAL_DIR, `${id}.json`);
|
|
123
|
+
return safeReadJSON(runPath, null);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Compare two runs and categorize fixtures by diff.
|
|
128
|
+
*
|
|
129
|
+
* @param {string} id1
|
|
130
|
+
* @param {string} id2
|
|
131
|
+
* @returns {Promise<{ improved: object[], regressed: object[], unchanged: object[] }>}
|
|
132
|
+
*/
|
|
133
|
+
export async function compareRuns(id1, id2) {
|
|
134
|
+
const [run1, run2] = await Promise.all([getRun(id1), getRun(id2)]);
|
|
135
|
+
|
|
136
|
+
if (!run1 || !run2) {
|
|
137
|
+
return { improved: [], regressed: [], unchanged: [], error: 'run not found' };
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// Build a map of fixtureId -> result for each run
|
|
141
|
+
const map1 = new Map(run1.results.map((r) => [r.fixtureId, r]));
|
|
142
|
+
const map2 = new Map(run2.results.map((r) => [r.fixtureId, r]));
|
|
143
|
+
|
|
144
|
+
/** @type {object[]} */
|
|
145
|
+
const improved = [];
|
|
146
|
+
/** @type {object[]} */
|
|
147
|
+
const regressed = [];
|
|
148
|
+
/** @type {object[]} */
|
|
149
|
+
const unchanged = [];
|
|
150
|
+
|
|
151
|
+
// Check all fixtures from run1
|
|
152
|
+
for (const [fixtureId, result1] of map1) {
|
|
153
|
+
const result2 = map2.get(fixtureId);
|
|
154
|
+
if (!result2) {
|
|
155
|
+
// Fixture disappeared
|
|
156
|
+
continue;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
const entry = {
|
|
160
|
+
fixtureId,
|
|
161
|
+
run1Ok: result1.ok,
|
|
162
|
+
run2Ok: result2.ok,
|
|
163
|
+
run1LatencyMs: result1.latencyMs,
|
|
164
|
+
run2LatencyMs: result2.latencyMs,
|
|
165
|
+
};
|
|
166
|
+
|
|
167
|
+
if (result1.ok && !result2.ok) {
|
|
168
|
+
// Was passing, now failing = regressed
|
|
169
|
+
regressed.push({ ...entry, kind: 'regressed' });
|
|
170
|
+
} else if (!result1.ok && result2.ok) {
|
|
171
|
+
// Was failing, now passing = improved
|
|
172
|
+
improved.push({ ...entry, kind: 'improved' });
|
|
173
|
+
} else {
|
|
174
|
+
// Same result
|
|
175
|
+
unchanged.push({ ...entry, kind: 'unchanged' });
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
return { improved, regressed, unchanged };
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* Delete a run by id.
|
|
184
|
+
*
|
|
185
|
+
* @param {string} id
|
|
186
|
+
* @returns {{ ok: boolean }}
|
|
187
|
+
*/
|
|
188
|
+
export async function deleteRun(id) {
|
|
189
|
+
if (!id) return { ok: false };
|
|
190
|
+
const runPath = join(EVAL_DIR, `${id}.json`);
|
|
191
|
+
try {
|
|
192
|
+
if (existsSync(runPath)) unlinkSync(runPath);
|
|
193
|
+
} catch { /* ignore */ }
|
|
194
|
+
|
|
195
|
+
const index = loadIndex();
|
|
196
|
+
index.runs = index.runs.filter((r) => r.id !== id);
|
|
197
|
+
saveIndex(index);
|
|
198
|
+
|
|
199
|
+
return { ok: true };
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* Build a unique run id.
|
|
204
|
+
*
|
|
205
|
+
* @returns {string}
|
|
206
|
+
*/
|
|
207
|
+
export function buildRunId() {
|
|
208
|
+
const ts = new Date().toISOString().replace(/[:.]/g, '-').slice(0, 19);
|
|
209
|
+
const rand = Math.random().toString(36).slice(2, 6);
|
|
210
|
+
return `run_${ts}_${rand}`;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
// ── Test reset ────────────────────────────────────────────────────────────────
|
|
214
|
+
|
|
215
|
+
/**
|
|
216
|
+
* Wipe all eval data. For test isolation only.
|
|
217
|
+
*/
|
|
218
|
+
export function __resetStoreForTests() {
|
|
219
|
+
try {
|
|
220
|
+
const index = loadIndex();
|
|
221
|
+
for (const r of index.runs) {
|
|
222
|
+
try { unlinkSync(join(EVAL_DIR, `${r.id}.json`)); } catch { /* ignore */ }
|
|
223
|
+
}
|
|
224
|
+
try { unlinkSync(INDEX_FILE); } catch { /* ignore */ }
|
|
225
|
+
} catch { /* ignore */ }
|
|
226
|
+
}
|
|
@@ -0,0 +1,347 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* src/server/eval.mjs
|
|
3
|
+
*
|
|
4
|
+
* v5.0.0 — Eval runner for evaluating AI agent outputs against golden fixtures.
|
|
5
|
+
*
|
|
6
|
+
* Supports:
|
|
7
|
+
* - contains/notContains substring checks
|
|
8
|
+
* - regex pattern matching
|
|
9
|
+
* - JSON schema validation
|
|
10
|
+
* - maxTokens bounds
|
|
11
|
+
* - maxLatencyMs bounds
|
|
12
|
+
* - Parallel fixture execution with configurable concurrency
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import { readFileSync, readdirSync } from 'node:fs';
|
|
16
|
+
import { join, extname } from 'node:path';
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* @typedef {object} Fixture
|
|
20
|
+
* @property {string} id
|
|
21
|
+
* @property {string} name
|
|
22
|
+
* @property {string} description
|
|
23
|
+
* @property {string} agent
|
|
24
|
+
* @property {string} prompt
|
|
25
|
+
* @property {FixtureExpectations} expected
|
|
26
|
+
* @property {string[]} [tags]
|
|
27
|
+
*/
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* @typedef {object} FixtureExpectations
|
|
31
|
+
* @property {string[]} [contains]
|
|
32
|
+
* @property {string[]} [notContains]
|
|
33
|
+
* @property {string[]} [regex]
|
|
34
|
+
* @property {object} [jsonSchema]
|
|
35
|
+
* @property {number} [maxTokens]
|
|
36
|
+
* @property {number} [maxLatencyMs]
|
|
37
|
+
*/
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* @typedef {object} CheckResult
|
|
41
|
+
* @property {string} kind
|
|
42
|
+
* @property {boolean} ok
|
|
43
|
+
* @property {string} [message]
|
|
44
|
+
*/
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* @typedef {object} EvalResult
|
|
48
|
+
* @property {string} fixtureId
|
|
49
|
+
* @property {boolean} ok
|
|
50
|
+
* @property {CheckResult[]} checks
|
|
51
|
+
* @property {number} latencyMs
|
|
52
|
+
* @property {string} content
|
|
53
|
+
* @property {{ inputTokens: number, outputTokens: number, totalTokens: number }} usage
|
|
54
|
+
*/
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* @typedef {object} SuiteResult
|
|
58
|
+
* @property {string} suitePath
|
|
59
|
+
* @property {number} total
|
|
60
|
+
* @property {number} passed
|
|
61
|
+
* @property {number} failed
|
|
62
|
+
* @property {EvalResult[]} results
|
|
63
|
+
*/
|
|
64
|
+
|
|
65
|
+
// ── Check implementations ──────────────────────────────────────────────────────
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* @param {string} content
|
|
69
|
+
* @param {string[]} needles
|
|
70
|
+
* @returns {CheckResult[]}
|
|
71
|
+
*/
|
|
72
|
+
function checkContains(content, needles) {
|
|
73
|
+
return needles.map((needle) => {
|
|
74
|
+
const ok = content.includes(needle);
|
|
75
|
+
return {
|
|
76
|
+
kind: 'contains',
|
|
77
|
+
ok,
|
|
78
|
+
message: ok ? undefined : `expected to contain: "${needle}"`,
|
|
79
|
+
};
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* @param {string} content
|
|
85
|
+
* @param {string[]} needles
|
|
86
|
+
* @returns {CheckResult[]}
|
|
87
|
+
*/
|
|
88
|
+
function checkNotContains(content, needles) {
|
|
89
|
+
return needles.map((needle) => {
|
|
90
|
+
const ok = !content.includes(needle);
|
|
91
|
+
return {
|
|
92
|
+
kind: 'notContains',
|
|
93
|
+
ok,
|
|
94
|
+
message: ok ? undefined : `expected NOT to contain: "${needle}"`,
|
|
95
|
+
};
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* @param {string} content
|
|
101
|
+
* @param {string[]} patterns
|
|
102
|
+
* @returns {CheckResult[]}
|
|
103
|
+
*/
|
|
104
|
+
function checkRegex(content, patterns) {
|
|
105
|
+
return patterns.map((pattern) => {
|
|
106
|
+
let ok = false;
|
|
107
|
+
let message;
|
|
108
|
+
try {
|
|
109
|
+
const re = new RegExp(pattern);
|
|
110
|
+
ok = re.test(content);
|
|
111
|
+
message = ok ? undefined : `regex did not match: ${pattern}`;
|
|
112
|
+
} catch (err) {
|
|
113
|
+
ok = false;
|
|
114
|
+
message = `invalid regex: ${pattern} — ${err.message}`;
|
|
115
|
+
}
|
|
116
|
+
return { kind: 'regex', ok, message };
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* @param {string} content
|
|
122
|
+
* @param {object} schema
|
|
123
|
+
* @returns {CheckResult}
|
|
124
|
+
*/
|
|
125
|
+
function checkJsonSchema(content, schema) {
|
|
126
|
+
if (!schema) return { kind: 'jsonSchema', ok: true };
|
|
127
|
+
let parsed;
|
|
128
|
+
try {
|
|
129
|
+
parsed = JSON.parse(content);
|
|
130
|
+
} catch {
|
|
131
|
+
return { kind: 'jsonSchema', ok: false, message: 'response is not valid JSON' };
|
|
132
|
+
}
|
|
133
|
+
// Basic schema validation — check required fields exist
|
|
134
|
+
const errors = [];
|
|
135
|
+
if (schema.type === 'object' && typeof parsed !== 'object' || Array.isArray(parsed)) {
|
|
136
|
+
errors.push(`expected object, got ${typeof parsed}`);
|
|
137
|
+
}
|
|
138
|
+
if (schema.properties) {
|
|
139
|
+
for (const [key, prop] of Object.entries(schema.properties)) {
|
|
140
|
+
if (parsed[key] === undefined) {
|
|
141
|
+
if (!schema.required?.includes(key)) continue;
|
|
142
|
+
errors.push(`missing required property: ${key}`);
|
|
143
|
+
} else if (prop.type && typeof parsed[key] !== prop.type) {
|
|
144
|
+
errors.push(`property "${key}" expected type ${prop.type}, got ${typeof parsed[key]}`);
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
return {
|
|
149
|
+
kind: 'jsonSchema',
|
|
150
|
+
ok: errors.length === 0,
|
|
151
|
+
message: errors.length > 0 ? errors.join('; ') : undefined,
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* @param {{ inputTokens: number, outputTokens: number, totalTokens: number }} usage
|
|
157
|
+
* @param {number} maxTokens
|
|
158
|
+
* @returns {CheckResult}
|
|
159
|
+
*/
|
|
160
|
+
function checkMaxTokens(usage, maxTokens) {
|
|
161
|
+
if (maxTokens == null) return { kind: 'maxTokens', ok: true };
|
|
162
|
+
const ok = usage.totalTokens <= maxTokens;
|
|
163
|
+
return {
|
|
164
|
+
kind: 'maxTokens',
|
|
165
|
+
ok,
|
|
166
|
+
message: ok ? undefined : `token count ${usage.totalTokens} exceeds max ${maxTokens}`,
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* @param {number} latencyMs
|
|
172
|
+
* @param {number} maxLatencyMs
|
|
173
|
+
* @returns {CheckResult}
|
|
174
|
+
*/
|
|
175
|
+
function checkMaxLatency(latencyMs, maxLatencyMs) {
|
|
176
|
+
if (maxLatencyMs == null) return { kind: 'maxLatencyMs', ok: true };
|
|
177
|
+
const ok = latencyMs <= maxLatencyMs;
|
|
178
|
+
return {
|
|
179
|
+
kind: 'maxLatencyMs',
|
|
180
|
+
ok,
|
|
181
|
+
message: ok ? undefined : `latency ${latencyMs}ms exceeds max ${maxLatencyMs}ms`,
|
|
182
|
+
};
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// ── Core runner ───────────────────────────────────────────────────────────────
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* Run a single fixture against an LLM.
|
|
189
|
+
*
|
|
190
|
+
* @param {Fixture} fixture
|
|
191
|
+
* @param {{ llmCall: (prompt: string, opts: {agent: string}) => Promise<{content: string, usage: object}>, timeoutMs?: number }} opts
|
|
192
|
+
* @returns {Promise<EvalResult>}
|
|
193
|
+
*/
|
|
194
|
+
export async function runFixture(
|
|
195
|
+
fixture,
|
|
196
|
+
{ llmCall, timeoutMs = 60000 } = {},
|
|
197
|
+
) {
|
|
198
|
+
if (!llmCall) throw new Error('llmCall is required');
|
|
199
|
+
const start = Date.now();
|
|
200
|
+
let content = '';
|
|
201
|
+
let usage = { inputTokens: 0, outputTokens: 0, totalTokens: 0 };
|
|
202
|
+
let timeoutHit = false;
|
|
203
|
+
let callError = null;
|
|
204
|
+
|
|
205
|
+
try {
|
|
206
|
+
const ctrl = new AbortController();
|
|
207
|
+
const timer = setTimeout(() => ctrl.abort(), timeoutMs);
|
|
208
|
+
const result = await Promise.race([
|
|
209
|
+
llmCall(fixture.prompt, { agent: fixture.agent }),
|
|
210
|
+
new Promise((_, reject) => ctrl.signal.addEventListener('abort', () => reject(new Error('timeout')))),
|
|
211
|
+
]);
|
|
212
|
+
clearTimeout(timer);
|
|
213
|
+
content = result.content || '';
|
|
214
|
+
usage = result.usage || { inputTokens: 0, outputTokens: 0, totalTokens: 0 };
|
|
215
|
+
} catch (err) {
|
|
216
|
+
if (err.message === 'timeout') {
|
|
217
|
+
timeoutHit = true;
|
|
218
|
+
} else {
|
|
219
|
+
callError = err;
|
|
220
|
+
}
|
|
221
|
+
content = '';
|
|
222
|
+
usage = { inputTokens: 0, outputTokens: 0, totalTokens: 0 };
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
const latencyMs = Date.now() - start;
|
|
226
|
+
const checks = [];
|
|
227
|
+
const expected = fixture.expected || {};
|
|
228
|
+
|
|
229
|
+
// Timeout check
|
|
230
|
+
if (timeoutHit) {
|
|
231
|
+
checks.push({ kind: 'timeout', ok: false, message: `call exceeded ${timeoutMs}ms` });
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
// Error check (non-timeout error)
|
|
235
|
+
if (callError) {
|
|
236
|
+
checks.push({ kind: 'error', ok: false, message: callError.message });
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
// Content checks
|
|
240
|
+
if (expected.contains?.length) {
|
|
241
|
+
checks.push(...checkContains(content, expected.contains));
|
|
242
|
+
}
|
|
243
|
+
if (expected.notContains?.length) {
|
|
244
|
+
checks.push(...checkNotContains(content, expected.notContains));
|
|
245
|
+
}
|
|
246
|
+
if (expected.regex?.length) {
|
|
247
|
+
checks.push(...checkRegex(content, expected.regex));
|
|
248
|
+
}
|
|
249
|
+
if (expected.jsonSchema) {
|
|
250
|
+
checks.push(checkJsonSchema(content, expected.jsonSchema));
|
|
251
|
+
}
|
|
252
|
+
if (expected.maxTokens != null) {
|
|
253
|
+
checks.push(checkMaxTokens(usage, expected.maxTokens));
|
|
254
|
+
}
|
|
255
|
+
if (expected.maxLatencyMs != null) {
|
|
256
|
+
checks.push(checkMaxLatency(latencyMs, expected.maxLatencyMs));
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
const ok = checks.every((c) => c.ok);
|
|
260
|
+
|
|
261
|
+
return {
|
|
262
|
+
fixtureId: fixture.id,
|
|
263
|
+
ok,
|
|
264
|
+
checks,
|
|
265
|
+
latencyMs,
|
|
266
|
+
content,
|
|
267
|
+
usage,
|
|
268
|
+
};
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
/**
|
|
272
|
+
* Load all JSON fixtures from a directory.
|
|
273
|
+
*
|
|
274
|
+
* @param {string} suitePath
|
|
275
|
+
* @returns {Fixture[]}
|
|
276
|
+
*/
|
|
277
|
+
export function loadFixtures(suitePath) {
|
|
278
|
+
/** @type {Fixture[]} */
|
|
279
|
+
const fixtures = [];
|
|
280
|
+
try {
|
|
281
|
+
const entries = readdirSync(suitePath, { withFileTypes: true });
|
|
282
|
+
for (const entry of entries) {
|
|
283
|
+
if (entry.isFile() && extname(entry.name) === '.json') {
|
|
284
|
+
const full = join(suitePath, entry.name);
|
|
285
|
+
try {
|
|
286
|
+
const raw = readFileSync(full, 'utf8');
|
|
287
|
+
const parsed = JSON.parse(raw);
|
|
288
|
+
// Support both single fixture and suite of fixtures
|
|
289
|
+
if (parsed.id && parsed.prompt) {
|
|
290
|
+
fixtures.push(parsed);
|
|
291
|
+
} else if (Array.isArray(parsed.fixtures)) {
|
|
292
|
+
fixtures.push(...parsed.fixtures);
|
|
293
|
+
}
|
|
294
|
+
} catch { /* skip invalid JSON */ }
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
} catch { /* dir not found */ }
|
|
298
|
+
return fixtures;
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
/**
|
|
302
|
+
* Run a suite of fixtures.
|
|
303
|
+
*
|
|
304
|
+
* @param {string} suitePath
|
|
305
|
+
* @param {{ llmCall: Function, concurrency?: number, timeoutMs?: number }} opts
|
|
306
|
+
* @returns {Promise<SuiteResult>}
|
|
307
|
+
*/
|
|
308
|
+
export async function runSuite(
|
|
309
|
+
suitePath,
|
|
310
|
+
{ llmCall, concurrency = 5, timeoutMs = 60000 } = {},
|
|
311
|
+
) {
|
|
312
|
+
const fixtures = loadFixtures(suitePath);
|
|
313
|
+
/** @type {EvalResult[]} */
|
|
314
|
+
const results = [];
|
|
315
|
+
let passed = 0;
|
|
316
|
+
let failed = 0;
|
|
317
|
+
|
|
318
|
+
// Run in batches of `concurrency`
|
|
319
|
+
for (let i = 0; i < fixtures.length; i += concurrency) {
|
|
320
|
+
const batch = fixtures.slice(i, i + concurrency);
|
|
321
|
+
const batchResults = await Promise.all(
|
|
322
|
+
batch.map((fixture) =>
|
|
323
|
+
runFixture(fixture, { llmCall, timeoutMs }).catch((err) => ({
|
|
324
|
+
fixtureId: fixture.id,
|
|
325
|
+
ok: false,
|
|
326
|
+
checks: [{ kind: 'error', ok: false, message: err.message }],
|
|
327
|
+
latencyMs: 0,
|
|
328
|
+
content: '',
|
|
329
|
+
usage: { inputTokens: 0, outputTokens: 0, totalTokens: 0 },
|
|
330
|
+
}))
|
|
331
|
+
)
|
|
332
|
+
);
|
|
333
|
+
for (const r of batchResults) {
|
|
334
|
+
results.push(r);
|
|
335
|
+
if (r.ok) passed++;
|
|
336
|
+
else failed++;
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
return {
|
|
341
|
+
suitePath,
|
|
342
|
+
total: fixtures.length,
|
|
343
|
+
passed,
|
|
344
|
+
failed,
|
|
345
|
+
results,
|
|
346
|
+
};
|
|
347
|
+
}
|
|
@@ -1051,6 +1051,115 @@ function statMaybe(path) {
|
|
|
1051
1051
|
}
|
|
1052
1052
|
}
|
|
1053
1053
|
|
|
1054
|
+
/**
|
|
1055
|
+
* Get a graph representation of the LightRAG index.
|
|
1056
|
+
*
|
|
1057
|
+
* If `root` is given, fetches entities reachable within `depth` hops
|
|
1058
|
+
* from that entity via LightRAG's /graph endpoint. Otherwise returns
|
|
1059
|
+
* all entities up to `limit`.
|
|
1060
|
+
*
|
|
1061
|
+
* Falls back to reading .bizar/memory/lightrag/kv_store_*.json directly
|
|
1062
|
+
* when the LightRAG server is unavailable.
|
|
1063
|
+
*
|
|
1064
|
+
* @param {{ projectRoot: string, root?: string|null, depth?: number, limit?: number }}
|
|
1065
|
+
* @returns {Promise<{nodes: Array<{id:string,label:string,type:string,size:number,group:string}>, edges: Array<{source:string,target:string,type:string,weight:number}>}>}
|
|
1066
|
+
*/
|
|
1067
|
+
export async function getLightRAGGraph({ projectRoot, root = null, depth = 2, limit = 200 }) {
|
|
1068
|
+
const config = resolveLightRAGConfig(projectRoot);
|
|
1069
|
+
|
|
1070
|
+
// Attempt to query the running server.
|
|
1071
|
+
if (await isRunning(config)) {
|
|
1072
|
+
try {
|
|
1073
|
+
const nodes = [];
|
|
1074
|
+
const edges = [];
|
|
1075
|
+
|
|
1076
|
+
if (root) {
|
|
1077
|
+
// Use LightRAG's /graph endpoint with root entity.
|
|
1078
|
+
const url = `http://${config.host}:${config.port}/graph?root=${encodeURIComponent(root)}&depth=${depth}&limit=${limit}`;
|
|
1079
|
+
const res = await httpGet(url, config.timeoutMs);
|
|
1080
|
+
if (res.status === 200) {
|
|
1081
|
+
let parsed;
|
|
1082
|
+
try { parsed = JSON.parse(res.body); } catch { parsed = {}; }
|
|
1083
|
+
// LightRAG /graph shape varies; normalise to our contract.
|
|
1084
|
+
// LightRAG returns { entities: [...], relations: [...] } or similar.
|
|
1085
|
+
const ents = parsed.entities || parsed.nodes || [];
|
|
1086
|
+
const rels = parsed.relations || parsed.edges || [];
|
|
1087
|
+
for (const e of ents.slice(0, limit)) {
|
|
1088
|
+
const id = String(e.id || e.name || JSON.stringify(e));
|
|
1089
|
+
nodes.push({
|
|
1090
|
+
id,
|
|
1091
|
+
label: String(e.label || e.name || id).slice(0, 60),
|
|
1092
|
+
type: String(e.type || 'entity'),
|
|
1093
|
+
size: Number(e.size || 1),
|
|
1094
|
+
group: String(e.group || e.type || 'default'),
|
|
1095
|
+
});
|
|
1096
|
+
}
|
|
1097
|
+
for (const r of rels) {
|
|
1098
|
+
edges.push({
|
|
1099
|
+
source: String(r.source || r.from || r.src || ''),
|
|
1100
|
+
target: String(r.target || r.to || r.dst || ''),
|
|
1101
|
+
type: String(r.type || 'related'),
|
|
1102
|
+
weight: Number(r.weight || 1),
|
|
1103
|
+
});
|
|
1104
|
+
}
|
|
1105
|
+
return { nodes, edges };
|
|
1106
|
+
}
|
|
1107
|
+
} else {
|
|
1108
|
+
// Return all entities from kv_store_full_docs.json.
|
|
1109
|
+
const docsPath = join(config.workingDir, 'kv_store_full_docs.json');
|
|
1110
|
+
if (existsSync(docsPath)) {
|
|
1111
|
+
try {
|
|
1112
|
+
const docs = JSON.parse(readFileSync(docsPath, 'utf8'));
|
|
1113
|
+
const keys = Object.keys(docs || {}).slice(0, limit);
|
|
1114
|
+
// Group by top-level path segment as a proxy for group.
|
|
1115
|
+
for (const k of keys) {
|
|
1116
|
+
const parts = k.replace(/^bizar:\/\//, '').split('/');
|
|
1117
|
+
const group = parts.length > 1 ? parts[0] : 'root';
|
|
1118
|
+
nodes.push({
|
|
1119
|
+
id: k,
|
|
1120
|
+
label: parts[parts.length - 1].replace(/[_-]/g, ' '),
|
|
1121
|
+
type: 'note',
|
|
1122
|
+
size: 1,
|
|
1123
|
+
group,
|
|
1124
|
+
});
|
|
1125
|
+
}
|
|
1126
|
+
return { nodes, edges };
|
|
1127
|
+
} catch (err) {
|
|
1128
|
+
console.warn('[lightrag] getLightRAGGraph fallback read failed:', err?.message);
|
|
1129
|
+
}
|
|
1130
|
+
}
|
|
1131
|
+
}
|
|
1132
|
+
} catch (err) {
|
|
1133
|
+
console.warn('[lightrag] getLightRAGGraph server query failed, falling back:', err?.message);
|
|
1134
|
+
}
|
|
1135
|
+
}
|
|
1136
|
+
|
|
1137
|
+
// Fallback: read kv_store files directly.
|
|
1138
|
+
const nodes = [];
|
|
1139
|
+
const edges = [];
|
|
1140
|
+
try {
|
|
1141
|
+
const docsPath = join(config.workingDir, 'kv_store_full_docs.json');
|
|
1142
|
+
if (existsSync(docsPath)) {
|
|
1143
|
+
const docs = JSON.parse(readFileSync(docsPath, 'utf8'));
|
|
1144
|
+
const keys = Object.keys(docs || {}).slice(0, limit);
|
|
1145
|
+
for (const k of keys) {
|
|
1146
|
+
const parts = k.replace(/^bizar:\/\//, '').split('/');
|
|
1147
|
+
const group = parts.length > 1 ? parts[0] : 'root';
|
|
1148
|
+
nodes.push({
|
|
1149
|
+
id: k,
|
|
1150
|
+
label: parts[parts.length - 1].replace(/[_-]/g, ' '),
|
|
1151
|
+
type: 'note',
|
|
1152
|
+
size: 1,
|
|
1153
|
+
group,
|
|
1154
|
+
});
|
|
1155
|
+
}
|
|
1156
|
+
}
|
|
1157
|
+
} catch (err) {
|
|
1158
|
+
console.warn('[lightrag] getLightRAGGraph fallback read error:', err?.message);
|
|
1159
|
+
}
|
|
1160
|
+
return { nodes, edges };
|
|
1161
|
+
}
|
|
1162
|
+
|
|
1054
1163
|
/**
|
|
1055
1164
|
* Rebuild the graph from scratch. Stops the server, wipes the working dir,
|
|
1056
1165
|
* and re-runs reindexVault. Returns { ok, started, error?, markerPath? }.
|