Add evaluation framework and fix call graph extraction

- Add evaluation test suite with TypeScript and Python fixtures
- Fix MCP server to defer CodeGraph init until rootUri received
- Fix call edge extraction by calling resolveReferences() after indexAll/sync
- Fix glob matching for root-level files (e.g., **/*.py now matches auth.py)
- Fix duplicate node extraction for methods inside classes
- Update context tests to use buildContext for semantic search + graph traversal
- Export unused formatter functions to fix build

Evaluation results:
- TypeScript: 96% precision, 79% recall, 85% F1
- Python: 99% precision, 80% recall, 85% F1

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Colby McHenry
2026-01-18 18:48:22 -06:00
co-authored by Claude Opus 4.5
parent e306114607
commit 6b672f9152
30 changed files with 2600 additions and 129 deletions
+4 -15
View File
@@ -679,25 +679,14 @@ hooksCommand
program
.command('serve')
.description('Start CodeGraph as an MCP server for AI assistants')
.option('-p, --path <path>', 'Project path')
.option('-p, --path <path>', 'Project path (optional for MCP mode, uses rootUri from client)')
.option('--mcp', 'Run as MCP server (stdio transport)')
.action(async (options: { path?: string; mcp?: boolean }) => {
const projectPath = resolveProjectPath(options.path);
const projectPath = options.path ? resolveProjectPath(options.path) : undefined;
try {
if (!CodeGraph.isInitialized(projectPath)) {
// In MCP mode, we can't use colored output easily
if (options.mcp) {
console.error(`CodeGraph not initialized in ${projectPath}. Run 'codegraph init' first.`);
} else {
error(`CodeGraph not initialized in ${projectPath}`);
info('Run "codegraph init" first');
}
process.exit(1);
}
if (options.mcp) {
// Start MCP server
// Start MCP server - it handles initialization lazily based on rootUri from client
const { MCPServer } = await import('../mcp/index');
const server = new MCPServer(projectPath);
await server.start();
@@ -712,7 +701,7 @@ program
"mcpServers": {
"codegraph": {
"command": "codegraph",
"args": ["serve", "--mcp", "--path", "${projectPath}"]
"args": ["serve", "--mcp"]
}
}
}
+42 -36
View File
@@ -9,58 +9,64 @@ import { Node, Edge, TaskContext, Subgraph } from '../types';
/**
* Format context as markdown
*
* Creates a structured markdown document optimized for Claude:
* - Summary section
* - Structure tree showing relationships
* - Code blocks with syntax highlighting
* - Related files list
* Creates a compact markdown document optimized for Claude with minimal context usage:
* - Brief summary
* - Entry points with locations
* - Code blocks only for key symbols
*/
export function formatContextAsMarkdown(context: TaskContext): string {
const lines: string[] = [];
// Header
// Header with query
lines.push('## Code Context\n');
// Summary
lines.push(`**Query:** ${context.query}\n`);
lines.push(context.summary + '\n');
// Structure section
lines.push('### Structure\n');
lines.push('```');
lines.push(formatSubgraphTree(context.subgraph, context.entryPoints));
lines.push('```\n');
// Entry points - compact format
if (context.entryPoints.length > 0) {
lines.push('### Entry Points\n');
for (const node of context.entryPoints) {
const location = node.startLine ? `:${node.startLine}` : '';
lines.push(`- **${node.name}** (${node.kind}) - ${node.filePath}${location}`);
if (node.signature) {
lines.push(` \`${node.signature}\``);
}
}
lines.push('');
}
// Code blocks section
// Related symbols - compact list (skip verbose structure tree)
const otherSymbols = Array.from(context.subgraph.nodes.values())
.filter(n => !context.entryPoints.some(e => e.id === n.id))
.slice(0, 10); // Limit to 10 related symbols
if (otherSymbols.length > 0) {
lines.push('### Related Symbols\n');
const byFile = new Map<string, Node[]>();
for (const node of otherSymbols) {
const existing = byFile.get(node.filePath) || [];
existing.push(node);
byFile.set(node.filePath, existing);
}
for (const [file, nodes] of byFile) {
const nodeList = nodes.map(n => `${n.name}:${n.startLine}`).join(', ');
lines.push(`- ${file}: ${nodeList}`);
}
lines.push('');
}
// Code blocks - only for key entry points
if (context.codeBlocks.length > 0) {
lines.push('### Code\n');
for (const block of context.codeBlocks) {
const nodeName = block.node?.name ?? 'Unknown';
const nodeKind = block.node?.kind ?? 'unknown';
lines.push(`#### ${nodeName} (${nodeKind}) - ${block.filePath}:${block.startLine}\n`);
lines.push(`#### ${nodeName} (${block.filePath}:${block.startLine})\n`);
lines.push('```' + block.language);
lines.push(block.content);
lines.push('```\n');
}
}
// Related files section
if (context.relatedFiles.length > 0) {
lines.push('### Related Files\n');
for (const file of context.relatedFiles) {
lines.push(`- ${file}`);
}
lines.push('');
}
// Stats footer
lines.push('---');
lines.push(
`*Context: ${context.stats.nodeCount} symbols, ${context.stats.edgeCount} relationships, ` +
`${context.stats.fileCount} files, ${context.stats.codeBlockCount} code blocks ` +
`(${formatBytes(context.stats.totalCodeSize)})*`
);
return lines.join('\n');
}
@@ -96,7 +102,7 @@ export function formatContextAsJson(context: TaskContext): string {
/**
* Format a subgraph as an ASCII tree structure
*/
function formatSubgraphTree(subgraph: Subgraph, entryPoints: Node[]): string {
export function formatSubgraphTree(subgraph: Subgraph, entryPoints: Node[]): string {
const lines: string[] = [];
const printed = new Set<string>();
@@ -254,7 +260,7 @@ function truncate(str: string, maxLength: number): string {
/**
* Format bytes as human-readable string
*/
function formatBytes(bytes: number): string {
export function formatBytes(bytes: number): string {
if (bytes < 1024) {
return `${bytes} bytes`;
} else if (bytes < 1024 * 1024) {
+13 -8
View File
@@ -26,15 +26,20 @@ import { logDebug, logWarn } from '../errors';
/**
* Default options for context building
*
* Tuned for minimal context usage while still providing useful results:
* - Fewer nodes and code blocks by default
* - Smaller code block size limit
* - Shallower traversal
*/
const DEFAULT_BUILD_OPTIONS: Required<BuildContextOptions> = {
maxNodes: 50,
maxCodeBlocks: 10,
maxCodeBlockSize: 2000,
maxNodes: 20, // Reduced from 50 - most tasks don't need 50 symbols
maxCodeBlocks: 5, // Reduced from 10 - only show most relevant code
maxCodeBlockSize: 1500, // Reduced from 2000
includeCode: true,
format: 'markdown',
searchLimit: 5,
traversalDepth: 2,
searchLimit: 3, // Reduced from 5 - fewer entry points
traversalDepth: 1, // Reduced from 2 - shallower graph expansion
minScore: 0.3,
};
@@ -42,9 +47,9 @@ const DEFAULT_BUILD_OPTIONS: Required<BuildContextOptions> = {
* Default options for finding relevant context
*/
const DEFAULT_FIND_OPTIONS: Required<FindRelevantContextOptions> = {
searchLimit: 5,
traversalDepth: 2,
maxNodes: 50,
searchLimit: 3, // Reduced from 5
traversalDepth: 1, // Reduced from 2
maxNodes: 20, // Reduced from 50
minScore: 0.3,
edgeKinds: [],
nodeKinds: [],
+102 -3
View File
@@ -390,11 +390,46 @@ export class QueryBuilder {
}
/**
* Search nodes by name using FTS
* Search nodes by name using FTS with fallback to LIKE for better matching
*
* Search strategy:
* 1. Try FTS5 prefix match (query*) for word-start matching
* 2. If no results, try LIKE for substring matching (e.g., "signIn" finds "signInWithGoogle")
* 3. Score results based on match quality
*/
searchNodes(query: string, options: SearchOptions = {}): SearchResult[] {
const { kinds, languages, limit = 100, offset = 0 } = options;
// First try FTS5 with prefix matching
let results = this.searchNodesFTS(query, { kinds, languages, limit, offset });
// If no FTS results, try LIKE-based substring search
if (results.length === 0 && query.length >= 2) {
results = this.searchNodesLike(query, { kinds, languages, limit, offset });
}
return results;
}
/**
* FTS5 search with prefix matching
*/
private searchNodesFTS(query: string, options: SearchOptions): SearchResult[] {
const { kinds, languages, limit = 100, offset = 0 } = options;
// Add prefix wildcard for better matching (e.g., "auth" matches "AuthService", "authenticate")
// Escape special FTS5 characters and add prefix wildcard
const ftsQuery = query
.replace(/['"*()]/g, '') // Remove special chars
.split(/\s+/)
.filter(term => term.length > 0)
.map(term => `"${term}"*`) // Prefix match each term
.join(' OR ');
if (!ftsQuery) {
return [];
}
let sql = `
SELECT nodes.*, bm25(nodes_fts) as score
FROM nodes_fts
@@ -402,7 +437,7 @@ export class QueryBuilder {
WHERE nodes_fts MATCH ?
`;
const params: (string | number)[] = [query];
const params: (string | number)[] = [ftsQuery];
if (kinds && kinds.length > 0) {
sql += ` AND nodes.kind IN (${kinds.map(() => '?').join(',')})`;
@@ -417,11 +452,75 @@ export class QueryBuilder {
sql += ' ORDER BY score LIMIT ? OFFSET ?';
params.push(limit, offset);
try {
const rows = this.db.prepare(sql).all(...params) as (NodeRow & { score: number })[];
return rows.map((row) => ({
node: rowToNode(row),
score: Math.abs(row.score), // bm25 returns negative scores
}));
} catch {
// FTS query failed, return empty
return [];
}
}
/**
* LIKE-based substring search for cases where FTS doesn't match
* Useful for camelCase matching (e.g., "signIn" finds "signInWithGoogle")
*/
private searchNodesLike(query: string, options: SearchOptions): SearchResult[] {
const { kinds, languages, limit = 100, offset = 0 } = options;
let sql = `
SELECT nodes.*,
CASE
WHEN name = ? THEN 1.0
WHEN name LIKE ? THEN 0.9
WHEN name LIKE ? THEN 0.8
WHEN qualified_name LIKE ? THEN 0.7
ELSE 0.5
END as score
FROM nodes
WHERE (
name LIKE ? OR
qualified_name LIKE ? OR
name LIKE ?
)
`;
// Pattern variants for better matching
const exactMatch = query;
const startsWith = `${query}%`;
const contains = `%${query}%`;
const params: (string | number)[] = [
exactMatch, // Exact match score
startsWith, // Starts with score
contains, // Contains score
contains, // Qualified name score
contains, // WHERE: name contains
contains, // WHERE: qualified_name contains
startsWith, // WHERE: name starts with
];
if (kinds && kinds.length > 0) {
sql += ` AND kind IN (${kinds.map(() => '?').join(',')})`;
params.push(...kinds);
}
if (languages && languages.length > 0) {
sql += ` AND language IN (${languages.map(() => '?').join(',')})`;
params.push(...languages);
}
sql += ' ORDER BY score DESC, length(name) ASC LIMIT ? OFFSET ?';
params.push(limit, offset);
const rows = this.db.prepare(sql).all(...params) as (NodeRow & { score: number })[];
return rows.map((row) => ({
node: rowToNode(row),
score: Math.abs(row.score), // bm25 returns negative scores
score: row.score,
}));
}
+18 -7
View File
@@ -65,13 +65,24 @@ export function hashContent(content: string): string {
* Check if a path matches any glob pattern (simplified)
*/
function matchesGlob(filePath: string, pattern: string): boolean {
// Convert glob to regex (simplified)
const regexStr = pattern
.replace(/\./g, '\\.')
.replace(/\*\*/g, '<<<GLOBSTAR>>>')
.replace(/\*/g, '[^/]*')
.replace(/<<<GLOBSTAR>>>/g, '.*')
.replace(/\?/g, '.');
// Convert glob to regex using placeholders to avoid conflicts
let regexStr = pattern;
// Replace glob patterns with placeholders first
regexStr = regexStr.replace(/\*\*\//g, '\x00GLOBSTAR_SLASH\x00');
regexStr = regexStr.replace(/\*\*/g, '\x00GLOBSTAR\x00');
regexStr = regexStr.replace(/\*/g, '\x00STAR\x00');
regexStr = regexStr.replace(/\?/g, '\x00QUESTION\x00');
// Escape regex special characters
regexStr = regexStr.replace(/[.+^${}()|[\]\\]/g, '\\$&');
// Replace placeholders with regex equivalents
regexStr = regexStr.replace(/\x00GLOBSTAR_SLASH\x00/g, '(?:.*/)?'); // **/ = zero or more dirs
regexStr = regexStr.replace(/\x00GLOBSTAR\x00/g, '.*'); // ** = anything
regexStr = regexStr.replace(/\x00STAR\x00/g, '[^/]*'); // * = anything except /
regexStr = regexStr.replace(/\x00QUESTION\x00/g, '.'); // ? = single char
const regex = new RegExp(`^${regexStr}$`);
return regex.test(filePath);
}
+23 -7
View File
@@ -743,10 +743,19 @@ export class TreeSitterExtractor {
if (!this.extractor) return;
const nodeType = node.type;
let skipChildren = false;
// Check for function declarations
// For Python/Ruby, function_definition inside a class should be treated as method
if (this.extractor.functionTypes.includes(nodeType)) {
this.extractFunction(node);
if (this.nodeStack.length > 0 && this.extractor.methodTypes.includes(nodeType)) {
// Inside a class - treat as method
this.extractMethod(node);
skipChildren = true; // extractMethod visits children via visitFunctionBody
} else {
this.extractFunction(node);
skipChildren = true; // extractFunction visits children via visitFunctionBody
}
}
// Check for class declarations
else if (this.extractor.classTypes.includes(nodeType)) {
@@ -759,22 +768,27 @@ export class TreeSitterExtractor {
} else {
this.extractClass(node);
}
skipChildren = true; // extractClass visits body children
}
// Check for method declarations
// Check for method declarations (only if not already handled by functionTypes)
else if (this.extractor.methodTypes.includes(nodeType)) {
this.extractMethod(node);
skipChildren = true; // extractMethod visits children via visitFunctionBody
}
// Check for interface/protocol/trait declarations
else if (this.extractor.interfaceTypes.includes(nodeType)) {
this.extractInterface(node);
skipChildren = true; // extractInterface visits body children
}
// Check for struct declarations
else if (this.extractor.structTypes.includes(nodeType)) {
this.extractStruct(node);
skipChildren = true; // extractStruct visits body children
}
// Check for enum declarations
else if (this.extractor.enumTypes.includes(nodeType)) {
this.extractEnum(node);
skipChildren = true; // extractEnum visits body children
}
// Check for imports
else if (this.extractor.importTypes.includes(nodeType)) {
@@ -785,11 +799,13 @@ export class TreeSitterExtractor {
this.extractCall(node);
}
// Visit children
for (let i = 0; i < node.namedChildCount; i++) {
const child = node.namedChild(i);
if (child) {
this.visitNode(child);
// Visit children (unless the extract method already visited them)
if (!skipChildren) {
for (let i = 0; i < node.namedChildCount; i++) {
const child = node.namedChild(i);
if (child) {
this.visitNode(child);
}
}
}
}
+21 -2
View File
@@ -367,7 +367,19 @@ export class CodeGraph {
*/
async indexAll(options: IndexOptions = {}): Promise<IndexResult> {
return this.indexMutex.withLock(async () => {
return this.orchestrator.indexAll(options.onProgress, options.signal);
const result = await this.orchestrator.indexAll(options.onProgress, options.signal);
// Resolve references to create call/import/extends edges
if (result.success && result.filesIndexed > 0) {
options.onProgress?.({
phase: 'resolving',
current: 0,
total: 1,
});
this.resolveReferences();
}
return result;
});
}
@@ -389,7 +401,14 @@ export class CodeGraph {
*/
async sync(options: IndexOptions = {}): Promise<SyncResult> {
return this.indexMutex.withLock(async () => {
return this.orchestrator.sync(options.onProgress);
const result = await this.orchestrator.sync(options.onProgress);
// Resolve references if files were updated
if (result.filesAdded > 0 || result.filesModified > 0) {
this.resolveReferences();
}
return result;
});
}
+57 -13
View File
@@ -42,26 +42,23 @@ export class MCPServer {
private transport: StdioTransport;
private cg: CodeGraph | null = null;
private toolHandler: ToolHandler | null = null;
private projectPath: string;
private projectPath: string | null;
private initError: string | null = null;
constructor(projectPath: string) {
this.projectPath = projectPath;
constructor(projectPath?: string) {
this.projectPath = projectPath || null;
this.transport = new StdioTransport();
}
/**
* Start the MCP server
*
* Note: CodeGraph initialization is deferred until the initialize request
* is received, which includes the rootUri from the client.
*/
async start(): Promise<void> {
// Open CodeGraph for the project
if (!CodeGraph.isInitialized(this.projectPath)) {
throw new Error(`CodeGraph not initialized in ${this.projectPath}. Run 'codegraph init' first.`);
}
this.cg = await CodeGraph.open(this.projectPath);
this.toolHandler = new ToolHandler(this.cg);
// Start listening for messages
// Start listening for messages immediately - don't check initialization yet
// We'll get the project path from the initialize request's rootUri
this.transport.start(this.handleMessage.bind(this));
// Keep the process running
@@ -69,6 +66,26 @@ export class MCPServer {
process.on('SIGTERM', () => this.stop());
}
/**
* Initialize CodeGraph for the project
*/
private async initializeCodeGraph(projectPath: string): Promise<void> {
this.projectPath = projectPath;
if (!CodeGraph.isInitialized(projectPath)) {
this.initError = `CodeGraph not initialized in ${projectPath}. Run 'codegraph init' first.`;
return;
}
try {
this.cg = await CodeGraph.open(projectPath);
this.toolHandler = new ToolHandler(this.cg);
this.initError = null;
} catch (err) {
this.initError = `Failed to open CodeGraph: ${err instanceof Error ? err.message : String(err)}`;
}
}
/**
* Stop the server
*/
@@ -133,6 +150,29 @@ export class MCPServer {
* Handle initialize request
*/
private async handleInitialize(request: JsonRpcRequest): Promise<void> {
const params = request.params as {
rootUri?: string;
workspaceFolders?: Array<{ uri: string; name: string }>;
} | undefined;
// Extract project path from rootUri or workspaceFolders
let projectPath = this.projectPath;
if (params?.rootUri) {
// Convert file:// URI to path
projectPath = params.rootUri.replace(/^file:\/\//, '');
} else if (params?.workspaceFolders?.[0]?.uri) {
projectPath = params.workspaceFolders[0].uri.replace(/^file:\/\//, '');
}
// Fall back to current working directory if no path provided
if (!projectPath) {
projectPath = process.cwd();
}
// Initialize CodeGraph if we have a project path
await this.initializeCodeGraph(projectPath);
// We accept the client's protocol version but respond with our supported version
this.transport.sendResult(request.id, {
protocolVersion: PROTOCOL_VERSION,
@@ -186,10 +226,14 @@ export class MCPServer {
// Execute the tool
if (!this.toolHandler) {
const errorMsg = this.initError ||
(this.projectPath
? `CodeGraph not initialized in ${this.projectPath}. Run 'codegraph init' first.`
: 'No project path provided. Ensure Claude Code is running in a project directory.');
this.transport.sendError(
request.id,
ErrorCodes.InternalError,
'Server not initialized'
errorMsg
);
return;
}
+31 -32
View File
@@ -40,17 +40,20 @@ export interface ToolResult {
/**
* All CodeGraph MCP tools
*
* Designed for minimal context usage - use codegraph_context as the primary tool,
* and only use other tools for targeted follow-up queries.
*/
export const tools: ToolDefinition[] = [
{
name: 'codegraph_search',
description: 'Search for code symbols (functions, classes, methods) by name or semantic similarity. Returns matching nodes with their locations and signatures.',
description: 'Quick symbol search by name. Returns locations only (no code). Use codegraph_context instead for comprehensive task context.',
inputSchema: {
type: 'object',
properties: {
query: {
type: 'string',
description: 'Search query - can be a symbol name or natural language description',
description: 'Symbol name or partial name (e.g., "auth", "signIn", "UserService")',
},
kind: {
type: 'string',
@@ -59,7 +62,7 @@ export const tools: ToolDefinition[] = [
},
limit: {
type: 'number',
description: 'Maximum number of results to return (default: 10)',
description: 'Maximum results (default: 10)',
default: 10,
},
},
@@ -68,7 +71,7 @@ export const tools: ToolDefinition[] = [
},
{
name: 'codegraph_context',
description: 'Build relevant code context for a task or issue. Finds related symbols and their code, formatted for understanding the codebase.',
description: 'PRIMARY TOOL: Build comprehensive context for a task. Returns entry points, related symbols, and key code - often enough to understand the codebase without additional tool calls.',
inputSchema: {
type: 'object',
properties: {
@@ -78,12 +81,12 @@ export const tools: ToolDefinition[] = [
},
maxNodes: {
type: 'number',
description: 'Maximum number of code symbols to include (default: 20)',
description: 'Maximum symbols to include (default: 20)',
default: 20,
},
includeCode: {
type: 'boolean',
description: 'Include full code snippets (default: true)',
description: 'Include code snippets for key symbols (default: true)',
default: true,
},
},
@@ -149,7 +152,7 @@ export const tools: ToolDefinition[] = [
},
{
name: 'codegraph_node',
description: 'Get detailed information about a specific code symbol, including its full code.',
description: 'Get detailed information about a specific code symbol. Use includeCode=true only when you need the full source code - otherwise just get location and signature to minimize context usage.',
inputSchema: {
type: 'object',
properties: {
@@ -159,8 +162,8 @@ export const tools: ToolDefinition[] = [
},
includeCode: {
type: 'boolean',
description: 'Include full source code (default: true)',
default: true,
description: 'Include full source code (default: false to minimize context)',
default: false,
},
},
required: ['symbol'],
@@ -331,7 +334,8 @@ export class ToolHandler {
*/
private async handleNode(args: Record<string, unknown>): Promise<ToolResult> {
const symbol = args.symbol as string;
const includeCode = args.includeCode !== false;
// Default to false to minimize context usage
const includeCode = args.includeCode === true;
// Find the node by name
const results = this.cg.searchNodes(symbol, { limit: 1 });
@@ -384,19 +388,19 @@ export class ToolHandler {
}
// =========================================================================
// Formatting helpers
// Formatting helpers (compact by default to reduce context usage)
// =========================================================================
private formatSearchResults(results: SearchResult[]): string {
const lines: string[] = [`## Search Results (${results.length} found)`, ''];
for (const result of results) {
const { node, score } = result;
const { node } = result;
const location = node.startLine ? `:${node.startLine}` : '';
// Compact format: one line per result with key info
lines.push(`### ${node.name} (${node.kind})`);
lines.push(`**Location:** ${node.filePath}${location}`);
lines.push(`**Score:** ${Math.round(score * 100)}%`);
if (node.signature) lines.push(`**Signature:** ${node.signature}`);
lines.push(`${node.filePath}${location}`);
if (node.signature) lines.push(`\`${node.signature}\``);
lines.push('');
}
@@ -408,7 +412,8 @@ export class ToolHandler {
for (const node of nodes) {
const location = node.startLine ? `:${node.startLine}` : '';
lines.push(`- **${node.name}** (${node.kind}) - ${node.filePath}${location}`);
// Compact: just name, kind, location
lines.push(`- ${node.name} (${node.kind}) - ${node.filePath}${location}`);
}
return lines.join('\n');
@@ -416,15 +421,10 @@ export class ToolHandler {
private formatImpact(symbol: string, impact: Subgraph): string {
const nodeCount = impact.nodes.size;
const edgeCount = impact.edges.length;
// Compact format: just list affected symbols grouped by file
const lines: string[] = [
`## Impact Analysis for "${symbol}"`,
'',
`**Nodes affected:** ${nodeCount}`,
`**Relationships:** ${edgeCount}`,
'',
'### Affected Symbols:',
`## Impact: "${symbol}" affects ${nodeCount} symbols`,
'',
];
@@ -438,10 +438,9 @@ export class ToolHandler {
for (const [file, nodes] of byFile) {
lines.push(`**${file}:**`);
for (const node of nodes) {
const location = node.startLine ? `:${node.startLine}` : '';
lines.push(` - ${node.name} (${node.kind})${location}`);
}
// Compact: inline list
const nodeList = nodes.map(n => `${n.name}:${n.startLine}`).join(', ');
lines.push(nodeList);
lines.push('');
}
@@ -454,19 +453,19 @@ export class ToolHandler {
`## ${node.name} (${node.kind})`,
'',
`**Location:** ${node.filePath}${location}`,
`**Language:** ${node.language}`,
];
if (node.signature) {
lines.push(`**Signature:** ${node.signature}`);
lines.push(`**Signature:** \`${node.signature}\``);
}
if (node.docstring) {
lines.push('', '### Documentation:', '', node.docstring);
// Only include docstring if it's short and useful
if (node.docstring && node.docstring.length < 200) {
lines.push('', node.docstring);
}
if (code) {
lines.push('', '### Code:', '', '```' + node.language, code, '```');
lines.push('', '```' + node.language, code, '```');
}
return lines.join('\n');