You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
BodyNode carries no id field while PyCallable does (codeanalyzer/schema/py_schema.py:127 vs :354). The id exists: the Neo4j projection mints <callable-id>@<local> for every :PyBodyNode via _global_ordinal (neo4j/project.py:123), which mirrors IdentityMap.global_id (dataflow/identity.py:84). So the analyzer computes the id, projects it to the graph, and drops it from the JSON model. PyCallableParameter (py_schema.py:286) has the same gap.
A consumer holding a BodyNode from the JSON path cannot name it. The only workaround is composing f"{callable.id}@{body_key}" in the consumer, which reimplements the can:// grammar outside the analyzer and drifts silently. python-sdk did exactly that on signature instead of id (python-sdk#320). On the Odoo graph these are the dataflow endpoints:
kind
count
actual_in
229,035
formal_in
197,847
actual_out
133,267
formal_out
120,172
call
77,332
statement
55,169
Scope boundary
Adds id to BodyNode and PyCallableParameter in analysis.json only. Not PyCallsite (legacy list, folded into body nodes by #120). Not class attributes or variables (their ids are being redone under #173). No Neo4j change: the graph already carries the id. No schema_version bump.
Goals
BodyNode.id: str = "", populated for every body node at every level it exists (L1 call, L3 statements and @entry/@exit, L4 param vertices), value = _global_ordinal(callable.id, local_key)
PyCallableParameter.id: str = "", value = f"{callable.id}@formal_in:{i}" for position i in parameters
Stamp from the one existing function, not inlined at the four construction sites (schema/l1_body.py:12, dataflow/builder.py:276,279,291,538)
Parity test: for a fixture at -a 4, every body[k].id equals the PyBodyNode merge key the projector emits for k
Formal-order test at -a 4: for every callable and every i, body[parameters[i].id].of == parameters[i].name
Test that a call node's id is never equal to its callee
Monotonicity CI green
.claude/SCHEMA_DECISIONS.md entry; README schema section and docs/skills/analyzing-canpy-graphs vocabulary updated
Caveats and known risks
parameters[i].id names a formal_in node that exists in body only at L4. Below L4 it is a forward reference by design; document it rather than emitting it conditionally.
Python's formal_in vertices for captures and globals follow the declared parameters (dataflow/sdg.py:145), so indices past len(parameters) are not parameters. The formal-order test only covers 0..len(parameters)-1.
A cached analysis.json from 1.4.0 parses (default ""), and the analyzer version guard rebuilds it, so no stale id-less payload is served.
Definition of done
Both tests above fail on main and pass after.
analysis.json diff against a 1.4.0 fixture shows only added id keys.
Full test suite green.
Ships as codeanalyzer-python 1.5.0; python-sdk#320 unblocked.
Spec:
2026-09-05-body-node-id-in-analysis-json.mdProblem
BodyNodecarries noidfield whilePyCallabledoes (codeanalyzer/schema/py_schema.py:127vs:354). The id exists: the Neo4j projection mints<callable-id>@<local>for every:PyBodyNodevia_global_ordinal(neo4j/project.py:123), which mirrorsIdentityMap.global_id(dataflow/identity.py:84). So the analyzer computes the id, projects it to the graph, and drops it from the JSON model.PyCallableParameter(py_schema.py:286) has the same gap.A consumer holding a
BodyNodefrom the JSON path cannot name it. The only workaround is composingf"{callable.id}@{body_key}"in the consumer, which reimplements thecan://grammar outside the analyzer and drifts silently. python-sdk did exactly that onsignatureinstead ofid(python-sdk#320). On the Odoo graph these are the dataflow endpoints:actual_informal_inactual_outformal_outcallstatementScope boundary
Adds
idtoBodyNodeandPyCallableParameterinanalysis.jsononly. NotPyCallsite(legacy list, folded into body nodes by #120). Not class attributes or variables (their ids are being redone under #173). No Neo4j change: the graph already carries the id. Noschema_versionbump.Goals
BodyNode.id: str = "", populated for every body node at every level it exists (L1call, L3 statements and@entry/@exit, L4 param vertices), value =_global_ordinal(callable.id, local_key)PyCallableParameter.id: str = "", value =f"{callable.id}@formal_in:{i}"for positioniinparametersschema/l1_body.py:12,dataflow/builder.py:276,279,291,538)-a 4, everybody[k].idequals thePyBodyNodemerge key the projector emits fork-a 4: for every callable and everyi,body[parameters[i].id].of == parameters[i].namecallnode'sidis never equal to itscallee.claude/SCHEMA_DECISIONS.mdentry; README schema section anddocs/skills/analyzing-canpy-graphsvocabulary updatedCaveats and known risks
parameters[i].idnames aformal_innode that exists inbodyonly at L4. Below L4 it is a forward reference by design; document it rather than emitting it conditionally.formal_invertices for captures and globals follow the declared parameters (dataflow/sdg.py:145), so indices pastlen(parameters)are not parameters. The formal-order test only covers0..len(parameters)-1.analysis.jsonfrom 1.4.0 parses (default""), and the analyzer version guard rebuilds it, so no stale id-less payload is served.Definition of done
mainand pass after.analysis.jsondiff against a 1.4.0 fixture shows only addedidkeys.