@@ -251,6 +251,69 @@ when git is unavailable or the workspace is not a git repo.
251251 scan). This is a pre-existing gap tracked in #25 and is NOT made
252252 worse by this change.
253253
254+ ### Hybrid Type Resolution (issue #13 )
255+
256+ Adds a post-AST-pass type resolution layer that uses the per-file import
257+ registry to refine CALLS edges. Previously ` user.profile.update() ` was
258+ recorded as a call to ` update ` with no target type — the call graph had
259+ holes wherever methods were called on imported objects. Now the receiver
260+ type is resolved via the import registry, and the CALLS edge's
261+ ` target_id ` is refined to the correct target node (e.g. ` Profile.update `
262+ in ` models.py ` instead of an arbitrary ` update ` match).
263+
264+ ### Added (type resolution)
265+
266+ - ** ` scripts/hybrid_type_resolver.py ` ** — New module with:
267+ - ` build_import_registry(workspace, db_path) ` — Scans Python
268+ ` from X import Y ` / ` import X.Y as Z ` and TS/JS
269+ ` import {Y} from 'X' ` / ` import * as X from 'Y' ` statements. Stores
270+ results in a new ` import_registry ` SQLite table
271+ ` (file, local_name, module_path, symbol_name, line) ` . Also writes
272+ IMPORTS edges to ` graph_edges ` (edge_type='IMPORTS') so the graph
273+ model now carries import relationships alongside CALLS.
274+ - ` resolve_receiver_type(file_path, receiver_expr, import_registry) ` —
275+ Resolves a dotted receiver expression (` user.profile ` ) to a fully
276+ qualified type (` models.Profile ` ) via the import registry + class
277+ definitions in ` graph_nodes ` . Best-effort: returns ` None ` when
278+ unresolvable, never crashes.
279+ - ` refine_call_edges(workspace, db_path) ` — For each CALLS edge with
280+ a generic/unresolved ` target_id ` , attempts to resolve the receiver
281+ type and updates ` target_id ` to the resolved node. Stores
282+ ` {"resolved_type": "...", "resolution_method": "import_registry"} `
283+ in the edge's ` extra_json ` on success, or
284+ ` {"resolution_attempted": true, "failure_reason": "..."} ` on failure.
285+ Returns stats: ` {edges_total, edges_refined, edges_unresolved} ` .
286+
287+ - ** ` resolve-types ` command + ` codelens_resolve_types ` MCP tool** —
288+ Manually triggers type resolution without a full re-scan. Useful for
289+ agents who want to refresh type resolution after adding new imports.
290+ Output: `{status, edges_total, edges_refined, edges_unresolved,
291+ import_registry_size}`.
292+
293+ - ** IMPORTS edges in graph model** — The graph now carries two edge
294+ types: ` CALLS ` (from #8 ) and ` IMPORTS ` (from #13 ). Future
295+ ` query_graph ` work (#9 , Phase 3) can traverse both.
296+
297+ ### Changed (type resolution)
298+
299+ - ** ` scripts/commands/scan.py ` ** — After ` populate_graph_tables() ` (from
300+ #8 ), calls ` refine_call_edges(workspace, db_path) ` . Scan output now
301+ includes a ` type_resolution ` field: ` {edges_refined, edges_unresolved} ` .
302+ - ** ` scripts/graph_model.py ` ** — ` graph_stats() ` now reports IMPORTS
303+ edges in the ` edge_types ` breakdown alongside CALLS.
304+
305+ ### Non-Breaking (type resolution)
306+
307+ - Type resolution is best-effort: unresolvable edges are left unchanged
308+ with a ` resolution_attempted ` flag. No CALLS edge is ever deleted.
309+ - The ` import_registry ` table is additive — no existing table modified.
310+ - On the ` clean_app ` fixture: 11/97 CALLS edges refined, 55 unresolved
311+ (the remaining 31 are self-referential or std-lib calls that don't
312+ need refinement). On the synthetic ` type_resolution ` fixture
313+ (` tests/fixtures/type_resolution/ ` ), ` user.profile.update() ` correctly
314+ refines to ` Profile.update ` even when a ` Cache.update ` competitor
315+ exists.
316+
254317### Changed
255318
256319- ** ` scripts/persistent_registry.py ` ** — Calls ` init_graph_schema(conn) `
0 commit comments