feat(mcp): line numbers in explore output + per-file cluster fixes (#188)

* feat(mcp): line numbers in explore output + per-file cluster fixes

Follow-up to #185. Three changes to codegraph_explore:

1. Source sections now carry cat -n style line-number prefixes
   (<num>\t<code>), so the agent can cite file:line straight from the
   payload instead of re-Reading the file just to recover a line number.
   Isolated A/B: the no-line-numbers arm spent 2 Reads + a grep to find a
   line number the line-numbered arm cited with zero follow-up calls.
   Payload cost ~3-5%. Toggle off with CODEGRAPH_EXPLORE_LINENUMS=0.

2. Per-file cluster selection now ranks clusters containing a query entry
   point ahead of dense declaration blocks. Density-only ranking buried
   the relevant methods (perform/didCreateURLRequest/task in Alamofire's
   Session.swift) under the top-of-file class header + property list.

3. Whole-file "envelope" nodes (a class/struct/etc. spanning >50% of the
   file) are excluded from clustering. The Session class spans ~1,400
   lines; keeping it collapsed every method into one giant cluster that
   tail-trimmed down to just the class header, hiding the methods.

Net vs the 0.7.10 baseline, line numbers on: Alamofire -60%, Excalidraw
-32%, VS Code -12% per explore call.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(mcp): language-neutral omission markers in explore output

The gap separator and the two tail-trim markers used C-style `//`
comments, which aren't comments in Python, Ruby, etc. Switch to plain
`... (gap) ...` / `... (trimmed) ...` so they read correctly inside any
language's fenced source block. With line numbers on, the line-number
jump already corroborates a gap.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(mcp): language-neutral truncation marker in codegraph_context

Sibling to the explore marker fix: codegraph_context's code-block
truncation used a C-style `// ... truncated ...`. Switch to
`... (truncated) ...` so it reads correctly in any language's fenced
source block.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* chore(release): bump version to 0.7.11

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Colby Mchenry
2026-05-19 17:16:12 -05:00
committed by GitHub
co-authored by Claude Opus 4.7
parent 93e53e7c69
commit 2c1a314b84
6 changed files with 149 additions and 26 deletions
+43
View File
@@ -188,4 +188,47 @@ describe('codegraph_explore output respects the adaptive budget', () => {
const sourceFollowsHeader = text.indexOf('### Source Code') > 0;
expect(hasRelationships || sourceFollowsHeader).toBe(true);
});
it('prefixes source lines with line numbers by default (cat -n style)', async () => {
delete process.env.CODEGRAPH_EXPLORE_LINENUMS;
const result = await handler.execute('codegraph_explore', { query: 'Session method helper' });
const text = result.content?.[0]?.text ?? '';
// At least one fenced source line should look like `<digits>\t<code>`.
expect(/\n\d+\t/.test(text)).toBe(true);
});
it('omits line numbers when CODEGRAPH_EXPLORE_LINENUMS=0', async () => {
process.env.CODEGRAPH_EXPLORE_LINENUMS = '0';
try {
const result = await handler.execute('codegraph_explore', { query: 'Session method helper' });
const text = result.content?.[0]?.text ?? '';
// The synthetic source has no tab-prefixed numeric lines of its own,
// so none should appear when the toggle is off.
expect(/\n\d+\t(?:export| )/.test(text)).toBe(false);
} finally {
delete process.env.CODEGRAPH_EXPLORE_LINENUMS;
}
});
it('uses language-neutral omission markers (no C-style // in the output)', async () => {
// The gap/trimmed separators must not assume `//` is a comment — that's
// wrong in Python, Ruby, etc. They render inside fenced source blocks.
const result = await handler.execute('codegraph_explore', { query: 'Session method helper' });
const text = result.content?.[0]?.text ?? '';
expect(text).not.toContain('// ... (gap)');
expect(text).not.toContain('// ... trimmed');
});
it('does not collapse a whole-file class into just its header (envelope filter)', async () => {
// The synthetic `Session` class spans the entire file. Without the
// envelope filter it would form one giant cluster that tail-trims to
// the class declaration, hiding the methods. Confirm real method bodies
// make it into the output. Regression guard for the #185 follow-up.
const result = await handler.execute('codegraph_explore', { query: 'Session method helper' });
const text = result.content?.[0]?.text ?? '';
// A method body line (`methodN(arg: string)`) should appear, not just
// the `export class Session {` opener.
const hasMethodBody = /method\d+\(arg: string\)/.test(text);
expect(hasMethodBody).toBe(true);
});
});