diff --git a/graphify/markdown_resolution.py b/graphify/markdown_resolution.py index 1c174adb8..a131f5ed2 100644 --- a/graphify/markdown_resolution.py +++ b/graphify/markdown_resolution.py @@ -229,7 +229,14 @@ def resolve_markdown_mentions( def _add(rc: dict, target: str | None, confidence: str, score: float) -> None: caller = str(rc.get("caller_nid", "")) - if target is None or target == caller or (caller, target) in existing: + if target is None or target == caller: + return + doc = _posix(str(rc.get("source_file", ""))) + if (caller, target) in existing: + # The edge is already in the graph, but this document still CITES that + # target, so it stays on record as owner evidence for a later method + # mention in the same document (#4609 review finding). + cited_targets.setdefault(doc, set()).add(target) return existing.add((caller, target)) all_edges.append({ @@ -243,7 +250,7 @@ def resolve_markdown_mentions( "source_location": rc.get("source_location"), "weight": 1.0, }) - cited_targets.setdefault(_posix(str(rc.get("source_file", ""))), set()).add(target) + cited_targets.setdefault(doc, set()).add(target) # Pass 1: explicit evidence resolves on its own — a cited path, a dotted # qualifier chain, or a trailing (). Record those targets per document. diff --git a/tests/test_markdown_code_spans.py b/tests/test_markdown_code_spans.py index 7cde0df9e..603c21eca 100644 --- a/tests/test_markdown_code_spans.py +++ b/tests/test_markdown_code_spans.py @@ -346,3 +346,39 @@ def test_owner_cited_in_another_document_does_not_count(tmp_path): b = _node(r, "B") assert {t for (s, t) in _references(r) if s == b["id"]} == set() + + +def test_owner_evidence_survives_an_edge_that_already_exists(): + """A mention whose edge is already in the graph still counts as citing its target. + + The resolver skips an edge that is already there, but the document has still + cited that symbol, so the evidence must stay on record for a later method + mention in the same document (#4609, review finding on the PR). + """ + from graphify.markdown_resolution import resolve_markdown_mentions + + per_file = [{"raw_calls": [ + {"caller_nid": "doc_notes", "callee": "HandlerResult", "language": "markdown", + "context": "HandlerResult", "source_file": "notes.md", "source_location": "L3"}, + {"caller_nid": "doc_notes", "callee": "successWithWarnings", "language": "markdown", + "context": "successWithWarnings", "source_file": "notes.md", "source_location": "L3"}, + ]}] + nodes = [ + {"id": "doc_notes", "label": "Notes", "file_type": "document", + "source_file": "notes.md"}, + {"id": "handler_handlerresult", "label": "HandlerResult", "file_type": "code", + "_callable": True, "_callable_class": True, "source_file": "handler.py"}, + {"id": "handler_handlerresult_successwithwarnings", "label": ".successWithWarnings()", + "file_type": "code", "_callable": True, "source_file": "handler.py"}, + ] + edges = [ + {"source": "handler_handlerresult", + "target": "handler_handlerresult_successwithwarnings", "relation": "method"}, + # The class reference edge is already in the graph. + {"source": "doc_notes", "target": "handler_handlerresult", "relation": "references"}, + ] + + resolve_markdown_mentions(per_file, nodes, edges) + + assert ("doc_notes", "handler_handlerresult_successwithwarnings") in { + (e["source"], e["target"]) for e in edges if e.get("relation") == "references"}