homegraph 1.5.0 → 1.5.1

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 (214) hide show
  1. package/LICENSE +21 -21
  2. package/README.md +305 -305
  3. package/dist/bin/command-supervision.d.ts.map +1 -1
  4. package/dist/bin/command-supervision.js +7 -4
  5. package/dist/bin/command-supervision.js.map +1 -1
  6. package/dist/bin/homegraph.js +9 -9
  7. package/dist/db/index.js +36 -36
  8. package/dist/db/migrations.js +37 -37
  9. package/dist/db/queries.js +156 -156
  10. package/dist/db/schema.sql +203 -203
  11. package/dist/directory.js +5 -5
  12. package/dist/extraction/languages/arkts-viewtree.d.ts +2 -4
  13. package/dist/extraction/languages/arkts-viewtree.d.ts.map +1 -1
  14. package/dist/extraction/languages/arkts-viewtree.js +6 -21
  15. package/dist/extraction/languages/arkts-viewtree.js.map +1 -1
  16. package/dist/extraction/languages/arkts.d.ts +16 -6
  17. package/dist/extraction/languages/arkts.d.ts.map +1 -1
  18. package/dist/extraction/languages/arkts.js +174 -21
  19. package/dist/extraction/languages/arkts.js.map +1 -1
  20. package/dist/extraction/wasm/tree-sitter-c_sharp.wasm +0 -0
  21. package/dist/extraction/wasm/tree-sitter-cfml.wasm +0 -0
  22. package/dist/extraction/wasm/tree-sitter-cfquery.wasm +0 -0
  23. package/dist/extraction/wasm/tree-sitter-cfscript.wasm +0 -0
  24. package/dist/extraction/wasm/tree-sitter-cobol.wasm +0 -0
  25. package/dist/extraction/wasm/tree-sitter-erlang.wasm +0 -0
  26. package/dist/extraction/wasm/tree-sitter-nix.wasm +0 -0
  27. package/dist/extraction/wasm/tree-sitter-pascal.wasm +0 -0
  28. package/dist/extraction/wasm/tree-sitter-vbnet.wasm +0 -0
  29. package/dist/installer/instructions-template.js +9 -9
  30. package/dist/mcp/liveness-watchdog.d.ts +18 -0
  31. package/dist/mcp/liveness-watchdog.d.ts.map +1 -1
  32. package/dist/mcp/liveness-watchdog.js +185 -73
  33. package/dist/mcp/liveness-watchdog.js.map +1 -1
  34. package/dist/mcp/server-instructions.js +47 -47
  35. package/dist/reasoning/reasoner.js +32 -32
  36. package/dist/spec/db/commit-node.js +4 -4
  37. package/dist/spec/db/fragment-node.js +10 -10
  38. package/dist/spec/db/fts.js +8 -8
  39. package/dist/spec/db/relations.js +88 -88
  40. package/dist/spec/db/schema.js +6 -6
  41. package/dist/spec/db/schema.sql +121 -121
  42. package/dist/spec/db/spec-node.js +4 -4
  43. package/dist/spec/llm/prompts.js +53 -53
  44. package/dist/spec/utils.d.ts +16 -4
  45. package/dist/spec/utils.d.ts.map +1 -1
  46. package/dist/spec/utils.js +58 -6
  47. package/dist/spec/utils.js.map +1 -1
  48. package/package.json +62 -62
  49. package/scripts/_tmp-cfwk-resolve.log +0 -0
  50. package/scripts/_tmp-cfwk-sig.log +0 -0
  51. package/scripts/_tmp-cfwk-vt.log +0 -0
  52. package/scripts/add-lang/bench.sh +60 -60
  53. package/scripts/add-lang/check-grammar.mjs +75 -75
  54. package/scripts/add-lang/dump-ast.mjs +103 -103
  55. package/scripts/add-lang/verify-extraction.mjs +70 -70
  56. package/scripts/agent-eval/ab-adoption.sh +91 -91
  57. package/scripts/agent-eval/ab-hook.sh +86 -86
  58. package/scripts/agent-eval/ab-impl.sh +78 -78
  59. package/scripts/agent-eval/ab-new-vs-baseline.sh +102 -102
  60. package/scripts/agent-eval/ab-sufficiency.sh +78 -78
  61. package/scripts/agent-eval/arms-F.sh +21 -21
  62. package/scripts/agent-eval/arms-matrix.sh +37 -37
  63. package/scripts/agent-eval/audit.sh +68 -68
  64. package/scripts/agent-eval/bench-readme.sh +28 -28
  65. package/scripts/agent-eval/bench-why-repo.sh +22 -22
  66. package/scripts/agent-eval/block-read-hook.sh +19 -19
  67. package/scripts/agent-eval/hook-settings.json +15 -15
  68. package/scripts/agent-eval/itrun.sh +120 -120
  69. package/scripts/agent-eval/offload-eval-3arm.sh +72 -72
  70. package/scripts/agent-eval/offload-eval-cost.mjs +133 -133
  71. package/scripts/agent-eval/offload-eval-effort.mjs +108 -108
  72. package/scripts/agent-eval/offload-eval-frontload-matrix.sh +25 -25
  73. package/scripts/agent-eval/offload-eval-frontload.sh +47 -47
  74. package/scripts/agent-eval/offload-eval-ground-truth.json +18 -18
  75. package/scripts/agent-eval/offload-eval-hook.mjs +84 -84
  76. package/scripts/agent-eval/offload-eval-judge.mjs +103 -103
  77. package/scripts/agent-eval/offload-eval-matrix.sh +20 -20
  78. package/scripts/agent-eval/offload-eval-metrics.mjs +94 -94
  79. package/scripts/agent-eval/offload-eval-refs1.sh +50 -50
  80. package/scripts/agent-eval/offload-eval-setup.sh +24 -24
  81. package/scripts/agent-eval/offload-eval-styles.sh +71 -71
  82. package/scripts/agent-eval/offload-eval-summarize.mjs +68 -68
  83. package/scripts/agent-eval/offload-eval.md +76 -76
  84. package/scripts/agent-eval/parse-arms.mjs +116 -116
  85. package/scripts/agent-eval/parse-bench-readme.mjs +84 -84
  86. package/scripts/agent-eval/parse-run.mjs +45 -45
  87. package/scripts/agent-eval/parse-session.mjs +93 -93
  88. package/scripts/agent-eval/probe-context.mjs +21 -21
  89. package/scripts/agent-eval/probe-explore.mjs +40 -40
  90. package/scripts/agent-eval/probe-node.mjs +20 -20
  91. package/scripts/agent-eval/probe-sweep.mjs +119 -119
  92. package/scripts/agent-eval/probe-trace.mjs +20 -20
  93. package/scripts/agent-eval/redirect-read-hook.sh +38 -38
  94. package/scripts/agent-eval/repro-concurrent-explore.mjs +119 -119
  95. package/scripts/agent-eval/repro-daemon-clients.mjs +125 -125
  96. package/scripts/agent-eval/run-agent.sh +34 -34
  97. package/scripts/agent-eval/run-all.sh +75 -75
  98. package/scripts/agent-eval/run-arms.sh +56 -56
  99. package/scripts/agent-eval/seq-matrix.mjs +137 -137
  100. package/scripts/bench-arkts-init-rss.log +0 -0
  101. package/scripts/build-bundle.sh +123 -123
  102. package/scripts/exp_boundary_eval/README.md +247 -247
  103. package/scripts/exp_boundary_eval/__pycache__/_utils.cpython-310.pyc +0 -0
  104. package/scripts/exp_boundary_eval/__pycache__/_utils.cpython-38.pyc +0 -0
  105. package/scripts/exp_boundary_eval/__pycache__/analyze.cpython-310.pyc +0 -0
  106. package/scripts/exp_boundary_eval/__pycache__/analyze.cpython-38.pyc +0 -0
  107. package/scripts/exp_boundary_eval/__pycache__/deveco_arm.cpython-38.pyc +0 -0
  108. package/scripts/exp_boundary_eval/__pycache__/run_all.cpython-310.pyc +0 -0
  109. package/scripts/exp_boundary_eval/__pycache__/run_all.cpython-38.pyc +0 -0
  110. package/scripts/exp_boundary_eval/__pycache__/run_one.cpython-310.pyc +0 -0
  111. package/scripts/exp_boundary_eval/__pycache__/run_one.cpython-38.pyc +0 -0
  112. package/scripts/exp_boundary_eval/__pycache__/run_session.cpython-310.pyc +0 -0
  113. package/scripts/exp_boundary_eval/__pycache__/run_session.cpython-38.pyc +0 -0
  114. package/scripts/exp_boundary_eval/__pycache__/setup.cpython-310.pyc +0 -0
  115. package/scripts/exp_boundary_eval/__pycache__/setup.cpython-38.pyc +0 -0
  116. package/scripts/exp_boundary_eval/__pycache__/win_mcp_launcher.cpython-38.pyc +0 -0
  117. package/scripts/exp_boundary_eval/_test_mcp_chain.py +78 -78
  118. package/scripts/exp_boundary_eval/_test_stdin.py +8 -8
  119. package/scripts/exp_boundary_eval/_utils.py +1116 -1116
  120. package/scripts/exp_boundary_eval/analyze.py +1313 -1313
  121. package/scripts/exp_boundary_eval/deveco_arm.py +519 -519
  122. package/scripts/exp_boundary_eval/run_all.py +378 -378
  123. package/scripts/exp_boundary_eval/run_one.py +165 -165
  124. package/scripts/exp_boundary_eval/run_session.py +158 -158
  125. package/scripts/exp_boundary_eval/setup.py +120 -120
  126. package/scripts/exp_boundary_eval/win_mcp_launcher.py +73 -73
  127. package/scripts/exp_boundary_eval/win_mcp_stdio_wrap.js +36 -36
  128. package/scripts/exp_boundary_eval/win_node_launcher.py +24 -24
  129. package/scripts/extract-release-notes.mjs +130 -130
  130. package/scripts/local-install.sh +41 -41
  131. package/scripts/npm-sdk.js +75 -75
  132. package/scripts/npm-shim.js +275 -275
  133. package/scripts/ohos-sdk-publish.mjs +133 -133
  134. package/scripts/pack-npm.sh +119 -119
  135. package/scripts/prepare-release.mjs +270 -270
  136. package/scripts/probe-arkts-mem-why-run.log +0 -0
  137. package/scripts/probe-banner-livecard-ir.log +0 -0
  138. package/scripts/probe-cfwk-ast.stderr.log +0 -0
  139. package/scripts/probe-cfwk-ast.stdout.log +0 -0
  140. package/scripts/probe-cfwk-attr-shape.log +0 -0
  141. package/scripts/probe-cfwk-cfgdump.stderr.log +0 -0
  142. package/scripts/probe-cfwk-cfgdump.stdout.log +0 -0
  143. package/scripts/probe-cfwk-cvc.stderr.log +0 -0
  144. package/scripts/probe-cfwk-cvc.stdout.log +0 -0
  145. package/scripts/probe-cfwk-diag2.log +0 -0
  146. package/scripts/probe-cfwk-diag3.log +0 -0
  147. package/scripts/probe-cfwk-fedbg.stderr.log +0 -0
  148. package/scripts/probe-cfwk-fedbg.stdout.log +0 -0
  149. package/scripts/probe-cfwk-fileresult.log +0 -0
  150. package/scripts/probe-cfwk-fix.stderr.log +0 -0
  151. package/scripts/probe-cfwk-fix.stdout.log +0 -0
  152. package/scripts/probe-cfwk-fix2.stderr.log +0 -0
  153. package/scripts/probe-cfwk-fix2.stdout.log +0 -0
  154. package/scripts/probe-cfwk-fix3.stderr.log +0 -0
  155. package/scripts/probe-cfwk-fix3.stdout.log +0 -0
  156. package/scripts/probe-cfwk-foreach.log +0 -0
  157. package/scripts/probe-cfwk-getmethod-throw.log +0 -0
  158. package/scripts/probe-cfwk-hg-extract.log +0 -0
  159. package/scripts/probe-cfwk-pr1003-noprior.stderr.log +0 -0
  160. package/scripts/probe-cfwk-pr1003-noprior.stdout.log +0 -0
  161. package/scripts/probe-cfwk-pr1003.stderr.log +0 -0
  162. package/scripts/probe-cfwk-pr1003.stdout.log +0 -0
  163. package/scripts/probe-cfwk-preroot-noprior.stderr.log +0 -0
  164. package/scripts/probe-cfwk-preroot-noprior.stdout.log +0 -0
  165. package/scripts/probe-cfwk-preroot-prior.stderr.log +0 -0
  166. package/scripts/probe-cfwk-preroot-prior.stdout.log +0 -0
  167. package/scripts/probe-cfwk-preroot-skipstate.stderr.log +0 -0
  168. package/scripts/probe-cfwk-preroot-skipstate.stdout.log +0 -0
  169. package/scripts/probe-cfwk-resolve-sim.log +0 -0
  170. package/scripts/probe-cfwk-tree-shape.log +0 -0
  171. package/scripts/probe-cfwk-walk-abort.log +0 -0
  172. package/scripts/probe-cfwk.log +0 -0
  173. package/scripts/probe-force-index.log +0 -0
  174. package/scripts/probe-no-force-index.log +0 -0
  175. package/scripts/probe-samefile-ir.stderr.log +0 -0
  176. package/scripts/probe-samefile-ir.stdout.log +224 -0
  177. package/scripts/probe-sdk-vs-project.log +0 -0
  178. package/scripts/probe-viewtree-downgrade.log +0 -0
  179. package/dist/arkts/ohos-api-index.d.ts +0 -15
  180. package/dist/arkts/ohos-api-index.d.ts.map +0 -1
  181. package/dist/arkts/ohos-api-index.js +0 -190
  182. package/dist/arkts/ohos-api-index.js.map +0 -1
  183. package/dist/arkts/ohos-sdk-input.d.ts +0 -36
  184. package/dist/arkts/ohos-sdk-input.d.ts.map +0 -1
  185. package/dist/arkts/ohos-sdk-input.js +0 -214
  186. package/dist/arkts/ohos-sdk-input.js.map +0 -1
  187. package/dist/extraction/languages/arkts-state-decorators.d.ts +0 -13
  188. package/dist/extraction/languages/arkts-state-decorators.d.ts.map +0 -1
  189. package/dist/extraction/languages/arkts-state-decorators.js +0 -26
  190. package/dist/extraction/languages/arkts-state-decorators.js.map +0 -1
  191. package/dist/extraction/languages/ohos-api-consumer.d.ts +0 -34
  192. package/dist/extraction/languages/ohos-api-consumer.d.ts.map +0 -1
  193. package/dist/extraction/languages/ohos-api-consumer.js +0 -283
  194. package/dist/extraction/languages/ohos-api-consumer.js.map +0 -1
  195. package/dist/spec/build/git-scanner.d.ts +0 -93
  196. package/dist/spec/build/git-scanner.d.ts.map +0 -1
  197. package/dist/spec/build/git-scanner.js +0 -254
  198. package/dist/spec/build/git-scanner.js.map +0 -1
  199. package/dist/spec/git-utils.d.ts +0 -8
  200. package/dist/spec/git-utils.d.ts.map +0 -1
  201. package/dist/spec/git-utils.js +0 -14
  202. package/dist/spec/git-utils.js.map +0 -1
  203. package/dist/spec/mine/clusterer.d.ts +0 -63
  204. package/dist/spec/mine/clusterer.d.ts.map +0 -1
  205. package/dist/spec/mine/clusterer.js +0 -904
  206. package/dist/spec/mine/clusterer.js.map +0 -1
  207. package/dist/spec/mine/progress-handler.d.ts +0 -22
  208. package/dist/spec/mine/progress-handler.d.ts.map +0 -1
  209. package/dist/spec/mine/progress-handler.js +0 -108
  210. package/dist/spec/mine/progress-handler.js.map +0 -1
  211. package/dist/spec/mine/progress.d.ts +0 -23
  212. package/dist/spec/mine/progress.d.ts.map +0 -1
  213. package/dist/spec/mine/progress.js +0 -12
  214. package/dist/spec/mine/progress.js.map +0 -1
