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.
Files changed (175) hide show
  1. package/LICENSE +21 -21
  2. package/README.md +243 -243
  3. package/dist/bin/homegraph.js +371 -9
  4. package/dist/bin/homegraph.js.map +1 -1
  5. package/dist/db/migrations.js +18 -18
  6. package/dist/db/queries.js +140 -140
  7. package/dist/db/schema.sql +152 -152
  8. package/dist/directory.js +5 -5
  9. package/dist/extraction/wasm/tree-sitter-c_sharp.wasm +0 -0
  10. package/dist/extraction/wasm/tree-sitter-pascal.wasm +0 -0
  11. package/dist/installer/instructions-template.js +9 -9
  12. package/dist/installer/targets/shared.d.ts +5 -6
  13. package/dist/installer/targets/shared.d.ts.map +1 -1
  14. package/dist/installer/targets/shared.js +5 -6
  15. package/dist/installer/targets/shared.js.map +1 -1
  16. package/dist/mcp/liveness-watchdog.js +16 -16
  17. package/dist/mcp/server-instructions.js +69 -69
  18. package/dist/mcp/tools.d.ts +17 -4
  19. package/dist/mcp/tools.d.ts.map +1 -1
  20. package/dist/mcp/tools.js +192 -22
  21. package/dist/mcp/tools.js.map +1 -1
  22. package/dist/reasoning/reasoner.js +32 -32
  23. package/dist/spec/config.d.ts +43 -0
  24. package/dist/spec/config.d.ts.map +1 -0
  25. package/dist/spec/config.js +275 -0
  26. package/dist/spec/config.js.map +1 -0
  27. package/dist/spec/db/commit-node.d.ts +23 -0
  28. package/dist/spec/db/commit-node.d.ts.map +1 -0
  29. package/dist/spec/db/commit-node.js +62 -0
  30. package/dist/spec/db/commit-node.js.map +1 -0
  31. package/dist/spec/db/fragment-node.d.ts +23 -0
  32. package/dist/spec/db/fragment-node.d.ts.map +1 -0
  33. package/dist/spec/db/fragment-node.js +120 -0
  34. package/dist/spec/db/fragment-node.js.map +1 -0
  35. package/dist/spec/db/fts.d.ts +60 -0
  36. package/dist/spec/db/fts.d.ts.map +1 -0
  37. package/dist/spec/db/fts.js +285 -0
  38. package/dist/spec/db/fts.js.map +1 -0
  39. package/dist/spec/db/index.d.ts +13 -0
  40. package/dist/spec/db/index.d.ts.map +1 -0
  41. package/dist/spec/db/index.js +50 -0
  42. package/dist/spec/db/index.js.map +1 -0
  43. package/dist/spec/db/relations.d.ts +55 -0
  44. package/dist/spec/db/relations.d.ts.map +1 -0
  45. package/dist/spec/db/relations.js +158 -0
  46. package/dist/spec/db/relations.js.map +1 -0
  47. package/dist/spec/db/schema.d.ts +30 -0
  48. package/dist/spec/db/schema.d.ts.map +1 -0
  49. package/dist/spec/db/schema.js +87 -0
  50. package/dist/spec/db/schema.js.map +1 -0
  51. package/dist/spec/db/schema.sql +107 -0
  52. package/dist/spec/db/spec-node.d.ts +41 -0
  53. package/dist/spec/db/spec-node.d.ts.map +1 -0
  54. package/dist/spec/db/spec-node.js +114 -0
  55. package/dist/spec/db/spec-node.js.map +1 -0
  56. package/dist/spec/evolve/impact-locator.d.ts +13 -0
  57. package/dist/spec/evolve/impact-locator.d.ts.map +1 -0
  58. package/dist/spec/evolve/impact-locator.js +25 -0
  59. package/dist/spec/evolve/impact-locator.js.map +1 -0
  60. package/dist/spec/evolve/llm-client.d.ts +50 -0
  61. package/dist/spec/evolve/llm-client.d.ts.map +1 -0
  62. package/dist/spec/evolve/llm-client.js +176 -0
  63. package/dist/spec/evolve/llm-client.js.map +1 -0
  64. package/dist/spec/evolve/logic-checker.d.ts +12 -0
  65. package/dist/spec/evolve/logic-checker.d.ts.map +1 -0
  66. package/dist/spec/evolve/logic-checker.js +48 -0
  67. package/dist/spec/evolve/logic-checker.js.map +1 -0
  68. package/dist/spec/evolve/pipeline.d.ts +40 -0
  69. package/dist/spec/evolve/pipeline.d.ts.map +1 -0
  70. package/dist/spec/evolve/pipeline.js +314 -0
  71. package/dist/spec/evolve/pipeline.js.map +1 -0
  72. package/dist/spec/evolve/spec-rewriter.d.ts +42 -0
  73. package/dist/spec/evolve/spec-rewriter.d.ts.map +1 -0
  74. package/dist/spec/evolve/spec-rewriter.js +254 -0
  75. package/dist/spec/evolve/spec-rewriter.js.map +1 -0
  76. package/dist/spec/graph/queries.d.ts +58 -0
  77. package/dist/spec/graph/queries.d.ts.map +1 -0
  78. package/dist/spec/graph/queries.js +215 -0
  79. package/dist/spec/graph/queries.js.map +1 -0
  80. package/dist/spec/mining/diff-parser.d.ts +33 -0
  81. package/dist/spec/mining/diff-parser.d.ts.map +1 -0
  82. package/dist/spec/mining/diff-parser.js +166 -0
  83. package/dist/spec/mining/diff-parser.js.map +1 -0
  84. package/dist/spec/mining/git-scanner.d.ts +91 -0
  85. package/dist/spec/mining/git-scanner.d.ts.map +1 -0
  86. package/dist/spec/mining/git-scanner.js +266 -0
  87. package/dist/spec/mining/git-scanner.js.map +1 -0
  88. package/dist/spec/mining/pipeline.d.ts +53 -0
  89. package/dist/spec/mining/pipeline.d.ts.map +1 -0
  90. package/dist/spec/mining/pipeline.js +165 -0
  91. package/dist/spec/mining/pipeline.js.map +1 -0
  92. package/dist/spec/mining/scope-resolver.d.ts +45 -0
  93. package/dist/spec/mining/scope-resolver.d.ts.map +1 -0
  94. package/dist/spec/mining/scope-resolver.js +103 -0
  95. package/dist/spec/mining/scope-resolver.js.map +1 -0
  96. package/dist/spec/mining/spec-extractor.d.ts +69 -0
  97. package/dist/spec/mining/spec-extractor.d.ts.map +1 -0
  98. package/dist/spec/mining/spec-extractor.js +369 -0
  99. package/dist/spec/mining/spec-extractor.js.map +1 -0
  100. package/dist/spec/types.d.ts +149 -0
  101. package/dist/spec/types.d.ts.map +1 -0
  102. package/dist/spec/types.js +15 -0
  103. package/dist/spec/types.js.map +1 -0
  104. package/dist/spec/utils.d.ts +166 -0
  105. package/dist/spec/utils.d.ts.map +1 -0
  106. package/dist/spec/utils.js +461 -0
  107. package/dist/spec/utils.js.map +1 -0
  108. package/package.json +58 -57
  109. package/scripts/add-lang/bench.sh +60 -60
  110. package/scripts/add-lang/check-grammar.mjs +75 -75
  111. package/scripts/add-lang/dump-ast.mjs +103 -103
  112. package/scripts/add-lang/verify-extraction.mjs +70 -70
  113. package/scripts/agent-eval/ab-adoption.sh +91 -91
  114. package/scripts/agent-eval/ab-hook.sh +86 -86
  115. package/scripts/agent-eval/ab-impl.sh +78 -78
  116. package/scripts/agent-eval/ab-new-vs-baseline.sh +102 -102
  117. package/scripts/agent-eval/ab-sufficiency.sh +78 -78
  118. package/scripts/agent-eval/arms-F.sh +21 -21
  119. package/scripts/agent-eval/arms-matrix.sh +37 -37
  120. package/scripts/agent-eval/audit.sh +68 -68
  121. package/scripts/agent-eval/bench-readme.sh +28 -28
  122. package/scripts/agent-eval/bench-why-repo.sh +22 -22
  123. package/scripts/agent-eval/block-read-hook.sh +19 -19
  124. package/scripts/agent-eval/hook-settings.json +15 -15
  125. package/scripts/agent-eval/itrun.sh +120 -120
  126. package/scripts/agent-eval/offload-eval-3arm.sh +72 -72
  127. package/scripts/agent-eval/offload-eval-cost.mjs +133 -133
  128. package/scripts/agent-eval/offload-eval-effort.mjs +108 -108
  129. package/scripts/agent-eval/offload-eval-frontload-matrix.sh +25 -25
  130. package/scripts/agent-eval/offload-eval-frontload.sh +47 -47
  131. package/scripts/agent-eval/offload-eval-ground-truth.json +18 -18
  132. package/scripts/agent-eval/offload-eval-hook.mjs +84 -84
  133. package/scripts/agent-eval/offload-eval-judge.mjs +103 -103
  134. package/scripts/agent-eval/offload-eval-matrix.sh +20 -20
  135. package/scripts/agent-eval/offload-eval-metrics.mjs +94 -94
  136. package/scripts/agent-eval/offload-eval-refs1.sh +50 -50
  137. package/scripts/agent-eval/offload-eval-setup.sh +24 -24
  138. package/scripts/agent-eval/offload-eval-styles.sh +71 -71
  139. package/scripts/agent-eval/offload-eval-summarize.mjs +68 -68
  140. package/scripts/agent-eval/offload-eval.md +76 -76
  141. package/scripts/agent-eval/parse-arms.mjs +116 -116
  142. package/scripts/agent-eval/parse-bench-readme.mjs +84 -84
  143. package/scripts/agent-eval/parse-run.mjs +45 -45
  144. package/scripts/agent-eval/parse-session.mjs +93 -93
  145. package/scripts/agent-eval/probe-context.mjs +21 -21
  146. package/scripts/agent-eval/probe-explore.mjs +40 -40
  147. package/scripts/agent-eval/probe-node.mjs +20 -20
  148. package/scripts/agent-eval/probe-sweep.mjs +119 -119
  149. package/scripts/agent-eval/probe-trace.mjs +20 -20
  150. package/scripts/agent-eval/redirect-read-hook.sh +38 -38
  151. package/scripts/agent-eval/run-agent.sh +34 -34
  152. package/scripts/agent-eval/run-all.sh +69 -69
  153. package/scripts/agent-eval/run-arms.sh +56 -56
  154. package/scripts/agent-eval/seq-matrix.mjs +137 -137
  155. package/scripts/build-bundle.sh +118 -118
  156. package/scripts/extract-release-notes.mjs +130 -130
  157. package/scripts/local-install.sh +41 -41
  158. package/scripts/npm-sdk.js +75 -75
  159. package/scripts/npm-shim.js +246 -246
  160. package/scripts/pack-npm.sh +119 -119
  161. package/scripts/prepare-release.mjs +270 -270
  162. package/scripts/qa_eval/README.md +407 -404
  163. package/scripts/qa_eval/_test_deveco_probe.py +41 -41
  164. package/scripts/qa_eval/agent_runner.py +526 -526
  165. package/scripts/qa_eval/data/.gitignore +4 -4
  166. package/scripts/qa_eval/data/test-set.jsonl +2 -22
  167. package/scripts/qa_eval/eval_metrics.py +274 -233
  168. package/scripts/qa_eval/external_agent.py +976 -671
  169. package/scripts/qa_eval/llm_config.py +92 -92
  170. package/scripts/qa_eval/memory_monitor.py +132 -132
  171. package/scripts/qa_eval/my_answer_accuracy.py +187 -187
  172. package/scripts/qa_eval/requirements.txt +2 -2
  173. package/scripts/qa_eval/run_pipeline.py +804 -711
  174. package/scripts/qa_eval/stats_efficiency.py +279 -279
  175. package/scripts/qa_eval/stats_scores.py +207 -207
