java-codebase-rag 0.9.1__tar.gz → 0.9.2__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (116) hide show
  1. {java_codebase_rag-0.9.1/java_codebase_rag.egg-info → java_codebase_rag-0.9.2}/PKG-INFO +1 -1
  2. {java_codebase_rag-0.9.1 → java_codebase_rag-0.9.2}/java_codebase_rag/jrag.py +20 -4
  3. {java_codebase_rag-0.9.1 → java_codebase_rag-0.9.2/java_codebase_rag.egg-info}/PKG-INFO +1 -1
  4. {java_codebase_rag-0.9.1 → java_codebase_rag-0.9.2}/java_ontology.py +4 -0
  5. {java_codebase_rag-0.9.1 → java_codebase_rag-0.9.2}/pyproject.toml +1 -1
  6. {java_codebase_rag-0.9.1 → java_codebase_rag-0.9.2}/resolve_service.py +24 -0
  7. {java_codebase_rag-0.9.1 → java_codebase_rag-0.9.2}/tests/test_jrag_traversal_direct.py +133 -2
  8. {java_codebase_rag-0.9.1 → java_codebase_rag-0.9.2}/tests/test_mcp_v2.py +29 -0
  9. {java_codebase_rag-0.9.1 → java_codebase_rag-0.9.2}/tests/test_resolve_service.py +47 -0
  10. {java_codebase_rag-0.9.1 → java_codebase_rag-0.9.2}/LICENSE +0 -0
  11. {java_codebase_rag-0.9.1 → java_codebase_rag-0.9.2}/README.md +0 -0
  12. {java_codebase_rag-0.9.1 → java_codebase_rag-0.9.2}/ast_java.py +0 -0
  13. {java_codebase_rag-0.9.1 → java_codebase_rag-0.9.2}/brownfield_events.py +0 -0
  14. {java_codebase_rag-0.9.1 → java_codebase_rag-0.9.2}/build_ast_graph.py +0 -0
  15. {java_codebase_rag-0.9.1 → java_codebase_rag-0.9.2}/chunk_heuristics.py +0 -0
  16. {java_codebase_rag-0.9.1 → java_codebase_rag-0.9.2}/graph_enrich.py +0 -0
  17. {java_codebase_rag-0.9.1 → java_codebase_rag-0.9.2}/graph_types.py +0 -0
  18. {java_codebase_rag-0.9.1 → java_codebase_rag-0.9.2}/index_common.py +0 -0
  19. {java_codebase_rag-0.9.1 → java_codebase_rag-0.9.2}/java_codebase_rag/__init__.py +0 -0
  20. {java_codebase_rag-0.9.1 → java_codebase_rag-0.9.2}/java_codebase_rag/_fdlimit.py +0 -0
  21. {java_codebase_rag-0.9.1 → java_codebase_rag-0.9.2}/java_codebase_rag/_stdio.py +0 -0
  22. {java_codebase_rag-0.9.1 → java_codebase_rag-0.9.2}/java_codebase_rag/cli.py +0 -0
  23. {java_codebase_rag-0.9.1 → java_codebase_rag-0.9.2}/java_codebase_rag/cli_format.py +0 -0
  24. {java_codebase_rag-0.9.1 → java_codebase_rag-0.9.2}/java_codebase_rag/cli_progress.py +0 -0
  25. {java_codebase_rag-0.9.1 → java_codebase_rag-0.9.2}/java_codebase_rag/config.py +0 -0
  26. {java_codebase_rag-0.9.1 → java_codebase_rag-0.9.2}/java_codebase_rag/install_data/agents/explorer-rag-cli.md +0 -0
  27. {java_codebase_rag-0.9.1 → java_codebase_rag-0.9.2}/java_codebase_rag/install_data/agents/explorer-rag-enhanced.md +0 -0
  28. {java_codebase_rag-0.9.1 → java_codebase_rag-0.9.2}/java_codebase_rag/install_data/skills/explore-codebase/SKILL.md +0 -0
  29. {java_codebase_rag-0.9.1 → java_codebase_rag-0.9.2}/java_codebase_rag/install_data/skills/explore-codebase-cli/SKILL.md +0 -0
  30. {java_codebase_rag-0.9.1 → java_codebase_rag-0.9.2}/java_codebase_rag/installer.py +0 -0
  31. {java_codebase_rag-0.9.1 → java_codebase_rag-0.9.2}/java_codebase_rag/jrag_envelope.py +0 -0
  32. {java_codebase_rag-0.9.1 → java_codebase_rag-0.9.2}/java_codebase_rag/jrag_hints.py +0 -0
  33. {java_codebase_rag-0.9.1 → java_codebase_rag-0.9.2}/java_codebase_rag/jrag_render.py +0 -0
  34. {java_codebase_rag-0.9.1 → java_codebase_rag-0.9.2}/java_codebase_rag/lance_optimize.py +0 -0
  35. {java_codebase_rag-0.9.1 → java_codebase_rag-0.9.2}/java_codebase_rag/pipeline.py +0 -0
  36. {java_codebase_rag-0.9.1 → java_codebase_rag-0.9.2}/java_codebase_rag/progress.py +0 -0
  37. {java_codebase_rag-0.9.1 → java_codebase_rag-0.9.2}/java_codebase_rag.egg-info/SOURCES.txt +0 -0
  38. {java_codebase_rag-0.9.1 → java_codebase_rag-0.9.2}/java_codebase_rag.egg-info/dependency_links.txt +0 -0
  39. {java_codebase_rag-0.9.1 → java_codebase_rag-0.9.2}/java_codebase_rag.egg-info/entry_points.txt +0 -0
  40. {java_codebase_rag-0.9.1 → java_codebase_rag-0.9.2}/java_codebase_rag.egg-info/requires.txt +0 -0
  41. {java_codebase_rag-0.9.1 → java_codebase_rag-0.9.2}/java_codebase_rag.egg-info/top_level.txt +0 -0
  42. {java_codebase_rag-0.9.1 → java_codebase_rag-0.9.2}/java_index_flow_lancedb.py +0 -0
  43. {java_codebase_rag-0.9.1 → java_codebase_rag-0.9.2}/java_index_v1_common.py +0 -0
  44. {java_codebase_rag-0.9.1 → java_codebase_rag-0.9.2}/ladybug_queries.py +0 -0
  45. {java_codebase_rag-0.9.1 → java_codebase_rag-0.9.2}/mcp_hints.py +0 -0
  46. {java_codebase_rag-0.9.1 → java_codebase_rag-0.9.2}/mcp_v2.py +0 -0
  47. {java_codebase_rag-0.9.1 → java_codebase_rag-0.9.2}/path_filtering.py +0 -0
  48. {java_codebase_rag-0.9.1 → java_codebase_rag-0.9.2}/pr_analysis.py +0 -0
  49. {java_codebase_rag-0.9.1 → java_codebase_rag-0.9.2}/search_lancedb.py +0 -0
  50. {java_codebase_rag-0.9.1 → java_codebase_rag-0.9.2}/server.py +0 -0
  51. {java_codebase_rag-0.9.1 → java_codebase_rag-0.9.2}/setup.cfg +0 -0
  52. {java_codebase_rag-0.9.1 → java_codebase_rag-0.9.2}/tests/test_agent_skills_static.py +0 -0
  53. {java_codebase_rag-0.9.1 → java_codebase_rag-0.9.2}/tests/test_assign_endpoint_client_extraction.py +0 -0
  54. {java_codebase_rag-0.9.1 → java_codebase_rag-0.9.2}/tests/test_ast_graph_build.py +0 -0
  55. {java_codebase_rag-0.9.1 → java_codebase_rag-0.9.2}/tests/test_ast_java_calls.py +0 -0
  56. {java_codebase_rag-0.9.1 → java_codebase_rag-0.9.2}/tests/test_ast_java_capabilities.py +0 -0
  57. {java_codebase_rag-0.9.1 → java_codebase_rag-0.9.2}/tests/test_ast_java_thread_safety.py +0 -0
  58. {java_codebase_rag-0.9.1 → java_codebase_rag-0.9.2}/tests/test_bank_chat_brownfield_integration.py +0 -0
  59. {java_codebase_rag-0.9.1 → java_codebase_rag-0.9.2}/tests/test_brownfield_clients.py +0 -0
  60. {java_codebase_rag-0.9.1 → java_codebase_rag-0.9.2}/tests/test_brownfield_events.py +0 -0
  61. {java_codebase_rag-0.9.1 → java_codebase_rag-0.9.2}/tests/test_brownfield_overrides.py +0 -0
  62. {java_codebase_rag-0.9.1 → java_codebase_rag-0.9.2}/tests/test_brownfield_routes.py +0 -0
  63. {java_codebase_rag-0.9.1 → java_codebase_rag-0.9.2}/tests/test_call_edge_matching.py +0 -0
  64. {java_codebase_rag-0.9.1 → java_codebase_rag-0.9.2}/tests/test_call_edges_e2e.py +0 -0
  65. {java_codebase_rag-0.9.1 → java_codebase_rag-0.9.2}/tests/test_call_graph_receiver_resolution.py +0 -0
  66. {java_codebase_rag-0.9.1 → java_codebase_rag-0.9.2}/tests/test_call_graph_smoke_roundtrip.py +0 -0
  67. {java_codebase_rag-0.9.1 → java_codebase_rag-0.9.2}/tests/test_call_invariant.py +0 -0
  68. {java_codebase_rag-0.9.1 → java_codebase_rag-0.9.2}/tests/test_cli_progress_stdout_invariant.py +0 -0
  69. {java_codebase_rag-0.9.1 → java_codebase_rag-0.9.2}/tests/test_cli_quiet_parity.py +0 -0
  70. {java_codebase_rag-0.9.1 → java_codebase_rag-0.9.2}/tests/test_client_hint_recovery.py +0 -0
  71. {java_codebase_rag-0.9.1 → java_codebase_rag-0.9.2}/tests/test_client_node_extraction.py +0 -0
  72. {java_codebase_rag-0.9.1 → java_codebase_rag-0.9.2}/tests/test_client_role_rename.py +0 -0
  73. {java_codebase_rag-0.9.1 → java_codebase_rag-0.9.2}/tests/test_config.py +0 -0
  74. {java_codebase_rag-0.9.1 → java_codebase_rag-0.9.2}/tests/test_cross_service_resolution_flag.py +0 -0
  75. {java_codebase_rag-0.9.1 → java_codebase_rag-0.9.2}/tests/test_edge_navigation_doc.py +0 -0
  76. {java_codebase_rag-0.9.1 → java_codebase_rag-0.9.2}/tests/test_fd_limit.py +0 -0
  77. {java_codebase_rag-0.9.1 → java_codebase_rag-0.9.2}/tests/test_feign_not_exposer.py +0 -0
  78. {java_codebase_rag-0.9.1 → java_codebase_rag-0.9.2}/tests/test_graph_enrich.py +0 -0
  79. {java_codebase_rag-0.9.1 → java_codebase_rag-0.9.2}/tests/test_graph_only_boot.py +0 -0
  80. {java_codebase_rag-0.9.1 → java_codebase_rag-0.9.2}/tests/test_incremental_graph.py +0 -0
  81. {java_codebase_rag-0.9.1 → java_codebase_rag-0.9.2}/tests/test_install_data_sync.py +0 -0
  82. {java_codebase_rag-0.9.1 → java_codebase_rag-0.9.2}/tests/test_installer.py +0 -0
  83. {java_codebase_rag-0.9.1 → java_codebase_rag-0.9.2}/tests/test_installer_integration.py +0 -0
  84. {java_codebase_rag-0.9.1 → java_codebase_rag-0.9.2}/tests/test_installer_surface.py +0 -0
  85. {java_codebase_rag-0.9.1 → java_codebase_rag-0.9.2}/tests/test_java_codebase_rag_cli.py +0 -0
  86. {java_codebase_rag-0.9.1 → java_codebase_rag-0.9.2}/tests/test_jrag_auto_scope.py +0 -0
  87. {java_codebase_rag-0.9.1 → java_codebase_rag-0.9.2}/tests/test_jrag_envelope.py +0 -0
  88. {java_codebase_rag-0.9.1 → java_codebase_rag-0.9.2}/tests/test_jrag_listing.py +0 -0
  89. {java_codebase_rag-0.9.1 → java_codebase_rag-0.9.2}/tests/test_jrag_locate.py +0 -0
  90. {java_codebase_rag-0.9.1 → java_codebase_rag-0.9.2}/tests/test_jrag_orientation.py +0 -0
  91. {java_codebase_rag-0.9.1 → java_codebase_rag-0.9.2}/tests/test_jrag_render.py +0 -0
  92. {java_codebase_rag-0.9.1 → java_codebase_rag-0.9.2}/tests/test_jrag_status.py +0 -0
  93. {java_codebase_rag-0.9.1 → java_codebase_rag-0.9.2}/tests/test_jrag_token_budget.py +0 -0
  94. {java_codebase_rag-0.9.1 → java_codebase_rag-0.9.2}/tests/test_jrag_traversal_compose.py +0 -0
  95. {java_codebase_rag-0.9.1 → java_codebase_rag-0.9.2}/tests/test_ladybug_queries.py +0 -0
  96. {java_codebase_rag-0.9.1 → java_codebase_rag-0.9.2}/tests/test_lance_optimize.py +0 -0
  97. {java_codebase_rag-0.9.1 → java_codebase_rag-0.9.2}/tests/test_lancedb_e2e.py +0 -0
  98. {java_codebase_rag-0.9.1 → java_codebase_rag-0.9.2}/tests/test_mcp_hints.py +0 -0
  99. {java_codebase_rag-0.9.1 → java_codebase_rag-0.9.2}/tests/test_mcp_server_project_root.py +0 -0
  100. {java_codebase_rag-0.9.1 → java_codebase_rag-0.9.2}/tests/test_mcp_tools.py +0 -0
  101. {java_codebase_rag-0.9.1 → java_codebase_rag-0.9.2}/tests/test_mcp_v2_compose.py +0 -0
  102. {java_codebase_rag-0.9.1 → java_codebase_rag-0.9.2}/tests/test_meta_chain_core.py +0 -0
  103. {java_codebase_rag-0.9.1 → java_codebase_rag-0.9.2}/tests/test_microservice_scope.py +0 -0
  104. {java_codebase_rag-0.9.1 → java_codebase_rag-0.9.2}/tests/test_outgoing_call_extraction.py +0 -0
  105. {java_codebase_rag-0.9.1 → java_codebase_rag-0.9.2}/tests/test_packaging_metadata.py +0 -0
  106. {java_codebase_rag-0.9.1 → java_codebase_rag-0.9.2}/tests/test_path_filtering.py +0 -0
  107. {java_codebase_rag-0.9.1 → java_codebase_rag-0.9.2}/tests/test_pipeline.py +0 -0
  108. {java_codebase_rag-0.9.1 → java_codebase_rag-0.9.2}/tests/test_pr_analysis.py +0 -0
  109. {java_codebase_rag-0.9.1 → java_codebase_rag-0.9.2}/tests/test_progress.py +0 -0
  110. {java_codebase_rag-0.9.1 → java_codebase_rag-0.9.2}/tests/test_resolve_routes_messaging_layer_c.py +0 -0
  111. {java_codebase_rag-0.9.1 → java_codebase_rag-0.9.2}/tests/test_route_extraction.py +0 -0
  112. {java_codebase_rag-0.9.1 → java_codebase_rag-0.9.2}/tests/test_schema_consistency.py +0 -0
  113. {java_codebase_rag-0.9.1 → java_codebase_rag-0.9.2}/tests/test_search_lancedb.py +0 -0
  114. {java_codebase_rag-0.9.1 → java_codebase_rag-0.9.2}/tests/test_search_lancedb_capability.py +0 -0
  115. {java_codebase_rag-0.9.1 → java_codebase_rag-0.9.2}/tests/test_string_value_atoms.py +0 -0
  116. {java_codebase_rag-0.9.1 → java_codebase_rag-0.9.2}/tests/test_vectors_progress.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: java-codebase-rag
