Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -198,6 +198,7 @@ Multiple Claude Code instances run concurrently in this repo. **Every session mu

**Rules:**
- Run `/worktree` before starting work
- **Always sync with `origin/main` before starting feature work.** Run `git fetch origin && git log --oneline origin/main -10` to check recent merges. If the current branch is behind main, create a new branch from `origin/main`. Never implement features on stale branches — the work may already exist on main.
- Stage only files you explicitly changed
- Commit with specific file paths: `git commit <files> -m "msg"`
- Ignore unexpected dirty files — they belong to another session
Expand Down
22 changes: 13 additions & 9 deletions docs/roadmap/ROADMAP.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ Codegraph is a strong local-first code graph CLI. This roadmap describes planned
| [**2.5**](#phase-25--analysis-expansion) | Analysis Expansion | Complexity metrics, community detection, flow tracing, co-change, manifesto, boundary rules, check, triage, audit, batch, hybrid search | **Complete** (v2.7.0) |
| [**2.7**](#phase-27--deep-analysis--graph-enrichment) | Deep Analysis & Graph Enrichment | Dataflow analysis, intraprocedural CFG, AST node storage, expanded node/edge types, extractors refactoring, CLI consolidation, interactive viewer, exports command, normalizeSymbol | **Complete** (v3.0.0) |
| [**3**](#phase-3--architectural-refactoring) | Architectural Refactoring (Vertical Slice) | Unified AST analysis framework, command/query separation, repository pattern, queries.js decomposition, composable MCP, CLI commands, domain errors, builder pipeline, presentation layer, domain grouping, curated API, unified graph model, qualified names, CLI composability | **Complete** (v3.1.5) |
| [**4**](#phase-4--resolution-accuracy) | Resolution Accuracy | Dead role sub-categories, receiver type tracking, interface/trait implementation edges, resolution precision/recall benchmarks, `package.json` exports field, monorepo workspace resolution | Planned |
| [**4**](#phase-4--resolution-accuracy) | Resolution Accuracy | Dead role sub-categories, receiver type tracking, interface/trait implementation edges, resolution precision/recall benchmarks, `package.json` exports field, monorepo workspace resolution | **In Progress** (4.2 complete) |
| [**5**](#phase-5--typescript-migration) | TypeScript Migration | Project setup, core type definitions, leaf -> core -> orchestration module migration, test migration, supply-chain security, CI coverage gates | Planned |
| [**6**](#phase-6--native-analysis-acceleration) | Native Analysis Acceleration | Move JS-only build phases (AST nodes, CFG, dataflow, insert nodes, structure, roles, complexity) to Rust; fix incremental rebuild data loss on native; sub-100ms 1-file rebuilds | Planned |
| [**7**](#phase-7--runtime--extensibility) | Runtime & Extensibility | Event-driven pipeline, unified engine strategy, subgraph export filtering, transitive confidence, query caching, configuration profiles, pagination, plugin system, DX & onboarding, confidence annotations, shell completion | Planned |
Expand Down Expand Up @@ -1005,17 +1005,21 @@ src/domain/

The coarse `dead` role is now sub-classified into four categories: `dead-leaf` (parameters, properties, constants), `dead-entry` (CLI commands, MCP tools, route/handler files), `dead-ffi` (cross-language FFI — `.rs`, `.c`, `.go`, etc.), and `dead-unresolved` (genuinely unreferenced callables). The `--role dead` filter matches all sub-roles for backward compatibility. Risk weights are tuned per sub-role. `VALID_ROLES`, `DEAD_SUB_ROLES` exported from `shared/kinds.js`. Stats, MCP `node_roles`, CLI `roles`/`triage` all updated.

### 4.2 -- Receiver Type Tracking for Method Dispatch
### 4.2 -- Receiver Type Tracking for Method Dispatch

The single highest-impact resolution improvement. Currently `obj.method()` resolves to ANY exported `method` in scope — no receiver type tracking. This misses repository pattern calls (`repo.findCallers()`), builder chains, and visitor dispatch.
The single highest-impact resolution improvement. Previously `obj.method()` resolved to ANY exported `method` in scope — no receiver type tracking. This missed repository pattern calls (`repo.findCallers()`), builder chains, and visitor dispatch.

- Track variable-to-type assignments: when `const x = new SomeClass()` or `const x: SomeClass = ...`, record `x → SomeClass` in a per-file type map
- During edge building, resolve `x.method()` to `SomeClass.method` using the type map
- Leverage the existing `qualified_name` and `scope` columns (Phase 3.12) for matching
- Confidence: `1.0` for explicit constructor, `0.9` for type annotation, `0.7` for factory function return
- Covers: TypeScript annotated variables, constructor assignments, factory patterns
**Implemented:**
- ✅ Upgraded `typeMap` from `Map<string, string>` to `Map<string, {type, confidence}>` across all 8 language extractors
- ✅ Graded confidence per type source: `1.0` constructor (`new Foo()`), `0.9` type annotation / typed parameter, `0.7` factory method (`Foo.create()`)
- ✅ Factory pattern extraction: JS/TS (`Foo.create()`), Go (`NewFoo()`, `&Struct{}`, `Struct{}`), Python (`Foo()`, `Foo.create()`)
- ✅ Edge builder uses type map for precise `ClassName.method` qualified-name lookup in both JS fallback and native supplement paths
- ✅ Receiver edges carry type-source confidence instead of hardcoded 0.9/0.7
- ✅ `setIfHigher` logic ensures highest-confidence assignment wins per variable
- ✅ Incremental build path updated to consume new format
- ✅ Backwards-compatible: `typeof entry === 'string'` guards handle mixed old/new formats

**Affected files:** `src/domain/graph/builder/stages/build-edges.js`, `src/extractors/*.js`
**Affected files:** `src/domain/graph/builder/stages/build-edges.js`, `src/domain/graph/builder/incremental.js`, `src/extractors/*.js` (all 8 languages)

### 4.3 -- Interface and Trait Implementation Tracking

Expand Down
7 changes: 6 additions & 1 deletion src/domain/graph/builder/incremental.js
Original file line number Diff line number Diff line change
Expand Up @@ -101,7 +101,12 @@ function resolveCallTargets(stmts, call, relPath, importedNames, typeMap) {
}
// Type-aware resolution: translate variable receiver to declared type
if ((!targets || targets.length === 0) && call.receiver && typeMap) {
const typeName = typeMap.get(call.receiver);
const typeEntry = typeMap.get(call.receiver);
const typeName = typeEntry
? typeof typeEntry === 'string'
? typeEntry
: typeEntry.type
: null;
if (typeName) {
const qualified = `${typeName}.${call.name}`;
targets = stmts.findNodeByName.all(qualified);
Expand Down
28 changes: 22 additions & 6 deletions src/domain/graph/builder/stages/build-edges.js
Original file line number Diff line number Diff line change
Expand Up @@ -104,7 +104,11 @@ function buildCallEdgesNative(ctx, getNodeIdStmt, allEdgeRows, allNodes, native)
const importedNames = buildImportedNamesForNative(ctx, relPath, symbols, rootDir);
const typeMap =
symbols.typeMap instanceof Map
? [...symbols.typeMap.entries()].map(([name, typeName]) => ({ name, typeName }))
? [...symbols.typeMap.entries()].map(([name, entry]) => ({
name,
typeName: typeof entry === 'string' ? entry : entry.type,
confidence: typeof entry === 'object' ? entry.confidence : 0.9,
}))
: Array.isArray(symbols.typeMap)
? symbols.typeMap
: [];
Expand Down Expand Up @@ -166,7 +170,9 @@ function supplementReceiverEdges(ctx, nativeFiles, getNodeIdStmt, allEdgeRows) {

for (const nf of nativeFiles) {
const relPath = nf.file;
const typeMap = new Map(nf.typeMap.map((t) => [t.name, t.typeName]));
const typeMap = new Map(
nf.typeMap.map((t) => [t.name, { type: t.typeName, confidence: t.confidence ?? 0.9 }]),
);
const fileNodeRow = { id: nf.fileNodeId };

for (const call of nf.calls) {
Expand All @@ -180,7 +186,8 @@ function supplementReceiverEdges(ctx, nativeFiles, getNodeIdStmt, allEdgeRows) {
buildReceiverEdge(ctx, call, caller, relPath, seenCallEdges, allEdgeRows, typeMap);

// Type-resolved method call: caller → Type.method
const typeName = typeMap.get(call.receiver);
const typeEntry = typeMap.get(call.receiver);
const typeName = typeEntry ? typeEntry.type : null;
if (typeName) {
const qualifiedName = `${typeName}.${call.name}`;
const targets = (ctx.nodesByName.get(qualifiedName) || []).filter(
Expand Down Expand Up @@ -298,7 +305,12 @@ function resolveCallTargets(ctx, call, relPath, importedNames, typeMap) {
function resolveByMethodOrGlobal(ctx, call, relPath, typeMap) {
// Type-aware resolution: translate variable receiver to its declared type
if (call.receiver && typeMap) {
const typeName = typeMap.get(call.receiver);
const typeEntry = typeMap.get(call.receiver);
const typeName = typeEntry
? typeof typeEntry === 'string'
? typeEntry
: typeEntry.type
: null;
if (typeName) {
const qualifiedName = `${typeName}.${call.name}`;
const typed = (ctx.nodesByName.get(qualifiedName) || []).filter((n) => n.kind === 'method');
Expand Down Expand Up @@ -367,7 +379,10 @@ function buildFileCallEdges(

function buildReceiverEdge(ctx, call, caller, relPath, seenCallEdges, allEdgeRows, typeMap) {
const receiverKinds = new Set(['class', 'struct', 'interface', 'type', 'module']);
const effectiveReceiver = typeMap?.get(call.receiver) || call.receiver;
const typeEntry = typeMap?.get(call.receiver);
const typeName = typeEntry ? (typeof typeEntry === 'string' ? typeEntry : typeEntry.type) : null;
const typeConfidence = typeEntry && typeof typeEntry === 'object' ? typeEntry.confidence : null;
const effectiveReceiver = typeName || call.receiver;
const samefile = ctx.nodesByNameAndFile.get(`${effectiveReceiver}|${relPath}`) || [];
const candidates = samefile.length > 0 ? samefile : ctx.nodesByName.get(effectiveReceiver) || [];
const receiverNodes = candidates.filter((n) => receiverKinds.has(n.kind));
Expand All @@ -376,7 +391,8 @@ function buildReceiverEdge(ctx, call, caller, relPath, seenCallEdges, allEdgeRow
const recvKey = `recv|${caller.id}|${recvTarget.id}`;
if (!seenCallEdges.has(recvKey)) {
seenCallEdges.add(recvKey);
const confidence = effectiveReceiver !== call.receiver ? 0.9 : 0.7;
// Use type source confidence when available, otherwise 0.7 for untyped receiver
const confidence = typeConfidence ?? (typeName ? 0.9 : 0.7);
allEdgeRows.push([caller.id, recvTarget.id, 'receiver', confidence, 0]);
}
}
Expand Down
4 changes: 2 additions & 2 deletions src/extractors/csharp.js
Original file line number Diff line number Diff line change
Expand Up @@ -330,7 +330,7 @@ function extractCSharpTypeMapDepth(node, ctx, depth) {
if (child && child.type === 'variable_declarator') {
const nameNode = child.childForFieldName('name') || child.child(0);
if (nameNode && nameNode.type === 'identifier') {
ctx.typeMap.set(nameNode.text, typeName);
ctx.typeMap.set(nameNode.text, { type: typeName, confidence: 0.9 });
}
}
}
Expand All @@ -344,7 +344,7 @@ function extractCSharpTypeMapDepth(node, ctx, depth) {
const nameNode = node.childForFieldName('name');
if (typeNode && nameNode) {
const typeName = extractCSharpTypeName(typeNode);
if (typeName) ctx.typeMap.set(nameNode.text, typeName);
if (typeName) ctx.typeMap.set(nameNode.text, { type: typeName, confidence: 0.9 });
}
}

Expand Down
78 changes: 74 additions & 4 deletions src/extractors/go.js
Original file line number Diff line number Diff line change
Expand Up @@ -208,10 +208,17 @@ function extractGoTypeMap(node, ctx) {
extractGoTypeMapDepth(node, ctx, 0);
}

function setIfHigher(typeMap, name, type, confidence) {
const existing = typeMap.get(name);
if (!existing || confidence > existing.confidence) {
typeMap.set(name, { type, confidence });
}
}

function extractGoTypeMapDepth(node, ctx, depth) {
if (depth >= 200) return;

// var x MyType = ... or var x, y MyType → var_declaration > var_spec
// var x MyType = ... or var x, y MyType → var_declaration > var_spec (confidence 0.9)
if (node.type === 'var_spec') {
const typeNode = node.childForFieldName('type');
if (typeNode) {
Expand All @@ -220,14 +227,14 @@ function extractGoTypeMapDepth(node, ctx, depth) {
for (let i = 0; i < node.childCount; i++) {
const child = node.child(i);
if (child && child.type === 'identifier') {
ctx.typeMap.set(child.text, typeName);
setIfHigher(ctx.typeMap, child.text, typeName, 0.9);
}
}
}
}
}

// Function/method parameter types: parameter_declaration has identifiers then a type
// Function/method parameter types: parameter_declaration (confidence 0.9)
if (node.type === 'parameter_declaration') {
const typeNode = node.childForFieldName('type');
if (typeNode) {
Expand All @@ -236,7 +243,70 @@ function extractGoTypeMapDepth(node, ctx, depth) {
for (let i = 0; i < node.childCount; i++) {
const child = node.child(i);
if (child && child.type === 'identifier') {
ctx.typeMap.set(child.text, typeName);
setIfHigher(ctx.typeMap, child.text, typeName, 0.9);
}
}
}
}
}

// short_var_declaration: x := Struct{}, x := &Struct{}, x := NewFoo()
// Handles multi-variable forms: x, y := A{}, B{}
if (node.type === 'short_var_declaration') {
const left = node.childForFieldName('left');
const right = node.childForFieldName('right');
if (left && right) {
const lefts =
left.type === 'expression_list'
? Array.from({ length: left.childCount }, (_, i) => left.child(i)).filter(
(c) => c?.type === 'identifier',
)
: left.type === 'identifier'
? [left]
: [];
const rights =
right.type === 'expression_list'
? Array.from({ length: right.childCount }, (_, i) => right.child(i)).filter(
(c) => c?.isNamed,
)
: [right];

for (let idx = 0; idx < lefts.length; idx++) {
const varNode = lefts[idx];
const rhs = rights[idx];
if (!varNode || !rhs) continue;

// x := Struct{...} — composite literal (confidence 1.0)
if (rhs.type === 'composite_literal') {
const typeNode = rhs.childForFieldName('type');
if (typeNode) {
const typeName = extractGoTypeName(typeNode);
if (typeName) setIfHigher(ctx.typeMap, varNode.text, typeName, 1.0);
}
}
// x := &Struct{...} — address-of composite literal (confidence 1.0)
if (rhs.type === 'unary_expression') {
const operand = rhs.childForFieldName('operand');
if (operand && operand.type === 'composite_literal') {
const typeNode = operand.childForFieldName('type');
if (typeNode) {
const typeName = extractGoTypeName(typeNode);
if (typeName) setIfHigher(ctx.typeMap, varNode.text, typeName, 1.0);
}
}
}
// x := NewFoo() or x := pkg.NewFoo() — factory function (confidence 0.7)
if (rhs.type === 'call_expression') {
const fn = rhs.childForFieldName('function');
if (fn && fn.type === 'selector_expression') {
const field = fn.childForFieldName('field');
if (field?.text.startsWith('New')) {
const typeName = field.text.slice(3);
if (typeName) setIfHigher(ctx.typeMap, varNode.text, typeName, 0.7);
}
} else if (fn && fn.type === 'identifier' && fn.text.startsWith('New')) {
const typeName = fn.text.slice(3);
if (typeName) setIfHigher(ctx.typeMap, varNode.text, typeName, 0.7);
}
}
}
Expand Down
4 changes: 2 additions & 2 deletions src/extractors/java.js
Original file line number Diff line number Diff line change
Expand Up @@ -235,7 +235,7 @@ function handleJavaLocalVarDecl(node, ctx) {
const child = node.child(i);
if (child?.type === 'variable_declarator') {
const nameNode = child.childForFieldName('name');
if (nameNode) ctx.typeMap.set(nameNode.text, typeName);
if (nameNode) ctx.typeMap.set(nameNode.text, { type: typeName, confidence: 0.9 });
}
}
}
Expand Down Expand Up @@ -280,7 +280,7 @@ function extractJavaParameters(paramListNode, typeMap) {
if (typeNode) {
const typeName =
typeNode.type === 'generic_type' ? typeNode.child(0)?.text : typeNode.text;
if (typeName) typeMap.set(nameNode.text, typeName);
if (typeName) typeMap.set(nameNode.text, { type: typeName, confidence: 0.9 });
}
}
}
Expand Down
Loading
Loading