java-codebase-rag 0.6.2__tar.gz → 0.6.4__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 (93) hide show
  1. {java_codebase_rag-0.6.2/java_codebase_rag.egg-info → java_codebase_rag-0.6.4}/PKG-INFO +13 -10
  2. {java_codebase_rag-0.6.2 → java_codebase_rag-0.6.4}/README.md +11 -9
  3. {java_codebase_rag-0.6.2 → java_codebase_rag-0.6.4}/ast_java.py +28 -57
  4. {java_codebase_rag-0.6.2 → java_codebase_rag-0.6.4}/build_ast_graph.py +118 -26
  5. {java_codebase_rag-0.6.2 → java_codebase_rag-0.6.4}/graph_enrich.py +13 -35
  6. {java_codebase_rag-0.6.2 → java_codebase_rag-0.6.4}/java_codebase_rag/cli.py +97 -24
  7. {java_codebase_rag-0.6.2 → java_codebase_rag-0.6.4}/java_codebase_rag/cli_format.py +8 -35
  8. {java_codebase_rag-0.6.2 → java_codebase_rag-0.6.4}/java_codebase_rag/cli_progress.py +17 -21
  9. {java_codebase_rag-0.6.2 → java_codebase_rag-0.6.4}/java_codebase_rag/install_data/agents/explorer-rag-enhanced.md +3 -3
  10. {java_codebase_rag-0.6.2 → java_codebase_rag-0.6.4}/java_codebase_rag/install_data/skills/explore-codebase/SKILL.md +5 -5
  11. {java_codebase_rag-0.6.2 → java_codebase_rag-0.6.4}/java_codebase_rag/installer.py +180 -43
  12. java_codebase_rag-0.6.4/java_codebase_rag/lance_optimize.py +222 -0
  13. {java_codebase_rag-0.6.2 → java_codebase_rag-0.6.4}/java_codebase_rag/pipeline.py +71 -23
  14. java_codebase_rag-0.6.4/java_codebase_rag/progress.py +570 -0
  15. {java_codebase_rag-0.6.2 → java_codebase_rag-0.6.4/java_codebase_rag.egg-info}/PKG-INFO +13 -10
  16. {java_codebase_rag-0.6.2 → java_codebase_rag-0.6.4}/java_codebase_rag.egg-info/SOURCES.txt +5 -1
  17. {java_codebase_rag-0.6.2 → java_codebase_rag-0.6.4}/java_codebase_rag.egg-info/requires.txt +1 -0
  18. {java_codebase_rag-0.6.2 → java_codebase_rag-0.6.4}/java_index_flow_lancedb.py +238 -18
  19. {java_codebase_rag-0.6.2 → java_codebase_rag-0.6.4}/ladybug_queries.py +45 -51
  20. {java_codebase_rag-0.6.2 → java_codebase_rag-0.6.4}/mcp_v2.py +9 -17
  21. {java_codebase_rag-0.6.2 → java_codebase_rag-0.6.4}/path_filtering.py +14 -17
  22. {java_codebase_rag-0.6.2 → java_codebase_rag-0.6.4}/pr_analysis.py +3 -4
  23. {java_codebase_rag-0.6.2 → java_codebase_rag-0.6.4}/pyproject.toml +2 -1
  24. {java_codebase_rag-0.6.2 → java_codebase_rag-0.6.4}/search_lancedb.py +2 -2
  25. {java_codebase_rag-0.6.2 → java_codebase_rag-0.6.4}/server.py +75 -9
  26. {java_codebase_rag-0.6.2 → java_codebase_rag-0.6.4}/tests/test_ast_graph_build.py +136 -0
  27. java_codebase_rag-0.6.4/tests/test_ast_java_thread_safety.py +66 -0
  28. {java_codebase_rag-0.6.2 → java_codebase_rag-0.6.4}/tests/test_cli_progress_stdout_invariant.py +3 -3
  29. {java_codebase_rag-0.6.2 → java_codebase_rag-0.6.4}/tests/test_installer.py +293 -1
  30. {java_codebase_rag-0.6.2 → java_codebase_rag-0.6.4}/tests/test_java_codebase_rag_cli.py +226 -2
  31. {java_codebase_rag-0.6.2 → java_codebase_rag-0.6.4}/tests/test_microservice_scope.py +118 -0
  32. {java_codebase_rag-0.6.2 → java_codebase_rag-0.6.4}/tests/test_path_filtering.py +14 -14
  33. java_codebase_rag-0.6.4/tests/test_progress.py +463 -0
  34. java_codebase_rag-0.6.4/tests/test_vectors_progress.py +349 -0
  35. java_codebase_rag-0.6.2/java_codebase_rag/lance_optimize.py +0 -148
  36. {java_codebase_rag-0.6.2 → java_codebase_rag-0.6.4}/LICENSE +0 -0
  37. {java_codebase_rag-0.6.2 → java_codebase_rag-0.6.4}/brownfield_events.py +0 -0
  38. {java_codebase_rag-0.6.2 → java_codebase_rag-0.6.4}/chunk_heuristics.py +0 -0
  39. {java_codebase_rag-0.6.2 → java_codebase_rag-0.6.4}/index_common.py +0 -0
  40. {java_codebase_rag-0.6.2 → java_codebase_rag-0.6.4}/java_codebase_rag/__init__.py +0 -0
  41. {java_codebase_rag-0.6.2 → java_codebase_rag-0.6.4}/java_codebase_rag/_fdlimit.py +0 -0
  42. {java_codebase_rag-0.6.2 → java_codebase_rag-0.6.4}/java_codebase_rag/config.py +0 -0
  43. {java_codebase_rag-0.6.2 → java_codebase_rag-0.6.4}/java_codebase_rag.egg-info/dependency_links.txt +0 -0
  44. {java_codebase_rag-0.6.2 → java_codebase_rag-0.6.4}/java_codebase_rag.egg-info/entry_points.txt +0 -0
  45. {java_codebase_rag-0.6.2 → java_codebase_rag-0.6.4}/java_codebase_rag.egg-info/top_level.txt +0 -0
  46. {java_codebase_rag-0.6.2 → java_codebase_rag-0.6.4}/java_index_v1_common.py +0 -0
  47. {java_codebase_rag-0.6.2 → java_codebase_rag-0.6.4}/java_ontology.py +0 -0
  48. {java_codebase_rag-0.6.2 → java_codebase_rag-0.6.4}/mcp_hints.py +0 -0
  49. {java_codebase_rag-0.6.2 → java_codebase_rag-0.6.4}/setup.cfg +0 -0
  50. {java_codebase_rag-0.6.2 → java_codebase_rag-0.6.4}/tests/test_agent_skills_static.py +0 -0
  51. {java_codebase_rag-0.6.2 → java_codebase_rag-0.6.4}/tests/test_assign_endpoint_client_extraction.py +0 -0
  52. {java_codebase_rag-0.6.2 → java_codebase_rag-0.6.4}/tests/test_ast_java_calls.py +0 -0
  53. {java_codebase_rag-0.6.2 → java_codebase_rag-0.6.4}/tests/test_ast_java_capabilities.py +0 -0
  54. {java_codebase_rag-0.6.2 → java_codebase_rag-0.6.4}/tests/test_bank_chat_brownfield_integration.py +0 -0
  55. {java_codebase_rag-0.6.2 → java_codebase_rag-0.6.4}/tests/test_brownfield_clients.py +0 -0
  56. {java_codebase_rag-0.6.2 → java_codebase_rag-0.6.4}/tests/test_brownfield_events.py +0 -0
  57. {java_codebase_rag-0.6.2 → java_codebase_rag-0.6.4}/tests/test_brownfield_overrides.py +0 -0
  58. {java_codebase_rag-0.6.2 → java_codebase_rag-0.6.4}/tests/test_brownfield_routes.py +0 -0
  59. {java_codebase_rag-0.6.2 → java_codebase_rag-0.6.4}/tests/test_call_edge_matching.py +0 -0
  60. {java_codebase_rag-0.6.2 → java_codebase_rag-0.6.4}/tests/test_call_edges_e2e.py +0 -0
  61. {java_codebase_rag-0.6.2 → java_codebase_rag-0.6.4}/tests/test_call_graph_receiver_resolution.py +0 -0
  62. {java_codebase_rag-0.6.2 → java_codebase_rag-0.6.4}/tests/test_call_graph_smoke_roundtrip.py +0 -0
  63. {java_codebase_rag-0.6.2 → java_codebase_rag-0.6.4}/tests/test_call_invariant.py +0 -0
  64. {java_codebase_rag-0.6.2 → java_codebase_rag-0.6.4}/tests/test_cli_quiet_parity.py +0 -0
  65. {java_codebase_rag-0.6.2 → java_codebase_rag-0.6.4}/tests/test_client_hint_recovery.py +0 -0
  66. {java_codebase_rag-0.6.2 → java_codebase_rag-0.6.4}/tests/test_client_node_extraction.py +0 -0
  67. {java_codebase_rag-0.6.2 → java_codebase_rag-0.6.4}/tests/test_client_role_rename.py +0 -0
  68. {java_codebase_rag-0.6.2 → java_codebase_rag-0.6.4}/tests/test_config.py +0 -0
  69. {java_codebase_rag-0.6.2 → java_codebase_rag-0.6.4}/tests/test_cross_service_resolution_flag.py +0 -0
  70. {java_codebase_rag-0.6.2 → java_codebase_rag-0.6.4}/tests/test_edge_navigation_doc.py +0 -0
  71. {java_codebase_rag-0.6.2 → java_codebase_rag-0.6.4}/tests/test_fd_limit.py +0 -0
  72. {java_codebase_rag-0.6.2 → java_codebase_rag-0.6.4}/tests/test_feign_not_exposer.py +0 -0
  73. {java_codebase_rag-0.6.2 → java_codebase_rag-0.6.4}/tests/test_graph_enrich.py +0 -0
  74. {java_codebase_rag-0.6.2 → java_codebase_rag-0.6.4}/tests/test_incremental_graph.py +0 -0
  75. {java_codebase_rag-0.6.2 → java_codebase_rag-0.6.4}/tests/test_installer_integration.py +0 -0
  76. {java_codebase_rag-0.6.2 → java_codebase_rag-0.6.4}/tests/test_ladybug_queries.py +0 -0
  77. {java_codebase_rag-0.6.2 → java_codebase_rag-0.6.4}/tests/test_lance_optimize.py +0 -0
  78. {java_codebase_rag-0.6.2 → java_codebase_rag-0.6.4}/tests/test_lancedb_e2e.py +0 -0
  79. {java_codebase_rag-0.6.2 → java_codebase_rag-0.6.4}/tests/test_mcp_hints.py +0 -0
  80. {java_codebase_rag-0.6.2 → java_codebase_rag-0.6.4}/tests/test_mcp_server_project_root.py +0 -0
  81. {java_codebase_rag-0.6.2 → java_codebase_rag-0.6.4}/tests/test_mcp_tools.py +0 -0
  82. {java_codebase_rag-0.6.2 → java_codebase_rag-0.6.4}/tests/test_mcp_v2.py +0 -0
  83. {java_codebase_rag-0.6.2 → java_codebase_rag-0.6.4}/tests/test_mcp_v2_compose.py +0 -0
  84. {java_codebase_rag-0.6.2 → java_codebase_rag-0.6.4}/tests/test_meta_chain_core.py +0 -0
  85. {java_codebase_rag-0.6.2 → java_codebase_rag-0.6.4}/tests/test_outgoing_call_extraction.py +0 -0
  86. {java_codebase_rag-0.6.2 → java_codebase_rag-0.6.4}/tests/test_packaging_metadata.py +0 -0
  87. {java_codebase_rag-0.6.2 → java_codebase_rag-0.6.4}/tests/test_pr_analysis.py +0 -0
  88. {java_codebase_rag-0.6.2 → java_codebase_rag-0.6.4}/tests/test_resolve_routes_messaging_layer_c.py +0 -0
  89. {java_codebase_rag-0.6.2 → java_codebase_rag-0.6.4}/tests/test_route_extraction.py +0 -0
  90. {java_codebase_rag-0.6.2 → java_codebase_rag-0.6.4}/tests/test_schema_consistency.py +0 -0
  91. {java_codebase_rag-0.6.2 → java_codebase_rag-0.6.4}/tests/test_search_lancedb.py +0 -0
  92. {java_codebase_rag-0.6.2 → java_codebase_rag-0.6.4}/tests/test_search_lancedb_capability.py +0 -0
  93. {java_codebase_rag-0.6.2 → java_codebase_rag-0.6.4}/tests/test_string_value_atoms.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: java-codebase-rag
