@ngockhoale/ukit 3.0.7 → 3.0.9

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 (105) hide show
  1. package/CHANGELOG.md +18 -1
  2. package/manifests/documentation.yaml +11 -0
  3. package/package.json +1 -1
  4. package/scripts/audit/decision-coverage.mjs +29 -2
  5. package/scripts/bench/data-foundation.mjs +52 -3
  6. package/scripts/bench/decision-runtime-baseline.mjs +427 -0
  7. package/scripts/bench/decision-runtime-metrics.mjs +67 -0
  8. package/scripts/bench/decision-runtime-variant.mjs +626 -0
  9. package/scripts/bench/memory-ablation.mjs +495 -0
  10. package/scripts/bench/memory-baseline.mjs +596 -0
  11. package/scripts/bench/memory-bench.mjs +661 -0
  12. package/scripts/bench/memory-canary.mjs +321 -0
  13. package/scripts/bench/memory-corpus.mjs +354 -0
  14. package/scripts/bench/memory-gate.mjs +389 -0
  15. package/scripts/bench/memory-metrics.mjs +179 -0
  16. package/scripts/bench/parallel-agents.mjs +33 -11
  17. package/scripts/bench/recorder-overhead.mjs +204 -0
  18. package/scripts/bench/sqlite-spike.mjs +451 -0
  19. package/scripts/measure-decision-gateway.mjs +306 -0
  20. package/scripts/perf/audit-perf.mjs +35 -17
  21. package/src/bug/triageBug.js +4 -3
  22. package/src/cli/commands/memory.js +357 -63
  23. package/src/context/detectProjectContext.js +11 -1
  24. package/src/core/agentRuntime/adapters.js +254 -0
  25. package/src/core/agentRuntime/artifacts.js +192 -0
  26. package/src/core/agentRuntime/completionGate.js +176 -0
  27. package/src/core/agentRuntime/context.js +149 -0
  28. package/src/core/agentRuntime/contract.js +247 -0
  29. package/src/core/agentRuntime/diagnostics.js +244 -0
  30. package/src/core/agentRuntime/evaluation.js +163 -0
  31. package/src/core/agentRuntime/eventStore.js +404 -0
  32. package/src/core/agentRuntime/liveness.js +60 -0
  33. package/src/core/agentRuntime/planCompiler.js +322 -0
  34. package/src/core/agentRuntime/promotion.js +53 -0
  35. package/src/core/agentRuntime/qualityComparison.js +112 -0
  36. package/src/core/agentRuntime/recovery.js +266 -0
  37. package/src/core/agentRuntime/resourcePolicy.js +78 -0
  38. package/src/core/agentRuntime/runtimeSupport.js +237 -0
  39. package/src/core/agentRuntime/supervisor.js +565 -0
  40. package/src/core/agentRuntime/vmEngine.js +621 -0
  41. package/src/core/codeintel/analogy.js +3 -2
  42. package/src/core/experiments/dynamicWorkflow.js +17 -2
  43. package/src/core/fileOps.js +21 -3
  44. package/src/core/memory/deltaOverlays.js +75 -30
  45. package/src/core/memory/learningCandidates.js +93 -48
  46. package/src/core/memory/memoryFlags.js +83 -0
  47. package/src/core/memory/memoryFreshness.js +190 -0
  48. package/src/core/memory/memoryHit.js +144 -0
  49. package/src/core/memory/migrate.js +69 -189
  50. package/src/core/memory/migrateMapping.js +232 -0
  51. package/src/core/memory/mutateMemory.js +323 -0
  52. package/src/core/memory/policy.js +96 -0
  53. package/src/core/memory/projectIdentity.js +266 -0
  54. package/src/core/memory/recordIndex.js +178 -0
  55. package/src/core/memory/recordStore.js +133 -20
  56. package/src/core/memory/records.js +144 -6
  57. package/src/core/memory/retrieval.js +259 -125
  58. package/src/core/memory/store.js +16 -5
  59. package/src/core/memory/storeBackup.js +226 -0
  60. package/src/core/memory/storeV2.js +63 -26
  61. package/src/core/memory/storeV2Loader.js +30 -12
  62. package/src/core/memory/userMemory.js +38 -20
  63. package/src/core/memory/writeClassification.js +161 -0
  64. package/src/core/memory/writeGuard.js +129 -0
  65. package/src/core/observability/adapters/hookTelemetryAdapter.js +90 -0
  66. package/src/core/observability/analytics/cohorts.js +148 -0
  67. package/src/core/observability/analytics/storeDigest.js +163 -0
  68. package/src/core/observability/evaluation/experimentPlan.js +95 -0
  69. package/src/core/observability/evaluation/findings.js +99 -0
  70. package/src/core/observability/evaluation/optimizationKnowledge.js +10 -1
  71. package/src/core/observability/evaluation/perturbation.js +273 -0
  72. package/src/core/observability/evaluation/replay.js +7 -1
  73. package/src/core/observability/evaluation/scorecard.js +23 -3
  74. package/src/core/observability/rollout.js +11 -7
  75. package/src/core/observability/schema/compatibility.js +135 -0
  76. package/src/core/observability/schema/registry.js +99 -0
  77. package/src/core/observability/schema/validate.js +7 -0
  78. package/src/core/observability/support/import.js +53 -9
  79. package/src/core/observability/support/paths.js +13 -3
  80. package/src/core/observability/support/projector.js +148 -12
  81. package/src/core/output/index.js +12 -2
  82. package/src/core/runtimeConfig.js +83 -0
  83. package/src/core/runtimePaths.js +3 -0
  84. package/src/core/sensitiveValueScanner.js +40 -0
  85. package/src/core/token/index.js +40 -3
  86. package/src/decision/client.js +37 -13
  87. package/src/decision/protocol.js +1 -1
  88. package/src/decision/registry.js +5 -3
  89. package/src/decision/runtimeDecide.js +242 -0
  90. package/src/decision/runtimeFilter.js +150 -0
  91. package/src/decision/runtimeScheduler.js +239 -0
  92. package/src/index/buildIndex.js +13 -12
  93. package/src/index/queryIndex.js +35 -14
  94. package/src/index/relatedTests.js +50 -8
  95. package/src/index/resolveContext.js +9 -4
  96. package/src/manifest/selectItems.js +7 -3
  97. package/src/render/instructionRenderer.js +17 -5
  98. package/template_project/.claude/ukit/index/lib/index-core.mjs +94 -39
  99. package/template_project/.claude/ukit/index/route-task.mjs +121 -19
  100. package/template_project/.claude/ukit/index/unic-decision.mjs +28 -13
  101. package/template_project/.claude/ukit/runtime/memory-flags.mjs +51 -0
  102. package/template_project/.claude/ukit/runtime/memory-freshness.mjs +155 -0
  103. package/template_project/.claude/ukit/runtime/memory-policy.mjs +286 -0
  104. package/template_project/.claude/ukit/runtime/output-compression.mjs +3 -0
  105. package/template_project/.claude/ukit/runtime/reinject-context.mjs +145 -14
