@ngockhoale/ukit 3.4.47 → 3.4.48

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 (62) hide show
  1. package/CHANGELOG.md +8 -0
  2. package/manifests/documentation.yaml +22 -0
  3. package/manifests/platform.full.yaml +27 -0
  4. package/package.json +4 -1
  5. package/scripts/dev/worktree-preflight.mjs +20 -1
  6. package/src/core/diffPlan.js +38 -12
  7. package/src/core/executionContracts.js +67 -16
  8. package/src/core/metadata.js +4 -0
  9. package/src/core/runInstallPipeline.js +19 -2
  10. package/src/core/runtimeConfig.js +1 -1
  11. package/src/index/playbookRegistry.js +22 -3
  12. package/src/index/taskRouting.js +2 -2
  13. package/template_project/.claude/agents/bug-debugger.md +1 -0
  14. package/template_project/.claude/agents/code-reviewer.md +1 -0
  15. package/template_project/.claude/agents/feature-implementer.md +1 -0
  16. package/template_project/.claude/agents/handoff-planner.md +1 -0
  17. package/template_project/.claude/agents/ukit-small-task-maintainer.md +1 -0
  18. package/template_project/.claude/hooks/completion-gate.sh +1 -1
  19. package/template_project/.claude/hooks/delegation-gate.mjs +343 -0
  20. package/template_project/.claude/hooks/skill-router.sh +147 -6
  21. package/template_project/.claude/settings.json +8 -8
  22. package/template_project/.claude/ukit/index/playbook-registry.mjs +22 -3
  23. package/template_project/.claude/ukit/index/route-resolver.mjs +67 -15
  24. package/template_project/.claude/ukit/index/route-task.mjs +115 -37
  25. package/template_project/.claude/ukit/index/tier-lane.mjs +202 -0
  26. package/template_project/.claude/ukit/runtime/cron-operation-adapter.mjs +3 -1
  27. package/template_project/.claude/ukit/runtime/execution-ledger.mjs +306 -20
  28. package/template_project/.claude/ukit/runtime/hook-chain-runner.mjs +8 -0
  29. package/template_project/.claude/ukit/runtime/loop-state.mjs +3 -1
  30. package/template_project/.claude/ukit/runtime/observability-emit.mjs +38 -0
  31. package/template_project/.claude/ukit/runtime/playbook-execution.mjs +704 -0
  32. package/template_project/.claude/ukit/runtime/stop-coordinator.mjs +79 -3
  33. package/template_project/.omp/agents/bug-debugger.md +1 -0
  34. package/template_project/.omp/agents/code-reviewer.md +1 -0
  35. package/template_project/.omp/agents/feature-implementer.md +1 -0
  36. package/template_project/.omp/agents/handoff-planner.md +1 -0
  37. package/template_project/.omp/agents/ukit-small-task-maintainer.md +1 -0
  38. package/template_project/.omp/hooks/pre/ukit-bridge.js +254 -7
  39. package/template_project/ukit/storage/config.json +1 -1
  40. package/scripts/bench/data-foundation.mjs +0 -929
  41. package/scripts/bench/decision-runtime-baseline.mjs +0 -427
  42. package/scripts/bench/decision-runtime-variant.mjs +0 -626
  43. package/scripts/bench/goldTasks.json +0 -38
  44. package/scripts/bench/memory-ablation.mjs +0 -495
  45. package/scripts/bench/memory-baseline.mjs +0 -596
  46. package/scripts/bench/memory-bench.mjs +0 -661
  47. package/scripts/bench/memory-canary.mjs +0 -331
  48. package/scripts/bench/memory-corpus.mjs +0 -354
  49. package/scripts/bench/memory-gate.mjs +0 -389
  50. package/scripts/bench/memory-metrics.mjs +0 -179
  51. package/scripts/bench/outline-savings.mjs +0 -107
  52. package/scripts/bench/parallel-agents.mjs +0 -224
  53. package/scripts/bench/recorder-overhead.mjs +0 -204
  54. package/scripts/bench/runGold.mjs +0 -238
  55. package/scripts/bench/sqlite-spike.mjs +0 -451
  56. package/scripts/bench/subagent-orchestrator-corpus.mjs +0 -275
  57. package/scripts/bench/subagent-orchestrator-eval.mjs +0 -565
  58. package/scripts/bench/v3-ceremony.mjs +0 -220
  59. package/scripts/perf/audit-perf.mjs +0 -1195
  60. package/scripts/perf/diff-perf-findings.mjs +0 -136
  61. package/scripts/perf/perf-findings.json +0 -474
  62. package/scripts/perf/perf-measure.md +0 -271
