java-codebase-rag 0.9.0__tar.gz → 0.9.1__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.0/java_codebase_rag.egg-info → java_codebase_rag-0.9.1}/PKG-INFO +1 -1
  2. {java_codebase_rag-0.9.0 → java_codebase_rag-0.9.1}/java_codebase_rag/config.py +27 -3
  3. {java_codebase_rag-0.9.0 → java_codebase_rag-0.9.1}/java_codebase_rag/lance_optimize.py +25 -0
  4. {java_codebase_rag-0.9.0 → java_codebase_rag-0.9.1}/java_codebase_rag/pipeline.py +20 -0
  5. {java_codebase_rag-0.9.0 → java_codebase_rag-0.9.1/java_codebase_rag.egg-info}/PKG-INFO +1 -1
  6. {java_codebase_rag-0.9.0 → java_codebase_rag-0.9.1}/java_codebase_rag.egg-info/SOURCES.txt +1 -0
  7. {java_codebase_rag-0.9.0 → java_codebase_rag-0.9.1}/java_index_flow_lancedb.py +66 -6
  8. {java_codebase_rag-0.9.0 → java_codebase_rag-0.9.1}/pyproject.toml +1 -1
  9. {java_codebase_rag-0.9.0 → java_codebase_rag-0.9.1}/server.py +25 -0
  10. {java_codebase_rag-0.9.0 → java_codebase_rag-0.9.1}/tests/test_config.py +4 -0
  11. {java_codebase_rag-0.9.0 → java_codebase_rag-0.9.1}/tests/test_lance_optimize.py +75 -14
  12. java_codebase_rag-0.9.1/tests/test_pipeline.py +103 -0
  13. {java_codebase_rag-0.9.0 → java_codebase_rag-0.9.1}/LICENSE +0 -0
  14. {java_codebase_rag-0.9.0 → java_codebase_rag-0.9.1}/README.md +0 -0
  15. {java_codebase_rag-0.9.0 → java_codebase_rag-0.9.1}/ast_java.py +0 -0
  16. {java_codebase_rag-0.9.0 → java_codebase_rag-0.9.1}/brownfield_events.py +0 -0
  17. {java_codebase_rag-0.9.0 → java_codebase_rag-0.9.1}/build_ast_graph.py +0 -0
  18. {java_codebase_rag-0.9.0 → java_codebase_rag-0.9.1}/chunk_heuristics.py +0 -0
  19. {java_codebase_rag-0.9.0 → java_codebase_rag-0.9.1}/graph_enrich.py +0 -0
  20. {java_codebase_rag-0.9.0 → java_codebase_rag-0.9.1}/graph_types.py +0 -0
  21. {java_codebase_rag-0.9.0 → java_codebase_rag-0.9.1}/index_common.py +0 -0
  22. {java_codebase_rag-0.9.0 → java_codebase_rag-0.9.1}/java_codebase_rag/__init__.py +0 -0
  23. {java_codebase_rag-0.9.0 → java_codebase_rag-0.9.1}/java_codebase_rag/_fdlimit.py +0 -0
  24. {java_codebase_rag-0.9.0 → java_codebase_rag-0.9.1}/java_codebase_rag/_stdio.py +0 -0
  25. {java_codebase_rag-0.9.0 → java_codebase_rag-0.9.1}/java_codebase_rag/cli.py +0 -0
  26. {java_codebase_rag-0.9.0 → java_codebase_rag-0.9.1}/java_codebase_rag/cli_format.py +0 -0
  27. {java_codebase_rag-0.9.0 → java_codebase_rag-0.9.1}/java_codebase_rag/cli_progress.py +0 -0
  28. {java_codebase_rag-0.9.0 → java_codebase_rag-0.9.1}/java_codebase_rag/install_data/agents/explorer-rag-cli.md +0 -0
  29. {java_codebase_rag-0.9.0 → java_codebase_rag-0.9.1}/java_codebase_rag/install_data/agents/explorer-rag-enhanced.md +0 -0
  30. {java_codebase_rag-0.9.0 → java_codebase_rag-0.9.1}/java_codebase_rag/install_data/skills/explore-codebase/SKILL.md +0 -0
  31. {java_codebase_rag-0.9.0 → java_codebase_rag-0.9.1}/java_codebase_rag/install_data/skills/explore-codebase-cli/SKILL.md +0 -0
  32. {java_codebase_rag-0.9.0 → java_codebase_rag-0.9.1}/java_codebase_rag/installer.py +0 -0
  33. {java_codebase_rag-0.9.0 → java_codebase_rag-0.9.1}/java_codebase_rag/jrag.py +0 -0
  34. {java_codebase_rag-0.9.0 → java_codebase_rag-0.9.1}/java_codebase_rag/jrag_envelope.py +0 -0
  35. {java_codebase_rag-0.9.0 → java_codebase_rag-0.9.1}/java_codebase_rag/jrag_hints.py +0 -0
  36. {java_codebase_rag-0.9.0 → java_codebase_rag-0.9.1}/java_codebase_rag/jrag_render.py +0 -0
  37. {java_codebase_rag-0.9.0 → java_codebase_rag-0.9.1}/java_codebase_rag/progress.py +0 -0
  38. {java_codebase_rag-0.9.0 → java_codebase_rag-0.9.1}/java_codebase_rag.egg-info/dependency_links.txt +0 -0
  39. {java_codebase_rag-0.9.0 → java_codebase_rag-0.9.1}/java_codebase_rag.egg-info/entry_points.txt +0 -0
  40. {java_codebase_rag-0.9.0 → java_codebase_rag-0.9.1}/java_codebase_rag.egg-info/requires.txt +0 -0
  41. {java_codebase_rag-0.9.0 → java_codebase_rag-0.9.1}/java_codebase_rag.egg-info/top_level.txt +0 -0
  42. {java_codebase_rag-0.9.0 → java_codebase_rag-0.9.1}/java_index_v1_common.py +0 -0
  43. {java_codebase_rag-0.9.0 → java_codebase_rag-0.9.1}/java_ontology.py +0 -0
  44. {java_codebase_rag-0.9.0 → java_codebase_rag-0.9.1}/ladybug_queries.py +0 -0
  45. {java_codebase_rag-0.9.0 → java_codebase_rag-0.9.1}/mcp_hints.py +0 -0
  46. {java_codebase_rag-0.9.0 → java_codebase_rag-0.9.1}/mcp_v2.py +0 -0
  47. {java_codebase_rag-0.9.0 → java_codebase_rag-0.9.1}/path_filtering.py +0 -0
  48. {java_codebase_rag-0.9.0 → java_codebase_rag-0.9.1}/pr_analysis.py +0 -0
  49. {java_codebase_rag-0.9.0 → java_codebase_rag-0.9.1}/resolve_service.py +0 -0
  50. {java_codebase_rag-0.9.0 → java_codebase_rag-0.9.1}/search_lancedb.py +0 -0
  51. {java_codebase_rag-0.9.0 → java_codebase_rag-0.9.1}/setup.cfg +0 -0
  52. {java_codebase_rag-0.9.0 → java_codebase_rag-0.9.1}/tests/test_agent_skills_static.py +0 -0
  53. {java_codebase_rag-0.9.0 → java_codebase_rag-0.9.1}/tests/test_assign_endpoint_client_extraction.py +0 -0
  54. {java_codebase_rag-0.9.0 → java_codebase_rag-0.9.1}/tests/test_ast_graph_build.py +0 -0
  55. {java_codebase_rag-0.9.0 → java_codebase_rag-0.9.1}/tests/test_ast_java_calls.py +0 -0
  56. {java_codebase_rag-0.9.0 → java_codebase_rag-0.9.1}/tests/test_ast_java_capabilities.py +0 -0
  57. {java_codebase_rag-0.9.0 → java_codebase_rag-0.9.1}/tests/test_ast_java_thread_safety.py +0 -0
  58. {java_codebase_rag-0.9.0 → java_codebase_rag-0.9.1}/tests/test_bank_chat_brownfield_integration.py +0 -0
  59. {java_codebase_rag-0.9.0 → java_codebase_rag-0.9.1}/tests/test_brownfield_clients.py +0 -0
  60. {java_codebase_rag-0.9.0 → java_codebase_rag-0.9.1}/tests/test_brownfield_events.py +0 -0
  61. {java_codebase_rag-0.9.0 → java_codebase_rag-0.9.1}/tests/test_brownfield_overrides.py +0 -0
  62. {java_codebase_rag-0.9.0 → java_codebase_rag-0.9.1}/tests/test_brownfield_routes.py +0 -0
  63. {java_codebase_rag-0.9.0 → java_codebase_rag-0.9.1}/tests/test_call_edge_matching.py +0 -0
  64. {java_codebase_rag-0.9.0 → java_codebase_rag-0.9.1}/tests/test_call_edges_e2e.py +0 -0
  65. {java_codebase_rag-0.9.0 → java_codebase_rag-0.9.1}/tests/test_call_graph_receiver_resolution.py +0 -0
  66. {java_codebase_rag-0.9.0 → java_codebase_rag-0.9.1}/tests/test_call_graph_smoke_roundtrip.py +0 -0
  67. {java_codebase_rag-0.9.0 → java_codebase_rag-0.9.1}/tests/test_call_invariant.py +0 -0
  68. {java_codebase_rag-0.9.0 → java_codebase_rag-0.9.1}/tests/test_cli_progress_stdout_invariant.py +0 -0
  69. {java_codebase_rag-0.9.0 → java_codebase_rag-0.9.1}/tests/test_cli_quiet_parity.py +0 -0
  70. {java_codebase_rag-0.9.0 → java_codebase_rag-0.9.1}/tests/test_client_hint_recovery.py +0 -0
  71. {java_codebase_rag-0.9.0 → java_codebase_rag-0.9.1}/tests/test_client_node_extraction.py +0 -0
  72. {java_codebase_rag-0.9.0 → java_codebase_rag-0.9.1}/tests/test_client_role_rename.py +0 -0
  73. {java_codebase_rag-0.9.0 → java_codebase_rag-0.9.1}/tests/test_cross_service_resolution_flag.py +0 -0
  74. {java_codebase_rag-0.9.0 → java_codebase_rag-0.9.1}/tests/test_edge_navigation_doc.py +0 -0
  75. {java_codebase_rag-0.9.0 → java_codebase_rag-0.9.1}/tests/test_fd_limit.py +0 -0
  76. {java_codebase_rag-0.9.0 → java_codebase_rag-0.9.1}/tests/test_feign_not_exposer.py +0 -0
  77. {java_codebase_rag-0.9.0 → java_codebase_rag-0.9.1}/tests/test_graph_enrich.py +0 -0
  78. {java_codebase_rag-0.9.0 → java_codebase_rag-0.9.1}/tests/test_graph_only_boot.py +0 -0
  79. {java_codebase_rag-0.9.0 → java_codebase_rag-0.9.1}/tests/test_incremental_graph.py +0 -0
  80. {java_codebase_rag-0.9.0 → java_codebase_rag-0.9.1}/tests/test_install_data_sync.py +0 -0
  81. {java_codebase_rag-0.9.0 → java_codebase_rag-0.9.1}/tests/test_installer.py +0 -0
  82. {java_codebase_rag-0.9.0 → java_codebase_rag-0.9.1}/tests/test_installer_integration.py +0 -0
  83. {java_codebase_rag-0.9.0 → java_codebase_rag-0.9.1}/tests/test_installer_surface.py +0 -0
  84. {java_codebase_rag-0.9.0 → java_codebase_rag-0.9.1}/tests/test_java_codebase_rag_cli.py +0 -0
  85. {java_codebase_rag-0.9.0 → java_codebase_rag-0.9.1}/tests/test_jrag_auto_scope.py +0 -0
  86. {java_codebase_rag-0.9.0 → java_codebase_rag-0.9.1}/tests/test_jrag_envelope.py +0 -0
  87. {java_codebase_rag-0.9.0 → java_codebase_rag-0.9.1}/tests/test_jrag_listing.py +0 -0
  88. {java_codebase_rag-0.9.0 → java_codebase_rag-0.9.1}/tests/test_jrag_locate.py +0 -0
  89. {java_codebase_rag-0.9.0 → java_codebase_rag-0.9.1}/tests/test_jrag_orientation.py +0 -0
  90. {java_codebase_rag-0.9.0 → java_codebase_rag-0.9.1}/tests/test_jrag_render.py +0 -0
  91. {java_codebase_rag-0.9.0 → java_codebase_rag-0.9.1}/tests/test_jrag_status.py +0 -0
  92. {java_codebase_rag-0.9.0 → java_codebase_rag-0.9.1}/tests/test_jrag_token_budget.py +0 -0
  93. {java_codebase_rag-0.9.0 → java_codebase_rag-0.9.1}/tests/test_jrag_traversal_compose.py +0 -0
  94. {java_codebase_rag-0.9.0 → java_codebase_rag-0.9.1}/tests/test_jrag_traversal_direct.py +0 -0
  95. {java_codebase_rag-0.9.0 → java_codebase_rag-0.9.1}/tests/test_ladybug_queries.py +0 -0
  96. {java_codebase_rag-0.9.0 → java_codebase_rag-0.9.1}/tests/test_lancedb_e2e.py +0 -0
  97. {java_codebase_rag-0.9.0 → java_codebase_rag-0.9.1}/tests/test_mcp_hints.py +0 -0
  98. {java_codebase_rag-0.9.0 → java_codebase_rag-0.9.1}/tests/test_mcp_server_project_root.py +0 -0
  99. {java_codebase_rag-0.9.0 → java_codebase_rag-0.9.1}/tests/test_mcp_tools.py +0 -0
  100. {java_codebase_rag-0.9.0 → java_codebase_rag-0.9.1}/tests/test_mcp_v2.py +0 -0
  101. {java_codebase_rag-0.9.0 → java_codebase_rag-0.9.1}/tests/test_mcp_v2_compose.py +0 -0
  102. {java_codebase_rag-0.9.0 → java_codebase_rag-0.9.1}/tests/test_meta_chain_core.py +0 -0
  103. {java_codebase_rag-0.9.0 → java_codebase_rag-0.9.1}/tests/test_microservice_scope.py +0 -0
  104. {java_codebase_rag-0.9.0 → java_codebase_rag-0.9.1}/tests/test_outgoing_call_extraction.py +0 -0
  105. {java_codebase_rag-0.9.0 → java_codebase_rag-0.9.1}/tests/test_packaging_metadata.py +0 -0
  106. {java_codebase_rag-0.9.0 → java_codebase_rag-0.9.1}/tests/test_path_filtering.py +0 -0
  107. {java_codebase_rag-0.9.0 → java_codebase_rag-0.9.1}/tests/test_pr_analysis.py +0 -0
  108. {java_codebase_rag-0.9.0 → java_codebase_rag-0.9.1}/tests/test_progress.py +0 -0
  109. {java_codebase_rag-0.9.0 → java_codebase_rag-0.9.1}/tests/test_resolve_routes_messaging_layer_c.py +0 -0
  110. {java_codebase_rag-0.9.0 → java_codebase_rag-0.9.1}/tests/test_resolve_service.py +0 -0
  111. {java_codebase_rag-0.9.0 → java_codebase_rag-0.9.1}/tests/test_route_extraction.py +0 -0
  112. {java_codebase_rag-0.9.0 → java_codebase_rag-0.9.1}/tests/test_schema_consistency.py +0 -0
  113. {java_codebase_rag-0.9.0 → java_codebase_rag-0.9.1}/tests/test_search_lancedb.py +0 -0
  114. {java_codebase_rag-0.9.0 → java_codebase_rag-0.9.1}/tests/test_search_lancedb_capability.py +0 -0
  115. {java_codebase_rag-0.9.0 → java_codebase_rag-0.9.1}/tests/test_string_value_atoms.py +0 -0
  116. {java_codebase_rag-0.9.0 → java_codebase_rag-0.9.1}/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.0
