Summary
With graphifyy 0.9.54, direct Python recursion does not produce a calls self-edge in the output of graphify.extract.extract(). An ordinary call to the same function is extracted correctly.
This reproduces before graph construction, deduplication, export, or querying. The example invokes the upstream extractor directly: no custom adapter, semantic backend, or LLM is involved.
Minimal reproduction
Run this as a Python script in an environment with graphifyy==0.9.54 and its Python tree-sitter dependencies installed:
from pathlib import Path
from tempfile import TemporaryDirectory
from graphify.extract import extract
with TemporaryDirectory() as directory:
root = Path(directory)
source = root / "repro.py"
source.write_text(
"def factorial(n):\n"
" return 1 if n < 2 else n * factorial(n - 1)\n"
"\n"
"def entry(n):\n"
" return factorial(n)\n",
encoding="utf-8",
)
result = extract([source], root=root, cache_root=root)
calls = [
(edge["source"], edge["target"])
for edge in result["edges"]
if edge.get("relation") == "calls"
]
print("calls:", calls)
print("ordinary call:", ("repro_entry", "repro_factorial") in calls)
print("recursive call:", ("repro_factorial", "repro_factorial") in calls)
Actual result
calls: [('repro_entry', 'repro_factorial')]
ordinary call: True
recursive call: False
Both function nodes exist, but the call in the body of factorial is absent.
Expected result
In addition to entry -> factorial, include an extracted factorial -> factorial calls edge with the recursive call's source location. This would preserve the actual call structure for downstream inspection.
If omitting direct recursion is an intentional extractor contract, could that limitation be documented, or could retaining recursive calls be supported explicitly?
Source observations
The same condition is present on the current v8 branch at commit 937e59a5476fcb2665d6c4f4b7c0d0a4142011b6:
- extractors/engine.py, direct-call handling:
if tgt_nid and tgt_nid != caller_nid: excludes a resolved call whose target is its caller.
- In the installed 0.9.54
extract.py, the common raw-call resolution path also requires tgt != caller (line 7154).
- Conversely, the installed
build.py explicitly preserves meaningful recursive calls self-edges while filtering only collapsed import/re-export self-loops. A supplied recursive edge survives graph construction.
This appears to be an extraction-stage omission, not loss of an existing edge during serialization or incremental merge. I have not established when the behavior was introduced, so this report does not claim a regression introduced specifically in 0.9.54.
Related reports checked
#2037 and #2038 address false import self-loops and preserving an already-supplied recursive calls edge in build_from_json(). The regression test in #2038 constructs that self-edge manually; it does not test extraction of a recursive Python function. The maintainer marked that issue fixed in 0.9.21.
#2374 / #2401 concern false self-loops caused by stub rewiring, rather than an absent direct recursive call.
I searched existing open/closed issues and PRs using recursion, recursive calls, self-loop, self-call, factorial, and caller_nid, and did not find a report for this specific extraction-stage case. Please link this to an existing report if I missed one.
Environment
- Graphifyy: 0.9.54
- Python: 3.13.14
- Windows 11, build 26200, AMD64
- tree-sitter: 0.25.2
- tree-sitter-python: 0.25.0
The extractor and build modules used for the reproduction match the same-version upstream package files. Only a disposable synthetic source file and temporary extraction cache are used.
Summary
With
graphifyy 0.9.54, direct Python recursion does not produce acallsself-edge in the output ofgraphify.extract.extract(). An ordinary call to the same function is extracted correctly.This reproduces before graph construction, deduplication, export, or querying. The example invokes the upstream extractor directly: no custom adapter, semantic backend, or LLM is involved.
Minimal reproduction
Run this as a Python script in an environment with
graphifyy==0.9.54and its Python tree-sitter dependencies installed:Actual result
Both function nodes exist, but the call in the body of
factorialis absent.Expected result
In addition to
entry -> factorial, include an extractedfactorial -> factorialcallsedge with the recursive call's source location. This would preserve the actual call structure for downstream inspection.If omitting direct recursion is an intentional extractor contract, could that limitation be documented, or could retaining recursive calls be supported explicitly?
Source observations
The same condition is present on the current
v8branch at commit937e59a5476fcb2665d6c4f4b7c0d0a4142011b6:if tgt_nid and tgt_nid != caller_nid:excludes a resolved call whose target is its caller.extract.py, the common raw-call resolution path also requirestgt != caller(line 7154).build.pyexplicitly preserves meaningful recursivecallsself-edges while filtering only collapsed import/re-export self-loops. A supplied recursive edge survives graph construction.This appears to be an extraction-stage omission, not loss of an existing edge during serialization or incremental merge. I have not established when the behavior was introduced, so this report does not claim a regression introduced specifically in 0.9.54.
Related reports checked
#2037 and #2038 address false import self-loops and preserving an already-supplied recursive
callsedge inbuild_from_json(). The regression test in #2038 constructs that self-edge manually; it does not test extraction of a recursive Python function. The maintainer marked that issue fixed in 0.9.21.#2374 / #2401 concern false self-loops caused by stub rewiring, rather than an absent direct recursive call.
I searched existing open/closed issues and PRs using recursion, recursive calls, self-loop, self-call, factorial, and caller_nid, and did not find a report for this specific extraction-stage case. Please link this to an existing report if I missed one.
Environment
The extractor and build modules used for the reproduction match the same-version upstream package files. Only a disposable synthetic source file and temporary extraction cache are used.