3
- Version: 0.9.1
3
+ Version: 0.9.2
4
4
  Summary: MCP server for semantic + structural search over Java codebases
5
5
  Author: HumanBean17
6
6
  License-Expression: MIT
@@ -812,11 +812,27 @@ def build_parser() -> argparse.ArgumentParser:
812
812
  "CALLS hops from the route handler. Intra-service is an INDEX-TIME data "
813
813
  "property: CALLS edges are intra-codebase by construction, and the query "
814
814
  "carries no microservice predicate, so the result reflects whatever the "
815
- "fixture indexed (no query-time constraint). --max-hops clamped to 1..8."
815
+ "fixture indexed (no query-time constraint). --depth clamped to 1..8."
816
816
  ),
817
817
  )
818
- flow.add_argument("query", help="Route path (e.g. '/chat/assign'). Resolved with hint_kind=route.")
819
- flow.add_argument("--max-hops", type=int, default=5, dest="max_hops", help="Max CALLS hops (clamped 1..8, default 5).")
818
+ flow.add_argument(
819
+ "query",
820
+ help=(
821
+ "Route path (e.g. '/chat/assign') or Kafka topic name (e.g. "
822
+ "'banking.chat.compliance.review'). Resolved with hint_kind=route; "
823
+ "kafka_topic Routes match on topic."
824
+ ),
825
+ )
826
+ # Primary flag is --depth (consistent with callers/callees/impact/decompose).
827
+ # --max-hops is kept as a hidden back-compat alias (same dest).
828
+ flow.add_argument(
829
+ "--depth", type=int, default=5, dest="depth",
830
+ help="Max CALLS hops (clamped 1..8, default 5).",
831
+ )
832
+ flow.add_argument(
833
+ "--max-hops", type=int, dest="depth",
834
+ default=argparse.SUPPRESS, help=argparse.SUPPRESS,
835
+ )
820
836
  flow.set_defaults(handler=_cmd_flow)