@@ -0,0 +1,596 @@
1
+ #!/usr/bin/env node
2
+ // TASK-003 — memory baseline harness (SPEC §5 FR-003).
3
+ //
4
+ // Runs the fixed baseline checklist against TEMP COPIES of the TASK-002 fixture
5
+ // corpus and emits baseline-report.json + baseline-report.md. The harness never
6
+ // touches the repo's own .ukit/ store: every probe runs with cwd/HOME pointed at
7
+ // a mkdtemp fixture copy.
8
+ //
9
+ // Exit codes: 0 = ran to completion (verdicts are data), 1 = usage/fixture
10
+ // error, 2 = harness crash. Per-probe timeout default 30s → verdict 'error'.
11
+ // Privacy (SPEC §7/§10): probe text and record bodies never reach the report —
12
+ // commands reference fixture ids and redact probe arguments as `<probe>`.
13
+
14
+ import { execSync, spawn } from 'node:child_process';
15
+ import crypto from 'node:crypto';
16
+ import fs from 'node:fs';
17
+ import os from 'node:os';
18
+ import path from 'node:path';
19
+ import { createRequire } from 'node:module';
20
+ import { fileURLToPath } from 'node:url';
21
+
22
+ const REPO_ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '../..');
23
+ const NODE = process.execPath;
24
+ const UKIT_BIN = path.join(REPO_ROOT, 'bin/ukit');
25
+ const ROUTE_TASK = path.join(REPO_ROOT, 'template_project/.claude/ukit/index/route-task.mjs');
26
+ const REINJECT = path.join(REPO_ROOT, 'template_project/.claude/ukit/runtime/reinject-context.mjs');
27
+
28
+ const DEFAULT_RUNS = 9;
29
+ const DEFAULT_TIMEOUT_MS = 30_000;
30
+ const VITEST_TIMEOUT_MS = 600_000;
31
+ const BURST_PROCESSES = 4;
32
+ const BURST_WRITES_PER_PROCESS = 10;
33
+
34
+ // SPEC §5 FR-003 checklist item 1 — the memory-scoped vitest set.
35
+ const MEMORY_TEST_SET = [
36
+ 'tests/core/memoryV2.test.js',
37
+ 'tests/core/memoryRetrieval.test.js',
38
+ 'tests/core/memoryMigration.test.js',
39
+ 'tests/core/migrateLegacy.test.js',
40
+ 'tests/core/userMemory.test.js',
41
+ 'tests/core/deltaOverlays.test.js',
42
+ 'tests/core/learningCandidates.test.js',
43
+ 'tests/cli/memoryCommand.test.js',
44
+ 'tests/audit/memory.test.js',
45
+ 'tests/hooks/sessionEpisode.test.js',
46
+ 'tests/hooks/sessionEpisodeHook.test.js',
47
+ 'tests/index/routeTaskUserLayer.test.js',
48
+ ];
49
+
50
+ const ALL_PROBES = [
51
+ 'vitest-memory-set',
52
+ 'pack',
53
+ 'cli-search',
54
+ 'cli-recall',
55
+ 'route-task',
56
+ 'reinject-context',
57
+ 'write-burst',
58
+ 'corrupt-store',
59
+ ];
60
+
61
+ const USAGE = `Usage: node scripts/bench/memory-baseline.mjs --fixture-root <dir> --out <file.json> [options]
62
+
63
+ Runs the C55 W0 memory baseline checklist (SPEC §5 FR-003) against temp copies
64
+ of the fixture corpus and writes <file.json> plus a sibling .md report.
65
+
66
+ Options:
67
+ --fixture-root <dir> Fixture corpus root containing manifest.json (required)
68
+ --out <file.json> Report output path; <file>.md is written beside it (required)
69
+ --runs <n> Latency probe repetitions (default ${DEFAULT_RUNS})
70
+ --timeout <ms> Per-probe timeout (default ${DEFAULT_TIMEOUT_MS})
71
+ --skip <ids> Comma-separated probe ids to skip
72
+ --only <ids> Comma-separated probe ids to run (overrides --skip)
73
+ --help Show this help
74
+
75
+ Probe ids: ${ALL_PROBES.join(', ')}
76
+
77
+ Exit codes: 0 = completed (verdicts are data), 1 = usage/fixture error, 2 = crash.`;
78
+
79
+ // ---------- small helpers ----------
80
+
81
+ function parseArgs(argv) {
82
+ const opts = { runs: DEFAULT_RUNS, timeoutMs: DEFAULT_TIMEOUT_MS, skip: new Set(), only: null };
83
+ for (let i = 0; i < argv.length; i += 1) {
84
+ const arg = argv[i];
85
+ if (arg === '--help' || arg === '-h') return { help: true };
86
+ const takeValue = () => {
87
+ i += 1;
88
+ if (i >= argv.length) throw new Error(`Missing value for ${arg}`);
89
+ return argv[i];
90
+ };
91
+ if (arg === '--fixture-root') opts.fixtureRoot = takeValue();
92
+ else if (arg === '--out') opts.out = takeValue();
93
+ else if (arg === '--runs') opts.runs = Number(takeValue());
94
+ else if (arg === '--timeout') opts.timeoutMs = Number(takeValue());
95
+ else if (arg === '--skip') opts.skip = new Set(takeValue().split(',').map((s) => s.trim()).filter(Boolean));
96
+ else if (arg === '--only') opts.only = new Set(takeValue().split(',').map((s) => s.trim()).filter(Boolean));
97
+ else throw new Error(`Unknown argument: ${arg}`);
98
+ }
99
+ return opts;
100
+ }
101
+
102
+ function sha256File(filePath) {
103
+ return crypto.createHash('sha256').update(fs.readFileSync(filePath)).digest('hex');
104
+ }
105
+
106
+ function percentile(sorted, q) {
107
+ if (sorted.length === 0) return null;
108
+ const rank = Math.max(1, Math.ceil((q / 100) * sorted.length));
109
+ return sorted[rank - 1];
110
+ }
111
+
112
+ function latencyStats(durations) {
113
+ const sorted = [...durations].sort((a, b) => a - b);
114
+ return {
115
+ runs: sorted.length,
116
+ p50: percentile(sorted, 50),
117
+ p95: percentile(sorted, 95),
118
+ p99: percentile(sorted, 99),
119
+ };
120
+ }
121
+
122
+ /**
123
+ * Spawn one probe command. `redactArgs` lists argv indices replaced by `<probe>`
124
+ * in the recorded cmd string so prompt text never reaches the report.
125
+ */
126
+ function runCommand({ id, argv, cwd, env, stdin, timeoutMs, redactArgs = [], measured = false }) {
127
+ const redact = new Set(redactArgs);
128
+ const cmd = argv.map((a, i) => (redact.has(i) ? '<probe>' : a)).join(' ');
129
+ return new Promise((resolve) => {
130
+ const started = Date.now();
131
+ let timedOut = false;
132
+ let spawnError = null;
133
+ let child;
134
+ try {
135
+ child = spawn(argv[0], argv.slice(1), {
136
+ cwd,
137
+ env: { ...process.env, ...env },
138
+ stdio: ['pipe', 'pipe', 'pipe'],
139
+ });
140
+ } catch (error) {
141
+ resolve({ id, cmd, exitCode: -1, durationMs: Date.now() - started, timedOut: false, verdict: 'error', note: String(error?.message ?? error) });
142
+ return;
143
+ }
144
+ const timer = setTimeout(() => {
145
+ timedOut = true;
146
+ child.kill('SIGKILL');
147
+ }, timeoutMs);
148
+ child.stdout.resume();
149
+ child.stderr.resume();
150
+ child.on('error', (error) => { spawnError = error; });
151
+ if (stdin != null) child.stdin.write(stdin);
152
+ child.stdin.end();
153
+ child.on('close', (code) => {
154
+ clearTimeout(timer);
155
+ const durationMs = Date.now() - started;
156
+ let verdict;
157
+ if (timedOut || spawnError) verdict = 'error';
158
+ else if (measured) verdict = code === 0 ? 'measured' : 'fail';
159
+ else verdict = code === 0 ? 'pass' : 'fail';
160
+ const entry = { id, cmd, exitCode: timedOut || code == null ? -1 : code, durationMs, verdict };
161
+ if (spawnError) entry.note = String(spawnError.message ?? spawnError);
162
+ resolve(entry);
163
+ });
164
+ });
165
+ }
166
+
167
+ // ---------- fixture helpers ----------
168
+
169
+ function loadManifest(fixtureRoot) {
170
+ const manifestPath = path.join(fixtureRoot, 'manifest.json');
171
+ if (!fs.existsSync(manifestPath)) {
172
+ throw new Error(`fixture manifest not found: ${manifestPath}`);
173
+ }
174
+ const manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf8'));
175
+ if (!Array.isArray(manifest.fixtures)) {
176
+ throw new Error(`fixture manifest has no fixtures[] array: ${manifestPath}`);
177
+ }
178
+ for (const fixture of manifest.fixtures) {
179
+ const abs = path.join(fixtureRoot, fixture.path);
180
+ if (!fs.existsSync(abs)) {
181
+ throw new Error(`fixture path missing on disk: ${fixture.path} (${fixture.id})`);
182
+ }
183
+ if (fixture.sha256 && sha256File(abs) !== fixture.sha256) {
184
+ throw new Error(`fixture sha256 mismatch: ${fixture.path} (${fixture.id})`);
185
+ }
186
+ }
187
+ return manifest;
188
+ }
189
+
190
+ function fixtureById(manifest, id) {
191
+ const fixture = manifest.fixtures.find((f) => f.id === id);
192
+ if (!fixture) throw new Error(`fixture id not in manifest: ${id}`);
193
+ return fixture;
194
+ }
195
+
196
+ /** Repo dir = fixture path up to (not including) the `.ukit` segment. */
197
+ function fixtureRepoDir(fixtureRoot, fixture) {
198
+ const segments = fixture.path.split('/');
199
+ const cut = segments.indexOf('.ukit');
200
+ if (cut < 0) throw new Error(`fixture path has no .ukit segment: ${fixture.path}`);
201
+ return path.join(fixtureRoot, ...segments.slice(0, cut));
202
+ }
203
+
204
+ function copyDir(src, dst) {
205
+ fs.cpSync(src, dst, { recursive: true });
206
+ }
207
+
208
+ /** First few words of an active record's text — used as probe input, never reported. */
209
+ function deriveProbeText(repoDir) {
210
+ const recordsPath = path.join(repoDir, '.ukit/storage/memory/v2/records.json');
211
+ try {
212
+ const doc = JSON.parse(fs.readFileSync(recordsPath, 'utf8'));
213
+ const record = (doc.records ?? []).find((r) => r.status === 'active' && typeof r.text === 'string')
214
+ ?? (doc.records ?? []).find((r) => typeof r.text === 'string');
215
+ if (record) return record.text.split(/\s+/).slice(0, 4).join(' ');
216
+ } catch { /* fall through to generic probe */ }
217
+ return 'memory baseline probe';
218
+ }
219
+
220
+ // ---------- probes ----------
221
+
222
+ async function probeVitest(ctx) {
223
+ let vitestBin;
224
+ try {
225
+ const require = createRequire(import.meta.url);
226
+ vitestBin = path.join(path.dirname(require.resolve('vitest/package.json')), 'vitest.mjs');
227
+ } catch (error) {
228
+ ctx.commands.push({
229
+ id: 'vitest-memory-set',
230
+ cmd: 'vitest run <memory-scoped set>',
231
+ exitCode: -1,
232
+ durationMs: 0,
233
+ verdict: 'error',
234
+ note: `vitest not resolvable: ${error?.message ?? error}`,
235
+ });
236
+ return;
237
+ }
238
+ const entry = await runCommand({
239
+ id: 'vitest-memory-set',
240
+ argv: [NODE, vitestBin, 'run', ...MEMORY_TEST_SET],
241
+ cwd: REPO_ROOT,
242
+ timeoutMs: VITEST_TIMEOUT_MS,
243
+ });
244
+ ctx.commands.push(entry);
245
+ }
246
+
247
+ async function probePack(ctx) {
248
+ const entry = await runCommand({
249
+ id: 'pack',
250
+ argv: ['npm', 'pack', '--dry-run', '--json'],
251
+ cwd: REPO_ROOT,
252
+ timeoutMs: ctx.timeoutMs,
253
+ });
254
+ ctx.commands.push(entry);
255
+ // Re-run capturing stdout for the stats (dry-run is side-effect free).
256
+ const captured = await new Promise((resolve) => {
257
+ const child = spawn('npm', ['pack', '--dry-run', '--json'], { cwd: REPO_ROOT, stdio: ['ignore', 'pipe', 'pipe'] });
258
+ let out = '';
259
+ child.stdout.on('data', (d) => { out += d; });
260
+ child.stderr.resume();
261
+ const timer = setTimeout(() => { child.kill('SIGKILL'); }, ctx.timeoutMs);
262
+ child.on('close', () => { clearTimeout(timer); resolve(out); });
263
+ child.on('error', () => { clearTimeout(timer); resolve(''); });
264
+ });
265
+ try {
266
+ const parsed = JSON.parse(captured);
267
+ const entry0 = Array.isArray(parsed) ? parsed[0] : parsed;
268
+ ctx.report.pack = {
269
+ fileCount: Number.isInteger(entry0?.totalFiles) ? entry0.totalFiles : (entry0?.files?.length ?? 0),
270
+ totalBytes: Number.isInteger(entry0?.unpackedSize)
271
+ ? entry0.unpackedSize
272
+ : (entry0?.files ?? []).reduce((sum, f) => sum + (f.size ?? 0), 0),
273
+ verdict: 'measured',
274
+ };
275
+ } catch {
276
+ ctx.report.pack = { fileCount: 0, totalBytes: 0, verdict: 'error' };
277
+ }
278
+ }
279
+
280
+ async function probeLatency(ctx, { id, argv, cwd, env, stdin, redactArgs }) {
281
+ const durations = [];
282
+ for (let run = 0; run < ctx.runs; run += 1) {
283
+ const entry = await runCommand({
284
+ id, argv, cwd, env, stdin, redactArgs,
285
+ timeoutMs: ctx.timeoutMs,
286
+ measured: true,
287
+ });
288
+ ctx.commands.push(entry);
289
+ if (entry.verdict === 'measured') durations.push(entry.durationMs);
290
+ }
291
+ ctx.report.latency[id] = latencyStats(durations);
292
+ }
293
+
294
+ async function probeWriteBurst(ctx) {
295
+ const fixture = fixtureById(ctx.manifest, 'fx-stale');
296
+ const repoDir = fixtureRepoDir(ctx.fixtureRoot, fixture);
297
+ // `memory --user add` writes the same v2 records.json document through the
298
+ // same mutateRecordStore/withFileLock path as the project store; HOME is
299
+ // redirected to a temp dir seeded with the fixture's .ukit state so the
300
+ // burst targets a temp copy of a fixture store, never the repo's own.
301
+ const home = fs.mkdtempSync(path.join(os.tmpdir(), 'ukit-burst-home-'));
302
+ copyDir(path.join(repoDir, '.ukit'), path.join(home, '.ukit'));
303
+
304
+ const expectedWrites = BURST_PROCESSES * BURST_WRITES_PER_PROCESS;
305
+ const started = Date.now();
306
+ let spawnFailures = 0;
307
+ const lanes = [];
308
+ for (let lane = 0; lane < BURST_PROCESSES; lane += 1) {
309
+ lanes.push((async () => {
310
+ for (let w = 0; w < BURST_WRITES_PER_PROCESS; w += 1) {
311
+ const marker = `burst-${lane}-${w}`;
312
+ const entry = await runCommand({
313
+ id: 'write-burst',
314
+ argv: [NODE, UKIT_BIN, 'memory', '--user', 'add', '--type', 'derived_fact', marker],
315
+ cwd: ctx.tmpRoot,
316
+ env: { HOME: home, UKIT_CLI_DEADLINE_MS: '25000' },
317
+ timeoutMs: ctx.timeoutMs,
318
+ redactArgs: [7],
319
+ measured: true,
320
+ });
321
+ if (entry.verdict !== 'measured') spawnFailures += 1;
322
+ }
323
+ })());
324
+ }
325
+ await Promise.all(lanes);
326
+ const durationMs = Date.now() - started;
327
+
328
+ let persistedWrites = 0;
329
+ let parseOk = true;
330
+ try {
331
+ const doc = JSON.parse(fs.readFileSync(path.join(home, '.ukit/storage/memory/v2/records.json'), 'utf8'));
332
+ persistedWrites = (doc.records ?? []).filter((r) => typeof r.text === 'string' && r.text.startsWith('burst-')).length;
333
+ } catch {
334
+ parseOk = false;
335
+ }
336
+ const lostUpdates = Math.max(0, expectedWrites - persistedWrites);
337
+ const verdict = !parseOk || spawnFailures === expectedWrites
338
+ ? 'error'
339
+ : lostUpdates > 0 ? 'reproduced' : 'not-reproduced';
340
+
341
+ ctx.commands.push({
342
+ id: 'write-burst',
343
+ cmd: `${NODE} bin/ukit memory --user add <probe> ×${expectedWrites} (${BURST_PROCESSES}×${BURST_WRITES_PER_PROCESS})`,
344
+ exitCode: spawnFailures === 0 ? 0 : 1,
345
+ durationMs,
346
+ verdict: 'measured',
347
+ });
348
+ ctx.report.writeBurst = {
349
+ processes: BURST_PROCESSES,
350
+ writesPerProcess: BURST_WRITES_PER_PROCESS,
351
+ expectedWrites,
352
+ persistedWrites,
353
+ lostUpdates,
354
+ spawnFailures,
355
+ verdict,
356
+ };
357
+ }
358
+
359
+ async function probeCorruptStore(ctx) {
360
+ const fixture = fixtureById(ctx.manifest, 'fx-corrupt-json');
361
+ const repoDir = fixtureRepoDir(ctx.fixtureRoot, fixture);
362
+ const tmpRepo = path.join(ctx.tmpRoot, 'corrupt-repo');
363
+ copyDir(repoDir, tmpRepo);
364
+ const recordsPath = path.join(tmpRepo, '.ukit/storage/memory/v2/records.json');
365
+ const before = sha256File(recordsPath);
366
+
367
+ const entry = await runCommand({
368
+ id: 'corrupt-store',
369
+ argv: [NODE, UKIT_BIN, 'memory', 'search', 'baseline-corrupt-probe'],
370
+ cwd: tmpRepo,
371
+ env: { UKIT_CLI_DEADLINE_MS: '25000' },
372
+ timeoutMs: ctx.timeoutMs,
373
+ redactArgs: [4],
374
+ measured: true,
375
+ });
376
+ ctx.commands.push(entry);
377
+
378
+ const after = sha256File(recordsPath);
379
+ const fileBytesUnchanged = before === after;
380
+ let wroteEmpty = false;
381
+ if (!fileBytesUnchanged) {
382
+ try {
383
+ const doc = JSON.parse(fs.readFileSync(recordsPath, 'utf8'));
384
+ wroteEmpty = Array.isArray(doc?.records) && doc.records.length === 0;
385
+ } catch { /* still unparseable — not an empty overwrite */ }
386
+ }
387
+ const readOk = entry.exitCode === 0 && !entry.timedOut;
388
+ ctx.report.corruptStore = {
389
+ readOk,
390
+ fileBytesUnchanged,
391
+ wroteEmpty,
392
+ verdict: !fileBytesUnchanged
393
+ ? (wroteEmpty ? 'corrupt-file-overwritten-empty' : 'corrupt-file-mutated')
394
+ : (readOk ? 'read-degrades-safely' : 'read-fails-but-bytes-unchanged'),
395
+ };
396
+ }
397
+
398
+ function renderMarkdown(report) {
399
+ const lines = [];
400
+ lines.push('# Memory baseline report');
401
+ lines.push('');
402
+ lines.push(`- sha: \`${report.meta.sha}\``);
403
+ lines.push(`- version: \`${report.meta.version}\``);
404
+ lines.push(`- node: \`${report.meta.node}\` · os: \`${report.meta.os}\` · arch: \`${report.meta.arch}\``);
405
+ lines.push(`- date: ${report.meta.date}`);
406
+ lines.push(`- fs: \`${report.meta.fs}\` — ${report.meta.fsNote}`);
407
+ if (report.skipped.length > 0) lines.push(`- skipped probes: ${report.skipped.join(', ')}`);
408
+ lines.push('');
409
+ lines.push('## Commands');
410
+ lines.push('');
411
+ lines.push('| id | cmd | exit | durationMs | verdict |');
412
+ lines.push('|----|-----|------|------------|---------|');
413
+ for (const c of report.commands) {
414
+ lines.push(`| ${c.id} | \`${c.cmd}\` | ${c.exitCode} | ${c.durationMs} | ${c.verdict} |`);
415
+ }
416
+ lines.push('');
417
+ lines.push('## Latency (ms)');
418
+ lines.push('');
419
+ lines.push('| probe | runs | p50 | p95 | p99 |');
420
+ lines.push('|-------|------|-----|-----|-----|');
421
+ for (const [id, s] of Object.entries(report.latency)) {
422
+ lines.push(`| ${id} | ${s.runs} | ${s.p50 ?? 'n/a'} | ${s.p95 ?? 'n/a'} | ${s.p99 ?? 'n/a'} |`);
423
+ }
424
+ lines.push('');
425
+ lines.push('## Write burst');
426
+ lines.push('');
427
+ const wb = report.writeBurst;
428
+ lines.push(`- ${wb.processes} processes × ${wb.writesPerProcess} writes = ${wb.expectedWrites} expected`);
429
+ lines.push(`- persisted: ${wb.persistedWrites} · lost updates: ${wb.lostUpdates} · spawn failures: ${wb.spawnFailures ?? 0}`);
430
+ lines.push(`- verdict: **${wb.verdict}**`);
431
+ lines.push('');
432
+ lines.push('## Corrupt store');
433
+ lines.push('');
434
+ const cs = report.corruptStore;
435
+ lines.push(`- readOk: ${cs.readOk} · fileBytesUnchanged: ${cs.fileBytesUnchanged} · wroteEmpty: ${cs.wroteEmpty}`);
436
+ lines.push(`- verdict: **${cs.verdict}**`);
437
+ lines.push('');
438
+ lines.push('## Pack');
439
+ lines.push('');
440
+ lines.push(`- fileCount: ${report.pack.fileCount} · totalBytes: ${report.pack.totalBytes} · verdict: ${report.pack.verdict}`);
441
+ lines.push('');
442
+ return lines.join('\n');
443
+ }
444
+
445
+ // ---------- main ----------
446
+
447
+ async function main() {
448
+ let opts;
449
+ try {
450
+ opts = parseArgs(process.argv.slice(2));
451
+ } catch (error) {
452
+ console.error(`[memory-baseline] ${error.message}\n\n${USAGE}`);
453
+ process.exit(1);
454
+ }
455
+ if (opts.help) {
456
+ console.log(USAGE);
457
+ process.exit(0);
458
+ }
459
+ if (!opts.fixtureRoot || !opts.out) {
460
+ console.error(`[memory-baseline] --fixture-root and --out are required.\n\n${USAGE}`);
461
+ process.exit(1);
462
+ }
463
+ if (!Number.isFinite(opts.runs) || opts.runs < 1) {
464
+ console.error('[memory-baseline] --runs must be a positive integer.');
465
+ process.exit(1);
466
+ }
467
+
468
+ const fixtureRoot = path.resolve(opts.fixtureRoot);
469
+ if (!fs.existsSync(fixtureRoot)) {
470
+ console.error(`[memory-baseline] fixture root not found: ${fixtureRoot}`);
471
+ process.exit(1);
472
+ }
473
+
474
+ let manifest;
475
+ try {
476
+ manifest = loadManifest(fixtureRoot);
477
+ } catch (error) {
478
+ console.error(`[memory-baseline] fixture error: ${error.message}`);
479
+ process.exit(1);
480
+ }
481
+
482
+ const selected = opts.only ?? new Set(ALL_PROBES.filter((id) => !opts.skip.has(id)));
483
+ const skipped = ALL_PROBES.filter((id) => !selected.has(id));
484
+
485
+ const tmpRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'ukit-baseline-'));
486
+
487
+ // Temp probe repo: copy of the fx-stale fixture repo + a minimal valid
488
+ // config so `memory recall` (which requires a valid runtime config) runs.
489
+ const staleFixture = fixtureById(manifest, 'fx-stale');
490
+ const probeRepo = path.join(tmpRoot, 'probe-repo');
491
+ copyDir(fixtureRepoDir(fixtureRoot, staleFixture), probeRepo);
492
+ fs.writeFileSync(path.join(probeRepo, '.ukit/storage/config.json'), '{}\n');
493
+ const probeText = deriveProbeText(probeRepo);
494
+
495
+ const report = {
496
+ schemaVersion: 1,
497
+ meta: {
498
+ sha: 'unknown',
499
+ version: 'unknown',
500
+ node: process.version,
501
+ os: process.platform,
502
+ arch: process.arch,
503
+ date: new Date().toISOString(),
504
+ fs: 'local',
505
+ fsNote: 'local filesystem only; SMB/network-FS write burst deferred (SPEC §14)',
506
+ },
507
+ commands: [],
508
+ latency: {},
509
+ writeBurst: {
510
+ processes: BURST_PROCESSES,
511
+ writesPerProcess: BURST_WRITES_PER_PROCESS,
512
+ expectedWrites: BURST_PROCESSES * BURST_WRITES_PER_PROCESS,
513
+ persistedWrites: 0,
514
+ lostUpdates: 0,
515
+ verdict: 'skipped',
516
+ },
517
+ corruptStore: { readOk: false, fileBytesUnchanged: false, wroteEmpty: false, verdict: 'skipped' },
518
+ pack: { fileCount: 0, totalBytes: 0, verdict: 'skipped' },
519
+ skipped,
520
+ };
521
+
522
+ try {
523
+ report.meta.sha = execSync('git rev-parse HEAD', { cwd: REPO_ROOT, encoding: 'utf8' }).trim();
524
+ } catch { /* leave 'unknown' */ }
525
+ try {
526
+ report.meta.version = JSON.parse(fs.readFileSync(path.join(REPO_ROOT, 'package.json'), 'utf8')).version ?? 'unknown';
527
+ } catch { /* leave 'unknown' */ }
528
+
529
+ const ctx = {
530
+ manifest,
531
+ fixtureRoot,
532
+ tmpRoot,
533
+ runs: opts.runs,
534
+ timeoutMs: opts.timeoutMs,
535
+ commands: report.commands,
536
+ report,
537
+ };
538
+
539
+ if (selected.has('vitest-memory-set')) await probeVitest(ctx);
540
+ if (selected.has('pack')) await probePack(ctx);
541
+
542
+ if (selected.has('cli-search')) {
543
+ await probeLatency(ctx, {
544
+ id: 'cli-search',
545
+ argv: [NODE, UKIT_BIN, 'memory', 'search', probeText],
546
+ cwd: probeRepo,
547
+ env: { UKIT_CLI_DEADLINE_MS: '25000' },
548
+ redactArgs: [4],
549
+ });
550
+ }
551
+ if (selected.has('cli-recall')) {
552
+ await probeLatency(ctx, {
553
+ id: 'cli-recall',
554
+ argv: [NODE, UKIT_BIN, 'memory', 'recall', probeText],
555
+ cwd: probeRepo,
556
+ env: { UKIT_CLI_DEADLINE_MS: '25000' },
557
+ redactArgs: [4],
558
+ });
559
+ }
560
+ if (selected.has('route-task')) {
561
+ await probeLatency(ctx, {
562
+ id: 'route-task',
563
+ argv: [NODE, ROUTE_TASK, 'baseline route probe: memory recall path', '--root', probeRepo],
564
+ cwd: tmpRoot,
565
+ redactArgs: [2],
566
+ });
567
+ }
568
+ if (selected.has('reinject-context')) {
569
+ await probeLatency(ctx, {
570
+ id: 'reinject-context',
571
+ argv: [NODE, REINJECT],
572
+ cwd: tmpRoot,
573
+ env: { CLAUDE_PROJECT_DIR: probeRepo },
574
+ stdin: JSON.stringify({ session_id: 'bench' }),
575
+ });
576
+ }
577
+ if (selected.has('write-burst')) await probeWriteBurst(ctx);
578
+ if (selected.has('corrupt-store')) await probeCorruptStore(ctx);
579
+
580
+ const outPath = path.resolve(opts.out);
581
+ fs.mkdirSync(path.dirname(outPath), { recursive: true });
582
+ fs.writeFileSync(outPath, `${JSON.stringify(report, null, 2)}\n`);
583
+ const mdPath = outPath.replace(/\.json$/i, '') + '.md';
584
+ fs.writeFileSync(mdPath, renderMarkdown(report));
585
+
586
+ console.log(`[memory-baseline] report written: ${outPath}`);
587
+ console.log(`[memory-baseline] markdown: ${mdPath}`);
588
+ console.log(`[memory-baseline] commands=${report.commands.length} skipped=[${skipped.join(',')}] `
589
+ + `writeBurst=${report.writeBurst.verdict} corruptStore=${report.corruptStore.verdict}`);
590
+ process.exit(0);
591
+ }
592
+
593
+ main().catch((error) => {
594
+ console.error(`[memory-baseline] harness crash: ${error?.stack ?? error}`);
595
+ process.exit(2);
596
+ });