java-codebase-rag 0.9.0__py3-none-any.whl → 0.9.2__py3-none-any.whl

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.
@@ -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"
java_codebase_rag/jrag.py CHANGED
@@ -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
@@ -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.2
4
4
  Summary: MCP server for semantic + structural search over Java codebases
5
5
  Author: HumanBean17
6
6
  License-Expression: MIT
@@ -5,39 +5,39 @@ chunk_heuristics.py,sha256=aQk2NOKxzUdqoUAJUO3G3LE0MN_bYZWNLQ0tkmj5uts,1813
5
5
  graph_enrich.py,sha256=Fxp7JurH9wWTOTX1nErjH4H6Jw5bB68YCqR17rsX6gw,62829
6
6
  graph_types.py,sha256=P6RVdEiqZlaSgTyXepnGbtMoZq0m_MaP2q6M6i7Uxlc,4539
7
7
  index_common.py,sha256=HT6FKHFJ084eFvd3fR1j8z8gf4eWoPHVW8GXLpw464I,285
8
- java_index_flow_lancedb.py,sha256=OFrvOV2xWPvBkQ1kS6nxcOjdJbYAGcEirzeevrOU1PM,25598
8
+ java_index_flow_lancedb.py,sha256=kDkhnRxl-Kz0YNMY27DTk3gwa8H2nWyGDCQC8hxx-jI,28970
9
9
  java_index_v1_common.py,sha256=nF1KrSqboF_RRvWerG9knRRFmWwsrG_CvhgnsoZ8KqA,1154
10
- java_ontology.py,sha256=ooqr8GucOINpzhdEQ3QzVe5A9GfiR0nUTlySDehn9GA,17129
10
+ java_ontology.py,sha256=eUGFa96GV_09s2f2sWSMdqYmoxU3-1pyU2iipvcDjG0,17219
11
11
  ladybug_queries.py,sha256=rVnVEHwWwE4USeX7tICYEl1SSiSxJGnBNe1VX66R3Xk,100531
12
12
  mcp_hints.py,sha256=zp-4cnOmbYD0YovmZiLS2oGcvWcWE7n8jKVz4_xifno,42512
13
13
  mcp_v2.py,sha256=ABNHiZEEwQ76uPF9E5fQWncJ4JmmgtG3vmt5TDxBGVo,68901
14
14
  path_filtering.py,sha256=R--XzI51LXBu5IBKMCnJWbkNr6I5d-SDmltyQQnWco0,17674
15
15
  pr_analysis.py,sha256=zrmZZD5yotJtM02Kif6_jgI_oeformOao793akp0N6Y,18394
16
- resolve_service.py,sha256=tC5FQsGmqhqn0EOexVDlRq5egnzKDTrI7CMzk0nPpG8,25135
16
+ resolve_service.py,sha256=lTHPSn5zgIFjNICiOZc6m_bBJShTn72kShhkofwZdoE,26364
17
17
  search_lancedb.py,sha256=1sGSZ6H8J9hGcKAwHpzB7F0_in3A_sEtOI5LvlZYIRI,43864
18
- server.py,sha256=yNpJX_0D1xXY1FHGGBrb4JMOg2THw4_2Ao8YCdijBp0,35944
18
+ server.py,sha256=nOK3DOr-i3PJnVmE7neye_tptJumgXfIXPlTIn01MPQ,37065
19
19
  java_codebase_rag/__init__.py,sha256=AbpHGcgLb-kRsJGnwFEktk7uzpZOCcBY74-YBdrKVGs,1
20
20
  java_codebase_rag/_fdlimit.py,sha256=vkwjsPbZfxzZ2DZTPWO5DxtuNlLzOADzIq07iYX7GCU,2465
21
21
  java_codebase_rag/_stdio.py,sha256=TDNbpt2EP0_Zd622ihdlKwlMfxkKHWOfgLVcU6TcNbo,1458
22
22
  java_codebase_rag/cli.py,sha256=8WKk-_Zl1Sp7wHX4Y5f3bkIy8hHRH_QWrNuX_3WYTm4,45269
23
23
  java_codebase_rag/cli_format.py,sha256=CT7-xdwZ0bMCdP68_UOwkvm-mnLluU3LutlM-mDNk60,1839
24
24
  java_codebase_rag/cli_progress.py,sha256=q6Wh97yzLGs1B8UFk_WAKivfQu7Y5RnUUE-T2YHWkIs,3237
25
- java_codebase_rag/config.py,sha256=Yl7Nf0O_ZOZTPtyPMap83jRM7qNhWwfbVJTKSEdERi4,24916
25
+ java_codebase_rag/config.py,sha256=1EAlKtQx7LUo-gPoE6BOVLz0YEl1OpWFRABkF06zjgk,26464
26
26
  java_codebase_rag/installer.py,sha256=c-_tR1Ct_O3yhmruFRnDoM1Wilz-igD9qdkDPsNqLMI,79934
