labgate 0.5.31 → 0.5.33

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 (73) hide show
  1. package/README.md +50 -2
  2. package/dist/cli.js +533 -0
  3. package/dist/cli.js.map +1 -1
  4. package/dist/lib/config.d.ts +11 -0
  5. package/dist/lib/config.js +45 -4
  6. package/dist/lib/config.js.map +1 -1
  7. package/dist/lib/container.d.ts +3 -3
  8. package/dist/lib/container.js +144 -12
  9. package/dist/lib/container.js.map +1 -1
  10. package/dist/lib/display-mcp.d.ts +10 -0
  11. package/dist/lib/display-mcp.js +160 -0
  12. package/dist/lib/display-mcp.js.map +1 -0
  13. package/dist/lib/display-store.d.ts +24 -0
  14. package/dist/lib/display-store.js +150 -0
  15. package/dist/lib/display-store.js.map +1 -0
  16. package/dist/lib/explorer-autopilot.d.ts +16 -0
  17. package/dist/lib/explorer-autopilot.js +573 -0
  18. package/dist/lib/explorer-autopilot.js.map +1 -0
  19. package/dist/lib/explorer-claude.d.ts +16 -0
  20. package/dist/lib/explorer-claude.js +361 -0
  21. package/dist/lib/explorer-claude.js.map +1 -0
  22. package/dist/lib/explorer-compare.d.ts +9 -0
  23. package/dist/lib/explorer-compare.js +190 -0
  24. package/dist/lib/explorer-compare.js.map +1 -0
  25. package/dist/lib/explorer-eval.d.ts +23 -0
  26. package/dist/lib/explorer-eval.js +161 -0
  27. package/dist/lib/explorer-eval.js.map +1 -0
  28. package/dist/lib/explorer-gc.d.ts +11 -0
  29. package/dist/lib/explorer-gc.js +304 -0
  30. package/dist/lib/explorer-gc.js.map +1 -0
  31. package/dist/lib/explorer-git.d.ts +14 -0
  32. package/dist/lib/explorer-git.js +136 -0
  33. package/dist/lib/explorer-git.js.map +1 -0
  34. package/dist/lib/explorer-lock.d.ts +5 -0
  35. package/dist/lib/explorer-lock.js +100 -0
  36. package/dist/lib/explorer-lock.js.map +1 -0
  37. package/dist/lib/explorer-mcp.d.ts +11 -0
  38. package/dist/lib/explorer-mcp.js +611 -0
  39. package/dist/lib/explorer-mcp.js.map +1 -0
  40. package/dist/lib/explorer-retention.d.ts +4 -0
  41. package/dist/lib/explorer-retention.js +58 -0
  42. package/dist/lib/explorer-retention.js.map +1 -0
  43. package/dist/lib/explorer-store.d.ts +77 -0
  44. package/dist/lib/explorer-store.js +950 -0
  45. package/dist/lib/explorer-store.js.map +1 -0
  46. package/dist/lib/explorer-types.d.ts +161 -0
  47. package/dist/lib/explorer-types.js +3 -0
  48. package/dist/lib/explorer-types.js.map +1 -0
  49. package/dist/lib/explorer.d.ts +31 -0
  50. package/dist/lib/explorer.js +247 -0
  51. package/dist/lib/explorer.js.map +1 -0
  52. package/dist/lib/results-store.js +37 -3
  53. package/dist/lib/results-store.js.map +1 -1
  54. package/dist/lib/test/integration-harness.js +1 -1
  55. package/dist/lib/test/integration-harness.js.map +1 -1
  56. package/dist/lib/ui.html +5115 -2052
  57. package/dist/lib/ui.js +906 -39
  58. package/dist/lib/ui.js.map +1 -1
  59. package/dist/lib/web-terminal.js +4 -3
  60. package/dist/lib/web-terminal.js.map +1 -1
  61. package/dist/mcp-bundles/dataset-mcp.bundle.mjs +0 -8
  62. package/dist/mcp-bundles/display-mcp.bundle.mjs +30209 -0
  63. package/dist/mcp-bundles/explorer-mcp.bundle.mjs +40036 -0
  64. package/dist/mcp-bundles/results-mcp.bundle.mjs +30 -4
  65. package/package.json +3 -2
  66. package/templates/tsp-lab/API_CONTRACT.md +20 -0
  67. package/templates/tsp-lab/EVAL.md +20 -0
  68. package/templates/tsp-lab/PROBLEM.md +18 -0
  69. package/templates/tsp-lab/data/generate_instances.py +51 -0
  70. package/templates/tsp-lab/data/instances.jsonl +12 -0
  71. package/templates/tsp-lab/eval.py +148 -0
  72. package/templates/tsp-lab/solver.py +88 -0
  73. package/templates/tsp-lab/stub-patches/enable_two_opt.patch +14 -0
