java-codebase-rag 0.6.1__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 (93) hide show
  1. {java_codebase_rag-0.6.1/java_codebase_rag.egg-info → java_codebase_rag-0.6.3}/PKG-INFO +4 -1
  2. {java_codebase_rag-0.6.1 → java_codebase_rag-0.6.3}/README.md +2 -0
  3. {java_codebase_rag-0.6.1 → java_codebase_rag-0.6.3}/build_ast_graph.py +95 -7
  4. {java_codebase_rag-0.6.1 → java_codebase_rag-0.6.3}/java_codebase_rag/cli.py +105 -15
  5. {java_codebase_rag-0.6.1 → java_codebase_rag-0.6.3}/java_codebase_rag/cli_format.py +8 -35
  6. {java_codebase_rag-0.6.1 → java_codebase_rag-0.6.3}/java_codebase_rag/cli_progress.py +17 -21
  7. {java_codebase_rag-0.6.1 → java_codebase_rag-0.6.3}/java_codebase_rag/config.py +54 -5
  8. {java_codebase_rag-0.6.1 → java_codebase_rag-0.6.3}/java_codebase_rag/installer.py +192 -45
  9. java_codebase_rag-0.6.3/java_codebase_rag/lance_optimize.py +222 -0
  10. {java_codebase_rag-0.6.1 → java_codebase_rag-0.6.3}/java_codebase_rag/pipeline.py +71 -23
  11. java_codebase_rag-0.6.3/java_codebase_rag/progress.py +570 -0
  12. {java_codebase_rag-0.6.1 → java_codebase_rag-0.6.3/java_codebase_rag.egg-info}/PKG-INFO +4 -1
  13. {java_codebase_rag-0.6.1 → java_codebase_rag-0.6.3}/java_codebase_rag.egg-info/SOURCES.txt +4 -1
  14. java_codebase_rag-0.6.3/java_codebase_rag.egg-info/entry_points.txt +3 -0
  15. {java_codebase_rag-0.6.1 → java_codebase_rag-0.6.3}/java_codebase_rag.egg-info/requires.txt +1 -0
  16. {java_codebase_rag-0.6.1 → java_codebase_rag-0.6.3}/java_index_flow_lancedb.py +155 -0
  17. {java_codebase_rag-0.6.1 → java_codebase_rag-0.6.3}/pyproject.toml +3 -2
  18. {java_codebase_rag-0.6.1 → java_codebase_rag-0.6.3}/server.py +75 -9
  19. {java_codebase_rag-0.6.1 → java_codebase_rag-0.6.3}/tests/test_ast_graph_build.py +136 -0
  20. {java_codebase_rag-0.6.1 → java_codebase_rag-0.6.3}/tests/test_cli_progress_stdout_invariant.py +3 -3
  21. {java_codebase_rag-0.6.1 → java_codebase_rag-0.6.3}/tests/test_config.py +149 -0
  22. {java_codebase_rag-0.6.1 → java_codebase_rag-0.6.3}/tests/test_installer.py +427 -0
  23. {java_codebase_rag-0.6.1 → java_codebase_rag-0.6.3}/tests/test_java_codebase_rag_cli.py +282 -2
  24. {java_codebase_rag-0.6.1 → java_codebase_rag-0.6.3}/tests/test_microservice_scope.py +118 -0
  25. java_codebase_rag-0.6.3/tests/test_progress.py +463 -0
  26. java_codebase_rag-0.6.3/tests/test_vectors_progress.py +349 -0
  27. java_codebase_rag-0.6.1/java_codebase_rag/lance_optimize.py +0 -148
  28. java_codebase_rag-0.6.1/java_codebase_rag.egg-info/entry_points.txt +0 -3
  29. {java_codebase_rag-0.6.1 → java_codebase_rag-0.6.3}/LICENSE +0 -0
  30. {java_codebase_rag-0.6.1 → java_codebase_rag-0.6.3}/ast_java.py +0 -0
  31. {java_codebase_rag-0.6.1 → java_codebase_rag-0.6.3}/brownfield_events.py +0 -0
  32. {java_codebase_rag-0.6.1 → java_codebase_rag-0.6.3}/chunk_heuristics.py +0 -0
  33. {java_codebase_rag-0.6.1 → java_codebase_rag-0.6.3}/graph_enrich.py +0 -0
  34. {java_codebase_rag-0.6.1 → java_codebase_rag-0.6.3}/index_common.py +0 -0
  35. {java_codebase_rag-0.6.1 → java_codebase_rag-0.6.3}/java_codebase_rag/__init__.py +0 -0
  36. {java_codebase_rag-0.6.1 → java_codebase_rag-0.6.3}/java_codebase_rag/_fdlimit.py +0 -0
  37. {java_codebase_rag-0.6.1 → java_codebase_rag-0.6.3}/java_codebase_rag/install_data/agents/explorer-rag-enhanced.md +0 -0
  38. {java_codebase_rag-0.6.1 → java_codebase_rag-0.6.3}/java_codebase_rag/install_data/skills/explore-codebase/SKILL.md +0 -0
  39. {java_codebase_rag-0.6.1 → java_codebase_rag-0.6.3}/java_codebase_rag.egg-info/dependency_links.txt +0 -0
  40. {java_codebase_rag-0.6.1 → java_codebase_rag-0.6.3}/java_codebase_rag.egg-info/top_level.txt +0 -0
  41. {java_codebase_rag-0.6.1 → java_codebase_rag-0.6.3}/java_index_v1_common.py +0 -0
  42. {java_codebase_rag-0.6.1 → java_codebase_rag-0.6.3}/java_ontology.py +0 -0
  43. {java_codebase_rag-0.6.1 → java_codebase_rag-0.6.3}/ladybug_queries.py +0 -0
  44. {java_codebase_rag-0.6.1 → java_codebase_rag-0.6.3}/mcp_hints.py +0 -0
  45. {java_codebase_rag-0.6.1 → java_codebase_rag-0.6.3}/mcp_v2.py +0 -0
  46. {java_codebase_rag-0.6.1 → java_codebase_rag-0.6.3}/path_filtering.py +0 -0
  47. {java_codebase_rag-0.6.1 → java_codebase_rag-0.6.3}/pr_analysis.py +0 -0
  48. {java_codebase_rag-0.6.1 → java_codebase_rag-0.6.3}/search_lancedb.py +0 -0
  49. {java_codebase_rag-0.6.1 → java_codebase_rag-0.6.3}/setup.cfg +0 -0
  50. {java_codebase_rag-0.6.1 → java_codebase_rag-0.6.3}/tests/test_agent_skills_static.py +0 -0
  51. {java_codebase_rag-0.6.1 → java_codebase_rag-0.6.3}/tests/test_assign_endpoint_client_extraction.py +0 -0
  52. {java_codebase_rag-0.6.1 → java_codebase_rag-0.6.3}/tests/test_ast_java_calls.py +0 -0
  53. {java_codebase_rag-0.6.1 → java_codebase_rag-0.6.3}/tests/test_ast_java_capabilities.py +0 -0
  54. {java_codebase_rag-0.6.1 → java_codebase_rag-0.6.3}/tests/test_bank_chat_brownfield_integration.py +0 -0
  55. {java_codebase_rag-0.6.1 → java_codebase_rag-0.6.3}/tests/test_brownfield_clients.py +0 -0
  56. {java_codebase_rag-0.6.1 → java_codebase_rag-0.6.3}/tests/test_brownfield_events.py +0 -0
  57. {java_codebase_rag-0.6.1 → java_codebase_rag-0.6.3}/tests/test_brownfield_overrides.py +0 -0
  58. {java_codebase_rag-0.6.1 → java_codebase_rag-0.6.3}/tests/test_brownfield_routes.py +0 -0
  59. {java_codebase_rag-0.6.1 → java_codebase_rag-0.6.3}/tests/test_call_edge_matching.py +0 -0
  60. {java_codebase_rag-0.6.1 → java_codebase_rag-0.6.3}/tests/test_call_edges_e2e.py +0 -0
  61. {java_codebase_rag-0.6.1 → java_codebase_rag-0.6.3}/tests/test_call_graph_receiver_resolution.py +0 -0
  62. {java_codebase_rag-0.6.1 → java_codebase_rag-0.6.3}/tests/test_call_graph_smoke_roundtrip.py +0 -0
  63. {java_codebase_rag-0.6.1 → java_codebase_rag-0.6.3}/tests/test_call_invariant.py +0 -0
  64. {java_codebase_rag-0.6.1 → java_codebase_rag-0.6.3}/tests/test_cli_quiet_parity.py +0 -0
  65. {java_codebase_rag-0.6.1 → java_codebase_rag-0.6.3}/tests/test_client_hint_recovery.py +0 -0
  66. {java_codebase_rag-0.6.1 → java_codebase_rag-0.6.3}/tests/test_client_node_extraction.py +0 -0
  67. {java_codebase_rag-0.6.1 → java_codebase_rag-0.6.3}/tests/test_client_role_rename.py +0 -0
  68. {java_codebase_rag-0.6.1 → java_codebase_rag-0.6.3}/tests/test_cross_service_resolution_flag.py +0 -0
  69. {java_codebase_rag-0.6.1 → java_codebase_rag-0.6.3}/tests/test_edge_navigation_doc.py +0 -0
  70. {java_codebase_rag-0.6.1 → java_codebase_rag-0.6.3}/tests/test_fd_limit.py +0 -0
  71. {java_codebase_rag-0.6.1 → java_codebase_rag-0.6.3}/tests/test_feign_not_exposer.py +0 -0
  72. {java_codebase_rag-0.6.1 → java_codebase_rag-0.6.3}/tests/test_graph_enrich.py +0 -0
  73. {java_codebase_rag-0.6.1 → java_codebase_rag-0.6.3}/tests/test_incremental_graph.py +0 -0
  74. {java_codebase_rag-0.6.1 → java_codebase_rag-0.6.3}/tests/test_installer_integration.py +0 -0
  75. {java_codebase_rag-0.6.1 → java_codebase_rag-0.6.3}/tests/test_ladybug_queries.py +0 -0
  76. {java_codebase_rag-0.6.1 → java_codebase_rag-0.6.3}/tests/test_lance_optimize.py +0 -0
  77. {java_codebase_rag-0.6.1 → java_codebase_rag-0.6.3}/tests/test_lancedb_e2e.py +0 -0
  78. {java_codebase_rag-0.6.1 → java_codebase_rag-0.6.3}/tests/test_mcp_hints.py +0 -0
  79. {java_codebase_rag-0.6.1 → java_codebase_rag-0.6.3}/tests/test_mcp_server_project_root.py +0 -0
  80. {java_codebase_rag-0.6.1 → java_codebase_rag-0.6.3}/tests/test_mcp_tools.py +0 -0
  81. {java_codebase_rag-0.6.1 → java_codebase_rag-0.6.3}/tests/test_mcp_v2.py +0 -0
  82. {java_codebase_rag-0.6.1 → java_codebase_rag-0.6.3}/tests/test_mcp_v2_compose.py +0 -0
  83. {java_codebase_rag-0.6.1 → java_codebase_rag-0.6.3}/tests/test_meta_chain_core.py +0 -0
  84. {java_codebase_rag-0.6.1 → java_codebase_rag-0.6.3}/tests/test_outgoing_call_extraction.py +0 -0
  85. {java_codebase_rag-0.6.1 → java_codebase_rag-0.6.3}/tests/test_packaging_metadata.py +0 -0
  86. {java_codebase_rag-0.6.1 → java_codebase_rag-0.6.3}/tests/test_path_filtering.py +0 -0
  87. {java_codebase_rag-0.6.1 → java_codebase_rag-0.6.3}/tests/test_pr_analysis.py +0 -0
  88. {java_codebase_rag-0.6.1 → java_codebase_rag-0.6.3}/tests/test_resolve_routes_messaging_layer_c.py +0 -0
  89. {java_codebase_rag-0.6.1 → java_codebase_rag-0.6.3}/tests/test_route_extraction.py +0 -0
  90. {java_codebase_rag-0.6.1 → java_codebase_rag-0.6.3}/tests/test_schema_consistency.py +0 -0
  91. {java_codebase_rag-0.6.1 → java_codebase_rag-0.6.3}/tests/test_search_lancedb.py +0 -0
  92. {java_codebase_rag-0.6.1 → java_codebase_rag-0.6.3}/tests/test_search_lancedb_capability.py +0 -0
  93. {java_codebase_rag-0.6.1 → 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.1
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)
@@ -6,6 +6,7 @@ from __future__ import annotations
6
6
  import argparse
