| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163 |
- #!/usr/bin/env node
- /**
- * "Did the symbol the agent NAMED actually render?" (CG-38).
- *
- * This is the measurement the whole CG-24 epic was missing. Every other probe
- * here scores the response in AGGREGATE — `probe-suite-envelope.mjs` measures how
- * much source came back, `probe-file-spend.mjs` measures whether the bytes went
- * to the files that earned them, `probe-allocation.mjs` measures group shares.
- * All three are green on a response that returns 25K of source from the right
- * file and still omits the one function the agent asked for by name. That is
- * exactly what CG-38 was: `queueMessage` at L1087 of a 1,414-line file, whose
- * file won rank #1 with 67% of the envelope, never rendered — the agent got a
- * same-stem `QueuedMessage` INTERFACE at L70 instead and had to Read the file.
- *
- * So the assertion here is per-SYMBOL and binary: for each named symbol, does its
- * definition line appear in the rendered source? Nothing else can substitute —
- * not the file being present, not its share, not its byte count.
- *
- * Usage (needs a current `npm run build`):
- * node scripts/agent-eval/probe-named-symbol.mjs
- * node scripts/agent-eval/probe-named-symbol.mjs --verbose
- * # any indexed repo, ad hoc:
- * node scripts/agent-eval/probe-named-symbol.mjs <repo> "<query>" sym1 sym2
- *
- * Exit code is 1 when any expected symbol is missing, so this can gate.
- */
- import { cpSync, mkdtempSync, rmSync, readFileSync, existsSync } from 'node:fs';
- import { tmpdir } from 'node:os';
- import { join, resolve, dirname } from 'node:path';
- import { fileURLToPath, pathToFileURL } from 'node:url';
- const HERE = dirname(fileURLToPath(import.meta.url));
- const REPO = resolve(HERE, '..', '..');
- const load = async (rel) => import(pathToFileURL(resolve(REPO, rel)).href);
- const idxMod = await load('dist/index.js');
- const toolsMod = await load('dist/mcp/tools.js');
- const CodeGraph = idxMod.default?.default ?? idxMod.default ?? idxMod.CodeGraph;
- const { ToolHandler } = toolsMod;
- /**
- * The fixture cases. `symbols` are what the agent names; each must come back with
- * its DEFINITION rendered. The queries deliberately cover both shapes the bug was
- * reported on — a bare symbol bag and a prose question — because the failure had
- * a different cause on each and a fix for one does not imply the other.
- */
- const FIXTURE = '__tests__/fixtures/tail-render-ts';
- const CASES = [
- {
- id: 'tail-symbol-bag',
- why: 'two sibling closures past L1000, named directly; neither calls the other',
- query: 'queueMessage flushQueuedMessages',
- symbols: ['queueMessage', 'flushQueuedMessages'],
- },
- {
- id: 'tail-prose',
- why: 'same two symbols named inside a prose question',
- query: 'how does queueMessage hand its entries to flushQueuedMessages',
- symbols: ['queueMessage', 'flushQueuedMessages'],
- },
- {
- id: 'tail-with-decoy',
- why: 'the same-stem QueuedMessage interface at L70 must not stand in for the functions',
- query: 'explain queueMessage, removeQueuedMessage and flushQueuedMessages',
- symbols: ['queueMessage', 'removeQueuedMessage', 'flushQueuedMessages'],
- },
- ];
- /** Every `<n>\t<text>` line number present in the response's source blocks. */
- function renderedLines(response) {
- const out = new Set();
- for (const m of response.matchAll(/^(\d+)\t/gm)) out.add(Number(m[1]));
- return out;
- }
- /**
- * A symbol counts as rendered only when its DECLARATION line is among the lines
- * the response actually sent — not when its name merely appears somewhere (it
- * shows up in the section header symbol list and in call sites regardless, which
- * is precisely how this defect hid for a whole epic).
- */
- function check(cg, response, names) {
- const lines = renderedLines(response);
- return names.map((name) => {
- const node = (cg.getNodesByName?.(name) ?? []).find((n) => n.startLine > 0);
- return {
- name,
- file: node?.filePath ?? '(not indexed)',
- line: node?.startLine ?? 0,
- rendered: !!node && lines.has(node.startLine),
- };
- });
- }
- async function runCase(root, { query, symbols }) {
- const cg = CodeGraph.openSync(root);
- try {
- const res = await new ToolHandler(cg).execute('codegraph_explore', { query });
- const response = res.content?.[0]?.text ?? '';
- return { response, results: check(cg, response, symbols) };
- } finally {
- try { cg.close?.(); } catch { /* already closed */ }
- }
- }
- const argv = process.argv.slice(2);
- const verbose = argv.includes('--verbose');
- const positional = argv.filter((a) => !a.startsWith('--'));
- let failures = 0;
- let checked = 0;
- if (positional.length >= 3) {
- // Ad-hoc mode: <repo> "<query>" sym...
- const [repo, query, ...symbols] = positional;
- const { response, results } = await runCase(resolve(repo), { query, symbols });
- console.log(`\n${repo}\n query "${query}" · ${response.length} chars\n`);
- for (const r of results) {
- checked += 1;
- if (!r.rendered) failures += 1;
- console.log(` ${r.rendered ? 'PASS' : 'FAIL'} ${r.name} ${r.file}:${r.line}`);
- }
- } else {
- const src = join(REPO, FIXTURE);
- if (!existsSync(src)) {
- console.error(`fixture missing: ${src}`);
- process.exit(2);
- }
- const dir = mkdtempSync(join(tmpdir(), 'cg-named-'));
- try {
- cpSync(src, dir, { recursive: true });
- rmSync(join(dir, '.codegraph'), { recursive: true, force: true });
- const cg = CodeGraph.initSync(dir);
- await cg.indexAll();
- cg.close?.();
- console.log(`\ntail-render-ts · agent-named symbols must render\n`);
- for (const c of CASES) {
- const { response, results } = await runCase(dir, c);
- console.log(`── ${c.id} — ${c.why}`);
- console.log(` query "${c.query}"`);
- console.log(` response ${response.length.toLocaleString()} chars`);
- for (const r of results) {
- checked += 1;
- if (!r.rendered) failures += 1;
- console.log(` ${r.rendered ? 'PASS' : 'FAIL'} ${r.name} defined at ${r.file}:${r.line}`
- + (r.rendered ? '' : ' — DEFINITION NOT IN RESPONSE'));
- }
- if (verbose && failures) {
- const spans = [...renderedLines(response)].sort((a, b) => a - b);
- console.log(` rendered lines: ${spans[0]}..${spans[spans.length - 1]} (${spans.length} lines)`);
- }
- console.log();
- }
- } finally {
- rmSync(dir, { recursive: true, force: true });
- }
- }
- console.log(failures === 0
- ? `Every agent-named symbol rendered (${checked} checked).`
- : `${failures} of ${checked} agent-named symbols did NOT render.`);
- process.exit(failures === 0 ? 0 : 1);
|