feat(explore): surface interface/registry dispatch boundaries and window oversize spine methods

Two gaps closed in `codegraph_explore` output quality:

**Interface/registry dispatch (#687 extension).** When a named token resolves to
a large same-name family (≥8 members) that doesn't land on the connected flow, the
static path truly ends there — the target is chosen at runtime from N implementations
(plugin/strategy/handler interface). `buildPolymorphicBoundaries` detects this via
`implements`/`extends` edges, ranks candidate supertypes by their TRUE graph-wide
implementer count (not FTS sample frequency, which is biased), and emits a
"## Interface dispatch" section naming the supertype, implementer count, and a few
concrete targets. Fires only for uncovered named tokens; a connected flow stays silent.

**Oversize spine method windowing.** A flow entry that is a god-method (e.g. n8n's
962-line `processRunExecutionData`) previously lost the per-file budget to denser
peripheral blocks and was dropped, forcing the agent to `Read` it back. The spine
call site (edge line to the next hop) is now tracked via `spineCallSites` and used
to window the method to its signature head + a ±28-line band around the call, keeping
it under the OVERSIZE_SPINE_LINES threshold. Spine clusters also rank first in the
budget sort and may exceed the per-file cap up to a 2.5× ceiling so they can never be
starved by co-flow files.

Test suite gains an `interface dispatch` describe block (announce, silent-on-connected,
silent-below-threshold) and uses `beforeAll`/`afterAll` to pin `CODEGRAPH_OFFLOAD_DISABLE=1`
so structural assertions are hermetic regardless of machine config.
This commit is contained in:
Colby McHenry
2026-06-20 12:32:05 -05:00
parent 4f8782cbe5
commit e7d9f8c6fa
2 changed files with 298 additions and 15 deletions
+108 -1
View File
@@ -8,7 +8,7 @@
* showing nothing. Deterministic, query-time only, no graph mutation, and a
* fully connected flow must never produce the section.
*/
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
import { describe, it, expect, beforeAll, afterAll, afterEach } from 'vitest';
import * as fs from 'fs';
import * as path from 'path';
import * as os from 'os';
@@ -16,6 +16,19 @@ import CodeGraph from '../src/index';
import { ToolHandler } from '../src/mcp/tools';
import { scanDynamicDispatch } from '../src/mcp/dynamic-boundaries';
// These suites assert on the RAW codegraph_explore output (the Flow / boundary
// sections). The managed reasoning-offload, when configured on the dev machine
// (~/.codegraph/config.json `{"offload":{"managed":true}}`), REPLACES that output
// with a remote Cerebras synthesis — so the structural assertions only hold with
// the offload off. Disable it for this file so the suite is hermetic regardless
// of machine config, then restore.
let _prevOffloadDisable: string | undefined;
beforeAll(() => { _prevOffloadDisable = process.env.CODEGRAPH_OFFLOAD_DISABLE; process.env.CODEGRAPH_OFFLOAD_DISABLE = '1'; });
afterAll(() => {
if (_prevOffloadDisable === undefined) delete process.env.CODEGRAPH_OFFLOAD_DISABLE;
else process.env.CODEGRAPH_OFFLOAD_DISABLE = _prevOffloadDisable;
});
// ---------------------------------------------------------------------------
// Unit: the scanner
// ---------------------------------------------------------------------------
@@ -297,3 +310,97 @@ describe('codegraph_explore — dynamic boundaries', () => {
expect(text).toContain('handle_save');
});
});
// ---------------------------------------------------------------------------
// Integration: interface/registry dispatch (a named method has many impls)
// ---------------------------------------------------------------------------
describe('codegraph_explore — interface dispatch', () => {
let testDir: string;
let cg: CodeGraph;
let handler: ToolHandler;
const setup = async (files: Record<string, string>, include: string[]) => {
testDir = fs.mkdtempSync(path.join(os.tmpdir(), 'codegraph-iface-'));
const src = path.join(testDir, 'src');
fs.mkdirSync(src, { recursive: true });
for (const [name, content] of Object.entries(files)) {
fs.writeFileSync(path.join(src, name), content);
}
cg = CodeGraph.initSync(testDir, { config: { include, exclude: [] } });
await cg.indexAll();
handler = new ToolHandler(cg);
};
afterEach(() => {
if (cg) cg.destroy();
if (testDir && fs.existsSync(testDir)) fs.rmSync(testDir, { recursive: true, force: true });
});
// 9 classes implement INodeType, each with execute(); a runtime registry lookup
// dispatches to one. The agent names the static entry + `execute`, which can't
// resolve to a single impl — the boundary IS the answer.
const nodeFamily = (n: number) => {
const names = ['Http', 'Set', 'If', 'Merge', 'Code', 'Webhook', 'Cron', 'Func', 'NoOp', 'Switch', 'Wait', 'Filter'];
return [
'export interface INodeType { execute(): unknown; }',
...names.slice(0, n).map((nm, i) => `export class ${nm}Node implements INodeType { execute() { return ${i}; } }`),
].join('\n');
};
const engine = [
"import { registry } from './registry';",
'export class WorkflowExecute {',
' processRunExecutionData() { return this.runNode(); }',
' runNode() { return this.executeNode(); }',
' executeNode() {',
" const nodeType = registry.get('http');",
' return nodeType.execute();',
' }',
'}',
].join('\n');
const registry = [
"import type { INodeType } from './nodes';",
'class Registry {',
' private m: Record<string, INodeType> = {};',
' get(k: string): INodeType { return this.m[k]!; }',
'}',
'export const registry = new Registry();',
].join('\n');
it('announces the interface, the TRUE implementer count, and sample targets', async () => {
await setup({ 'nodes.ts': nodeFamily(9), 'registry.ts': registry, 'engine.ts': engine }, ['**/*.ts']);
const res = await handler.execute('codegraph_explore', { query: 'processRunExecutionData executeNode execute' });
const text = res.content[0].text as string;
expect(text).toContain('## Interface dispatch (a named method has many implementations)');
expect(text).toMatch(/`execute` → runtime dispatch to \*\*9\*\* types implementing `INodeType`/);
// a couple of concrete targets, with file:line
expect(text).toMatch(/\b\w+Node\.execute` \(/);
// never steer to Read
expect(text).not.toMatch(/\buse Read\b/i);
});
it('stays SILENT on a fully connected flow with no polymorphic family', async () => {
await setup({
'pipeline.ts': [
'export function stepOne() { return stepTwo(); }',
'export function stepTwo() { return stepThree(); }',
'export function stepThree() { return 3; }',
].join('\n'),
}, ['**/*.ts']);
const res = await handler.execute('codegraph_explore', { query: 'stepOne stepThree' });
const text = res.content[0].text as string;
expect(text).toContain('## Flow');
expect(text).not.toContain('## Interface dispatch');
});
it('stays SILENT when the interface family is below the polymorphism threshold (3 impls)', async () => {
await setup({ 'nodes.ts': nodeFamily(3), 'registry.ts': registry, 'engine.ts': engine }, ['**/*.ts']);
const res = await handler.execute('codegraph_explore', { query: 'processRunExecutionData executeNode execute' });
const text = res.content[0].text as string;
expect(text).not.toContain('## Interface dispatch');
});
});