@@ -1,451 +0,0 @@
1
- #!/usr/bin/env node
2
- // TASK-005 — SQLite viability spike (SPEC §5 FR-009/FR-010, M03-03).
3
- //
4
- // Prototype-only probe matrix answering the runtime/packaging questions for
5
- // gate G3: node:sqlite availability across Node >=20 (20.x needs
6
- // --experimental-sqlite; >=22.5 is unflagged), WAL behavior, busy-timeout
7
- // under a second writer, concurrent-writer burst, backup(), schema migration
8
- // roundtrip, file footprint vs JSON, license. Every cell gets an explicit
9
- // verdict `tested|unsupported|failed` + detail string — verdicts are data.
10
- //
11
- // Isolation (FR-010): writes only under a mkdtemp dir or an explicit
12
- // --wal-dir target; never touches repo stores or package.json. Temp dirs are
13
- // cleaned up; files created inside --wal-dir are prefixed `ukit-spike-` and
14
- // removed after the run.
15
- //
16
- // Exit codes: 0 = ran to completion (verdicts are data), 1 = usage error,
17
- // 2 = harness crash.
18
- //
19
- // Test hook: UKIT_SPIKE_SQLITE_IMPORT=fail forces the node:sqlite import to
20
- // fail so the unsupported path is exercisable on Node >=22.5.
21
-
22
- import { execSync, spawn } from 'node:child_process';
23
- import fs from 'node:fs';
24
- import os from 'node:os';
25
- import path from 'node:path';
26
- import { fileURLToPath } from 'node:url';
27
-
28
- const REPO_ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '../..');
29
- const NODE = process.execPath;
30
- const SPIKE_PREFIX = 'ukit-spike-';
31
- const BURST_PROCESSES = 2;
32
- const BURST_ROWS_PER_PROCESS = 25;
33
- const FOOTPRINT_ROWS = 500;
34
-
35
- const ALL_CELLS = [
36
- 'node-sqlite-availability',
37
- 'open-create',
38
- 'wal-mode',
39
- 'busy-timeout',
40
- 'concurrent-writers',
41
- 'backup',
42
- 'migration-roundtrip',
43
- 'footprint',
44
- 'license',
45
- ];
46
-
47
- const USAGE = `Usage: node scripts/bench/sqlite-spike.mjs --out <file.json> [--wal-dir <dir>]
48
-
49
- Options:
50
- --out <file.json> Report output path; <file>.md is written beside it (required)
51
- --wal-dir <dir> Target dir for the WAL/busy-timeout probes (default: mkdtemp
52
- local dir). Point at a network-FS mount to probe WAL there.
53
-
54
- Exit codes: 0 = completed (verdicts are data), 1 = usage error, 2 = crash.`;
55
-
56
- // ---------- small helpers ----------
57
-
58
- function parseArgs(argv) {
59
- const opts = { out: null, walDir: null };
60
- for (let i = 0; i < argv.length; i += 1) {
61
- const arg = argv[i];
62
- if (arg === '--out') { opts.out = argv[++i]; }
63
- else if (arg === '--wal-dir') { opts.walDir = argv[++i]; }
64
- else if (arg === '--help' || arg === '-h') { console.log(USAGE); process.exit(0); }
65
- else { throw new Error(`unknown argument: ${arg}\n${USAGE}`); }
66
- }
67
- if (!opts.out) throw new Error(`--out is required\n${USAGE}`);
68
- return opts;
69
- }
70
-
71
- function cell(verdict, detail) {
72
- return { verdict, detail: String(detail) };
73
- }
74
-
75
- function spawnNode(argv, { timeoutMs = 30_000 } = {}) {
76
- return new Promise((resolve) => {
77
- const child = spawn(NODE, argv, { stdio: ['ignore', 'pipe', 'pipe'] });
78
- let stdout = '';
79
- let stderr = '';
80
- const timer = setTimeout(() => {
81
- child.kill('SIGKILL');
82
- resolve({ exitCode: null, signal: 'SIGKILL', stdout, stderr, timedOut: true });
83
- }, timeoutMs);
84
- child.stdout.on('data', (d) => { stdout += d; });
85
- child.stderr.on('data', (d) => { stderr += d; });
86
- child.on('error', (error) => {
87
- clearTimeout(timer);
88
- resolve({ exitCode: null, signal: null, stdout, stderr: stderr + String(error), timedOut: false });
89
- });
90
- child.on('close', (exitCode, signal) => {
91
- clearTimeout(timer);
92
- resolve({ exitCode, signal, stdout, stderr, timedOut: false });
93
- });
94
- });
95
- }
96
-
97
- /** Child helper source: open db, BEGIN EXCLUSIVE, print 'locked', hold, commit. */
98
- const LOCK_HOLDER_SOURCE = `
99
- import('node:sqlite').then(({ DatabaseSync }) => {
100
- const db = new DatabaseSync(process.argv[1]);
101
- db.exec('CREATE TABLE IF NOT EXISTS spike_lock (id INTEGER PRIMARY KEY, v TEXT)');
102
- db.exec('BEGIN EXCLUSIVE');
103
- process.stdout.write('locked\\n');
104
- setTimeout(() => { db.exec('COMMIT'); db.close(); process.exit(0); }, 1500);
105
- }).catch((e) => { process.stderr.write(String(e)); process.exit(1); });
106
- `;
107
-
108
- /** Child helper source: insert N rows with busy_timeout, print inserted count. */
109
- const BURST_WRITER_SOURCE = `
110
- import('node:sqlite').then(({ DatabaseSync }) => {
111
- const [dbPath, tag, n] = [process.argv[1], process.argv[2], Number(process.argv[3])];
112
- const db = new DatabaseSync(dbPath);
113
- db.exec('PRAGMA busy_timeout = 10000');
114
- const ins = db.prepare('INSERT INTO spike_burst (writer, seq) VALUES (?, ?)');
115
- for (let i = 0; i < n; i += 1) ins.run(tag, i);
116
- db.close();
117
- process.stdout.write(String(n));
118
- process.exit(0);
119
- }).catch((e) => { process.stderr.write(String(e)); process.exit(1); });
120
- `;
121
-
122
- // ---------- probes ----------
123
-
124
- async function probeAvailability() {
125
- const major = Number(process.versions.node.split('.')[0]);
126
- const minor = Number(process.versions.node.split('.')[1]);
127
- const flagNote = major === 20
128
- ? 'Node 20.x requires --experimental-sqlite'
129
- : (major === 22 && minor < 5)
130
- ? 'node:sqlite lands in Node 22.5; earlier 22.x unavailable'
131
- : 'unflagged on this Node';
132
- if (process.env.UKIT_SPIKE_SQLITE_IMPORT === 'fail') {
133
- return cell('unsupported', `node:sqlite import failed (injected); ${flagNote}`);
134
- }
135
- try {
136
- const mod = await import('node:sqlite');
137
- if (typeof mod.DatabaseSync !== 'function') {
138
- return cell('unsupported', `node:sqlite imported but DatabaseSync missing; ${flagNote}`);
139
- }
140
- const warned = major < 22 || (major === 22 && minor < 5)
141
- ? 'experimental flag required'
142
- : 'experimental-warning emitted on stderr (still experimental)';
143
- return cell('tested', `node ${process.version}: DatabaseSync present; ${flagNote}; ${warned}`);
144
- } catch (error) {
145
- return cell('unsupported', `node:sqlite import failed: ${error?.message ?? error}; ${flagNote}`);
146
- }
147
- }
148
-
149
- async function withDb(dir, name, fn) {
150
- const { DatabaseSync } = await import('node:sqlite');
151
- const dbPath = path.join(dir, `${SPIKE_PREFIX}${name}.db`);
152
- const db = new DatabaseSync(dbPath);
153
- try {
154
- return await fn(db, dbPath);
155
- } finally {
156
- try { db.close(); } catch { /* already closed */ }
157
- }
158
- }
159
-
160
- async function probeOpenCreate(dir) {
161
- try {
162
- return await withDb(dir, 'open', async (db, dbPath) => {
163
- db.exec('CREATE TABLE t (id INTEGER PRIMARY KEY, v TEXT)');
164
- db.prepare('INSERT INTO t (v) VALUES (?)').run('x');
165
- const row = db.prepare('SELECT COUNT(*) AS n FROM t').get();
166
- const bytes = fs.statSync(dbPath).size;
167
- return cell('tested', `open+create+insert ok; rows=${row.n}; fileBytes=${bytes}`);
168
- });
169
- } catch (error) {
170
- return cell('failed', `open-create threw: ${error?.message ?? error}`);
171
- }
172
- }
173
-
174
- async function probeWalMode(dir) {
175
- try {
176
- return await withDb(dir, 'wal', async (db, dbPath) => {
177
- const mode = db.prepare('PRAGMA journal_mode = WAL').get()?.journal_mode;
178
- db.exec('CREATE TABLE t (id INTEGER PRIMARY KEY)');
179
- db.prepare('INSERT INTO t DEFAULT VALUES').run();
180
- const walExists = fs.existsSync(`${dbPath}-wal`) || mode === 'wal';
181
- if (mode !== 'wal') {
182
- return cell('failed', `journal_mode=${mode} (expected wal); dir=${path.basename(dir)}`);
183
- }
184
- return cell('tested', `journal_mode=wal verified; walFileObserved=${walExists}`);
185
- });
186
- } catch (error) {
187
- return cell('failed', `wal-mode threw: ${error?.message ?? error}`);
188
- }
189
- }
190
-
191
- async function probeBusyTimeout(dir) {
192
- try {
193
- const dbPath = path.join(dir, `${SPIKE_PREFIX}busy.db`);
194
- const holder = spawn(NODE, ['--input-type=module', '-e', LOCK_HOLDER_SOURCE, dbPath],
195
- { stdio: ['ignore', 'pipe', 'pipe'] });
196
- let locked = false;
197
- await new Promise((resolve) => {
198
- const timer = setTimeout(resolve, 10_000);
199
- holder.stdout.on('data', (d) => {
200
- if (String(d).includes('locked')) { locked = true; clearTimeout(timer); resolve(); }
201
- });
202
- holder.on('exit', () => { clearTimeout(timer); resolve(); });
203
- });
204
- if (!locked) {
205
- holder.kill('SIGKILL');
206
- return cell('failed', 'lock-holder child never acquired the write lock');
207
- }
208
- const { DatabaseSync } = await import('node:sqlite');
209
- const db = new DatabaseSync(dbPath);
210
- try {
211
- db.exec('PRAGMA busy_timeout = 5000');
212
- const started = Date.now();
213
- db.exec('INSERT INTO spike_lock (v) VALUES (\'waited\')');
214
- const waitedMs = Date.now() - started;
215
- const row = db.prepare('SELECT COUNT(*) AS n FROM spike_lock').get();
216
- await new Promise((resolve) => holder.on('close', resolve));
217
- return cell('tested', `busy_timeout=5000 waited ${waitedMs}ms for second writer; rows=${row.n}`);
218
- } finally {
219
- try { db.close(); } catch { /* noop */ }
220
- }
221
- } catch (error) {
222
- return cell('failed', `busy-timeout threw: ${error?.message ?? error}`);
223
- }
224
- }
225
-
226
- async function probeConcurrentWriters(dir) {
227
- try {
228
- const dbPath = path.join(dir, `${SPIKE_PREFIX}burst.db`);
229
- // Parent creates the schema + WAL first: concurrent CREATE TABLE /
230
- // journal_mode changes are not covered by busy_timeout and race.
231
- {
232
- const { DatabaseSync } = await import('node:sqlite');
233
- const setup = new DatabaseSync(dbPath);
234
- setup.exec('PRAGMA journal_mode = WAL');
235
- setup.exec('CREATE TABLE IF NOT EXISTS spike_burst (id INTEGER PRIMARY KEY, writer TEXT, seq INTEGER)');
236
- setup.close();
237
- }
238
- const expected = BURST_PROCESSES * BURST_ROWS_PER_PROCESS;
239
- const runs = [];
240
- for (let i = 0; i < BURST_PROCESSES; i += 1) {
241
- runs.push(spawnNode(
242
- ['--input-type=module', '-e', BURST_WRITER_SOURCE, dbPath, `w${i}`, String(BURST_ROWS_PER_PROCESS)],
243
- { timeoutMs: 30_000 },
244
- ));
245
- }
246
- const results = await Promise.all(runs);
247
- const spawnFailures = results.filter((r) => r.exitCode !== 0).length;
248
- const { DatabaseSync } = await import('node:sqlite');
249
- const db = new DatabaseSync(dbPath);
250
- let persisted = 0;
251
- try {
252
- persisted = db.prepare('SELECT COUNT(*) AS n FROM spike_burst').get().n;
253
- } finally {
254
- db.close();
255
- }
256
- const lost = expected - persisted;
257
- if (spawnFailures > 0) {
258
- const err = results.find((r) => r.exitCode !== 0)?.stderr?.trim() ?? 'unknown';
259
- return cell('failed', `${spawnFailures}/${BURST_PROCESSES} writer processes failed: ${err}; persisted=${persisted}/${expected}`);
260
- }
261
- if (lost !== 0) {
262
- return cell('failed', `lost updates: persisted=${persisted}/${expected} lost=${lost}`);
263
- }
264
- return cell('tested', `${BURST_PROCESSES} processes x ${BURST_ROWS_PER_PROCESS} inserts; persisted=${persisted}/${expected}; lost=0`);
265
- } catch (error) {
266
- return cell('failed', `concurrent-writers threw: ${error?.message ?? error}`);
267
- }
268
- }
269
-
270
- async function probeBackup(dir) {
271
- try {
272
- const { DatabaseSync } = await import('node:sqlite');
273
- const srcPath = path.join(dir, `${SPIKE_PREFIX}backup-src.db`);
274
- const dstPath = path.join(dir, `${SPIKE_PREFIX}backup-dst.db`);
275
- const src = new DatabaseSync(srcPath);
276
- src.exec('CREATE TABLE t (id INTEGER PRIMARY KEY, v TEXT)');
277
- const ins = src.prepare('INSERT INTO t (v) VALUES (?)');
278
- for (let i = 0; i < 10; i += 1) ins.run(`row-${i}`);
279
- if (typeof src.backup !== 'function') {
280
- src.close();
281
- return cell('unsupported', 'DatabaseSync.backup() not present on this Node');
282
- }
283
- await src.backup(dstPath);
284
- src.close();
285
- const dst = new DatabaseSync(dstPath);
286
- const row = dst.prepare('SELECT COUNT(*) AS n FROM t').get();
287
- dst.close();
288
- if (row.n !== 10) return cell('failed', `backup copy row-count=${row.n} (expected 10)`);
289
- return cell('tested', `backup() snapshot reopened; row-count=${row.n} equal`);
290
- } catch (error) {
291
- return cell('failed', `backup threw: ${error?.message ?? error}`);
292
- }
293
- }
294
-
295
- async function probeMigration(dir) {
296
- try {
297
- return await withDb(dir, 'migrate', async (db) => {
298
- db.exec('CREATE TABLE m (id INTEGER PRIMARY KEY, a TEXT)');
299
- db.prepare('INSERT INTO m (a) VALUES (?)').run('before');
300
- db.exec('ALTER TABLE m ADD COLUMN b TEXT DEFAULT \'d\'');
301
- db.prepare('UPDATE m SET b = ? WHERE id = 1').run('after');
302
- const row = db.prepare('SELECT a, b FROM m WHERE id = 1').get();
303
- if (row.a !== 'before' || row.b !== 'after') {
304
- return cell('failed', `roundtrip mismatch: ${JSON.stringify(row)}`);
305
- }
306
- return cell('tested', 'create→alter→read roundtrip ok; added column readable');
307
- });
308
- } catch (error) {
309
- return cell('failed', `migration-roundtrip threw: ${error?.message ?? error}`);
310
- }
311
- }
312
-
313
- async function probeFootprint(dir) {
314
- try {
315
- const rows = [];
316
- for (let i = 0; i < FOOTPRINT_ROWS; i += 1) {
317
- rows.push({ id: `rec-${i}`, text: `synthetic memory record ${i} lorem ipsum dolor sit amet`, kind: 'fact' });
318
- }
319
- const jsonPath = path.join(dir, `${SPIKE_PREFIX}footprint.json`);
320
- fs.writeFileSync(jsonPath, JSON.stringify({ schemaVersion: 2, records: rows }));
321
- const jsonBytes = fs.statSync(jsonPath).size;
322
- return await withDb(dir, 'footprint', async (db, dbPath) => {
323
- db.exec('CREATE TABLE r (id TEXT PRIMARY KEY, text TEXT, kind TEXT)');
324
- const ins = db.prepare('INSERT INTO r (id, text, kind) VALUES (?, ?, ?)');
325
- for (const r of rows) ins.run(r.id, r.text, r.kind);
326
- db.exec('PRAGMA wal_checkpoint(TRUNCATE)');
327
- const dbBytes = fs.statSync(dbPath).size;
328
- const ratio = jsonBytes > 0 ? (dbBytes / jsonBytes).toFixed(2) : 'n/a';
329
- return cell('tested', `${FOOTPRINT_ROWS} rows: sqlite=${dbBytes}B json=${jsonBytes}B ratio=${ratio}`);
330
- });
331
- } catch (error) {
332
- return cell('failed', `footprint threw: ${error?.message ?? error}`);
333
- }
334
- }
335
-
336
- function probeLicense() {
337
- return cell('tested', 'SQLite is public domain; node:sqlite ships with Node — no new runtime dependency, no license obligation');
338
- }
339
-
340
- // ---------- report ----------
341
-
342
- function renderMarkdown(report) {
343
- const lines = [];
344
- lines.push('# SQLite viability spike report');
345
- lines.push('');
346
- lines.push(`- sha: \`${report.env.sha}\` · package: \`${report.env.package}\``);
347
- lines.push(`- node: \`${report.env.node}\` · os: \`${report.env.os}\` · arch: \`${report.env.arch}\``);
348
- lines.push(`- date: ${report.env.date} · wal-dir: \`${report.env.walDir}\``);
349
- lines.push('');
350
- lines.push('| cell | verdict | detail |');
351
- lines.push('|------|---------|--------|');
352
- for (const name of ALL_CELLS) {
353
- const c = report.cells[name] ?? { verdict: 'failed', detail: 'cell missing' };
354
- lines.push(`| ${name} | ${c.verdict} | ${String(c.detail).replaceAll('|', '\\|')} |`);
355
- }
356
- lines.push('');
357
- const s = report.summary;
358
- lines.push(`Summary: ${s.tested} tested · ${s.unsupported} unsupported · ${s.failed} failed (${s.total} cells)`);
359
- lines.push('');
360
- return lines.join('\n');
361
- }
362
-
363
- function summarize(cells) {
364
- const s = { tested: 0, unsupported: 0, failed: 0, total: ALL_CELLS.length };
365
- for (const name of ALL_CELLS) {
366
- const v = cells[name]?.verdict ?? 'failed';
367
- if (v === 'tested') s.tested += 1;
368
- else if (v === 'unsupported') s.unsupported += 1;
369
- else s.failed += 1;
370
- }
371
- return s;
372
- }
373
-
374
- // ---------- main ----------
375
-
376
- async function main() {
377
- const opts = parseArgs(process.argv.slice(2));
378
-
379
- const tmpRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'ukit-sqlite-spike-'));
380
- const walDir = opts.walDir ? path.resolve(opts.walDir) : tmpRoot;
381
- const createdInWalDir = [];
382
-
383
- const env = {
384
- sha: 'unknown',
385
- package: 'unknown',
386
- node: process.version,
387
- os: process.platform,
388
- arch: process.arch,
389
- date: new Date().toISOString(),
390
- fs: opts.walDir ? 'user-provided (--wal-dir)' : 'local (mkdtemp)',
391
- walDir,
392
- };
393
- try {
394
- env.sha = execSync('git rev-parse HEAD', { cwd: REPO_ROOT, encoding: 'utf8', timeout: 10_000 }).trim();
395
- } catch { /* leave 'unknown' */ }
396
- try {
397
- env.package = JSON.parse(fs.readFileSync(path.join(REPO_ROOT, 'package.json'), 'utf8')).version ?? 'unknown';
398
- } catch { /* leave 'unknown' */ }
399
-
400
- const cells = {};
401
- try {
402
- cells['node-sqlite-availability'] = await probeAvailability();
403
-
404
- if (cells['node-sqlite-availability'].verdict === 'unsupported') {
405
- for (const name of ALL_CELLS) {
406
- if (!cells[name]) cells[name] = cell('unsupported', 'node:sqlite unavailable — probe not runnable');
407
- }
408
- } else {
409
- cells['open-create'] = await probeOpenCreate(tmpRoot);
410
- cells['wal-mode'] = await probeWalMode(walDir);
411
- cells['busy-timeout'] = await probeBusyTimeout(walDir);
412
- cells['concurrent-writers'] = await probeConcurrentWriters(tmpRoot);
413
- cells.backup = await probeBackup(tmpRoot);
414
- cells['migration-roundtrip'] = await probeMigration(tmpRoot);
415
- cells.footprint = await probeFootprint(tmpRoot);
416
- cells.license = probeLicense();
417
- }
418
- } finally {
419
- // FR-010 cleanup: remove mkdtemp tree and any spike files left in --wal-dir.
420
- try { fs.rmSync(tmpRoot, { recursive: true, force: true }); } catch { /* best effort */ }
421
- if (opts.walDir) {
422
- try {
423
- for (const entry of fs.readdirSync(walDir)) {
424
- if (entry.startsWith(SPIKE_PREFIX)) {
425
- createdInWalDir.push(entry);
426
- fs.rmSync(path.join(walDir, entry), { recursive: true, force: true });
427
- }
428
- }
429
- } catch { /* unreadable wal-dir is already recorded as cell data */ }
430
- }
431
- }
432
-
433
- const report = { env, cells, summary: summarize(cells) };
434
-
435
- const outPath = path.resolve(opts.out);
436
- fs.mkdirSync(path.dirname(outPath), { recursive: true });
437
- fs.writeFileSync(outPath, `${JSON.stringify(report, null, 2)}\n`);
438
- const mdPath = outPath.replace(/\.json$/i, '') + '.md';
439
- fs.writeFileSync(mdPath, renderMarkdown(report));
440
-
441
- const s = report.summary;
442
- console.log(`[sqlite-spike] report written: ${outPath}`);
443
- console.log(`[sqlite-spike] markdown: ${mdPath}`);
444
- console.log(`[sqlite-spike] cells=${s.total} tested=${s.tested} unsupported=${s.unsupported} failed=${s.failed}`);
445
- process.exit(0);
446
- }
447
-
448
- main().catch((error) => {
449
- console.error(`[sqlite-spike] harness crash: ${error?.stack ?? error}`);
450
- process.exit(2);
451
- });