3
+ Version: 0.9.1
4
4
  Summary: MCP server for semantic + structural search over Java codebases
5
5
  Author: HumanBean17
6
6
  License-Expression: MIT
@@ -47,15 +47,39 @@ ENV_RUN_HEAVY = "JAVA_CODEBASE_RAG_RUN_HEAVY"
47
47
  COCOINDEX_MAX_INFLIGHT_COMPONENTS_ENV = "COCOINDEX_MAX_INFLIGHT_COMPONENTS"
48
48
  COCOINDEX_DEFAULT_MAX_INFLIGHT_COMPONENTS = "256"
49
49
 
50
+ # Lance native DataFusion hash-join memory pool ceiling (FairSpillPool). The
51
+ # lance default is ~100 MiB, tuned for query workloads — too small for the
52
+ # single big ``merge_insert`` cocoindex emits at the end of a flow component.
53
+ # On ``--full-reprocess`` (all rows match the existing table → bulk-update
54
+ # path) the hash join builds on a large side and exhausts the pool somewhere
55
+ # around 75k-100k chunks: "Resources exhausted: Failed to allocate ... for
56
+ # HashJoinInput ... N MiB remain available for the total pool". cocoindex is a
57
+ # bare pass-through to lancedb (it never sets a Session/memory_limit), so it
58
+ # inherits this default — we raise it here. FairSpillPool is a *reservation
59
+ # ceiling*, not a pre-allocation: setting 1 GiB does not reserve 1 GiB upfront,
60
+ # it just allows the join to grow before spilling/erroring, so it is safe on
61
+ # memory-constrained hosts. An operator can still override via their own
62
+ # ``LANCE_MEM_POOL_SIZE`` (subprocess_env copies os.environ, and apply is via
63
+ # ``setdefault`` so the operator value wins). Increment is unaffected (tiny
64
+ # batch → tiny hash table); only the full-reprocess write path is at risk.
65
+ LANCE_MEM_POOL_SIZE_ENV = "LANCE_MEM_POOL_SIZE"
66
+ LANCE_DEFAULT_MEM_POOL_SIZE = "1073741824" # 1 GiB
67
+
50
68
 
