Summary
PyNeo4jBackend._to_uri (cldk/analysis/python/neo4j/neo4j_backend.py) mis-parses the
graph's PyCFGNode.id and appends a spurious trailing @ to every vertex URI it
produces, breaking parity between the local and Neo4j backends for program_graph(),
sdg_edges(), and resolve_location() (and, transitively, every cldk.graph.Engine
verb run against a PyNeo4jBackend provider).
Root cause
def _to_uri(self, cfg_node_id: str) -> str:
sig, _, key = cfg_node_id.partition("#")
can = self._sig_to_can().get(sig, sig)
return f"{can}@{key.removeprefix('@')}"
This assumes the raw PyCFGNode.id stored in the graph has the shape
"<dotted_sig>#<local_key>". In codeanalyzer-python 1.0.2's actual projection, n.id is
already the fully-qualified can://... vertex id (no # separator at all). Since
partition("#") never matches, key is always "" and sig is the full id (a miss
against the dotted-signature→can map, so it falls through unchanged) — every call
returns f"{already_correct_id}@", i.e. the correct id with one extra trailing @.
Reproduction
tests/graph/test_py_parity_live.py (added in #270 Task 6, env-gated on
CLDK_TEST_NEO4J_URI) reproduces this against a real analyzer run + a real Neo4j
instance:
podman run -d --name cldk-graph-parity -p 7687:7687 -e NEO4J_AUTH=neo4j/testpassword neo4j:5
CLDK_TEST_NEO4J_URI=bolt://localhost:7687 CLDK_TEST_NEO4J_USER=neo4j \
CLDK_TEST_NEO4J_PASSWORD=testpassword uv run --extra neo4j pytest tests/graph/test_py_parity_live.py -v
test_program_graph_parity_per_callable, test_sdg_parity, and test_verb_parity all
fail; test_max_level_parity passes. Diffing node sets confirms the only discrepancy
is the trailing @ — stripping one trailing @ from every remote-side id makes every
set/edge comparison match exactly (hand-verified against the pyfix fixture: 3/3
callables' program graphs, sdg edges, and all three verb-parity checks agree modulo
that one character).
Fix sketch
_to_uri should treat cfg_node_id as already being the canonical URI when no # is
present (or, more robustly, the projector/reconstruction contract for PyCFGNode.id
should be re-confirmed and _to_uri updated to match it exactly — this may also affect
resolve_location's reliance on the same id shape).
Scope note (also worth fixing nearby)
While diagnosing this, tests/graph/test_py_parity_live.py's harness needed
app_name == proj.name (not an arbitrary custom name) when re-emitting to Neo4j from an
already-analyzed PyCodeanalyzer.application object: codeanalyzer.neo4j.emit.assign_ids
re-stamps every module/class/callable .id in place using whatever app_name is passed,
but does not retroactively update the already-baked-in cfg/ddg/param_in/param_out edge
src/dst strings from the original analysis. Passing a different app_name at emit time than
the one the analyzer used internally (options.app_name or project_dir.name) silently
corrupts the shared application object's internal id consistency. This is now documented
inline in the test, but may be worth a doc note on emit_neo4j/assign_ids itself for
anyone else populating Neo4j from an in-memory analysis rather than a fresh analyze() run.
Filed while executing #270 Task 6 (dual-backend parity suite).
Summary
PyNeo4jBackend._to_uri(cldk/analysis/python/neo4j/neo4j_backend.py) mis-parses thegraph's
PyCFGNode.idand appends a spurious trailing@to every vertex URI itproduces, breaking parity between the local and Neo4j backends for
program_graph(),sdg_edges(), andresolve_location()(and, transitively, everycldk.graph.Engineverb run against a
PyNeo4jBackendprovider).Root cause
This assumes the raw
PyCFGNode.idstored in the graph has the shape"<dotted_sig>#<local_key>". In codeanalyzer-python 1.0.2's actual projection,n.idisalready the fully-qualified
can://...vertex id (no#separator at all). Sincepartition("#")never matches,keyis always""andsigis the full id (a missagainst the dotted-signature→can map, so it falls through unchanged) — every call
returns
f"{already_correct_id}@", i.e. the correct id with one extra trailing@.Reproduction
tests/graph/test_py_parity_live.py(added in #270 Task 6, env-gated onCLDK_TEST_NEO4J_URI) reproduces this against a real analyzer run + a real Neo4jinstance:
test_program_graph_parity_per_callable,test_sdg_parity, andtest_verb_parityallfail;
test_max_level_paritypasses. Diffing node sets confirms the only discrepancyis the trailing
@— stripping one trailing@from every remote-side id makes everyset/edge comparison match exactly (hand-verified against the pyfix fixture: 3/3
callables' program graphs, sdg edges, and all three verb-parity checks agree modulo
that one character).
Fix sketch
_to_urishould treatcfg_node_idas already being the canonical URI when no#ispresent (or, more robustly, the projector/reconstruction contract for
PyCFGNode.idshould be re-confirmed and
_to_uriupdated to match it exactly — this may also affectresolve_location's reliance on the same id shape).Scope note (also worth fixing nearby)
While diagnosing this,
tests/graph/test_py_parity_live.py's harness neededapp_name == proj.name(not an arbitrary custom name) when re-emitting to Neo4j from analready-analyzed
PyCodeanalyzer.applicationobject:codeanalyzer.neo4j.emit.assign_idsre-stamps every module/class/callable
.idin place using whateverapp_nameis passed,but does not retroactively update the already-baked-in cfg/ddg/param_in/param_out edge
src/dst strings from the original analysis. Passing a different
app_nameat emit time thanthe one the analyzer used internally (
options.app_name or project_dir.name) silentlycorrupts the shared application object's internal id consistency. This is now documented
inline in the test, but may be worth a doc note on
emit_neo4j/assign_idsitself foranyone else populating Neo4j from an in-memory analysis rather than a fresh
analyze()run.Filed while executing #270 Task 6 (dual-backend parity suite).