From 3959f1e8b3e6595c89fa24c30ff5014f6d06fd97 Mon Sep 17 00:00:00 2001 From: Aaron Queen Date: Sun, 11 Oct 2026 01:59:55 -0600 Subject: [PATCH] feat(explore): label name-only calls in the Relationships list --- CHANGELOG.md | 1 + __tests__/edge-trust.test.ts | 34 ++++++++++++++++++++++++++++++++++ src/mcp/tools.ts | 6 +++--- 3 files changed, 38 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 18af4965a7..e2a2862133 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -99,6 +99,7 @@ and adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ### Fixes +- **Explore's Relationships list marks a call matched by name alone.** `codegraph_node`'s trail and explore's Flow already label a hop the resolver could bind only by the callee's name as `[name match, unverified]`; the flat Relationships list (shown for repositories of 500 files or more) now does too, so every place explore draws a call edge shows the same warning. - **A TypeScript property written as a getter now types the calls made through it.** `c.req.header()` reached nothing when `req` was `get req(): HonoRequest`, because only `req: T` fields were read. A plain constructor parameter named like the field (`constructor(req: Request)`) was also taken for the field's declaration and hid the real one; it now counts only with a modifier (`private req: T`), or in a class that extends another and may pass it to `super`. On hono this adds 74 call edges and removes none, and no added edge contradicts the compiler where it reached the caller; on zod the edges are unchanged. - **Indexing no longer depends on the runtime's directory order.** Node lists a directory's files alphabetically while Bun lists them in the filesystem's own order, so under Bun files were indexed in a different order and equal-score search and explore results came out differently from Node's. Directories are now read in name order on both. - The first session search over a large Claude Code history is faster: transcripts of 256 KB and up are scanned for the entries that can hold prose instead of every line being decoded and parsed, since about 95% of a transcript is tool traffic. The indexed passages and titles are identical (checked on 536 real transcripts); spaced JSON or a large file with no match falls back to the whole-file parse. In-process cold build of those 536 transcripts: about 1.0 s before, 0.7 s after. diff --git a/__tests__/edge-trust.test.ts b/__tests__/edge-trust.test.ts index afcbcad092..58e0ea3701 100644 --- a/__tests__/edge-trust.test.ts +++ b/__tests__/edge-trust.test.ts @@ -73,3 +73,37 @@ describe('codegraph_node trail labels name-only hops', () => { expect(calls).toMatch(/proven \([^)]*\)(?! \[name match)/); }); }); + +describe('explore\'s Relationships list labels name-only hops', () => { + let dir: string; + let cg: CodeGraph; + + beforeEach(async () => { + dir = fs.mkdtempSync(path.join(os.tmpdir(), 'codegraph-edge-trust-rel-')); + const write = (rel: string, body: string) => { + fs.mkdirSync(path.dirname(path.join(dir, rel)), { recursive: true }); + fs.writeFileSync(path.join(dir, rel), body); + }; + write('pkg_a/tools.py', 'def helper():\n return 1\n'); + write('pkg_b/other.py', 'def helper():\n return 2\n'); + write('pkg_c/util.py', 'def proven():\n return 3\n'); + write('app/main.py', 'from pkg_c.util import proven\n\n\ndef run():\n helper()\n proven()\n'); + // The Relationships section is only rendered for repositories of 500 files or more. + for (let i = 0; i < 520; i++) write(`pad/f${i}.py`, `def pad_${i}():\n return ${i}\n`); + cg = CodeGraph.initSync(dir, { config: { include: ['**/*.py'], exclude: [] } }); + await cg.indexAll(); + }); + + afterEach(() => { + try { cg.close(); } catch { /* ignore */ } + fs.rmSync(dir, { recursive: true, force: true }); + }); + + it('marks the name-matched hop and leaves the imported one plain', async () => { + const result = await new ToolHandler(cg).execute('codegraph_explore', { query: 'run helper proven' }); + const text = result.content?.[0]?.type === 'text' ? result.content[0].text : ''; + const rels = text.split('\n').filter((l) => l.startsWith('- run → ')); + expect(rels).toContain('- run → helper [name match, unverified]'); + expect(rels).toContain('- run → proven'); + }); +}); diff --git a/src/mcp/tools.ts b/src/mcp/tools.ts index 4fa29b0e3c..853af84960 100644 --- a/src/mcp/tools.ts +++ b/src/mcp/tools.ts @@ -6117,14 +6117,14 @@ export class ToolHandler { lines.push(''); // Group edges by kind for readability - const byKind = new Map>(); + const byKind = new Map>(); for (const edge of significantEdges) { const sourceNode = subgraph.nodes.get(edge.source); const targetNode = subgraph.nodes.get(edge.target); if (!sourceNode || !targetNode) continue; const group = byKind.get(edge.kind) || []; - group.push({ source: sourceNode.name, target: targetNode.name }); + group.push({ source: sourceNode.name, target: targetNode.name, guess: isNameGuess(edge) }); byKind.set(edge.kind, group); } @@ -6133,7 +6133,7 @@ export class ToolHandler { const shown = edges.slice(0, cap); lines.push(`**${kind}:**`); for (const e of shown) { - lines.push(`- ${e.source} → ${e.target}`); + lines.push(`- ${e.source} → ${e.target}${e.guess ? ' [name match, unverified]' : ''}`); } if (edges.length > cap) { lines.push(`- ... and ${edges.length - cap} more`);