Repository navigation
Description
Problem
Second half of the JSON-vs-graph parity sweep. #202 covers the module source and the flattened span; this issue covers the fields the sweep found after those two, all verified by reading codeanalyzer/schema/py_schema.py against codeanalyzer/neo4j/schema.py and codeanalyzer/neo4j/project.py. codeanalyzer-java had the same class of gap and filed it as codellm-devkit/codeanalyzer-java#256 and #257.
Comments. PyComment is {content, is_docstring, start_line, end_line, start_column, end_column}, and analysis.json attaches lists of them to modules, classes, callables and attributes. The graph keeps one docstring string per class, callable and attribute, joined from the entries whose is_docstring is true (project.py:783,806,825,854). Three facts go: module-level comments entirely (:PyModule has no comment property), every non-docstring comment, and every comment's position — so no comment can be located in the file or sliced out of the module source that #202 adds. python-sdk documents the consequence: it raises from get_all_comments rather than answer with a smaller set claiming to be every comment. Won't fix — comments are out of scope for this issue and no follow-up is tracked. Comments are ignored across the analyzers: codeanalyzer-java closed the same gap as wontfix (codellm-devkit/codeanalyzer-java#257), so docstring is where the comment model stops here too. python-sdk raising from get_all_comments on the Neo4j backend is the permanent answer for that backend, not a workaround waiting on this issue; consumers that need comment text or positions read analysis.json or use the in-memory backend.
Decorator positions. PyDecorator carries a span; :PyDecorator carries {name, qualified_name} and PY_DECORATED_BY carries expression, positional_arguments and keyword_arguments_json — everything except where the decorator is written.
Call-site detail. PyCallsite carries callee_signature and argument_types; :PyBodyNode carries method_name, receiver_expr, receiver_type, return_type, is_constructor_call and arguments_json, and neither of those two. callee_signature is what distinguishes overload targets at a resolved call site.
Variable values. PyVariableDeclaration carries both initializer (the source text) and value (the literal-evaluated result); :PyVariable carries only initializer.
Import positions. PyImport carries a line/column range; PY_IMPORTS carries spellings, imported_names and aliases and no positions. Lowest-value item on the list, included for completeness rather than urgency.
The invariant being restored: the JSON payload and the graph carry the same facts. Re-expressing a JSON field as an edge or a flattened property is fine — arguments_json and keyword_arguments_json are good examples — but silently dropping one is not.
Scope boundary
This analyzer's Neo4j projection. Does NOT touch the JSON projection, which is already correct. Does NOT drop PyCallable.code or PyClass.code, which become derivable once #202 lands but which python-sdk reads. Does NOT move schema_version, held at 2.0.0 per codellm-devkit/.github#50. Depends on #202 for the byte offsets that make any of the position work sliceable.
Goals
module-level comments are reachable from— won't fix, see above:PyModulenon-docstring comments are carried, distinguishable from docstrings— won't fixeach comment's position is carried, so it can be sliced out of the module source— won't fix-
docstringis retained on every node that has it today, unchanged - the decorator's position is carried
-
:PyBodyNodecarriescallee_signatureandargument_types -
:PyVariablecarriesvalue -
PY_IMPORTScarries the import's position - the checked-in graph schema snapshot is regenerated, and the conformance test asserts each field against the JSON value per node rather than by presence
Caveats
-
The comment decision is made, and it is "no". Graph collapses every comment into one docstring string: file-level comments, non-javadoc comments, comment spans and body-node comments are all dropped codeanalyzer-java#257 is closed as
wontfix: comments are globally ignored, so no analyzer projects them and none of the three gets to move first. This analyzer keeps thedocstringderivation it has, which is more than codeanalyzer-typescript projects (nothing) and the same as codeanalyzer-java. TheJSON payload == graphinvariant below therefore holds for every field except comments, and stays broken for those by decision rather than by omission. -
valueisOptional[Any]. Neo4j properties are scalars and arrays of scalars, so a dict or list value needs a serialization decision (thearguments_jsonprecedent) rather than a raw put. -
schema_versiondoes not move, so none of these additions is detectable from the payload. Detection is by presence. -
argumentshere is not codeanalyzer-java'sarguments— do not align them by renaming.PyCallArgumentis a structured argument record (ast_kind,inferred_type,value,name), andarguments_jsonis its legitimate re-expression, so there is nothing to close on this side. codeanalyzer-java's:JBodyNode.argumentsis a different fact: one body-localline:colid per argument, positionally aligned withargument_exprandargument_types. The term is already coined twice across the analyzers; recorded here so neither meaning is later "fixed" into the other. If this analyzer ever records per-argument ids,argumentsis the spelling only for body-local ids, and the structured list needs a different name. -
Never coin
argument_nodes, and never owner-qualify a per-argument id. Graph drops body_span, type_parameters, enum/record spans and decorators, call-site argument ids, callee_signature and annotation spans codeanalyzer-java#256 specified anargument_nodesproperty holding<callable-id>@<local>ordinals so a consumer could hop straight to the:JBodyNodefor each argument. It was implemented and it was wrong: an argument expression is a body node only when it is itself a call site (f(g(x))). Measured on daytrader8, 537 of 3666 argument ids named a real body-node key, so the property would have published 3129 ids resolving to nothing. It was dropped before merge and the canonical local ids ship verbatim. A consumer that wants the node for a nested call builds the global id the way the projection does: owner id,@, local id.
Definition of done
- Each carried decorator and import position slices the module source to that node's own text — the check that the position survived rather than merely being declared.
- Existing
docstringvalues are unchanged on every node that had one. - Every other field above is asserted equal to its JSON value per node, not by presence.
- Emitted schema matches the checked-in snapshot; existing suite green.
Activity
rahlk commentedon Sep 10, 2026
Correction from codeanalyzer-java#256:
argument_nodeswas wrong, andargumentsmeans something else herecodeanalyzer-java#256 asked for an
argument_nodesproperty on:JBodyNode: the per-argument id list, mapped from body-localline:colkeys to global<callable-id>@<local>ordinals so a consumer could hop straight to the argument's body node. It was implemented, the conformance test refused it, and the requirement was wrong rather than the test.An argument expression is a body node only when it is itself a call site —
f(g(x))makesg(x)a node,f(x)makesxnothing. Java builds body nodes from method calls, constructor calls and explicit constructor invocations only. Measured on daytrader8: 537 of 3666 argument ids name a real body-node key. The property would have published 3129 ids that resolve to nothing, with a name promising they resolve.What shipped instead: the canonical local ids verbatim, as
arguments, positionally aligned withargument_exprandargument_types, with the comment stating that they are deliberately not owner-qualified. A consumer that wants the node for a nested call builds the global id itself — owner id,@, local id.What this means for this repo. Nothing to implement, but two things to not do, now recorded in the caveats:
argumentshere is a different fact fromargumentsthere.PyCallArgumentis a structured record (ast_kind,inferred_type,value,name) andarguments_jsonis a legitimate re-expression of it — no drop, no gap. Java'sargumentsis a list of body-local ids. The term is coined twice across the analyzers; the point of writing that down is that neither side gets "aligned" into the other later. If this analyzer ever records per-argument ids,argumentsis the spelling for those, and the structured list needs a new name at that time.argument_nodes, and if per-argument ids are ever carried, keep them body-local. Owner-qualifying them, or documenting them as node references, reproduces the defect Java caught before merge.rahlk commentedon Sep 10, 2026
Design landed. Spec:
docs/design/specs/2026-09-10-neo4j-json-parity.md(branchfix/issue-202-203-graph-json-parity). One PR closes this issue and its sibling — same three files, one snapshot regeneration, one pass over the conformance test.Correction to this issue's text. It cites codellm-devkit/.github#50 for "
schema_versionstays at the held2.0.0baseline". #50 is "scope every destructive Neo4j statement on thecan://id prefix; retire_module", and it mandates the opposite — "MAJOR graph-contract bump: a property is removed" — for that work, whose python leg is #173 (closed). The hold is real, but its source is the standing 2026-09-07 ruling: payloadschema_versionand graphSCHEMA_VERSIONboth stay2.0.0until the 2.0.0 line leaves release-candidate, across all three analyzers. Consumers gate on the analyzer-version floor, which moves with 1.5.2.Because the version holds while properties are added,
neo4j/schema.py:27's promise of "MINOR on an additive change" becomes false. The same PR amends that note to record the hold — a file should not document a rule it breaks.Two goals change.
Comments — dropped, not deferred. codellm-devkit/codeanalyzer-java#257 closed
NOT_PLANNEDtoday: comments are globally ignored across the analyzers, the ceiling is accepted rather than deferred, anddocstringon the graph is where the comment model stops. Its closing comment names this issue as dropping its comment goals for the same reason.python-sdk'sget_all_comments/get_comment_in_fileraising on the Neo4j backend is therefore the permanent answer for that backend, not a workaround awaiting this work; the in-memory backend remains the way to reach comments. Every other goal here stands. Reopen the comment question only if a consumer turns up that needs comment text or positions out of the graph.argument_types— satisfied by its successor, not carried.schema/py_schema.py:311records it as the legacy field that "mixed these two vocabularies in one list", split by #86 intoPyCallArgument{ast_kind, inferred_type}— which the graph already carries asarguments_jsonon:PyBodyNode. Carryingargument_typesas well would store one fact twice, one copy in a vocabulary this repo deliberately retired.callee_signatureis carried, by a projection-time join fromPyCallable.call_siteson(start_line, start_column), asserted per call site rather than by presence. Traversal viaPY_RESOLVES_TOwas rejected: 20-28% of call sites never resolve a callee, which is exactly where the field is the only source of the fact.fix(neo4j): the graph projection carries the facts analysis.json carries