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:
@@ -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');
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user