@@ -0,0 +1,950 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.ExplorerStore = void 0;
4
+ exports.makeExplorerExperimentId = makeExplorerExperimentId;
5
+ exports.makeExplorerRunId = makeExplorerRunId;
6
+ const crypto_1 = require("crypto");
7
+ const fs_1 = require("fs");
8
+ const path_1 = require("path");
9
+ const config_js_1 = require("./config.js");
10
+ const explorer_retention_js_1 = require("./explorer-retention.js");
11
+ let Database;
12
+ try {
13
+ Database = require('better-sqlite3');
14
+ }
15
+ catch {
16
+ Database = null;
17
+ }
18
+ const ACTIVE_RUN_STATES = new Set(['queued', 'running', 'commit_ok']);
19
+ const RUN_STATUS_VALUES = [
20
+ 'queued',
21
+ 'running',
22
+ 'commit_ok',
23
+ 'eval_ok',
24
+ 'failed_build',
25
+ 'failed_eval',
26
+ 'timeout',
27
+ 'invalid_output',
28
+ ];
29
+ const SCHEMA = `
30
+ CREATE TABLE IF NOT EXISTS experiments (
31
+ id TEXT PRIMARY KEY,
32
+ name TEXT NOT NULL,
33
+ created_at TEXT NOT NULL,
34
+ status TEXT NOT NULL,
35
+ repo_path TEXT NOT NULL,
36
+ base_ref TEXT NOT NULL,
37
+ eval_command TEXT NOT NULL,
38
+ eval_timeout_sec INTEGER NOT NULL,
39
+ policy_json TEXT NOT NULL,
40
+ constraints_json TEXT NOT NULL,
41
+ prompt_preamble TEXT NOT NULL
42
+ );
43
+
44
+ CREATE TABLE IF NOT EXISTS runs (
45
+ id TEXT PRIMARY KEY,
46
+ experiment_id TEXT NOT NULL,
47
+ parent_run_id TEXT,
48
+ parent_commit_sha TEXT NOT NULL,
49
+ branch_name TEXT NOT NULL,
50
+ worktree_path TEXT NOT NULL,
51
+ commit_sha TEXT,
52
+ status TEXT NOT NULL,
53
+ score REAL,
54
+ metrics_json TEXT,
55
+ model TEXT NOT NULL,
56
+ summary_md TEXT NOT NULL,
57
+ created_at TEXT NOT NULL,
58
+ started_at TEXT,
59
+ finished_at TEXT,
60
+ artifact_dir TEXT NOT NULL,
61
+ worktree_pruned_at TEXT,
62
+ artifacts_pruned_at TEXT,
63
+ artifacts_mode_kept TEXT
64
+ );
65
+
66
+ CREATE INDEX IF NOT EXISTS idx_runs_experiment_created ON runs(experiment_id, created_at DESC);
67
+ CREATE INDEX IF NOT EXISTS idx_runs_experiment_score ON runs(experiment_id, score DESC);
68
+ CREATE INDEX IF NOT EXISTS idx_runs_experiment_status ON runs(experiment_id, status);
69
+
70
+ CREATE TABLE IF NOT EXISTS events (
71
+ id TEXT PRIMARY KEY,
72
+ experiment_id TEXT NOT NULL,
73
+ run_id TEXT,
74
+ type TEXT NOT NULL,
75
+ payload_json TEXT NOT NULL,
76
+ created_at TEXT NOT NULL
77
+ );
78
+
79
+ CREATE INDEX IF NOT EXISTS idx_events_experiment_created ON events(experiment_id, created_at DESC);
80
+ `;
81
+ function nowIso() {
82
+ return new Date().toISOString();
83
+ }
84
+ function sanitizeString(value, maxLen) {
85
+ if (typeof value !== 'string')
86
+ return '';
87
+ const trimmed = value.trim();
88
+ if (!trimmed)
89
+ return '';
90
+ return trimmed.length > maxLen ? trimmed.slice(0, maxLen) : trimmed;
91
+ }
92
+ function sanitizeJsonString(value, fallback = '{}') {
93
+ if (typeof value !== 'string' || !value.trim())
94
+ return fallback;
95
+ try {
96
+ const parsed = JSON.parse(value);
97
+ return JSON.stringify(parsed);
98
+ }
99
+ catch {
100
+ return fallback;
101
+ }
102
+ }
103
+ function clampInt(value, fallback, min, max) {
104
+ if (!Number.isFinite(value))
105
+ return fallback;
106
+ return Math.min(max, Math.max(min, Math.floor(value)));
107
+ }
108
+ function initStatusCounts() {
109
+ return {
110
+ queued: 0,
111
+ running: 0,
112
+ commit_ok: 0,
113
+ eval_ok: 0,
114
+ failed_build: 0,
115
+ failed_eval: 0,
116
+ timeout: 0,
117
+ invalid_output: 0,
118
+ };
119
+ }
120
+ function makeExplorerExperimentId() {
121
+ const stamp = new Date().toISOString().replace(/[-:.TZ]/g, '').slice(0, 14);
122
+ return `exp_${stamp}_${(0, crypto_1.randomUUID)().slice(0, 8)}`;
123
+ }
124
+ function makeExplorerRunId() {
125
+ return `run_${Date.now()}_${(0, crypto_1.randomUUID)().slice(0, 8)}`;
126
+ }
127
+ function makeEventId() {
128
+ return `evt_${Date.now()}_${(0, crypto_1.randomUUID)().slice(0, 8)}`;
129
+ }
130
+ function mapRunRow(row) {
131
+ return {
132
+ id: String(row.id),
133
+ experiment_id: String(row.experiment_id),
134
+ parent_run_id: row.parent_run_id ? String(row.parent_run_id) : null,
135
+ parent_commit_sha: String(row.parent_commit_sha),
136
+ branch_name: String(row.branch_name),
137
+ worktree_path: String(row.worktree_path),
138
+ commit_sha: row.commit_sha ? String(row.commit_sha) : null,
139
+ status: row.status,
140
+ score: row.score == null ? null : Number(row.score),
141
+ metrics_json: row.metrics_json == null ? null : String(row.metrics_json),
142
+ model: String(row.model),
143
+ summary_md: String(row.summary_md),
144
+ created_at: String(row.created_at),
145
+ started_at: row.started_at ? String(row.started_at) : null,
146
+ finished_at: row.finished_at ? String(row.finished_at) : null,
147
+ artifact_dir: String(row.artifact_dir),
148
+ worktree_pruned_at: row.worktree_pruned_at ? String(row.worktree_pruned_at) : null,
149
+ artifacts_pruned_at: row.artifacts_pruned_at ? String(row.artifacts_pruned_at) : null,
150
+ artifacts_mode_kept: row.artifacts_mode_kept
151
+ ? String(row.artifacts_mode_kept)
152
+ : null,
153
+ };
154
+ }
155
+ function normalizeRunObject(row) {
156
+ return {
157
+ id: String(row.id || ''),
158
+ experiment_id: String(row.experiment_id || ''),
159
+ parent_run_id: row.parent_run_id ? String(row.parent_run_id) : null,
160
+ parent_commit_sha: String(row.parent_commit_sha || ''),
161
+ branch_name: String(row.branch_name || ''),
162
+ worktree_path: String(row.worktree_path || ''),
163
+ commit_sha: row.commit_sha ? String(row.commit_sha) : null,
164
+ status: String(row.status || 'queued'),
165
+ score: row.score == null ? null : Number(row.score),
166
+ metrics_json: row.metrics_json == null ? null : String(row.metrics_json),
167
+ model: String(row.model || 'stub'),
168
+ summary_md: String(row.summary_md || ''),
169
+ created_at: String(row.created_at || nowIso()),
170
+ started_at: row.started_at ? String(row.started_at) : null,
171
+ finished_at: row.finished_at ? String(row.finished_at) : null,
172
+ artifact_dir: String(row.artifact_dir || ''),
173
+ worktree_pruned_at: row.worktree_pruned_at ? String(row.worktree_pruned_at) : null,
174
+ artifacts_pruned_at: row.artifacts_pruned_at ? String(row.artifacts_pruned_at) : null,
175
+ artifacts_mode_kept: row.artifacts_mode_kept
176
+ ? String(row.artifacts_mode_kept)
177
+ : null,
178
+ };
179
+ }
180
+ function mapExperimentRow(row) {
181
+ return {
182
+ id: String(row.id),
183
+ name: String(row.name),
184
+ created_at: String(row.created_at),
185
+ status: row.status,
186
+ repo_path: String(row.repo_path),
187
+ base_ref: String(row.base_ref),
188
+ eval_command: String(row.eval_command),
189
+ eval_timeout_sec: Number(row.eval_timeout_sec),
190
+ policy_json: String(row.policy_json),
191
+ constraints_json: String(row.constraints_json),
192
+ prompt_preamble: String(row.prompt_preamble),
193
+ };
194
+ }
195
+ function mapEventRow(row) {
196
+ return {
197
+ id: String(row.id),
198
+ experiment_id: String(row.experiment_id),
199
+ run_id: row.run_id ? String(row.run_id) : null,
200
+ type: row.type,
201
+ payload_json: String(row.payload_json),
202
+ created_at: String(row.created_at),
203
+ };
204
+ }
205
+ class ExplorerStore {
206
+ dbPath;
207
+ db;
208
+ useJsonFallback = false;
209
+ jsonPath = null;
210
+ jsonStore = {
211
+ version: 1,
212
+ experiments: [],
213
+ runs: [],
214
+ events: [],
215
+ };
216
+ constructor(dbPath = (0, config_js_1.getExplorerDbPath)()) {
217
+ this.dbPath = dbPath;
218
+ const dir = (0, path_1.dirname)(dbPath);
219
+ if (!(0, fs_1.existsSync)(dir)) {
220
+ (0, fs_1.mkdirSync)(dir, { recursive: true, mode: 0o700 });
221
+ }
222
+ const forceJson = process.env.LABGATE_EXPLORER_DB_FORCE_JSON === '1';
223
+ if (Database && !forceJson) {
224
+ try {
225
+ this.db = new Database(dbPath);
226
+ this.db.pragma('journal_mode = WAL');
227
+ this.db.pragma('busy_timeout = 5000');
228
+ this.db.exec(SCHEMA);
229
+ try {
230
+ this.db.exec('ALTER TABLE runs ADD COLUMN worktree_pruned_at TEXT');
231
+ }
232
+ catch { /* already exists */ }
233
+ try {
234
+ this.db.exec('ALTER TABLE runs ADD COLUMN artifacts_pruned_at TEXT');
235
+ }
236
+ catch { /* already exists */ }
237
+ try {
238
+ this.db.exec('ALTER TABLE runs ADD COLUMN artifacts_mode_kept TEXT');
239
+ }
240
+ catch { /* already exists */ }
241
+ try {
242
+ (0, fs_1.chmodSync)(dbPath, 0o600);
243
+ }
244
+ catch {
245
+ // Best effort on FSs with limited chmod support.
246
+ }
247
+ return;
248
+ }
249
+ catch {
250
+ // Fallback below.
251
+ }
252
+ }
253
+ this.useJsonFallback = true;
254
+ this.jsonPath = `${dbPath}.json`;
255
+ this.loadJson();
256
+ this.flushJson();
257
+ }
258
+ getPath() {
259
+ return this.dbPath;
260
+ }
261
+ createExperiment(input) {
262
+ const id = sanitizeString(input.id, 128) || makeExplorerExperimentId();
263
+ const name = sanitizeString(input.name, 240);
264
+ if (!name)
265
+ throw new Error('name is required');
266
+ const repoPath = sanitizeString(input.repo_path, 4096);
267
+ const baseRef = sanitizeString(input.base_ref, 256) || 'HEAD';
268
+ const evalCommand = sanitizeString(input.eval_command, 4000);
269
+ const evalTimeout = clampInt(input.eval_timeout_sec, 120, 5, 86_400);
270
+ if (!repoPath)
271
+ throw new Error('repo_path is required');
272
+ if (!evalCommand)
273
+ throw new Error('eval_command is required');
274
+ const created = {
275
+ id,
276
+ name,
277
+ created_at: nowIso(),
278
+ status: 'active',
279
+ repo_path: repoPath,
280
+ base_ref: baseRef,
281
+ eval_command: evalCommand,
282
+ eval_timeout_sec: evalTimeout,
283
+ policy_json: sanitizeJsonString(input.policy_json, '{}'),
284
+ constraints_json: sanitizeJsonString(input.constraints_json, '{}'),
285
+ prompt_preamble: sanitizeString(input.prompt_preamble, 20_000),
286
+ };
287
+ if (this.useJsonFallback) {
288
+ if (this.jsonStore.experiments.some((row) => row.id === id)) {
289
+ throw new Error(`Experiment id already exists: ${id}`);
290
+ }
291
+ this.jsonStore.experiments.push(created);
292
+ this.flushJson();
293
+ return created;
294
+ }
295
+ this.db.prepare(`INSERT INTO experiments (
296
+ id, name, created_at, status, repo_path, base_ref, eval_command,
297
+ eval_timeout_sec, policy_json, constraints_json, prompt_preamble
298
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`).run(created.id, created.name, created.created_at, created.status, created.repo_path, created.base_ref, created.eval_command, created.eval_timeout_sec, created.policy_json, created.constraints_json, created.prompt_preamble);
299
+ return created;
300
+ }
301
+ listExperiments(limit = 100, offset = 0) {
302
+ const safeLimit = clampInt(limit, 100, 1, 1000);
303
+ const safeOffset = clampInt(offset, 0, 0, 100_000);
304
+ if (this.useJsonFallback) {
305
+ return [...this.jsonStore.experiments]
306
+ .sort((a, b) => b.created_at.localeCompare(a.created_at))
307
+ .slice(safeOffset, safeOffset + safeLimit)
308
+ .map((row) => ({ ...row }));
309
+ }
310
+ const rows = this.db
311
+ .prepare('SELECT * FROM experiments ORDER BY created_at DESC LIMIT ? OFFSET ?')
312
+ .all(safeLimit, safeOffset);
313
+ return rows.map(mapExperimentRow);
314
+ }
315
+ getExperiment(experimentId) {
316
+ const id = sanitizeString(experimentId, 128);
317
+ if (!id)
318
+ return null;
319
+ if (this.useJsonFallback) {
320
+ const row = this.jsonStore.experiments.find((x) => x.id === id);
321
+ return row ? { ...row } : null;
322
+ }
323
+ const row = this.db.prepare('SELECT * FROM experiments WHERE id = ?').get(id);
324
+ if (!row)
325
+ return null;
326
+ return mapExperimentRow(row);
327
+ }
328
+ setExperimentStatus(experimentId, status) {
329
+ const id = sanitizeString(experimentId, 128);
330
+ if (!id)
331
+ return false;
332
+ if (this.useJsonFallback) {
333
+ const idx = this.jsonStore.experiments.findIndex((row) => row.id === id);
334
+ if (idx < 0)
335
+ return false;
336
+ this.jsonStore.experiments[idx] = {
337
+ ...this.jsonStore.experiments[idx],
338
+ status,
339
+ };
340
+ this.flushJson();
341
+ return true;
342
+ }
343
+ const result = this.db.prepare('UPDATE experiments SET status = ? WHERE id = ?').run(status, id);
344
+ return result.changes > 0;
345
+ }
346
+ createRun(input) {
347
+ const id = sanitizeString(input.id, 128) || makeExplorerRunId();
348
+ const experimentId = sanitizeString(input.experiment_id, 128);
349
+ const parentCommitSha = sanitizeString(input.parent_commit_sha, 256);
350
+ const branchName = sanitizeString(input.branch_name, 256);
351
+ const worktreePath = sanitizeString(input.worktree_path, 4096);
352
+ const artifactDir = sanitizeString(input.artifact_dir, 4096);
353
+ if (!experimentId)
354
+ throw new Error('experiment_id is required');
355
+ if (!parentCommitSha)
356
+ throw new Error('parent_commit_sha is required');
357
+ if (!branchName)
358
+ throw new Error('branch_name is required');
359
+ if (!worktreePath)
360
+ throw new Error('worktree_path is required');
361
+ if (!artifactDir)
362
+ throw new Error('artifact_dir is required');
363
+ const created = {
364
+ id,
365
+ experiment_id: experimentId,
366
+ parent_run_id: input.parent_run_id ? sanitizeString(input.parent_run_id, 128) : null,
367
+ parent_commit_sha: parentCommitSha,
368
+ branch_name: branchName,
369
+ worktree_path: worktreePath,
370
+ commit_sha: null,
371
+ status: 'queued',
372
+ score: null,
373
+ metrics_json: null,
374
+ model: sanitizeString(input.model, 64) || 'stub',
375
+ summary_md: '',
376
+ created_at: nowIso(),
377
+ started_at: null,
378
+ finished_at: null,
379
+ artifact_dir: artifactDir,
380
+ worktree_pruned_at: null,
381
+ artifacts_pruned_at: null,
382
+ artifacts_mode_kept: null,
383
+ };
384
+ if (this.useJsonFallback) {
385
+ if (this.jsonStore.runs.some((row) => row.id === id)) {
386
+ throw new Error(`Run id already exists: ${id}`);
387
+ }
388
+ this.jsonStore.runs.push(created);
389
+ this.flushJson();
390
+ return created;
391
+ }
392
+ this.db.prepare(`INSERT INTO runs (
393
+ id, experiment_id, parent_run_id, parent_commit_sha, branch_name, worktree_path,
394
+ commit_sha, status, score, metrics_json, model, summary_md,
395
+ created_at, started_at, finished_at, artifact_dir,
396
+ worktree_pruned_at, artifacts_pruned_at, artifacts_mode_kept
397
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`).run(created.id, created.experiment_id, created.parent_run_id, created.parent_commit_sha, created.branch_name, created.worktree_path, created.commit_sha, created.status, created.score, created.metrics_json, created.model, created.summary_md, created.created_at, created.started_at, created.finished_at, created.artifact_dir, created.worktree_pruned_at, created.artifacts_pruned_at, created.artifacts_mode_kept);
398
+ return created;
399
+ }
400
+ markRunStarted(runId) {
401
+ const id = sanitizeString(runId, 128);
402
+ if (!id)
403
+ return false;
404
+ const startedAt = nowIso();
405
+ if (this.useJsonFallback) {
406
+ const idx = this.jsonStore.runs.findIndex((row) => row.id === id);
407
+ if (idx < 0)
408
+ return false;
409
+ this.jsonStore.runs[idx] = {
410
+ ...this.jsonStore.runs[idx],
411
+ status: 'running',
412
+ started_at: startedAt,
413
+ };
414
+ this.flushJson();
415
+ return true;
416
+ }
417
+ const result = this.db
418
+ .prepare('UPDATE runs SET status = ?, started_at = ? WHERE id = ?')
419
+ .run('running', startedAt, id);
420
+ return result.changes > 0;
421
+ }
422
+ recordRunCommit(runId, commitSha) {
423
+ const id = sanitizeString(runId, 128);
424
+ const commit = sanitizeString(commitSha, 256);
425
+ if (!id || !commit)
426
+ return false;
427
+ if (this.useJsonFallback) {
428
+ const idx = this.jsonStore.runs.findIndex((row) => row.id === id);
429
+ if (idx < 0)
430
+ return false;
431
+ this.jsonStore.runs[idx] = {
432
+ ...this.jsonStore.runs[idx],
433
+ commit_sha: commit,
434
+ status: 'commit_ok',
435
+ };
436
+ this.flushJson();
437
+ return true;
438
+ }
439
+ const result = this.db
440
+ .prepare('UPDATE runs SET commit_sha = ?, status = ? WHERE id = ?')
441
+ .run(commit, 'commit_ok', id);
442
+ return result.changes > 0;
443
+ }
444
+ recordRunResult(runId, result) {
445
+ const id = sanitizeString(runId, 128);
446
+ if (!id)
447
+ return false;
448
+ const finishedAt = nowIso();
449
+ const summary = sanitizeString(result.summary_md, 20_000);
450
+ let metricsJson = null;
451
+ if (typeof result.metrics_json === 'string' && result.metrics_json.trim()) {
452
+ try {
453
+ metricsJson = JSON.stringify(JSON.parse(result.metrics_json));
454
+ }
455
+ catch {
456
+ metricsJson = null;
457
+ }
458
+ }
459
+ const normalizedScore = Number.isFinite(result.score)
460
+ ? Number(result.score)
461
+ : null;
462
+ if (this.useJsonFallback) {
463
+ const idx = this.jsonStore.runs.findIndex((row) => row.id === id);
464
+ if (idx < 0)
465
+ return false;
466
+ this.jsonStore.runs[idx] = {
467
+ ...this.jsonStore.runs[idx],
468
+ status: result.status,
469
+ score: normalizedScore,
470
+ metrics_json: metricsJson,
471
+ summary_md: summary,
472
+ finished_at: finishedAt,
473
+ };
474
+ this.flushJson();
475
+ return true;
476
+ }
477
+ const runResult = this.db
478
+ .prepare('UPDATE runs SET status = ?, score = ?, metrics_json = ?, summary_md = ?, finished_at = ? WHERE id = ?')
479
+ .run(result.status, normalizedScore, metricsJson, summary, finishedAt, id);
480
+ return runResult.changes > 0;
481
+ }
482
+ getRun(runId) {
483
+ const id = sanitizeString(runId, 128);
484
+ if (!id)
485
+ return null;
486
+ if (this.useJsonFallback) {
487
+ const row = this.jsonStore.runs.find((x) => x.id === id);
488
+ return row ? normalizeRunObject(row) : null;
489
+ }
490
+ const row = this.db.prepare('SELECT * FROM runs WHERE id = ?').get(id);
491
+ if (!row)
492
+ return null;
493
+ return mapRunRow(row);
494
+ }
495
+ listRuns(experimentId, opts) {
496
+ const id = sanitizeString(experimentId, 128);
497
+ if (!id)
498
+ return [];
499
+ const safeLimit = clampInt(opts?.limit, 50, 1, 1000);
500
+ const safeOffset = clampInt(opts?.offset, 0, 0, 100_000);
501
+ if (this.useJsonFallback) {
502
+ return this.jsonStore.runs
503
+ .filter((row) => row.experiment_id === id)
504
+ .sort((a, b) => b.created_at.localeCompare(a.created_at))
505
+ .slice(safeOffset, safeOffset + safeLimit)
506
+ .map((row) => normalizeRunObject(row));
507
+ }
508
+ const rows = this.db
509
+ .prepare('SELECT * FROM runs WHERE experiment_id = ? ORDER BY created_at DESC LIMIT ? OFFSET ?')
510
+ .all(id, safeLimit, safeOffset);
511
+ return rows.map(mapRunRow);
512
+ }
513
+ getBestRun(experimentId) {
514
+ const id = sanitizeString(experimentId, 128);
515
+ if (!id)
516
+ return null;
517
+ if (this.useJsonFallback) {
518
+ const rows = this.jsonStore.runs
519
+ .filter((row) => row.experiment_id === id && row.status === 'eval_ok' && row.score !== null)
520
+ .sort((a, b) => {
521
+ const scoreDelta = b.score - a.score;
522
+ if (scoreDelta !== 0)
523
+ return scoreDelta;
524
+ return b.created_at.localeCompare(a.created_at);
525
+ });
526
+ return rows[0] ? normalizeRunObject(rows[0]) : null;
527
+ }
528
+ const row = this.db
529
+ .prepare(`SELECT * FROM runs
530
+ WHERE experiment_id = ? AND status = 'eval_ok' AND score IS NOT NULL
531
+ ORDER BY score DESC, created_at DESC
532
+ LIMIT 1`)
533
+ .get(id);
534
+ if (!row)
535
+ return null;
536
+ return mapRunRow(row);
537
+ }
538
+ hasActiveRun(experimentId) {
539
+ const id = sanitizeString(experimentId, 128);
540
+ if (!id)
541
+ return false;
542
+ if (this.useJsonFallback) {
543
+ return this.jsonStore.runs.some((row) => row.experiment_id === id && ACTIVE_RUN_STATES.has(row.status));
544
+ }
545
+ const row = this.db
546
+ .prepare(`SELECT COUNT(*) as count FROM runs
547
+ WHERE experiment_id = ? AND status IN ('queued', 'running', 'commit_ok')`)
548
+ .get(id);
549
+ return Number(row?.count || 0) > 0;
550
+ }
551
+ listActiveRuns(experimentId) {
552
+ const id = sanitizeString(experimentId, 128);
553
+ if (!id)
554
+ return [];
555
+ if (this.useJsonFallback) {
556
+ return this.jsonStore.runs
557
+ .filter((row) => row.experiment_id === id && ACTIVE_RUN_STATES.has(row.status))
558
+ .sort((a, b) => a.created_at.localeCompare(b.created_at))
559
+ .map((row) => normalizeRunObject(row));
560
+ }
561
+ const rows = this.db
562
+ .prepare(`SELECT * FROM runs
563
+ WHERE experiment_id = ? AND status IN ('queued', 'running', 'commit_ok')
564
+ ORDER BY created_at ASC`)
565
+ .all(id);
566
+ return rows.map(mapRunRow);
567
+ }
568
+ getRunCount(experimentId) {
569
+ const id = sanitizeString(experimentId, 128);
570
+ if (!id)
571
+ return 0;
572
+ if (this.useJsonFallback) {
573
+ let count = 0;
574
+ for (const run of this.jsonStore.runs) {
575
+ if (run.experiment_id === id)
576
+ count++;
577
+ }
578
+ return count;
579
+ }
580
+ const row = this.db.prepare('SELECT COUNT(*) as count FROM runs WHERE experiment_id = ?').get(id);
581
+ return Number(row?.count || 0);
582
+ }
583
+ getRunStatusCounts(experimentId) {
584
+ const id = sanitizeString(experimentId, 128);
585
+ const counts = initStatusCounts();
586
+ if (!id)
587
+ return counts;
588
+ if (this.useJsonFallback) {
589
+ for (const run of this.jsonStore.runs) {
590
+ if (run.experiment_id !== id)
591
+ continue;
592
+ counts[run.status] = (counts[run.status] || 0) + 1;
593
+ }
594
+ return counts;
595
+ }
596
+ const rows = this.db
597
+ .prepare('SELECT status, COUNT(*) as count FROM runs WHERE experiment_id = ? GROUP BY status')
598
+ .all(id);
599
+ for (const row of rows) {
600
+ const status = String(row.status);
601
+ if (!RUN_STATUS_VALUES.includes(status))
602
+ continue;
603
+ counts[status] = Number(row.count || 0);
604
+ }
605
+ return counts;
606
+ }
607
+ getLatestRun(experimentId) {
608
+ const id = sanitizeString(experimentId, 128);
609
+ if (!id)
610
+ return null;
611
+ if (this.useJsonFallback) {
612
+ const row = this.jsonStore.runs
613
+ .filter((run) => run.experiment_id === id)
614
+ .sort((a, b) => b.created_at.localeCompare(a.created_at) || b.id.localeCompare(a.id))[0];
615
+ return row ? normalizeRunObject(row) : null;
616
+ }
617
+ const row = this.db
618
+ .prepare('SELECT * FROM runs WHERE experiment_id = ? ORDER BY created_at DESC, id DESC LIMIT 1')
619
+ .get(id);
620
+ if (!row)
621
+ return null;
622
+ return mapRunRow(row);
623
+ }
624
+ getExperimentOverview(experimentId) {
625
+ const experiment = this.getExperiment(experimentId);
626
+ if (!experiment)
627
+ return null;
628
+ const retentionPolicy = this.getRetentionPolicy(experimentId);
629
+ const runCount = this.getRunCount(experimentId);
630
+ const activeRuns = this.listActiveRuns(experimentId);
631
+ const statusCounts = this.getRunStatusCounts(experimentId);
632
+ const bestRun = this.getBestRun(experimentId);
633
+ const latestRun = this.getLatestRun(experimentId);
634
+ return {
635
+ experiment,
636
+ retention_policy: retentionPolicy,
637
+ run_count: runCount,
638
+ active_run_count: activeRuns.length,
639
+ status_counts: statusCounts,
640
+ best_run: bestRun,
641
+ latest_run: latestRun,
642
+ };
643
+ }
644
+ getRetentionPolicy(experimentId) {
645
+ const experiment = this.getExperiment(experimentId);
646
+ if (!experiment)
647
+ return (0, explorer_retention_js_1.defaultRetentionPolicy)();
648
+ let constraints = {};
649
+ try {
650
+ const parsed = JSON.parse(experiment.constraints_json || '{}');
651
+ if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) {
652
+ constraints = parsed;
653
+ }
654
+ }
655
+ catch {
656
+ constraints = {};
657
+ }
658
+ return (0, explorer_retention_js_1.normalizeRetentionPolicy)(constraints.retention);
659
+ }
660
+ setRetentionPolicy(experimentId, retention) {
661
+ const experiment = this.getExperiment(experimentId);
662
+ if (!experiment)
663
+ return false;
664
+ let constraints = {};
665
+ try {
666
+ const parsed = JSON.parse(experiment.constraints_json || '{}');
667
+ if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) {
668
+ constraints = parsed;
669
+ }
670
+ }
671
+ catch {
672
+ constraints = {};
673
+ }
674
+ constraints.retention = (0, explorer_retention_js_1.normalizeRetentionPolicy)(retention);
675
+ const nextJson = JSON.stringify(constraints);
676
+ if (this.useJsonFallback) {
677
+ const idx = this.jsonStore.experiments.findIndex((row) => row.id === experiment.id);
678
+ if (idx < 0)
679
+ return false;
680
+ this.jsonStore.experiments[idx] = {
681
+ ...this.jsonStore.experiments[idx],
682
+ constraints_json: nextJson,
683
+ };
684
+ this.flushJson();
685
+ return true;
686
+ }
687
+ const result = this.db
688
+ .prepare('UPDATE experiments SET constraints_json = ? WHERE id = ?')
689
+ .run(nextJson, experiment.id);
690
+ return result.changes > 0;
691
+ }
692
+ setRunWorktreePruned(runId, prunedAt = nowIso()) {
693
+ const id = sanitizeString(runId, 128);
694
+ if (!id)
695
+ return false;
696
+ if (this.useJsonFallback) {
697
+ const idx = this.jsonStore.runs.findIndex((row) => row.id === id);
698
+ if (idx < 0)
699
+ return false;
700
+ this.jsonStore.runs[idx] = {
701
+ ...this.jsonStore.runs[idx],
702
+ worktree_pruned_at: prunedAt,
703
+ };
704
+ this.flushJson();
705
+ return true;
706
+ }
707
+ const result = this.db
708
+ .prepare('UPDATE runs SET worktree_pruned_at = ? WHERE id = ?')
709
+ .run(prunedAt, id);
710
+ return result.changes > 0;
711
+ }
712
+ setRunArtifactsPruned(runId, modeKept, prunedAt = nowIso()) {
713
+ const id = sanitizeString(runId, 128);
714
+ if (!id)
715
+ return false;
716
+ if (this.useJsonFallback) {
717
+ const idx = this.jsonStore.runs.findIndex((row) => row.id === id);
718
+ if (idx < 0)
719
+ return false;
720
+ this.jsonStore.runs[idx] = {
721
+ ...this.jsonStore.runs[idx],
722
+ artifacts_pruned_at: prunedAt,
723
+ artifacts_mode_kept: modeKept,
724
+ };
725
+ this.flushJson();
726
+ return true;
727
+ }
728
+ const result = this.db
729
+ .prepare('UPDATE runs SET artifacts_pruned_at = ?, artifacts_mode_kept = ? WHERE id = ?')
730
+ .run(prunedAt, modeKept, id);
731
+ return result.changes > 0;
732
+ }
733
+ listFinishedRuns(experimentId) {
734
+ const id = sanitizeString(experimentId, 128);
735
+ if (!id)
736
+ return [];
737
+ const finishedStates = new Set([
738
+ 'eval_ok',
739
+ 'failed_build',
740
+ 'failed_eval',
741
+ 'timeout',
742
+ 'invalid_output',
743
+ ]);
744
+ if (this.useJsonFallback) {
745
+ return this.jsonStore.runs
746
+ .filter((run) => run.experiment_id === id && finishedStates.has(run.status))
747
+ .map((run) => normalizeRunObject(run))
748
+ .sort((a, b) => (b.finished_at || '').localeCompare(a.finished_at || '') || b.created_at.localeCompare(a.created_at));
749
+ }
750
+ const rows = this.db
751
+ .prepare(`SELECT * FROM runs
752
+ WHERE experiment_id = ? AND status IN ('eval_ok', 'failed_build', 'failed_eval', 'timeout', 'invalid_output')
753
+ ORDER BY finished_at DESC, created_at DESC`)
754
+ .all(id);
755
+ return rows.map(mapRunRow);
756
+ }
757
+ createEvent(experimentId, type, payload, runId) {
758
+ const expId = sanitizeString(experimentId, 128);
759
+ if (!expId)
760
+ throw new Error('experiment_id is required');
761
+ const event = {
762
+ id: makeEventId(),
763
+ experiment_id: expId,
764
+ run_id: runId ? sanitizeString(runId, 128) : null,
765
+ type,
766
+ payload_json: JSON.stringify(payload || {}),
767
+ created_at: nowIso(),
768
+ };
769
+ if (this.useJsonFallback) {
770
+ this.jsonStore.events.push(event);
771
+ this.flushJson();
772
+ return event;
773
+ }
774
+ this.db.prepare('INSERT INTO events (id, experiment_id, run_id, type, payload_json, created_at) VALUES (?, ?, ?, ?, ?, ?)').run(event.id, event.experiment_id, event.run_id, event.type, event.payload_json, event.created_at);
775
+ return event;
776
+ }
777
+ listEvents(experimentId, opts) {
778
+ const expId = sanitizeString(experimentId, 128);
779
+ if (!expId)
780
+ return [];
781
+ const safeLimit = clampInt(opts?.limit, 100, 1, 1000);
782
+ const safeOffset = clampInt(opts?.offset, 0, 0, 100_000);
783
+ if (this.useJsonFallback) {
784
+ return this.jsonStore.events
785
+ .filter((row) => row.experiment_id === expId)
786
+ .sort((a, b) => b.created_at.localeCompare(a.created_at))
787
+ .slice(safeOffset, safeOffset + safeLimit)
788
+ .map((row) => ({ ...row }));
789
+ }
790
+ const rows = this.db
791
+ .prepare('SELECT * FROM events WHERE experiment_id = ? ORDER BY created_at DESC LIMIT ? OFFSET ?')
792
+ .all(expId, safeLimit, safeOffset);
793
+ return rows.map(mapEventRow);
794
+ }
795
+ getLeaderboard(experimentId, topK = 10) {
796
+ const expId = sanitizeString(experimentId, 128);
797
+ if (!expId)
798
+ return [];
799
+ const safeTopK = clampInt(topK, 10, 1, 500);
800
+ if (this.useJsonFallback) {
801
+ return this.jsonStore.runs
802
+ .filter((row) => row.experiment_id === expId && row.status === 'eval_ok' && row.score !== null)
803
+ .sort((a, b) => {
804
+ const scoreDelta = b.score - a.score;
805
+ if (scoreDelta !== 0)
806
+ return scoreDelta;
807
+ return b.created_at.localeCompare(a.created_at);
808
+ })
809
+ .slice(0, safeTopK)
810
+ .map((row) => ({
811
+ run_id: row.id,
812
+ score: row.score,
813
+ status: row.status,
814
+ commit_sha: row.commit_sha,
815
+ created_at: row.created_at,
816
+ }));
817
+ }
818
+ const rows = this.db
819
+ .prepare(`SELECT id as run_id, score, status, commit_sha, created_at
820
+ FROM runs
821
+ WHERE experiment_id = ? AND status = 'eval_ok' AND score IS NOT NULL
822
+ ORDER BY score DESC, created_at DESC
823
+ LIMIT ?`)
824
+ .all(expId, safeTopK);
825
+ return rows.map((row) => ({
826
+ run_id: String(row.run_id),
827
+ score: Number(row.score),
828
+ status: row.status,
829
+ commit_sha: row.commit_sha ? String(row.commit_sha) : null,
830
+ created_at: String(row.created_at),
831
+ }));
832
+ }
833
+ getTree(experimentId, mode = 'best_path') {
834
+ const expId = sanitizeString(experimentId, 128);
835
+ if (!expId) {
836
+ return { mode, nodes: [], edges: [] };
837
+ }
838
+ const allRuns = this.getAllRunsForTree(expId);
839
+ if (allRuns.length === 0) {
840
+ return { mode, nodes: [], edges: [] };
841
+ }
842
+ if (mode === 'full') {
843
+ const nodes = allRuns.map((run) => ({
844
+ run_id: run.id,
845
+ parent_run_id: run.parent_run_id,
846
+ score: run.score,
847
+ status: run.status,
848
+ commit_sha: run.commit_sha,
849
+ created_at: run.created_at,
850
+ }));
851
+ const edges = allRuns
852
+ .filter((run) => !!run.parent_run_id)
853
+ .map((run) => ({ from: run.parent_run_id, to: run.id }));
854
+ return { mode, nodes, edges };
855
+ }
856
+ const best = this.getBestRun(expId);
857
+ if (!best) {
858
+ return { mode, nodes: [], edges: [] };
859
+ }
860
+ const byId = new Map(allRuns.map((run) => [run.id, run]));
861
+ const path = [];
862
+ const visited = new Set();
863
+ let cursor = best;
864
+ while (cursor && !visited.has(cursor.id)) {
865
+ visited.add(cursor.id);
866
+ path.push(cursor);
867
+ if (!cursor.parent_run_id)
868
+ break;
869
+ cursor = byId.get(cursor.parent_run_id);
870
+ }
871
+ path.reverse();
872
+ const nodes = path.map((run) => ({
873
+ run_id: run.id,
874
+ parent_run_id: run.parent_run_id,
875
+ score: run.score,
876
+ status: run.status,
877
+ commit_sha: run.commit_sha,
878
+ created_at: run.created_at,
879
+ }));
880
+ const edges = [];
881
+ for (let i = 1; i < path.length; i++) {
882
+ edges.push({ from: path[i - 1].id, to: path[i].id });
883
+ }
884
+ return { mode, nodes, edges };
885
+ }
886
+ close() {
887
+ if (this.useJsonFallback) {
888
+ this.flushJson();
889
+ return;
890
+ }
891
+ this.db.close();
892
+ }
893
+ getAllRunsForTree(experimentId) {
894
+ if (this.useJsonFallback) {
895
+ return this.jsonStore.runs
896
+ .filter((row) => row.experiment_id === experimentId)
897
+ .sort((a, b) => a.created_at.localeCompare(b.created_at))
898
+ .map((row) => normalizeRunObject(row));
899
+ }
900
+ const rows = this.db
901
+ .prepare('SELECT * FROM runs WHERE experiment_id = ? ORDER BY created_at ASC')
902
+ .all(experimentId);
903
+ return rows.map(mapRunRow);
904
+ }
905
+ loadJson() {
906
+ if (!this.jsonPath || !(0, fs_1.existsSync)(this.jsonPath))
907
+ return;
908
+ try {
909
+ const parsed = JSON.parse((0, fs_1.readFileSync)(this.jsonPath, 'utf-8'));
910
+ const experiments = Array.isArray(parsed?.experiments) ? parsed.experiments : [];
911
+ const runs = Array.isArray(parsed?.runs) ? parsed.runs : [];
912
+ const events = Array.isArray(parsed?.events) ? parsed.events : [];
913
+ this.jsonStore = {
914
+ version: 1,
915
+ experiments,
916
+ runs,
917
+ events,
918
+ };
919
+ }
920
+ catch {
921
+ this.jsonStore = {
922
+ version: 1,
923
+ experiments: [],
924
+ runs: [],
925
+ events: [],
926
+ };
927
+ }
928
+ }
929
+ flushJson() {
930
+ if (!this.jsonPath)
931
+ return;
932
+ try {
933
+ (0, fs_1.writeFileSync)(this.jsonPath, JSON.stringify(this.jsonStore, null, 2) + '\n', {
934
+ encoding: 'utf-8',
935
+ mode: 0o600,
936
+ });
937
+ try {
938
+ (0, fs_1.chmodSync)(this.jsonPath, 0o600);
939
+ }
940
+ catch {
941
+ // Best effort only.
942
+ }
943
+ }
944
+ catch {
945
+ // Best effort; continue in-memory.
946
+ }
947
+ }
948
+ }
949
+ exports.ExplorerStore = ExplorerStore;
950
+ //# sourceMappingURL=explorer-store.js.map