Debug Symbol Resolution Issues
Symbol resolution issues appear as broken imports in canopy health, missing results from canopy_search_symbols, or canopy_trace_imports returning empty when you know imports exist. This guide walks through the diagnostic tools and the common causes.
Step 1: Check what symbols were extracted
Section titled “Step 1: Check what symbols were extracted”Before diagnosing why a symbol can’t be found, confirm it was extracted at all.
Use canopy_parse_file (MCP) or canopy stats (CLI) to inspect what Canopy sees in a specific file.
Via MCP:
Call canopy_parse_file on src/lib/payments.tsExpected output:
canopy_parse_file: src/lib/payments.ts
Exports: - createPayment (function, line 12) - PaymentConfig (interface, line 3) - PaymentResult (type alias, line 8) - DEFAULT_CURRENCY (const, line 1)
Imports: - ../models/cart (CartItem, CartTotal) → resolved: src/models/cart.ts - ../config/env (ENV) → resolved: src/config/env.ts - stripe (Stripe) → resolved: node_modules/stripe (external, not indexed)
Symbol count: 4 exports, 3 importsIf the symbol you’re looking for is NOT in the exports list, Canopy never extracted it. This could mean:
- The file has a syntax error that prevented parsing
- The export uses an unusual syntax Canopy doesn’t recognize
- The file is in an
ignored_pathsdirectory
Step 2: Check import resolution
Section titled “Step 2: Check import resolution”Broken imports appear in canopy health as P0 findings. To inspect why a specific import doesn’t resolve, use canopy_trace_imports:
Via MCP:
Call canopy_trace_imports on src/api/orders.tsExpected output for a working file:
canopy_trace_imports: src/api/orders.ts
Direct imports (4): ../lib/payments → RESOLVED → src/lib/payments.ts ../models/order → RESOLVED → src/models/order.ts ../utils/validate → RESOLVED → src/utils/validate.ts @stripe/stripe-js → EXTERNAL (not indexed)A broken import looks like:
../lib/stripe-client → UNRESOLVED Attempted paths: src/lib/stripe-client.ts — not found src/lib/stripe-client/index.ts — not found src/lib/stripe-client.js — not found“Not found” means the file doesn’t exist at that path. Either the import is wrong, the file was moved, or there’s a path alias that Canopy doesn’t know about.
Step 3: Check path alias configuration
Section titled “Step 3: Check path alias configuration”Path aliases are a common source of broken imports. If your TypeScript project uses @app/*, @shared/*, or ~/*, Canopy needs to know how to resolve them.
First, check if Canopy already reads your tsconfig.json:
FORGE_LOG=debug canopy index . 2>&1 | grep "tsconfig"If Canopy found and read your tsconfig.json, aliases defined in compilerOptions.paths are resolved automatically.
If aliases still don’t resolve after indexing, add them explicitly to .canopy/config.toml:
[resolve]aliases = [ { alias = "@app", target = "src" }, { alias = "@shared", target = "packages/shared/src" }, { alias = "~", target = "src" },]Re-run canopy index . after adding aliases. Then re-run canopy_trace_imports to verify the imports resolve.
Step 4: Check module reachability
Section titled “Step 4: Check module reachability”canopy_check_wiring tells you whether a module is reachable from any entry point:
Via MCP:
Call canopy_check_wiring on src/lib/payments.tsExpected output when reachable:
canopy_check_wiring: src/lib/payments.ts
Reachable: YES Entry point: src/index.ts Path: src/index.ts → src/api/orders.ts → src/lib/payments.ts (3 hops)Expected output when unreachable:
canopy_check_wiring: src/lib/old-payments.ts
Reachable: NO No path from any entry point to this file. Imported by: 0 files Note: this file may be dead code or a new module not yet wired in.Unreachable modules aren’t necessarily broken — they might be utilities called at runtime via dynamic import, new modules not yet connected, or intentionally standalone scripts. Use context to determine if “unreachable” is a real problem.
Step 5: Search for a symbol by name
Section titled “Step 5: Search for a symbol by name”If you know a symbol exists but can’t find it in Canopy’s results:
canopy search --symbols "createPayment"Or via MCP:
Call canopy_search_symbols with query="createPayment"If the symbol doesn’t appear, it wasn’t extracted. Go back to Step 1 and check canopy_parse_file for the file you expect it to be in.
Step 6: Run the full health check
Section titled “Step 6: Run the full health check”canopy healthLook for:
broken_import— an import that doesn’t resolve to any fileunresolved_module— a module-level import that Canopy couldn’t findmissing_export— a symbol is imported somewhere but not exported from the source
These findings point directly to resolution failures.
Common issues and fixes
Section titled “Common issues and fixes”TypeScript barrel files (index.ts re-exports)
If a file re-exports symbols from other modules with export * from './payments', Canopy tracks these re-exports. But if the barrel file itself is in ignored_paths or has a parse error, downstream imports that go through it appear broken.
Check the barrel file:
Call canopy_parse_file on src/lib/index.tsIf it shows no exports, the barrel file isn’t parsing correctly. Check for syntax errors.
Dynamic imports
import(...) is a dynamic import and Canopy tracks it separately from static imports. canopy_trace_imports includes dynamic imports in its output, but they’re marked as DYNAMIC:
import('../plugins/' + pluginName) → DYNAMIC (unresolvable at index time)Dynamic imports with variable paths can’t be resolved statically — this is expected, not a bug.
Non-standard import syntax
Some tools and frameworks use non-standard import syntax (Webpack magic comments, Vite’s import.meta.glob, etc.). Canopy may not recognize these as imports. If a widely-used file shows 0 imports in canopy_parse_file, it likely uses custom syntax.
Workaround: add the file to ignored_paths if it’s causing false positive health findings, or use canopy_search to find references manually.
canopy_trace_dependents returns fewer results than expected
canopy_trace_dependents returns only files that statically import the target. Files that reference it via a string path (e.g., require('./payments') in a config file, or a dynamic import(path)) don’t appear. This is a limitation of static analysis, not a bug.