7
7
  import asyncio
8
8
  import json
9
+ import os
9
10
  import pprint
10
11
  import shutil
11
12
  import sys
@@ -130,15 +131,41 @@ def _run_with_pipeline_progress(
130
131
  cfg: ResolvedOperatorConfig,
131
132
  *,
132
133
  quiet: bool,
133
- work: Callable[[], int],
134
+ verbose: bool = False,
135
+ work: Callable[["PipelineProgress | None"], int],
134
136
  ) -> int:
135
- if quiet:
136
- 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
+
137
160
  _pipeline_header(subcommand, cfg)
138
161
  t0 = time.perf_counter()
139
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()
140
167
  try:
141
- code = int(work())
168
+ code = int(work(progress))
142
169
  return code
143
170
  except BaseException as exc:
144
171
  # Keep footer aligned with process outcome (main maps unhandled Exception -> exit 2).
@@ -154,9 +181,26 @@ def _run_with_pipeline_progress(
154
181
  code = 2
155
182
  raise
156
183
  finally:
184
+ renderer.stop()
157
185
  _pipeline_footer(subcommand, t0, code)
158
186
 
159
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
+
160
204
  def _jsonable(value: Any) -> Any:
161
205
  if hasattr(value, "model_dump"):
162
206
  return value.model_dump()
@@ -265,7 +309,7 @@ def _cmd_init(args: argparse.Namespace) -> int:
265
309
  return 2
266
310
  cfg.index_dir.mkdir(parents=True, exist_ok=True)
267
311
 
268
- def work() -> int:
312
+ def work(progress: "PipelineProgress | None") -> int:
269
313
  env = cfg.subprocess_env()
270
314
  verbose = bool(args.verbose)
271
315
  coco = run_cocoindex_update(
@@ -274,6 +318,8 @@ def _cmd_init(args: argparse.Namespace) -> int:
274
318
  quiet=bool(args.quiet),
275
319
  verbose=verbose,
276
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,
277
323
  )
278
324
  if coco.returncode != 0:
279
325
  _emit(
@@ -294,6 +340,8 @@ def _cmd_init(args: argparse.Namespace) -> int:
294
340
  verbose=verbose,
295
341
  quiet=bool(args.quiet),
296
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,
297
345
  )
298
346
  if g.returncode != 0:
299
347
  _emit(
@@ -309,7 +357,9 @@ def _cmd_init(args: argparse.Namespace) -> int:
309
357
  _emit({"success": True, "message": "init completed"})
310
358
  return 0
311
359
 
312
- 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
+ )
313
363
 
314
364
 
315
365
  def _cmd_increment(args: argparse.Namespace) -> int:
@@ -322,7 +372,7 @@ def _cmd_increment(args: argparse.Namespace) -> int:
322
372
  if vectors_only:
323
373
  _emit_increment_ladybug_warning()
324
374
 
325
- def work() -> int:
375
+ def work(progress: "PipelineProgress | None") -> int:
326
376
  env = cfg.subprocess_env()
327
377
  coco = run_cocoindex_update(
328
378
  env,
@@ -330,6 +380,8 @@ def _cmd_increment(args: argparse.Namespace) -> int:
330
380
  quiet=bool(args.quiet),
331
381
  verbose=bool(args.verbose),
332
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,
333
385
  )
334
386
  if coco.returncode != 0:
335
387
  _emit(
@@ -355,6 +407,8 @@ def _cmd_increment(args: argparse.Namespace) -> int:
355
407
  verbose=bool(args.verbose),
356
408
  quiet=bool(args.quiet),
357
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,
358
412
  )
359
413
 
360
414
  # Check if incremental fell back to full rebuild
@@ -388,7 +442,9 @@ def _cmd_increment(args: argparse.Namespace) -> int:
388
442
  _emit({"success": True, "message": "increment completed (Lance + graph updated)"})
389
443
  return 0
390
444
 
391
- 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
+ )
392
448
 
