Skip to content

feat(extract): capture annotation arguments and add a Kotlin annotation collector - #3

Open
fredagenttasks[bot] wants to merge 2 commits into
v8from
fred-implement-part-1-fred-long-term-2fa7476d
Open

feat(extract): capture annotation arguments and add a Kotlin annotation collector#3
fredagenttasks[bot] wants to merge 2 commits into
v8from
fred-implement-part-1-fred-long-term-2fa7476d

Conversation

@fredagenttasks

@fredagenttasks fredagenttasks Bot commented Jul 30, 2026

Copy link
Copy Markdown

Part 1 of the Fred Long-Term Knowledge Architecture Proposal needs endpoint nodes, and an endpoint is a route path. graphify collected annotation names and threw the arguments away, so @GetMapping("/api/orders/{id}") was indistinguishable from a bare marker. Kotlin was worse: it had no annotation collector at all.

Targets v8 (this fork's default branch), not main.

What changed

1. Annotation arguments are captured (Java). _java_annotation_names (engine.py:551-574) read only child_by_field_name("name"). It is now a thin wrapper over a new _java_annotations, which also reads the annotation_argument_list. Both existing attribute-edge callers (engine.py:2753, :3341) are unchanged.

2. Kotlin gains an annotation collector. _kotlin_extra_walk handled only enum_entry. The new collector reads both shapes the grammar produces — and the second one is the important one:

# class WITH a primary constructor -> annotations are a preceding SIBLING
annotated_expression  '@RestController\n@RequestMapping("/a")'
class_declaration     'class G(private val s: Svc) { ... }'   <- no `modifiers` child

# class WITHOUT one -> annotations are in `modifiers`
class_declaration     '@RestController\nclass H { ... }'
  modifiers
    annotation        '@RestController'

Spring constructor injection hits the first case, so a naive port of the Java collector returns nothing for essentially every real controller. A guard (_kotlin_is_annotation_chain) requires the sibling to hold only annotations, so an unrelated annotated expression that merely precedes the class isn't misattributed to it.

Kotlin also now emits the references / context="attribute" edges Java already did.

3. Kotlin const val values. @Table(TableNames.REGISTRATION) is a reference, and the graph held no value for it — TableNames was a node, its members were not. const val NAME = <literal> properties now emit nodes carrying metadata.const_value.

4. Kotlin member calls now resolve by receiver type, closing Graphify-Labs#1965. Ports upstream PR Graphify-Labs/graphify#1965 (oleksii-tumanov/graphify#fix/kotlin-typed-receiver-calls) into this fork's extraction/type-resolution layer:

  • method-scoped Kotlin receiver types are collected per-file from class properties, constructor parameters, getter returns, and local constructor/getter bindings, threaded through walk_calls alongside the existing extra_locals (False indirect_call edges (INFERRED 0.8): call arguments bind as callees to an unrelated single-letter test helper across files Graphify-Labs/graphify#2241) plumbing
  • a new corpus-level kotlin_member_calls resolver (_resolve_kotlin_member_calls) resolves a member call (a.b()) only when the receiver type and the selected method's owner are unambiguous, guarded by package/import scope, inheritance, extension-function dispatch, companion binding, and lexical shadowing
  • existing dispatch for super, object/companion-qualified calls, private visibility, and inner-class dispatch is preserved

This closes the gap called out below: a Kotlin Spring Controller -> Service -> Repository chain (constructor injection) now produces resolved calls edges end-to-end, verified by test_controller_service_repository_chain_resolves_end_to_end.

Emitted shape

{
  "label": "RegistrationController",
  "metadata": {
    "annotations": [
      {"name": "RestController", "args": []},
      {"name": "RequestMapping",
       "args": [{"name": "value", "value": "/api/registration", "kind": "string"}]}
    ]
  }
}

A single positional argument is keyed value, matching the Java/Spring shorthand that @GetMapping("/x") means value = "/x". A symbolic argument is tagged kind: "reference" rather than silently reduced. sanitize_metadata already round-trips nested lists, so no export change was needed.

Measured

On a 26-file Kotlin corpus mirroring bizzabo/registration's annotation census, 58 / 58 annotations and 4 / 4 constants are captured:

{'GetMapping': 12, 'RequestMapping': 9, 'Service': 9, 'PostMapping': 8,
 'Component': 6, 'RestController': 5, 'Repository': 4, 'Table': 4, 'Controller': 1}
const nodes: 4

Before this change, Kotlin emitted zero annotation data of any kind.

A Kotlin Controller -> Service -> Repository constructor-injection chain now yields 2/2 resolved calls edges (controller -> service, service -> repository) instead of the previous 0.

Tests

tests/test_kotlin_annotations.py (12 cases across both grammar shapes, array args, @Table(CONST), and the negative case where an unrelated annotated expression precedes a class), tests/test_java_annotation_args.py (positional, named-pair, field_access, marker, plus a regression test that the existing attribute edges are unchanged), and tests/test_kotlin_member_calls.py (43 cases: the four typed-receiver shapes from Graphify-Labs#1699, ambiguity/shadowing guards, extension/companion/inheritance dispatch, cross-file package/import resolution, portable-AST-cache stability, and the new controller/service/repository chain).

Full suite: 3686 passed. The remaining failures are pre-existing and environmental (tree_sitter_hcl/openai/boto3 not installed, AWS_PROFILE-driven ollama backend detection, wheel-payload) — verified identical on a clean checkout of this branch's prior commit.

Previously known gap — now closed

Kotlin member calls still don't resolve, so the controller -> service -> repository chain does not close on v8: the same corpus yields 0 calls edges. That is upstream Graphify-Labs/graphify#1965, still open. This PR is orthogonal — it is what makes the endpoint and table nodes extractable.

Closed by porting Graphify-Labs/graphify#1965 in the latest commit (see item 4 above).

Requested by:


Task Details:

This PR was created automatically by Fred Agent.

…on collector

Annotation collection kept only the name, so `@GetMapping("/orders/{id}")`
was indistinguishable from a bare marker and the route path — the thing that
makes the declaration an HTTP endpoint — never reached the graph. The
collected annotations are now attached to the declaration's node as
`metadata.annotations`, each with its arguments: a single positional argument
is keyed `value` to match the Java/Spring shorthand, and a symbolic argument
(`@Table(TableNames.ORDERS)`) is tagged `kind: "reference"` instead of being
dropped.

Kotlin had no annotation collector at all — `_kotlin_extra_walk` handled only
`enum_entry` — so a Kotlin Spring service yielded no annotation data of any
kind. It reads both shapes the grammar produces: the declaration's `modifiers`
child, and the preceding `annotated_expression` sibling chain that a class
declaring a primary constructor gets instead, which is the shape almost every
constructor-injected bean has. Kotlin also now emits the
`references`/`context="attribute"` edges Java already did.

Kotlin `const val NAME = <literal>` properties become nodes carrying
`metadata.const_value`, so a constant referenced from an annotation argument
can be resolved to the value it holds.

`_java_annotation_names` is preserved as a wrapper over the new
`_java_annotations`, leaving its two existing attribute-edge callers unchanged.

Co-authored-by: fred-agent <fred-agent@bizzabo.com>
@fredagenttasks
fredagenttasks Bot marked this pull request as ready for review July 30, 2026 09:25
@fredagenttasks
fredagenttasks Bot marked this pull request as draft July 30, 2026 10:55
@fredagenttasks
fredagenttasks Bot marked this pull request as ready for review July 30, 2026 12:32
@fredagenttasks
fredagenttasks Bot marked this pull request as draft July 31, 2026 09:57
…aphify-Labs#1965)

Ports oleksii-tumanov's upstream fix (Graphify-Labs#1965) into this
fork's extraction/type-resolution layer, extending the annotation-argument
capture and Kotlin annotation collector added in this branch.

- collect method-scoped Kotlin receiver types from class properties,
  constructor parameters, getter returns, and local constructor/getter
  bindings, threaded through walk_calls alongside the existing Graphify-Labs#2241
  extra_locals plumbing
- register a corpus-level kotlin_member_calls resolver
  (_resolve_kotlin_member_calls) that resolves a member call only when the
  receiver type and the selected method's owner are unambiguous, guarded by
  package/import scope, inheritance, extension-function dispatch, companion
  binding, and lexical shadowing
- preserve existing Kotlin dispatch for super, object/companion-qualified
  calls, private visibility, and inner-class dispatch receivers

This closes the receiver-typing gap tracked by Graphify-Labs#1965:
a Kotlin Spring Controller -> Service -> Repository chain (constructor
injection) now produces resolved calls edges end-to-end instead of 0, per
the new test_controller_service_repository_chain_resolves_end_to_end.

Co-authored-by: fred-agent <fred-agent@bizzabo.com>
@fredagenttasks
fredagenttasks Bot marked this pull request as ready for review July 31, 2026 10:09
@fredagenttasks
fredagenttasks Bot marked this pull request as draft July 31, 2026 10:35
@fredagenttasks
fredagenttasks Bot marked this pull request as ready for review July 31, 2026 12:20
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant