Problem
classify_fqn decides a namespace from the root segment alone (indices.py:192). When a third-party package shares its name with an internal directory, its symbols classify INTERNAL — and, since #459, are then reported as unresolved internal rather than as the third-party calls they are.
Measured on owner-api (2026-09-17)
| target shape |
edges |
what it really is |
alembic.op.add_column, …drop_column, …get_bind, … |
552 |
the alembic library, from alembic import op — internal only because app/alembic/ exists |
app.add_middleware, app.include_router, app.mount |
~9 |
FastAPI methods on an instance named app |
app.models.User.is_deleted.is_ and similar |
~16 |
SQLAlchemy on a model attribute |
Together ~750 edges on one backend, reported as unresolved when EXTERNAL would be the truthful answer for the library half.
Why it matters
unresolved_ratio is the number the project advertises as its honesty measure (README, docs/CASE_STUDY.md). Counting a library call as "could not place" overstates the gap in the same way that counting it INTERNAL used to understate it. The import map already knows the answer: from alembic import op is recorded there as a third-party root.
Sketch
When the root segment is both an internal directory and a key in the file's import map pointing at a third-party root, prefer the import map. The file that owns the reference is the tiebreaker — the same shape as the source_file argument added in #454, which made stdlib names mean stdlib only in Python sources.
Acceptance
from alembic import op; op.add_column(...) in a repository that also has app/alembic/ classifies EXTERNAL.
- A test pins the opposite direction: a genuine internal
alembic.env.run_migrations still classifies INTERNAL.
- The owner-api unresolved share drops by the ~550 alembic edges; the number goes in the PR.
Found while reviewing #493.
Problem
classify_fqndecides a namespace from the root segment alone (indices.py:192). When a third-party package shares its name with an internal directory, its symbols classify INTERNAL — and, since #459, are then reported as unresolved internal rather than as the third-party calls they are.Measured on owner-api (2026-09-17)
alembic.op.add_column,…drop_column,…get_bind, …from alembic import op— internal only becauseapp/alembic/existsapp.add_middleware,app.include_router,app.mountappapp.models.User.is_deleted.is_and similarTogether ~750 edges on one backend, reported as unresolved when EXTERNAL would be the truthful answer for the library half.
Why it matters
unresolved_ratiois the number the project advertises as its honesty measure (README,docs/CASE_STUDY.md). Counting a library call as "could not place" overstates the gap in the same way that counting it INTERNAL used to understate it. The import map already knows the answer:from alembic import opis recorded there as a third-party root.Sketch
When the root segment is both an internal directory and a key in the file's import map pointing at a third-party root, prefer the import map. The file that owns the reference is the tiebreaker — the same shape as the
source_fileargument added in #454, which made stdlib names mean stdlib only in Python sources.Acceptance
from alembic import op; op.add_column(...)in a repository that also hasapp/alembic/classifies EXTERNAL.alembic.env.run_migrationsstill classifies INTERNAL.Found while reviewing #493.