3
- Version: 0.6.2
3
+ Version: 0.6.4
4
4
  Summary: MCP server for semantic + structural search over Java codebases
5
5
  Author: HumanBean17
6
6
  License-Expression: MIT
@@ -28,6 +28,7 @@ Requires-Dist: pyarrow<24,>=23.0.1
28
28
  Requires-Dist: pydantic<3,>=2.0
29
29
  Requires-Dist: PyYAML<7,>=6.0.3
30
30
  Requires-Dist: questionary<3,>=2.0
31
+ Requires-Dist: rich<15,>=14
31
32
  Requires-Dist: sentence-transformers<6,>=5.4.0
32
33
  Requires-Dist: tree-sitter<0.26,>=0.25.2
33
34
  Requires-Dist: tree-sitter-java<0.24,>=0.23.5
@@ -43,7 +44,7 @@ Dynamic: license-file
43
44
 
44
45
  A graph-native code intelligence layer for Java microservice estates, exposed to LLM agents via the **Model Context Protocol (MCP)**.
45
46
 
46
- The system extracts a deterministic property graph from Java source (tree-sitter), stores it in **Kuzu** (graph) alongside a **LanceDB** vector index (chunks), and exposes a deliberately small MCP surface — **five tools**: `search`, `find`, `describe`, `neighbors`, `resolve` — that collapse onto three primitive agent operations: **locate**, **inspect**, **walk**.
47
+ The system extracts a deterministic property graph from Java source (tree-sitter), stores it in **LadybugDB** (graph) alongside a **LanceDB** vector index (chunks), and exposes a deliberately small MCP surface — **five tools**: `search`, `find`, `describe`, `neighbors`, `resolve` — that collapse onto three primitive agent operations: **locate**, **inspect**, **walk**.
47
48
 
