fix: issue-triage quick wins (extraction, MCP probes, gitignore, CJK, impact) (#654)
Batch of small, localized fixes from an open-issue triage: - .codegraph/.gitignore now ignores everything but itself, so the database, daemon.pid, sockets, and logs stop showing up in git status (#492, #484) - MCP server answers resources/list and prompts/list with empty lists instead of -32601, clearing scary log lines in opencode/Codex (#621) - index SAP HANA .xsjs/.xsjslib as JavaScript (#556) and TS .mts/.cts (#366) - visit anonymous AMD/CommonJS/IIFE wrapper bodies so their inner functions and calls are indexed instead of coming up empty (#528) - batch the changed-file lookup so a huge first sync no longer hits "too many SQL variables" (#540) - list files with `git ls-files -z` so non-ASCII/CJK paths survive core.quotepath and are no longer silently skipped (#541) - attach Go methods on generic receivers (*T[P]) to their type (#583, RC1) - impact no longer climbs the structural `contains` edge, so a leaf symbol stops dragging in its sibling methods (#536) - README: explicit `codegraph install` step, run in a new shell (#631) Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
2a22f9f55a
commit
ddb1a8f72d
+13
-4
@@ -1588,10 +1588,19 @@ export class QueryBuilder {
|
||||
getUnresolvedReferencesByFiles(filePaths: string[]): UnresolvedReference[] {
|
||||
if (filePaths.length === 0) return [];
|
||||
|
||||
const placeholders = filePaths.map(() => '?').join(',');
|
||||
const rows = this.db
|
||||
.prepare(`SELECT * FROM unresolved_refs WHERE file_path IN (${placeholders})`)
|
||||
.all(...filePaths) as UnresolvedRefRow[];
|
||||
// Chunk under SQLite's parameter limit: the first sync of a very large repo
|
||||
// passes every changed file here, which an unbounded `IN (...)` would bind
|
||||
// as one parameter each — exceeding MAX_VARIABLE_NUMBER and aborting with
|
||||
// "too many SQL variables". (#540)
|
||||
const rows: UnresolvedRefRow[] = [];
|
||||
for (let i = 0; i < filePaths.length; i += SQLITE_PARAM_CHUNK_SIZE) {
|
||||
const chunk = filePaths.slice(i, i + SQLITE_PARAM_CHUNK_SIZE);
|
||||
const placeholders = chunk.map(() => '?').join(',');
|
||||
const chunkRows = this.db
|
||||
.prepare(`SELECT * FROM unresolved_refs WHERE file_path IN (${placeholders})`)
|
||||
.all(...chunk) as UnresolvedRefRow[];
|
||||
rows.push(...chunkRows);
|
||||
}
|
||||
|
||||
return rows.map((row) => ({
|
||||
fromNodeId: row.from_node_id,
|
||||
|
||||
+6
-17
@@ -83,22 +83,11 @@ export function createDirectory(projectRoot: string): void {
|
||||
// Create .gitignore inside .codegraph (if it doesn't exist)
|
||||
const gitignorePath = path.join(codegraphDir, '.gitignore');
|
||||
if (!fs.existsSync(gitignorePath)) {
|
||||
const gitignoreContent = `# CodeGraph data files
|
||||
# These are local to each machine and should not be committed
|
||||
|
||||
# Database
|
||||
*.db
|
||||
*.db-wal
|
||||
*.db-shm
|
||||
|
||||
# Cache
|
||||
cache/
|
||||
|
||||
# Logs
|
||||
*.log
|
||||
|
||||
# Hook markers
|
||||
.dirty
|
||||
const gitignoreContent = `# CodeGraph data files — local to each machine, not for committing.
|
||||
# Ignore everything in .codegraph/ except this file itself, so transient
|
||||
# files (the database, daemon.pid, sockets, logs) never show up in git.
|
||||
*
|
||||
!.gitignore
|
||||
`;
|
||||
|
||||
fs.writeFileSync(gitignorePath, gitignoreContent, 'utf-8');
|
||||
@@ -245,7 +234,7 @@ export function validateDirectory(projectRoot: string): {
|
||||
const gitignorePath = path.join(codegraphDir, '.gitignore');
|
||||
if (!fs.existsSync(gitignorePath)) {
|
||||
try {
|
||||
const gitignoreContent = `# CodeGraph data files\n# These are local to each machine and should not be committed\n\n# Database\n*.db\n*.db-wal\n*.db-shm\n\n# Cache\ncache/\n\n# Logs\n*.log\n\n# Hook markers\n.dirty\n`;
|
||||
const gitignoreContent = `# CodeGraph data files — local to each machine, not for committing.\n# Ignore everything in .codegraph/ except this file itself, so transient\n# files (the database, daemon.pid, sockets, logs) never show up in git.\n*\n!.gitignore\n`;
|
||||
fs.writeFileSync(gitignorePath, gitignoreContent, 'utf-8');
|
||||
} catch {
|
||||
// Non-fatal: warn but don't block
|
||||
|
||||
@@ -46,9 +46,15 @@ const WASM_GRAMMAR_FILES: Record<GrammarLanguage, string> = {
|
||||
export const EXTENSION_MAP: Record<string, Language> = {
|
||||
'.ts': 'typescript',
|
||||
'.tsx': 'tsx',
|
||||
// ESM/CJS TypeScript module extensions — parsed as TS (no JSX). (#366)
|
||||
'.mts': 'typescript',
|
||||
'.cts': 'typescript',
|
||||
'.js': 'javascript',
|
||||
'.mjs': 'javascript',
|
||||
'.cjs': 'javascript',
|
||||
// SAP HANA XS Classic server-side JavaScript. (#556)
|
||||
'.xsjs': 'javascript',
|
||||
'.xsjslib': 'javascript',
|
||||
'.jsx': 'jsx',
|
||||
'.py': 'python',
|
||||
'.pyw': 'python',
|
||||
|
||||
+14
-14
@@ -198,32 +198,32 @@ function collectGitFiles(repoDir: string, prefix: string, files: Set<string>): v
|
||||
// Without this, monorepos using submodules index 0 files. (See issue #147.)
|
||||
// Note: --recurse-submodules only supports -c/--cached and --stage modes — it
|
||||
// can't be combined with -o, so untracked files are gathered separately below.
|
||||
const tracked = execFileSync('git', ['ls-files', '-c', '--recurse-submodules'], gitOpts);
|
||||
for (const line of tracked.split('\n')) {
|
||||
const trimmed = line.trim();
|
||||
if (trimmed) {
|
||||
files.add(normalizePath(prefix + trimmed));
|
||||
}
|
||||
// -z gives NUL-separated, unquoted output so non-ASCII (e.g. CJK) paths
|
||||
// survive verbatim. Without it git octal-escapes and double-quotes such paths
|
||||
// (the core.quotepath default), and the quoted form never matches a real file
|
||||
// on disk → those files are silently dropped from the index. (#541)
|
||||
const tracked = execFileSync('git', ['ls-files', '-z', '-c', '--recurse-submodules'], gitOpts);
|
||||
for (const rel of tracked.split('\0')) {
|
||||
if (rel) files.add(normalizePath(prefix + rel));
|
||||
}
|
||||
|
||||
// Untracked files (submodules manage their own untracked state). Embedded git
|
||||
// repos surface here as a single "subdir/" entry that git refuses to descend
|
||||
// into — recurse into those as their own repos so their source gets indexed.
|
||||
const untracked = execFileSync('git', ['ls-files', '-o', '--exclude-standard'], gitOpts);
|
||||
for (const line of untracked.split('\n')) {
|
||||
const trimmed = line.trim();
|
||||
if (!trimmed) continue;
|
||||
if (trimmed.endsWith('/')) {
|
||||
const untracked = execFileSync('git', ['ls-files', '-z', '-o', '--exclude-standard'], gitOpts);
|
||||
for (const rel of untracked.split('\0')) {
|
||||
if (!rel) continue;
|
||||
if (rel.endsWith('/')) {
|
||||
// git only emits a trailing-slash directory entry for an embedded repo.
|
||||
// Guard with a .git check anyway, and skip anything else exactly as git
|
||||
// itself skips it (we never descend into a non-repo opaque dir).
|
||||
const childDir = path.join(repoDir, trimmed);
|
||||
const childDir = path.join(repoDir, rel);
|
||||
if (fs.existsSync(path.join(childDir, '.git'))) {
|
||||
collectGitFiles(childDir, prefix + trimmed, files);
|
||||
collectGitFiles(childDir, prefix + rel, files);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
files.add(normalizePath(prefix + trimmed));
|
||||
files.add(normalizePath(prefix + rel));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -56,8 +56,12 @@ export const goExtractor: LanguageExtractor = {
|
||||
if (!receiver) return undefined;
|
||||
// Find the type identifier inside the receiver
|
||||
const text = getNodeText(receiver, source);
|
||||
// Extract type name from patterns like "(sl *Type)", "(sl Type)", "(*Type)", "(Type)"
|
||||
const match = text.match(/\*?\s*([A-Za-z_][A-Za-z0-9_]*)\s*\)/);
|
||||
// Extract type name from "(sl *Type)", "(sl Type)", "(*Type)", "(Type)" and
|
||||
// generic receivers "(s *Stack[T])". Anchor on the opening "(" and skip an
|
||||
// optional receiver var name; the old `name)`-anchored pattern never matched
|
||||
// the `[T])` suffix, so generic-type methods were orphaned from their type
|
||||
// (no struct→method `contains` edge). (#583)
|
||||
const match = text.match(/\(\s*(?:[A-Za-z_]\w*\s+)?\*?\s*([A-Za-z_]\w*)/);
|
||||
return match?.[1];
|
||||
},
|
||||
};
|
||||
|
||||
@@ -630,7 +630,19 @@ export class TreeSitterExtractor {
|
||||
}
|
||||
}
|
||||
}
|
||||
if (name === '<anonymous>') return; // Skip anonymous functions
|
||||
if (name === '<anonymous>') {
|
||||
// Don't emit a node for the anonymous wrapper itself, but still visit its
|
||||
// body: AMD/RequireJS and CommonJS module wrappers (`define([], function(){…})`,
|
||||
// `(function(){…})()`) hold named inner functions and calls that would
|
||||
// otherwise be lost — the dispatcher set skipChildren, so nothing else
|
||||
// descends into this subtree. (#528)
|
||||
const body = this.extractor.resolveBody?.(node, this.extractor.bodyField)
|
||||
?? getChildByField(node, this.extractor.bodyField);
|
||||
if (body) {
|
||||
this.visitFunctionBody(body, '');
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Check for misparse artifacts (e.g. C++ macros causing "namespace detail" functions)
|
||||
// Skip the node but still visit the body for calls and structural nodes
|
||||
|
||||
@@ -521,8 +521,11 @@ export class GraphTraverser {
|
||||
}
|
||||
}
|
||||
|
||||
// Get all incoming edges (things that depend on this node)
|
||||
const incomingEdges = this.queries.getIncomingEdges(nodeId);
|
||||
// Get all incoming edges (things that depend on this node). Exclude
|
||||
// `contains`: a container "contains" its members but does not *depend* on
|
||||
// them, so following it upward would climb to the parent class and then
|
||||
// re-expand every sibling member — exploding impact for a leaf symbol. (#536)
|
||||
const incomingEdges = this.queries.getIncomingEdges(nodeId).filter((e) => e.kind !== 'contains');
|
||||
if (incomingEdges.length === 0) return;
|
||||
const sources = this.queries.getNodesByIds(incomingEdges.map((e) => e.source));
|
||||
|
||||
|
||||
@@ -230,6 +230,14 @@ export async function runLocalHandshakeProxy(deps: LocalHandshakeDeps): Promise<
|
||||
routeToDaemon(line); // prime the daemon so it resolves the project (its reply is suppressed below)
|
||||
} else if (msg.method === 'tools/list') {
|
||||
writeClient({ jsonrpc: '2.0', id: msg.id, result: { tools: getStaticTools() } });
|
||||
} else if (msg.method === 'resources/list') {
|
||||
// No resources exposed — answer the probe locally so it never reaches
|
||||
// the daemon as an unhandled method and logs `-32601`. (#621)
|
||||
writeClient({ jsonrpc: '2.0', id: msg.id, result: { resources: [] } });
|
||||
} else if (msg.method === 'resources/templates/list') {
|
||||
writeClient({ jsonrpc: '2.0', id: msg.id, result: { resourceTemplates: [] } });
|
||||
} else if (msg.method === 'prompts/list') {
|
||||
writeClient({ jsonrpc: '2.0', id: msg.id, result: { prompts: [] } });
|
||||
} else {
|
||||
routeToDaemon(line);
|
||||
}
|
||||
|
||||
@@ -132,6 +132,19 @@ export class MCPSession {
|
||||
case 'ping':
|
||||
if (isRequest) this.transport.sendResult((message as JsonRpcRequest).id, {});
|
||||
break;
|
||||
case 'resources/list':
|
||||
// We expose no MCP resources, but some clients (opencode, Codex) probe
|
||||
// for them on connect; reply with an empty list instead of a
|
||||
// MethodNotFound error that surfaces as a scary `-32601` log line. (#621)
|
||||
if (isRequest) this.transport.sendResult((message as JsonRpcRequest).id, { resources: [] });
|
||||
break;
|
||||
case 'resources/templates/list':
|
||||
if (isRequest) this.transport.sendResult((message as JsonRpcRequest).id, { resourceTemplates: [] });
|
||||
break;
|
||||
case 'prompts/list':
|
||||
// Likewise — no prompts exposed, but answer the probe cleanly. (#621)
|
||||
if (isRequest) this.transport.sendResult((message as JsonRpcRequest).id, { prompts: [] });
|
||||
break;
|
||||
default:
|
||||
if (isRequest) {
|
||||
this.transport.sendError(
|
||||
|
||||
Reference in New Issue
Block a user