393
449
 
394
450
  def _cmd_reprocess(args: argparse.Namespace) -> int:
@@ -396,14 +452,18 @@ def _cmd_reprocess(args: argparse.Namespace) -> int:
396
452
  _startup_hints(cfg)
397
453
  cfg.apply_to_os_environ()
398
454
 
399
- def work() -> int:
455
+ def work(progress: "PipelineProgress | None") -> int:
400
456
  env = cfg.subprocess_env()
401
457
  verbose = bool(args.verbose)
402
458
  vectors_only = bool(getattr(args, "vectors_only", False))
403
459
  graph_only = bool(getattr(args, "graph_only", False))
404
460
 
405
461
  if vectors_only:
406
- 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
+ )
407
467
  if _is_cocoindex_preflight_blocker(coco):
408
468
  payload: dict[str, Any] = {
409
469
  "success": False,
@@ -442,6 +502,8 @@ def _cmd_reprocess(args: argparse.Namespace) -> int:
442
502
  verbose=verbose,
443
503
  quiet=bool(args.quiet),
444
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,
445
507
  )
446
508
  if _is_graph_preflight_blocker(g):
447
509
  payload = {
@@ -476,12 +538,21 @@ def _cmd_reprocess(args: argparse.Namespace) -> int:
476
538
 
477
539
  import server # lazy: pulls sentence_transformers/torch/lancedb/kuzu
478
540
 
479
- 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
+ )
480
549
  payload = result.model_dump()
