java-codebase-rag 0.6.2__tar.gz → 0.6.3__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 (92) hide show
  1. {java_codebase_rag-0.6.2/java_codebase_rag.egg-info → java_codebase_rag-0.6.3}/PKG-INFO +4 -1
  2. {java_codebase_rag-0.6.2 → java_codebase_rag-0.6.3}/README.md +2 -0
  3. {java_codebase_rag-0.6.2 → java_codebase_rag-0.6.3}/build_ast_graph.py +95 -7
  4. {java_codebase_rag-0.6.2 → java_codebase_rag-0.6.3}/java_codebase_rag/cli.py +87 -14
  5. {java_codebase_rag-0.6.2 → java_codebase_rag-0.6.3}/java_codebase_rag/cli_format.py +8 -35
  6. {java_codebase_rag-0.6.2 → java_codebase_rag-0.6.3}/java_codebase_rag/cli_progress.py +17 -21
  7. {java_codebase_rag-0.6.2 → java_codebase_rag-0.6.3}/java_codebase_rag/installer.py +180 -43
  8. java_codebase_rag-0.6.3/java_codebase_rag/lance_optimize.py +222 -0
  9. {java_codebase_rag-0.6.2 → java_codebase_rag-0.6.3}/java_codebase_rag/pipeline.py +71 -23
  10. java_codebase_rag-0.6.3/java_codebase_rag/progress.py +570 -0
  11. {java_codebase_rag-0.6.2 → java_codebase_rag-0.6.3/java_codebase_rag.egg-info}/PKG-INFO +4 -1
  12. {java_codebase_rag-0.6.2 → java_codebase_rag-0.6.3}/java_codebase_rag.egg-info/SOURCES.txt +4 -1
  13. {java_codebase_rag-0.6.2 → java_codebase_rag-0.6.3}/java_codebase_rag.egg-info/requires.txt +1 -0
  14. {java_codebase_rag-0.6.2 → java_codebase_rag-0.6.3}/java_index_flow_lancedb.py +155 -0
  15. {java_codebase_rag-0.6.2 → java_codebase_rag-0.6.3}/pyproject.toml +2 -1
  16. {java_codebase_rag-0.6.2 → java_codebase_rag-0.6.3}/server.py +75 -9
  17. {java_codebase_rag-0.6.2 → java_codebase_rag-0.6.3}/tests/test_ast_graph_build.py +136 -0
  18. {java_codebase_rag-0.6.2 → java_codebase_rag-0.6.3}/tests/test_cli_progress_stdout_invariant.py +3 -3
  19. {java_codebase_rag-0.6.2 → java_codebase_rag-0.6.3}/tests/test_installer.py +293 -1
  20. {java_codebase_rag-0.6.2 → java_codebase_rag-0.6.3}/tests/test_java_codebase_rag_cli.py +226 -2
  21. {java_codebase_rag-0.6.2 → java_codebase_rag-0.6.3}/tests/test_microservice_scope.py +118 -0
  22. java_codebase_rag-0.6.3/tests/test_progress.py +463 -0
  23. java_codebase_rag-0.6.3/tests/test_vectors_progress.py +349 -0
  24. java_codebase_rag-0.6.2/java_codebase_rag/lance_optimize.py +0 -148
  25. {java_codebase_rag-0.6.2 → java_codebase_rag-0.6.3}/LICENSE +0 -0
  26. {java_codebase_rag-0.6.2 → java_codebase_rag-0.6.3}/ast_java.py +0 -0
  27. {java_codebase_rag-0.6.2 → java_codebase_rag-0.6.3}/brownfield_events.py +0 -0
  28. {java_codebase_rag-0.6.2 → java_codebase_rag-0.6.3}/chunk_heuristics.py +0 -0
  29. {java_codebase_rag-0.6.2 → java_codebase_rag-0.6.3}/graph_enrich.py +0 -0
  30. {java_codebase_rag-0.6.2 → java_codebase_rag-0.6.3}/index_common.py +0 -0
  31. {java_codebase_rag-0.6.2 → java_codebase_rag-0.6.3}/java_codebase_rag/__init__.py +0 -0
  32. {java_codebase_rag-0.6.2 → java_codebase_rag-0.6.3}/java_codebase_rag/_fdlimit.py +0 -0
  33. {java_codebase_rag-0.6.2 → java_codebase_rag-0.6.3}/java_codebase_rag/config.py +0 -0
  34. {java_codebase_rag-0.6.2 → java_codebase_rag-0.6.3}/java_codebase_rag/install_data/agents/explorer-rag-enhanced.md +0 -0
  35. {java_codebase_rag-0.6.2 → java_codebase_rag-0.6.3}/java_codebase_rag/install_data/skills/explore-codebase/SKILL.md +0 -0
  36. {java_codebase_rag-0.6.2 → java_codebase_rag-0.6.3}/java_codebase_rag.egg-info/dependency_links.txt +0 -0
  37. {java_codebase_rag-0.6.2 → java_codebase_rag-0.6.3}/java_codebase_rag.egg-info/entry_points.txt +0 -0
  38. {java_codebase_rag-0.6.2 → java_codebase_rag-0.6.3}/java_codebase_rag.egg-info/top_level.txt +0 -0
  39. {java_codebase_rag-0.6.2 → java_codebase_rag-0.6.3}/java_index_v1_common.py +0 -0
  40. {java_codebase_rag-0.6.2 → java_codebase_rag-0.6.3}/java_ontology.py +0 -0
  41. {java_codebase_rag-0.6.2 → java_codebase_rag-0.6.3}/ladybug_queries.py +0 -0
  42. {java_codebase_rag-0.6.2 → java_codebase_rag-0.6.3}/mcp_hints.py +0 -0
  43. {java_codebase_rag-0.6.2 → java_codebase_rag-0.6.3}/mcp_v2.py +0 -0
  44. {java_codebase_rag-0.6.2 → java_codebase_rag-0.6.3}/path_filtering.py +0 -0
  45. {java_codebase_rag-0.6.2 → java_codebase_rag-0.6.3}/pr_analysis.py +0 -0
  46. {java_codebase_rag-0.6.2 → java_codebase_rag-0.6.3}/search_lancedb.py +0 -0
  47. {java_codebase_rag-0.6.2 → java_codebase_rag-0.6.3}/setup.cfg +0 -0
  48. {java_codebase_rag-0.6.2 → java_codebase_rag-0.6.3}/tests/test_agent_skills_static.py +0 -0
  49. {java_codebase_rag-0.6.2 → java_codebase_rag-0.6.3}/tests/test_assign_endpoint_client_extraction.py +0 -0
  50. {java_codebase_rag-0.6.2 → java_codebase_rag-0.6.3}/tests/test_ast_java_calls.py +0 -0
  51. {java_codebase_rag-0.6.2 → java_codebase_rag-0.6.3}/tests/test_ast_java_capabilities.py +0 -0
  52. {java_codebase_rag-0.6.2 → java_codebase_rag-0.6.3}/tests/test_bank_chat_brownfield_integration.py +0 -0
  53. {java_codebase_rag-0.6.2 → java_codebase_rag-0.6.3}/tests/test_brownfield_clients.py +0 -0
  54. {java_codebase_rag-0.6.2 → java_codebase_rag-0.6.3}/tests/test_brownfield_events.py +0 -0
  55. {java_codebase_rag-0.6.2 → java_codebase_rag-0.6.3}/tests/test_brownfield_overrides.py +0 -0
  56. {java_codebase_rag-0.6.2 → java_codebase_rag-0.6.3}/tests/test_brownfield_routes.py +0 -0
  57. {java_codebase_rag-0.6.2 → java_codebase_rag-0.6.3}/tests/test_call_edge_matching.py +0 -0
  58. {java_codebase_rag-0.6.2 → java_codebase_rag-0.6.3}/tests/test_call_edges_e2e.py +0 -0
  59. {java_codebase_rag-0.6.2 → java_codebase_rag-0.6.3}/tests/test_call_graph_receiver_resolution.py +0 -0
  60. {java_codebase_rag-0.6.2 → java_codebase_rag-0.6.3}/tests/test_call_graph_smoke_roundtrip.py +0 -0
  61. {java_codebase_rag-0.6.2 → java_codebase_rag-0.6.3}/tests/test_call_invariant.py +0 -0
  62. {java_codebase_rag-0.6.2 → java_codebase_rag-0.6.3}/tests/test_cli_quiet_parity.py +0 -0
  63. {java_codebase_rag-0.6.2 → java_codebase_rag-0.6.3}/tests/test_client_hint_recovery.py +0 -0
  64. {java_codebase_rag-0.6.2 → java_codebase_rag-0.6.3}/tests/test_client_node_extraction.py +0 -0
  65. {java_codebase_rag-0.6.2 → java_codebase_rag-0.6.3}/tests/test_client_role_rename.py +0 -0
  66. {java_codebase_rag-0.6.2 → java_codebase_rag-0.6.3}/tests/test_config.py +0 -0
  67. {java_codebase_rag-0.6.2 → java_codebase_rag-0.6.3}/tests/test_cross_service_resolution_flag.py +0 -0
  68. {java_codebase_rag-0.6.2 → java_codebase_rag-0.6.3}/tests/test_edge_navigation_doc.py +0 -0
  69. {java_codebase_rag-0.6.2 → java_codebase_rag-0.6.3}/tests/test_fd_limit.py +0 -0
  70. {java_codebase_rag-0.6.2 → java_codebase_rag-0.6.3}/tests/test_feign_not_exposer.py +0 -0
  71. {java_codebase_rag-0.6.2 → java_codebase_rag-0.6.3}/tests/test_graph_enrich.py +0 -0
  72. {java_codebase_rag-0.6.2 → java_codebase_rag-0.6.3}/tests/test_incremental_graph.py +0 -0
  73. {java_codebase_rag-0.6.2 → java_codebase_rag-0.6.3}/tests/test_installer_integration.py +0 -0
  74. {java_codebase_rag-0.6.2 → java_codebase_rag-0.6.3}/tests/test_ladybug_queries.py +0 -0
  75. {java_codebase_rag-0.6.2 → java_codebase_rag-0.6.3}/tests/test_lance_optimize.py +0 -0
  76. {java_codebase_rag-0.6.2 → java_codebase_rag-0.6.3}/tests/test_lancedb_e2e.py +0 -0
  77. {java_codebase_rag-0.6.2 → java_codebase_rag-0.6.3}/tests/test_mcp_hints.py +0 -0
  78. {java_codebase_rag-0.6.2 → java_codebase_rag-0.6.3}/tests/test_mcp_server_project_root.py +0 -0
  79. {java_codebase_rag-0.6.2 → java_codebase_rag-0.6.3}/tests/test_mcp_tools.py +0 -0
  80. {java_codebase_rag-0.6.2 → java_codebase_rag-0.6.3}/tests/test_mcp_v2.py +0 -0
  81. {java_codebase_rag-0.6.2 → java_codebase_rag-0.6.3}/tests/test_mcp_v2_compose.py +0 -0
  82. {java_codebase_rag-0.6.2 → java_codebase_rag-0.6.3}/tests/test_meta_chain_core.py +0 -0
  83. {java_codebase_rag-0.6.2 → java_codebase_rag-0.6.3}/tests/test_outgoing_call_extraction.py +0 -0
  84. {java_codebase_rag-0.6.2 → java_codebase_rag-0.6.3}/tests/test_packaging_metadata.py +0 -0
  85. {java_codebase_rag-0.6.2 → java_codebase_rag-0.6.3}/tests/test_path_filtering.py +0 -0
  86. {java_codebase_rag-0.6.2 → java_codebase_rag-0.6.3}/tests/test_pr_analysis.py +0 -0
  87. {java_codebase_rag-0.6.2 → java_codebase_rag-0.6.3}/tests/test_resolve_routes_messaging_layer_c.py +0 -0
  88. {java_codebase_rag-0.6.2 → java_codebase_rag-0.6.3}/tests/test_route_extraction.py +0 -0
  89. {java_codebase_rag-0.6.2 → java_codebase_rag-0.6.3}/tests/test_schema_consistency.py +0 -0
  90. {java_codebase_rag-0.6.2 → java_codebase_rag-0.6.3}/tests/test_search_lancedb.py +0 -0
  91. {java_codebase_rag-0.6.2 → java_codebase_rag-0.6.3}/tests/test_search_lancedb_capability.py +0 -0
  92. {java_codebase_rag-0.6.2 → java_codebase_rag-0.6.3}/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.3
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
@@ -106,6 +107,8 @@ 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.
@@ -65,6 +65,8 @@ 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.
@@ -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)
@@ -1972,7 +2058,7 @@ def pass4_routes(
1972
2058
  meta_chain = collect_annotation_meta_chain(prs)
1973
2059
  if verbose:
1974
2060
  _verbose_stderr_line(_PASS4_START)
1975
- with _VerbosePassHeartbeats("[graph] pass 4", verbose=verbose):
2061
+ with _graph_pass_progress("4/6", verbose=verbose), _VerbosePassHeartbeats("[graph] pass 4", verbose=verbose):
1976
2062
 
1977
2063
  for ast in asts.values():
1978
2064
  stats.routes_skipped_unresolved += ast.routes_skipped_unresolved
@@ -2149,7 +2235,7 @@ def pass5_imperative_edges(
2149
2235
 
2150
2236
  if verbose:
2151
2237
  _verbose_stderr_line(_PASS5_START)
2152
- with _VerbosePassHeartbeats("[graph] pass 5", verbose=verbose):
2238
+ with _graph_pass_progress("5/6", verbose=verbose), _VerbosePassHeartbeats("[graph] pass 5", verbose=verbose):
2153
2239
  for member in sorted(tables.members, key=lambda x: x.node_id):
2154
2240
  if member.decl.is_constructor:
2155
2241
  continue
@@ -2551,7 +2637,7 @@ def pass6_match_edges(
2551
2637
 
2552
2638
  if verbose:
2553
2639
  _verbose_stderr_line(_PASS6_START)
2554
- with _VerbosePassHeartbeats("[graph] pass 6", verbose=verbose):
2640
+ with _graph_pass_progress("6/6", verbose=verbose), _VerbosePassHeartbeats("[graph] pass 6", verbose=verbose):
2555
2641
  for row in tables.http_call_rows:
2556
2642
  if row.match != "unresolved":
2557
2643
  continue
@@ -3586,7 +3672,9 @@ def incremental_rebuild(
3586
3672
  _verbose_stderr_line("[increment] rebuilding scoped files (passes 1-4)")
3587
3673
 
3588
3674
  tables = GraphTables()
3589
- asts = pass1_parse(source_root, tables, verbose=verbose, scope_files=scope_files)
3675
+ asts = pass1_parse(
3676
+ source_root, tables, verbose=verbose, scope_files=scope_files, removed_files=removed
3677
+ )
3590
3678
 
3591
3679
  # Load existing types and members for cross-file resolution (only from unchanged files)
3592
3680
  _load_existing_types(conn, tables, exclude_files=scope_files)
@@ -131,15 +131,41 @@ def _run_with_pipeline_progress(
131
131
  cfg: ResolvedOperatorConfig,
132
132
  *,
133
133
  quiet: bool,
134
- work: Callable[[], int],
134
+ verbose: bool = False,
135
+ work: Callable[["PipelineProgress | None"], int],
135
136
  ) -> int:
136
- if quiet:
137
- return int(work())
137
+ """Run ``work`` under the unified progress renderer (default TTY mode only).
138
+
139
+ ``work`` receives a :class:`PipelineProgress` whose ``on_progress`` callback
140
+ should be forwarded to the graph/vectors pipeline helpers so their
141
+ ``JCIRAG_PROGRESS`` events feed the renderer. In ``--quiet`` or ``--verbose``
142
+ mode the context is ``None`` (no Live region: quiet is silent, verbose
143
+ raw-relays subprocess output).
144
+ """
145
+ if quiet or verbose:
146
+ return int(work(None))
147
+ from java_codebase_rag.progress import build_index_progress_context
148
+
149
+ # PR-3 owns all three tasks in order: Vectors → Optimize → Graph. The vectors
150
+ # task is fed by the cocoindex child's per-file ticks + approximate total
151
+ # (subprocess transport, parsed by ProgressRelay); the optimize task is fed
152
+ # in-process by lance_optimize; the graph task is fed by the build_ast_graph
153
+ # child (subprocess transport). A task only becomes visible/running once its
154
+ # first event arrives.
155
+ renderer, on_progress, console = build_index_progress_context()
156
+ progress = PipelineProgress(renderer=renderer)
157
+ progress.on_progress = on_progress
158
+ progress.console = console
159
+
138
160
  _pipeline_header(subcommand, cfg)
139
161
  t0 = time.perf_counter()
140
162
  code = 0
163
+ # start() always flips _started (the non-TTY fallback is a no-op for Live but
164
+ # still needs the flag so apply() routes to the concise-line printer). The
165
+ # TTY Live region is entered inside start() only when the console is a TTY.
166
+ renderer.start()
141
167
  try:
142
- code = int(work())
168
+ code = int(work(progress))
143
169
  return code
144
170
  except BaseException as exc:
145
171
  # Keep footer aligned with process outcome (main maps unhandled Exception -> exit 2).
@@ -155,9 +181,26 @@ def _run_with_pipeline_progress(
155
181
  code = 2
156
182
  raise
157
183
  finally:
184
+ renderer.stop()
158
185
  _pipeline_footer(subcommand, t0, code)
159
186
 
160
187
 
188
+ class PipelineProgress:
189
+ """Progress context handed to ``work``: the renderer + a ready ``on_progress``.
190
+
191
+ ``on_progress``/``console`` are wired by :func:`_run_with_pipeline_progress`
192
+ and should be forwarded to the pipeline helpers' ``on_progress`` /
193
+ ``on_progress_console`` parameters. ``console`` is the renderer's stderr
194
+ ``rich.Console`` so the subprocess drain routes non-progress lines through
195
+ ``console.print`` while the Live region is up (single-writer invariant).
196
+ """
197
+
198
+ def __init__(self, *, renderer: "object | None") -> None:
199
+ self.renderer = renderer
200
+ self.on_progress: "Callable | None" = None
201
+ self.console: "object | None" = None
202
+
203
+
161
204
  def _jsonable(value: Any) -> Any:
162
205
  if hasattr(value, "model_dump"):
163
206
  return value.model_dump()
@@ -266,7 +309,7 @@ def _cmd_init(args: argparse.Namespace) -> int:
266
309
  return 2
267
310
  cfg.index_dir.mkdir(parents=True, exist_ok=True)
268
311
 
269
- def work() -> int:
312
+ def work(progress: "PipelineProgress | None") -> int:
270
313
  env = cfg.subprocess_env()
271
314
  verbose = bool(args.verbose)
272
315
  coco = run_cocoindex_update(
@@ -275,6 +318,8 @@ def _cmd_init(args: argparse.Namespace) -> int:
275
318
  quiet=bool(args.quiet),
276
319
  verbose=verbose,
277
320
  lance_project_root=None if args.quiet else cfg.source_root,
321
+ on_progress=progress.on_progress if progress is not None else None,
322
+ on_progress_console=progress.console if progress is not None else None,
278
323
  )
279
324
  if coco.returncode != 0:
280
325
  _emit(
@@ -295,6 +340,8 @@ def _cmd_init(args: argparse.Namespace) -> int:
295
340
  verbose=verbose,
296
341
  quiet=bool(args.quiet),
297
342
  env=env,
343
+ on_progress=progress.on_progress if progress is not None else None,
344
+ on_progress_console=progress.console if progress is not None else None,
298
345
  )
299
346
  if g.returncode != 0:
300
347
  _emit(
@@ -310,7 +357,9 @@ def _cmd_init(args: argparse.Namespace) -> int:
310
357
  _emit({"success": True, "message": "init completed"})
311
358
  return 0
312
359
 
313
- return _run_with_pipeline_progress("init", cfg, quiet=bool(args.quiet), work=work)
360
+ return _run_with_pipeline_progress(
361
+ "init", cfg, quiet=bool(args.quiet), verbose=bool(args.verbose), work=work
362
+ )
314
363
 
315
364
 
316
365
  def _cmd_increment(args: argparse.Namespace) -> int:
@@ -323,7 +372,7 @@ def _cmd_increment(args: argparse.Namespace) -> int:
323
372
  if vectors_only:
324
373
  _emit_increment_ladybug_warning()
325
374
 
326
- def work() -> int:
375
+ def work(progress: "PipelineProgress | None") -> int:
327
376
  env = cfg.subprocess_env()
328
377
  coco = run_cocoindex_update(
329
378
  env,
@@ -331,6 +380,8 @@ def _cmd_increment(args: argparse.Namespace) -> int:
331
380
  quiet=bool(args.quiet),
332
381
  verbose=bool(args.verbose),
333
382
  lance_project_root=None if args.quiet else cfg.source_root,
383
+ on_progress=progress.on_progress if progress is not None else None,
384
+ on_progress_console=progress.console if progress is not None else None,
334
385
  )
335
386
  if coco.returncode != 0:
336
387
  _emit(
@@ -356,6 +407,8 @@ def _cmd_increment(args: argparse.Namespace) -> int:
356
407
  verbose=bool(args.verbose),
357
408
  quiet=bool(args.quiet),
358
409
  env=env,
410
+ on_progress=progress.on_progress if progress is not None else None,
411
+ on_progress_console=progress.console if progress is not None else None,
359
412
  )
360
413
 
361
414
  # Check if incremental fell back to full rebuild
@@ -389,7 +442,9 @@ def _cmd_increment(args: argparse.Namespace) -> int:
389
442
  _emit({"success": True, "message": "increment completed (Lance + graph updated)"})
390
443
  return 0
391
444
 
392
- return _run_with_pipeline_progress("increment", cfg, quiet=bool(args.quiet), work=work)
445
+ return _run_with_pipeline_progress(
446
+ "increment", cfg, quiet=bool(args.quiet), verbose=bool(args.verbose), work=work
447
+ )
393
448
 
394
449
 
395
450
  def _cmd_reprocess(args: argparse.Namespace) -> int:
@@ -397,14 +452,18 @@ def _cmd_reprocess(args: argparse.Namespace) -> int:
397
452
  _startup_hints(cfg)
398
453
  cfg.apply_to_os_environ()
399
454
 
400
- def work() -> int:
455
+ def work(progress: "PipelineProgress | None") -> int:
401
456
  env = cfg.subprocess_env()
402
457
  verbose = bool(args.verbose)
403
458
  vectors_only = bool(getattr(args, "vectors_only", False))
404
459
  graph_only = bool(getattr(args, "graph_only", False))
405
460
 
406
461
  if vectors_only:
407
- coco = run_cocoindex_update(env, full_reprocess=True, quiet=bool(args.quiet), verbose=verbose)
462
+ coco = run_cocoindex_update(
463
+ env, full_reprocess=True, quiet=bool(args.quiet), verbose=verbose,
464
+ on_progress=progress.on_progress if progress is not None else None,
465
+ on_progress_console=progress.console if progress is not None else None,
466
+ )
408
467
  if _is_cocoindex_preflight_blocker(coco):
409
468
  payload: dict[str, Any] = {
410
469
  "success": False,
@@ -443,6 +502,8 @@ def _cmd_reprocess(args: argparse.Namespace) -> int:
443
502
  verbose=verbose,
444
503
  quiet=bool(args.quiet),
445
504
  env=env,
505
+ on_progress=progress.on_progress if progress is not None else None,
506
+ on_progress_console=progress.console if progress is not None else None,
446
507
  )
447
508
  if _is_graph_preflight_blocker(g):
448
509
  payload = {
@@ -477,12 +538,21 @@ def _cmd_reprocess(args: argparse.Namespace) -> int:
477
538
 
478
539
  import server # lazy: pulls sentence_transformers/torch/lancedb/kuzu
479
540
 
480
- result = asyncio.run(server.run_refresh_pipeline(quiet=bool(args.quiet), verbose=verbose))
541
+ result = asyncio.run(
542
+ server.run_refresh_pipeline(
543
+ quiet=bool(args.quiet),
544
+ verbose=verbose,
545
+ on_progress=progress.on_progress if progress is not None else None,
546
+ on_progress_console=progress.console if progress is not None else None,
547
+ )
548
+ )
481
549
  payload = result.model_dump()
482
550
  _emit_reprocess_outcome(payload)
483
551
  return _reprocess_exit_code(payload)
484
552
 
485
- return _run_with_pipeline_progress("reprocess", cfg, quiet=bool(args.quiet), work=work)
553
+ return _run_with_pipeline_progress(
554
+ "reprocess", cfg, quiet=bool(args.quiet), verbose=bool(args.verbose), work=work
555
+ )
486
556
 
487
557
 
488
558
  def _cmd_install(args: argparse.Namespace) -> int:
@@ -495,6 +565,7 @@ def _cmd_install(args: argparse.Namespace) -> int:
495
565
  model=args.model,
496
566
  source_root=None, # None means cwd; installer confirms interactively
497
567
  quiet=bool(args.quiet),
568
+ verbose=bool(args.verbose),
498
569
  )
499
570
 
500
571
 
@@ -504,6 +575,8 @@ def _cmd_update(args: argparse.Namespace) -> int:
504
575
  return run_update(
505
576
  force=bool(args.force),
506
577
  dry_run=bool(args.dry_run),
578
+ quiet=bool(args.quiet),
579
+ verbose=bool(args.verbose),
507
580
  )
508
581
 
509
582
 
@@ -537,7 +610,7 @@ def _cmd_erase(args: argparse.Namespace) -> int:
537
610
  print("Aborted.", file=sys.stderr)
538
611
  return 2
539
612
 
540
- def work() -> int:
613
+ def work(progress: "PipelineProgress | None") -> int:
541
614
  env = cfg.subprocess_env()
542
615
  drop = run_cocoindex_drop(env, quiet=bool(args.quiet))
543
616
  if drop.returncode == 127:
@@ -570,7 +643,7 @@ def _cmd_erase(args: argparse.Namespace) -> int:
570
643
  _emit({"success": True, "message": "erase completed"})
571
644
  return 0
572
645
 
573
- return _run_with_pipeline_progress("erase", cfg, quiet=bool(args.quiet), work=work)
646
+ return _run_with_pipeline_progress("erase", cfg, quiet=bool(args.quiet), verbose=bool(getattr(args, "verbose", False)), work=work)
574
647
 
575
648
 
576
649
  def _cmd_meta(args: argparse.Namespace) -> int:
@@ -1,10 +1,7 @@
1
1
  """TTY-aware ANSI formatting for CLI stderr progress."""
2
2
  from __future__ import annotations
3
3
 
4
- import itertools
5
4
  import sys
6
- import threading
7
- import time
8
5
 
9
6
  _RESET = "\033[0m"
10
7
  _BOLD = "\033[1m"
@@ -16,8 +13,6 @@ _CYAN = "\033[36m"
16
13
  CHECK = "✓"
17
14
  CROSS = "✗"
18
15
 
19
- _SPINNER_FRAMES = "⠋⠙⠹⠸⠼⠴⠦⠧⠇⠏"
20
-
21
16
  _NOISE_CONTAINS: tuple[bytes, ...] = (
22
17
  b"lance::",
23
18
  b"FutureWarning",
@@ -25,6 +20,14 @@ _NOISE_CONTAINS: tuple[bytes, ...] = (
25
20
  b'"event": "brownfield-',
26
21
  b"unknown producer source strategy",
27
22
  b"unknown client source strategy",
23
+ # Builder verbose heartbeats / pass banners: in default mode the renderer's
24
+ # bar subsumes these, so they must NOT also appear as raw lines above the
25
+ # Live region. --verbose raw-relay bypasses this filter and still shows them.
26
+ b"[graph] pass ",
27
+ b"[graph] scoped write ",
28
+ b"[graph] writing ",
29
+ b"[graph] done ",
30
+ b"[increment] ",
28
31
  )
29
32
 
30
33
 
@@ -80,33 +83,3 @@ def styled_check() -> str:
80
83
 
81
84
  def styled_cross() -> str:
82
85
  return red(CROSS) if stderr_is_tty() else CROSS
83
-
84
-
85
- class Spinner:
86
- """Braille spinner that overwrites the current stderr line until stopped."""
87
-
88
- def __init__(self, label: str) -> None:
89
- self._label = label
90
- self._stop = threading.Event()
91
- self._thread: threading.Thread | None = None
92
-
93
- def start(self) -> None:
94
- self._thread = threading.Thread(target=self._run, name="spinner", daemon=True)
95
- self._thread.start()
96
-
97
- def stop(self) -> None:
98
- self._stop.set()
99
- if self._thread is not None:
100
- self._thread.join(timeout=2.0)
101
- sys.stderr.buffer.write(b"\r\x1b[2K")
102
- sys.stderr.buffer.flush()
103
-
104
- def _run(self) -> None:
105
- frames = itertools.cycle(_SPINNER_FRAMES)
106
- t0 = time.monotonic()
107
- while not self._stop.wait(0.3):
108
- elapsed = time.monotonic() - t0
109
- frame = next(frames)
110
- line = f"\r{frame} {self._label} · {elapsed:.0f}s"
111
- sys.stderr.buffer.write(line.encode())
112
- sys.stderr.buffer.flush()
@@ -3,26 +3,10 @@ from __future__ import annotations
3
3
 
4
4
  import asyncio
5
5
  import sys
6
+ from typing import Callable
6
7
 
7
- from java_codebase_rag.cli_format import bold_cyan, is_noise_line, styled_check, styled_cross
8
-
9
-
10
- def emit_vectors_start() -> None:
11
- print(
12
- bold_cyan("[vectors]") + " running · cocoindex update",
13
- file=sys.stderr,
14
- flush=True,
15
- )
16
-
17
-
18
- def emit_vectors_finish(*, elapsed_s: float, exit_code: int) -> None:
19
- marker = styled_check() if exit_code == 0 else styled_cross()
20
- print(
21
- f"{marker} {bold_cyan('[vectors]')} finished · {elapsed_s:.2f}s"
22
- + (f" (exit={exit_code})" if exit_code != 0 else ""),
23
- file=sys.stderr,
24
- flush=True,
25
- )
8
+ from java_codebase_rag.cli_format import is_noise_line
9
+ from java_codebase_rag.progress import ProgressEvent, make_relay
26
10
 
27
11
 
28
12
  class _AsyncLineFilter:
@@ -61,8 +45,15 @@ async def accumulate_and_relay_subprocess_streams(
61
45
  *,
62
46
  relay: bool,
63
47
  verbose: bool = True,
48
+ on_progress: Callable[[ProgressEvent], None] | None = None,
49
+ on_progress_console: object | None = None,
64
50
  ) -> tuple[bytes, bytes]:
65
- """Read stdout and stderr until EOF; optionally copy non-noise stderr chunks to stderr."""
51
+ """Read stdout and stderr until EOF; optionally copy non-noise stderr chunks to stderr.
52
+
53
+ When ``on_progress`` is set, stderr is drained through a :class:`ProgressRelay`
54
+ so ``JCIRAG_PROGRESS`` lines are parsed and routed to ``on_progress`` (and
55
+ suppressed from the relay), matching the sync ``pipeline._popen_capturing_stderr``.
56
+ """
66
57
  stdout = proc.stdout
67
58
  stderr = proc.stderr
68
59
  if stdout is None or stderr is None:
@@ -70,7 +61,12 @@ async def accumulate_and_relay_subprocess_streams(
70
61
 
71
62
  out_buf = bytearray()
72
63
  err_buf = bytearray()
73
- filt = _AsyncLineFilter() if (relay and not verbose) else None
64
+ if on_progress is not None:
65
+ filt = make_relay(on_progress, console=on_progress_console, verbose=verbose)
66
+ elif relay and not verbose:
67
+ filt = _AsyncLineFilter()
68
+ else:
69
+ filt = None
74
70
 
75
71
  async def drain_stdout(reader: asyncio.StreamReader, target: bytearray) -> None:
76
72
  while True: