homegraph 1.1.2 → 1.1.3
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/LICENSE +21 -21
- package/README.md +243 -243
- package/dist/bin/homegraph.js +371 -9
- package/dist/bin/homegraph.js.map +1 -1
- package/dist/db/migrations.js +18 -18
- package/dist/db/queries.js +140 -140
- package/dist/db/schema.sql +152 -152
- package/dist/directory.js +5 -5
- package/dist/extraction/wasm/tree-sitter-c_sharp.wasm +0 -0
- package/dist/extraction/wasm/tree-sitter-pascal.wasm +0 -0
- package/dist/installer/instructions-template.js +9 -9
- package/dist/installer/targets/shared.d.ts +5 -6
- package/dist/installer/targets/shared.d.ts.map +1 -1
- package/dist/installer/targets/shared.js +5 -6
- package/dist/installer/targets/shared.js.map +1 -1
- package/dist/mcp/liveness-watchdog.js +16 -16
- package/dist/mcp/server-instructions.js +69 -69
- package/dist/mcp/tools.d.ts +17 -4
- package/dist/mcp/tools.d.ts.map +1 -1
- package/dist/mcp/tools.js +192 -22
- package/dist/mcp/tools.js.map +1 -1
- package/dist/reasoning/reasoner.js +32 -32
- package/dist/spec/config.d.ts +43 -0
- package/dist/spec/config.d.ts.map +1 -0
- package/dist/spec/config.js +275 -0
- package/dist/spec/config.js.map +1 -0
- package/dist/spec/db/commit-node.d.ts +23 -0
- package/dist/spec/db/commit-node.d.ts.map +1 -0
- package/dist/spec/db/commit-node.js +62 -0
- package/dist/spec/db/commit-node.js.map +1 -0
- package/dist/spec/db/fragment-node.d.ts +23 -0
- package/dist/spec/db/fragment-node.d.ts.map +1 -0
- package/dist/spec/db/fragment-node.js +120 -0
- package/dist/spec/db/fragment-node.js.map +1 -0
- package/dist/spec/db/fts.d.ts +60 -0
- package/dist/spec/db/fts.d.ts.map +1 -0
- package/dist/spec/db/fts.js +285 -0
- package/dist/spec/db/fts.js.map +1 -0
- package/dist/spec/db/index.d.ts +13 -0
- package/dist/spec/db/index.d.ts.map +1 -0
- package/dist/spec/db/index.js +50 -0
- package/dist/spec/db/index.js.map +1 -0
- package/dist/spec/db/relations.d.ts +55 -0
- package/dist/spec/db/relations.d.ts.map +1 -0
- package/dist/spec/db/relations.js +158 -0
- package/dist/spec/db/relations.js.map +1 -0
- package/dist/spec/db/schema.d.ts +30 -0
- package/dist/spec/db/schema.d.ts.map +1 -0
- package/dist/spec/db/schema.js +87 -0
- package/dist/spec/db/schema.js.map +1 -0
- package/dist/spec/db/schema.sql +107 -0
- package/dist/spec/db/spec-node.d.ts +41 -0
- package/dist/spec/db/spec-node.d.ts.map +1 -0
- package/dist/spec/db/spec-node.js +114 -0
- package/dist/spec/db/spec-node.js.map +1 -0
- package/dist/spec/evolve/impact-locator.d.ts +13 -0
- package/dist/spec/evolve/impact-locator.d.ts.map +1 -0
- package/dist/spec/evolve/impact-locator.js +25 -0
- package/dist/spec/evolve/impact-locator.js.map +1 -0
- package/dist/spec/evolve/llm-client.d.ts +50 -0
- package/dist/spec/evolve/llm-client.d.ts.map +1 -0
- package/dist/spec/evolve/llm-client.js +176 -0
- package/dist/spec/evolve/llm-client.js.map +1 -0
- package/dist/spec/evolve/logic-checker.d.ts +12 -0
- package/dist/spec/evolve/logic-checker.d.ts.map +1 -0
- package/dist/spec/evolve/logic-checker.js +48 -0
- package/dist/spec/evolve/logic-checker.js.map +1 -0
- package/dist/spec/evolve/pipeline.d.ts +40 -0
- package/dist/spec/evolve/pipeline.d.ts.map +1 -0
- package/dist/spec/evolve/pipeline.js +314 -0
- package/dist/spec/evolve/pipeline.js.map +1 -0
- package/dist/spec/evolve/spec-rewriter.d.ts +42 -0
- package/dist/spec/evolve/spec-rewriter.d.ts.map +1 -0
- package/dist/spec/evolve/spec-rewriter.js +254 -0
- package/dist/spec/evolve/spec-rewriter.js.map +1 -0
- package/dist/spec/graph/queries.d.ts +58 -0
- package/dist/spec/graph/queries.d.ts.map +1 -0
- package/dist/spec/graph/queries.js +215 -0
- package/dist/spec/graph/queries.js.map +1 -0
- package/dist/spec/mining/diff-parser.d.ts +33 -0
- package/dist/spec/mining/diff-parser.d.ts.map +1 -0
- package/dist/spec/mining/diff-parser.js +166 -0
- package/dist/spec/mining/diff-parser.js.map +1 -0
- package/dist/spec/mining/git-scanner.d.ts +91 -0
- package/dist/spec/mining/git-scanner.d.ts.map +1 -0
- package/dist/spec/mining/git-scanner.js +266 -0
- package/dist/spec/mining/git-scanner.js.map +1 -0
- package/dist/spec/mining/pipeline.d.ts +53 -0
- package/dist/spec/mining/pipeline.d.ts.map +1 -0
- package/dist/spec/mining/pipeline.js +165 -0
- package/dist/spec/mining/pipeline.js.map +1 -0
- package/dist/spec/mining/scope-resolver.d.ts +45 -0
- package/dist/spec/mining/scope-resolver.d.ts.map +1 -0
- package/dist/spec/mining/scope-resolver.js +103 -0
- package/dist/spec/mining/scope-resolver.js.map +1 -0
- package/dist/spec/mining/spec-extractor.d.ts +69 -0
- package/dist/spec/mining/spec-extractor.d.ts.map +1 -0
- package/dist/spec/mining/spec-extractor.js +369 -0
- package/dist/spec/mining/spec-extractor.js.map +1 -0
- package/dist/spec/types.d.ts +149 -0
- package/dist/spec/types.d.ts.map +1 -0
- package/dist/spec/types.js +15 -0
- package/dist/spec/types.js.map +1 -0
- package/dist/spec/utils.d.ts +166 -0
- package/dist/spec/utils.d.ts.map +1 -0
- package/dist/spec/utils.js +461 -0
- package/dist/spec/utils.js.map +1 -0
- package/package.json +58 -57
- package/scripts/add-lang/bench.sh +60 -60
- package/scripts/add-lang/check-grammar.mjs +75 -75
- package/scripts/add-lang/dump-ast.mjs +103 -103
- package/scripts/add-lang/verify-extraction.mjs +70 -70
- package/scripts/agent-eval/ab-adoption.sh +91 -91
- package/scripts/agent-eval/ab-hook.sh +86 -86
- package/scripts/agent-eval/ab-impl.sh +78 -78
- package/scripts/agent-eval/ab-new-vs-baseline.sh +102 -102
- package/scripts/agent-eval/ab-sufficiency.sh +78 -78
- package/scripts/agent-eval/arms-F.sh +21 -21
- package/scripts/agent-eval/arms-matrix.sh +37 -37
- package/scripts/agent-eval/audit.sh +68 -68
- package/scripts/agent-eval/bench-readme.sh +28 -28
- package/scripts/agent-eval/bench-why-repo.sh +22 -22
- package/scripts/agent-eval/block-read-hook.sh +19 -19
- package/scripts/agent-eval/hook-settings.json +15 -15
- package/scripts/agent-eval/itrun.sh +120 -120
- package/scripts/agent-eval/offload-eval-3arm.sh +72 -72
- package/scripts/agent-eval/offload-eval-cost.mjs +133 -133
- package/scripts/agent-eval/offload-eval-effort.mjs +108 -108
- package/scripts/agent-eval/offload-eval-frontload-matrix.sh +25 -25
- package/scripts/agent-eval/offload-eval-frontload.sh +47 -47
- package/scripts/agent-eval/offload-eval-ground-truth.json +18 -18
- package/scripts/agent-eval/offload-eval-hook.mjs +84 -84
- package/scripts/agent-eval/offload-eval-judge.mjs +103 -103
- package/scripts/agent-eval/offload-eval-matrix.sh +20 -20
- package/scripts/agent-eval/offload-eval-metrics.mjs +94 -94
- package/scripts/agent-eval/offload-eval-refs1.sh +50 -50
- package/scripts/agent-eval/offload-eval-setup.sh +24 -24
- package/scripts/agent-eval/offload-eval-styles.sh +71 -71
- package/scripts/agent-eval/offload-eval-summarize.mjs +68 -68
- package/scripts/agent-eval/offload-eval.md +76 -76
- package/scripts/agent-eval/parse-arms.mjs +116 -116
- package/scripts/agent-eval/parse-bench-readme.mjs +84 -84
- package/scripts/agent-eval/parse-run.mjs +45 -45
- package/scripts/agent-eval/parse-session.mjs +93 -93
- package/scripts/agent-eval/probe-context.mjs +21 -21
- package/scripts/agent-eval/probe-explore.mjs +40 -40
- package/scripts/agent-eval/probe-node.mjs +20 -20
- package/scripts/agent-eval/probe-sweep.mjs +119 -119
- package/scripts/agent-eval/probe-trace.mjs +20 -20
- package/scripts/agent-eval/redirect-read-hook.sh +38 -38
- package/scripts/agent-eval/run-agent.sh +34 -34
- package/scripts/agent-eval/run-all.sh +69 -69
- package/scripts/agent-eval/run-arms.sh +56 -56
- package/scripts/agent-eval/seq-matrix.mjs +137 -137
- package/scripts/build-bundle.sh +118 -118
- package/scripts/extract-release-notes.mjs +130 -130
- package/scripts/local-install.sh +41 -41
- package/scripts/npm-sdk.js +75 -75
- package/scripts/npm-shim.js +246 -246
- package/scripts/pack-npm.sh +119 -119
- package/scripts/prepare-release.mjs +270 -270
- package/scripts/qa_eval/README.md +407 -404
- package/scripts/qa_eval/_test_deveco_probe.py +41 -41
- package/scripts/qa_eval/agent_runner.py +526 -526
- package/scripts/qa_eval/data/.gitignore +4 -4
- package/scripts/qa_eval/data/test-set.jsonl +2 -22
- package/scripts/qa_eval/eval_metrics.py +274 -233
- package/scripts/qa_eval/external_agent.py +976 -671
- package/scripts/qa_eval/llm_config.py +92 -92
- package/scripts/qa_eval/memory_monitor.py +132 -132
- package/scripts/qa_eval/my_answer_accuracy.py +187 -187
- package/scripts/qa_eval/requirements.txt +2 -2
- package/scripts/qa_eval/run_pipeline.py +804 -711
- package/scripts/qa_eval/stats_efficiency.py +279 -279
- package/scripts/qa_eval/stats_scores.py +207 -207
|
@@ -1,84 +1,84 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
// UserPromptSubmit hook — APPROACH 1: additive context-injection.
|
|
3
|
-
// Front-loads homegraph's structural answer for flow/impact/"how/where" prompts so the
|
|
4
|
-
// agent's reflex grep/read has nothing left to find. Strictly additive (never blocks),
|
|
5
|
-
// gated to structural prompts (no cost otherwise), and uses RAW explore (offload disabled)
|
|
6
|
-
// so the injected context is accurate — never the (currently low-fidelity) synthesis.
|
|
7
|
-
//
|
|
8
|
-
// Reads {prompt, cwd} as JSON on stdin; prints the explore result to stdout (which Claude
|
|
9
|
-
// Code injects into the agent's context). Any failure -> silent exit 0 (degradable).
|
|
10
|
-
import { pathToFileURL, fileURLToPath } from 'node:url';
|
|
11
|
-
import { resolve, join, dirname } from 'node:path';
|
|
12
|
-
import { existsSync, readFileSync, appendFileSync } from 'node:fs';
|
|
13
|
-
|
|
14
|
-
// Resolve the engine repo from this script's own location (scripts/agent-eval/ -> ../..),
|
|
15
|
-
// overridable with CG_ENGINE. The hook ships inside the repo, so it finds its own dist.
|
|
16
|
-
const HERE = dirname(fileURLToPath(import.meta.url));
|
|
17
|
-
const ENGINE = process.env.CG_ENGINE || resolve(HERE, '..', '..');
|
|
18
|
-
const BUDGET = Number(process.env.CG_FRONTLOAD_BUDGET || 16000);
|
|
19
|
-
|
|
20
|
-
// Debug log only when CG_FRONTLOAD_DEBUG is set to a file path (the harness points it at a
|
|
21
|
-
// log to count injections); off by default so the shipped hook writes nothing extra.
|
|
22
|
-
const DBG = process.env.CG_FRONTLOAD_DEBUG;
|
|
23
|
-
const dbg = (m) => { if (!DBG) return; try { appendFileSync(DBG, `[${new Date().toISOString()}] ${m}\n`); } catch { /* ignore */ } };
|
|
24
|
-
|
|
25
|
-
let input = {};
|
|
26
|
-
try { input = JSON.parse(readFileSync(0, 'utf8')); } catch (e) { dbg('stdin parse fail: ' + e.message); }
|
|
27
|
-
const prompt = String(input.prompt || '');
|
|
28
|
-
const cwd = String(input.cwd || process.cwd());
|
|
29
|
-
dbg(`invoked: promptLen=${prompt.length} cwd=${cwd}`);
|
|
30
|
-
|
|
31
|
-
// Gate: only structural / flow / impact / where-how questions. Cheap regex; silent no-op
|
|
32
|
-
// otherwise so non-structural prompts ("fix this typo") cost nothing.
|
|
33
|
-
const STRUCTURAL = /\b(how|where|trace|flow|path|reach(es|ed)?|call(s|ed|er|ers|ee)?|depend|impact|affect|wire[ds]?|connect|implement|architect|structure|breaks?|what calls|why does)\b/i;
|
|
34
|
-
if (!prompt || !STRUCTURAL.test(prompt)) { dbg('gate: non-structural, no-op'); process.exit(0); }
|
|
35
|
-
dbg('gate: structural PASS');
|
|
36
|
-
|
|
37
|
-
// Find the index: cwd, then walk up a few levels.
|
|
38
|
-
let root = cwd, found = null;
|
|
39
|
-
for (let i = 0; i < 6 && root; i++) {
|
|
40
|
-
if (existsSync(join(root, '.homegraph'))) { found = root; break; }
|
|
41
|
-
const parent = resolve(root, '..'); if (parent === root) break; root = parent;
|
|
42
|
-
}
|
|
43
|
-
if (!found) { dbg(`no .homegraph found from cwd=${cwd}`); process.exit(0); }
|
|
44
|
-
dbg(`found index at ${found}`);
|
|
45
|
-
|
|
46
|
-
try {
|
|
47
|
-
process.env.HOMEGRAPH_OFFLOAD_DISABLE = '1'; // raw, accurate — never the unfixed offload
|
|
48
|
-
process.env.HOMEGRAPH_TELEMETRY = '0'; process.env.DO_NOT_TRACK = '1';
|
|
49
|
-
const load = async (rel) => import(pathToFileURL(resolve(ENGINE, rel)).href);
|
|
50
|
-
const idx = await load('dist/index.js');
|
|
51
|
-
const tools = await load('dist/mcp/tools.js');
|
|
52
|
-
const HomeGraph = idx.default?.default ?? idx.default ?? idx.HomeGraph;
|
|
53
|
-
const ToolHandler = tools.ToolHandler ?? tools.default?.ToolHandler;
|
|
54
|
-
if (typeof HomeGraph?.openSync !== 'function' || typeof ToolHandler !== 'function') process.exit(0);
|
|
55
|
-
|
|
56
|
-
// Retry once on a transient busy/locked index (the hook's openSync can race a
|
|
57
|
-
// freshly-warming daemon on the first prompt of a session).
|
|
58
|
-
let text = '';
|
|
59
|
-
for (let attempt = 1; attempt <= 2; attempt++) {
|
|
60
|
-
try {
|
|
61
|
-
const cg = HomeGraph.openSync(found);
|
|
62
|
-
const h = new ToolHandler(cg);
|
|
63
|
-
const res = await h.execute('homegraph_explore', { query: prompt });
|
|
64
|
-
text = res?.content?.[0]?.text ?? '';
|
|
65
|
-
try { cg.close?.(); } catch { /* ignore */ }
|
|
66
|
-
dbg(`explore attempt ${attempt} returned ${text.length} chars`);
|
|
67
|
-
break;
|
|
68
|
-
} catch (e) {
|
|
69
|
-
dbg(`explore attempt ${attempt} failed: ${e?.message || e}`);
|
|
70
|
-
if (attempt === 2) throw e;
|
|
71
|
-
await new Promise((r) => setTimeout(r, 800));
|
|
72
|
-
}
|
|
73
|
-
}
|
|
74
|
-
if (!text.trim()) { dbg('empty explore result, no-op'); process.exit(0); }
|
|
75
|
-
if (text.length > BUDGET) text = text.slice(0, BUDGET) + '\n…[front-load truncated to budget]';
|
|
76
|
-
|
|
77
|
-
process.stdout.write(
|
|
78
|
-
`## HomeGraph structural context (auto-retrieved for this question)\n` +
|
|
79
|
-
`The code graph was queried for your question; the relevant symbols, source, and call flow are below. ` +
|
|
80
|
-
`Treat the quoted source as already read. If you need more, call homegraph_explore with specific symbol names rather than grepping or reading files.\n\n` +
|
|
81
|
-
text + '\n'
|
|
82
|
-
);
|
|
83
|
-
dbg(`INJECTED ${text.length} chars`);
|
|
84
|
-
} catch (e) { dbg('ERROR: ' + (e?.stack || e?.message || e)); process.exit(0); } // degradable
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// UserPromptSubmit hook — APPROACH 1: additive context-injection.
|
|
3
|
+
// Front-loads homegraph's structural answer for flow/impact/"how/where" prompts so the
|
|
4
|
+
// agent's reflex grep/read has nothing left to find. Strictly additive (never blocks),
|
|
5
|
+
// gated to structural prompts (no cost otherwise), and uses RAW explore (offload disabled)
|
|
6
|
+
// so the injected context is accurate — never the (currently low-fidelity) synthesis.
|
|
7
|
+
//
|
|
8
|
+
// Reads {prompt, cwd} as JSON on stdin; prints the explore result to stdout (which Claude
|
|
9
|
+
// Code injects into the agent's context). Any failure -> silent exit 0 (degradable).
|
|
10
|
+
import { pathToFileURL, fileURLToPath } from 'node:url';
|
|
11
|
+
import { resolve, join, dirname } from 'node:path';
|
|
12
|
+
import { existsSync, readFileSync, appendFileSync } from 'node:fs';
|
|
13
|
+
|
|
14
|
+
// Resolve the engine repo from this script's own location (scripts/agent-eval/ -> ../..),
|
|
15
|
+
// overridable with CG_ENGINE. The hook ships inside the repo, so it finds its own dist.
|
|
16
|
+
const HERE = dirname(fileURLToPath(import.meta.url));
|
|
17
|
+
const ENGINE = process.env.CG_ENGINE || resolve(HERE, '..', '..');
|
|
18
|
+
const BUDGET = Number(process.env.CG_FRONTLOAD_BUDGET || 16000);
|
|
19
|
+
|
|
20
|
+
// Debug log only when CG_FRONTLOAD_DEBUG is set to a file path (the harness points it at a
|
|
21
|
+
// log to count injections); off by default so the shipped hook writes nothing extra.
|
|
22
|
+
const DBG = process.env.CG_FRONTLOAD_DEBUG;
|
|
23
|
+
const dbg = (m) => { if (!DBG) return; try { appendFileSync(DBG, `[${new Date().toISOString()}] ${m}\n`); } catch { /* ignore */ } };
|
|
24
|
+
|
|
25
|
+
let input = {};
|
|
26
|
+
try { input = JSON.parse(readFileSync(0, 'utf8')); } catch (e) { dbg('stdin parse fail: ' + e.message); }
|
|
27
|
+
const prompt = String(input.prompt || '');
|
|
28
|
+
const cwd = String(input.cwd || process.cwd());
|
|
29
|
+
dbg(`invoked: promptLen=${prompt.length} cwd=${cwd}`);
|
|
30
|
+
|
|
31
|
+
// Gate: only structural / flow / impact / where-how questions. Cheap regex; silent no-op
|
|
32
|
+
// otherwise so non-structural prompts ("fix this typo") cost nothing.
|
|
33
|
+
const STRUCTURAL = /\b(how|where|trace|flow|path|reach(es|ed)?|call(s|ed|er|ers|ee)?|depend|impact|affect|wire[ds]?|connect|implement|architect|structure|breaks?|what calls|why does)\b/i;
|
|
34
|
+
if (!prompt || !STRUCTURAL.test(prompt)) { dbg('gate: non-structural, no-op'); process.exit(0); }
|
|
35
|
+
dbg('gate: structural PASS');
|
|
36
|
+
|
|
37
|
+
// Find the index: cwd, then walk up a few levels.
|
|
38
|
+
let root = cwd, found = null;
|
|
39
|
+
for (let i = 0; i < 6 && root; i++) {
|
|
40
|
+
if (existsSync(join(root, '.homegraph'))) { found = root; break; }
|
|
41
|
+
const parent = resolve(root, '..'); if (parent === root) break; root = parent;
|
|
42
|
+
}
|
|
43
|
+
if (!found) { dbg(`no .homegraph found from cwd=${cwd}`); process.exit(0); }
|
|
44
|
+
dbg(`found index at ${found}`);
|
|
45
|
+
|
|
46
|
+
try {
|
|
47
|
+
process.env.HOMEGRAPH_OFFLOAD_DISABLE = '1'; // raw, accurate — never the unfixed offload
|
|
48
|
+
process.env.HOMEGRAPH_TELEMETRY = '0'; process.env.DO_NOT_TRACK = '1';
|
|
49
|
+
const load = async (rel) => import(pathToFileURL(resolve(ENGINE, rel)).href);
|
|
50
|
+
const idx = await load('dist/index.js');
|
|
51
|
+
const tools = await load('dist/mcp/tools.js');
|
|
52
|
+
const HomeGraph = idx.default?.default ?? idx.default ?? idx.HomeGraph;
|
|
53
|
+
const ToolHandler = tools.ToolHandler ?? tools.default?.ToolHandler;
|
|
54
|
+
if (typeof HomeGraph?.openSync !== 'function' || typeof ToolHandler !== 'function') process.exit(0);
|
|
55
|
+
|
|
56
|
+
// Retry once on a transient busy/locked index (the hook's openSync can race a
|
|
57
|
+
// freshly-warming daemon on the first prompt of a session).
|
|
58
|
+
let text = '';
|
|
59
|
+
for (let attempt = 1; attempt <= 2; attempt++) {
|
|
60
|
+
try {
|
|
61
|
+
const cg = HomeGraph.openSync(found);
|
|
62
|
+
const h = new ToolHandler(cg);
|
|
63
|
+
const res = await h.execute('homegraph_explore', { query: prompt });
|
|
64
|
+
text = res?.content?.[0]?.text ?? '';
|
|
65
|
+
try { cg.close?.(); } catch { /* ignore */ }
|
|
66
|
+
dbg(`explore attempt ${attempt} returned ${text.length} chars`);
|
|
67
|
+
break;
|
|
68
|
+
} catch (e) {
|
|
69
|
+
dbg(`explore attempt ${attempt} failed: ${e?.message || e}`);
|
|
70
|
+
if (attempt === 2) throw e;
|
|
71
|
+
await new Promise((r) => setTimeout(r, 800));
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
if (!text.trim()) { dbg('empty explore result, no-op'); process.exit(0); }
|
|
75
|
+
if (text.length > BUDGET) text = text.slice(0, BUDGET) + '\n…[front-load truncated to budget]';
|
|
76
|
+
|
|
77
|
+
process.stdout.write(
|
|
78
|
+
`## HomeGraph structural context (auto-retrieved for this question)\n` +
|
|
79
|
+
`The code graph was queried for your question; the relevant symbols, source, and call flow are below. ` +
|
|
80
|
+
`Treat the quoted source as already read. If you need more, call homegraph_explore with specific symbol names rather than grepping or reading files.\n\n` +
|
|
81
|
+
text + '\n'
|
|
82
|
+
);
|
|
83
|
+
dbg(`INJECTED ${text.length} chars`);
|
|
84
|
+
} catch (e) { dbg('ERROR: ' + (e?.stack || e?.message || e)); process.exit(0); } // degradable
|
|
@@ -1,103 +1,103 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
// Accuracy judge. For each run in results.jsonl:
|
|
3
|
-
// - end-to-end: agent finalAnswer vs verified ground truth (all arms)
|
|
4
|
-
// - fidelity: offload synthesized answer vs ground truth (offload arm only)
|
|
5
|
-
// Judge = claude -p sonnet --effort high, no tools, run from a neutral cwd,
|
|
6
|
-
// JSON-only verdicts. Writes judged.jsonl (one line per run, verdicts merged).
|
|
7
|
-
//
|
|
8
|
-
// Usage: judge.mjs --results <f> --truth <f> --out <f> [--concurrency 4]
|
|
9
|
-
import { readFileSync, writeFileSync, existsSync } from 'fs';
|
|
10
|
-
import { execFile } from 'child_process';
|
|
11
|
-
|
|
12
|
-
const A = {};
|
|
13
|
-
for (let i = 2; i < process.argv.length; i += 2) A[process.argv[i].replace(/^--/, '')] = process.argv[i + 1];
|
|
14
|
-
const results = readFileSync(A.results, 'utf8').split('\n').filter(Boolean).map(l => JSON.parse(l));
|
|
15
|
-
const truth = JSON.parse(readFileSync(A.truth, 'utf8'));
|
|
16
|
-
const OUT = A.out || '/tmp/cg-offload-eval/judged.jsonl';
|
|
17
|
-
const CONC = Number(A.concurrency || 4);
|
|
18
|
-
|
|
19
|
-
function askJudge(prompt) {
|
|
20
|
-
return new Promise((resolve) => {
|
|
21
|
-
execFile('claude', ['-p', prompt, '--model', 'sonnet', '--effort', 'high',
|
|
22
|
-
'--max-budget-usd', '0.5', '--strict-mcp-config', '--mcp-config', '{"mcpServers":{}}'],
|
|
23
|
-
// Run from a neutral dir with no repo files so the judge can't "cheat" by reading source.
|
|
24
|
-
{ cwd: process.env.AGENT_EVAL_OUT || '/tmp', maxBuffer: 1 << 24, timeout: 120000 },
|
|
25
|
-
(err, stdout) => {
|
|
26
|
-
const raw = (stdout || '').trim();
|
|
27
|
-
const m = raw.match(/\{[\s\S]*\}/);
|
|
28
|
-
if (!m) return resolve({ verdict: 'error', score: null, note: (err ? 'exec ' + err.message : 'no json').slice(0, 80) });
|
|
29
|
-
try { resolve(JSON.parse(m[0])); } catch { resolve({ verdict: 'error', score: null, note: 'parse fail' }); }
|
|
30
|
-
});
|
|
31
|
-
});
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
const e2ePrompt = (gt, ans) => `You are scoring whether an AI coding agent correctly answered a code-flow question about a repository. Judge ONLY against the verified ground truth. Do NOT use any tools.
|
|
35
|
-
|
|
36
|
-
QUESTION: ${gt.question}
|
|
37
|
-
|
|
38
|
-
VERIFIED GROUND TRUTH (the actual call path + files):
|
|
39
|
-
${gt.truth}
|
|
40
|
-
|
|
41
|
-
AGENT'S ANSWER:
|
|
42
|
-
${ans || '(empty)'}
|
|
43
|
-
|
|
44
|
-
Score how correct the agent's answer is vs the ground truth. A "pass" means it identifies the core mechanism and the major hops with the right files/symbols and makes no materially wrong claim. "partial" = right area but misses major hops or has notable errors. "fail" = wrong layer, fabricated, or misses the mechanism.
|
|
45
|
-
Output ONLY minified JSON, no prose, no code fences:
|
|
46
|
-
{"verdict":"pass|partial|fail","score":<0-100>,"missedHops":["..."],"wrongClaims":["..."],"note":"<=20 words"}`;
|
|
47
|
-
|
|
48
|
-
const fidPrompt = (gt, ans) => `You are scoring the FIDELITY of a machine-synthesized code-exploration answer against verified ground truth. The synthesized answer claims to trace a flow and cite file:line locations. Do NOT use any tools.
|
|
49
|
-
|
|
50
|
-
QUESTION: ${gt.question}
|
|
51
|
-
|
|
52
|
-
VERIFIED GROUND TRUTH (the actual call path + files):
|
|
53
|
-
${gt.truth}
|
|
54
|
-
|
|
55
|
-
SYNTHESIZED ANSWER (to score):
|
|
56
|
-
${ans || '(empty)'}
|
|
57
|
-
|
|
58
|
-
Judge: (1) is the traced call path correct vs ground truth? (2) are the cited files/symbols correct (not fabricated)? (3) if it gave a "Coverage:" verdict, was that verdict honest about what it actually covered? A confident WRONG trace is the worst outcome — penalize it harder than an honest "partial/not found".
|
|
59
|
-
Output ONLY minified JSON, no prose, no code fences:
|
|
60
|
-
{"verdict":"pass|partial|fail","score":<0-100>,"fabrication":<true|false>,"coverageHonest":<true|false>,"missedHops":["..."],"note":"<=20 words"}`;
|
|
61
|
-
|
|
62
|
-
// Build the job list
|
|
63
|
-
const jobs = [];
|
|
64
|
-
for (const r of results) {
|
|
65
|
-
const gt = truth[r.repo];
|
|
66
|
-
if (!gt) { r._nojudge = true; continue; }
|
|
67
|
-
jobs.push({ r, kind: 'e2e', prompt: e2ePrompt(gt, r.finalAnswer) });
|
|
68
|
-
if (r.arm === 'offload' && Array.isArray(r.offloadAnswers))
|
|
69
|
-
r.offloadAnswers.forEach((ans, i) => { if (ans && ans.trim()) jobs.push({ r, kind: 'fid', idx: i, prompt: fidPrompt(gt, ans) }); });
|
|
70
|
-
}
|
|
71
|
-
console.error(`judging ${jobs.length} verdicts across ${results.length} runs (concurrency ${CONC})...`);
|
|
72
|
-
|
|
73
|
-
let done = 0;
|
|
74
|
-
async function worker(queue) {
|
|
75
|
-
while (queue.length) {
|
|
76
|
-
const job = queue.shift();
|
|
77
|
-
const v = await askJudge(job.prompt);
|
|
78
|
-
if (job.kind === 'e2e') job.r.e2e = v; else (job.r._fid ??= []).push(v);
|
|
79
|
-
console.error(` [${++done}/${jobs.length}] ${job.r.repo}/${job.r.arm}#${job.r.rep} ${job.kind}: ${v.verdict}${v.score != null ? ' ' + v.score : ''}`);
|
|
80
|
-
}
|
|
81
|
-
}
|
|
82
|
-
const q = [...jobs];
|
|
83
|
-
await Promise.all(Array.from({ length: CONC }, () => worker(q)));
|
|
84
|
-
|
|
85
|
-
// Aggregate per-answer fidelity verdicts into one fidelity object per offload run.
|
|
86
|
-
const medOf = (a) => { a = [...a].sort((x, y) => x - y); return a.length ? (a.length % 2 ? a[(a.length - 1) / 2] : (a[a.length / 2 - 1] + a[a.length / 2]) / 2) : null; };
|
|
87
|
-
for (const r of results) {
|
|
88
|
-
if (r._fid?.length) {
|
|
89
|
-
const scores = r._fid.map(v => v.score).filter(x => x != null);
|
|
90
|
-
r.fidelity = {
|
|
91
|
-
n: r._fid.length, scores,
|
|
92
|
-
max: scores.length ? Math.max(...scores) : null,
|
|
93
|
-
min: scores.length ? Math.min(...scores) : null,
|
|
94
|
-
median: medOf(scores),
|
|
95
|
-
anyFabrication: r._fid.some(v => v.fabrication === true),
|
|
96
|
-
allCoverageHonest: r._fid.every(v => v.coverageHonest !== false),
|
|
97
|
-
verdicts: r._fid.map(v => v.verdict),
|
|
98
|
-
};
|
|
99
|
-
}
|
|
100
|
-
delete r._fid;
|
|
101
|
-
}
|
|
102
|
-
writeFileSync(OUT, results.map(r => JSON.stringify(r)).join('\n') + '\n');
|
|
103
|
-
console.error(`wrote ${OUT}`);
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Accuracy judge. For each run in results.jsonl:
|
|
3
|
+
// - end-to-end: agent finalAnswer vs verified ground truth (all arms)
|
|
4
|
+
// - fidelity: offload synthesized answer vs ground truth (offload arm only)
|
|
5
|
+
// Judge = claude -p sonnet --effort high, no tools, run from a neutral cwd,
|
|
6
|
+
// JSON-only verdicts. Writes judged.jsonl (one line per run, verdicts merged).
|
|
7
|
+
//
|
|
8
|
+
// Usage: judge.mjs --results <f> --truth <f> --out <f> [--concurrency 4]
|
|
9
|
+
import { readFileSync, writeFileSync, existsSync } from 'fs';
|
|
10
|
+
import { execFile } from 'child_process';
|
|
11
|
+
|
|
12
|
+
const A = {};
|
|
13
|
+
for (let i = 2; i < process.argv.length; i += 2) A[process.argv[i].replace(/^--/, '')] = process.argv[i + 1];
|
|
14
|
+
const results = readFileSync(A.results, 'utf8').split('\n').filter(Boolean).map(l => JSON.parse(l));
|
|
15
|
+
const truth = JSON.parse(readFileSync(A.truth, 'utf8'));
|
|
16
|
+
const OUT = A.out || '/tmp/cg-offload-eval/judged.jsonl';
|
|
17
|
+
const CONC = Number(A.concurrency || 4);
|
|
18
|
+
|
|
19
|
+
function askJudge(prompt) {
|
|
20
|
+
return new Promise((resolve) => {
|
|
21
|
+
execFile('claude', ['-p', prompt, '--model', 'sonnet', '--effort', 'high',
|
|
22
|
+
'--max-budget-usd', '0.5', '--strict-mcp-config', '--mcp-config', '{"mcpServers":{}}'],
|
|
23
|
+
// Run from a neutral dir with no repo files so the judge can't "cheat" by reading source.
|
|
24
|
+
{ cwd: process.env.AGENT_EVAL_OUT || '/tmp', maxBuffer: 1 << 24, timeout: 120000 },
|
|
25
|
+
(err, stdout) => {
|
|
26
|
+
const raw = (stdout || '').trim();
|
|
27
|
+
const m = raw.match(/\{[\s\S]*\}/);
|
|
28
|
+
if (!m) return resolve({ verdict: 'error', score: null, note: (err ? 'exec ' + err.message : 'no json').slice(0, 80) });
|
|
29
|
+
try { resolve(JSON.parse(m[0])); } catch { resolve({ verdict: 'error', score: null, note: 'parse fail' }); }
|
|
30
|
+
});
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const e2ePrompt = (gt, ans) => `You are scoring whether an AI coding agent correctly answered a code-flow question about a repository. Judge ONLY against the verified ground truth. Do NOT use any tools.
|
|
35
|
+
|
|
36
|
+
QUESTION: ${gt.question}
|
|
37
|
+
|
|
38
|
+
VERIFIED GROUND TRUTH (the actual call path + files):
|
|
39
|
+
${gt.truth}
|
|
40
|
+
|
|
41
|
+
AGENT'S ANSWER:
|
|
42
|
+
${ans || '(empty)'}
|
|
43
|
+
|
|
44
|
+
Score how correct the agent's answer is vs the ground truth. A "pass" means it identifies the core mechanism and the major hops with the right files/symbols and makes no materially wrong claim. "partial" = right area but misses major hops or has notable errors. "fail" = wrong layer, fabricated, or misses the mechanism.
|
|
45
|
+
Output ONLY minified JSON, no prose, no code fences:
|
|
46
|
+
{"verdict":"pass|partial|fail","score":<0-100>,"missedHops":["..."],"wrongClaims":["..."],"note":"<=20 words"}`;
|
|
47
|
+
|
|
48
|
+
const fidPrompt = (gt, ans) => `You are scoring the FIDELITY of a machine-synthesized code-exploration answer against verified ground truth. The synthesized answer claims to trace a flow and cite file:line locations. Do NOT use any tools.
|
|
49
|
+
|
|
50
|
+
QUESTION: ${gt.question}
|
|
51
|
+
|
|
52
|
+
VERIFIED GROUND TRUTH (the actual call path + files):
|
|
53
|
+
${gt.truth}
|
|
54
|
+
|
|
55
|
+
SYNTHESIZED ANSWER (to score):
|
|
56
|
+
${ans || '(empty)'}
|
|
57
|
+
|
|
58
|
+
Judge: (1) is the traced call path correct vs ground truth? (2) are the cited files/symbols correct (not fabricated)? (3) if it gave a "Coverage:" verdict, was that verdict honest about what it actually covered? A confident WRONG trace is the worst outcome — penalize it harder than an honest "partial/not found".
|
|
59
|
+
Output ONLY minified JSON, no prose, no code fences:
|
|
60
|
+
{"verdict":"pass|partial|fail","score":<0-100>,"fabrication":<true|false>,"coverageHonest":<true|false>,"missedHops":["..."],"note":"<=20 words"}`;
|
|
61
|
+
|
|
62
|
+
// Build the job list
|
|
63
|
+
const jobs = [];
|
|
64
|
+
for (const r of results) {
|
|
65
|
+
const gt = truth[r.repo];
|
|
66
|
+
if (!gt) { r._nojudge = true; continue; }
|
|
67
|
+
jobs.push({ r, kind: 'e2e', prompt: e2ePrompt(gt, r.finalAnswer) });
|
|
68
|
+
if (r.arm === 'offload' && Array.isArray(r.offloadAnswers))
|
|
69
|
+
r.offloadAnswers.forEach((ans, i) => { if (ans && ans.trim()) jobs.push({ r, kind: 'fid', idx: i, prompt: fidPrompt(gt, ans) }); });
|
|
70
|
+
}
|
|
71
|
+
console.error(`judging ${jobs.length} verdicts across ${results.length} runs (concurrency ${CONC})...`);
|
|
72
|
+
|
|
73
|
+
let done = 0;
|
|
74
|
+
async function worker(queue) {
|
|
75
|
+
while (queue.length) {
|
|
76
|
+
const job = queue.shift();
|
|
77
|
+
const v = await askJudge(job.prompt);
|
|
78
|
+
if (job.kind === 'e2e') job.r.e2e = v; else (job.r._fid ??= []).push(v);
|
|
79
|
+
console.error(` [${++done}/${jobs.length}] ${job.r.repo}/${job.r.arm}#${job.r.rep} ${job.kind}: ${v.verdict}${v.score != null ? ' ' + v.score : ''}`);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
const q = [...jobs];
|
|
83
|
+
await Promise.all(Array.from({ length: CONC }, () => worker(q)));
|
|
84
|
+
|
|
85
|
+
// Aggregate per-answer fidelity verdicts into one fidelity object per offload run.
|
|
86
|
+
const medOf = (a) => { a = [...a].sort((x, y) => x - y); return a.length ? (a.length % 2 ? a[(a.length - 1) / 2] : (a[a.length / 2 - 1] + a[a.length / 2]) / 2) : null; };
|
|
87
|
+
for (const r of results) {
|
|
88
|
+
if (r._fid?.length) {
|
|
89
|
+
const scores = r._fid.map(v => v.score).filter(x => x != null);
|
|
90
|
+
r.fidelity = {
|
|
91
|
+
n: r._fid.length, scores,
|
|
92
|
+
max: scores.length ? Math.max(...scores) : null,
|
|
93
|
+
min: scores.length ? Math.min(...scores) : null,
|
|
94
|
+
median: medOf(scores),
|
|
95
|
+
anyFabrication: r._fid.some(v => v.fabrication === true),
|
|
96
|
+
allCoverageHonest: r._fid.every(v => v.coverageHonest !== false),
|
|
97
|
+
verdicts: r._fid.map(v => v.verdict),
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
delete r._fid;
|
|
101
|
+
}
|
|
102
|
+
writeFileSync(OUT, results.map(r => JSON.stringify(r)).join('\n') + '\n');
|
|
103
|
+
console.error(`wrote ${OUT}`);
|
|
@@ -1,20 +1,20 @@
|
|
|
1
|
-
#!/usr/bin/env bash
|
|
2
|
-
# Drive the 3-arm campaign (offload/raw/nocg) across all 4 tiers, n reps each, into one
|
|
3
|
-
# results.jsonl. Reads the canonical question per repo from offload-eval-ground-truth.json.
|
|
4
|
-
# Env: REPS (default 3) AGENT_EVAL_OUT=<scratch dir>
|
|
5
|
-
set -uo pipefail
|
|
6
|
-
HERE="$(cd "$(dirname "$0")" && pwd)"
|
|
7
|
-
OUT="${AGENT_EVAL_OUT:-/tmp/cg-offload-eval}"
|
|
8
|
-
GT="$HERE/offload-eval-ground-truth.json"
|
|
9
|
-
REPS="${REPS:-3}"
|
|
10
|
-
export RESULTS="$OUT/results.jsonl"
|
|
11
|
-
: > "$RESULTS"
|
|
12
|
-
for repo in mtkruto postybirb shapeshift trezor; do
|
|
13
|
-
case "$repo" in mtkruto) tier=small;; postybirb) tier=medium;; shapeshift) tier=complex;; trezor) tier=large;; esac
|
|
14
|
-
Q=$(node -e "console.log(JSON.parse(require('fs').readFileSync(process.argv[1],'utf8'))[process.argv[2]].question)" "$GT" "$repo")
|
|
15
|
-
echo ""; echo "### $repo ($tier) $(date +%H:%M:%S)"
|
|
16
|
-
bash "$HERE/offload-eval-3arm.sh" "$OUT/repos/$repo" "$tier" "$REPS" "$Q"
|
|
17
|
-
done
|
|
18
|
-
echo ""; echo "###### MATRIX DONE -> $RESULTS ($(wc -l < "$RESULTS") runs). Judge + summarize with:"
|
|
19
|
-
echo " node $HERE/offload-eval-judge.mjs --results $RESULTS --truth $GT --out $OUT/judged.jsonl"
|
|
20
|
-
echo " node $HERE/offload-eval-summarize.mjs $OUT/judged.jsonl"
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Drive the 3-arm campaign (offload/raw/nocg) across all 4 tiers, n reps each, into one
|
|
3
|
+
# results.jsonl. Reads the canonical question per repo from offload-eval-ground-truth.json.
|
|
4
|
+
# Env: REPS (default 3) AGENT_EVAL_OUT=<scratch dir>
|
|
5
|
+
set -uo pipefail
|
|
6
|
+
HERE="$(cd "$(dirname "$0")" && pwd)"
|
|
7
|
+
OUT="${AGENT_EVAL_OUT:-/tmp/cg-offload-eval}"
|
|
8
|
+
GT="$HERE/offload-eval-ground-truth.json"
|
|
9
|
+
REPS="${REPS:-3}"
|
|
10
|
+
export RESULTS="$OUT/results.jsonl"
|
|
11
|
+
: > "$RESULTS"
|
|
12
|
+
for repo in mtkruto postybirb shapeshift trezor; do
|
|
13
|
+
case "$repo" in mtkruto) tier=small;; postybirb) tier=medium;; shapeshift) tier=complex;; trezor) tier=large;; esac
|
|
14
|
+
Q=$(node -e "console.log(JSON.parse(require('fs').readFileSync(process.argv[1],'utf8'))[process.argv[2]].question)" "$GT" "$repo")
|
|
15
|
+
echo ""; echo "### $repo ($tier) $(date +%H:%M:%S)"
|
|
16
|
+
bash "$HERE/offload-eval-3arm.sh" "$OUT/repos/$repo" "$tier" "$REPS" "$Q"
|
|
17
|
+
done
|
|
18
|
+
echo ""; echo "###### MATRIX DONE -> $RESULTS ($(wc -l < "$RESULTS") runs). Judge + summarize with:"
|
|
19
|
+
echo " node $HERE/offload-eval-judge.mjs --results $RESULTS --truth $GT --out $OUT/judged.jsonl"
|
|
20
|
+
echo " node $HERE/offload-eval-summarize.mjs $OUT/judged.jsonl"
|
|
@@ -1,94 +1,94 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
// Extract one eval run's metrics from its Claude stream-json transcript + the
|
|
3
|
-
// offload usage sidecar log, emit ONE merged JSON line.
|
|
4
|
-
//
|
|
5
|
-
// Usage: extract-metrics.mjs --run <run.jsonl> --usage <usage.jsonl|-> \
|
|
6
|
-
// --arm <a> --rep <n> --repo <r> --tier <t> --q <question>
|
|
7
|
-
import { readFileSync, existsSync } from 'fs';
|
|
8
|
-
|
|
9
|
-
const args = {};
|
|
10
|
-
for (let i = 2; i < process.argv.length; i += 2) args[process.argv[i].replace(/^--/, '')] = process.argv[i + 1];
|
|
11
|
-
|
|
12
|
-
const runFile = args.run;
|
|
13
|
-
const lines = existsSync(runFile) ? readFileSync(runFile, 'utf8').split('\n').filter(Boolean) : [];
|
|
14
|
-
|
|
15
|
-
const toolCounts = {};
|
|
16
|
-
let result = null;
|
|
17
|
-
const tok = { gen: 0, fresh: 0, cached: 0 };
|
|
18
|
-
const offloadAnswers = [];
|
|
19
|
-
let exploreResults = 0; // tool_results from explore (offload or raw)
|
|
20
|
-
let lastAssistantText = '';
|
|
21
|
-
|
|
22
|
-
for (const line of lines) {
|
|
23
|
-
let ev; try { ev = JSON.parse(line); } catch { continue; }
|
|
24
|
-
|
|
25
|
-
// per-turn token usage (authoritative token measure; result.usage is last-turn only)
|
|
26
|
-
const u = ev.message?.usage;
|
|
27
|
-
if (u) {
|
|
28
|
-
tok.gen += u.output_tokens || 0;
|
|
29
|
-
tok.fresh += (u.input_tokens || 0) + (u.cache_creation_input_tokens || 0);
|
|
30
|
-
tok.cached += u.cache_read_input_tokens || 0;
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
if (ev.type === 'assistant' && Array.isArray(ev.message?.content)) {
|
|
34
|
-
for (const b of ev.message.content) {
|
|
35
|
-
if (b.type === 'tool_use') toolCounts[b.name] = (toolCounts[b.name] || 0) + 1;
|
|
36
|
-
if (b.type === 'text' && b.text?.trim()) lastAssistantText = b.text.trim();
|
|
37
|
-
}
|
|
38
|
-
}
|
|
39
|
-
// tool_results arrive in user messages
|
|
40
|
-
if (ev.type === 'user' && Array.isArray(ev.message?.content)) {
|
|
41
|
-
for (const b of ev.message.content) {
|
|
42
|
-
if (b.type !== 'tool_result') continue;
|
|
43
|
-
const text = Array.isArray(b.content)
|
|
44
|
-
? b.content.map(c => (typeof c === 'string' ? c : c.text || '')).join('')
|
|
45
|
-
: (typeof b.content === 'string' ? b.content : '');
|
|
46
|
-
// An offload answer is either the 'plain'/'report' synthesis (carries the
|
|
47
|
-
// "Synthesized by HomeGraph" footer) or a 'refs' answer (carries the re-expanded
|
|
48
|
-
// "### Referenced source — verbatim" appendix). A refs call that cited nothing
|
|
49
|
-
// valid falls back to RAW source, which is correctly counted as a raw explore below.
|
|
50
|
-
if (/Synthesized by HomeGraph|### Referenced source — verbatim/.test(text)) { offloadAnswers.push(text); exploreResults++; }
|
|
51
|
-
else if (/Found \d+ symbols? across|\*\*Exploration:/.test(text)) exploreResults++;
|
|
52
|
-
}
|
|
53
|
-
}
|
|
54
|
-
if (ev.type === 'result') result = ev;
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
// offload usage sidecar (HomeGraph AI tokens + cost) — one JSON line per offload call
|
|
58
|
-
const ai = { calls: 0, promptTokens: 0, completionTokens: 0, totalTokens: 0, credits: 0, costUsd: 0, ms: 0 };
|
|
59
|
-
if (args.usage && args.usage !== '-' && existsSync(args.usage)) {
|
|
60
|
-
for (const line of readFileSync(args.usage, 'utf8').split('\n').filter(Boolean)) {
|
|
61
|
-
let e; try { e = JSON.parse(line); } catch { continue; }
|
|
62
|
-
ai.calls++;
|
|
63
|
-
ai.promptTokens += e.promptTokens || 0;
|
|
64
|
-
ai.completionTokens += e.completionTokens || 0;
|
|
65
|
-
ai.totalTokens += e.totalTokens || 0;
|
|
66
|
-
ai.credits += e.creditsCharged || 0;
|
|
67
|
-
ai.costUsd += e.costUsd || 0;
|
|
68
|
-
ai.ms += e.ms || 0;
|
|
69
|
-
}
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
// front-load hook fired iff its injected header appears in the transcript
|
|
73
|
-
const frontload = lines.some(l => l.includes('auto-retrieved for this question'));
|
|
74
|
-
const get = (n) => toolCounts[n] || 0;
|
|
75
|
-
const read = get('Read');
|
|
76
|
-
const grep = get('Grep') + get('Bash') + get('Glob');
|
|
77
|
-
const explore = get('mcp__homegraph__homegraph_explore');
|
|
78
|
-
const cgAny = Object.keys(toolCounts).filter(k => /mcp__homegraph__/.test(k)).reduce((s, k) => s + toolCounts[k], 0);
|
|
79
|
-
|
|
80
|
-
const out = {
|
|
81
|
-
repo: args.repo, tier: args.tier, arm: args.arm, rep: Number(args.rep), question: args.q,
|
|
82
|
-
ok: result?.subtype === 'success',
|
|
83
|
-
durationSec: result ? +(result.duration_ms / 1000).toFixed(1) : null,
|
|
84
|
-
numTurns: result?.num_turns ?? null,
|
|
85
|
-
costUsdMain: result ? +(result.total_cost_usd || 0).toFixed(4) : null,
|
|
86
|
-
tokGen: tok.gen, tokFresh: tok.fresh, tokCached: tok.cached, tokBillable: tok.gen + tok.fresh,
|
|
87
|
-
read, grep, explore, cgAny, frontload,
|
|
88
|
-
offloadFired: offloadAnswers.length,
|
|
89
|
-
ai,
|
|
90
|
-
// text payloads for the accuracy judge (kept separate; large)
|
|
91
|
-
finalAnswer: (result?.result || lastAssistantText || '').slice(0, 8000),
|
|
92
|
-
offloadAnswers: offloadAnswers.map(a => a.slice(0, 6000)),
|
|
93
|
-
};
|
|
94
|
-
process.stdout.write(JSON.stringify(out) + '\n');
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Extract one eval run's metrics from its Claude stream-json transcript + the
|
|
3
|
+
// offload usage sidecar log, emit ONE merged JSON line.
|
|
4
|
+
//
|
|
5
|
+
// Usage: extract-metrics.mjs --run <run.jsonl> --usage <usage.jsonl|-> \
|
|
6
|
+
// --arm <a> --rep <n> --repo <r> --tier <t> --q <question>
|
|
7
|
+
import { readFileSync, existsSync } from 'fs';
|
|
8
|
+
|
|
9
|
+
const args = {};
|
|
10
|
+
for (let i = 2; i < process.argv.length; i += 2) args[process.argv[i].replace(/^--/, '')] = process.argv[i + 1];
|
|
11
|
+
|
|
12
|
+
const runFile = args.run;
|
|
13
|
+
const lines = existsSync(runFile) ? readFileSync(runFile, 'utf8').split('\n').filter(Boolean) : [];
|
|
14
|
+
|
|
15
|
+
const toolCounts = {};
|
|
16
|
+
let result = null;
|
|
17
|
+
const tok = { gen: 0, fresh: 0, cached: 0 };
|
|
18
|
+
const offloadAnswers = [];
|
|
19
|
+
let exploreResults = 0; // tool_results from explore (offload or raw)
|
|
20
|
+
let lastAssistantText = '';
|
|
21
|
+
|
|
22
|
+
for (const line of lines) {
|
|
23
|
+
let ev; try { ev = JSON.parse(line); } catch { continue; }
|
|
24
|
+
|
|
25
|
+
// per-turn token usage (authoritative token measure; result.usage is last-turn only)
|
|
26
|
+
const u = ev.message?.usage;
|
|
27
|
+
if (u) {
|
|
28
|
+
tok.gen += u.output_tokens || 0;
|
|
29
|
+
tok.fresh += (u.input_tokens || 0) + (u.cache_creation_input_tokens || 0);
|
|
30
|
+
tok.cached += u.cache_read_input_tokens || 0;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
if (ev.type === 'assistant' && Array.isArray(ev.message?.content)) {
|
|
34
|
+
for (const b of ev.message.content) {
|
|
35
|
+
if (b.type === 'tool_use') toolCounts[b.name] = (toolCounts[b.name] || 0) + 1;
|
|
36
|
+
if (b.type === 'text' && b.text?.trim()) lastAssistantText = b.text.trim();
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
// tool_results arrive in user messages
|
|
40
|
+
if (ev.type === 'user' && Array.isArray(ev.message?.content)) {
|
|
41
|
+
for (const b of ev.message.content) {
|
|
42
|
+
if (b.type !== 'tool_result') continue;
|
|
43
|
+
const text = Array.isArray(b.content)
|
|
44
|
+
? b.content.map(c => (typeof c === 'string' ? c : c.text || '')).join('')
|
|
45
|
+
: (typeof b.content === 'string' ? b.content : '');
|
|
46
|
+
// An offload answer is either the 'plain'/'report' synthesis (carries the
|
|
47
|
+
// "Synthesized by HomeGraph" footer) or a 'refs' answer (carries the re-expanded
|
|
48
|
+
// "### Referenced source — verbatim" appendix). A refs call that cited nothing
|
|
49
|
+
// valid falls back to RAW source, which is correctly counted as a raw explore below.
|
|
50
|
+
if (/Synthesized by HomeGraph|### Referenced source — verbatim/.test(text)) { offloadAnswers.push(text); exploreResults++; }
|
|
51
|
+
else if (/Found \d+ symbols? across|\*\*Exploration:/.test(text)) exploreResults++;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
if (ev.type === 'result') result = ev;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// offload usage sidecar (HomeGraph AI tokens + cost) — one JSON line per offload call
|
|
58
|
+
const ai = { calls: 0, promptTokens: 0, completionTokens: 0, totalTokens: 0, credits: 0, costUsd: 0, ms: 0 };
|
|
59
|
+
if (args.usage && args.usage !== '-' && existsSync(args.usage)) {
|
|
60
|
+
for (const line of readFileSync(args.usage, 'utf8').split('\n').filter(Boolean)) {
|
|
61
|
+
let e; try { e = JSON.parse(line); } catch { continue; }
|
|
62
|
+
ai.calls++;
|
|
63
|
+
ai.promptTokens += e.promptTokens || 0;
|
|
64
|
+
ai.completionTokens += e.completionTokens || 0;
|
|
65
|
+
ai.totalTokens += e.totalTokens || 0;
|
|
66
|
+
ai.credits += e.creditsCharged || 0;
|
|
67
|
+
ai.costUsd += e.costUsd || 0;
|
|
68
|
+
ai.ms += e.ms || 0;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// front-load hook fired iff its injected header appears in the transcript
|
|
73
|
+
const frontload = lines.some(l => l.includes('auto-retrieved for this question'));
|
|
74
|
+
const get = (n) => toolCounts[n] || 0;
|
|
75
|
+
const read = get('Read');
|
|
76
|
+
const grep = get('Grep') + get('Bash') + get('Glob');
|
|
77
|
+
const explore = get('mcp__homegraph__homegraph_explore');
|
|
78
|
+
const cgAny = Object.keys(toolCounts).filter(k => /mcp__homegraph__/.test(k)).reduce((s, k) => s + toolCounts[k], 0);
|
|
79
|
+
|
|
80
|
+
const out = {
|
|
81
|
+
repo: args.repo, tier: args.tier, arm: args.arm, rep: Number(args.rep), question: args.q,
|
|
82
|
+
ok: result?.subtype === 'success',
|
|
83
|
+
durationSec: result ? +(result.duration_ms / 1000).toFixed(1) : null,
|
|
84
|
+
numTurns: result?.num_turns ?? null,
|
|
85
|
+
costUsdMain: result ? +(result.total_cost_usd || 0).toFixed(4) : null,
|
|
86
|
+
tokGen: tok.gen, tokFresh: tok.fresh, tokCached: tok.cached, tokBillable: tok.gen + tok.fresh,
|
|
87
|
+
read, grep, explore, cgAny, frontload,
|
|
88
|
+
offloadFired: offloadAnswers.length,
|
|
89
|
+
ai,
|
|
90
|
+
// text payloads for the accuracy judge (kept separate; large)
|
|
91
|
+
finalAnswer: (result?.result || lastAssistantText || '').slice(0, 8000),
|
|
92
|
+
offloadAnswers: offloadAnswers.map(a => a.slice(0, 6000)),
|
|
93
|
+
};
|
|
94
|
+
process.stdout.write(JSON.stringify(out) + '\n');
|