Repository navigation
Description
Problem
PyNeo4jBackend's per-callable edge query binds the containment relationship twice:
MATCH (c:PyCallable) WHERE c.id STARTS WITH $prefix AND c.signature = $sig
MATCH (c)-[:PY_HAS_BODY_NODE]->(s:PyBodyNode)-[r:{rel}]->(d:PyBodyNode)<-[:PY_HAS_BODY_NODE]-(c)(cldk/analysis/python/neo4j/neo4j_backend.py:1335-1338)
When s and d are the same node — a self-loop, which a data-dependence edge legitimately is when a statement redefines a variable it reads — both PY_HAS_BODY_NODE hops match the same relationship. Cypher's relationship-uniqueness rule forbids that within one MATCH, so the row is discarded. No error, no diagnostic: the page is simply short.
Measured
On the leg-1.6 reference graph (odoo-slim-19, codeanalyzer-python 1.4.1) there are 64,702 PY_DDG self-loops. PY_CFG_NEXT and PY_SUMMARY have none, so get_cfg and get_summary are unaffected on this corpus — but the query shape is shared, so they are equally vulnerable to a corpus that has them.
For one callable, AccountAccruedOrdersWizard._compute_move_vals:
| query | edges |
|---|---|
| the shipped spelling | 28,146 |
the same, with the second hop as a WHERE predicate instead of a pattern |
28,394 |
| self-loops on that callable | 248 |
The difference is exactly the self-loops. total is computed with the same MATCH, so the count agrees with the short page and complete reports True — the result claims to be whole.
Where it does not happen
TypeScript's equivalent anchors on the callable's id prefix rather than a second containment hop, so it is unaffected:
MATCH (s:CanNode:TSBodyNode) WHERE s.id STARTS WITH $callable_id + '@' ...That spelling was chosen there on measurement (it is also 1.3× to 5× faster), and it happens to sidestep this entirely. Java's backend does not rebuild per-callable graphs yet, so it has nothing to fix.
Scope boundary
In scope: the three _own_edges query shapes on PyNeo4jBackend, and a test that fails on the current code. Out of scope: the ordering and paging logic around them, which is correct.
Goals
- A self-loop edge appears in the page and in
total - A regression test with a self-loop in the fixture graph, failing before the fix
- The three accessors keep their measured performance — if the fix costs a plan change, measure it and say so
Definition of done
-
get_ddg()for the callable above returns 28,394 edges rather than 28,146 - The live parity suite compares an edge count that includes self-loops
Activity
docs(spec): correct §2 — Python's value traversals are scoped by id, …