Summary
When a function makes a call through a function-valued variable (campoFn(r), c.get(r), pred(r)) and an argument of that call is an identifier that shares its name with an unrelated symbol elsewhere in the repo, graphify emits an indirect_call edge from the calling function to that unrelated symbol, with confidence 0.8.
Single-letter identifiers make this explode: a file-local one-letter test helper (const r = ... inside *.test.ts) became the target of fabricated indirect_call edges from every function in the repo that passes a variable named r to a callback.
Version / environment
- graphifyy 0.9.28 (PyPI), installed via
uv tool install graphifyy==0.9.28
- Windows 11, Python 3.13
graphify . --code-only (pure AST path, no LLM)
Minimal repro (2 files)
src/round.test.ts:
import { describe, it, expect } from 'vitest';
// Local one-letter rounding helper, private to this test file.
const r = (v: number) => Math.round(v * 100) / 100;
describe('rounding', () => {
it('rounds to 2 decimals', () => {
expect(r(1.005)).toBe(1.01);
});
});
src/report.ts:
type Row = { t: string; v: number };
type ColDef = { label: string; get: (row: Row) => number };
// None of these functions reference the test helper `r` from round.test.ts.
// The only `r` below is a loop variable / arrow parameter used as an ARGUMENT
// to calls made through function-valued variables (dispatch).
export function serieTemporal(
rows: Row[],
bucketFn: (t: string) => string,
campoFn: (row: Row) => number,
): Map<string, number[]> {
const buckets = new Map<string, number[]>();
for (const r of rows) {
const k = bucketFn(r.t);
const arr = buckets.get(k) ?? [];
arr.push(campoFn(r));
buckets.set(k, arr);
}
return buckets;
}
export function buildSheet(rows: Row[], cols: ColDef[]): Record<string, number>[] {
return rows.map((r) => {
const o: Record<string, number> = {};
for (const c of cols) o[c.label] = c.get(r);
return o;
});
}
export function filterRows(rows: Row[], pred: (row: Row) => boolean): Row[] {
const out: Row[] = [];
for (const r of rows) if (pred(r)) out.push(r);
return out;
}
Run:
Observed
serieTemporal() --indirect_call--> r() [INFERRED 0.8]
buildSheet() --indirect_call--> r() [INFERRED 0.8]
filterRows() --indirect_call--> r() [INFERRED 0.8]
Baseline control: the same two files without the variable-dispatch calls (plain rows.map((r) => r.v) / rows.filter((r) => ...) only) produce zero false edges — the trigger is specifically the indirect-call path picking up the argument identifier.
Expected
No edge at all. r in the callers is (a) an argument, never a callee, and (b) the target r is lexically file-local to a *.test.ts and not exported — it should not be a cross-file resolution candidate.
Real-world impact
In a private ~120-file TS/React repo (where I first hit this): 8 of the graph's 18 INFERRED edges were this exact fabrication pointing at one ghost node. That node received the graph's highest betweenness centrality (0.040), so GRAPH_REPORT.md promoted it to the top "Surprising Connections" entry and the #1 "Suggested Question" — the report's headline finding was an artifact.
Note these edges carry confidence 0.8, so filtering confidence_score == 0.5 (which helps with #2236-style fabrications) does not catch this class.
Related
Suggested direction
- Never treat call arguments as callee candidates for
indirect_call.
- Resolve callees respecting lexical scope and imports (a non-exported test-file local should not be bindable cross-file).
- If fuzzy label binding is kept, require a minimum identifier length — or drastically lower the confidence for 1–2 character labels so they are filterable.
Summary
When a function makes a call through a function-valued variable (
campoFn(r),c.get(r),pred(r)) and an argument of that call is an identifier that shares its name with an unrelated symbol elsewhere in the repo, graphify emits anindirect_calledge from the calling function to that unrelated symbol, with confidence 0.8.Single-letter identifiers make this explode: a file-local one-letter test helper (
const r = ...inside*.test.ts) became the target of fabricatedindirect_calledges from every function in the repo that passes a variable namedrto a callback.Version / environment
uv tool install graphifyy==0.9.28graphify . --code-only(pure AST path, no LLM)Minimal repro (2 files)
src/round.test.ts:src/report.ts:Run:
Observed
Baseline control: the same two files without the variable-dispatch calls (plain
rows.map((r) => r.v)/rows.filter((r) => ...)only) produce zero false edges — the trigger is specifically the indirect-call path picking up the argument identifier.Expected
No edge at all.
rin the callers is (a) an argument, never a callee, and (b) the targetris lexically file-local to a*.test.tsand not exported — it should not be a cross-file resolution candidate.Real-world impact
In a private ~120-file TS/React repo (where I first hit this): 8 of the graph's 18 INFERRED edges were this exact fabrication pointing at one ghost node. That node received the graph's highest betweenness centrality (0.040), so
GRAPH_REPORT.mdpromoted it to the top "Surprising Connections" entry and the #1 "Suggested Question" — the report's headline finding was an artifact.Note these edges carry confidence 0.8, so filtering
confidence_score == 0.5(which helps with #2236-style fabrications) does not catch this class.Related
callsedges are never resolved for member/dotted calls (obj.method(),Type.method()) by deliberate design — only bare (undotted) calls resolve, via a fragile label-text match that isn't scoped to real definitions #2041 — bare-call label-text matching not scoped to real definitions (likely the same resolver root)indirect_call(different trigger, same family)Suggested direction
indirect_call.