Skip to content

Graph drops comments, decorator and import positions, call-site callee_signature and argument_types, and variable values #203

Closed

Description

@rahlk

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 :PyModule — won't fix, see above
  • non-docstring comments are carried, distinguishable from docstrings — won't fix
  • each comment's position is carried, so it can be sliced out of the module source — won't fix
  • docstring is retained on every node that has it today, unchanged
  • the decorator's position is carried
  • :PyBodyNode carries callee_signature and argument_types
  • :PyVariable carries value
  • PY_IMPORTS carries 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 the docstring derivation it has, which is more than codeanalyzer-typescript projects (nothing) and the same as codeanalyzer-java. The JSON payload == graph invariant below therefore holds for every field except comments, and stays broken for those by decision rather than by omission.

  • value is Optional[Any]. Neo4j properties are scalars and arrays of scalars, so a dict or list value needs a serialization decision (the arguments_json precedent) rather than a raw put.

  • schema_version does not move, so none of these additions is detectable from the payload. Detection is by presence.

  • arguments here is not codeanalyzer-java's arguments — do not align them by renaming. PyCallArgument is a structured argument record (ast_kind, inferred_type, value, name), and arguments_json is its legitimate re-expression, so there is nothing to close on this side. codeanalyzer-java's :JBodyNode.arguments is a different fact: one body-local line:col id per argument, positionally aligned with argument_expr and argument_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, arguments is 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 an argument_nodes property holding <callable-id>@<local> ordinals so a consumer could hop straight to the :JBodyNode for 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 docstring values 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

  1. rahlk commented on Sep 10, 2026

    @rahlk
    ContributorAuthor

    Correction from codeanalyzer-java#256: argument_nodes was wrong, and arguments means something else here

    codeanalyzer-java#256 asked for an argument_nodes property on :JBodyNode: the per-argument id list, mapped from body-local line:col keys 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)) makes g(x) a node, f(x) makes x nothing. 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 with argument_expr and argument_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:

    1. arguments here is a different fact from arguments there. PyCallArgument is a structured record (ast_kind, inferred_type, value, name) and arguments_json is a legitimate re-expression of it — no drop, no gap. Java's arguments is 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, arguments is the spelling for those, and the structured list needs a new name at that time.
    2. Never coin 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.
  2. rahlk commented on Sep 10, 2026

    @rahlk
    ContributorAuthor

    Design landed. Spec: docs/design/specs/2026-09-10-neo4j-json-parity.md (branch fix/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_version stays at the held 2.0.0 baseline". #50 is "scope every destructive Neo4j statement on the can:// 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: payload schema_version and graph SCHEMA_VERSION both stay 2.0.0 until 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_PLANNED today: comments are globally ignored across the analyzers, the ceiling is accepted rather than deferred, and docstring on 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's get_all_comments / get_comment_in_file raising 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:311 records it as the legacy field that "mixed these two vocabularies in one list", split by #86 into PyCallArgument{ast_kind, inferred_type} — which the graph already carries as arguments_json on :PyBodyNode. Carrying argument_types as well would store one fact twice, one copy in a vocabulary this repo deliberately retired.

    callee_signature is carried, by a projection-time join from PyCallable.call_sites on (start_line, start_column), asserted per call site rather than by presence. Traversal via PY_RESOLVES_TO was rejected: 20-28% of call sites never resolve a callee, which is exactly where the field is the only source of the fact.

  3. added a commit that references this issue on Sep 10, 2026
    f58d4b5
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions