Summary
Spring MVC routing annotations (@RequestMapping, @GetMapping, @PostMapping, …) on Kotlin controllers are silently not turned into routes. The exact same annotations on Java controllers work. As a result a Kotlin Spring backend gets 0 real HTTP routes in the graph, and cross-repo-intelligence produces 0 CROSS_HTTP_CALLS edges into that service even when the calling side has perfectly clean url_path values — because there is nothing to match against on the callee side.
Environment
- Repo:
DeusData/codebase-memory-mcp, main (cloned 2026-06).
- Reproduced against a real Kotlin + Spring Boot codebase and confirmed by reading
internal/cbm/.
- Mode:
full index, then cross-repo-intelligence.
Reproduction
Minimal Kotlin controller (textbook Spring, annotations on the class + methods):
@RestController
@RequestMapping("/api/v1/connectors")
class ConnectorController(private val service: ConnectorService) {
@PostMapping
fun createConnector(@RequestBody req: ConnectorCreateRequestDto): ResponseEntity<Results> = ...
@GetMapping("/{id}")
fun getConnectorById(@PathVariable id: Long): ResponseEntity<Results> = ...
}
Index the project (full), then query the graph.
Expected
ConnectorController methods produce Route entries, e.g. POST /api/v1/connectors, GET /api/v1/connectors/{id}.
Actual
- The
ConnectorController class node exists, but every method has empty route_method / route_path.
- The method's
decorators property captures custom annotations (e.g. @RequiresIntegratedPermission) but not @PostMapping / @GetMapping.
- Project-wide: 0 routes with
source = "decorator" (only config-file URLs are present, as source = "infra").
- For contrast, an equivalent Java Spring service in the same workspace yields dozens of
decorator-source routes.
- Downstream:
cross-repo-intelligence returns cross_http_calls: 0 for callers that hit this service, even though those callers' HTTP_CALLS edges have correct url_path (e.g. /api/v1/connectors/{id}).
Root cause
internal/cbm/extract_defs.c, try_route_from_annotation() (≈ line 1260):
static bool try_route_from_annotation(CBMArena *a, TSNode annotation, const char *source,
const char **out_path, const char **out_method) {
TSNode name_node = ts_node_child_by_field_name(annotation, TS_FIELD("name"));
if (ts_node_is_null(name_node)) {
return false; // <-- Kotlin always bails out here
}
...
}
The function reads the annotation name via the name field. This is a Java-grammar assumption:
- tree-sitter-java —
marker_annotation / annotation nodes expose a name: field, so the lookup succeeds.
(lang_specs.c: java_decorator_types[] = {"marker_annotation", "annotation", NULL})
- tree-sitter-kotlin — the
annotation node has no name field. The name is nested:
@PostMapping → (annotation (user_type (type_identifier)))
@RequestMapping("/x") → (annotation (constructor_invocation (user_type (type_identifier)) (value_arguments (value_argument (string_literal)))))
(lang_specs.c: kotlin_decorator_types[] = {"annotation", NULL})
So for every Kotlin annotation ts_node_child_by_field_name(annotation, "name") returns null and the function returns false before annotation_route_method() is ever consulted. No Kotlin Spring route is ever emitted.
Note: find_jvm_modifiers() already routes CBM_LANG_KOTLIN to the "modifiers" wrapper correctly, and extract_route_from_annotations() iterates it — so the scaffolding is in place. The break is solely the name-field assumption inside try_route_from_annotation.
Proposed fix (focused)
In try_route_from_annotation, when the name field is absent, fall back to a nested-identifier lookup so it works for the Kotlin annotation shape:
- If
name field is null, descend into the annotation node to the first user_type / constructor_invocation → type_identifier (or simple_identifier) and use that as the annotation name.
- For arguments: in addition to the
arguments field / argument_list child, recognize Kotlin's value_arguments node (and unwrap the value_argument children) in find_decorator_args().
- In
extract_route_path_from_args(), accept Kotlin's string node kind (string_literal) and descend through the value_argument wrapper.
This keeps the change contained to the three route-extraction helpers; no API/pipeline changes.
Related (separate PRs, not in scope here)
- Class-level base path is not merged with method-level paths.
extract_route_from_decorators operates on the method node only; a class-level @RequestMapping("/api/v1/connectors") is never prepended to method paths. This affects Java too (methods with bare @PostMapping get path /), but is most visible in Kotlin controllers that put the base path on the class. Happy to file separately.
Repro / test
Suggest adding a Kotlin Spring case to tests/test_httplink.c mirroring the existing Java route-extraction test, asserting POST /api/v1/connectors and GET /api/v1/connectors/{id} are emitted.
Happy to submit a PR with the fix + test (DCO-signed, C-only) if this direction looks right.
Summary
Spring MVC routing annotations (
@RequestMapping,@GetMapping,@PostMapping, …) on Kotlin controllers are silently not turned into routes. The exact same annotations on Java controllers work. As a result a Kotlin Spring backend gets 0 real HTTP routes in the graph, andcross-repo-intelligenceproduces 0CROSS_HTTP_CALLSedges into that service even when the calling side has perfectly cleanurl_pathvalues — because there is nothing to match against on the callee side.Environment
DeusData/codebase-memory-mcp,main(cloned 2026-06).internal/cbm/.fullindex, thencross-repo-intelligence.Reproduction
Minimal Kotlin controller (textbook Spring, annotations on the class + methods):
Index the project (
full), then query the graph.Expected
ConnectorControllermethods produce Route entries, e.g.POST /api/v1/connectors,GET /api/v1/connectors/{id}.Actual
ConnectorControllerclass node exists, but every method has emptyroute_method/route_path.decoratorsproperty captures custom annotations (e.g.@RequiresIntegratedPermission) but not@PostMapping/@GetMapping.source = "decorator"(only config-file URLs are present, assource = "infra").decorator-source routes.cross-repo-intelligencereturnscross_http_calls: 0for callers that hit this service, even though those callers'HTTP_CALLSedges have correcturl_path(e.g./api/v1/connectors/{id}).Root cause
internal/cbm/extract_defs.c,try_route_from_annotation()(≈ line 1260):The function reads the annotation name via the
namefield. This is a Java-grammar assumption:marker_annotation/annotationnodes expose aname:field, so the lookup succeeds.(
lang_specs.c:java_decorator_types[] = {"marker_annotation", "annotation", NULL})annotationnode has nonamefield. The name is nested:@PostMapping→(annotation (user_type (type_identifier)))@RequestMapping("/x")→(annotation (constructor_invocation (user_type (type_identifier)) (value_arguments (value_argument (string_literal)))))(
lang_specs.c:kotlin_decorator_types[] = {"annotation", NULL})So for every Kotlin annotation
ts_node_child_by_field_name(annotation, "name")returns null and the function returnsfalsebeforeannotation_route_method()is ever consulted. No Kotlin Spring route is ever emitted.Note:
find_jvm_modifiers()already routesCBM_LANG_KOTLINto the"modifiers"wrapper correctly, andextract_route_from_annotations()iterates it — so the scaffolding is in place. The break is solely the name-field assumption insidetry_route_from_annotation.Proposed fix (focused)
In
try_route_from_annotation, when thenamefield is absent, fall back to a nested-identifier lookup so it works for the Kotlinannotationshape:namefield is null, descend into the annotation node to the firstuser_type/constructor_invocation→type_identifier(orsimple_identifier) and use that as the annotation name.argumentsfield /argument_listchild, recognize Kotlin'svalue_argumentsnode (and unwrap thevalue_argumentchildren) infind_decorator_args().extract_route_path_from_args(), accept Kotlin's string node kind (string_literal) and descend through thevalue_argumentwrapper.This keeps the change contained to the three route-extraction helpers; no API/pipeline changes.
Related (separate PRs, not in scope here)
extract_route_from_decoratorsoperates on the method node only; a class-level@RequestMapping("/api/v1/connectors")is never prepended to method paths. This affects Java too (methods with bare@PostMappingget path/), but is most visible in Kotlin controllers that put the base path on the class. Happy to file separately.Repro / test
Suggest adding a Kotlin Spring case to
tests/test_httplink.cmirroring the existing Java route-extraction test, assertingPOST /api/v1/connectorsandGET /api/v1/connectors/{id}are emitted.Happy to submit a PR with the fix + test (DCO-signed, C-only) if this direction looks right.