@@ -1,68 +1,68 @@
1
- #!/usr/bin/env node
2
- // Aggregate judged.jsonl (or results.jsonl) into a per-repo, per-arm report:
3
- // time, main tokens/cost, AI tokens/cost, total cost, tool mix, accuracy.
4
- // Usage: summarize.mjs <judged-or-results.jsonl>
5
- import { readFileSync } from 'fs';
6
- const rows = readFileSync(process.argv[2], 'utf8').split('\n').filter(Boolean).map(l => JSON.parse(l));
7
-
8
- const med = (xs) => { const a = xs.filter(x => x != null).sort((p, q) => p - q); if (!a.length) return null; const m = Math.floor(a.length / 2); return a.length % 2 ? a[m] : (a[m - 1] + a[m]) / 2; };
9
- const rng = (xs) => { const a = xs.filter(x => x != null); return a.length ? `${Math.min(...a)}–${Math.max(...a)}` : '—'; };
10
- const d2 = (x) => x == null ? '—' : (+x).toFixed(2);
11
- const d3 = (x) => x == null ? '—' : (+x).toFixed(3);
12
- const d4 = (x) => x == null ? '—' : (+x).toFixed(4);
13
-
14
- const ARM_ORDER = ['frontload', 'offload', 'raw', 'nocg'];
15
- const byRepo = {};
16
- for (const r of rows) (byRepo[r.repo] ??= {});
17
- for (const r of rows) ((byRepo[r.repo][r.arm] ??= []).push(r));
18
-
19
- const verdictTally = (rs, field) => {
20
- const t = { pass: 0, partial: 0, fail: 0, error: 0 };
21
- for (const r of rs) { const v = r[field]?.verdict; if (v in t) t[v]++; }
22
- return t;
23
- };
24
-
25
- for (const repo of Object.keys(byRepo)) {
26
- const tier = byRepo[repo][Object.keys(byRepo[repo])[0]][0].tier;
27
- console.log(`\n${'='.repeat(78)}\n${repo} [${tier}]\n${'='.repeat(78)}`);
28
- console.log(`${'arm'.padEnd(9)} n ${'time(s)'.padStart(9)} ${'mainCost'.padStart(9)} ${'aiCost'.padStart(8)} ${'totCost'.padStart(8)} ${'mainTok'.padStart(8)} ${'aiTok'.padStart(7)} ${'rd'.padStart(3)} ${'gr'.padStart(3)} ${'exp'.padStart(3)} ${'off'.padStart(3)} e2e(P/p/F) fidScore`);
29
- for (const arm of ARM_ORDER) {
30
- const rs = byRepo[repo][arm]; if (!rs) continue;
31
- const n = rs.length;
32
- const mainCost = med(rs.map(r => r.costUsdMain));
33
- const aiCost = med(rs.map(r => r.ai?.costUsd ?? 0));
34
- const totCost = (mainCost ?? 0) + (aiCost ?? 0);
35
- const e2e = verdictTally(rs, 'e2e');
36
- const fidScores = arm === 'offload' ? rs.flatMap(r => r.fidelity?.scores ?? []) : [];
37
- const fid = fidScores.length ? med(fidScores) : null;
38
- const fab = arm === 'offload' && rs.some(r => r.fidelity?.anyFabrication);
39
- const e2eScore = med(rs.map(r => r.e2e?.score).filter(x => x != null));
40
- console.log(
41
- `${arm.padEnd(9)} ${String(n).padStart(1)} ${String(med(rs.map(r => r.durationSec))).padStart(9)} ` +
42
- `${('$' + d3(mainCost)).padStart(9)} ${('$' + d3(aiCost)).padStart(8)} ${('$' + d3(totCost)).padStart(8)} ` +
43
- `${String(Math.round(med(rs.map(r => r.tokBillable)) / 1000) + 'k').padStart(8)} ${String(Math.round(med(rs.map(r => r.ai?.totalTokens ?? 0)) / 1000) + 'k').padStart(7)} ` +
44
- `${String(med(rs.map(r => r.read))).padStart(3)} ${String(med(rs.map(r => r.grep))).padStart(3)} ${String(med(rs.map(r => r.explore))).padStart(3)} ${String(med(rs.map(r => r.offloadFired))).padStart(3)} ` +
45
- `${(e2e.pass + '/' + e2e.partial + '/' + e2e.fail).padStart(9)} ${e2eScore != null ? 'e2e=' + e2eScore : ''} ${fid != null ? 'fid=' + fid + (fab ? ' FAB!' : '') : ''}`
46
- );
47
- }
48
- // ranges line for the two key metrics (variance matters)
49
- for (const arm of ARM_ORDER) {
50
- const rs = byRepo[repo][arm]; if (!rs) continue;
51
- console.log(` ${arm} ranges: time ${rng(rs.map(r => r.durationSec))}s · mainCost $${rng(rs.map(r => r.costUsdMain))} · read ${rng(rs.map(r => r.read))} · explore ${rng(rs.map(r => r.explore))} · offloadFired ${rng(rs.map(r => r.offloadFired))}`);
52
- }
53
- }
54
-
55
- // Cross-repo roll-up: offload vs raw vs nocg deltas
56
- console.log(`\n${'='.repeat(78)}\nCROSS-REPO SUMMARY (medians per repo, then averaged)\n${'='.repeat(78)}`);
57
- console.log(`${'repo'.padEnd(12)} ${'arm'.padEnd(8)} ${'time'.padStart(7)} ${'totCost'.padStart(8)} ${'read'.padStart(5)} ${'e2e pass%'.padStart(9)} ${'fid'.padStart(5)}`);
58
- for (const repo of Object.keys(byRepo)) {
59
- for (const arm of ARM_ORDER) {
60
- const rs = byRepo[repo][arm]; if (!rs) continue;
61
- const e2e = verdictTally(rs, 'e2e');
62
- const passPct = Math.round(100 * e2e.pass / rs.length);
63
- const totCost = (med(rs.map(r => r.costUsdMain)) ?? 0) + (med(rs.map(r => r.ai?.costUsd ?? 0)) ?? 0);
64
- const fid = arm === 'offload' ? med(rs.flatMap(r => r.fidelity?.scores ?? [])) : null;
65
- console.log(`${repo.padEnd(12)} ${arm.padEnd(8)} ${(med(rs.map(r => r.durationSec)) + 's').padStart(7)} ${('$' + d3(totCost)).padStart(8)} ${String(med(rs.map(r => r.read))).padStart(5)} ${(passPct + '%').padStart(9)} ${String(fid ?? '—').padStart(5)}`);
66
- }
67
- }
68
- console.log('');
1
+ #!/usr/bin/env node
2
+ // Aggregate judged.jsonl (or results.jsonl) into a per-repo, per-arm report:
3
+ // time, main tokens/cost, AI tokens/cost, total cost, tool mix, accuracy.
4
+ // Usage: summarize.mjs <judged-or-results.jsonl>
5
+ import { readFileSync } from 'fs';
6
+ const rows = readFileSync(process.argv[2], 'utf8').split('\n').filter(Boolean).map(l => JSON.parse(l));
7
+
8
+ const med = (xs) => { const a = xs.filter(x => x != null).sort((p, q) => p - q); if (!a.length) return null; const m = Math.floor(a.length / 2); return a.length % 2 ? a[m] : (a[m - 1] + a[m]) / 2; };
9
+ const rng = (xs) => { const a = xs.filter(x => x != null); return a.length ? `${Math.min(...a)}–${Math.max(...a)}` : '—'; };
10
+ const d2 = (x) => x == null ? '—' : (+x).toFixed(2);
11
+ const d3 = (x) => x == null ? '—' : (+x).toFixed(3);
12
+ const d4 = (x) => x == null ? '—' : (+x).toFixed(4);
13
+
14
+ const ARM_ORDER = ['frontload', 'offload', 'raw', 'nocg'];
15
+ const byRepo = {};
16
+ for (const r of rows) (byRepo[r.repo] ??= {});
17
+ for (const r of rows) ((byRepo[r.repo][r.arm] ??= []).push(r));
18
+
19
+ const verdictTally = (rs, field) => {
20
+ const t = { pass: 0, partial: 0, fail: 0, error: 0 };
21
+ for (const r of rs) { const v = r[field]?.verdict; if (v in t) t[v]++; }
22
+ return t;
23
+ };
24
+
25
+ for (const repo of Object.keys(byRepo)) {
26
+ const tier = byRepo[repo][Object.keys(byRepo[repo])[0]][0].tier;
27
+ console.log(`\n${'='.repeat(78)}\n${repo} [${tier}]\n${'='.repeat(78)}`);
28
+ console.log(`${'arm'.padEnd(9)} n ${'time(s)'.padStart(9)} ${'mainCost'.padStart(9)} ${'aiCost'.padStart(8)} ${'totCost'.padStart(8)} ${'mainTok'.padStart(8)} ${'aiTok'.padStart(7)} ${'rd'.padStart(3)} ${'gr'.padStart(3)} ${'exp'.padStart(3)} ${'off'.padStart(3)} e2e(P/p/F) fidScore`);
29
+ for (const arm of ARM_ORDER) {
30
+ const rs = byRepo[repo][arm]; if (!rs) continue;
31
+ const n = rs.length;
32
+ const mainCost = med(rs.map(r => r.costUsdMain));
33
+ const aiCost = med(rs.map(r => r.ai?.costUsd ?? 0));
34
+ const totCost = (mainCost ?? 0) + (aiCost ?? 0);
35
+ const e2e = verdictTally(rs, 'e2e');
36
+ const fidScores = arm === 'offload' ? rs.flatMap(r => r.fidelity?.scores ?? []) : [];
37
+ const fid = fidScores.length ? med(fidScores) : null;
38
+ const fab = arm === 'offload' && rs.some(r => r.fidelity?.anyFabrication);
39
+ const e2eScore = med(rs.map(r => r.e2e?.score).filter(x => x != null));
40
+ console.log(
41
+ `${arm.padEnd(9)} ${String(n).padStart(1)} ${String(med(rs.map(r => r.durationSec))).padStart(9)} ` +
42
+ `${('$' + d3(mainCost)).padStart(9)} ${('$' + d3(aiCost)).padStart(8)} ${('$' + d3(totCost)).padStart(8)} ` +
43
+ `${String(Math.round(med(rs.map(r => r.tokBillable)) / 1000) + 'k').padStart(8)} ${String(Math.round(med(rs.map(r => r.ai?.totalTokens ?? 0)) / 1000) + 'k').padStart(7)} ` +
44
+ `${String(med(rs.map(r => r.read))).padStart(3)} ${String(med(rs.map(r => r.grep))).padStart(3)} ${String(med(rs.map(r => r.explore))).padStart(3)} ${String(med(rs.map(r => r.offloadFired))).padStart(3)} ` +
45
+ `${(e2e.pass + '/' + e2e.partial + '/' + e2e.fail).padStart(9)} ${e2eScore != null ? 'e2e=' + e2eScore : ''} ${fid != null ? 'fid=' + fid + (fab ? ' FAB!' : '') : ''}`
46
+ );
47
+ }
48
+ // ranges line for the two key metrics (variance matters)
49
+ for (const arm of ARM_ORDER) {
50
+ const rs = byRepo[repo][arm]; if (!rs) continue;
51
+ console.log(` ${arm} ranges: time ${rng(rs.map(r => r.durationSec))}s · mainCost $${rng(rs.map(r => r.costUsdMain))} · read ${rng(rs.map(r => r.read))} · explore ${rng(rs.map(r => r.explore))} · offloadFired ${rng(rs.map(r => r.offloadFired))}`);
52
+ }
53
+ }
54
+
55
+ // Cross-repo roll-up: offload vs raw vs nocg deltas
56
+ console.log(`\n${'='.repeat(78)}\nCROSS-REPO SUMMARY (medians per repo, then averaged)\n${'='.repeat(78)}`);
57
+ console.log(`${'repo'.padEnd(12)} ${'arm'.padEnd(8)} ${'time'.padStart(7)} ${'totCost'.padStart(8)} ${'read'.padStart(5)} ${'e2e pass%'.padStart(9)} ${'fid'.padStart(5)}`);
58
+ for (const repo of Object.keys(byRepo)) {
59
+ for (const arm of ARM_ORDER) {
60
+ const rs = byRepo[repo][arm]; if (!rs) continue;
61
+ const e2e = verdictTally(rs, 'e2e');
62
+ const passPct = Math.round(100 * e2e.pass / rs.length);
63
+ const totCost = (med(rs.map(r => r.costUsdMain)) ?? 0) + (med(rs.map(r => r.ai?.costUsd ?? 0)) ?? 0);
64
+ const fid = arm === 'offload' ? med(rs.flatMap(r => r.fidelity?.scores ?? [])) : null;
65
+ console.log(`${repo.padEnd(12)} ${arm.padEnd(8)} ${(med(rs.map(r => r.durationSec)) + 's').padStart(7)} ${('$' + d3(totCost)).padStart(8)} ${String(med(rs.map(r => r.read))).padStart(5)} ${(passPct + '%').padStart(9)} ${String(fid ?? '—').padStart(5)}`);
66
+ }
67
+ }
68
+ console.log('');
@@ -1,76 +1,76 @@
1
- # HomeGraph AI offload — accuracy & adoption eval harness
2
-
3
- Measures the managed **offload** (`homegraph_explore` → reasoning model synthesis) and the
4
- **front-load hook** (approach 1) against plain homegraph and no-homegraph, across repo sizes,
5
- on **time · main-session tokens/cost · HomeGraph-AI tokens/cost · accuracy**.
6
-
7
- All agent arms run `claude -p --model sonnet --effort high` (the deliberate floor model — an
8
- affordance that lands on Sonnet generalizes up). Everything writes to a scratch dir
9
- (`AGENT_EVAL_OUT`, default `/tmp/cg-offload-eval`); nothing here is shipped to users.
10
-
11
- ## Repos (selected via a memory-probe gate — NOT trained on)
12
-
13
- Famous repos (express, excalidraw, n8n, …) are useless for *accuracy* evals: Sonnet answers their
14
- flow questions from memory, so the no-homegraph baseline is dishonest. These four passed a no-tools
15
- probe (Sonnet could not name their real flow internals) and are cloned fresh by `offload-eval-setup.sh`:
16
-
17
- | tier | repo | ~src files | canonical flow |
18
- |---|---|---|---|
19
- | small | MTKruto/MTKruto | 322 TS | `sendMessage` → invoke → TL serialize → transport |
20
- | medium | mvdicarlo/postybirb-plus | 608 TS | submission → queue → per-website `.post()` |
21
- | complex | shapeshift/web | 3.2k TS (35-pkg monorepo) | swap → swapper registry → concrete swapper |
22
- | large | trezor/trezor-suite | 8k TS monorepo | send-form → sign thunk → `@trezor/connect` |
23
-
24
- Verified ground-truth flows (the judge's reference) live in `offload-eval-ground-truth.json`.
25
-
26
- ## Arms
27
-
28
- - **offload** — homegraph + managed offload ON (requires `homegraph login`); records AI tokens/credits via `HOMEGRAPH_OFFLOAD_USAGE_LOG`.
29
- - **raw** — homegraph, `HOMEGRAPH_OFFLOAD_DISABLE=1` (returns raw source).
30
- - **nocg** — empty MCP config; Read/Grep baseline.
31
- - **frontload** — homegraph (offload-disabled) + a `UserPromptSubmit` hook (`offload-eval-hook.mjs`) that runs raw explore on the prompt and injects the result into context (approach 1).
32
-
33
- ## Run it
34
-
35
- ```bash
36
- npm run build # the harness shells out to dist/
37
- homegraph login # only needed for the offload arm
38
- export AGENT_EVAL_OUT=/tmp/cg-offload-eval
39
-
40
- bash scripts/agent-eval/offload-eval-setup.sh # clone + index the 4 repos
41
- bash scripts/agent-eval/offload-eval-matrix.sh # 3 arms × 4 tiers × REPS (default 3)
42
- node scripts/agent-eval/offload-eval-judge.mjs \
43
- --results $AGENT_EVAL_OUT/results.jsonl \
44
- --truth scripts/agent-eval/offload-eval-ground-truth.json \
45
- --out $AGENT_EVAL_OUT/judged.jsonl
46
- node scripts/agent-eval/offload-eval-summarize.mjs $AGENT_EVAL_OUT/judged.jsonl
47
-
48
- bash scripts/agent-eval/offload-eval-frontload-matrix.sh # frontload arm + judge + merged summary
49
- ```
50
-
51
- Single repo: `offload-eval-3arm.sh <indexed-repo> <tier> <reps> "<question>"` (or `-frontload.sh`).
52
-
53
- ## Files
54
-
55
- - `offload-eval-setup.sh` — clone + index the 4 repos.
56
- - `offload-eval-3arm.sh` / `-frontload.sh` — one repo, the arms.
57
- - `offload-eval-matrix.sh` / `-frontload-matrix.sh` — drive all 4 tiers.
58
- - `offload-eval-hook.mjs` — the front-load `UserPromptSubmit` hook (resolves its own engine; `CG_FRONTLOAD_DEBUG=<path>` to log injections; `CG_FRONTLOAD_BUDGET` to cap injected chars).
59
- - `offload-eval-metrics.mjs` — one run's stream-json + usage log → one JSON metrics line.
60
- - `offload-eval-judge.mjs` — Sonnet judge: end-to-end (agent final vs ground truth) + per-answer offload fidelity.
61
- - `offload-eval-summarize.mjs` — per-tier, per-arm table + cross-repo roll-up.
62
- - `offload-eval-ground-truth.json` — source-verified canonical flows.
63
-
64
- ## Findings (2026-06, n=3 — direction consistent, magnitudes noisy)
65
-
66
- - **Raw homegraph is the efficiency win** — ~nocg accuracy, fewer reads, faster, no AI cost.
67
- - **The offload is the least-accurate arm in all 4 tiers** — synthesized fidelity 12–27/100 with
68
- fabrication in 3/4 (e.g. invented website services; traced `ClientPlain`/`SessionPlain` instead of
69
- the real encrypted path). Its speed/cost win is narrow (medium-only) and inversely correlated with
70
- accuracy. **Use raw until offload fidelity is fixed.**
71
- - **The front-load hook SOLVES adoption** — reads → 0–1 in every tier (incl. large, where the agent
72
- otherwise read 12–24 files); fired 12/12, 0 errors. Wins on medium/complex (100% pass). But it
73
- **regresses small/large to partial** — it suppresses the reads that compensate for explore's gaps at
74
- **dynamic boundaries** (async queues, redux thunks, facade/factory indirection).
75
- - **Master lever for BOTH:** explore's dynamic-dispatch coverage. Fix it → front-load is complete
76
- everywhere and the offload has the full flow to synthesize.
1
+ # HomeGraph AI offload — accuracy & adoption eval harness
2
+
3
+ Measures the managed **offload** (`homegraph_explore` → reasoning model synthesis) and the
4
+ **front-load hook** (approach 1) against plain homegraph and no-homegraph, across repo sizes,
5
+ on **time · main-session tokens/cost · HomeGraph-AI tokens/cost · accuracy**.
6
+
7
+ All agent arms run `claude -p --model sonnet --effort high` (the deliberate floor model — an
8
+ affordance that lands on Sonnet generalizes up). Everything writes to a scratch dir
9
+ (`AGENT_EVAL_OUT`, default `/tmp/cg-offload-eval`); nothing here is shipped to users.
10
+
11
+ ## Repos (selected via a memory-probe gate — NOT trained on)
12
+
13
+ Famous repos (express, excalidraw, n8n, …) are useless for *accuracy* evals: Sonnet answers their
14
+ flow questions from memory, so the no-homegraph baseline is dishonest. These four passed a no-tools
15
+ probe (Sonnet could not name their real flow internals) and are cloned fresh by `offload-eval-setup.sh`:
16
+
17
+ | tier | repo | ~src files | canonical flow |
18
+ |---|---|---|---|
19
+ | small | MTKruto/MTKruto | 322 TS | `sendMessage` → invoke → TL serialize → transport |
20
+ | medium | mvdicarlo/postybirb-plus | 608 TS | submission → queue → per-website `.post()` |
21
+ | complex | shapeshift/web | 3.2k TS (35-pkg monorepo) | swap → swapper registry → concrete swapper |
22
+ | large | trezor/trezor-suite | 8k TS monorepo | send-form → sign thunk → `@trezor/connect` |
23
+
24
+ Verified ground-truth flows (the judge's reference) live in `offload-eval-ground-truth.json`.
25
+
26
+ ## Arms
27
+
28
+ - **offload** — homegraph + managed offload ON (requires `homegraph login`); records AI tokens/credits via `HOMEGRAPH_OFFLOAD_USAGE_LOG`.
29
+ - **raw** — homegraph, `HOMEGRAPH_OFFLOAD_DISABLE=1` (returns raw source).
30
+ - **nocg** — empty MCP config; Read/Grep baseline.
31
+ - **frontload** — homegraph (offload-disabled) + a `UserPromptSubmit` hook (`offload-eval-hook.mjs`) that runs raw explore on the prompt and injects the result into context (approach 1).
32
+
33
+ ## Run it
34
+
35
+ ```bash
36
+ npm run build # the harness shells out to dist/
37
+ homegraph login # only needed for the offload arm
38
+ export AGENT_EVAL_OUT=/tmp/cg-offload-eval
39
+
40
+ bash scripts/agent-eval/offload-eval-setup.sh # clone + index the 4 repos
41
+ bash scripts/agent-eval/offload-eval-matrix.sh # 3 arms × 4 tiers × REPS (default 3)
42
+ node scripts/agent-eval/offload-eval-judge.mjs \
43
+ --results $AGENT_EVAL_OUT/results.jsonl \
44
+ --truth scripts/agent-eval/offload-eval-ground-truth.json \
45
+ --out $AGENT_EVAL_OUT/judged.jsonl
46
+ node scripts/agent-eval/offload-eval-summarize.mjs $AGENT_EVAL_OUT/judged.jsonl
47
+
48
+ bash scripts/agent-eval/offload-eval-frontload-matrix.sh # frontload arm + judge + merged summary
49
+ ```
50
+
51
+ Single repo: `offload-eval-3arm.sh <indexed-repo> <tier> <reps> "<question>"` (or `-frontload.sh`).
52
+
53
+ ## Files
54
+
55
+ - `offload-eval-setup.sh` — clone + index the 4 repos.
56
+ - `offload-eval-3arm.sh` / `-frontload.sh` — one repo, the arms.
57
+ - `offload-eval-matrix.sh` / `-frontload-matrix.sh` — drive all 4 tiers.
58
+ - `offload-eval-hook.mjs` — the front-load `UserPromptSubmit` hook (resolves its own engine; `CG_FRONTLOAD_DEBUG=<path>` to log injections; `CG_FRONTLOAD_BUDGET` to cap injected chars).
59
+ - `offload-eval-metrics.mjs` — one run's stream-json + usage log → one JSON metrics line.
60
+ - `offload-eval-judge.mjs` — Sonnet judge: end-to-end (agent final vs ground truth) + per-answer offload fidelity.
61
+ - `offload-eval-summarize.mjs` — per-tier, per-arm table + cross-repo roll-up.
62
+ - `offload-eval-ground-truth.json` — source-verified canonical flows.
63
+
64
+ ## Findings (2026-06, n=3 — direction consistent, magnitudes noisy)
65
+
66
+ - **Raw homegraph is the efficiency win** — ~nocg accuracy, fewer reads, faster, no AI cost.
67
+ - **The offload is the least-accurate arm in all 4 tiers** — synthesized fidelity 12–27/100 with
68
+ fabrication in 3/4 (e.g. invented website services; traced `ClientPlain`/`SessionPlain` instead of
69
+ the real encrypted path). Its speed/cost win is narrow (medium-only) and inversely correlated with
70
+ accuracy. **Use raw until offload fidelity is fixed.**
71
+ - **The front-load hook SOLVES adoption** — reads → 0–1 in every tier (incl. large, where the agent
72
+ otherwise read 12–24 files); fired 12/12, 0 errors. Wins on medium/complex (100% pass). But it
73
+ **regresses small/large to partial** — it suppresses the reads that compensate for explore's gaps at
74
+ **dynamic boundaries** (async queues, redux thunks, facade/factory indirection).
75
+ - **Master lever for BOTH:** explore's dynamic-dispatch coverage. Fix it → front-load is complete
76
+ everywhere and the offload has the full flow to synthesize.
@@ -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).');