@@ -1,116 +1,116 @@
1
- #!/usr/bin/env node
2
- // Analyze the tool-surface ablation (/tmp/arms/<repo>/<arm>-r<n>.jsonl).
3
- // Compares arms A–E on trace adoption, Read/Grep fallback, homegraph payload,
4
- // round-trips, and duration — averaged across runs per arm.
5
- //
6
- // The decisive signal is READS: if removing a tool raises Reads on a question
7
- // class, that tool was load-bearing for it (not redundant). If removing it
8
- // changes nothing, it was redundant.
9
- //
10
- // A control all tools no steering (baseline)
11
- // B steer all tools trace-first (adoption)
12
- // C no-explore hide explore trace-first (is explore redundant?)
13
- // D trace-centric hide explore+context trace-first (is the survey pair redundant?)
14
- // E control-probe hide explore+context trace-first (NON-flow Q — should degrade)
15
- //
16
- // Usage: node scripts/agent-eval/parse-arms.mjs [/tmp/arms]
17
- import { readFileSync, readdirSync, existsSync, statSync } from 'fs';
18
- import { join } from 'path';
19
-
20
- const ROOT = process.argv[2] || '/tmp/arms';
21
- const cgShort = (n) => n.replace('mcp__homegraph__homegraph_', '').replace('mcp__homegraph__', '');
22
-
23
- function parse(file) {
24
- if (!existsSync(file)) return null;
25
- const lines = readFileSync(file, 'utf8').split('\n').filter(Boolean);
26
- const calls = []; let result = null, initCg = 0;
27
- for (const l of lines) {
28
- let ev; try { ev = JSON.parse(l); } catch { continue; }
29
- if (ev.type === 'system' && ev.subtype === 'init') initCg = (ev.tools || []).filter(t => /homegraph/.test(t)).length;
30
- if (ev.type === 'assistant') for (const b of (ev.message?.content || [])) if (b.type === 'tool_use')
31
- calls.push({ id: b.id, name: b.name, out: 0 });
32
- if (ev.type === 'user') for (const b of (ev.message?.content || [])) if (b.type === 'tool_result') {
33
- const c = b.content;
34
- const txt = typeof c === 'string' ? c : Array.isArray(c) ? c.map(x => x?.text || '').join('') : '';
35
- const call = calls.find(k => k.id === b.tool_use_id); if (call) call.out = txt.length;
36
- }
37
- if (ev.type === 'result') result = ev;
38
- }
39
- const cg = calls.filter(c => c.name.includes('homegraph'));
40
- return {
41
- initCg,
42
- reads: calls.filter(c => c.name === 'Read').length,
43
- greps: calls.filter(c => c.name === 'Grep').length + calls.filter(c => c.name === 'Glob').length,
44
- cgCalls: cg.length,
45
- cgSeq: cg.map(c => cgShort(c.name)),
46
- cgOut: cg.reduce((s, c) => s + c.out, 0),
47
- traceUsed: cg.some(c => c.name.includes('trace')),
48
- turns: result?.num_turns ?? null,
49
- dur: result?.duration_ms ? Math.round(result.duration_ms / 1000) : null,
50
- cost: result?.total_cost_usd || 0,
51
- ok: result?.subtype === 'success',
52
- };
53
- }
54
-
55
- // repo -> arm -> [runs]
56
- const data = {};
57
- if (!existsSync(ROOT)) { console.error(`no ${ROOT}`); process.exit(1); }
58
- for (const repo of readdirSync(ROOT)) {
59
- const rdir = join(ROOT, repo);
60
- if (!statSync(rdir).isDirectory()) continue;
61
- for (const f of readdirSync(rdir)) {
62
- const m = f.match(/^([A-I])-r(\d+)\.jsonl$/); if (!m) continue;
63
- const p = parse(join(rdir, f)); if (!p || !p.ok) continue;
64
- (((data[repo] ??= {})[m[1]]) ??= []).push(p);
65
- }
66
- }
67
-
68
- const avg = (a, f) => a.length ? a.reduce((s, x) => s + (f(x) || 0), 0) / a.length : 0;
69
- const k = (n) => (n / 1000).toFixed(1);
70
- const pad = (s, n) => String(s).padEnd(n);
71
- const ARMS = ['A', 'H', 'I', 'B', 'F', 'G', 'C', 'D', 'E'];
72
- const LABEL = { A: 'A all/none(old)', H: 'H body-trace/none', I: 'I bodytrace+dest', B: 'B all/steer(thin)', F: 'F all/steer(body)', G: 'G ported(noprompt)', C: 'C no-explore', D: 'D trace-centric', E: 'E nonflow-probe' };
73
-
74
- // ---- per repo × arm ----
75
- console.log('\n=== PER REPO × ARM (avg over runs) ===');
76
- console.log(pad('repo', 22), pad('arm', 16), 'tools', 'trace', pad('reads', 6), pad('cgOutK', 7), pad('turns', 6), 'dur');
77
- for (const repo of Object.keys(data).sort()) {
78
- for (const arm of ARMS) {
79
- const runs = data[repo][arm]; if (!runs?.length) continue;
80
- console.log(
81
- pad(repo, 22), pad(LABEL[arm], 16),
82
- pad(runs[0].initCg, 5),
83
- pad(runs.filter(r => r.traceUsed).length + '/' + runs.length, 5),
84
- pad(avg(runs, r => r.reads).toFixed(1), 6),
85
- pad(k(avg(runs, r => r.cgOut)), 7),
86
- pad(avg(runs, r => r.turns).toFixed(1), 6),
87
- avg(runs, r => r.dur).toFixed(0) + 's',
88
- );
89
- }
90
- }
91
-
92
- // ---- aggregate per arm (flow arms A–D over the flow repos; E shown apart) ----
93
- console.log('\n=== AGGREGATE PER ARM (mean across repos) ===');
94
- console.log(pad('arm', 16), pad('adoption', 9), pad('reads', 7), pad('greps', 7), pad('cgOutK', 8), pad('turns', 7), pad('dur', 6), 'cost');
95
- for (const arm of ARMS) {
96
- const all = [];
97
- for (const repo of Object.keys(data)) for (const r of (data[repo][arm] || [])) all.push({ ...r, repo });
98
- if (!all.length) continue;
99
- const repos = new Set(all.map(r => r.repo)).size;
100
- const adopt = all.filter(r => r.traceUsed).length;
101
- console.log(
102
- pad(LABEL[arm], 16),
103
- pad(`${adopt}/${all.length}`, 9),
104
- pad(avg(all, r => r.reads).toFixed(2), 7),
105
- pad(avg(all, r => r.greps).toFixed(2), 7),
106
- pad(k(avg(all, r => r.cgOut)), 8),
107
- pad(avg(all, r => r.turns).toFixed(1), 7),
108
- pad(avg(all, r => r.dur).toFixed(0) + 's', 6),
109
- '$' + avg(all, r => r.cost).toFixed(3),
110
- ` (${repos} repos)`,
111
- );
112
- }
113
-
114
- console.log('\nRead the signal: B vs A = does steering alone fix adoption + cut payload.');
115
- console.log('C vs B = is explore redundant (reads should NOT jump). D vs C = is context redundant.');
116
- console.log('E = non-flow under trace-centric; reads SHOULD jump (proves survey tools are load-bearing).');
1
+ #!/usr/bin/env node
2
+ // Analyze the tool-surface ablation (/tmp/arms/<repo>/<arm>-r<n>.jsonl).
3
+ // Compares arms A–E on trace adoption, Read/Grep fallback, homegraph payload,
4
+ // round-trips, and duration — averaged across runs per arm.
5
+ //
6
+ // The decisive signal is READS: if removing a tool raises Reads on a question
7
+ // class, that tool was load-bearing for it (not redundant). If removing it
8
+ // changes nothing, it was redundant.
9
+ //
10
+ // A control all tools no steering (baseline)
11
+ // B steer all tools trace-first (adoption)
12
+ // C no-explore hide explore trace-first (is explore redundant?)
13
+ // D trace-centric hide explore+context trace-first (is the survey pair redundant?)
14
+ // E control-probe hide explore+context trace-first (NON-flow Q — should degrade)
15
+ //
16
+ // Usage: node scripts/agent-eval/parse-arms.mjs [/tmp/arms]
17
+ import { readFileSync, readdirSync, existsSync, statSync } from 'fs';
18
+ import { join } from 'path';
19
+
20
+ const ROOT = process.argv[2] || '/tmp/arms';
21
+ const cgShort = (n) => n.replace('mcp__homegraph__homegraph_', '').replace('mcp__homegraph__', '');
22
+
23
+ function parse(file) {
24
+ if (!existsSync(file)) return null;
25
+ const lines = readFileSync(file, 'utf8').split('\n').filter(Boolean);
26
+ const calls = []; let result = null, initCg = 0;
27
+ for (const l of lines) {
28
+ let ev; try { ev = JSON.parse(l); } catch { continue; }
29
+ if (ev.type === 'system' && ev.subtype === 'init') initCg = (ev.tools || []).filter(t => /homegraph/.test(t)).length;
30
+ if (ev.type === 'assistant') for (const b of (ev.message?.content || [])) if (b.type === 'tool_use')
31
+ calls.push({ id: b.id, name: b.name, out: 0 });
32
+ if (ev.type === 'user') for (const b of (ev.message?.content || [])) if (b.type === 'tool_result') {
33
+ const c = b.content;
34
+ const txt = typeof c === 'string' ? c : Array.isArray(c) ? c.map(x => x?.text || '').join('') : '';
35
+ const call = calls.find(k => k.id === b.tool_use_id); if (call) call.out = txt.length;
36
+ }
37
+ if (ev.type === 'result') result = ev;
38
+ }
39
+ const cg = calls.filter(c => c.name.includes('homegraph'));
40
+ return {
41
+ initCg,
42
+ reads: calls.filter(c => c.name === 'Read').length,
43
+ greps: calls.filter(c => c.name === 'Grep').length + calls.filter(c => c.name === 'Glob').length,
44
+ cgCalls: cg.length,
45
+ cgSeq: cg.map(c => cgShort(c.name)),
46
+ cgOut: cg.reduce((s, c) => s + c.out, 0),
47
+ traceUsed: cg.some(c => c.name.includes('trace')),
48
+ turns: result?.num_turns ?? null,
49
+ dur: result?.duration_ms ? Math.round(result.duration_ms / 1000) : null,
50
+ cost: result?.total_cost_usd || 0,
51
+ ok: result?.subtype === 'success',
52
+ };
53
+ }
54
+
55
+ // repo -> arm -> [runs]
56
+ const data = {};
57
+ if (!existsSync(ROOT)) { console.error(`no ${ROOT}`); process.exit(1); }
58
+ for (const repo of readdirSync(ROOT)) {
59
+ const rdir = join(ROOT, repo);
60
+ if (!statSync(rdir).isDirectory()) continue;
61
+ for (const f of readdirSync(rdir)) {
62
+ const m = f.match(/^([A-I])-r(\d+)\.jsonl$/); if (!m) continue;
63
+ const p = parse(join(rdir, f)); if (!p || !p.ok) continue;
64
+ (((data[repo] ??= {})[m[1]]) ??= []).push(p);
65
+ }
66
+ }
67
+
68
+ const avg = (a, f) => a.length ? a.reduce((s, x) => s + (f(x) || 0), 0) / a.length : 0;
69
+ const k = (n) => (n / 1000).toFixed(1);
70
+ const pad = (s, n) => String(s).padEnd(n);
71
+ const ARMS = ['A', 'H', 'I', 'B', 'F', 'G', 'C', 'D', 'E'];
72
+ const LABEL = { A: 'A all/none(old)', H: 'H body-trace/none', I: 'I bodytrace+dest', B: 'B all/steer(thin)', F: 'F all/steer(body)', G: 'G ported(noprompt)', C: 'C no-explore', D: 'D trace-centric', E: 'E nonflow-probe' };
73
+
74
+ // ---- per repo × arm ----
75
+ console.log('\n=== PER REPO × ARM (avg over runs) ===');
76
+ console.log(pad('repo', 22), pad('arm', 16), 'tools', 'trace', pad('reads', 6), pad('cgOutK', 7), pad('turns', 6), 'dur');
77
+ for (const repo of Object.keys(data).sort()) {
78
+ for (const arm of ARMS) {
79
+ const runs = data[repo][arm]; if (!runs?.length) continue;
80
+ console.log(
81
+ pad(repo, 22), pad(LABEL[arm], 16),
82
+ pad(runs[0].initCg, 5),
83
+ pad(runs.filter(r => r.traceUsed).length + '/' + runs.length, 5),
84
+ pad(avg(runs, r => r.reads).toFixed(1), 6),
85
+ pad(k(avg(runs, r => r.cgOut)), 7),
86
+ pad(avg(runs, r => r.turns).toFixed(1), 6),
87
+ avg(runs, r => r.dur).toFixed(0) + 's',
88
+ );
89
+ }
90
+ }
91
+
92
+ // ---- aggregate per arm (flow arms A–D over the flow repos; E shown apart) ----
93
+ console.log('\n=== AGGREGATE PER ARM (mean across repos) ===');
94
+ console.log(pad('arm', 16), pad('adoption', 9), pad('reads', 7), pad('greps', 7), pad('cgOutK', 8), pad('turns', 7), pad('dur', 6), 'cost');
95
+ for (const arm of ARMS) {
96
+ const all = [];
97
+ for (const repo of Object.keys(data)) for (const r of (data[repo][arm] || [])) all.push({ ...r, repo });
98
+ if (!all.length) continue;
99
+ const repos = new Set(all.map(r => r.repo)).size;
100
+ const adopt = all.filter(r => r.traceUsed).length;
101
+ console.log(
102
+ pad(LABEL[arm], 16),
103
+ pad(`${adopt}/${all.length}`, 9),
104
+ pad(avg(all, r => r.reads).toFixed(2), 7),
105
+ pad(avg(all, r => r.greps).toFixed(2), 7),
106
+ pad(k(avg(all, r => r.cgOut)), 8),
107
+ pad(avg(all, r => r.turns).toFixed(1), 7),
108
+ pad(avg(all, r => r.dur).toFixed(0) + 's', 6),
109
+ '$' + avg(all, r => r.cost).toFixed(3),
110
+ ` (${repos} repos)`,
111
+ );
112
+ }
113
+
114
+ console.log('\nRead the signal: B vs A = does steering alone fix adoption + cut payload.');
115
+ console.log('C vs B = is explore redundant (reads should NOT jump). D vs C = is context redundant.');
116
+ console.log('E = non-flow under trace-centric; reads SHOULD jump (proves survey tools are load-bearing).');
@@ -1,84 +1,84 @@
1
- #!/usr/bin/env node
2
- // Aggregate the README A/B (bench-readme.sh output): per repo, median of N runs
3
- // per arm → time, tool calls, tokens, cost, and % saved. Plus an average row.
4
- //
5
- // Tokens = SUM of per-turn assistant `usage` (input + output + cache read +
6
- // cache creation) — the cumulative "total tokens processed". NOTE: `result.usage`
7
- // is last-turn-only in current Claude Code, so it under-counts badly; don't use it.
8
- // `total_cost_usd` and `duration_ms` are already cumulative.
9
- //
10
- // Usage: node parse-bench-readme.mjs [/tmp/ab-readme]
11
- import { readFileSync, existsSync, readdirSync } from 'fs';
12
- import { join } from 'path';
13
- const ROOT = process.argv[2] || '/tmp/ab-readme';
14
- const REPOS = ['vscode', 'excalidraw', 'django', 'tokio', 'okhttp', 'gin', 'alamofire'];
15
-
16
- function parse(file) {
17
- if (!existsSync(file)) return null;
18
- const L = readFileSync(file, 'utf8').split('\n').filter(Boolean);
19
- let tools = 0, reads = 0, grep = 0, cg = 0, tokens = 0, r = null, raced = false;
20
- for (const l of L) { let e; try { e = JSON.parse(l); } catch { continue; }
21
- if (e.type === 'assistant') {
22
- const u = e.message?.usage;
23
- if (u) tokens += (u.input_tokens || 0) + (u.output_tokens || 0) + (u.cache_read_input_tokens || 0) + (u.cache_creation_input_tokens || 0);
24
- for (const b of (e.message?.content || [])) if (b.type === 'tool_use') {
25
- const n = b.name;
26
- if (n === 'ToolSearch') continue;
27
- tools++;
28
- if (n === 'Read') reads++;
29
- else if (n === 'Grep' || n === 'Glob') grep++;
30
- else if (/homegraph/.test(n)) cg++;
31
- }
32
- }
33
- // MCP cold-start race: the headless agent fired before `homegraph serve --mcp`
34
- // finished registering its tools, so early calls returned "No such tool
35
- // available" and the agent floundered into grep/Read. That measures HomeGraph's
36
- // startup latency, NOT its steady-state value — flag the run so the aggregate
37
- // can exclude it (an artifact of headless first-turn timing, not the tool).
38
- if (e.type === 'user') for (const b of (Array.isArray(e.message?.content) ? e.message.content : [])) {
39
- if (b.type === 'tool_result') {
40
- const t = Array.isArray(b.content) ? b.content.map(c => c.text || '').join('') : (b.content || '');
41
- if (/No such tool available/.test(t)) raced = true;
42
- }
43
- }
44
- if (e.type === 'result') r = e;
45
- }
46
- if (!r || r.subtype !== 'success') return null;
47
- return { dur: r.duration_ms / 1000, tools, reads, grep, cg, tokens, cost: r.total_cost_usd || 0, raced };
48
- }
49
- const median = (arr) => { const v = [...arr].sort((a, b) => a - b); const n = v.length; return n === 0 ? 0 : n % 2 ? v[(n - 1) / 2] : (v[n / 2 - 1] + v[n / 2]) / 2; };
50
- const fmtTime = (s) => s >= 60 ? `${Math.floor(s / 60)}m ${Math.round(s % 60)}s` : `${Math.round(s)}s`;
51
- const fmtTok = (t) => t >= 1e6 ? `${(t / 1e6).toFixed(1)}M` : `${Math.round(t / 1000)}k`;
52
- const pct = (w, wo) => wo > 0 ? Math.round((1 - w / wo) * 100) : 0;
53
-
54
- console.log('repo n(w/wo) time WITH→WITHOUT tools W→WO tokens W→WO (saved) cost W→WO (saved)');
55
- const savings = { cost: [], tokens: [], time: [], tools: [] };
56
- for (const repo of REPOS) {
57
- const dir = join(ROOT, repo);
58
- const runDirs = existsSync(dir) ? readdirSync(dir).filter(d => /^run\d+$/.test(d)) : [];
59
- // Exclude MCP-cold-start-raced WITH runs by default — they measure a startup
60
- // race, not steady-state value. `CG_INCLUDE_RACED=1` keeps them (to see the raw
61
- // distribution). The WITHOUT arm has no MCP, so it's never raced.
62
- const includeRaced = process.env.CG_INCLUDE_RACED === '1';
63
- const W = [], WO = []; let racedExcluded = 0;
64
- for (const rd of runDirs) {
65
- const w = parse(join(dir, rd, 'run-headless-with.jsonl'));
66
- if (w) { if (w.raced && !includeRaced) racedExcluded++; else W.push(w); }
67
- const wo = parse(join(dir, rd, 'run-headless-without.jsonl')); if (wo) WO.push(wo);
68
- }
69
- if (!W.length || !WO.length) { console.log(`${repo.padEnd(11)} (incomplete: w=${W.length} wo=${WO.length})`); continue; }
70
- const m = (arr, k) => median(arr.map(x => x[k]));
71
- const wT = m(W, 'dur'), woT = m(WO, 'dur'), wTok = m(W, 'tokens'), woTok = m(WO, 'tokens');
72
- const wC = m(W, 'cost'), woC = m(WO, 'cost'), wTl = m(W, 'tools'), woTl = m(WO, 'tools');
73
- savings.time.push(pct(wT, woT)); savings.tokens.push(pct(wTok, woTok)); savings.cost.push(pct(wC, woC)); savings.tools.push(pct(wTl, woTl));
74
- console.log(
75
- `${repo.padEnd(11)} ${W.length}/${WO.length} ` +
76
- `${(fmtTime(wT) + '→' + fmtTime(woT)).padEnd(22)}` +
77
- `${(Math.round(wTl) + '→' + Math.round(woTl)).padEnd(12)}` +
78
- `${(fmtTok(wTok) + '→' + fmtTok(woTok) + ' (' + pct(wTok, woTok) + '%)').padEnd(24)}` +
79
- `$${wC.toFixed(2)}→$${woC.toFixed(2)} (${pct(wC, woC)}%)` +
80
- (racedExcluded ? ` [${racedExcluded} raced run${racedExcluded === 1 ? '' : 's'} excluded]` : '')
81
- );
82
- }
83
- const avg = (a) => a.length ? Math.round(a.reduce((s, x) => s + x, 0) / a.length) : 0;
84
- console.log(`\nAVERAGE saved: cost ${avg(savings.cost)}% · tokens ${avg(savings.tokens)}% · time ${avg(savings.time)}% · tool calls ${avg(savings.tools)}%`);
1
+ #!/usr/bin/env node
2
+ // Aggregate the README A/B (bench-readme.sh output): per repo, median of N runs
3
+ // per arm → time, tool calls, tokens, cost, and % saved. Plus an average row.
4
+ //
5
+ // Tokens = SUM of per-turn assistant `usage` (input + output + cache read +
6
+ // cache creation) — the cumulative "total tokens processed". NOTE: `result.usage`
7
+ // is last-turn-only in current Claude Code, so it under-counts badly; don't use it.
8
+ // `total_cost_usd` and `duration_ms` are already cumulative.
9
+ //
10
+ // Usage: node parse-bench-readme.mjs [/tmp/ab-readme]
11
+ import { readFileSync, existsSync, readdirSync } from 'fs';
12
+ import { join } from 'path';
13
+ const ROOT = process.argv[2] || '/tmp/ab-readme';
14
+ const REPOS = ['vscode', 'excalidraw', 'django', 'tokio', 'okhttp', 'gin', 'alamofire'];
15
+
16
+ function parse(file) {
17
+ if (!existsSync(file)) return null;
18
+ const L = readFileSync(file, 'utf8').split('\n').filter(Boolean);
19
+ let tools = 0, reads = 0, grep = 0, cg = 0, tokens = 0, r = null, raced = false;
20
+ for (const l of L) { let e; try { e = JSON.parse(l); } catch { continue; }
21
+ if (e.type === 'assistant') {
22
+ const u = e.message?.usage;
23
+ if (u) tokens += (u.input_tokens || 0) + (u.output_tokens || 0) + (u.cache_read_input_tokens || 0) + (u.cache_creation_input_tokens || 0);
24
+ for (const b of (e.message?.content || [])) if (b.type === 'tool_use') {
25
+ const n = b.name;
26
+ if (n === 'ToolSearch') continue;
27
+ tools++;
28
+ if (n === 'Read') reads++;
29
+ else if (n === 'Grep' || n === 'Glob') grep++;
30
+ else if (/homegraph/.test(n)) cg++;
31
+ }
32
+ }
33
+ // MCP cold-start race: the headless agent fired before `homegraph serve --mcp`
34
+ // finished registering its tools, so early calls returned "No such tool
35
+ // available" and the agent floundered into grep/Read. That measures HomeGraph's
36
+ // startup latency, NOT its steady-state value — flag the run so the aggregate
37
+ // can exclude it (an artifact of headless first-turn timing, not the tool).
38
+ if (e.type === 'user') for (const b of (Array.isArray(e.message?.content) ? e.message.content : [])) {
39
+ if (b.type === 'tool_result') {
40
+ const t = Array.isArray(b.content) ? b.content.map(c => c.text || '').join('') : (b.content || '');
41
+ if (/No such tool available/.test(t)) raced = true;
42
+ }
43
+ }
44
+ if (e.type === 'result') r = e;
45
+ }
46
+ if (!r || r.subtype !== 'success') return null;
47
+ return { dur: r.duration_ms / 1000, tools, reads, grep, cg, tokens, cost: r.total_cost_usd || 0, raced };
48
+ }
49
+ const median = (arr) => { const v = [...arr].sort((a, b) => a - b); const n = v.length; return n === 0 ? 0 : n % 2 ? v[(n - 1) / 2] : (v[n / 2 - 1] + v[n / 2]) / 2; };
50
+ const fmtTime = (s) => s >= 60 ? `${Math.floor(s / 60)}m ${Math.round(s % 60)}s` : `${Math.round(s)}s`;
51
+ const fmtTok = (t) => t >= 1e6 ? `${(t / 1e6).toFixed(1)}M` : `${Math.round(t / 1000)}k`;
52
+ const pct = (w, wo) => wo > 0 ? Math.round((1 - w / wo) * 100) : 0;
53
+
54
+ console.log('repo n(w/wo) time WITH→WITHOUT tools W→WO tokens W→WO (saved) cost W→WO (saved)');
55
+ const savings = { cost: [], tokens: [], time: [], tools: [] };
56
+ for (const repo of REPOS) {
57
+ const dir = join(ROOT, repo);
58
+ const runDirs = existsSync(dir) ? readdirSync(dir).filter(d => /^run\d+$/.test(d)) : [];
59
+ // Exclude MCP-cold-start-raced WITH runs by default — they measure a startup
60
+ // race, not steady-state value. `CG_INCLUDE_RACED=1` keeps them (to see the raw
61
+ // distribution). The WITHOUT arm has no MCP, so it's never raced.
62
+ const includeRaced = process.env.CG_INCLUDE_RACED === '1';
63
+ const W = [], WO = []; let racedExcluded = 0;
64
+ for (const rd of runDirs) {
65
+ const w = parse(join(dir, rd, 'run-headless-with.jsonl'));
66
+ if (w) { if (w.raced && !includeRaced) racedExcluded++; else W.push(w); }
67
+ const wo = parse(join(dir, rd, 'run-headless-without.jsonl')); if (wo) WO.push(wo);
68
+ }
69
+ if (!W.length || !WO.length) { console.log(`${repo.padEnd(11)} (incomplete: w=${W.length} wo=${WO.length})`); continue; }
70
+ const m = (arr, k) => median(arr.map(x => x[k]));
71
+ const wT = m(W, 'dur'), woT = m(WO, 'dur'), wTok = m(W, 'tokens'), woTok = m(WO, 'tokens');
72
+ const wC = m(W, 'cost'), woC = m(WO, 'cost'), wTl = m(W, 'tools'), woTl = m(WO, 'tools');
73
+ savings.time.push(pct(wT, woT)); savings.tokens.push(pct(wTok, woTok)); savings.cost.push(pct(wC, woC)); savings.tools.push(pct(wTl, woTl));
74
+ console.log(
75
+ `${repo.padEnd(11)} ${W.length}/${WO.length} ` +
76
+ `${(fmtTime(wT) + '→' + fmtTime(woT)).padEnd(22)}` +
77
+ `${(Math.round(wTl) + '→' + Math.round(woTl)).padEnd(12)}` +
78
+ `${(fmtTok(wTok) + '→' + fmtTok(woTok) + ' (' + pct(wTok, woTok) + '%)').padEnd(24)}` +
79
+ `$${wC.toFixed(2)}→$${woC.toFixed(2)} (${pct(wC, woC)}%)` +
80
+ (racedExcluded ? ` [${racedExcluded} raced run${racedExcluded === 1 ? '' : 's'} excluded]` : '')
81
+ );
82
+ }
83
+ const avg = (a) => a.length ? Math.round(a.reduce((s, x) => s + x, 0) / a.length) : 0;
84
+ console.log(`\nAVERAGE saved: cost ${avg(savings.cost)}% · tokens ${avg(savings.tokens)}% · time ${avg(savings.time)}% · tool calls ${avg(savings.tools)}%`);
@@ -1,45 +1,45 @@
1
- #!/usr/bin/env node
2
- // Parse a Claude Code stream-json run log: tool-call sequence + token usage.
3
- import { readFileSync } from 'fs';
4
- const file = process.argv[2];
5
- const lines = readFileSync(file, 'utf8').split('\n').filter(Boolean);
6
-
7
- const toolCalls = [];
8
- let result = null;
9
- let initTools = null;
10
-
11
- for (const line of lines) {
12
- let ev;
13
- try { ev = JSON.parse(line); } catch { continue; }
14
- if (ev.type === 'system' && ev.subtype === 'init') {
15
- initTools = (ev.tools || []).filter(t => /homegraph/.test(t));
16
- }
17
- if (ev.type === 'assistant' && ev.message?.content) {
18
- for (const block of ev.message.content) {
19
- if (block.type === 'tool_use') {
20
- let detail = '';
21
- if (block.name === 'Task') detail = ` [subagent_type=${block.input?.subagent_type ?? '?'}] ${(block.input?.description ?? '').slice(0,40)}`;
22
- else if (/homegraph/.test(block.name)) detail = ` ${JSON.stringify(block.input?.query ?? block.input?.task ?? block.input?.symbol ?? '').slice(0,60)}`;
23
- else if (block.name === 'Bash') detail = ` ${(block.input?.command ?? '').slice(0,50)}`;
24
- else if (block.name === 'Read') detail = ` ${(block.input?.file_path ?? '').split('/').slice(-1)[0]}`;
25
- toolCalls.push(`${block.name}${detail}`);
26
- }
27
- }
28
- }
29
- if (ev.type === 'result') result = ev;
30
- }
31
-
32
- console.log(`\n=== ${file.split('/').pop()} ===`);
33
- console.log(`homegraph tools exposed: ${initTools ? initTools.length : '?'}`);
34
- console.log(`\nTool calls (${toolCalls.length}):`);
35
- const counts = {};
36
- for (const tc of toolCalls) { const n = tc.split(' ')[0]; counts[n] = (counts[n]||0)+1; }
37
- console.log(' by type:', JSON.stringify(counts));
38
- toolCalls.forEach((tc, i) => console.log(` ${i+1}. ${tc}`));
39
-
40
- if (result) {
41
- const u = result.usage || {};
42
- const totalIn = (u.input_tokens||0) + (u.cache_read_input_tokens||0) + (u.cache_creation_input_tokens||0);
43
- console.log(`\nResult: ${result.subtype} | duration ${(result.duration_ms/1000).toFixed(0)}s | turns ${result.num_turns}`);
44
- console.log(` tokens: in=${totalIn} out=${u.output_tokens||0} | cost $${(result.total_cost_usd||0).toFixed(3)}`);
45
- }
1
+ #!/usr/bin/env node
2
+ // Parse a Claude Code stream-json run log: tool-call sequence + token usage.
3
+ import { readFileSync } from 'fs';
4
+ const file = process.argv[2];
5
+ const lines = readFileSync(file, 'utf8').split('\n').filter(Boolean);
6
+
7
+ const toolCalls = [];
8
+ let result = null;
9
+ let initTools = null;
10
+
11
+ for (const line of lines) {
12
+ let ev;
13
+ try { ev = JSON.parse(line); } catch { continue; }
14
+ if (ev.type === 'system' && ev.subtype === 'init') {
15
+ initTools = (ev.tools || []).filter(t => /homegraph/.test(t));
16
+ }
17
+ if (ev.type === 'assistant' && ev.message?.content) {
18
+ for (const block of ev.message.content) {
19
+ if (block.type === 'tool_use') {
20
+ let detail = '';
21
+ if (block.name === 'Task') detail = ` [subagent_type=${block.input?.subagent_type ?? '?'}] ${(block.input?.description ?? '').slice(0,40)}`;
22
+ else if (/homegraph/.test(block.name)) detail = ` ${JSON.stringify(block.input?.query ?? block.input?.task ?? block.input?.symbol ?? '').slice(0,60)}`;
23
+ else if (block.name === 'Bash') detail = ` ${(block.input?.command ?? '').slice(0,50)}`;
24
+ else if (block.name === 'Read') detail = ` ${(block.input?.file_path ?? '').split('/').slice(-1)[0]}`;
25
+ toolCalls.push(`${block.name}${detail}`);
26
+ }
27
+ }
28
+ }
29
+ if (ev.type === 'result') result = ev;
30
+ }
31
+
32
+ console.log(`\n=== ${file.split('/').pop()} ===`);
33
+ console.log(`homegraph tools exposed: ${initTools ? initTools.length : '?'}`);
34
+ console.log(`\nTool calls (${toolCalls.length}):`);
35
+ const counts = {};
36
+ for (const tc of toolCalls) { const n = tc.split(' ')[0]; counts[n] = (counts[n]||0)+1; }
37
+ console.log(' by type:', JSON.stringify(counts));
38
+ toolCalls.forEach((tc, i) => console.log(` ${i+1}. ${tc}`));
39
+
40
+ if (result) {
41
+ const u = result.usage || {};
42
+ const totalIn = (u.input_tokens||0) + (u.cache_read_input_tokens||0) + (u.cache_creation_input_tokens||0);
43
+ console.log(`\nResult: ${result.subtype} | duration ${(result.duration_ms/1000).toFixed(0)}s | turns ${result.num_turns}`);
44
+ console.log(` tokens: in=${totalIn} out=${u.output_tokens||0} | cost $${(result.total_cost_usd||0).toFixed(3)}`);
45
+ }