821
837
 
822
838
  # ---- Compose traversals + file inspection (PR-JRAG-3b) ----
@@ -3001,7 +3017,7 @@ def _cmd_flow(args: argparse.Namespace) -> int:
3001
3017
  args, reason="trace_request_flow carries no microservice predicate; intra-codebase is an index-time data property"
3002
3018
  )
3003
3019
 
3004
- max_hops = max(1, min(8, getattr(args, "max_hops", 5)))
3020
+ max_hops = max(1, min(8, getattr(args, "depth", 5)))
3005
3021
  flow_data = graph.trace_request_flow(entry_route_id=node.id, max_hops=max_hops)
3006
3022
 
3007
3023
  root_id = node.id
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: java-codebase-rag
3
- Version: 0.9.1
3
+ Version: 0.9.2
4
4
  Summary: MCP server for semantic + structural search over Java codebases
5
5
  Author: HumanBean17
6
6
  License-Expression: MIT
@@ -90,6 +90,8 @@ VALID_RESOLVE_REASONS: frozenset[str] = frozenset((
90
90
  "short_name",
91
91
  "route_template",
92
92
  "route_method_path",
93
+ "route_topic",
94
+ "route_topic_prefix",
93
95
  "client_target",
94
96
  "client_target_path",
95
97
  "client_name",
@@ -428,6 +430,8 @@ ResolveReason = Literal[
428
430
  "short_name",
429
431
  "route_template",
430
432
  "route_method_path",
433
+ "route_topic",
434
+ "route_topic_prefix",
431
435
  "client_target",
432
436
  "client_target_path",
433
437
  "client_name",
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "java-codebase-rag"
7
- version = "0.9.1"
7
+ version = "0.9.2"
8
8
  description = "MCP server for semantic + structural search over Java codebases"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.11"
@@ -43,11 +43,13 @@ _RESOLVE_REASON_PRIORITY: dict[ResolveReason, int] = {
43
43
  "producer_topic_prefix": 1,
44
44
  "fqn_suffix": 2,
45
45
  "route_template": 2,
46
+ "route_topic": 2,
46
47
  "client_fqn": 2,
47
48
  "short_name": 3,
48
49
  "client_target": 3,
49
50
  "client_name": 3,
50
51
  "producer_topic": 3,
52
+ "route_topic_prefix": 3,
51
53
  }
52
54
 
53
55
  _SYMBOL_RESOLVE_RETURN = (
@@ -295,6 +297,28 @@ def _resolve_route_candidates(
295
297
  path_val = str(row.get("path_template") or row.get("path") or "")
296
298
  out.append((_node_ref_from_row("route", row), "route_template", len(path_val)))
297
299
 
300
+ # Kafka/topic routes carry their name in ``topic`` (``path``/``path_template``
301
+ # are empty), so path-based matching above cannot reach them. Match on
302
+ # ``r.topic`` the same way ``_resolve_producer_candidates`` matches
303
+ # ``p.topic`` — this lets ``flow``/``callers``/``overview`` resolve a
304
+ # ``kafka_topic`` Route by topic name. ``_drop_route_mirrors`` below then
305
+ # discards the no-EXPOSES producer phantom in favour of the server route.
306
+ rows = g._rows( # noqa: SLF001
307
+ f"MATCH (r:Route) WHERE r.topic = $topic{scope} RETURN {_ROUTE_RESOLVE_RETURN} LIMIT $lim",
308
+ {"topic": identifier, "lim": lim, **scope_params},
309
+ )
310
+ for row in rows:
311
+ out.append((_node_ref_from_row("route", row), "route_topic", len(identifier)))
312
+
313
+ if not identifier.startswith("/"):
314
+ rows = g._rows( # noqa: SLF001
315
+ f"MATCH (r:Route) WHERE r.topic STARTS WITH $topic{scope} "
316
+ f"RETURN {_ROUTE_RESOLVE_RETURN} LIMIT $lim",
317
+ {"topic": identifier, "lim": lim, **scope_params},
318
+ )
319
+ for row in rows:
320
+ out.append((_node_ref_from_row("route", row), "route_topic_prefix", len(identifier)))
321
+
298
322
  return _drop_route_mirrors(g, out)
299
323
 
300
324
 
@@ -22,8 +22,11 @@ Tests (bank-chat fixture):
22
22
  13. test_impact_service_post_filter_emits_warning
23
23
  14. test_decompose_renders_role_waterfall
24
24
  15. test_flow_outbound_intra_service_on_fixture
25
- 16. test_traversal_resolve_ambiguous_stops
26
- 17. test_traversal_rejects_offset
25
+ 16. test_flow_follows_kafka_topic_on_fixture
26
+ 17. test_flow_depth_flag_and_max_hops_alias
27
+ 18. test_callers_topic_disambiguates_with_kind
28
+ 19. test_traversal_resolve_ambiguous_stops
29
+ 20. test_traversal_rejects_offset
27
30
  """
28
31
  from __future__ import annotations
29
32
 
@@ -620,6 +623,134 @@ def test_flow_outbound_intra_service_on_fixture(
620
623
  )
621
624
 
622
625
 
626
+ def test_flow_follows_kafka_topic_on_fixture(
627
+ corpus_root: Path, ladybug_db_path: Path
628
+ ) -> None:
629
+ """flow resolves a Kafka topic to its Route AND follows it (async edges).
630
+
631
+ Regression: ``_resolve_route_candidates`` matched only on path, so a
632
+ ``kafka_topic`` Route (name in ``topic``, ``path=''``) was unresolvable and
633
+ ``jrag flow <topic>`` returned 'none'. Resolution alone is not enough — this
634
+ test also asserts the follow graph is non-empty and carries an
635
+ ``ASYNC_CALLS`` edge, proving the kafka-specific inbound arm (topic-matched
636
+ Producer) actually fires once the route resolves.
637
+
638
+ ``banking.chat.incoming`` is the canonical dual-sided topic: produced by
639
+ ``FollowUpKafkaPublisher`` (inbound ``ASYNC_CALLS``) and consumed by
640
+ ``ChatKafkaListener`` (outbound ``CALLS`` to ``orchestrationService.handle``).
641
+ """
642
+ env = _env_for(corpus_root, ladybug_db_path)
643
+ proc = _run_jrag(
644
+ ["flow", "banking.chat.incoming", "--format", "json"], env=env
645
+ )
646
+ assert proc.returncode == 0, (
647
+ f"flow on kafka topic failed: rc={proc.returncode}\nstdout={proc.stdout}\nstderr={proc.stderr}"
648
+ )
649
+ payload = json.loads(proc.stdout)
650
+ assert payload["status"] == "ok", f"expected ok, got {payload}"
651
+ assert payload.get("root"), "expected root id (the kafka_topic Route)"
652
+ root_node = payload.get("nodes", {}).get(payload["root"], {})
653
+ assert root_node.get("kind") == "route", f"expected route root, got {root_node}"
654
+ # The follow graph must be non-empty AND carry a kafka async inbound edge —
655
+ # this is the part that proves "flow follows kafka", not just "resolves it".
656
+ edges = payload.get("edges", [])
657
+ assert edges, f"expected non-empty follow graph for a kafka topic, got edges={edges}"
658
+ assert any(e.get("edge_type") == "ASYNC_CALLS" for e in edges), (
659
+ f"expected an ASYNC_CALLS (topic-matched Producer) inbound edge, got {edges}"
660
+ )
661
+
662
+
663
+ def test_flow_depth_flag_and_max_hops_alias(
664
+ corpus_root: Path, ladybug_db_path: Path
665
+ ) -> None:
666
+ """flow uses --depth (consistent with callers/callees/impact/decompose).
667
+
668
+ --max-hops remains as a hidden back-compat alias (same dest). Both must be
669
+ accepted, produce the same traversal, and --depth must actually change the
670
+ traversal depth (more hops => strictly more CALLS edges).
671
+ """
672
+ env = _env_for(corpus_root, ladybug_db_path)
673
+
674
+ # --depth is the primary flag now.
675
+ proc_depth = _run_jrag(
676
+ ["flow", "/chat/assign", "--depth", "2", "--format", "json"], env=env
677
+ )
678
+ assert proc_depth.returncode == 0, (
679
+ f"flow --depth failed: rc={proc_depth.returncode}\nstdout={proc_depth.stdout}"
680
+ )
681
+ payload_depth = json.loads(proc_depth.stdout)
682
+ assert payload_depth["status"] == "ok", f"expected ok, got {payload_depth}"
683
+
684
+ # --max-hops is the hidden alias; must still be accepted (back-compat).
685
+ proc_alias = _run_jrag(
686
+ ["flow", "/chat/assign", "--max-hops", "2", "--format", "json"], env=env
687
+ )
688
+ assert proc_alias.returncode == 0, (
689
+ f"flow --max-hops alias failed: rc={proc_alias.returncode}\nstdout={proc_alias.stdout}"
690
+ )
691
+ payload_alias = json.loads(proc_alias.stdout)
692
+ assert payload_alias["status"] == "ok", f"expected ok, got {payload_alias}"
693
+ # Same dest -> same traversal (depth 2 in both cases).
694
+ assert payload_alias.get("root") == payload_depth.get("root"), (
695
+ "expected identical root id for --depth and --max-hops"
696
+ )
697
+
698
+ # --depth must actually affect the traversal: depth 1 yields strictly fewer
699
+ # outbound CALLS edges than depth 5 on this fixture (~3 vs ~20).
700
+ proc_shallow = _run_jrag(
701
+ ["flow", "/chat/assign", "--depth", "1", "--format", "json"], env=env
702
+ )
703
+ proc_deep = _run_jrag(
704
+ ["flow", "/chat/assign", "--depth", "5", "--format", "json"], env=env
705
+ )
706
+ shallow = json.loads(proc_shallow.stdout)
707
+ deep = json.loads(proc_deep.stdout)
708
+ assert len(shallow.get("edges", [])) < len(deep.get("edges", [])), (
709
+ f"--depth should change traversal size; depth=1 -> {len(shallow.get('edges', []))}, "
710
+ f"depth=5 -> {len(deep.get('edges', []))}"
711
+ )
712
+
713
+
714
+ def test_callers_topic_disambiguates_with_kind(
715
+ corpus_root: Path, ladybug_db_path: Path
716
+ ) -> None:
717
+ """A dual-sided topic resolves to BOTH a Producer and a Route -> ambiguous
718
+ without --kind; --kind route collapses to one and runs find_route_callers.
719
+
720
+ Behavior note (from enabling r.topic resolution): ``callers <topic>`` with no
721
+ --kind now searches all kinds, and a topic with both a Producer and a server
722
+ Route is genuinely ambiguous (previously only the Producer matched, which
723
+ then errored because callers only accepts Symbol/Route roots — jrag.py:2371).
724
+ ``--kind route`` is the disambiguation that yields a working callers run.
725
+ (--kind producer resolves to the Producer but callers rejects a Producer
726
+ root; use the ``producers`` command for the producer view.)
727
+ """
728
+ env = _env_for(corpus_root, ladybug_db_path)
729
+
730
+ # No --kind: Producer + Route both match -> ambiguous (no traversal).
731
+ proc_both = _run_jrag(
732
+ ["callers", "banking.chat.incoming", "--format", "json"], env=env
733
+ )
734
+ payload_both = json.loads(proc_both.stdout)
735
+ assert payload_both["status"] == "ambiguous", (
736
+ f"expected ambiguous for dual-sided topic without --kind, got {payload_both.get('status')}"
737
+ )
738
+ assert len(payload_both.get("candidates", [])) >= 2, (
739
+ f"expected >=2 candidates (producer + route), got {payload_both.get('candidates')}"
740
+ )
741
+
742
+ # --kind route: resolves to the single server Route and runs find_route_callers.
743
+ proc_route = _run_jrag(
744
+ ["callers", "banking.chat.incoming", "--kind", "route", "--format", "json"],
745
+ env=env,
746
+ )
747
+ payload_route = json.loads(proc_route.stdout)
748
+ assert payload_route["status"] == "ok", (
749
+ f"expected --kind route to resolve and run, got {payload_route.get('status')}"
750
+ )
751
+ assert payload_route.get("root"), "expected a resolved root for --kind route"
752
+
753
+
623
754
  # ----- Test 16: traversal resolve-ambiguous stops (no auto-pick) -----
624
755
 
625
756
 
@@ -1364,6 +1364,27 @@ def test_resolve_every_reason_in_closed_set_appears() -> None:
1364
1364
  "end_line": 1,
1365
1365
  "resolved": True,
1366
1366
  }
1367
+ # kafka_topic Route: name lives in ``topic`` (path empty) — exercises the
1368
+ # r.topic resolve branch (route_topic / route_topic_prefix reasons).
1369
+ kafka_route_row = {
1370
+ "id": "route:reasontopic000",
1371
+ "kind": "kafka_topic",
1372
+ "framework": "kafka",
1373
+ "method": "",
1374
+ "path": "",
1375
+ "path_template": "",
1376
+ "path_regex": "",
1377
+ "topic": "reason.created",
1378
+ "broker": "",
1379
+ "feign_name": "",
1380
+ "feign_url": "",
1381
+ "microservice": "svc",
1382
+ "module": "mod",
1383
+ "filename": "K.java",
1384
+ "start_line": 1,
1385
+ "end_line": 1,
1386
+ "resolved": True,
1387
+ }
1367
1388
  client_row = {
1368
1389
  "id": "client:reason",
1369
1390
  "client_kind": "feign_method",
@@ -1415,6 +1436,10 @@ def test_resolve_every_reason_in_closed_set_appears() -> None:
1415
1436
  return [route_row]
1416
1437
  if "r.path = $path OR r.path_template = $path" in query:
1417
1438
  return [route_row]
1439
+ if "r.topic STARTS WITH $topic" in query:
1440
+ return [kafka_route_row]
1441
+ if "WHERE r.topic = $topic" in query:
1442
+ return [kafka_route_row]
1418
1443
  if "WHERE c.id = $id" in query:
1419
1444
  return [client_row]
1420
1445
  if "STARTS WITH $path" in query:
@@ -1443,6 +1468,10 @@ def test_resolve_every_reason_in_closed_set_appears() -> None:
1443
1468
  seen.add(reason)
1444
1469
  for _node, reason, _spec in _resolve_route_candidates(g, "/reason"):
1445
1470
  seen.add(reason)
1471
+ # Topic resolution: exact ``r.topic`` (route_topic) + prefix
1472
+ # ``r.topic STARTS WITH`` (route_topic_prefix, since no leading '/').
1473
+ for _node, reason, _spec in _resolve_route_candidates(g, "reason.created"):
1474
+ seen.add(reason)
1446
1475
  for _node, reason, _spec in _resolve_client_candidates(g, "client:reason"):
1447
1476
  seen.add(reason)
1448
1477
  for _node, reason, _spec in _resolve_client_candidates(g, "reasonsvc"):
@@ -146,6 +146,53 @@ def test_resolve_service_producer_topic_prefix(ladybug_db_path: Path) -> None:
146
146
  assert result.message is not None
147
147
 
148
148
 
149
+ def test_resolve_service_route_kafka_topic(ladybug_db_path: Path) -> None:
150
+ """A Kafka topic name resolves to its Route via ``r.topic``.
151
+
152
+ Regression: ``_resolve_route_candidates`` matched only on
153
+ ``path``/``path_template``, so ``kafka_topic`` Routes (which carry their
154
+ name in ``topic`` with ``path=''``) were unresolvable — ``jrag flow
155
+ <topic>``/``callers <topic>``/``overview <topic>`` could not follow kafka
156
+ even when the route + EXPOSES edge existed. ``banking.chat.compliance.review``
157
+ is consumed by ``ComplianceReviewListener`` (consumer-only, no producer
158
+ phantom), so it resolves to exactly one Route.
159
+ """
160
+ g = LadybugGraph.get(str(ladybug_db_path))
161
+ result = resolve_v2("banking.chat.compliance.review", hint_kind="route", graph=g)
162
+
163
+ assert isinstance(result, ResolveOutput)
164
+ assert result.success is True
165
+ assert result.resolved_identifier == "banking.chat.compliance.review"
166
+ assert result.status == "one", (
167
+ f"topic should resolve to one Route, got status={result.status!r}: {result.message}"
168
+ )
169
+ assert result.node is not None
170
+ assert result.node.kind == "route", f"expected a Route node, got {result.node}"
171
+
172
+
173
+ def test_resolve_service_route_kafka_topic_drops_producer_mirror(ladybug_db_path: Path) -> None:
174
+ """A topic with both a producer (phantom, no EXPOSES) and a consumer
175
+ (server Route, EXPOSES) resolves to the single server Route.
176
+
177
+ ``banking.chat.incoming`` is produced by ``FollowUpKafkaPublisher`` (phantom
178
+ kafka_topic Route, ``microservice=''``, no EXPOSES) and consumed by
179
+ ``ChatKafkaListener`` (server Route with EXPOSES). Both carry the same topic
180
+ so topic-matching surfaces both; ``_drop_route_mirrors`` must collapse them
181
+ to the one exposed server route (status ``one``), not report ``many``.
182
+ """
183
+ g = LadybugGraph.get(str(ladybug_db_path))
184
+ result = resolve_v2("banking.chat.incoming", hint_kind="route", graph=g)
185
+
186
+ assert isinstance(result, ResolveOutput)
187
+ assert result.success is True
188
+ assert result.status == "one", (
189
+ f"producer+consumer topic should collapse to one server Route via mirror-drop, "
190
+ f"got status={result.status!r}: {result.message}"
191
+ )
192
+ assert result.node is not None
193
+ assert result.node.kind == "route", f"expected a Route node, got {result.node}"
194
+
195
+
149
196
  def test_resolve_service_hint_kind_filters(ladybug_db_path: Path) -> None:
150
197
  """hint_kind narrows the search space (route hint won't match symbol-only ids)."""
151
198
  g = LadybugGraph.get(str(ladybug_db_path))