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,93 +1,93 @@
1
- #!/usr/bin/env node
2
- // Parse the newest Claude Code session log for a project + its subagent logs,
3
- // and report the tool-call breakdown (main + subagents). Works for interactive
4
- // runs (driven via itrun.sh) — Claude Code writes full transcripts to
5
- // ~/.claude/projects/<escaped-cwd>/<session>.jsonl with subagents/ alongside.
6
- import { readFileSync, readdirSync, statSync, existsSync, realpathSync } from 'fs';
7
- import { join } from 'path';
8
- import { homedir } from 'os';
9
-
10
- const projectArg = process.argv[2];
11
- if (!projectArg) { console.error('usage: parse-session.mjs <project-dir>'); process.exit(1); }
12
-
13
- // Claude Code escapes the (real) cwd by replacing every "/" with "-".
14
- const real = realpathSync(projectArg);
15
- const escaped = real.replace(/\//g, '-');
16
- const projDir = join(homedir(), '.claude', 'projects', escaped);
17
- if (!existsSync(projDir)) { console.error('no session logs at', projDir); process.exit(1); }
18
-
19
- // Newest top-level session .jsonl
20
- const sessions = readdirSync(projDir)
21
- .filter(f => f.endsWith('.jsonl'))
22
- .map(f => ({ f, m: statSync(join(projDir, f)).mtimeMs }))
23
- .sort((a, b) => b.m - a.m);
24
- if (sessions.length === 0) { console.error('no .jsonl sessions in', projDir); process.exit(1); }
25
- const sessionId = sessions[0].f.replace('.jsonl', '');
26
-
27
- function tally(file) {
28
- const counts = {};
29
- for (const line of readFileSync(file, 'utf8').split('\n')) {
30
- if (!line) continue;
31
- let ev; try { ev = JSON.parse(line); } catch { continue; }
32
- const content = ev.message?.content;
33
- if (!Array.isArray(content)) continue;
34
- for (const b of content) {
35
- if (b.type === 'tool_use') counts[b.name] = (counts[b.name] || 0) + 1;
36
- }
37
- }
38
- return counts;
39
- }
40
-
41
- // Sum token usage from a transcript. The TUI's "Done (…Xk tokens…)" line only
42
- // covers a subagent's throughput; this works for main-thread runs too and is
43
- // consistent across both paths. `gen` = output, `fresh` = uncached input
44
- // (input + cache_creation), `cached` = cache reads (≈free), `total` = all.
45
- function sumTokens(file) {
46
- const t = { gen: 0, fresh: 0, cached: 0 };
47
- for (const line of readFileSync(file, 'utf8').split('\n')) {
48
- if (!line) continue;
49
- let ev; try { ev = JSON.parse(line); } catch { continue; }
50
- const u = ev.message?.usage;
51
- if (!u) continue;
52
- t.gen += u.output_tokens || 0;
53
- t.fresh += (u.input_tokens || 0) + (u.cache_creation_input_tokens || 0);
54
- t.cached += u.cache_read_input_tokens || 0;
55
- }
56
- return t;
57
- }
58
-
59
- const mainCounts = tally(join(projDir, sessionId + '.jsonl'));
60
-
61
- // Subagent transcripts live under <session>/subagents/*.jsonl
62
- const subDir = join(projDir, sessionId, 'subagents');
63
- const subCounts = {};
64
- let subAgentFiles = 0;
65
- if (existsSync(subDir)) {
66
- for (const f of readdirSync(subDir).filter(f => f.endsWith('.jsonl'))) {
67
- subAgentFiles++;
68
- const c = tally(join(subDir, f));
69
- for (const [k, v] of Object.entries(c)) subCounts[k] = (subCounts[k] || 0) + v;
70
- }
71
- }
72
-
73
- const fmt = (counts) => Object.entries(counts).sort((a, b) => b[1] - a[1])
74
- .map(([k, v]) => ` ${String(v).padStart(3)} ${k}`).join('\n') || ' (none)';
75
-
76
- console.log(`session: ${sessionId}`);
77
- console.log(`\nMAIN thread tools:\n${fmt(mainCounts)}`);
78
- console.log(`\nSUBAGENT tools (${subAgentFiles} subagent transcript${subAgentFiles === 1 ? '' : 's'}):\n${fmt(subCounts)}`);
79
-
80
- const explore = subCounts['mcp__homegraph__homegraph_explore'] || mainCounts['mcp__homegraph__homegraph_explore'] || 0;
81
- const reads = (subCounts['Read'] || 0) + (mainCounts['Read'] || 0);
82
- const greps = (subCounts['Grep'] || 0) + (mainCounts['Grep'] || 0) + (subCounts['Bash'] || 0) + (mainCounts['Bash'] || 0);
83
- console.log(`\nVERDICT: homegraph_explore used ${explore}x | Read ${reads} | Grep/Bash ${greps}`);
84
-
85
- // Token totals (main + subagents), consistent across main-thread and subagent runs.
86
- const tok = { gen: 0, fresh: 0, cached: 0 };
87
- const addTok = (t) => { tok.gen += t.gen; tok.fresh += t.fresh; tok.cached += t.cached; };
88
- addTok(sumTokens(join(projDir, sessionId + '.jsonl')));
89
- if (existsSync(subDir)) {
90
- for (const f of readdirSync(subDir).filter(f => f.endsWith('.jsonl'))) addTok(sumTokens(join(subDir, f)));
91
- }
92
- const k = (n) => (n / 1000).toFixed(1) + 'k';
93
- console.log(`TOKENS: gen ${k(tok.gen)} | fresh-in ${k(tok.fresh)} | cached-in ${k(tok.cached)} | billable≈ ${k(tok.gen + tok.fresh)}`);
1
+ #!/usr/bin/env node
2
+ // Parse the newest Claude Code session log for a project + its subagent logs,
3
+ // and report the tool-call breakdown (main + subagents). Works for interactive
4
+ // runs (driven via itrun.sh) — Claude Code writes full transcripts to
5
+ // ~/.claude/projects/<escaped-cwd>/<session>.jsonl with subagents/ alongside.
6
+ import { readFileSync, readdirSync, statSync, existsSync, realpathSync } from 'fs';
7
+ import { join } from 'path';
8
+ import { homedir } from 'os';
9
+
10
+ const projectArg = process.argv[2];
11
+ if (!projectArg) { console.error('usage: parse-session.mjs <project-dir>'); process.exit(1); }
12
+
13
+ // Claude Code escapes the (real) cwd by replacing every "/" with "-".
14
+ const real = realpathSync(projectArg);
15
+ const escaped = real.replace(/\//g, '-');
16
+ const projDir = join(homedir(), '.claude', 'projects', escaped);
17
+ if (!existsSync(projDir)) { console.error('no session logs at', projDir); process.exit(1); }
18
+
19
+ // Newest top-level session .jsonl
20
+ const sessions = readdirSync(projDir)
21
+ .filter(f => f.endsWith('.jsonl'))
22
+ .map(f => ({ f, m: statSync(join(projDir, f)).mtimeMs }))
23
+ .sort((a, b) => b.m - a.m);
24
+ if (sessions.length === 0) { console.error('no .jsonl sessions in', projDir); process.exit(1); }
25
+ const sessionId = sessions[0].f.replace('.jsonl', '');
26
+
27
+ function tally(file) {
28
+ const counts = {};
29
+ for (const line of readFileSync(file, 'utf8').split('\n')) {
30
+ if (!line) continue;
31
+ let ev; try { ev = JSON.parse(line); } catch { continue; }
32
+ const content = ev.message?.content;
33
+ if (!Array.isArray(content)) continue;
34
+ for (const b of content) {
35
+ if (b.type === 'tool_use') counts[b.name] = (counts[b.name] || 0) + 1;
36
+ }
37
+ }
38
+ return counts;
39
+ }
40
+
41
+ // Sum token usage from a transcript. The TUI's "Done (…Xk tokens…)" line only
42
+ // covers a subagent's throughput; this works for main-thread runs too and is
43
+ // consistent across both paths. `gen` = output, `fresh` = uncached input
44
+ // (input + cache_creation), `cached` = cache reads (≈free), `total` = all.
45
+ function sumTokens(file) {
46
+ const t = { gen: 0, fresh: 0, cached: 0 };
47
+ for (const line of readFileSync(file, 'utf8').split('\n')) {
48
+ if (!line) continue;
49
+ let ev; try { ev = JSON.parse(line); } catch { continue; }
50
+ const u = ev.message?.usage;
51
+ if (!u) continue;
52
+ t.gen += u.output_tokens || 0;
53
+ t.fresh += (u.input_tokens || 0) + (u.cache_creation_input_tokens || 0);
54
+ t.cached += u.cache_read_input_tokens || 0;
55
+ }
56
+ return t;
57
+ }
58
+
59
+ const mainCounts = tally(join(projDir, sessionId + '.jsonl'));
60
+
61
+ // Subagent transcripts live under <session>/subagents/*.jsonl
62
+ const subDir = join(projDir, sessionId, 'subagents');
63
+ const subCounts = {};
64
+ let subAgentFiles = 0;
65
+ if (existsSync(subDir)) {
66
+ for (const f of readdirSync(subDir).filter(f => f.endsWith('.jsonl'))) {
67
+ subAgentFiles++;
68
+ const c = tally(join(subDir, f));
69
+ for (const [k, v] of Object.entries(c)) subCounts[k] = (subCounts[k] || 0) + v;
70
+ }
71
+ }
72
+
73
+ const fmt = (counts) => Object.entries(counts).sort((a, b) => b[1] - a[1])
74
+ .map(([k, v]) => ` ${String(v).padStart(3)} ${k}`).join('\n') || ' (none)';
75
+
76
+ console.log(`session: ${sessionId}`);
77
+ console.log(`\nMAIN thread tools:\n${fmt(mainCounts)}`);
78
+ console.log(`\nSUBAGENT tools (${subAgentFiles} subagent transcript${subAgentFiles === 1 ? '' : 's'}):\n${fmt(subCounts)}`);
79
+
80
+ const explore = subCounts['mcp__homegraph__homegraph_explore'] || mainCounts['mcp__homegraph__homegraph_explore'] || 0;
81
+ const reads = (subCounts['Read'] || 0) + (mainCounts['Read'] || 0);
82
+ const greps = (subCounts['Grep'] || 0) + (mainCounts['Grep'] || 0) + (subCounts['Bash'] || 0) + (mainCounts['Bash'] || 0);
83
+ console.log(`\nVERDICT: homegraph_explore used ${explore}x | Read ${reads} | Grep/Bash ${greps}`);
84
+
85
+ // Token totals (main + subagents), consistent across main-thread and subagent runs.
86
+ const tok = { gen: 0, fresh: 0, cached: 0 };
87
+ const addTok = (t) => { tok.gen += t.gen; tok.fresh += t.fresh; tok.cached += t.cached; };
88
+ addTok(sumTokens(join(projDir, sessionId + '.jsonl')));
89
+ if (existsSync(subDir)) {
90
+ for (const f of readdirSync(subDir).filter(f => f.endsWith('.jsonl'))) addTok(sumTokens(join(subDir, f)));
91
+ }
92
+ const k = (n) => (n / 1000).toFixed(1) + 'k';
93
+ console.log(`TOKENS: gen ${k(tok.gen)} | fresh-in ${k(tok.fresh)} | cached-in ${k(tok.cached)} | billable≈ ${k(tok.gen + tok.fresh)}`);
@@ -1,21 +1,21 @@
1
- #!/usr/bin/env node
2
- // Probe homegraph_context (with call-paths) against an index using the built dist.
3
- // Usage: node probe-context.mjs <repo-with-.homegraph> <task words...>
4
- import { pathToFileURL } from 'node:url';
5
- import { resolve } from 'node:path';
6
-
7
- const [, , repo, ...taskParts] = process.argv;
8
- const task = taskParts.join(' ');
9
- if (!repo || !task) { console.error('usage: probe-context.mjs <repo> <task...>'); process.exit(1); }
10
-
11
- const load = async (rel) => import(pathToFileURL(resolve(rel)).href);
12
- const idx = await load('dist/index.js');
13
- const tools = await load('dist/mcp/tools.js');
14
- const HomeGraph = idx.default?.default ?? idx.default ?? idx.HomeGraph;
15
- const ToolHandler = tools.ToolHandler ?? tools.default?.ToolHandler;
16
-
17
- const cg = HomeGraph.openSync(repo);
18
- const h = new ToolHandler(cg);
19
- const res = await h.execute('homegraph_context', { task });
20
- console.log(res.content?.[0]?.text ?? '(no text)');
21
- try { cg.close?.(); } catch {}
1
+ #!/usr/bin/env node
2
+ // Probe homegraph_context (with call-paths) against an index using the built dist.
3
+ // Usage: node probe-context.mjs <repo-with-.homegraph> <task words...>
4
+ import { pathToFileURL } from 'node:url';
5
+ import { resolve } from 'node:path';
6
+
7
+ const [, , repo, ...taskParts] = process.argv;
8
+ const task = taskParts.join(' ');
9
+ if (!repo || !task) { console.error('usage: probe-context.mjs <repo> <task...>'); process.exit(1); }
10
+
11
+ const load = async (rel) => import(pathToFileURL(resolve(rel)).href);
12
+ const idx = await load('dist/index.js');
13
+ const tools = await load('dist/mcp/tools.js');
14
+ const HomeGraph = idx.default?.default ?? idx.default ?? idx.HomeGraph;
15
+ const ToolHandler = tools.ToolHandler ?? tools.default?.ToolHandler;
16
+
17
+ const cg = HomeGraph.openSync(repo);
18
+ const h = new ToolHandler(cg);
19
+ const res = await h.execute('homegraph_context', { task });
20
+ console.log(res.content?.[0]?.text ?? '(no text)');
21
+ try { cg.close?.(); } catch {}
@@ -1,40 +1,40 @@
1
- #!/usr/bin/env node
2
- // One-shot probe: run handleExplore against an existing index using the built
3
- // dist, print the output + a few stats. Lets us verify explore's coverage fix
4
- // without a full agent run. Usage: node probe-explore.mjs <repo-with-.homegraph> "<query>"
5
- import { pathToFileURL } from 'node:url';
6
- import { resolve } from 'node:path';
7
-
8
- const [, , repo, query] = process.argv;
9
- if (!repo || !query) {
10
- console.error('usage: probe-explore.mjs <repo> "<query>"');
11
- process.exit(1);
12
- }
13
-
14
- const load = async (rel) => import(pathToFileURL(resolve(rel)).href);
15
- const idx = await load('dist/index.js');
16
- const tools = await load('dist/mcp/tools.js');
17
-
18
- // esModuleInterop: dynamic import of CJS yields { default: module.exports, ...named }
19
- const HomeGraph = idx.default?.default ?? idx.default ?? idx.HomeGraph;
20
- const ToolHandler = tools.ToolHandler ?? tools.default?.ToolHandler;
21
-
22
- if (typeof HomeGraph?.openSync !== 'function') {
23
- console.error('could not resolve HomeGraph.openSync; index keys:', Object.keys(idx), 'default keys:', idx.default && Object.keys(idx.default));
24
- process.exit(2);
25
- }
26
- if (typeof ToolHandler !== 'function') {
27
- console.error('could not resolve ToolHandler; tools keys:', Object.keys(tools));
28
- process.exit(2);
29
- }
30
-
31
- const cg = HomeGraph.openSync(repo);
32
- const h = new ToolHandler(cg);
33
- const res = await h.execute('homegraph_explore', { query });
34
- const text = res.content?.[0]?.text ?? '(no text)';
35
- console.log(text);
36
- console.error('\n--- PROBE STATS ---');
37
- console.error('output chars:', text.length);
38
- console.error('triggerRender body present (-> setState({})):', /triggerRender[\s\S]{0,400}setState\(\{\}\)/.test(text));
39
- console.error('App.tsx in source section:', /\*\*`.*App\.tsx`\*\* —/.test(text));
40
- try { cg.close?.(); } catch {}
1
+ #!/usr/bin/env node
2
+ // One-shot probe: run handleExplore against an existing index using the built
3
+ // dist, print the output + a few stats. Lets us verify explore's coverage fix
4
+ // without a full agent run. Usage: node probe-explore.mjs <repo-with-.homegraph> "<query>"
5
+ import { pathToFileURL } from 'node:url';
6
+ import { resolve } from 'node:path';
7
+
8
+ const [, , repo, query] = process.argv;
9
+ if (!repo || !query) {
10
+ console.error('usage: probe-explore.mjs <repo> "<query>"');
11
+ process.exit(1);
12
+ }
13
+
14
+ const load = async (rel) => import(pathToFileURL(resolve(rel)).href);
15
+ const idx = await load('dist/index.js');
16
+ const tools = await load('dist/mcp/tools.js');
17
+
18
+ // esModuleInterop: dynamic import of CJS yields { default: module.exports, ...named }
19
+ const HomeGraph = idx.default?.default ?? idx.default ?? idx.HomeGraph;
20
+ const ToolHandler = tools.ToolHandler ?? tools.default?.ToolHandler;
21
+
22
+ if (typeof HomeGraph?.openSync !== 'function') {
23
+ console.error('could not resolve HomeGraph.openSync; index keys:', Object.keys(idx), 'default keys:', idx.default && Object.keys(idx.default));
24
+ process.exit(2);
25
+ }
26
+ if (typeof ToolHandler !== 'function') {
27
+ console.error('could not resolve ToolHandler; tools keys:', Object.keys(tools));
28
+ process.exit(2);
29
+ }
30
+
31
+ const cg = HomeGraph.openSync(repo);
32
+ const h = new ToolHandler(cg);
33
+ const res = await h.execute('homegraph_explore', { query });
34
+ const text = res.content?.[0]?.text ?? '(no text)';
35
+ console.log(text);
36
+ console.error('\n--- PROBE STATS ---');
37
+ console.error('output chars:', text.length);
38
+ console.error('triggerRender body present (-> setState({})):', /triggerRender[\s\S]{0,400}setState\(\{\}\)/.test(text));
39
+ console.error('App.tsx in source section:', /\*\*`.*App\.tsx`\*\* —/.test(text));
40
+ try { cg.close?.(); } catch {}
@@ -1,20 +1,20 @@
1
- #!/usr/bin/env node
2
- // Probe homegraph_node (with trail) against an index using the built dist.
3
- // Usage: node probe-node.mjs <repo-with-.homegraph> <symbol> [code]
4
- import { pathToFileURL } from 'node:url';
5
- import { resolve } from 'node:path';
6
-
7
- const [, , repo, symbol, code] = process.argv;
8
- if (!repo || !symbol) { console.error('usage: probe-node.mjs <repo> <symbol> [code]'); process.exit(1); }
9
-
10
- const load = async (rel) => import(pathToFileURL(resolve(rel)).href);
11
- const idx = await load('dist/index.js');
12
- const tools = await load('dist/mcp/tools.js');
13
- const HomeGraph = idx.default?.default ?? idx.default ?? idx.HomeGraph;
14
- const ToolHandler = tools.ToolHandler ?? tools.default?.ToolHandler;
15
-
16
- const cg = HomeGraph.openSync(repo);
17
- const h = new ToolHandler(cg);
18
- const res = await h.execute('homegraph_node', { symbol, includeCode: code === 'code' });
19
- console.log(res.content?.[0]?.text ?? '(no text)');
20
- try { cg.close?.(); } catch {}
1
+ #!/usr/bin/env node
2
+ // Probe homegraph_node (with trail) against an index using the built dist.
3
+ // Usage: node probe-node.mjs <repo-with-.homegraph> <symbol> [code]
4
+ import { pathToFileURL } from 'node:url';
5
+ import { resolve } from 'node:path';
6
+
7
+ const [, , repo, symbol, code] = process.argv;
8
+ if (!repo || !symbol) { console.error('usage: probe-node.mjs <repo> <symbol> [code]'); process.exit(1); }
9
+
10
+ const load = async (rel) => import(pathToFileURL(resolve(rel)).href);
11
+ const idx = await load('dist/index.js');
12
+ const tools = await load('dist/mcp/tools.js');
13
+ const HomeGraph = idx.default?.default ?? idx.default ?? idx.HomeGraph;
14
+ const ToolHandler = tools.ToolHandler ?? tools.default?.ToolHandler;
15
+
16
+ const cg = HomeGraph.openSync(repo);
17
+ const h = new ToolHandler(cg);
18
+ const res = await h.execute('homegraph_node', { symbol, includeCode: code === 'code' });
19
+ console.log(res.content?.[0]?.text ?? '(no text)');
20
+ try { cg.close?.(); } catch {}
@@ -1,119 +1,119 @@
1
- #!/usr/bin/env node
2
- // probe-sweep — direct MCP test across N repos × N tools, no claude needed.
3
- //
4
- // Measures response characteristics (size, sections present, signals fired)
5
- // for each (repo, query) pair against the built dist/. Sub-second per probe;
6
- // the full sweep below runs in ~10-30s vs hours for a real claude audit.
7
- //
8
- // Use this to iterate on backend changes rapidly: change tools.ts /
9
- // context-builder, npm run build, re-run probe-sweep, compare. Once a
10
- // change looks good on probe metrics, run a focused claude audit for the
11
- // few repos that matter to confirm end-to-end cost behavior.
12
- //
13
- // Usage: node scripts/agent-eval/probe-sweep.mjs [--tool=context|explore|trace] [--repos=a,b,c]
14
- import { pathToFileURL } from 'node:url';
15
- import { resolve } from 'node:path';
16
-
17
- const args = Object.fromEntries(
18
- process.argv.slice(2).map(a => a.startsWith('--') ? a.slice(2).split('=') : [a, true])
19
- );
20
- const TOOL = args.tool ?? 'context';
21
-
22
- const load = (rel) => import(pathToFileURL(resolve(rel)).href);
23
- const idx = await load('dist/index.js');
24
- const tools = await load('dist/mcp/tools.js');
25
- const HomeGraph = idx.default?.default ?? idx.default ?? idx.HomeGraph;
26
- const ToolHandler = tools.ToolHandler ?? tools.default?.ToolHandler;
27
-
28
- // Each entry: repo, query, optional 2nd arg for trace (from, to).
29
- // The query is the same prompt used in the real claude audits, so probe
30
- // output is directly comparable to the agent's would-be input.
31
- const SWEEP = [
32
- // Small realworld template repos (the loss cases from the cross-language sweep)
33
- { id: 'gin-rw', repo: '/tmp/homegraph-corpus/gin-realworld', q: 'How does this Gin app route a request through its middleware chain to a handler?' },
34
- { id: 'go-mux', repo: '/tmp/homegraph-corpus/go-mux', q: 'How does this gorilla/mux app route a request to its handler?' },
35
- { id: 'fastapi-rw', repo: '/tmp/homegraph-corpus/fastapi-realworld', q: 'How does FastAPI route a request through its dependencies to a handler?' },
36
- { id: 'spring-pc', repo: '/tmp/homegraph-corpus/spring-petclinic', q: 'How does Spring route an HTTP request to a controller method?' },
37
- { id: 'axum-rw', repo: '/tmp/homegraph-corpus/rust-axum-realworld', q: 'How does Axum route a request to its handler in this app?' },
38
- { id: 'express-rw', repo: '/tmp/homegraph-corpus/express-realworld', q: 'How does this Express app route a request through middleware to a handler?' },
39
- { id: 'kotlin-pc', repo: '/tmp/homegraph-corpus/kotlin-petclinic', q: 'How does the Kotlin Spring app route an HTTP request to its handler?' },
40
- { id: 'flask-mb', repo: '/tmp/homegraph-corpus/flask-microblog', q: 'How does this Flask app route a request to a view function?' },
41
- { id: 'vapor-tpl', repo: '/tmp/homegraph-corpus/vapor-template', q: 'How does Vapor route an HTTP request to its handler?' },
42
- { id: 'cpp-leveldb', repo: '/tmp/homegraph-corpus/cpp-leveldb', q: 'How does LevelDB handle a Put operation through to disk?' },
43
- { id: 'lualine', repo: '/tmp/homegraph-corpus/lualine.nvim', q: 'How does lualine assemble and render the statusline?' },
44
- { id: 'drupal-admin', repo: '/tmp/homegraph-corpus/drupal-admintoolbar', q: 'How does the Drupal admin toolbar module render its toolbar?' },
45
- { id: 'svelte-rw', repo: '/tmp/homegraph-corpus/svelte-realworld', q: 'How does this SvelteKit app route a request to a handler?' },
46
- { id: 'react-rw', repo: '/tmp/homegraph-corpus/react-realworld', q: 'How does this React app fetch and display articles?' },
47
- { id: 'rails-rw', repo: '/tmp/homegraph-corpus/rails-realworld', q: 'How does Rails route a request to a controller action?' },
48
- { id: 'flask-rest', repo: '/tmp/homegraph-corpus/flask-restful-realworld', q: 'How does Flask-RESTful route a request to a resource method?' },
49
- { id: 'laravel-rw', repo: '/tmp/homegraph-corpus/laravel-realworld', q: 'How does Laravel route a request to the controller method?' },
50
- { id: 'aspnet-rw', repo: '/tmp/homegraph-corpus/aspnet-realworld', q: 'How does ASP.NET route a request to the controller action?' },
51
- // The iter7 wins/ties (to make sure we don't regress)
52
- { id: 'cobra', repo: '/tmp/homegraph-corpus/cobra', q: 'How does cobra parse commands and flags?' },
53
- { id: 'sinatra', repo: '/tmp/homegraph-corpus/sinatra', q: 'How does sinatra route a request to its handler?' },
54
- { id: 'slim', repo: '/tmp/homegraph-corpus/slim', q: 'How does slim route a request and apply middleware?' },
55
- ];
56
-
57
- // Detect signals in response text — these are the levers we've added that
58
- // otherwise only show up via "agent ran X more tool calls" downstream.
59
- const detect = (text) => ({
60
- hasEntryPoints: /^### Entry Points/m.test(text),
61
- hasRelatedSymbols: /^### Related Symbols/m.test(text),
62
- hasFlowTrace: /^## Inline flow trace/m.test(text),
63
- hasRouteManifest: /^## Routing manifest/m.test(text),
64
- hasTopHandler: /^### Top handler file/m.test(text),
65
- hasSmallRepoTail: /This project is small/.test(text),
66
- });
67
-
68
- const filterRepos = args.repos ? new Set(String(args.repos).split(',')) : null;
69
- const subjects = SWEEP.filter(s => !filterRepos || filterRepos.has(s.id));
70
-
71
- const t0 = Date.now();
72
- const rows = [];
73
- for (const s of subjects) {
74
- try {
75
- const cg = HomeGraph.openSync(s.repo);
76
- const handler = new ToolHandler(cg);
77
- const t1 = Date.now();
78
- const res = await handler.execute('homegraph_' + TOOL,
79
- TOOL === 'context' ? { task: s.q } :
80
- TOOL === 'explore' ? { query: s.q } : { from: 'main', to: 'main' });
81
- const text = res.content?.[0]?.text ?? '';
82
- const signals = detect(text);
83
- rows.push({
84
- id: s.id,
85
- ms: Date.now() - t1,
86
- chars: text.length,
87
- lines: text.split('\n').length,
88
- ...signals,
89
- });
90
- try { cg.close?.(); } catch {}
91
- } catch (e) {
92
- rows.push({ id: s.id, error: String(e).slice(0, 80) });
93
- }
94
- }
95
-
96
- // Pretty-print as a compact table.
97
- const fmt = (r) =>
98
- r.error
99
- ? ` ${r.id.padEnd(13)} ERROR: ${r.error}`
100
- : ` ${r.id.padEnd(13)} ${String(r.chars).padStart(6)}c ${String(r.lines).padStart(4)}L ${String(r.ms).padStart(4)}ms` +
101
- ` ${r.hasEntryPoints ? 'EP ' : ' '}` +
102
- `${r.hasFlowTrace ? 'TRC ' : ' '}` +
103
- `${r.hasRouteManifest ? 'MAN ' : ' '}` +
104
- `${r.hasTopHandler ? 'HND ' : ' '}` +
105
- `${r.hasSmallRepoTail ? 'TAIL' : ' '}`;
106
- console.log(`=== probe-sweep tool=${TOOL} n=${subjects.length} (${Date.now() - t0}ms total) ===`);
107
- console.log(' id chars lines ms signals');
108
- console.log(' ' + '-'.repeat(56));
109
- for (const r of rows) console.log(fmt(r));
110
-
111
- // Sum + medians for the size pillar
112
- const sizes = rows.filter(r => !r.error).map(r => r.chars);
113
- sizes.sort((a, b) => a - b);
114
- const median = sizes[Math.floor(sizes.length / 2)];
115
- const sum = sizes.reduce((a, b) => a + b, 0);
116
- console.log(` ${'-'.repeat(64)}`);
117
- console.log(` median=${median}c total=${sum}c ` +
118
- `manifest=${rows.filter(r => r.hasRouteManifest).length}/${rows.filter(r => !r.error).length} ` +
119
- `top-handler=${rows.filter(r => r.hasTopHandler).length}/${rows.filter(r => !r.error).length}`);
1
+ #!/usr/bin/env node
2
+ // probe-sweep — direct MCP test across N repos × N tools, no claude needed.
3
+ //
4
+ // Measures response characteristics (size, sections present, signals fired)
5
+ // for each (repo, query) pair against the built dist/. Sub-second per probe;
6
+ // the full sweep below runs in ~10-30s vs hours for a real claude audit.
7
+ //
8
+ // Use this to iterate on backend changes rapidly: change tools.ts /
9
+ // context-builder, npm run build, re-run probe-sweep, compare. Once a
10
+ // change looks good on probe metrics, run a focused claude audit for the
11
+ // few repos that matter to confirm end-to-end cost behavior.
12
+ //
13
+ // Usage: node scripts/agent-eval/probe-sweep.mjs [--tool=context|explore|trace] [--repos=a,b,c]
14
+ import { pathToFileURL } from 'node:url';
15
+ import { resolve } from 'node:path';
16
+
17
+ const args = Object.fromEntries(
18
+ process.argv.slice(2).map(a => a.startsWith('--') ? a.slice(2).split('=') : [a, true])
19
+ );
20
+ const TOOL = args.tool ?? 'context';
21
+
22
+ const load = (rel) => import(pathToFileURL(resolve(rel)).href);
23
+ const idx = await load('dist/index.js');
24
+ const tools = await load('dist/mcp/tools.js');
25
+ const HomeGraph = idx.default?.default ?? idx.default ?? idx.HomeGraph;
26
+ const ToolHandler = tools.ToolHandler ?? tools.default?.ToolHandler;
27
+
28
+ // Each entry: repo, query, optional 2nd arg for trace (from, to).
29
+ // The query is the same prompt used in the real claude audits, so probe
30
+ // output is directly comparable to the agent's would-be input.
31
+ const SWEEP = [
32
+ // Small realworld template repos (the loss cases from the cross-language sweep)
33
+ { id: 'gin-rw', repo: '/tmp/homegraph-corpus/gin-realworld', q: 'How does this Gin app route a request through its middleware chain to a handler?' },
34
+ { id: 'go-mux', repo: '/tmp/homegraph-corpus/go-mux', q: 'How does this gorilla/mux app route a request to its handler?' },
35
+ { id: 'fastapi-rw', repo: '/tmp/homegraph-corpus/fastapi-realworld', q: 'How does FastAPI route a request through its dependencies to a handler?' },
36
+ { id: 'spring-pc', repo: '/tmp/homegraph-corpus/spring-petclinic', q: 'How does Spring route an HTTP request to a controller method?' },
37
+ { id: 'axum-rw', repo: '/tmp/homegraph-corpus/rust-axum-realworld', q: 'How does Axum route a request to its handler in this app?' },
38
+ { id: 'express-rw', repo: '/tmp/homegraph-corpus/express-realworld', q: 'How does this Express app route a request through middleware to a handler?' },
39
+ { id: 'kotlin-pc', repo: '/tmp/homegraph-corpus/kotlin-petclinic', q: 'How does the Kotlin Spring app route an HTTP request to its handler?' },
40
+ { id: 'flask-mb', repo: '/tmp/homegraph-corpus/flask-microblog', q: 'How does this Flask app route a request to a view function?' },
41
+ { id: 'vapor-tpl', repo: '/tmp/homegraph-corpus/vapor-template', q: 'How does Vapor route an HTTP request to its handler?' },
42
+ { id: 'cpp-leveldb', repo: '/tmp/homegraph-corpus/cpp-leveldb', q: 'How does LevelDB handle a Put operation through to disk?' },
43
+ { id: 'lualine', repo: '/tmp/homegraph-corpus/lualine.nvim', q: 'How does lualine assemble and render the statusline?' },
44
+ { id: 'drupal-admin', repo: '/tmp/homegraph-corpus/drupal-admintoolbar', q: 'How does the Drupal admin toolbar module render its toolbar?' },
45
+ { id: 'svelte-rw', repo: '/tmp/homegraph-corpus/svelte-realworld', q: 'How does this SvelteKit app route a request to a handler?' },
46
+ { id: 'react-rw', repo: '/tmp/homegraph-corpus/react-realworld', q: 'How does this React app fetch and display articles?' },
47
+ { id: 'rails-rw', repo: '/tmp/homegraph-corpus/rails-realworld', q: 'How does Rails route a request to a controller action?' },
48
+ { id: 'flask-rest', repo: '/tmp/homegraph-corpus/flask-restful-realworld', q: 'How does Flask-RESTful route a request to a resource method?' },
49
+ { id: 'laravel-rw', repo: '/tmp/homegraph-corpus/laravel-realworld', q: 'How does Laravel route a request to the controller method?' },
50
+ { id: 'aspnet-rw', repo: '/tmp/homegraph-corpus/aspnet-realworld', q: 'How does ASP.NET route a request to the controller action?' },
51
+ // The iter7 wins/ties (to make sure we don't regress)
52
+ { id: 'cobra', repo: '/tmp/homegraph-corpus/cobra', q: 'How does cobra parse commands and flags?' },
53
+ { id: 'sinatra', repo: '/tmp/homegraph-corpus/sinatra', q: 'How does sinatra route a request to its handler?' },
54
+ { id: 'slim', repo: '/tmp/homegraph-corpus/slim', q: 'How does slim route a request and apply middleware?' },
55
+ ];
56
+
57
+ // Detect signals in response text — these are the levers we've added that
58
+ // otherwise only show up via "agent ran X more tool calls" downstream.
59
+ const detect = (text) => ({
60
+ hasEntryPoints: /^### Entry Points/m.test(text),
61
+ hasRelatedSymbols: /^### Related Symbols/m.test(text),
62
+ hasFlowTrace: /^## Inline flow trace/m.test(text),
63
+ hasRouteManifest: /^## Routing manifest/m.test(text),
64
+ hasTopHandler: /^### Top handler file/m.test(text),
65
+ hasSmallRepoTail: /This project is small/.test(text),
66
+ });
67
+
68
+ const filterRepos = args.repos ? new Set(String(args.repos).split(',')) : null;
69
+ const subjects = SWEEP.filter(s => !filterRepos || filterRepos.has(s.id));
70
+
71
+ const t0 = Date.now();
72
+ const rows = [];
73
+ for (const s of subjects) {
74
+ try {
75
+ const cg = HomeGraph.openSync(s.repo);
76
+ const handler = new ToolHandler(cg);
77
+ const t1 = Date.now();
78
+ const res = await handler.execute('homegraph_' + TOOL,
79
+ TOOL === 'context' ? { task: s.q } :
80
+ TOOL === 'explore' ? { query: s.q } : { from: 'main', to: 'main' });
81
+ const text = res.content?.[0]?.text ?? '';
82
+ const signals = detect(text);
83
+ rows.push({
84
+ id: s.id,
85
+ ms: Date.now() - t1,
86
+ chars: text.length,
87
+ lines: text.split('\n').length,
88
+ ...signals,
89
+ });
90
+ try { cg.close?.(); } catch {}
91
+ } catch (e) {
92
+ rows.push({ id: s.id, error: String(e).slice(0, 80) });
93
+ }
94
+ }
95
+
96
+ // Pretty-print as a compact table.
97
+ const fmt = (r) =>
98
+ r.error
99
+ ? ` ${r.id.padEnd(13)} ERROR: ${r.error}`
100
+ : ` ${r.id.padEnd(13)} ${String(r.chars).padStart(6)}c ${String(r.lines).padStart(4)}L ${String(r.ms).padStart(4)}ms` +
101
+ ` ${r.hasEntryPoints ? 'EP ' : ' '}` +
102
+ `${r.hasFlowTrace ? 'TRC ' : ' '}` +
103
+ `${r.hasRouteManifest ? 'MAN ' : ' '}` +
104
+ `${r.hasTopHandler ? 'HND ' : ' '}` +
105
+ `${r.hasSmallRepoTail ? 'TAIL' : ' '}`;
106
+ console.log(`=== probe-sweep tool=${TOOL} n=${subjects.length} (${Date.now() - t0}ms total) ===`);
107
+ console.log(' id chars lines ms signals');
108
+ console.log(' ' + '-'.repeat(56));
109
+ for (const r of rows) console.log(fmt(r));
110
+
111
+ // Sum + medians for the size pillar
112
+ const sizes = rows.filter(r => !r.error).map(r => r.chars);
113
+ sizes.sort((a, b) => a - b);
114
+ const median = sizes[Math.floor(sizes.length / 2)];
115
+ const sum = sizes.reduce((a, b) => a + b, 0);
116
+ console.log(` ${'-'.repeat(64)}`);
117
+ console.log(` median=${median}c total=${sum}c ` +
118
+ `manifest=${rows.filter(r => r.hasRouteManifest).length}/${rows.filter(r => !r.error).length} ` +
119
+ `top-handler=${rows.filter(r => r.hasTopHandler).length}/${rows.filter(r => !r.error).length}`);