feat(extraction): enable same-file value-reference edges by default (TS/JS) (#895)

Value-reference edges (same-file `references` edges from a reader to the
file-scope const/var it reads) shipped behind CODEGRAPH_VALUE_REFS pending an
agent A/B. The A/B is in: on excalidraw the edges are correct and precise (node
count unchanged) and they transform the impact/blast-radius API — `impact` on a
const consumed by 103 readers goes from 1 affected symbol to the full radius.
That blast-radius API is what `codegraph impact` and CodeGraph Pro's verdict
engine consume, so the win is impact correctness; the agent path showed no
regression. Flip the default on; CODEGRAPH_VALUE_REFS=0 disables.

Also close the one precision gap the A/B surfaced: a bundled/Emscripten
`const Module` re-declared as an inner `var Module` / param produced false
positives (nested readers resolve to the inner binding). isGeneratedFile() is
path-only and can't catch content-minified bundles, so prune SHADOWED targets at
the syntax level — drop any value-ref target whose name is bound by more than one
`variable_declarator` in the file. On excalidraw this removes the 23 false
positives while preserving every real reader (impact unchanged at 170).

Adds regression coverage (there was none): same-file readers are edged, they
surface in the impact radius, shadowed consts are NOT edged, and
CODEGRAPH_VALUE_REFS=0 emits nothing.

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Colby Mchenry
2026-06-15 01:28:00 -05:00
committed by GitHub
co-authored by Claude Opus 4.8
parent b49147eab0
commit 2f6316500d
3 changed files with 159 additions and 5 deletions
+117
View File
@@ -0,0 +1,117 @@
/**
* Value-reference edges (TS/JS): same-file `references` edges from a reader
* symbol to the file-scope const/var it reads, so impact analysis catches
* "change this constant, affect its readers". Default on; CODEGRAPH_VALUE_REFS=0
* disables. See TreeSitterExtractor.flushValueRefs.
*/
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
import * as fs from 'fs';
import * as path from 'path';
import * as os from 'os';
import CodeGraph from '../src';
function valueRefReaders(cg: CodeGraph, constName: string): string[] {
const target = cg.searchNodes(constName).map((r) => r.node).find((n) => n.name === constName);
if (!target) return [];
return cg
.getIncomingEdges(target.id)
.filter((e) => e.kind === 'references' && (e.metadata as { valueRef?: boolean } | undefined)?.valueRef)
.map((e) => cg.getNode(e.source)?.name)
.filter((n): n is string => Boolean(n));
}
describe('value-reference edges', () => {
let dir: string;
let cg: CodeGraph | undefined;
beforeEach(() => {
dir = fs.mkdtempSync(path.join(os.tmpdir(), 'codegraph-valueref-'));
});
afterEach(() => {
cg?.destroy();
cg = undefined;
fs.rmSync(dir, { recursive: true, force: true });
});
function index(): CodeGraph {
const g = CodeGraph.initSync(dir, { config: { include: ['**/*.ts', '**/*.tsx'], exclude: [] } });
return g;
}
it('edges same-file readers to the file-scope const they read (default on)', async () => {
fs.writeFileSync(
path.join(dir, 'config.ts'),
[
'export const TABLE_CONFIG = { rows: 10, cols: 4 };',
'export function rowCount() { return TABLE_CONFIG.rows; }',
'export function describeTable() { return `${TABLE_CONFIG.rows}x${TABLE_CONFIG.cols}`; }',
'export const HEADER = TABLE_CONFIG.cols;',
].join('\n'),
);
cg = index();
await cg.indexAll();
const readers = valueRefReaders(cg, 'TABLE_CONFIG');
// rowCount, describeTable, and the HEADER const all read TABLE_CONFIG.
expect(readers).toEqual(expect.arrayContaining(['rowCount', 'describeTable', 'HEADER']));
});
it('surfaces those readers in the impact radius of the const', async () => {
fs.writeFileSync(
path.join(dir, 'palette.ts'),
[
'export const COLOR_PALETTE = { red: "#f00", blue: "#00f" };',
'export function pickRed() { return COLOR_PALETTE.red; }',
].join('\n'),
);
cg = index();
await cg.indexAll();
const target = cg.searchNodes('COLOR_PALETTE').map((r) => r.node).find((n) => n.name === 'COLOR_PALETTE')!;
const impacted = [...cg.getImpactRadius(target.id).nodes.values()].map((n) => n.name);
expect(impacted).toContain('pickRed');
});
it('does NOT edge a shadowed const — inner re-declaration makes the name ambiguous', async () => {
// The Emscripten/bundled pattern: a file-scope `const Module` re-declared as
// an inner `var Module` / param. Nested readers resolve to the INNER binding,
// so a file-scope edge would be a false positive. The shadow guard drops it.
fs.writeFileSync(
path.join(dir, 'bundled.ts'),
[
'const Module = (function () {',
' return function (Module) {',
' var Module = typeof Module !== "undefined" ? Module : {};',
' function locate() { return Module.path; }',
' function getFunc() { return Module.lookup; }',
' return { locate, getFunc };',
' };',
'})();',
'export default Module;',
].join('\n'),
);
cg = index();
await cg.indexAll();
// No reader should be edged to the outer `const Module`.
expect(valueRefReaders(cg, 'Module')).toEqual([]);
});
it('emits nothing when CODEGRAPH_VALUE_REFS=0', async () => {
const prev = process.env.CODEGRAPH_VALUE_REFS;
process.env.CODEGRAPH_VALUE_REFS = '0';
try {
fs.writeFileSync(
path.join(dir, 'config.ts'),
['export const TABLE_CONFIG = { rows: 10 };', 'export function rowCount() { return TABLE_CONFIG.rows; }'].join('\n'),
);
cg = index();
await cg.indexAll();
expect(valueRefReaders(cg, 'TABLE_CONFIG')).toEqual([]);
} finally {
if (prev === undefined) delete process.env.CODEGRAPH_VALUE_REFS;
else process.env.CODEGRAPH_VALUE_REFS = prev;
}
});
});