Found by running codemap cold on an unfamiliar Python repo. It reports the package's modules as unconnected and gives no indication anything was dropped.
Symptom
~/Code/agentgate — a 14-module Python package:
$ codemap --deps .
Src ════════════════════════════════════════
+12 standalone files
Tests ══════════════════════════════════════
test_agentgate ──┬──▶ src/agentgate/auth
├──▶ src/agentgate/config
└──▶ ... 8 total
13 files · 86 functions · 8 deps
Every source module is "standalone". The only edges in the whole project come from the test file.
That is not what the code says. src/agentgate/cli.py:12:
from .client import call
from .config import Paths, default_home, init_home, load_config, save_config
from .crypto_store import EncryptedStore
Counted across src/agentgate/:
| import form |
count |
codemap resolved |
relative — from .config import ... |
31 |
0 |
absolute — from agentgate.config import ... |
0 |
— |
absolute, in tests/ |
8 |
8 |
So --importers src/agentgate/config.py reports importer_count: 1 (the test file). cli.py imports it on line 13 and is not counted.
Relative imports are the dominant intra-package convention in modern Python, so this is not an edge case — it is most Python packages.
Cause
fuzzyResolveWithWorkspace routes any leading-dot import to resolveRelative (scanner/filegraph.go:316):
if strings.HasPrefix(imp, ".") {
return resolveRelative(imp, fromDir, idx, sourceLanguage)
}
But resolveRelative (filegraph.go:384) is written for filesystem-style specifiers and only understands a dot followed by a slash:
for strings.HasPrefix(rest, "../") { levels++; rest = strings.TrimPrefix(rest, "../") }
rest = strings.TrimPrefix(rest, "./")
Python's form has no slash. .client matches neither ../ nor ./, so rest stays .client and the lookup becomes <dir>/.client — a dotfile that does not exist. Nothing resolves, and nothing is reported.
Python's actual semantics are dot-counting, not path-prefix:
| specifier |
means |
.client |
sibling module client in the current package |
..pkg.mod |
up one package, then pkg/mod |
from . import x |
the current package itself |
Suggested fix
Give Python its own branch before the generic relative handler: count leading dots (first dot = current package, each additional = one level up), then translate the remaining dotted path to a path separator, then resolve <dir>/<path>.py or <dir>/<path>/__init__.py.
The bySuffix and byExact machinery already handles the final lookup — only the specifier translation is missing. Worth checking the same shape for other dot-separated languages before assuming Python is unique here.
Why this is the serious kind
Same class as #132 (TypeScript ESM). The graph is not merely incomplete — it is confidently wrong and says nothing:
--deps presents "12 standalone files" as a finding
--importers config.py answers 1 when the true answer is 4
coverage.status reports no problem
An agent asked "what depends on config.py?" gets a number that is quietly wrong by 4x and no signal to distrust it. This is the failure mode that makes a blast-radius answer unsafe to act on.
Related: #132 (same class, TS/ESM), #111 (per-query coverage would surface it), #133.
Found by running codemap cold on an unfamiliar Python repo. It reports the package's modules as unconnected and gives no indication anything was dropped.
Symptom
~/Code/agentgate— a 14-module Python package:Every source module is "standalone". The only edges in the whole project come from the test file.
That is not what the code says.
src/agentgate/cli.py:12:Counted across
src/agentgate/:from .config import ...from agentgate.config import ...tests/So
--importers src/agentgate/config.pyreportsimporter_count: 1(the test file).cli.pyimports it on line 13 and is not counted.Relative imports are the dominant intra-package convention in modern Python, so this is not an edge case — it is most Python packages.
Cause
fuzzyResolveWithWorkspaceroutes any leading-dot import toresolveRelative(scanner/filegraph.go:316):But
resolveRelative(filegraph.go:384) is written for filesystem-style specifiers and only understands a dot followed by a slash:Python's form has no slash.
.clientmatches neither../nor./, soreststays.clientand the lookup becomes<dir>/.client— a dotfile that does not exist. Nothing resolves, and nothing is reported.Python's actual semantics are dot-counting, not path-prefix:
.clientclientin the current package..pkg.modpkg/modfrom . import xSuggested fix
Give Python its own branch before the generic relative handler: count leading dots (first dot = current package, each additional = one level up), then translate the remaining dotted path to a path separator, then resolve
<dir>/<path>.pyor<dir>/<path>/__init__.py.The
bySuffixandbyExactmachinery already handles the final lookup — only the specifier translation is missing. Worth checking the same shape for other dot-separated languages before assuming Python is unique here.Why this is the serious kind
Same class as #132 (TypeScript ESM). The graph is not merely incomplete — it is confidently wrong and says nothing:
--depspresents "12 standalone files" as a finding--importers config.pyanswers1when the true answer is 4coverage.statusreports no problemAn agent asked "what depends on
config.py?" gets a number that is quietly wrong by 4x and no signal to distrust it. This is the failure mode that makes a blast-radius answer unsafe to act on.Related: #132 (same class, TS/ESM), #111 (per-query coverage would surface it), #133.