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
{{ message }}
Repository navigation
Schema-v2 migration hazard: Neo4j code property must be derived from source spans, not a model field #176
When this analyzer migrates to canonical schema v2, the per-node code field moves off JCallable/JType (source is stored once per compilation unit, sliced by byte spans). GraphProjector currently reads c.getCode() directly (e.g. around the callable/type row builders) and Schema.java builds the j_code_fts fulltext index over c.code/c.docstring.
If the migration drops the model field without changing the projector, every :JCallable/:JType node silently loses code — the fulltext index goes dead and any SDK RETURN c.code query returns null. This exact failure shipped in codeanalyzer-python 1.0.0/1.0.1 and was fixed in 1.0.2 by deriving code at projection time from the owning module's source and the node's byte span (codellm-devkit/codeanalyzer-python#105).
Action for the v2 migration checklist: keep the graph's code property and derive it at projection time (span slice), or explicitly drop it from the SchemaCatalog + j_code_fts and bump the Neo4j SCHEMA_VERSION. Declared-but-null is the one option that breaks consumers silently.
The hazard materialised in 3.0.1/3.0.2, though not as a null. code is still projected, so the fulltext index is alive — but its value changed meaning relative to v1, and the graph now disagrees with the analyzer's own JSON.
Measured while porting the python-sdk Java Neo4j backend onto the 3.0.1 vocabulary (daytrader8, --emit neo4j, contract 2.0.0):
:JCallable.code in the graph is the declaration slice, beginning at the callable's span (so @Override\n public void cancelOrder().
JCallable.code derived from the JSON is the body block, sliced from body_span ({\n\n Connection conn = null; … \n }), which is what v1's per-callable code field held and therefore what v1's graph held too, since the projector read that field.
The two are related but not equal: graph_code.endswith(json_code) holds on all 1,117 callables that have a body, and they are identical on the 129 that do not.
Two knock-on effects for consumers:
:JCallable carries neither body_span nor any span beyond start_line/end_line, and :JModule carries no source, so a graph-only consumer cannot recover the body block. It is not a lossy-but-derivable difference; the information is absent.
Anything that indexes into code by line drifts by the declaration prefix. In our parity run, code_start_line matched on 818 of 1,216 callables and tree-sitter-derived calling lines matched on 1,299 of 1,862 call-graph edges; every mismatch is a multi-line signature or an annotated declaration.
Suggested resolution, in preference order: slice the projected code from body_span so it means what the JSON's code means, or project body_span (and ideally the module source) so a consumer can slice it themselves. Either makes the two backends agree; the current shape forces every SDK to document a divergence it cannot close.
Unrelated to this issue but found in the same pass, in case it is useful: :JBodyNode ids end in an L:C that is a key, not a position — it equals the node's span.start column on only 629 of 4,006 call nodes, with deltas up to 12 — and the projection carries no column property, so call-site columns cannot be reconstructed from the graph at all. And LOCKS is in the contract but has zero relationships in a Maven projection, so locked_version is always null there.
Activity
rahlk commentedon Sep 6, 2026
The hazard materialised in 3.0.1/3.0.2, though not as a null.
codeis still projected, so the fulltext index is alive — but its value changed meaning relative to v1, and the graph now disagrees with the analyzer's own JSON.Measured while porting the python-sdk Java Neo4j backend onto the 3.0.1 vocabulary (daytrader8,
--emit neo4j, contract2.0.0)::JCallable.codein the graph is the declaration slice, beginning at the callable'sspan(so@Override\n public void cancelOrder().JCallable.codederived from the JSON is the body block, sliced frombody_span({\n\n Connection conn = null;…\n }), which is what v1's per-callablecodefield held and therefore what v1's graph held too, since the projector read that field.graph_code.endswith(json_code)holds on all 1,117 callables that have a body, and they are identical on the 129 that do not.Two knock-on effects for consumers:
:JCallablecarries neitherbody_spannor any span beyondstart_line/end_line, and:JModulecarries nosource, so a graph-only consumer cannot recover the body block. It is not a lossy-but-derivable difference; the information is absent.codeby line drifts by the declaration prefix. In our parity run,code_start_linematched on 818 of 1,216 callables and tree-sitter-derived calling lines matched on 1,299 of 1,862 call-graph edges; every mismatch is a multi-line signature or an annotated declaration.Suggested resolution, in preference order: slice the projected
codefrombody_spanso it means what the JSON'scodemeans, or projectbody_span(and ideally the modulesource) so a consumer can slice it themselves. Either makes the two backends agree; the current shape forces every SDK to document a divergence it cannot close.Unrelated to this issue but found in the same pass, in case it is useful:
:JBodyNodeids end in anL:Cthat is a key, not a position — it equals the node'sspan.startcolumn on only 629 of 4,006callnodes, with deltas up to 12 — and the projection carries no column property, so call-site columns cannot be reconstructed from the graph at all. AndLOCKSis in the contract but has zero relationships in a Maven projection, solocked_versionis always null there.