@ngockhoale/ukit 3.4.46 → 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.
- package/CHANGELOG.md +13 -5
- package/manifests/documentation.yaml +22 -0
- package/manifests/platform.full.yaml +27 -0
- package/package.json +4 -1
- package/scripts/dev/capture-omp-stall.mjs +246 -0
- package/scripts/dev/worktree-preflight.mjs +20 -1
- package/scripts/docs/render-instructions.mjs +29 -5
- package/src/core/agentRuntime/supervisor.js +15 -1
- package/src/core/diffPlan.js +38 -12
- package/src/core/executionContracts.js +67 -16
- package/src/core/metadata.js +4 -0
- package/src/core/runInstallPipeline.js +19 -2
- package/src/core/runtimeConfig.js +1 -1
- package/src/diagnostics/ompStallSnapshot.js +199 -0
- package/src/index/playbookRegistry.js +22 -3
- package/src/index/taskRouting.js +2 -2
- package/template_project/.claude/agents/bug-debugger.md +1 -0
- package/template_project/.claude/agents/code-reviewer.md +1 -0
- package/template_project/.claude/agents/feature-implementer.md +1 -0
- package/template_project/.claude/agents/handoff-planner.md +1 -0
- package/template_project/.claude/agents/ukit-small-task-maintainer.md +1 -0
- package/template_project/.claude/hooks/completion-gate.sh +1 -1
- package/template_project/.claude/hooks/delegation-gate.mjs +343 -0
- package/template_project/.claude/hooks/skill-router.sh +147 -6
- package/template_project/.claude/settings.json +8 -8
- package/template_project/.claude/ukit/index/playbook-registry.mjs +22 -3
- package/template_project/.claude/ukit/index/route-resolver.mjs +67 -15
- package/template_project/.claude/ukit/index/route-task.mjs +115 -37
- package/template_project/.claude/ukit/index/tier-lane.mjs +202 -0
- package/template_project/.claude/ukit/runtime/cron-operation-adapter.mjs +3 -1
- package/template_project/.claude/ukit/runtime/execution-ledger.mjs +306 -20
- package/template_project/.claude/ukit/runtime/hook-chain-runner.mjs +8 -0
- package/template_project/.claude/ukit/runtime/loop-state.mjs +3 -1
- package/template_project/.claude/ukit/runtime/observability-emit.mjs +38 -0
- package/template_project/.claude/ukit/runtime/playbook-execution.mjs +704 -0
- package/template_project/.claude/ukit/runtime/stop-coordinator.mjs +80 -3
- package/template_project/.omp/agents/bug-debugger.md +1 -0
- package/template_project/.omp/agents/code-reviewer.md +1 -0
- package/template_project/.omp/agents/feature-implementer.md +1 -0
- package/template_project/.omp/agents/handoff-planner.md +1 -0
- package/template_project/.omp/agents/ukit-small-task-maintainer.md +1 -0
- package/template_project/.omp/hooks/pre/ukit-bridge.js +254 -7
- package/template_project/ukit/storage/config.json +1 -1
- package/scripts/bench/data-foundation.mjs +0 -929
- package/scripts/bench/decision-runtime-baseline.mjs +0 -427
- package/scripts/bench/decision-runtime-variant.mjs +0 -626
- package/scripts/bench/goldTasks.json +0 -38
- package/scripts/bench/memory-ablation.mjs +0 -495
- package/scripts/bench/memory-baseline.mjs +0 -596
- package/scripts/bench/memory-bench.mjs +0 -661
- package/scripts/bench/memory-canary.mjs +0 -331
- package/scripts/bench/memory-corpus.mjs +0 -354
- package/scripts/bench/memory-gate.mjs +0 -389
- package/scripts/bench/memory-metrics.mjs +0 -179
- package/scripts/bench/outline-savings.mjs +0 -107
- package/scripts/bench/parallel-agents.mjs +0 -224
- package/scripts/bench/recorder-overhead.mjs +0 -204
- package/scripts/bench/runGold.mjs +0 -238
- package/scripts/bench/sqlite-spike.mjs +0 -451
- package/scripts/bench/subagent-orchestrator-corpus.mjs +0 -275
- package/scripts/bench/subagent-orchestrator-eval.mjs +0 -565
- package/scripts/bench/v3-ceremony.mjs +0 -220
- package/scripts/perf/audit-perf.mjs +0 -1195
- package/scripts/perf/diff-perf-findings.mjs +0 -136
- package/scripts/perf/perf-findings.json +0 -474
- package/scripts/perf/perf-measure.md +0 -271
|
@@ -1,224 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
/**
|
|
3
|
-
* parallel-agents.mjs — TASK-004 harness.
|
|
4
|
-
*
|
|
5
|
-
* Measures the *machine-contention* component of N parallel agents by spawning N concurrent
|
|
6
|
-
* child processes (default `yarn test:release-core`) per level and recording wall-clock.
|
|
7
|
-
* This deliberately does NOT claim to benchmark LLM agent wall-clock (token counts, server
|
|
8
|
-
* load and retries make that irreproducible) — see TASK-004 planner note #1.
|
|
9
|
-
*
|
|
10
|
-
* Usage:
|
|
11
|
-
* node scripts/bench/parallel-agents.mjs [--levels 1,3,5,10] [--cmd "<test command>"] [--out <path>]
|
|
12
|
-
*/
|
|
13
|
-
|
|
14
|
-
import fs from 'node:fs';
|
|
15
|
-
import os from 'node:os';
|
|
16
|
-
import path from 'node:path';
|
|
17
|
-
import { spawn } from 'node:child_process';
|
|
18
|
-
import { fileURLToPath } from 'node:url';
|
|
19
|
-
|
|
20
|
-
const SLOWDOWN_LIMIT = 2.0;
|
|
21
|
-
|
|
22
|
-
// BUG-C22-21: a level must never wait forever — one wedged `shell:true` child
|
|
23
|
-
// used to leave `settled < n` permanently and hang the whole benchmark
|
|
24
|
-
// mid-level. A per-level deadline SIGKILLs unsettled children (their whole
|
|
25
|
-
// process group — detached:true puts each `sh -c` in its own group so
|
|
26
|
-
// grandchildren die too) and resolves the level marked timedOut.
|
|
27
|
-
// UKIT_BENCH_LEVEL_TIMEOUT_MS overrides the budget for tests.
|
|
28
|
-
const LEVEL_TIMEOUT_MS = Number(process.env.UKIT_BENCH_LEVEL_TIMEOUT_MS) || 30 * 60 * 1000; // 30 min
|
|
29
|
-
|
|
30
|
-
function parseArgs(argv) {
|
|
31
|
-
const opts = { levels: '1,3,5,10', cmd: 'yarn test:release-core', out: '.cache/bench/parallel-agents.json' };
|
|
32
|
-
for (let i = 0; i < argv.length; i += 1) {
|
|
33
|
-
const key = argv[i];
|
|
34
|
-
if (key !== '--levels' && key !== '--cmd' && key !== '--out') {
|
|
35
|
-
throw new Error(`unknown argument: ${key}`);
|
|
36
|
-
}
|
|
37
|
-
const val = argv[i + 1];
|
|
38
|
-
if (val === undefined || val.startsWith('--')) {
|
|
39
|
-
throw new Error(`missing value for ${key}`);
|
|
40
|
-
}
|
|
41
|
-
opts[key.slice(2)] = val;
|
|
42
|
-
i += 1;
|
|
43
|
-
}
|
|
44
|
-
return opts;
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
function parseLevels(raw) {
|
|
48
|
-
if (!/^\d+(,\d+)*$/.test(raw.trim())) {
|
|
49
|
-
throw new Error(`unparseable --levels value: "${raw}" (expected comma-separated positive integers, e.g. 1,3,5,10)`);
|
|
50
|
-
}
|
|
51
|
-
const levels = raw.split(',').map((s) => parseInt(s, 10));
|
|
52
|
-
if (levels.some((n) => n < 1)) throw new Error(`--levels values must be >= 1, got: ${raw}`);
|
|
53
|
-
return levels;
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
function runLevel(n, cmd) {
|
|
57
|
-
return new Promise((resolve) => {
|
|
58
|
-
const t0 = Date.now();
|
|
59
|
-
let live = 0;
|
|
60
|
-
let highWater = 0;
|
|
61
|
-
let failures = 0;
|
|
62
|
-
let settled = 0;
|
|
63
|
-
let timedOut = false;
|
|
64
|
-
let finished = false;
|
|
65
|
-
const children = [];
|
|
66
|
-
// BUG-C22-21: per-level deadline — kill stragglers and resolve marked-failed.
|
|
67
|
-
const deadline = setTimeout(() => {
|
|
68
|
-
timedOut = true;
|
|
69
|
-
for (const child of children) {
|
|
70
|
-
if (!child.ukitSettled) {
|
|
71
|
-
child.ukitSettled = true;
|
|
72
|
-
settled += 1;
|
|
73
|
-
failures += 1;
|
|
74
|
-
try {
|
|
75
|
-
process.kill(-child.pid, 'SIGKILL'); // kill the whole process tree
|
|
76
|
-
} catch {
|
|
77
|
-
try { child.kill('SIGKILL'); } catch { /* already gone */ }
|
|
78
|
-
}
|
|
79
|
-
}
|
|
80
|
-
}
|
|
81
|
-
maybeFinish();
|
|
82
|
-
}, LEVEL_TIMEOUT_MS);
|
|
83
|
-
deadline.unref();
|
|
84
|
-
for (let i = 0; i < n; i += 1) {
|
|
85
|
-
const child = spawn(cmd, { shell: true, stdio: 'ignore', detached: true });
|
|
86
|
-
live += 1;
|
|
87
|
-
highWater = Math.max(highWater, live);
|
|
88
|
-
children.push(child);
|
|
89
|
-
child.on('error', () => {
|
|
90
|
-
if (child.ukitSettled) return;
|
|
91
|
-
child.ukitSettled = true;
|
|
92
|
-
live -= 1;
|
|
93
|
-
settled += 1;
|
|
94
|
-
failures += 1;
|
|
95
|
-
maybeFinish();
|
|
96
|
-
});
|
|
97
|
-
child.on('exit', (code) => {
|
|
98
|
-
if (child.ukitSettled) return;
|
|
99
|
-
child.ukitSettled = true;
|
|
100
|
-
live -= 1;
|
|
101
|
-
settled += 1;
|
|
102
|
-
if (code !== 0) failures += 1;
|
|
103
|
-
maybeFinish();
|
|
104
|
-
});
|
|
105
|
-
}
|
|
106
|
-
function maybeFinish() {
|
|
107
|
-
if (finished) return;
|
|
108
|
-
if (settled === n) {
|
|
109
|
-
finished = true;
|
|
110
|
-
clearTimeout(deadline);
|
|
111
|
-
if (timedOut) {
|
|
112
|
-
console.error(`[bench] level n=${n}: deadline ${LEVEL_TIMEOUT_MS}ms exceeded — unsettled children killed and counted as failures`);
|
|
113
|
-
}
|
|
114
|
-
resolve({ n, wallClockMs: Date.now() - t0, failures, concurrencyHighWaterMark: highWater, timedOut });
|
|
115
|
-
}
|
|
116
|
-
}
|
|
117
|
-
});
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
function fail(msg) {
|
|
121
|
-
process.stderr.write(`${msg}\n`);
|
|
122
|
-
process.exit(1);
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
// BUG-C27-24: `wallClockMs === 0` (a sub-ms --cmd) made the base `base` 0 →
|
|
126
|
-
// every slowdownFactor NaN → `eligible` empty → `eligible.reduce(..., eligible[0])`
|
|
127
|
-
// dereffed undefined and threw a cryptic TypeError AFTER the whole benchmark ran.
|
|
128
|
-
// `wallClockMs === 0` is below Date.now() resolution for a spawned child, so it is
|
|
129
|
-
// marked unmeasurable and excluded; an empty `eligible` yields recommended=null so
|
|
130
|
-
// main() fails with a clear message, and the reduce is seeded with rows[0] so it can
|
|
131
|
-
// never deref undefined.
|
|
132
|
-
export function computeRecommendation(rows) {
|
|
133
|
-
const base = rows[0].wallClockMs;
|
|
134
|
-
for (const row of rows) {
|
|
135
|
-
row.unmeasurable = row.wallClockMs === 0 || base === 0;
|
|
136
|
-
row.slowdownFactor = base === 0 ? NaN : row.wallClockMs / base;
|
|
137
|
-
row.perRunCost = row.wallClockMs / row.n;
|
|
138
|
-
}
|
|
139
|
-
const eligible = rows.filter((r) => !r.unmeasurable && r.slowdownFactor <= SLOWDOWN_LIMIT);
|
|
140
|
-
const recommended = eligible.length === 0
|
|
141
|
-
? null
|
|
142
|
-
: eligible.reduce((best, r) => (r.perRunCost < best.perRunCost ? r : best), rows[0]).n;
|
|
143
|
-
return { recommended, eligible };
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
async function main() {
|
|
148
|
-
const opts = (() => {
|
|
149
|
-
try {
|
|
150
|
-
return parseArgs(process.argv.slice(2));
|
|
151
|
-
} catch (err) {
|
|
152
|
-
fail(err.message);
|
|
153
|
-
}
|
|
154
|
-
})();
|
|
155
|
-
let levels;
|
|
156
|
-
try {
|
|
157
|
-
levels = parseLevels(opts.levels);
|
|
158
|
-
} catch (err) {
|
|
159
|
-
fail(err.message);
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
const cpuCount = os.cpus().length;
|
|
163
|
-
const loadAvgStart = os.loadavg();
|
|
164
|
-
const startedAt = new Date().toISOString();
|
|
165
|
-
|
|
166
|
-
const rows = [];
|
|
167
|
-
for (const n of levels) {
|
|
168
|
-
// eslint-disable-next-line no-await-in-loop -- levels are measured sequentially on purpose
|
|
169
|
-
rows.push(await runLevel(n, opts.cmd));
|
|
170
|
-
}
|
|
171
|
-
const { recommended } = computeRecommendation(rows);
|
|
172
|
-
if (recommended === null) {
|
|
173
|
-
fail('no level met slowdown limit (base wall clock 0ms?) — benchmark is unmeasurable; rerun with a slower --cmd');
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
const loadAvgEnd = os.loadavg();
|
|
177
|
-
const finishedAt = new Date().toISOString();
|
|
178
|
-
|
|
179
|
-
const result = {
|
|
180
|
-
cpuCount,
|
|
181
|
-
levels: rows.map(({ n, wallClockMs, slowdownFactor, perRunCost, failures, concurrencyHighWaterMark, timedOut, unmeasurable }) => ({
|
|
182
|
-
n, wallClockMs, slowdownFactor, perRunCost, failures, concurrencyHighWaterMark, timedOut, unmeasurable,
|
|
183
|
-
})),
|
|
184
|
-
recommended,
|
|
185
|
-
measurementConditions: { loadAvgStart, loadAvgEnd, startedAt, finishedAt, cpuCount },
|
|
186
|
-
};
|
|
187
|
-
|
|
188
|
-
// Print the human table first so a failed --out write still shows the numbers.
|
|
189
|
-
const header = 'n'.padStart(4) + ' ' + 'wallClockMs'.padStart(12) + ' ' + 'slowdownFactor'.padStart(15) + ' ' + 'perRunCost'.padStart(11) + ' ' + 'failures'.padStart(8) + ' ' + 'concurrencyHighWaterMark'.padStart(23);
|
|
190
|
-
console.log(header);
|
|
191
|
-
for (const r of result.levels) {
|
|
192
|
-
console.log(
|
|
193
|
-
String(r.n).padStart(4)
|
|
194
|
-
+ ' ' + String(r.wallClockMs).padStart(12)
|
|
195
|
-
+ ' ' + (r.unmeasurable ? 'unmeasurable'.padStart(15) : r.slowdownFactor.toFixed(2).padStart(15))
|
|
196
|
-
+ ' ' + r.perRunCost.toFixed(1).padStart(11)
|
|
197
|
-
+ ' ' + String(r.failures).padStart(8)
|
|
198
|
-
+ ' ' + String(r.concurrencyHighWaterMark).padStart(23),
|
|
199
|
-
);
|
|
200
|
-
}
|
|
201
|
-
console.log(`recommended: ${recommended} (best perRunCost with slowdownFactor <= ${SLOWDOWN_LIMIT}) on ${cpuCount} CPUs`);
|
|
202
|
-
console.log(`loadAvgStart: [${loadAvgStart.map((v) => v.toFixed(2)).join(', ')}] loadAvgEnd: [${loadAvgEnd.map((v) => v.toFixed(2)).join(', ')}]`);
|
|
203
|
-
|
|
204
|
-
// Overwrite (never append), and never leave a partial file behind.
|
|
205
|
-
const outPath = path.resolve(opts.out);
|
|
206
|
-
try {
|
|
207
|
-
fs.mkdirSync(path.dirname(outPath), { recursive: true });
|
|
208
|
-
const body = JSON.stringify(result, null, 2) + '\n';
|
|
209
|
-
fs.writeFileSync(outPath, body);
|
|
210
|
-
} catch (err) {
|
|
211
|
-
fail(`cannot write --out ${outPath}: ${err.message}`);
|
|
212
|
-
}
|
|
213
|
-
|
|
214
|
-
const totalFailures = result.levels.reduce((s, r) => s + r.failures, 0);
|
|
215
|
-
if (totalFailures > 0) {
|
|
216
|
-
const perLevel = result.levels.filter((r) => r.failures > 0).map((r) => `n=${r.n}: ${r.failures}/${r.n} failed`).join('; ');
|
|
217
|
-
fail(`bench command failed: ${perLevel}`);
|
|
218
|
-
}
|
|
219
|
-
}
|
|
220
|
-
|
|
221
|
-
const isMain = process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url);
|
|
222
|
-
if (isMain) {
|
|
223
|
-
main().catch((err) => fail(err && err.message ? err.message : String(err)));
|
|
224
|
-
}
|
|
@@ -1,204 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
// recorder-overhead.mjs (TASK-001, SPEC §2 G2-FR03) — paired baseline vs
|
|
3
|
-
// instrumented overhead measurement for the observer seam. REAL measurement
|
|
4
|
-
// on this host; no fabricated targets.
|
|
5
|
-
//
|
|
6
|
-
// Workload: hook-telemetry-shaped events derived deterministically from the
|
|
7
|
-
// seeded corpus (tests/fixtures/observability/corpus.json, seed=42) — one
|
|
8
|
-
// event per corpus record, timings taken from the record's own duration so
|
|
9
|
-
// the fixture stays the single source of truth.
|
|
10
|
-
//
|
|
11
|
-
// baseline arm: appendFileSync(JSON.stringify(row)) to a jsonl file —
|
|
12
|
-
// the status-quo owner write path (hook-telemetry append).
|
|
13
|
-
// instrumented arm: adaptHookTelemetryEvent(row) → recorder.emit() → flush()
|
|
14
|
-
// per event into a real temp segment store — the seam a
|
|
15
|
-
// hook would run inside its deadline.
|
|
16
|
-
//
|
|
17
|
-
// Both arms repeat the same event corpus --reps times, interleaved per rep
|
|
18
|
-
// (baseline pass, then instrumented pass) so drift hits both arms equally.
|
|
19
|
-
//
|
|
20
|
-
// node scripts/bench/recorder-overhead.mjs [--json] [--reps N]
|
|
21
|
-
|
|
22
|
-
import fs from 'node:fs';
|
|
23
|
-
import os from 'node:os';
|
|
24
|
-
import path from 'node:path';
|
|
25
|
-
import { execSync } from 'node:child_process';
|
|
26
|
-
import { fileURLToPath } from 'node:url';
|
|
27
|
-
|
|
28
|
-
import { adaptHookTelemetryEvent } from '../../src/core/observability/adapters/hookTelemetryAdapter.js';
|
|
29
|
-
import { createAdapterContext } from '../../src/core/observability/adapters/common.js';
|
|
30
|
-
import { createRecorder } from '../../src/core/observability/emit/recorder.js';
|
|
31
|
-
|
|
32
|
-
const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '../..');
|
|
33
|
-
const corpusPath = path.join(repoRoot, 'tests/fixtures/observability/corpus.json');
|
|
34
|
-
|
|
35
|
-
const args = process.argv.slice(2);
|
|
36
|
-
const json = args.includes('--json');
|
|
37
|
-
const repsFlag = args.indexOf('--reps');
|
|
38
|
-
const REPS = repsFlag !== -1 ? Number.parseInt(args[repsFlag + 1], 10) : 30;
|
|
39
|
-
if (!Number.isInteger(REPS) || REPS <= 0) {
|
|
40
|
-
console.error('--reps must be a positive integer');
|
|
41
|
-
process.exit(2);
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
function percentile(sorted, p) {
|
|
45
|
-
if (sorted.length === 0) return null;
|
|
46
|
-
const idx = Math.min(sorted.length - 1, Math.ceil((p / 100) * sorted.length) - 1);
|
|
47
|
-
return sorted[Math.max(0, idx)];
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
function stats(samples) {
|
|
51
|
-
const sorted = [...samples].sort((a, b) => a - b);
|
|
52
|
-
const n = sorted.length;
|
|
53
|
-
const mean = n ? sorted.reduce((s, v) => s + v, 0) / n : 0;
|
|
54
|
-
const variance = n ? sorted.reduce((s, v) => s + (v - mean) ** 2, 0) / n : 0;
|
|
55
|
-
return {
|
|
56
|
-
n,
|
|
57
|
-
min: n ? sorted[0] : null,
|
|
58
|
-
max: n ? sorted[n - 1] : null,
|
|
59
|
-
mean: Number(mean.toFixed(4)),
|
|
60
|
-
stdev: Number(Math.sqrt(variance).toFixed(4)),
|
|
61
|
-
p50: percentile(sorted, 50),
|
|
62
|
-
p95: percentile(sorted, 95),
|
|
63
|
-
p99: percentile(sorted, 99),
|
|
64
|
-
};
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
function dirBytes(dir) {
|
|
68
|
-
let total = 0;
|
|
69
|
-
if (!fs.existsSync(dir)) return 0;
|
|
70
|
-
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
71
|
-
const p = path.join(dir, entry.name);
|
|
72
|
-
if (entry.isDirectory()) total += dirBytes(p);
|
|
73
|
-
else total += fs.statSync(p).size;
|
|
74
|
-
}
|
|
75
|
-
return total;
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
function gitRevision() {
|
|
79
|
-
try {
|
|
80
|
-
return execSync('git rev-parse --short HEAD', { cwd: repoRoot, encoding: 'utf8', timeout: 10_000 }).trim();
|
|
81
|
-
} catch {
|
|
82
|
-
return 'unknown';
|
|
83
|
-
}
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
// --- workload: corpus records → hook-telemetry-shaped rows ------------------
|
|
87
|
-
let corpus;
|
|
88
|
-
let sourceRecords;
|
|
89
|
-
try {
|
|
90
|
-
corpus = JSON.parse(fs.readFileSync(corpusPath, 'utf8'));
|
|
91
|
-
sourceRecords = corpus.traces.flatMap((t) => t.records);
|
|
92
|
-
} catch {
|
|
93
|
-
console.error(
|
|
94
|
-
`recorder-overhead: cannot load ${corpusPath} — run 'node scripts/bench/data-foundation.mjs --fixture' first`,
|
|
95
|
-
);
|
|
96
|
-
process.exit(1);
|
|
97
|
-
}
|
|
98
|
-
const events = sourceRecords.map((r, i) => ({
|
|
99
|
-
v: 1,
|
|
100
|
-
ts: Date.parse(r.wall_time_utc),
|
|
101
|
-
hookEvent: 'PostToolUse',
|
|
102
|
-
toolName: typeof r.payload?.tool_name === 'string' ? r.payload.tool_name : 'BenchTool',
|
|
103
|
-
toolUseId: `bench-${r.record_id ?? i}`,
|
|
104
|
-
hook: 'post-tool-use/bench-hook',
|
|
105
|
-
elapsedMs: Number.isFinite(r.payload?.duration_ms) ? r.payload.duration_ms : (i % 53),
|
|
106
|
-
outcome: 'ok',
|
|
107
|
-
...(i % 3 === 0 ? { stageMs: i % 7 } : {}),
|
|
108
|
-
}));
|
|
109
|
-
|
|
110
|
-
const tmpRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'ukit-bench-'));
|
|
111
|
-
const baselineFile = path.join(tmpRoot, 'baseline.jsonl');
|
|
112
|
-
const segmentRoot = path.join(tmpRoot, 'segments');
|
|
113
|
-
|
|
114
|
-
const baselineMs = [];
|
|
115
|
-
const emitMs = [];
|
|
116
|
-
const flushMs = [];
|
|
117
|
-
|
|
118
|
-
const heapBefore = process.memoryUsage().heapUsed;
|
|
119
|
-
let recordsWritten = 0;
|
|
120
|
-
let recordsEmitted = 0;
|
|
121
|
-
let lastHealth = null;
|
|
122
|
-
const wallStart = Date.now();
|
|
123
|
-
|
|
124
|
-
const recorder = createRecorder({
|
|
125
|
-
root: segmentRoot,
|
|
126
|
-
config: { observability: { stage: 'default' } },
|
|
127
|
-
});
|
|
128
|
-
|
|
129
|
-
try {
|
|
130
|
-
for (let rep = 0; rep < REPS; rep += 1) {
|
|
131
|
-
// baseline arm — status-quo owner append path
|
|
132
|
-
for (const event of events) {
|
|
133
|
-
const line = JSON.stringify(event) + '\n';
|
|
134
|
-
const t0 = process.hrtime.bigint();
|
|
135
|
-
fs.appendFileSync(baselineFile, line);
|
|
136
|
-
baselineMs.push(Number(process.hrtime.bigint() - t0) / 1e6);
|
|
137
|
-
}
|
|
138
|
-
// instrumented arm — adapt → emit → flush per event (hook-deadline seam)
|
|
139
|
-
const ctx = createAdapterContext({ writerId: 'adapter-hook-bench' });
|
|
140
|
-
for (const event of events) {
|
|
141
|
-
const t0 = process.hrtime.bigint();
|
|
142
|
-
const record = adaptHookTelemetryEvent(event, ctx);
|
|
143
|
-
if (record) recorder.emit(record);
|
|
144
|
-
emitMs.push(Number(process.hrtime.bigint() - t0) / 1e6);
|
|
145
|
-
|
|
146
|
-
const f0 = process.hrtime.bigint();
|
|
147
|
-
await recorder.flush({ deadlineMs: 100 });
|
|
148
|
-
flushMs.push(Number(process.hrtime.bigint() - f0) / 1e6);
|
|
149
|
-
}
|
|
150
|
-
const h = recorder.health();
|
|
151
|
-
lastHealth = h;
|
|
152
|
-
recordsEmitted += events.length;
|
|
153
|
-
}
|
|
154
|
-
} finally {
|
|
155
|
-
const heapAfter = process.memoryUsage().heapUsed;
|
|
156
|
-
const segmentBytes = dirBytes(segmentRoot);
|
|
157
|
-
recordsWritten = lastHealth ? lastHealth.written : 0;
|
|
158
|
-
|
|
159
|
-
const report = {
|
|
160
|
-
bench: 'recorder-overhead',
|
|
161
|
-
host: {
|
|
162
|
-
platform: os.platform(),
|
|
163
|
-
arch: os.arch(),
|
|
164
|
-
release: os.release(),
|
|
165
|
-
cpu: os.cpus()?.[0]?.model ?? 'unknown',
|
|
166
|
-
node: process.version,
|
|
167
|
-
},
|
|
168
|
-
revision: gitRevision(),
|
|
169
|
-
measured_at: new Date().toISOString(),
|
|
170
|
-
workload: {
|
|
171
|
-
corpus: path.relative(repoRoot, corpusPath),
|
|
172
|
-
seed: corpus.seed,
|
|
173
|
-
events_per_rep: events.length,
|
|
174
|
-
reps: REPS,
|
|
175
|
-
},
|
|
176
|
-
reps: REPS,
|
|
177
|
-
baseline: stats(baselineMs),
|
|
178
|
-
emit: stats(emitMs),
|
|
179
|
-
flush: stats(flushMs),
|
|
180
|
-
bytesPerRecord: recordsWritten > 0 ? Math.round(segmentBytes / recordsWritten) : null,
|
|
181
|
-
heapDeltaBytes: heapAfter - heapBefore,
|
|
182
|
-
wall_clock_ms: Date.now() - wallStart,
|
|
183
|
-
instrumented: {
|
|
184
|
-
events: recordsEmitted,
|
|
185
|
-
recordsWritten,
|
|
186
|
-
segmentBytes,
|
|
187
|
-
health: lastHealth,
|
|
188
|
-
},
|
|
189
|
-
baselineBytes: fs.existsSync(baselineFile) ? fs.statSync(baselineFile).size : 0,
|
|
190
|
-
};
|
|
191
|
-
|
|
192
|
-
if (json) {
|
|
193
|
-
process.stdout.write(JSON.stringify(report, null, 2) + '\n');
|
|
194
|
-
} else {
|
|
195
|
-
const f = (v) => (v === null ? 'null' : v.toFixed(3));
|
|
196
|
-
console.log(`reps=${report.reps} events/rep=${events.length} node=${report.host.node} rev=${report.revision}`);
|
|
197
|
-
console.log(`baseline append ms p50=${f(report.baseline.p50)} p95=${f(report.baseline.p95)} p99=${f(report.baseline.p99)}`);
|
|
198
|
-
console.log(`instrumented emit ms p50=${f(report.emit.p50)} p95=${f(report.emit.p95)} p99=${f(report.emit.p99)} stdev=${report.emit.stdev}`);
|
|
199
|
-
console.log(`instrumented flush ms p50=${f(report.flush.p50)} p95=${f(report.flush.p95)} p99=${f(report.flush.p99)}`);
|
|
200
|
-
console.log(`bytes/record=${report.bytesPerRecord} heapDelta=${report.heapDeltaBytes}B written=${recordsWritten}`);
|
|
201
|
-
}
|
|
202
|
-
|
|
203
|
-
fs.rmSync(tmpRoot, { recursive: true, force: true });
|
|
204
|
-
}
|
|
@@ -1,238 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
// Gold-task benchmark harness skeleton (SPEC §10).
|
|
3
|
-
// Loads a corpus of {id, prompt, expectedMode, expectedAnchors[], maxTokens}
|
|
4
|
-
// cases, runs routeTask + compileContext per case, prints a scorecard with a
|
|
5
|
-
// totals block, optionally writes an aggregated JSON scorecard (--json <path>),
|
|
6
|
-
// and exits 0 only when every case passes. CI-3xx gates on the JSON summary.
|
|
7
|
-
|
|
8
|
-
import fs from 'node:fs/promises';
|
|
9
|
-
import path from 'node:path';
|
|
10
|
-
import { fileURLToPath } from 'node:url';
|
|
11
|
-
|
|
12
|
-
const REPO_ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..', '..');
|
|
13
|
-
const DEFAULT_CORPUS = path.join(REPO_ROOT, 'scripts', 'bench', 'goldTasks.json');
|
|
14
|
-
|
|
15
|
-
const USAGE = 'usage: runGold.mjs [--corpus <path>] [--project <dir>] [--json <path>]';
|
|
16
|
-
|
|
17
|
-
function parseArgs(argv) {
|
|
18
|
-
let corpusPath = DEFAULT_CORPUS;
|
|
19
|
-
let projectRoot = REPO_ROOT;
|
|
20
|
-
let jsonPath = null;
|
|
21
|
-
const valueFor = (flag, i) => {
|
|
22
|
-
const next = argv[i + 1];
|
|
23
|
-
if (next === undefined || next.startsWith('--')) {
|
|
24
|
-
throw new Error(`missing value for ${flag}`);
|
|
25
|
-
}
|
|
26
|
-
return next;
|
|
27
|
-
};
|
|
28
|
-
for (let i = 0; i < argv.length; i += 1) {
|
|
29
|
-
if (argv[i] === '--corpus') {
|
|
30
|
-
corpusPath = path.resolve(valueFor('--corpus', i));
|
|
31
|
-
i += 1;
|
|
32
|
-
} else if (argv[i] === '--project') {
|
|
33
|
-
projectRoot = path.resolve(valueFor('--project', i));
|
|
34
|
-
i += 1;
|
|
35
|
-
} else if (argv[i] === '--json') {
|
|
36
|
-
jsonPath = path.resolve(valueFor('--json', i));
|
|
37
|
-
i += 1;
|
|
38
|
-
} else {
|
|
39
|
-
throw new Error(`unknown argument: ${argv[i]}`);
|
|
40
|
-
}
|
|
41
|
-
}
|
|
42
|
-
return { corpusPath, projectRoot, jsonPath };
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
async function loadCorpus(corpusPath) {
|
|
46
|
-
const raw = await fs.readFile(corpusPath, 'utf8');
|
|
47
|
-
const cases = JSON.parse(raw);
|
|
48
|
-
if (!Array.isArray(cases)) {
|
|
49
|
-
throw new Error(`bench corpus must be a JSON array: ${corpusPath}`);
|
|
50
|
-
}
|
|
51
|
-
return cases;
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
function anchorPathsOf(packet) {
|
|
55
|
-
const fromAnchors = (packet.anchors ?? []).map((a) => a.path).filter(Boolean);
|
|
56
|
-
const fromEvidence = (packet.evidence ?? []).map((e) => e.path).filter(Boolean);
|
|
57
|
-
return new Set([...fromAnchors, ...fromEvidence]);
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
function normalizeMode(mode) {
|
|
61
|
-
return typeof mode === 'string' && mode.trim() !== '' ? mode.trim() : null;
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
export async function runGoldCase(projectRoot, testCase, { compileContext, routeTask }) {
|
|
65
|
-
const failures = [];
|
|
66
|
-
const expectedMode = normalizeMode(testCase.expectedMode);
|
|
67
|
-
const expectedAnchors = Array.isArray(testCase.expectedAnchors) ? testCase.expectedAnchors : [];
|
|
68
|
-
const maxTokens = typeof testCase.maxTokens === 'number' ? testCase.maxTokens : Infinity;
|
|
69
|
-
|
|
70
|
-
const routed = routeTask({
|
|
71
|
-
prompt: testCase.prompt ?? '',
|
|
72
|
-
hasError: testCase.hasError === true,
|
|
73
|
-
filesHinted: Array.isArray(testCase.filesHinted) ? testCase.filesHinted : undefined,
|
|
74
|
-
flags: testCase.flags && typeof testCase.flags === 'object' ? testCase.flags : {},
|
|
75
|
-
});
|
|
76
|
-
|
|
77
|
-
const packet = await compileContext(projectRoot, {
|
|
78
|
-
prompt: testCase.prompt ?? '',
|
|
79
|
-
hasError: testCase.hasError === true,
|
|
80
|
-
filesHinted: Array.isArray(testCase.filesHinted) ? testCase.filesHinted : undefined,
|
|
81
|
-
flags: testCase.flags && typeof testCase.flags === 'object' ? testCase.flags : {},
|
|
82
|
-
});
|
|
83
|
-
|
|
84
|
-
if (expectedMode && packet.task_type !== expectedMode) {
|
|
85
|
-
failures.push(`mode: expected ${expectedMode}, got ${packet.task_type || '(empty)'}`);
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
const anchorPaths = anchorPathsOf(packet);
|
|
89
|
-
const anchorHits = [];
|
|
90
|
-
const anchorMisses = [];
|
|
91
|
-
for (const expected of expectedAnchors) {
|
|
92
|
-
if (anchorPaths.has(expected)) {
|
|
93
|
-
anchorHits.push(expected);
|
|
94
|
-
} else {
|
|
95
|
-
anchorMisses.push(expected);
|
|
96
|
-
failures.push(`anchor missing: ${expected}`);
|
|
97
|
-
}
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
if (packet.budget?.used > maxTokens) {
|
|
101
|
-
failures.push(`budget: used ${packet.budget.used} > maxTokens ${maxTokens}`);
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
return {
|
|
105
|
-
id: testCase.id ?? '(unnamed)',
|
|
106
|
-
mode: packet.task_type,
|
|
107
|
-
expectedMode,
|
|
108
|
-
anchors: [...anchorPaths],
|
|
109
|
-
expectedAnchors,
|
|
110
|
-
anchorHits,
|
|
111
|
-
anchorMisses,
|
|
112
|
-
budgetUsed: packet.budget?.used ?? 0,
|
|
113
|
-
maxTokens,
|
|
114
|
-
routedMode: routed.mode,
|
|
115
|
-
pass: failures.length === 0,
|
|
116
|
-
failures,
|
|
117
|
-
};
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
export async function runGold({ projectRoot, corpusPath } = {}) {
|
|
121
|
-
const rootDir = projectRoot ?? REPO_ROOT;
|
|
122
|
-
const { compileContext } = await import(path.join(REPO_ROOT, 'src/core/codeintel/compiler.js'));
|
|
123
|
-
const { routeTask } = await import(path.join(REPO_ROOT, 'src/core/codeintel/router.js'));
|
|
124
|
-
const { buildCodeIndex } = await import(path.join(REPO_ROOT, 'src/index/buildIndex.js'));
|
|
125
|
-
const { INDEX_ARTIFACTS, getArtifactPath } = await import(path.join(REPO_ROOT, 'src/index/paths.js'));
|
|
126
|
-
|
|
127
|
-
// The bench needs an index to score anchors; build once when missing.
|
|
128
|
-
try {
|
|
129
|
-
await fs.stat(getArtifactPath(rootDir, INDEX_ARTIFACTS.files));
|
|
130
|
-
} catch {
|
|
131
|
-
console.log('[bench] index missing — building code index first');
|
|
132
|
-
await buildCodeIndex({ rootDir });
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
const cases = await loadCorpus(corpusPath ?? DEFAULT_CORPUS);
|
|
136
|
-
const results = [];
|
|
137
|
-
for (const testCase of cases) {
|
|
138
|
-
results.push(await runGoldCase(rootDir, testCase, { compileContext, routeTask }));
|
|
139
|
-
}
|
|
140
|
-
return { results, pass: results.every((r) => r.pass) };
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
/**
|
|
144
|
-
* Aggregate per-case results into a machine-readable summary for CI gating.
|
|
145
|
-
* Pooled anchor precision = hits/found, recall = hits/expected; cases with
|
|
146
|
-
* empty expectedAnchors contribute 0 to both denominators (excluded, not 1).
|
|
147
|
-
* @param {Array<object>} results per-case runGoldCase outputs
|
|
148
|
-
* @returns {{total:number, passed:number, passRate:number, modeAccuracy:number|null, anchorPrecision:number|null, anchorRecall:number|null, meanBudgetUsedPct:number|null}}
|
|
149
|
-
*/
|
|
150
|
-
export function summarizeResults(results) {
|
|
151
|
-
const total = results.length;
|
|
152
|
-
const passed = results.filter((r) => r.pass).length;
|
|
153
|
-
const passRate = total === 0 ? 0 : passed / total;
|
|
154
|
-
|
|
155
|
-
const withMode = results.filter((r) => r.expectedMode);
|
|
156
|
-
const modeCorrect = withMode.filter((r) => r.mode === r.expectedMode).length;
|
|
157
|
-
const modeAccuracy = withMode.length === 0 ? null : modeCorrect / withMode.length;
|
|
158
|
-
|
|
159
|
-
let hits = 0;
|
|
160
|
-
let found = 0;
|
|
161
|
-
let expected = 0;
|
|
162
|
-
for (const r of results) {
|
|
163
|
-
hits += Array.isArray(r.anchorHits) ? r.anchorHits.length : 0;
|
|
164
|
-
found += Array.isArray(r.anchors) ? r.anchors.length : 0;
|
|
165
|
-
expected += Array.isArray(r.expectedAnchors) ? r.expectedAnchors.length : 0;
|
|
166
|
-
}
|
|
167
|
-
const anchorPrecision = found === 0 ? null : hits / found;
|
|
168
|
-
const anchorRecall = expected === 0 ? null : hits / expected;
|
|
169
|
-
|
|
170
|
-
const budgeted = results.filter((r) => typeof r.maxTokens === 'number' && r.maxTokens > 0);
|
|
171
|
-
const meanBudgetUsedPct = budgeted.length === 0
|
|
172
|
-
? null
|
|
173
|
-
: budgeted.reduce((acc, r) => acc + (r.budgetUsed ?? 0) / r.maxTokens, 0) / budgeted.length;
|
|
174
|
-
|
|
175
|
-
return { total, passed, passRate, modeAccuracy, anchorPrecision, anchorRecall, meanBudgetUsedPct };
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
/**
|
|
179
|
-
* Write { generatedAt, results, summary } scorecard JSON to `jsonPath` (pretty-printed).
|
|
180
|
-
* @param {string} jsonPath output file path
|
|
181
|
-
* @param {Array<object>} results per-case runGoldCase outputs
|
|
182
|
-
*/
|
|
183
|
-
export async function writeScorecardJson(jsonPath, results) {
|
|
184
|
-
const payload = {
|
|
185
|
-
generatedAt: new Date().toISOString(),
|
|
186
|
-
results,
|
|
187
|
-
summary: summarizeResults(results),
|
|
188
|
-
};
|
|
189
|
-
await fs.writeFile(jsonPath, `${JSON.stringify(payload, null, 2)}\n`, 'utf8');
|
|
190
|
-
}
|
|
191
|
-
|
|
192
|
-
function fmtPct(value) {
|
|
193
|
-
return value === null ? 'n/a' : `${(value * 100).toFixed(1)}%`;
|
|
194
|
-
}
|
|
195
|
-
|
|
196
|
-
export function printScorecard(results) {
|
|
197
|
-
console.log('id | mode | expected | anchors | budget | verdict');
|
|
198
|
-
console.log('---|------|----------|---------|--------|--------');
|
|
199
|
-
for (const r of results) {
|
|
200
|
-
console.log(
|
|
201
|
-
`${r.id} | ${r.mode || '-'} | ${r.expectedMode || '-'} | ${r.anchors.length} | ${r.budgetUsed}/${r.maxTokens === Infinity ? '∞' : r.maxTokens} | ${r.pass ? 'PASS' : 'FAIL'}`,
|
|
202
|
-
);
|
|
203
|
-
for (const failure of r.failures) {
|
|
204
|
-
console.log(` ! ${failure}`);
|
|
205
|
-
}
|
|
206
|
-
}
|
|
207
|
-
const s = summarizeResults(results);
|
|
208
|
-
console.log(`scorecard: ${s.passed}/${s.total} passed`);
|
|
209
|
-
console.log(
|
|
210
|
-
`totals: passRate: ${fmtPct(s.passRate)} | modeAccuracy: ${fmtPct(s.modeAccuracy)} | anchorPrecision: ${fmtPct(s.anchorPrecision)} | anchorRecall: ${fmtPct(s.anchorRecall)} | meanBudgetUsedPct: ${fmtPct(s.meanBudgetUsedPct)}`,
|
|
211
|
-
);
|
|
212
|
-
}
|
|
213
|
-
|
|
214
|
-
const isMain = process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url);
|
|
215
|
-
if (isMain) {
|
|
216
|
-
let parsed;
|
|
217
|
-
try {
|
|
218
|
-
parsed = parseArgs(process.argv.slice(2));
|
|
219
|
-
} catch (error) {
|
|
220
|
-
console.error(`${USAGE}\n${error?.message ?? error}`);
|
|
221
|
-
process.exit(2);
|
|
222
|
-
}
|
|
223
|
-
const { corpusPath, projectRoot, jsonPath } = parsed;
|
|
224
|
-
try {
|
|
225
|
-
const { results, pass } = await runGold({ projectRoot, corpusPath });
|
|
226
|
-
printScorecard(results);
|
|
227
|
-
if (jsonPath) {
|
|
228
|
-
await writeScorecardJson(jsonPath, results);
|
|
229
|
-
console.log(`[bench] scorecard JSON written: ${jsonPath}`);
|
|
230
|
-
}
|
|
231
|
-
if (!pass) {
|
|
232
|
-
process.exitCode = 1;
|
|
233
|
-
}
|
|
234
|
-
} catch (error) {
|
|
235
|
-
console.error(`[bench] ${error?.message ?? error}`);
|
|
236
|
-
process.exitCode = 1;
|
|
237
|
-
}
|
|
238
|
-
}
|