51
69
  def cocoindex_subprocess_env_defaults() -> dict[str, str]:
52
- """Env defaults applied to every CocoIndex subprocess to bound concurrency.
70
+ """Env defaults applied to every CocoIndex subprocess.
71
+
72
+ Bounds CocoIndex concurrency (``COCOINDEX_MAX_INFLIGHT_COMPONENTS``; see
73
+ :issue:`306`) and raises the Lance hash-join memory ceiling
74
+ (``LANCE_MEM_POOL_SIZE``) so a large full-reprocess does not exhaust the
75
+ default ~100 MiB pool mid-``merge_insert``.
53
76
 
54
77
  Apply with ``env.setdefault(...)`` so a caller-provided (operator) value
55
- always wins. See :issue:`306`.
78
+ always wins.
56
79
  """
57
80
  return {
58
- COCOINDEX_MAX_INFLIGHT_COMPONENTS_ENV: COCOINDEX_DEFAULT_MAX_INFLIGHT_COMPONENTS
81
+ COCOINDEX_MAX_INFLIGHT_COMPONENTS_ENV: COCOINDEX_DEFAULT_MAX_INFLIGHT_COMPONENTS,
82
+ LANCE_MEM_POOL_SIZE_ENV: LANCE_DEFAULT_MEM_POOL_SIZE,
59
83
  }
60
84
 
61
85
  _DEFAULT_EMBEDDING_MODEL = "sentence-transformers/all-MiniLM-L6-v2"
@@ -184,6 +184,31 @@ async def optimize_lance_tables(
184
184
 
185
185
  if last_exc is None:
186
186
  results[name] = "ok"
187
+ # Best-effort BTREE scalar index on the primary key ("id").
188
+ # cocoindex's merge_insert defaults to use_index=True but
189
+ # never creates a scalar PK index itself (declaring
190
+ # primary_key in the schema does NOT auto-build a lance
191
+ # index), so without this every merge_insert — increment
192
+ # included — is a forced full scan of the PK column,
193
+ # O(existing rows). On a large repo that scan dominates
194
+ # increment wall-clock; with the index present the join does
195
+ # lookups (~O(batch*log N)). Failure is non-fatal (the table
196
+ # is still correct, just un-indexed) and never alters the
197
+ # "ok" status, mirroring the FTS block below. ``replace=True``
198
+ # keeps it idempotent across runs; table.optimize() above
199
+ # maintains it on subsequent runs.
200
+ try:
201
+ from lancedb.index import BTree
202
+ await table.create_index("id", config=BTree(), replace=True)
203
+ except Exception as exc:
204
+ low = str(exc).lower()
205
+ if not any(
206
+ w in low for w in ("exist", "duplicate", "already", "same name")
207
+ ) and not quiet:
208
+ print(
209
+ f"java-codebase-rag: optimize: {name} id-index skipped: {exc}",
210
+ file=sys.stderr,
211
+ )
187
212
  # Best-effort FTS index at index time (PR-SEARCH-3) so hybrid
188
213
  # search works on all tables (java/sql/yaml) without a
189
214
  # first-query race. Failure is non-fatal — the lazy
@@ -132,6 +132,26 @@ def run_cocoindex_update(
132
132
  on_progress: Callable[[ProgressEvent], None] | None = None,
133
133
  on_progress_console: object | None = None,
134
134
  ) -> subprocess.CompletedProcess[str]:
135
+ if full_reprocess:
136
+ # A full reprocess rebuilds every row, so DROP the Lance target tables
137
+ # first and let cocoindex recreate them via the fast INSERT path. The
138
+ # in-place alternative (cocoindex's bulk-update merge_insert) emits
139
+ # ~one deletion-vector + version commit PER matched row — O(rows) of
140
+ # tiny file IO that scales to multi-minute hangs on large repos
141
+ # (measured ~83s sys time / 3474 deletion files for 3475 chunks on
142
+ # Shopizer; drop+recreate is ~3.6s sys / 0 deletions, ~3.7x faster and
143
+ # hang-free). Output is identical either way (full recompute); only the
144
+ # write path differs. Drop failure is non-fatal — if it somehow fails,
145
+ # the update falls back to the slow in-place path. The same fix is
146
+ # applied on the async server path (``server.run_refresh_pipeline``).
147
+ drop = run_cocoindex_drop(env, quiet=quiet)
148
+ if drop.returncode != 0 and not is_cocoindex_preflight_blocker(drop):
149
+ print(
150
+ "java-codebase-rag: drop-before-reprocess failed "
151
+ f"(exit {drop.returncode}); falling back to in-place update: "
152
+ f"{(drop.stderr or '').strip()[:200]}",
153
+ file=sys.stderr,
154
+ )
135
155
  result = _run_cocoindex_update_impl(
136
156
  env,
137
157
  full_reprocess=full_reprocess,
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: java-codebase-rag
3
- Version: 0.9.0
3
+ Version: 0.9.1
4
4
  Summary: MCP server for semantic + structural search over Java codebases
5
5
  Author: HumanBean17
6
6
  License-Expression: MIT
@@ -101,6 +101,7 @@ tests/test_microservice_scope.py
101
101
  tests/test_outgoing_call_extraction.py
102
102
  tests/test_packaging_metadata.py
103
103
  tests/test_path_filtering.py
104
+ tests/test_pipeline.py
104
105
  tests/test_pr_analysis.py
105
106
  tests/test_progress.py
106
107
  tests/test_resolve_routes_messaging_layer_c.py
@@ -112,6 +112,38 @@ _NUM_TXN_BEFORE_OPTIMIZE = 10**12
112
112
  # parent clamps to total on the terminal event anyway).
113
113
  _VECTORS_TICK_EVERY = 25
114
114
 
115
+ # Bounded concurrency for the per-file drain in app_main. cocoindex's embedder
116
+ # is ``@coco.fn.as_async(batching=True, runner=GPU, max_batch_size=64)`` — but
117
+ # its batching layer only coalesces calls that are in flight SIMULTANEOUSLY. A
118
+ # serial ``async for … await`` loop keeps just one file's chunks (avg 1–3) in
119
+ # flight, so real batches stay tiny and MPS idles between them (measured ~138
120
+ # chunks/s vs the ~235 chunks/s ceiling at batch=64 for all-MiniLM-L6-v2).
121
+ # Draining many files at once with a semaphore puts their chunks in flight
122
+ # together → the embedder coalesces them into full batches → MPS climbs toward
123
+ # the ceiling. Measured on Shopizer (1167 files / 3475 chunks): full init drops
124
+ # from ~46.7s (serial) to ~36.0s (32) / ~34.3s (64), with identical row output.
125
+ #
126
+ # This stays inside ONE component, so the earlier mount_each→app_main win is
127
+ # preserved: still exactly ONE merge_insert per table at commit. Memoization
128
+ # (``@coco.fn(memo=True)``) and the lock-guarded tick counter are safe under
129
+ # concurrency; ``parse_java`` uses a per-thread tree-sitter Parser (already
130
+ # routed via ``asyncio.to_thread``) and ``splitter.split`` is synchronous so the
131
+ # event loop cannot reenter it.
132
+ #
133
+ # Default 64 is sized to MATCH the embedder's hardcoded max_batch_size=64 (the
134
+ # decorator above; not a constructor arg, so not raisable from the flow): ~64
135
+ # files in flight reliably fills a 64-chunk batch and saturates MPS. Going higher
136
+ # buys nothing — the batch is already capped — and lower underfills it. Memory
137
+ # is NOT the limiting factor here: cocoindex buffers ALL staged rows until the
138
+ # single final merge_insert regardless of concurrency, so peak RSS is set by
139
+ # total chunk count (the commit buffer), not by how many files process at once.
140
+ # Set to ``1`` for the old serial behavior; raise/lower only if you have also
141
+ # changed the effective batch size or are constraining the commit buffer itself.
142
+ _FILE_CONCURRENCY = max(
143
+ 1,
144
+ int(os.environ.get("JAVA_CODEBASE_RAG_FILE_CONCURRENCY", "64") or "64"),
145
+ )
146
+
115
147
  # Thread-safe counter: cocoindex may call process_*_file concurrently
116
148
  # (mount_each parallelism is implementation-defined). A module-level lock guards
117
149
  # both the counter and the emission so two threads never interleave a tick.
@@ -533,6 +565,29 @@ async def process_yaml_file(
533
565
  )
534
566
 
535
567
 
568
+ async def _drain_files_concurrently(
569
+ files: Any, process_fn: Any, table: Any, sem: asyncio.Semaphore
570
+ ) -> None:
571
+ """Run ``process_fn(file, table)`` over every file with bounded concurrency.
572
+
573
+ Replaces the serial ``async for … await process_*_file`` loop so the
574
+ embedder's batching layer sees many files' chunks in flight at once (see
575
+ ``_FILE_CONCURRENCY``). Materializes the async iterable up front — file
576
+ handles are lightweight and cocoindex already realized the collection when
577
+ the walker mounted, so this is not a second walk. An empty collection is a
578
+ no-op (e.g. SQL/YAML tables on a repo with none).
579
+ """
580
+ items = [f async for _, f in files.items()]
581
+ if not items:
582
+ return
583
+
584
+ async def _one(_file: Any) -> None:
585
+ async with sem:
586
+ await process_fn(_file, table)
587
+
588
+ await asyncio.gather(*(_one(f) for f in items))
589
+
590
+
536
591
  @coco.fn
537
592
  async def app_main() -> None:
538
593
  java_schema = await lancedb.TableSchema.from_class(
@@ -646,12 +701,17 @@ async def app_main() -> None:
646
701
  # unchanged files still skip re-embedding on incremental; _RowHandler.
647
702
  # reconcile skips rows whose fingerprint is unchanged → increment carries
648
703
  # only changed rows in its single merge_insert.
649
- async for _key, _file in java_files.items():
650
- await process_java_file(_file, java_table)
651
- async for _key, _file in sql_files.items():
652
- await process_sql_file(_file, sql_table)
653
- async for _key, _file in yaml_files.items():
654
- await process_yaml_file(_file, yaml_table)
704
+ #
705
+ # PERF (concurrency): drain files with a bounded semaphore instead of a
706
+ # serial ``async for await``. See ``_FILE_CONCURRENCY`` — this is what
707
+ # lets the embedder's batching layer fill real batches (embedding dominates
708
+ # init cost, and serial files starve it). One shared semaphore bounds total
709
+ # in-flight work; tables are drained in order (java dominates, sql/yaml are
710
+ # usually near-empty).
711
+ _sem = asyncio.Semaphore(_FILE_CONCURRENCY)
712
+ await _drain_files_concurrently(java_files, process_java_file, java_table, _sem)
713
+ await _drain_files_concurrently(sql_files, process_sql_file, sql_table, _sem)
714
+ await _drain_files_concurrently(yaml_files, process_yaml_file, yaml_table, _sem)
655
715
 
656
716
 
657
717
  app = coco.App(
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "java-codebase-rag"
7
- version = "0.9.0"
7
+ version = "0.9.1"
8
8
  description = "MCP server for semantic + structural search over Java codebases"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.11"
@@ -349,6 +349,31 @@ async def run_refresh_pipeline(
349
349
  )
350
350
  proc: asyncio.subprocess.Process | None = None
351
351
  out_b, err_b = b"", b""
352
+ # DROP the Lance target tables so the update takes the fast INSERT path
353
+ # instead of cocoindex's in-place bulk-update, which emits ~one deletion-
354
+ # vector + version commit PER matched row — O(rows) of tiny file IO that
355
+ # hangs for many minutes on large repos. Drop+recreate is identical output
356
+ # for a full rebuild (the very thing --full-reprocess means). Same fix on
357
+ # the sync path: pipeline.run_cocoindex_update. Drop failure is non-fatal:
358
+ # the update falls back to the slow in-place path.
359
+ try:
360
+ drop_proc = await asyncio.create_subprocess_exec(
361
+ str(cocoindex_bin),
362
+ "drop",
363
+ _COCOINDEX_TARGET,
364
+ "-f",
365
+ cwd=str(flow_path.parent),
366
+ env=_cocoindex_subprocess_env(root),
367
+ stdout=asyncio.subprocess.PIPE,
368
+ stderr=asyncio.subprocess.PIPE,
369
+ )
370
+ await drop_proc.communicate()
371
+ except Exception as exc:
372
+ print(
373
+ f"java-codebase-rag: drop-before-reprocess failed ({exc!s}); "
374
+ "falling back to in-place update",
375
+ file=sys.stderr,
376
+ )
352
377
  if quiet:
353
378
  try:
354
379
  proc = await asyncio.create_subprocess_exec(
@@ -506,6 +506,10 @@ def test_cocoindex_subprocess_env_defaults_uses_real_inflight_env_var() -> None:
506
506
  assert defaults["COCOINDEX_MAX_INFLIGHT_COMPONENTS"] == "256"
507
507
  # The bogus name from the broken #293 fix must NOT leak back in.
508
508
  assert "COCOINDEX_SOURCE_MAX_INFLIGHT_ROWS" not in defaults
509
+ # Lance native hash-join pool ceiling, raised from the ~100 MiB default so a
510
+ # large full-reprocess merge_insert does not exhaust it mid-commit. Applied
511
+ # via setdefault, so an operator's own value still wins.
512
+ assert defaults["LANCE_MEM_POOL_SIZE"] == "1073741824"
509
513
 
510
514
 
511
515
  class TestConfigSourcePointer:
@@ -123,31 +123,92 @@ async def test_optimize_builds_fts_index_after_success(monkeypatch, tmp_path) ->
123
123
  table = _FakeTable(name, [None]) # optimize succeeds on first try
124
124
  conn = _FakeConnection(table_names={name}, tables={name: table})
125
125
  _install_fake_lancedb(monkeypatch, conn)
126
- # Make ``from lancedb.index import FTS`` resolve against a stand-in module.
127
- index_mod = types.ModuleType("lancedb.index")
128
-
129
- class FTS: # stands in for lancedb.index.FTS config object
130
- pass
131
-
132
- index_mod.FTS = FTS
133
- monkeypatch.setitem(sys.modules, "lancedb.index", index_mod)
126
+ fts_cls, _btree_cls = _install_fake_lancedb_index(monkeypatch)
134
127
 
135
128
  results = await lance_optimize.optimize_lance_tables(tmp_path, quiet=True)
136
129
 
137
130
  assert results[name] == "ok"
138
- assert len(table.create_index_calls) == 1, (
139
- f"expected FTS create_index once after optimize, got {table.create_index_calls}"
140
- )
141
- call = table.create_index_calls[0]
142
- assert call["args"] and call["args"][0] == "text", (
143
- f"FTS index must target the 'text' column, got args={call['args']}"
131
+ # optimize now builds TWO indices (BTREE on "id" + FTS on "text"); find the
132
+ # FTS one by column rather than asserting on call count / order.
133
+ fts_calls = [
134
+ c for c in table.create_index_calls if c["args"] and c["args"][0] == "text"
135
+ ]
136
+ assert len(fts_calls) == 1, (
137
+ f"expected one FTS create_index on 'text', got {table.create_index_calls}"
144
138
  )
139
+ call = fts_calls[0]
145
140
  assert call["kwargs"].get("replace") is True, (
146
141
  f"FTS index must be built with replace=True, got kwargs={call['kwargs']}"
147
142
  )
143
+ assert isinstance(call["kwargs"].get("config"), fts_cls), (
144
+ f"FTS index config must be a lancedb.index.FTS, got kwargs={call['kwargs']}"
145
+ )
148
146
  assert conn.closed is True
149
147
 
150
148
 
149
+ async def test_optimize_builds_btree_pk_index_after_success(monkeypatch, tmp_path) -> None:
150
+ """A successful optimize builds a BTREE scalar index on the ``id`` PK.
151
+
152
+ cocoindex's ``merge_insert`` defaults to ``use_index=True`` but never creates
153
+ a scalar PK index itself (declaring ``primary_key`` does NOT auto-build a
154
+ lance index), so without this every ``merge_insert`` — increment included —
155
+ is a forced full scan of the PK column (O(existing rows)). The BTREE index
156
+ flips that to ~O(batch*log N) lookups. Guards the path against silent
157
+ regression under the broad ``except`` that swallows index-build failures.
158
+ """
159
+ import types
160
+
161
+ from java_codebase_rag import lance_optimize
162
+
163
+ name = lance_optimize.LANCE_TABLE_NAMES[0]
164
+ table = _FakeTable(name, [None]) # optimize succeeds on first try
165
+ conn = _FakeConnection(table_names={name}, tables={name: table})
166
+ _install_fake_lancedb(monkeypatch, conn)
167
+ _fts_cls, btree_cls = _install_fake_lancedb_index(monkeypatch)
168
+
169
+ results = await lance_optimize.optimize_lance_tables(tmp_path, quiet=True)
170
+
171
+ assert results[name] == "ok"
172
+ id_calls = [
173
+ c for c in table.create_index_calls if c["args"] and c["args"][0] == "id"
174
+ ]
175
+ assert len(id_calls) == 1, (
176
+ f"expected one BTREE create_index on 'id', got {table.create_index_calls}"
177
+ )
178
+ call = id_calls[0]
179
+ assert call["kwargs"].get("replace") is True, (
180
+ f"BTREE PK index must be built with replace=True, got kwargs={call['kwargs']}"
181
+ )
182
+ assert isinstance(call["kwargs"].get("config"), btree_cls), (
183
+ f"BTREE PK index config must be a lancedb.index.BTree, got kwargs={call['kwargs']}"
184
+ )
185
+
186
+
187
+ def _install_fake_lancedb_index(monkeypatch):
188
+ """Make ``from lancedb.index import FTS, BTree`` resolve to stand-in classes.
189
+
190
+ Returns the ``(FTS, BTree)`` config stand-ins so a test can assert the right
191
+ config object was passed. Both the FTS path (PR-SEARCH-3) and the BTREE PK
192
+ path build their index config via a local ``from lancedb.index import ...``;
193
+ without a stand-in those imports hit the broad ``except`` and silently no-op,
194
+ leaving the path unexercised.
195
+ """
196
+ import types
197
+
198
+ index_mod = types.ModuleType("lancedb.index")
199
+
200
+ class FTS: # stands in for lancedb.index.FTS config object
201
+ pass
202
+
203
+ class BTree: # stands in for lancedb.index.BTree config object
204
+ pass
205
+
206
+ index_mod.FTS = FTS
207
+ index_mod.BTree = BTree
208
+ monkeypatch.setitem(sys.modules, "lancedb.index", index_mod)
209
+ return FTS, BTree
210
+
211
+
151
212
  async def test_optimize_does_not_retry_non_conflict_error(monkeypatch, tmp_path) -> None:
152
213
  """A non-conflict exception is re-raised (captured per-table), never retried."""
153
214
  from java_codebase_rag import lance_optimize
@@ -0,0 +1,103 @@
1
+ """Tests for ``java_codebase_rag.pipeline`` subprocess helpers.
2
+
3
+ Focus: ``run_cocoindex_update`` drops the Lance target tables before a *full
4
+ reprocess* so the update takes the fast INSERT path. The in-place alternative
5
+ (cocoindex's bulk-update ``merge_insert``) emits ~one deletion-vector + version
6
+ commit per matched row — O(rows) of tiny file IO that hangs for many minutes on
7
+ large repos. Drop+recreate is identical output for a full rebuild.
8
+ """
9
+ from __future__ import annotations
10
+
11
+ import subprocess
12
+
13
+ from java_codebase_rag import pipeline
14
+
15
+
16
+ def _ok() -> subprocess.CompletedProcess:
17
+ return subprocess.CompletedProcess(args=[], returncode=0, stdout="", stderr="")
18
+
19
+
20
+ def _stub_impl(monkeypatch, seen: dict) -> None:
21
+ """Replace the impl + post-optimize with no-op stubs (no cocoindex/lancedb)."""
22
+
23
+ def fake_impl(env, **kwargs):
24
+ seen["update"] = seen.get("update", 0) + 1
25
+ seen["full_reprocess"] = kwargs.get("full_reprocess")
26
+ return _ok()
27
+
28
+ monkeypatch.setattr(pipeline, "_run_cocoindex_update_impl", fake_impl)
29
+ monkeypatch.setattr(pipeline, "_maybe_run_serialized_optimize", lambda *a, **k: None)
30
+
31
+
32
+ def test_full_reprocess_drops_tables_first(monkeypatch) -> None:
33
+ """full_reprocess=True drops exactly once before the update (INSERT path)."""
34
+ seen: dict = {}
35
+ _stub_impl(monkeypatch, seen)
36
+ drops: list[dict] = []
37
+
38
+ def fake_drop(env, *, quiet):
39
+ drops.append(env)
40
+ return _ok()
41
+
42
+ monkeypatch.setattr(pipeline, "run_cocoindex_drop", fake_drop)
43
+
44
+ pipeline.run_cocoindex_update({"X": "1"}, full_reprocess=True, quiet=True)
45
+
46
+ assert len(drops) == 1, "full_reprocess must drop exactly once before update"
47
+ assert drops[0] == {"X": "1"}, "drop must receive the same env as the update"
48
+ assert seen["update"] == 1
49
+
50
+
51
+ def test_increment_does_not_drop(monkeypatch) -> None:
52
+ """full_reprocess=False (increment) must NOT drop — it would lose the table."""
53
+ seen: dict = {}
54
+ _stub_impl(monkeypatch, seen)
55
+ drops: list[dict] = []
56
+
57
+ def fake_drop(env, *, quiet):
58
+ drops.append(env)
59
+ return _ok()
60
+
61
+ monkeypatch.setattr(pipeline, "run_cocoindex_drop", fake_drop)
62
+
63
+ pipeline.run_cocoindex_update({}, full_reprocess=False, quiet=True)
64
+
65
+ assert drops == [], "increment must not drop the tables"
66
+ assert seen["update"] == 1
67
+
68
+
69
+ def test_drop_failure_falls_back_to_inplace(monkeypatch, capsys) -> None:
70
+ """A non-preflight drop failure does not abort — the update still runs in-place."""
71
+ seen: dict = {}
72
+ _stub_impl(monkeypatch, seen)
73
+ monkeypatch.setattr(
74
+ pipeline,
75
+ "run_cocoindex_drop",
76
+ lambda env, *, quiet: subprocess.CompletedProcess(
77
+ args=[], returncode=2, stdout="", stderr="boom"
78
+ ),
79
+ )
80
+
81
+ pipeline.run_cocoindex_update({}, full_reprocess=True, quiet=True)
82
+
83
+ assert seen["update"] == 1, "update must still run after a non-fatal drop failure"
84
+ assert "drop-before-reprocess failed" in capsys.readouterr().err
85
+
86
+
87
+ def test_drop_preflight_blocker_is_silent(monkeypatch, capsys) -> None:
88
+ """A preflight drop stub (cocoindex not installed, e.g. graph-only) is not noisy."""
89
+ seen: dict = {}
90
+ _stub_impl(monkeypatch, seen)
91
+ monkeypatch.setattr(
92
+ pipeline,
93
+ "run_cocoindex_drop",
94
+ lambda env, *, quiet: subprocess.CompletedProcess(
95
+ args=["cocoindex"], returncode=127, stdout="", stderr="not found"
96
+ ),
97
+ )
98
+
99
+ pipeline.run_cocoindex_update({}, full_reprocess=True, quiet=True)
100
+
101
+ assert seen["update"] == 1
102
+ # 127 preflight is expected on graph-only installs and must NOT warn.
103
+ assert "drop-before-reprocess failed" not in capsys.readouterr().err