Repository navigation
AST 'uses' relation cross-products every local class with every imported symbol in a file (Python), false positives backfilled to confidence_score=0.5 #2236
Description
Problem
The AST uses relation for Python (extractors/resolution.py, the from .X import A, B, C resolution pass) does not check whether a local class actually references an imported symbol. It cross-products every class defined in a file against every name imported into that file, emitting a uses edge for each pair unconditionally:
# extractors/resolution.py, ~line 1922
local_classes = [
n["id"] for n in file_result.get("nodes", [])
if n.get("source_file") == str_path
and not n["label"].endswith((")", ".py"))
and n["id"] != _make_id(stem)
and n.get("file_type") != "rationale"
]
...
# ~line 1988
for name in imported_names:
tgt_nid = stem_to_entities[target_fq].get(name)
if tgt_nid:
for src_class_nid in local_classes:
new_edges.append({
"source": src_class_nid,
"target": tgt_nid,
"relation": "uses",
"confidence": "INFERRED",
"source_file": str_path,
"source_location": f"L{line}",
"weight": 0.8,
})There is no check that src_class_nid's body (fields, annotations, method bodies) actually mentions the imported name — every class in the file gets an edge to every import in the file, module-wide co-occurrence standing in for a real reference.
Compounding this, these edges never set confidence_score (only weight: 0.8). At export time, export.py:
# export.py:159
_CONFIDENCE_SCORE_DEFAULTS = {"EXTRACTED": 1.0, "INFERRED": 0.5, "AMBIGUOUS": 0.2}
...
# export.py:302-304
if "confidence_score" not in link:
...
link["confidence_score"] = _CONFIDENCE_SCORE_DEFAULTS.get(conf, 1.0)backfills the missing field to a flat 0.5 for every one of these edges. So every false-positive uses edge is also indistinguishable from a genuinely graded low-confidence edge — it shows up as a hard cliff at exactly confidence_score: 0.5, relation: uses, confidence: INFERRED.
Reproducer
Real-world repro from a FastAPI backend (backend/routers/admin.py):
class AgentConfigBody(BaseModel):
agent_name: str
task_type: Optional[str] = None
model_name: str
temperature: Optional[float] = None
is_active: bool = TrueAgentConfigBody has zero fields typed as AgentConfig, Subscription, or Payment. Yet the extracted graph contains:
AgentConfigBody --uses--> AgentConfig [INFERRED 0.5]
AgentConfigBody --uses--> Subscription [INFERRED 0.5]
AgentConfigBody --uses--> Payment [INFERRED 0.5]
...because admin.py happens to import all three ORM models somewhere else in the file for unrelated route handlers. The same pattern repeats for every other Pydantic body class in that file (QuotaOverrideBody, etc.) and in routers/plans.py (PackBody, PackUpdate, PlanUpdate each "using" Plan, Subscription, Payment).
On this ~2000-node graph (241 files, mixed FastAPI backend + React frontend), 191 of 5176 edges (3.7%) carried this exact signature: relation=uses (187) or relation=indirect_call (4, same missing-confidence_score pattern, worth checking separately), confidence=INFERRED, confidence_score=0.5 exactly. Filtering these out and re-clustering changed community structure — communities that looked artificially merged (e.g. "Admin API Routes" and "Billing Models") cleanly separated once the phantom edges were removed.
Why this is a different bug from #540
#540 tracks the LLM subagents' semantic extraction defaulting to 0.5 for genuinely-uncertain INFERRED edges — a calibration/prompting problem, already mitigated by the current extraction-spec's forced discrete rubric (0.55/0.65/0.75/0.85/0.95, "never 0.5").
This issue is about the deterministic AST pass (no LLM involved) producing edges that are structurally guaranteed to be false positives whenever a file defines >1 local class and imports >1 external symbol — it's a correctness bug in the resolver, not a calibration problem. The flat 0.5 is a downstream symptom (the missing-field default in export.py), not the root cause.
Suggested fix
- In
resolution.py's import-resolution pass, before emitting ausesedge for(src_class_nid, tgt_nid), check whethersrc_class_nid's own node span (or at minimum its field-annotation text) actually contains the imported name as a token. Skip the edge if not — this turns the cross-product into a real (if approximate) reference check. - Independently,
export.py's_CONFIDENCE_SCORE_DEFAULTS["INFERRED"] = 0.5silently masks any future producer that forgets to setconfidence_scorebehind a value that looks like a real score rather than "unset." Consider either requiringconfidence_scoreat the point of edge creation (fail loud in dev/tests) or writing an out-of-band sentinel (e.g.null, or aconfidence_score_source: "default"flag) so downstream consumers (and this exact investigation) don't have to reverse-engineer which edges were actually scored vs. defaulted.
Environment
graphifyy0.9.28 (pip show graphifyy)- Repro corpus: 241 files (223 code, 18 docs), FastAPI + SQLAlchemy backend, React frontend
- Found via
/graphifyskill's Step 4.5 health check + manualconfidence_scoredistribution audit
Activity
hopstreax commentedon Jul 27, 2026
Hi! I'd like to work on this issue if it's available.
I'll start by reproducing the reported behavior, trace the
usesedge generation inextractors/resolution.py, and investigate howconfidence_scoreis populated downstream. My plan is to first address the false-positiveusesedges by ensuring they're only created when the local class actually references the imported symbol. I'll keep theconfidence_scorediscussion separate unless it makes sense to include it as part of the same fix.Looking forward to working on this!
omsenjalia commentedon Oct 18, 2026
This looks fixed on the
v8branch by c08d9afa (fix(extract): attribute cross-file INFERREDusesedges to the referencing symbol (#2652)). Could a maintainer confirm and close?