481
550
  _emit_reprocess_outcome(payload)
482
551
  return _reprocess_exit_code(payload)
483
552
 
484
- 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
+ )
485
556
 
486
557
 
487
558
  def _cmd_install(args: argparse.Namespace) -> int:
@@ -494,6 +565,7 @@ def _cmd_install(args: argparse.Namespace) -> int:
494
565
  model=args.model,
495
566
  source_root=None, # None means cwd; installer confirms interactively
496
567
  quiet=bool(args.quiet),
568
+ verbose=bool(args.verbose),
497
569
  )
498
570
 
499
571
 
@@ -503,6 +575,8 @@ def _cmd_update(args: argparse.Namespace) -> int:
503
575
  return run_update(
504
576
  force=bool(args.force),
505
577
  dry_run=bool(args.dry_run),
578
+ quiet=bool(args.quiet),
579
+ verbose=bool(args.verbose),
506
580
  )
507
581
 
508
582
 
@@ -536,7 +610,7 @@ def _cmd_erase(args: argparse.Namespace) -> int:
536
610
  print("Aborted.", file=sys.stderr)
537
611
  return 2
538
612
 
539
- def work() -> int:
613
+ def work(progress: "PipelineProgress | None") -> int:
540
614
  env = cfg.subprocess_env()
