probe-suite-envelope.mjs 5.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124
  1. #!/usr/bin/env node
  2. /**
  3. * Deterministic 6-repo envelope sweep for `codegraph_explore` (CG-26).
  4. *
  5. * The allocation issues (CG-30 / CG-31 / CG-26) are all decided by how the
  6. * render loop divides a fixed byte ceiling, and the agent A/B is far too noisy
  7. * to see a 2K byte shift. This runs the SAME six queries the CG-30 and CG-31
  8. * benchmark tables use, against the same clean-rebuilt corpus indexes, and
  9. * prints the numbers those tables are made of: source chars delivered, files in
  10. * the final output, whether the hard ceiling cut anything, and whether the
  11. * epilogue survived.
  12. *
  13. * Numbers come from the CG-4 diagnostic (`CODEGRAPH_EXPLORE_DEBUG`), so this
  14. * measures the shipping allocator rather than re-deriving shares from markdown.
  15. *
  16. * Usage (needs a current `npm run build`, and full-REBUILT indexes — CG-33):
  17. * node scripts/agent-eval/probe-suite-envelope.mjs
  18. * node scripts/agent-eval/probe-suite-envelope.mjs --json > /tmp/new.json
  19. * node scripts/agent-eval/probe-suite-envelope.mjs --baseline /tmp/base.json
  20. * CORPUS=/tmp/codegraph-corpus node scripts/agent-eval/probe-suite-envelope.mjs
  21. */
  22. import { mkdtempSync, readFileSync, rmSync, existsSync } from 'node:fs';
  23. import { tmpdir } from 'node:os';
  24. import { join, resolve } from 'node:path';
  25. import { pathToFileURL } from 'node:url';
  26. const CORPUS = process.env.CORPUS ?? '/tmp/codegraph-corpus';
  27. /** The six suite repos + the exact queries the CG-30/CG-31 tables were measured on. */
  28. const SUITE = [
  29. { id: 'django', q: 'How does a QuerySet turn into SQL and fetch rows from the database?' },
  30. { id: 'excalidraw', q: 'How does updating an element re-render the canvas on screen?' },
  31. { id: 'okhttp', q: 'How does a call go through the interceptor chain to the network?' },
  32. { id: 'tokio', q: 'How does a spawned task get scheduled and run by a worker?' },
  33. { id: 'gin', q: 'How does a registered route handler get invoked for an incoming HTTP request?' },
  34. { id: 'alamofire', q: 'How does a request get built and sent through the session?' },
  35. ];
  36. const argv = process.argv.slice(2);
  37. const asJson = argv.includes('--json');
  38. const baselineAt = argv.includes('--baseline') ? argv[argv.indexOf('--baseline') + 1] : null;
  39. const only = argv.filter((a) => !a.startsWith('--') && a !== baselineAt);
  40. const say = (s = '') => { if (!asJson) console.log(s); };
  41. const num = (n) => Math.round(n).toLocaleString('en-US');
  42. const load = (rel) => import(pathToFileURL(resolve(rel)).href);
  43. const idx = await load('dist/index.js');
  44. const toolsMod = await load('dist/mcp/tools.js');
  45. const CodeGraph = idx.default?.default ?? idx.default ?? idx.CodeGraph;
  46. const ToolHandler = toolsMod.ToolHandler ?? toolsMod.default?.ToolHandler;
  47. if (typeof CodeGraph?.openSync !== 'function' || typeof ToolHandler !== 'function') {
  48. console.error('could not resolve CodeGraph/ToolHandler from dist/ — run `npm run build`');
  49. process.exit(2);
  50. }
  51. const tmp = mkdtempSync(join(tmpdir(), 'cg-suite-'));
  52. const results = [];
  53. try {
  54. for (const { id, q } of SUITE) {
  55. if (only.length > 0 && !only.includes(id)) continue;
  56. const repo = join(CORPUS, id);
  57. if (!existsSync(join(repo, '.codegraph', 'codegraph.db'))) {
  58. say(`${id}: no index at ${repo} — skipped`);
  59. continue;
  60. }
  61. const sidecar = join(tmp, `${id}.jsonl`);
  62. process.env.CODEGRAPH_EXPLORE_DEBUG = sidecar;
  63. const cg = CodeGraph.openSync(repo);
  64. const h = new ToolHandler(cg);
  65. const res = await h.execute('codegraph_explore', { query: q });
  66. const text = res.content?.[0]?.text ?? '';
  67. try { cg.close?.(); } catch { /* best effort */ }
  68. const report = JSON.parse(readFileSync(sidecar, 'utf8').trim().split('\n').pop());
  69. results.push({
  70. repo: id,
  71. sourceChars: report.envelope.sourceChars,
  72. envelopeChars: report.envelope.chars,
  73. allocatedChars: report.envelope.allocatedChars,
  74. hardCeiling: report.budget.hardCeiling,
  75. truncated: report.envelope.truncated,
  76. files: report.selection.filesInFinalOutput,
  77. // Did the response keep its trailing pointer list / notes, or did the
  78. // hard ceiling spend them? This is CG-26's residual 1.
  79. epilogueCut: text.includes('omitted for size'),
  80. sectionCut: text.includes('output truncated to budget'),
  81. notShown: text.includes('Not shown above'),
  82. budgetNote: text.includes('**Explore budget:'),
  83. });
  84. }
  85. } finally {
  86. rmSync(tmp, { recursive: true, force: true });
  87. }
  88. if (asJson) {
  89. console.log(JSON.stringify(results, null, 2));
  90. } else {
  91. const base = baselineAt ? JSON.parse(readFileSync(baselineAt, 'utf8')) : null;
  92. const byRepo = new Map((base ?? []).map((r) => [r.repo, r]));
  93. say('repo source Δ env files cut epilogue');
  94. say('-'.repeat(74));
  95. for (const r of results) {
  96. const b = byRepo.get(r.repo);
  97. const delta = b ? (r.sourceChars - b.sourceChars) : null;
  98. const dStr = delta === null ? '' : (delta > 0 ? `+${num(delta)}` : num(delta));
  99. const cut = r.sectionCut ? 'section' : r.epilogueCut ? 'epilogue' : '—';
  100. const epi = [r.notShown ? 'not-shown' : null, r.budgetNote ? 'budget-note' : null]
  101. .filter(Boolean).join('+') || 'none';
  102. say(
  103. `${r.repo.padEnd(12)} ${num(r.sourceChars).padStart(7)} ${dStr.padStart(8)} `
  104. + `${num(r.envelopeChars).padStart(7)} ${String(r.files).padStart(5)} ${cut.padEnd(12)} ${epi}`,
  105. );
  106. }
  107. if (base) {
  108. const lost = results.filter((r) => {
  109. const b = byRepo.get(r.repo);
  110. return b && (r.sourceChars < b.sourceChars || r.files < b.files);
  111. });
  112. say('');
  113. say(lost.length === 0
  114. ? 'No repo delivers less source or fewer files than the baseline.'
  115. : `REGRESSION: ${lost.map((r) => r.repo).join(', ')} deliver less than baseline.`);
  116. }
  117. }