27
- java_codebase_rag/jrag.py,sha256=cVUWrKOtkwe2r4u1nknzmrMQMy16bdt1n-dZQpiSFWs,191811
27
+ java_codebase_rag/jrag.py,sha256=KYv1pD_FTdYh0NqEwO_yzqgHiv5PDFcMiL26aA40QwI,192278
28
28
  java_codebase_rag/jrag_envelope.py,sha256=5jD3p2O-p9acAKHqoif5FFoqgP7Q2ziCuKSiPCHoywc,47505
29
29
  java_codebase_rag/jrag_hints.py,sha256=k2PFE4s3lZgBYHMdZcTjx1-w28nfQcBtQEVsSxI_DvE,9262
30
30
  java_codebase_rag/jrag_render.py,sha256=1nUyamL-MOOXDlKvssp-LsBgEtznUnGj_cPteSwuS90,31953
31
- java_codebase_rag/lance_optimize.py,sha256=_90eajcIpGUNm3GWtx6AYDERNHaMsaMZs7zu2KqNPEU,10347
32
- java_codebase_rag/pipeline.py,sha256=TkHb7DybFlpHje30aYbuFt5jWFSJBdCQ-7hc4jFaNhI,16481
31
+ java_codebase_rag/lance_optimize.py,sha256=HI3aFebP1fenLL6Cav1jMG5kLXSrHLndO_MIJY6qQVo,11977
32
+ java_codebase_rag/pipeline.py,sha256=L65mjK-IxkVWazUNVyIvzfQNi24behQu-Kdc7o_HwEk,17754
33
33
  java_codebase_rag/progress.py,sha256=2IxdMALDM0wAQCyJrrfZ975zM_85C-4BfHxf4AtYifE,23212
34
34
  java_codebase_rag/install_data/agents/explorer-rag-cli.md,sha256=mMij_BIQM4agaYhGVYjC-fQSe3We1HFeBvc5JBjJj6A,10071
35
35
  java_codebase_rag/install_data/agents/explorer-rag-enhanced.md,sha256=gZsNFbuK0lSnOIlplbbS_muz2ozokJqvFUv65QM0NDM,10406
36
36
  java_codebase_rag/install_data/skills/explore-codebase/SKILL.md,sha256=A-v2dueVnxwBzBlxoRjZ2zOJk8DranLQ1TElwn94h0s,11529
37
37
  java_codebase_rag/install_data/skills/explore-codebase-cli/SKILL.md,sha256=V5gIKKGkgk2KlAFf9JqPareQDMt1iQOI7cwhfOqJL1c,11348
38
- java_codebase_rag-0.9.0.dist-info/licenses/LICENSE,sha256=gxvtiHtuviR_q8ZAjWw-QTcF3DyPzg6ZY-lQrr8OPpw,1068
39
- java_codebase_rag-0.9.0.dist-info/METADATA,sha256=pIQfvYY6sWdmq-PIXm0vOUzNobTUtW14Z-Zp-53qaf8,20088
40
- java_codebase_rag-0.9.0.dist-info/WHEEL,sha256=K260EYznzXsJYBQGqmI8VTxEdiZYNvDZwW9cBh9-_MA,91
41
- java_codebase_rag-0.9.0.dist-info/entry_points.txt,sha256=cj3QTc11UYVQnj9T3orc4daiIGaCYrXP149vKbH2R4U,168
42
- java_codebase_rag-0.9.0.dist-info/top_level.txt,sha256=8vC-VN3cMwz5vhkSTaeJ1a1bDeqLWEfrTks1CvEvIg0,273
43
- java_codebase_rag-0.9.0.dist-info/RECORD,,
38
+ java_codebase_rag-0.9.2.dist-info/licenses/LICENSE,sha256=gxvtiHtuviR_q8ZAjWw-QTcF3DyPzg6ZY-lQrr8OPpw,1068
39
+ java_codebase_rag-0.9.2.dist-info/METADATA,sha256=3lfDnbvPb-vpc_81VLNobOgdUKncX6ejy3TDUUSj4Vo,20088
40
+ java_codebase_rag-0.9.2.dist-info/WHEEL,sha256=K260EYznzXsJYBQGqmI8VTxEdiZYNvDZwW9cBh9-_MA,91
41
+ java_codebase_rag-0.9.2.dist-info/entry_points.txt,sha256=cj3QTc11UYVQnj9T3orc4daiIGaCYrXP149vKbH2R4U,168
42
+ java_codebase_rag-0.9.2.dist-info/top_level.txt,sha256=8vC-VN3cMwz5vhkSTaeJ1a1bDeqLWEfrTks1CvEvIg0,273
43
+ java_codebase_rag-0.9.2.dist-info/RECORD,,
@@ -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(
java_ontology.py CHANGED
@@ -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",
resolve_service.py CHANGED
@@ -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
 
server.py CHANGED
@@ -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(