48
49
  > **What this MCP is:** a **GPS for code navigation**, not a reasoning engine.
49
50
  > Agents use a simple loop:
@@ -62,9 +63,9 @@ For the design rationale, the GPS metaphor, and the full ontology, see [`docs/pa
62
63
 
63
64
  Generic code-search tools (grep, ctags, vector-only RAG) hit a ceiling on real Java microservice estates: they find files but lose the structure that makes a Spring/JAX-RS system navigable. This project is built around five choices that target that gap.
64
65
 
65
- - **Hybrid RAG + GraphRAG, not either-or.** Semantic recall (LanceDB chunk vectors) and structural navigation (Kuzu property graph) are composed in one surface. `search` finds candidate nodes by meaning; `neighbors` walks the exact edge you care about (`CALLS`, `IMPLEMENTS`, `INJECTS`, `DECLARES_ROUTE`, …). The agent picks the right primitive per step instead of being forced into pure-vector or pure-symbol search.
66
+ - **Hybrid RAG + GraphRAG, not either-or.** Semantic recall (LanceDB chunk vectors) and structural navigation (LadybugDB property graph) are composed in one surface. `search` finds candidate nodes by meaning; `neighbors` walks the exact edge you care about (`CALLS`, `IMPLEMENTS`, `INJECTS`, `EXPOSES`, …). The agent picks the right primitive per step instead of being forced into pure-vector or pure-symbol search.
66
67
 
67
- - **A Java-tuned role model.** Symbols are labelled with stereotypes inferred from Spring and JAX-RS conventions — `CONTROLLER`, `SERVICE`, `REPOSITORY`, `CLIENT`, `PRODUCER`, `MAPPER`, `DTO`. Agents can ask "list controllers" or "who injects this repository" directly, instead of grep-ing for `@RestController` and hoping for the best. Roles drive both filtering (`find` with a `NodeFilter`) and ranking.
68
+ - **A Java-tuned role model.** Symbols are labelled with stereotypes inferred from Spring and JAX-RS conventions — `CONTROLLER`, `SERVICE`, `REPOSITORY`, `COMPONENT`, `CONFIG`, `ENTITY`, `CLIENT`, `MAPPER`, `DTO`. Agents can ask "list controllers" or "who injects this repository" directly, instead of grep-ing for `@RestController` and hoping for the best. Roles drive both filtering (`find` with a `NodeFilter`) and ranking.
68
69
 
69
70
  - **Ranking specialized for Java codebases.** The composite ranker is aware of role, microservice, and FQN structure — not a generic BM25. A search for `"chat ingress"` surfaces controllers before utility classes; a search scoped to one microservice doesn't drown in matches from the other 19. Defaults are tuned on the bank-chat fixture and exposed in `docs/CONFIGURATION.md` for per-repo overrides.
70
71
 
@@ -106,11 +107,13 @@ java-codebase-rag install --non-interactive --agent claude-code
106
107
 
107
108
  After `pip install --upgrade java-codebase-rag`, run `java-codebase-rag update` to refresh shipped artifacts and catch up the index (Lance + graph).
108
109
 
110
+ All indexing lifecycle commands (`init`, `increment`, `reprocess`, `install`, `update`) show a unified `Vectors → Optimize → Graph` progress bar on stderr during the index build (powered by `rich`); pass `--quiet` to suppress it.
111
+
109
112
  ### Manual registration
110
113
 
111
114
  If you prefer manual configuration, see [`docs/JAVA-CODEBASE-RAG-CLI.md`](./docs/JAVA-CODEBASE-RAG-CLI.md) for the full CLI reference.
112
115
 
113
- > **Stability disclaimer.** This package does **not** promise backward compatibility. MCP tool contracts, env vars, Lance/Kuzu schemas, config files, and Python APIs may change without a deprecation period. Track `main` and rebuild indexes when ontology or embedding settings change.
116
+ > **Stability disclaimer.** This package does **not** promise backward compatibility. MCP tool contracts, env vars, Lance/LadybugDB schemas, config files, and Python APIs may change without a deprecation period. Track `main` and rebuild indexes when ontology or embedding settings change.
114
117
 
115
118
  ---
116
119
 
@@ -123,7 +126,7 @@ This repo ships a small multi-module Spring fixture under [`tests/bank-chat-syst
123
126
  git clone https://github.com/HumanBean17/java-codebase-rag
124
127
  cd java-codebase-rag
125
128
 
126
- # 2. Build the index (Lance vectors + Kuzu graph). First run downloads the
129
+ # 2. Build the index (Lance vectors + LadybugDB graph). First run downloads the
127
130
  # embedding model (~90 MB) and takes ~30-60s on the fixture.
128
131
  java-codebase-rag init --source-root tests/bank-chat-system --index-dir /tmp/bank-chat-index
129
132
 
@@ -138,7 +141,7 @@ Smoke-test the index with two checks (`search_lancedb` ships with the package):
138
141
  JAVA_CODEBASE_RAG_INDEX_DIR=/tmp/bank-chat-index \
139
142
  python -m search_lancedb "chat ingress controller" --table java --limit 3
140
143
 
141
- # Vector + graph expansion — proves Kuzu is wired in
144
+ # Vector + graph expansion — proves LadybugDB is wired in
142
145
  JAVA_CODEBASE_RAG_INDEX_DIR=/tmp/bank-chat-index \
143
146
  python -m search_lancedb "chat ingress controller" --table java --limit 3 \
144
147
  --graph-expand --expand-depth 2
@@ -238,8 +241,8 @@ Run `java-codebase-rag --help` to list grouped subcommands. Operator playbook wi
238
241
  | Setup | `install` | Interactive setup wizard: config, MCP registration, skill/agent deployment, indexing. |
239
242
  | Setup | `update` | Refresh shipped artifacts (skill, agent, MCP entry) + incremental Lance/graph catch-up after pip upgrade. |
240
243
  | Lifecycle | `init` | First-time index. Refuses if artifacts already exist. |
241
- | Lifecycle | `increment` | CocoIndex catch-up + incremental Kuzu update. `--vectors-only` for Lance only. |
242
- | Lifecycle | `reprocess` | Full Lance + Kuzu rebuild. `--vectors-only` / `--graph-only` for a single phase. |
244
+ | Lifecycle | `increment` | CocoIndex catch-up + incremental LadybugDB update. `--vectors-only` for Lance only. |
245
+ | Lifecycle | `reprocess` | Full Lance + LadybugDB rebuild. `--vectors-only` / `--graph-only` for a single phase. |
243
246
  | Lifecycle | `erase` | Delete index artifacts. Requires `--yes` or TTY confirm. |
244
247
  | Introspection | `meta`, `tables`, `diagnose-ignore`, `unresolved-calls` | Health, table listing, ignore-layer diagnostics, receiver-failure call sites. |
245
248
  | Analysis | `analyze-pr` | Blast-radius / risk from a unified diff. |
@@ -274,7 +277,7 @@ python3 -m venv .venv
274
277
 
275
278
  The `cocoindex` package powers lifecycle commands that run the indexer (`init`, `increment`, `reprocess`, `erase`). Search and MCP navigation do not invoke it directly.
276
279
 
277
- The default embedding model is `sentence-transformers/all-MiniLM-L6-v2` (downloaded on first `init`). Override via the `EMBEDDING_MODEL` env var — see [`docs/CONFIGURATION.md` §1](./docs/CONFIGURATION.md#1-environment-variables).
280
+ The default embedding model is `sentence-transformers/all-MiniLM-L6-v2` (downloaded on first `init`). Override via the `SBERT_MODEL` env var — see [`docs/CONFIGURATION.md` §1](./docs/CONFIGURATION.md#1-environment-variables).
278
281
 
279
282
  ---
280
283
 
@@ -2,7 +2,7 @@
2
2
 
3
3
  A graph-native code intelligence layer for Java microservice estates, exposed to LLM agents via the **Model Context Protocol (MCP)**.
4
4
 
5
- The system extracts a deterministic property graph from Java source (tree-sitter), stores it in **Kuzu** (graph) alongside a **LanceDB** vector index (chunks), and exposes a deliberately small MCP surface — **five tools**: `search`, `find`, `describe`, `neighbors`, `resolve` — that collapse onto three primitive agent operations: **locate**, **inspect**, **walk**.
5
+ The system extracts a deterministic property graph from Java source (tree-sitter), stores it in **LadybugDB** (graph) alongside a **LanceDB** vector index (chunks), and exposes a deliberately small MCP surface — **five tools**: `search`, `find`, `describe`, `neighbors`, `resolve` — that collapse onto three primitive agent operations: **locate**, **inspect**, **walk**.
6
6
 
7
7
  > **What this MCP is:** a **GPS for code navigation**, not a reasoning engine.
8
8
  > Agents use a simple loop:
@@ -21,9 +21,9 @@ For the design rationale, the GPS metaphor, and the full ontology, see [`docs/pa
21
21
 
22
22
  Generic code-search tools (grep, ctags, vector-only RAG) hit a ceiling on real Java microservice estates: they find files but lose the structure that makes a Spring/JAX-RS system navigable. This project is built around five choices that target that gap.
23
23
 
24
- - **Hybrid RAG + GraphRAG, not either-or.** Semantic recall (LanceDB chunk vectors) and structural navigation (Kuzu property graph) are composed in one surface. `search` finds candidate nodes by meaning; `neighbors` walks the exact edge you care about (`CALLS`, `IMPLEMENTS`, `INJECTS`, `DECLARES_ROUTE`, …). The agent picks the right primitive per step instead of being forced into pure-vector or pure-symbol search.
24
+ - **Hybrid RAG + GraphRAG, not either-or.** Semantic recall (LanceDB chunk vectors) and structural navigation (LadybugDB property graph) are composed in one surface. `search` finds candidate nodes by meaning; `neighbors` walks the exact edge you care about (`CALLS`, `IMPLEMENTS`, `INJECTS`, `EXPOSES`, …). The agent picks the right primitive per step instead of being forced into pure-vector or pure-symbol search.
25
25
 
26
- - **A Java-tuned role model.** Symbols are labelled with stereotypes inferred from Spring and JAX-RS conventions — `CONTROLLER`, `SERVICE`, `REPOSITORY`, `CLIENT`, `PRODUCER`, `MAPPER`, `DTO`. Agents can ask "list controllers" or "who injects this repository" directly, instead of grep-ing for `@RestController` and hoping for the best. Roles drive both filtering (`find` with a `NodeFilter`) and ranking.
26
+ - **A Java-tuned role model.** Symbols are labelled with stereotypes inferred from Spring and JAX-RS conventions — `CONTROLLER`, `SERVICE`, `REPOSITORY`, `COMPONENT`, `CONFIG`, `ENTITY`, `CLIENT`, `MAPPER`, `DTO`. Agents can ask "list controllers" or "who injects this repository" directly, instead of grep-ing for `@RestController` and hoping for the best. Roles drive both filtering (`find` with a `NodeFilter`) and ranking.
27
27
 
28
28
  - **Ranking specialized for Java codebases.** The composite ranker is aware of role, microservice, and FQN structure — not a generic BM25. A search for `"chat ingress"` surfaces controllers before utility classes; a search scoped to one microservice doesn't drown in matches from the other 19. Defaults are tuned on the bank-chat fixture and exposed in `docs/CONFIGURATION.md` for per-repo overrides.
29
29
 
@@ -65,11 +65,13 @@ java-codebase-rag install --non-interactive --agent claude-code
65
65
 
66
66
  After `pip install --upgrade java-codebase-rag`, run `java-codebase-rag update` to refresh shipped artifacts and catch up the index (Lance + graph).
67
67
 
68
+ All indexing lifecycle commands (`init`, `increment`, `reprocess`, `install`, `update`) show a unified `Vectors → Optimize → Graph` progress bar on stderr during the index build (powered by `rich`); pass `--quiet` to suppress it.
69
+
68
70
  ### Manual registration
69
71
 
70
72
  If you prefer manual configuration, see [`docs/JAVA-CODEBASE-RAG-CLI.md`](./docs/JAVA-CODEBASE-RAG-CLI.md) for the full CLI reference.
71
73
 
72
- > **Stability disclaimer.** This package does **not** promise backward compatibility. MCP tool contracts, env vars, Lance/Kuzu schemas, config files, and Python APIs may change without a deprecation period. Track `main` and rebuild indexes when ontology or embedding settings change.
74
+ > **Stability disclaimer.** This package does **not** promise backward compatibility. MCP tool contracts, env vars, Lance/LadybugDB schemas, config files, and Python APIs may change without a deprecation period. Track `main` and rebuild indexes when ontology or embedding settings change.
73
75
 
74
76
  ---
75
77
 
@@ -82,7 +84,7 @@ This repo ships a small multi-module Spring fixture under [`tests/bank-chat-syst
82
84
  git clone https://github.com/HumanBean17/java-codebase-rag
83
85
  cd java-codebase-rag
84
86
 
85
- # 2. Build the index (Lance vectors + Kuzu graph). First run downloads the
87
+ # 2. Build the index (Lance vectors + LadybugDB graph). First run downloads the
86
88
  # embedding model (~90 MB) and takes ~30-60s on the fixture.
87
89
  java-codebase-rag init --source-root tests/bank-chat-system --index-dir /tmp/bank-chat-index
88
90
 
@@ -97,7 +99,7 @@ Smoke-test the index with two checks (`search_lancedb` ships with the package):
97
99
  JAVA_CODEBASE_RAG_INDEX_DIR=/tmp/bank-chat-index \
98
100
  python -m search_lancedb "chat ingress controller" --table java --limit 3
99
101
 
100
- # Vector + graph expansion — proves Kuzu is wired in
102
+ # Vector + graph expansion — proves LadybugDB is wired in
101
103
  JAVA_CODEBASE_RAG_INDEX_DIR=/tmp/bank-chat-index \
102
104
  python -m search_lancedb "chat ingress controller" --table java --limit 3 \
103
105
  --graph-expand --expand-depth 2
@@ -197,8 +199,8 @@ Run `java-codebase-rag --help` to list grouped subcommands. Operator playbook wi
197
199
  | Setup | `install` | Interactive setup wizard: config, MCP registration, skill/agent deployment, indexing. |
198
200
  | Setup | `update` | Refresh shipped artifacts (skill, agent, MCP entry) + incremental Lance/graph catch-up after pip upgrade. |
199
201
  | Lifecycle | `init` | First-time index. Refuses if artifacts already exist. |
200
- | Lifecycle | `increment` | CocoIndex catch-up + incremental Kuzu update. `--vectors-only` for Lance only. |
201
- | Lifecycle | `reprocess` | Full Lance + Kuzu rebuild. `--vectors-only` / `--graph-only` for a single phase. |
202
+ | Lifecycle | `increment` | CocoIndex catch-up + incremental LadybugDB update. `--vectors-only` for Lance only. |
203
+ | Lifecycle | `reprocess` | Full Lance + LadybugDB rebuild. `--vectors-only` / `--graph-only` for a single phase. |
202
204
  | Lifecycle | `erase` | Delete index artifacts. Requires `--yes` or TTY confirm. |
203
205
  | Introspection | `meta`, `tables`, `diagnose-ignore`, `unresolved-calls` | Health, table listing, ignore-layer diagnostics, receiver-failure call sites. |
204
206
  | Analysis | `analyze-pr` | Blast-radius / risk from a unified diff. |
@@ -233,7 +235,7 @@ python3 -m venv .venv
233
235
 
234
236
  The `cocoindex` package powers lifecycle commands that run the indexer (`init`, `increment`, `reprocess`, `erase`). Search and MCP navigation do not invoke it directly.
235
237
 
236
- The default embedding model is `sentence-transformers/all-MiniLM-L6-v2` (downloaded on first `init`). Override via the `EMBEDDING_MODEL` env var — see [`docs/CONFIGURATION.md` §1](./docs/CONFIGURATION.md#1-environment-variables).
238
+ The default embedding model is `sentence-transformers/all-MiniLM-L6-v2` (downloaded on first `init`). Override via the `SBERT_MODEL` env var — see [`docs/CONFIGURATION.md` §1](./docs/CONFIGURATION.md#1-environment-variables).
237
239
 
238
240
  ---
239
241
 
@@ -14,8 +14,8 @@ from __future__ import annotations
14
14
 
15
15
  import posixpath
16
16
  import sys
17
+ import threading
17
18
  from dataclasses import dataclass, field
18
- from functools import lru_cache
19
19
  from typing import Iterable
20
20
 
21
21
  import tree_sitter_java as _ts_java
@@ -215,10 +215,23 @@ _ANON_SUPER_AS_INTERFACE: frozenset[str] = frozenset({
215
215
  })
216
216
 
217
217
 
218
- @lru_cache(maxsize=1)
218
+ # tree-sitter's ``Parser`` mutates internal state during ``parse()`` and is NOT
219
+ # thread-safe, so each OS thread gets its own instance. ``parse_java`` is called
220
+ # concurrently from worker threads when indexing runs with cocoindex's inflight
221
+ # parallelism — both directly (java_index_flow_lancedb.py: process_java_file
222
+ # offloads parse+enrich to asyncio.to_thread) and transitively
223
+ # (graph_enrich.collect_annotation_meta_chain -> _collect_annotation_decl_index
224
+ # -> parse_java, reached from enrich_chunk). The ``Language`` is immutable and
225
+ # shared; per-thread ``Parser`` construction is lazy and cheap (once per thread),
226
+ # which also preserves parse parallelism instead of serializing it.
227
+ _parser_tls = threading.local()
228
+
229
+
219
230
  def _parser() -> Parser:
220
- lang = Language(_ts_java.language())
221
- return Parser(lang)
231
+ p = getattr(_parser_tls, "parser", None)
232
+ if p is None:
233
+ _parser_tls.parser = p = Parser(Language(_ts_java.language()))
234
+ return p
222
235
 
223
236
 
224
237
  # ---------- dataclasses ----------
@@ -1565,62 +1578,20 @@ def _parse_codebase_http_route_inner_annotation(
1565
1578
  return out
1566
1579
 
1567
1580
 
1568
- def _codebase_route_inner_annotation_nodes(container_ann: Node, src: bytes) -> list[Node]:
1569
- found: list[Node] = []
1570
-
1571
- def visit(n: Node) -> None:
1572
- if n.type == "annotation":
1573
- name_node = n.child_by_field_name("name")
1574
- n_simple = _txt(name_node, src).rsplit(".", 1)[-1] if name_node is not None else ""
1575
- if n_simple == "CodebaseHttpRoute":
1576
- found.append(n)
1577
- for c in n.children:
1578
- visit(c)
1581
+ def _inner_annotation_nodes(container_ann: Node, src: bytes, target_simple: str) -> list[Node]:
1582
+ """Collect nested ``@<target_simple>`` annotations anywhere under ``container_ann``.
1579
1583
 
1580
- visit(container_ann)
1581
- return found
1582
-
1583
-
1584
- def _codebase_async_route_inner_annotation_nodes(container_ann: Node, src: bytes) -> list[Node]:
1585
- found: list[Node] = []
1586
-
1587
- def visit(n: Node) -> None:
1588
- if n.type == "annotation":
1589
- name_node = n.child_by_field_name("name")
1590
- n_simple = _txt(name_node, src).rsplit(".", 1)[-1] if name_node is not None else ""
1591
- if n_simple == "CodebaseAsyncRoute":
1592
- found.append(n)
1593
- for c in n.children:
1594
- visit(c)
1595
-
1596
- visit(container_ann)
1597
- return found
1598
-
1599
-
1600
- def _codebase_http_client_inner_annotation_nodes(container_ann: Node, src: bytes) -> list[Node]:
1601
- found: list[Node] = []
1602
-
1603
- def visit(n: Node) -> None:
1604
- if n.type == "annotation":
1605
- name_node = n.child_by_field_name("name")
1606
- n_simple = _txt(name_node, src).rsplit(".", 1)[-1] if name_node is not None else ""
1607
- if n_simple == "CodebaseHttpClient":
1608
- found.append(n)
1609
- for c in n.children:
1610
- visit(c)
1611
-
1612
- visit(container_ann)
1613
- return found
1614
-
1615
-
1616
- def _codebase_producer_inner_annotation_nodes(container_ann: Node, src: bytes) -> list[Node]:
1584
+ Shared by the four brownfield container walkers — ``CodebaseHttpRoute``,
1585
+ ``CodebaseAsyncRoute``, ``CodebaseHttpClient``, ``CodebaseProducer`` — which
1586
+ differ only by the target annotation simple name.
1587
+ """
1617
1588
  found: list[Node] = []
1618
1589
 
1619
1590
  def visit(n: Node) -> None:
1620
1591
  if n.type == "annotation":
1621
1592
  name_node = n.child_by_field_name("name")
1622
1593
  n_simple = _txt(name_node, src).rsplit(".", 1)[-1] if name_node is not None else ""
1623
- if n_simple == "CodebaseProducer":
1594
+ if n_simple == target_simple:
1624
1595
  found.append(n)
1625
1596
  for c in n.children:
1626
1597
  visit(c)
@@ -1842,7 +1813,7 @@ def _outgoing_calls_from_codebase_http_client_producer_annotations(
1842
1813
  ),
1843
1814
  )
1844
1815
  elif simple == "CodebaseHttpClients":
1845
- for inner in _codebase_http_client_inner_annotation_nodes(ann, src):
1816
+ for inner in _inner_annotation_nodes(ann, src, "CodebaseHttpClient"):
1846
1817
  out.append(
1847
1818
  _parse_codebase_http_client_annotation(
1848
1819
  inner,
@@ -1869,7 +1840,7 @@ def _outgoing_calls_from_codebase_http_client_producer_annotations(
1869
1840
  ),
1870
1841
  )
1871
1842
  elif simple == "CodebaseProducers":
1872
- for inner in _codebase_producer_inner_annotation_nodes(ann, src):
1843
+ for inner in _inner_annotation_nodes(ann, src, "CodebaseProducer"):
1873
1844
  out.append(
1874
1845
  _parse_codebase_producer_annotation(
1875
1846
  inner,
@@ -2343,7 +2314,7 @@ def _collect_routes(
2343
2314
  ),
2344
2315
  )
2345
2316
  elif simple == "CodebaseHttpRoutes":
2346
- for inner in _codebase_route_inner_annotation_nodes(node, src):
2317
+ for inner in _inner_annotation_nodes(node, src, "CodebaseHttpRoute"):
2347
2318
  routes.extend(
2348
2319
  _parse_codebase_http_route_inner_annotation(
2349
2320
  inner,
@@ -2359,7 +2330,7 @@ def _collect_routes(
2359
2330
  elif simple in ("CodebaseAsyncRoute", "CodebaseAsyncRoutes"):
2360
2331
  nodes = [node]
2361
2332
  if simple == "CodebaseAsyncRoutes":
2362
- nodes = list(_codebase_async_route_inner_annotation_nodes(node, src))
2333
+ nodes = list(_inner_annotation_nodes(node, src, "CodebaseAsyncRoute"))
2363
2334
  for ann in nodes:
2364
2335
  pairs, _ = _annotation_kv_nodes(ann, src)
2365
2336
  topic_node = pairs.get("topic")
@@ -25,6 +25,7 @@ The LadybugDB DB is dropped and rebuilt on every run (Phase 1 is a full rebuild)
25
25
  from __future__ import annotations
26
26
 
27
27
  import argparse
28
+ import contextlib
28
29
  import hashlib
29
30
  import json
30
31
  import logging
@@ -84,6 +85,53 @@ def _verbose_stderr_line(content: str) -> None:
84
85
  print(content, file=sys.stderr, flush=True)
85
86
 
86
87
 
88
+ def _emit_graph_progress(parts: dict[str, object], *, verbose: bool) -> None:
89
+ """Emit one ``JCIRAG_PROGRESS kind=graph …`` line to stderr (gated by verbose).
90
+
91
+ The parent process (``pipeline.run_build_ast_graph`` /
92
+ ``run_incremental_graph``) passes ``--verbose`` in default AND verbose modes
93
+ (only suppressed for ``--quiet``), so this structured progress surfaces in
94
+ default mode (where the parent renders it) and verbose mode (raw relay). In
95
+ ``--quiet`` the builder is never invoked with ``--verbose`` so nothing is
96
+ emitted. Field order is fixed so the parser and tests can pin substrings.
97
+ """
98
+ if not verbose:
99
+ return
100
+ fields = ["kind=graph"]
101
+ for key in ("pass", "done", "total", "status", "elapsed_s"):
102
+ if key in parts:
103
+ fields.append(f"{key}={parts[key]}")
104
+ line = "JCIRAG_PROGRESS " + " ".join(fields)
105
+ _verbose_stderr_line(line)
106
+
107
+
108
+ # Pass-1 per-file tick cadence: bound stderr volume on huge trees without making
109
+ # the bar feel stale. A final tick on pass completion carries status=done.
110
+ _PASS1_TICK_EVERY = 25
111
+
112
+
113
+ @contextlib.contextmanager
114
+ def _graph_pass_progress(pass_label: str, *, verbose: bool):
115
+ """Emit ``pass=N/6 status=running`` on entry and ``status=done elapsed_s=…``
116
+ on exit for passes 2–6 (each advances the rendered bar by 1/6).
117
+
118
+ Usage: ``with _graph_pass_progress("2/6", verbose=verbose): …``
119
+ """
120
+ if not verbose:
121
+ yield
122
+ return
123
+ _emit_graph_progress({"pass": pass_label, "status": "running"}, verbose=verbose)
124
+ t0 = time.time()
125
+ try:
126
+ yield
127
+ finally:
128
+ elapsed = time.time() - t0
129
+ _emit_graph_progress(
130
+ {"pass": pass_label, "status": "done", "elapsed_s": f"{elapsed:.2f}"},
131
+ verbose=verbose,
132
+ )
133
+
134
+
87
135
  class _VerbosePassHeartbeats:
88
136
  """Emit ``[tag] running … Ns elapsed`` every 5s on stderr while in scope (verbose only)."""
89
137
 
@@ -837,7 +885,14 @@ def _register_type(
837
885
  return entry
838
886
 
839
887
 
840
- def pass1_parse(root: Path, tables: GraphTables, *, verbose: bool, scope_files: set[str] | None = None) -> dict[str, JavaFileAst]:
888
+ def pass1_parse(
889
+ root: Path,
890
+ tables: GraphTables,
891
+ *,
892
+ verbose: bool,
893
+ scope_files: set[str] | None = None,
894
+ removed_files: set[str] | None = None,
895
+ ) -> dict[str, JavaFileAst]:
841
896
  """Walk files, parse them, populate node indexes. Returns path -> AST.
842
897
 
843
898
  Args:
@@ -845,6 +900,11 @@ def pass1_parse(root: Path, tables: GraphTables, *, verbose: bool, scope_files:
845
900
  tables: GraphTables to populate.
846
901
  verbose: Whether to emit progress output.
847
902
  scope_files: Optional set of relative POSIX paths to parse. If None, parse all files.
903
+ removed_files: Optional set of relative POSIX paths that no longer exist
904
+ on disk (incremental deletions). These are members of ``scope_files``
905
+ (they were deleted, so they participate in scoped deletion) but are
906
+ never visited by the parse walk, so they must be excluded from the
907
+ pass-1 total to keep ``done`` from undercounting then two-way-clamping.
848
908
  """
849
909
  asts: dict[str, JavaFileAst] = {}
850
910
  ignore = LayeredIgnore(root)
@@ -852,6 +912,23 @@ def pass1_parse(root: Path, tables: GraphTables, *, verbose: bool, scope_files:
852
912
  n_files = 0
853
913
  if verbose:
854
914
  _verbose_stderr_line(_PASS1_START)
915
+ # Count-first: one filtered walk (no parsing) to set the EXACT total before
916
+ # the parse loop ticks. Single-layer ignore → the count is exact, so the
917
+ # rendered bar is determinate. For a scoped (incremental) parse the total is
918
+ # the number of files that will actually be visited: scope minus any removed
919
+ # files (which are members of scope for deletion but gone from disk, so the
920
+ # parse walk never ticks them); for a full rebuild it is the non-ignored
921
+ # .java count.
922
+ if verbose:
923
+ if scope_files is not None:
924
+ removed = removed_files if removed_files is not None else set()
925
+ pass1_total = len(scope_files - removed)
926
+ else:
927
+ pass1_total = sum(1 for _ in iter_java_source_files(root, ignore=ignore))
928
+ _emit_graph_progress(
929
+ {"pass": "1/6", "done": 0, "total": pass1_total, "status": "running"},
930
+ verbose=verbose,
931
+ )
855
932
  slow_sec = 0.0
856
933
  raw_slow = os.environ.get("JAVA_CODEBASE_RAG_TEST_GRAPH_SLOW_SEC", "").strip()
857
934
  if raw_slow:
@@ -871,6 +948,11 @@ def pass1_parse(root: Path, tables: GraphTables, *, verbose: bool, scope_files:
871
948
  if scope_files is not None and rel not in scope_files:
872
949
  continue
873
950
  n_files += 1
951
+ if verbose and (n_files % _PASS1_TICK_EVERY == 0):
952
+ _emit_graph_progress(
953
+ {"pass": "1/6", "done": n_files, "status": "running"},
954
+ verbose=verbose,
955
+ )
874
956
  try:
875
957
  content = p.read_bytes()
876
958
  except OSError:
@@ -906,6 +988,10 @@ def pass1_parse(root: Path, tables: GraphTables, *, verbose: bool, scope_files:
906
988
 
907
989
  if verbose:
908
990
  elapsed = time.time() - t0
991
+ _emit_graph_progress(
992
+ {"pass": "1/6", "done": n_files, "status": "done", "elapsed_s": f"{elapsed:.2f}"},
993
+ verbose=verbose,
994
+ )
909
995
  _verbose_stderr_line(
910
996
  f"[graph] pass 1 · parsed {n_files} files in {elapsed:.2f}s: "
911
997
  f"{len(tables.types)} types, {len(tables.members)} members, "
@@ -1145,7 +1231,7 @@ def pass2_edges(tables: GraphTables, asts: dict[str, JavaFileAst], *, verbose: b
1145
1231
  seen_inj: set[tuple[str, str, str, str]] = set()
1146
1232
  if verbose:
1147
1233
  _verbose_stderr_line(_PASS2_START)
1148
- with _VerbosePassHeartbeats("[graph] pass 2", verbose=verbose):
1234
+ with _graph_pass_progress("2/6", verbose=verbose), _VerbosePassHeartbeats("[graph] pass 2", verbose=verbose):
1149
1235
  for fqn, entry in tables.types.items():
1150
1236
  ast = asts.get(entry.file_path)
1151
1237
  if ast is None:
@@ -1818,7 +1904,7 @@ def pass3_calls(tables: GraphTables, asts: dict[str, JavaFileAst], *, verbose: b
1818
1904
  _verbose_stderr_line(_PASS3_START)
1819
1905
  _build_member_indexes(tables)
1820
1906
  stats = CallResolutionStats()
1821
- with _VerbosePassHeartbeats("[graph] pass 3", verbose=verbose):
1907
+ with _graph_pass_progress("3/6", verbose=verbose), _VerbosePassHeartbeats("[graph] pass 3", verbose=verbose):
1822
1908
  for rel_path, file_ast in asts.items():
1823
1909
  try:
1824
1910
  _process_file_calls(file_ast, rel_path, tables, stats)
@@ -1924,8 +2010,21 @@ def _producer_id(
1924
2010
  return f"p:{hashlib.sha1(key.encode()).hexdigest()[:16]}"
1925
2011
 
1926
2012
 
2013
+ # The four brownfield source layers — single source of truth. Consumed by the
2014
+ # client/producer source-layer classifiers, the *_from_brownfield_pct stats
2015
+ # (via brownfield_strategies), and the brownfield_only authoritativeness gate in
2016
+ # _is_brownfield_sourced. codebase_client/codebase_producer are caller-side
2017
+ # declaration strategies, not layers — they extend brownfield_strategies only.
2018
+ _BROWNFIELD_LAYERS = frozenset({
2019
+ "layer_a_meta",
2020
+ "layer_b_ann",
2021
+ "layer_b_fqn",
2022
+ "layer_c_source",
2023
+ })
2024
+
2025
+
1927
2026
  def _client_source_layer(strategy: str) -> str:
1928
- if strategy in {"layer_a_meta", "layer_b_ann", "layer_b_fqn", "layer_c_source"}:
2027
+ if strategy in _BROWNFIELD_LAYERS:
1929
2028
  return strategy
1930
2029
  # Some caller extraction paths emit client kind as strategy; treat those
1931
2030
  # as builtin-source declarations instead of warning on every row.
@@ -1937,7 +2036,7 @@ def _client_source_layer(strategy: str) -> str:
1937
2036
 
1938
2037
 
1939
2038
  def _producer_source_layer(strategy: str) -> str:
1940
- if strategy in {"layer_a_meta", "layer_b_ann", "layer_b_fqn", "layer_c_source"}:
2039
+ if strategy in _BROWNFIELD_LAYERS:
1941
2040
  return strategy
1942
2041
  if strategy in VALID_PRODUCER_KINDS:
1943
2042
  return "builtin"
@@ -1972,7 +2071,7 @@ def pass4_routes(
1972
2071
  meta_chain = collect_annotation_meta_chain(prs)
1973
2072
  if verbose:
1974
2073
  _verbose_stderr_line(_PASS4_START)
1975
- with _VerbosePassHeartbeats("[graph] pass 4", verbose=verbose):
2074
+ with _graph_pass_progress("4/6", verbose=verbose), _VerbosePassHeartbeats("[graph] pass 4", verbose=verbose):
1976
2075
 
1977
2076
  for ast in asts.values():
1978
2077
  stats.routes_skipped_unresolved += ast.routes_skipped_unresolved
@@ -2149,7 +2248,7 @@ def pass5_imperative_edges(
2149
2248
 
2150
2249
  if verbose:
2151
2250
  _verbose_stderr_line(_PASS5_START)
2152
- with _VerbosePassHeartbeats("[graph] pass 5", verbose=verbose):
2251
+ with _graph_pass_progress("5/6", verbose=verbose), _VerbosePassHeartbeats("[graph] pass 5", verbose=verbose):
2153
2252
  for member in sorted(tables.members, key=lambda x: x.node_id):
2154
2253
  if member.decl.is_constructor:
2155
2254
  continue
@@ -2372,15 +2471,14 @@ def pass5_imperative_edges(
2372
2471
  tables.producer_stats.producers_by_kind = defaultdict(int)
2373
2472
  for row in tables.producer_rows:
2374
2473
  tables.producer_stats.producers_by_kind[row.producer_kind] += 1
2375
- brownfield_strategies = frozenset(
2376
- (
2377
- "layer_b_ann",
2378
- "layer_a_meta",
2379
- "layer_c_source",
2380
- "layer_b_fqn",
2381
- "codebase_client",
2382
- "codebase_producer",
2383
- ),
2474
+ # brownfield_strategies = the four brownfield layers plus the two
2475
+ # caller-side declaration strategies (@CodebaseHttpClient /
2476
+ # @CodebaseProducer). These extend _BROWNFIELD_LAYERS deliberately:
2477
+ # the *_from_brownfield_pct stats count annotation-declared callers as
2478
+ # brownfield-sourced even though they are not "layers" and so do not
2479
+ # gate brownfield_only authoritativeness in _is_brownfield_sourced.
2480
+ brownfield_strategies = _BROWNFIELD_LAYERS | frozenset(
2481
+ {"codebase_client", "codebase_producer"},
2384
2482
  )
2385
2483
  if tables.call_edge_stats.http_calls_total:
2386
2484
  n_http = sum(
@@ -2482,14 +2580,6 @@ def _match_call_edge(
2482
2580
  return "cross_service", candidates
2483
2581
 
2484
2582
 
2485
- _BROWNFIELD_LAYERS = frozenset({
2486
- "layer_c_source",
2487
- "layer_b_ann",
2488
- "layer_b_fqn",
2489
- "layer_a_meta",
2490
- })
2491
-
2492
-
2493
2583
  def _is_brownfield_sourced(
2494
2584
  call_strategy: str,
2495
2585
  candidates: list[RouteRow],
@@ -2551,7 +2641,7 @@ def pass6_match_edges(
2551
2641
 
2552
2642
  if verbose:
2553
2643
  _verbose_stderr_line(_PASS6_START)
2554
- with _VerbosePassHeartbeats("[graph] pass 6", verbose=verbose):
2644
+ with _graph_pass_progress("6/6", verbose=verbose), _VerbosePassHeartbeats("[graph] pass 6", verbose=verbose):
2555
2645
  for row in tables.http_call_rows:
2556
2646
  if row.match != "unresolved":
2557
2647
  continue
@@ -3586,7 +3676,9 @@ def incremental_rebuild(
3586
3676
  _verbose_stderr_line("[increment] rebuilding scoped files (passes 1-4)")
3587
3677
 
3588
3678
  tables = GraphTables()
3589
- asts = pass1_parse(source_root, tables, verbose=verbose, scope_files=scope_files)
3679
+ asts = pass1_parse(
3680
+ source_root, tables, verbose=verbose, scope_files=scope_files, removed_files=removed
3681
+ )
3590
3682
 
3591
3683
  # Load existing types and members for cross-file resolution (only from unchanged files)
3592
3684
  _load_existing_types(conn, tables, exclude_files=scope_files)
@@ -23,7 +23,7 @@ import sys
23
23
  from dataclasses import dataclass, field, replace
24
24
  from functools import lru_cache
25
25
  from pathlib import Path
26
- from typing import Any
26
+ from typing import Any, TypeVar
27
27
  from ast_java import (
28
28
  AnnotationRef,
29
29
  JavaFileAst,
@@ -820,7 +820,15 @@ def _route_path_atom(raw_value: str, value_kind: str | None) -> tuple[str, str,
820
820
  return "", "constant_ref", 0.7, False
821
821
 
822
822
 
823
- def _route_hint_lookup(ann: AnnotationRef, hints: dict[str, RouteHint]) -> RouteHint | None:
823
+ _HINT = TypeVar("_HINT")
824
+
825
+
826
+ def _hint_lookup(ann: AnnotationRef, hints: dict[str, _HINT]) -> _HINT | None:
827
+ """Resolve a brownfield hint by qualified name, then simple name, then suffix.
828
+
829
+ Shared by route / http-client / async-producer hint resolution; the three
830
+ former copies differed only in the hint value type.
831
+ """
824
832
  q = ann.qualified.strip()
825
833
  if q in hints:
826
834
  return hints[q]
@@ -1118,7 +1126,7 @@ def resolve_routes_for_method(
1118
1126
 
1119
1127
  # ----- Step 2: Layer B — annotation route hints -----
1120
1128
  for _is_m, ann in combined_anns:
1121
- hint = _route_hint_lookup(ann, overrides.annotation_to_route_hint)
1129
+ hint = _hint_lookup(ann, overrides.annotation_to_route_hint)
1122
1130
  if hint is None:
1123
1131
  continue
1124
1132
  working.append(
@@ -1172,36 +1180,6 @@ def resolve_routes_for_method(
1172
1180
  return working
1173
1181
 
1174
1182
 
1175
- def _client_hint_lookup(
1176
- ann: AnnotationRef,
1177
- hints: dict[str, HttpClientHint],
1178
- ) -> HttpClientHint | None:
1179
- q = ann.qualified.strip()
1180
- if q in hints:
1181
- return hints[q]
1182
- if ann.name in hints:
1183
- return hints[ann.name]
1184
- for k, h in sorted(hints.items(), key=lambda kv: kv[0]):
1185
- if k.endswith("." + ann.name):
1186
- return h
1187
- return None
1188
-
1189
-
1190
- def _async_hint_lookup(
1191
- ann: AnnotationRef,
1192
- hints: dict[str, AsyncProducerHint],
1193
- ) -> AsyncProducerHint | None:
1194
- q = ann.qualified.strip()
1195
- if q in hints:
1196
- return hints[q]
1197
- if ann.name in hints:
1198
- return hints[ann.name]
1199
- for k, h in sorted(hints.items(), key=lambda kv: kv[0]):
1200
- if k.endswith("." + ann.name):
1201
- return h
1202
- return None
1203
-
1204
-
1205
1183
  def _call_from_http_hint(
1206
1184
  *,
1207
1185
  hint: HttpClientHint,
@@ -1296,7 +1274,7 @@ def resolve_http_client_for_method(
1296
1274
  anchor = builtin_http[0] if builtin_http else (layer_c_src[0] if layer_c_src else None)
1297
1275
 
1298
1276
  for _is_m, ann in combined_anns:
1299
- hint = _client_hint_lookup(ann, overrides.annotation_to_http_client_hint)
1277
+ hint = _hint_lookup(ann, overrides.annotation_to_http_client_hint)
1300
1278
  if hint is None:
1301
1279
  continue
1302
1280
  brownfield_calls.append(
@@ -1388,7 +1366,7 @@ def resolve_async_producer_for_method(
1388
1366
  anchor = builtin_async[0] if builtin_async else (layer_c_src[0] if layer_c_src else None)
1389
1367
 
1390
1368
  for _is_m, ann in combined_anns:
1391
- hint = _async_hint_lookup(ann, overrides.annotation_to_async_producer_hint)
1369
+ hint = _hint_lookup(ann, overrides.annotation_to_async_producer_hint)
1392
1370
  if hint is None:
1393
1371
  continue
1394
1372
  brownfield_calls.append(