541
615
  drop = run_cocoindex_drop(env, quiet=bool(args.quiet))
542
616
  if drop.returncode == 127:
@@ -569,7 +643,7 @@ def _cmd_erase(args: argparse.Namespace) -> int:
569
643
  _emit({"success": True, "message": "erase completed"})
570
644
  return 0
571
645
 
572
- 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)
573
647
 
574
648
 
575
649
  def _cmd_meta(args: argparse.Namespace) -> int:
@@ -930,5 +1004,21 @@ def main(argv: list[str] | None = None) -> int:
930
1004
  return 2
931
1005
 
932
1006
 
1007
+ def _console_script_main() -> None:
1008
+ """Real CLI entry: terminate without interpreter finalization.
1009
+
1010
+ A pyarrow/lance worker thread (loaded via lancedb in lifecycle commands) can
1011
+ outlive CPython finalization in a one-shot CLI subprocess and trip
1012
+ ``PyGILState_Release`` (SIGABRT, exit -6). Flushing + ``os._exit`` skips that
1013
+ racy teardown — the command has already done its work and emitted its result.
1014
+ ``main()`` stays return-based so in-process test callers (``cli.main(...)``)
1015
+ keep working.
1016
+ """
1017
+ rc = main()
1018
+ sys.stdout.flush()
1019
+ sys.stderr.flush()
1020
+ os._exit(rc)
1021
+
1022
+
933
1023
  if __name__ == "__main__":
934
- raise SystemExit(main())
1024
+ _console_script_main()
@@ -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: