Version: reproduced on 0.9.40 (and 0.9.17)
The AST extractor emits INFERRED uses edges that appear to encode "class C is defined in module M, M imports X, therefore C uses X". The endpoint is wrong whenever the importing module also defines a class: the function is the real user, not the class that happens to share the file. The edge is anchored at the import line, so source_location looks like corroboration.
Repro
helpers.py
api.py
from helpers import Helper
class Request:
x: int = 0
def handler(req):
return Helper()
from pathlib import Path
from graphify.extract import extract
r = extract([Path('api.py'), Path('helpers.py')], cache_root=Path('.'))
for e in r['edges']:
if e.get('confidence') == 'INFERRED':
print(e['source'], e['relation'], e['target'], e['source_file'], e['source_location'])
Actual
api_request --uses--> helpers_helper [INFERRED] api.py:L1
Expected — either api_handler --uses--> helpers_helper, or no inferred edge at all. Request never references Helper.
Why it matters
This is systematic rather than occasional in any framework codebase that co-locates request/response models with handlers. In a FastAPI + Pydantic repo (966 nodes) all 19 uses edges in the graph were false — not one was a genuine class-to-class relationship. Representative sample:
FieldGenerateRequest --uses--> Toggles | from backend.prompts.router import PromptRouter, Toggles
LLMResult --uses--> MockLLMClient | from backend.llm.mock_client import MockLLMClient
LLMClient --uses--> GeminiLLMClient | from backend.llm.gemini_client import GeminiLLMClient
The first is fully spurious — that request model never touches Toggles. The others invert a real relationship: the clients return LLMResult and implement LLMClient, not the reverse.
These edges reach users: they surface in GRAPH_REPORT.md under "Surprising Connections", they are traversed by query, and the report's own "Are the N inferred relationships involving X actually correct?" prompt sends people to a source_location that shows only an import statement.
Workaround
Dropping AST-inferred edges whose source_location resolves to an import / from … import statement removed all 19 false positives here and kept all 15 genuine inferred edges (isinstance assertions, monkeypatch.setattr, constructor calls), so the import-line anchor cleanly separates the two classes.
Checked for duplicates
#2241 (call arguments binding to a single-letter test helper), #991 / #726 / #630 (cross-language identifier collapse in semantic chunks) are different mechanisms; #1633 is a feature request. This is structural extraction, single-language, and reproduces with no LLM backend involved.
Version: reproduced on 0.9.40 (and 0.9.17)
The AST extractor emits
INFERREDusesedges that appear to encode "classCis defined in moduleM,MimportsX, thereforeCusesX". The endpoint is wrong whenever the importing module also defines a class: the function is the real user, not the class that happens to share the file. The edge is anchored at the import line, sosource_locationlooks like corroboration.Repro
helpers.pyapi.pyActual
Expected — either
api_handler --uses--> helpers_helper, or no inferred edge at all.Requestnever referencesHelper.Why it matters
This is systematic rather than occasional in any framework codebase that co-locates request/response models with handlers. In a FastAPI + Pydantic repo (966 nodes) all 19
usesedges in the graph were false — not one was a genuine class-to-class relationship. Representative sample:The first is fully spurious — that request model never touches
Toggles. The others invert a real relationship: the clients returnLLMResultand implementLLMClient, not the reverse.These edges reach users: they surface in
GRAPH_REPORT.mdunder "Surprising Connections", they are traversed byquery, and the report's own "Are the N inferred relationships involvingXactually correct?" prompt sends people to asource_locationthat shows only an import statement.Workaround
Dropping AST-inferred edges whose
source_locationresolves to animport/from … importstatement removed all 19 false positives here and kept all 15 genuine inferred edges (isinstanceassertions,monkeypatch.setattr, constructor calls), so the import-line anchor cleanly separates the two classes.Checked for duplicates
#2241 (call arguments binding to a single-letter test helper), #991 / #726 / #630 (cross-language identifier collapse in semantic chunks) are different mechanisms; #1633 is a feature request. This is structural extraction, single-language, and reproduces with no LLM backend involved.