java-codebase-rag 0.6.2__py3-none-any.whl → 0.6.4__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -14,6 +14,7 @@ import os
14
14
  import shutil
15
15
  import sys
16
16
  import tempfile
17
+ import time
17
18
  from dataclasses import dataclass
18
19
  from pathlib import Path
19
20
  from typing import Literal, NamedTuple
@@ -806,6 +807,38 @@ def update_gitignore(cwd: Path) -> None:
806
807
  gitignore_path.write_text("\n".join(lines), encoding="utf-8")
807
808
 
808
809
 
810
+ def _index_progress_header(subcommand: str, source_root: Path, index_dir: Path) -> None:
811
+ """Print the stderr header framing the indexing sub-step (install/update).
812
+
813
+ Mirrors the operator commands' ``_pipeline_header`` but lives in the
814
+ installer because the wizard's stdout framing differs. This brackets ONLY
815
+ the indexing sub-step — the wizard's prompts stay outside it on stdout.
816
+ """
817
+ from java_codebase_rag.cli_format import bold
818
+
819
+ print(
820
+ bold(
821
+ f"java-codebase-rag {subcommand} · source={source_root.resolve()} "
822
+ f"· index={index_dir.resolve()}"
823
+ ),
824
+ file=sys.stderr,
825
+ flush=True,
826
+ )
827
+
828
+
829
+ def _index_progress_footer(subcommand: str, started: float, *, ok: bool) -> None:
830
+ """Print the stderr footer closing the indexing sub-step framing."""
831
+ from java_codebase_rag.cli_format import bold, styled_check, styled_cross
832
+
833
+ elapsed = time.perf_counter() - started
834
+ marker = styled_check() if ok else styled_cross()
835
+ print(
836
+ f"{marker} {bold(f'java-codebase-rag {subcommand} · finished in {elapsed:.2f}s')}",
837
+ file=sys.stderr,
838
+ flush=True,
839
+ )
840
+
841
+
809
842
  def run_init_if_needed(
810
843
  source_root: Path,
811
844
  index_dir: Path,
@@ -813,15 +846,25 @@ def run_init_if_needed(
813
846
  *,
814
847
  non_interactive: bool,
815
848
  quiet: bool,
849
+ verbose: bool = False,
816
850
  ) -> bool:
817
851
  """Run init if index directory has no artifacts. Return True if init was run.
818
852
 
853
+ The indexing sub-step (CocoIndex update + AST graph build) renders the
854
+ unified ``Vectors → Optimize → Graph`` progress on **stderr** in default
855
+ mode (same renderer the operator commands use); the wizard's conversational
856
+ stdout is untouched by this function. ``--quiet`` is silent; ``--verbose``
857
+ raw-relays subprocess output. The indexing chatter that used to print to
858
+ stdout (``Creating index…`` / ``Index created successfully.``) now lives
859
+ on stderr framing so stdout stays the wizard payload.
860
+
819
861
  Args:
820
862
  source_root: Source root directory
821
863
  index_dir: Index directory path
822
864
  model: Embedding model path or "auto"
823
865
  non_interactive: If True, suppress prompts
824
- quiet: If True, suppress output
866
+ quiet: If True, suppress progress output
867
+ verbose: If True, raw-relay subprocess output (no Live region)
825
868
 
826
869
  Returns:
827
870
  True if init was run, False if skipped
@@ -837,36 +880,71 @@ def run_init_if_needed(
837
880
  print("Index already exists. Run `java-codebase-rag reprocess` to rebuild.")
838
881
  return False
839
882
 
840
- print("Creating index...")
841
883
  cfg = resolve_operator_config(
842
884
  source_root=source_root,
843
885
  cli_index_dir=None, # use default (<source_root>/.java-codebase-rag)
844
886
  cli_embedding_model=model if model != "auto" else None,
845
887
  )
846
888
  cfg.apply_to_os_environ()
847
-
848
889
  env = cfg.subprocess_env()
849
890
 
850
- # Run CocoIndex update
851
- coco = run_cocoindex_update(env, full_reprocess=False, quiet=quiet)
852
- if coco.returncode != 0:
853
- print(f"Error: CocoIndex update failed with code {coco.returncode}")
854
- return False
855
-
856
- # Run AST graph build
857
- g = run_build_ast_graph(
858
- source_root=cfg.source_root,
859
- ladybug_path=cfg.ladybug_path,
860
- verbose=not quiet,
861
- quiet=quiet,
862
- env=env,
863
- )
864
- if g.returncode != 0:
865
- print(f"Error: AST graph build failed with code {g.returncode}")
866
- return False
867
-
868
- print("Index created successfully.")
869
- return True
891
+ # Indexing sub-step: render unified progress on stderr in default mode only
892
+ # (quiet = silent; verbose = raw relay, no Live region). The renderer wraps
893
+ # just this sub-step, not the surrounding wizard.
894
+ on_progress, on_progress_console = None, None
895
+ renderer = None
896
+ if not quiet and not verbose:
897
+ from java_codebase_rag.progress import build_index_progress_context
898
+
899
+ renderer, on_progress, on_progress_console = build_index_progress_context()
900
+
901
+ started = time.perf_counter()
902
+ if renderer is not None:
903
+ _index_progress_header("install", cfg.source_root, cfg.index_dir)
904
+ renderer.start()
905
+ index_ok = True
906
+ try:
907
+ coco = run_cocoindex_update(
908
+ env,
909
+ full_reprocess=False,
910
+ quiet=quiet,
911
+ verbose=verbose,
912
+ on_progress=on_progress,
913
+ on_progress_console=on_progress_console,
914
+ )
915
+ if coco.returncode != 0:
916
+ print(
917
+ f"Error: CocoIndex update failed with code {coco.returncode}",
918
+ file=sys.stderr,
919
+ )
920
+ index_ok = False
921
+ else:
922
+ g = run_build_ast_graph(
923
+ source_root=cfg.source_root,
924
+ ladybug_path=cfg.ladybug_path,
925
+ verbose=verbose,
926
+ quiet=quiet,
927
+ env=env,
928
+ on_progress=on_progress,
929
+ on_progress_console=on_progress_console,
930
+ )
931
+ if g.returncode != 0:
932
+ print(
933
+ f"Error: AST graph build failed with code {g.returncode}",
934
+ file=sys.stderr,
935
+ )
936
+ index_ok = False
937
+ except BaseException:
938
+ # An exception from cocoindex/graph means the index did not succeed;
939
+ # flip the footer marker before re-raising so it renders a red cross
940
+ # (mirrors cli._run_with_pipeline_progress's BaseException handler).
941
+ index_ok = False
942
+ raise
943
+ finally:
944
+ if renderer is not None:
945
+ renderer.stop()
946
+ _index_progress_footer("install", started, ok=index_ok)
947
+ return index_ok
870
948
 
871
949
 
872
950
  def handle_rerun(cwd: Path, *, non_interactive: bool) -> dict | None:
@@ -1201,13 +1279,25 @@ def run_update(
1201
1279
  force: bool,
1202
1280
  dry_run: bool,
1203
1281
  cwd: Path | None = None,
1282
+ quiet: bool = False,
1283
+ verbose: bool = False,
1204
1284
  ) -> int:
1205
1285
  """Run the update pipeline. Returns exit code.
1206
1286
 
1287
+ The indexing sub-step (Lance catch-up + incremental graph) renders the
1288
+ unified ``Vectors → Optimize → Graph`` progress on **stderr** in default
1289
+ mode and no longer runs with ``quiet=True`` (the reason ``update`` was
1290
+ silent). ``--quiet`` is silent; ``--verbose`` raw-relays subprocess output.
1291
+ The wizard's host-detection / refresh / summary stdout is preserved; only
1292
+ the indexing chatter that used to print to stdout moves onto the stderr
1293
+ renderer framing.
1294
+
1207
1295
  Args:
1208
1296
  force: If True, overwrite all artifacts even if matching
1209
1297
  dry_run: If True, print changes without writing
1210
1298
  cwd: Current working directory (defaults to Path.cwd())
1299
+ quiet: If True, suppress progress output
1300
+ verbose: If True, raw-relay subprocess output (no Live region)
1211
1301
 
1212
1302
  Returns:
1213
1303
  Exit code (0=success, 1=partial, 2=fatal)
@@ -1282,30 +1372,74 @@ def run_update(
1282
1372
  # The "graph not implemented" warning belongs only on the vectors-only path
1283
1373
  # (increment --vectors-only), where the graph step is deliberately skipped.
1284
1374
  if not dry_run:
1285
- print("\nUpdating index (Lance + graph)...")
1286
1375
  cfg.apply_to_os_environ()
1287
1376
  env = cfg.subprocess_env()
1288
1377
 
1289
- coco = run_cocoindex_update(env, full_reprocess=False, quiet=True)
1290
- if coco.returncode != 0:
1291
- print(f"Error: Lance index update failed with code {coco.returncode}")
1292
- return 1
1293
-
1294
- g = run_incremental_graph(
1295
- source_root=cfg.source_root,
1296
- ladybug_path=cfg.ladybug_path,
1297
- verbose=False,
1298
- quiet=True,
1299
- env=env,
1300
- )
1301
- if g.returncode != 0:
1302
- # Artifacts above already refreshed; the graph catch-up is best-effort
1303
- # here. Surface a truthful, actionable message instead of leaving the
1304
- # graph silently stale or claiming the feature is unimplemented.
1305
- print(
1306
- f"\nWarning: incremental graph update failed (exit {g.returncode}). "
1307
- "Run `java-codebase-rag reprocess` for a full rebuild."
1378
+ # Indexing sub-step: render unified progress on stderr in default mode
1379
+ # only (quiet = silent; verbose = raw relay). No longer runs quiet=True
1380
+ # that was why `update` was silent. The renderer wraps just this
1381
+ # sub-step; the wizard's summary stdout below is outside it.
1382
+ on_progress, on_progress_console = None, None
1383
+ renderer = None
1384
+ if not quiet and not verbose:
1385
+ from java_codebase_rag.progress import build_index_progress_context
1386
+
1387
+ renderer, on_progress, on_progress_console = build_index_progress_context()
1388
+
1389
+ started = time.perf_counter()
1390
+ if renderer is not None:
1391
+ _index_progress_header("update", cfg.source_root, cfg.index_dir)
1392
+ renderer.start()
1393
+ index_ok = True
1394
+ try:
1395
+ coco = run_cocoindex_update(
1396
+ env,
1397
+ full_reprocess=False,
1398
+ quiet=quiet,
1399
+ verbose=verbose,
1400
+ on_progress=on_progress,
1401
+ on_progress_console=on_progress_console,
1308
1402
  )
1403
+ if coco.returncode != 0:
1404
+ print(
1405
+ f"Error: Lance index update failed with code {coco.returncode}",
1406
+ file=sys.stderr,
1407
+ )
1408
+ index_ok = False
1409
+ else:
1410
+ g = run_incremental_graph(
1411
+ source_root=cfg.source_root,
1412
+ ladybug_path=cfg.ladybug_path,
1413
+ verbose=verbose,
1414
+ quiet=quiet,
1415
+ env=env,
1416
+ on_progress=on_progress,
1417
+ on_progress_console=on_progress_console,
1418
+ )
1419
+ if g.returncode != 0:
1420
+ # The graph catch-up is best-effort: `update`'s primary job
1421
+ # is refreshing shipped artifacts + vectors (cocoindex). A
1422
+ # graph failure surfaces a truthful, actionable Warning on
1423
+ # stderr but does NOT flip index_ok (which drives both the
1424
+ # footer marker and the return code) — exit 0 with a green
1425
+ # check + the Warning line carrying the graph caveat.
1426
+ print(
1427
+ f"\nWarning: incremental graph update failed (exit {g.returncode}). "
1428
+ "Run `java-codebase-rag reprocess` for a full rebuild.",
1429
+ file=sys.stderr,
1430
+ )
1431
+ except BaseException:
1432
+ # An exception from cocoindex/graph means the index did not succeed;
1433
+ # flip the footer marker before re-raising so it renders a red cross
1434
+ # (mirrors cli._run_with_pipeline_progress's BaseException handler).
1435
+ index_ok = False
1436
+ raise
1437
+ finally:
1438
+ if renderer is not None:
1439
+ renderer.stop()
1440
+ _index_progress_footer("update", started, ok=index_ok)
1441
+ if not index_ok:
1442
+ return 1
1309
1443
  else:
1310
1444
  print("\nWould run incremental index update (Lance + graph).")
1311
1445
 
@@ -1325,6 +1459,7 @@ def run_install(
1325
1459
  model: str | None,
1326
1460
  source_root: Path | None = None,
1327
1461
  quiet: bool = False,
1462
+ verbose: bool = False,
1328
1463
  ) -> int:
1329
1464
  """Run the install pipeline. Returns exit code.
1330
1465
 
@@ -1335,6 +1470,7 @@ def run_install(
1335
1470
  model: Model from CLI flag
1336
1471
  source_root: Source root path (defaults to cwd if None)
1337
1472
  quiet: If True, suppress output
1473
+ verbose: If True, raw-relay subprocess indexing output (no Live region)
1338
1474
 
1339
1475
  Returns:
1340
1476
  Exit code (0=success, 1=partial, 2=fatal)
@@ -1433,6 +1569,7 @@ def run_install(
1433
1569
  resolved_model,
1434
1570
  non_interactive=non_interactive,
1435
1571
  quiet=quiet,
1572
+ verbose=verbose,
1436
1573
  )
1437
1574
 
1438
1575
  return 0
@@ -21,7 +21,14 @@ from __future__ import annotations
21
21
 
22
22
  import asyncio
23
23
  import sys
24
+ import time
24
25
  from pathlib import Path
26
+ from typing import Callable, Literal
27
+
28
+ # Mirrors ``ProgressStatus`` in ``progress.py``; kept local (rather than imported)
29
+ # so this module never pays the ``rich`` cost at import time — see
30
+ # ``_make_optimize_event``.
31
+ _OptimizeStatus = Literal["running", "done", "failed"]
25
32
 
26
33
  # Single source of truth for the three Lance table names created by the flow.
27
34
  # Keep in sync with ``search_lancedb.TABLES`` (the values there mirror these).
@@ -31,6 +38,33 @@ LANCE_TABLE_NAMES: tuple[str, ...] = (
31
38
  "yamlconfigindex_yaml_config",
32
39
  )
33
40
 
41
+
42
+ def _make_optimize_event(
43
+ *,
44
+ status: _OptimizeStatus,
45
+ elapsed_s: float | None = None,
46
+ ):
47
+ """Build a ``ProgressEvent(kind="optimize", …)`` lazily (progress is parent-side).
48
+
49
+ ``lance_optimize`` runs in-process in the parent (called by
50
+ ``pipeline._maybe_run_serialized_optimize`` and
51
+ ``server.run_refresh_pipeline``); it routes progress to the renderer via the
52
+ in-process ``on_progress`` callback — NOT via stderr (which would corrupt
53
+ the Live region). The import is local so the flow (which imports
54
+ ``LANCE_TABLE_NAMES`` at definition time) never pays the ``rich`` cost.
55
+ """
56
+ from java_codebase_rag.progress import ProgressEvent
57
+
58
+ return ProgressEvent(
59
+ kind="optimize",
60
+ phase=None,
61
+ pass_=None,
62
+ done=None,
63
+ total=None,
64
+ status=status,
65
+ elapsed_s=elapsed_s,
66
+ )
67
+
34
68
  # Commit conflicts are transient; a handful of exponential-backoff retries is
35
69
  # enough because, post-flow, there are no concurrent writers — only successive
36
70
  # optimize/compaction passes within this single serialized call can still
@@ -60,7 +94,12 @@ async def _list_table_names(db: object) -> set[str]:
60
94
  return set(await db.table_names())
61
95
 
62
96
 
63
- async def optimize_lance_tables(index_dir: Path, *, quiet: bool = False) -> dict[str, str]:
97
+ async def optimize_lance_tables(
98
+ index_dir: Path,
99
+ *,
100
+ quiet: bool = False,
101
+ on_progress: Callable | None = None,
102
+ ) -> dict[str, str]:
64
103
  """Optimize all known Lance tables under *index_dir*, serially, with retry.
65
104
 
66
105
  Runs ``table.optimize()`` for each name in :data:`LANCE_TABLE_NAMES` that
@@ -73,6 +112,13 @@ async def optimize_lance_tables(index_dir: Path, *, quiet: bool = False) -> dict
73
112
  index_dir: directory holding the Lance tables (the flow's LanceDB URI).
74
113
  quiet: when True, suppress the per-table success/skip info lines on
75
114
  stderr (errors are always logged).
115
+ on_progress: optional in-process progress callback (the parent's
116
+ renderer ``on_progress``). When given, emits
117
+ ``ProgressEvent(kind="optimize", status="running")`` on entry and a
118
+ terminal ``status="done"``/``"failed"`` event on exit (covers BOTH
119
+ call sites: ``pipeline._maybe_run_serialized_optimize`` and
120
+ ``server.run_refresh_pipeline``). In-process only — NEVER prints to
121
+ stderr (that would corrupt the Live region).
76
122
 
77
123
  Returns:
78
124
  Mapping of table name → status. Values are ``"ok"``, ``"skipped"``
@@ -82,67 +128,95 @@ async def optimize_lance_tables(index_dir: Path, *, quiet: bool = False) -> dict
82
128
  # not pay the lancedb import cost at flow-definition time.
83
129
  import lancedb
84
130
 
131
+ if on_progress is not None:
132
+ on_progress(_make_optimize_event(status="running"))
133
+ t0 = time.perf_counter()
85
134
  results: dict[str, str] = {}
86
- db = await lancedb.connect_async(str(index_dir))
135
+ failed = False
87
136
  try:
137
+ db = await lancedb.connect_async(str(index_dir))
88
138
  try:
89
- existing = await _list_table_names(db)
90
- except Exception as exc:
91
- print(
92
- f"java-codebase-rag: optimize: failed to list tables in "
93
- f"{index_dir}: {exc}",
94
- file=sys.stderr,
95
- )
96
- return {name: f"error: list failed: {exc}" for name in LANCE_TABLE_NAMES}
97
-
98
- for name in LANCE_TABLE_NAMES:
99
- if name not in existing:
100
- results[name] = "skipped"
101
- if not quiet:
102
- print(
103
- f"java-codebase-rag: optimize: {name} absent, skipped",
104
- file=sys.stderr,
105
- )
106
- continue
107
139
  try:
108
- table = await db.open_table(name)
140
+ existing = await _list_table_names(db)
109
141
  except Exception as exc:
110
- results[name] = f"error: open failed: {exc}"
111
142
  print(
112
- f"java-codebase-rag: optimize: {name} open failed: {exc}",
143
+ f"java-codebase-rag: optimize: failed to list tables in "
144
+ f"{index_dir}: {exc}",
113
145
  file=sys.stderr,
114
146
  )
115
- continue
116
-
117
- last_exc: BaseException | None = None
118
- for attempt in range(_MAX_ATTEMPTS):
147
+ failed = True
148
+ return {name: f"error: list failed: {exc}" for name in LANCE_TABLE_NAMES}
149
+
150
+ for name in LANCE_TABLE_NAMES:
151
+ if name not in existing:
152
+ results[name] = "skipped"
153
+ if not quiet:
154
+ print(
155
+ f"java-codebase-rag: optimize: {name} absent, skipped",
156
+ file=sys.stderr,
157
+ )
158
+ continue
119
159
  try:
120
- await table.optimize()
121
- last_exc = None
122
- break
160
+ table = await db.open_table(name)
123
161
  except Exception as exc:
124
- last_exc = exc
125
- if _is_retryable(exc) and attempt < _MAX_ATTEMPTS - 1:
126
- await asyncio.sleep(_BASE_BACKOFF_S * (2**attempt))
127
- continue
128
- # Non-retryable, or retries exhausted: stop the loop and
129
- # surface below — do not swallow silently.
130
- break
131
-
132
- if last_exc is None:
133
- results[name] = "ok"
134
- if not quiet:
162
+ results[name] = f"error: open failed: {exc}"
163
+ failed = True
135
164
  print(
136
- f"java-codebase-rag: optimize: {name} ok",
165
+ f"java-codebase-rag: optimize: {name} open failed: {exc}",
137
166
  file=sys.stderr,
138
167
  )
139
- else:
140
- results[name] = f"error: {last_exc}"
141
- print(
142
- f"java-codebase-rag: optimize: {name} failed: {last_exc}",
143
- file=sys.stderr,
144
- )
168
+ continue
169
+
170
+ last_exc: BaseException | None = None
171
+ for attempt in range(_MAX_ATTEMPTS):
172
+ try:
173
+ await table.optimize()
174
+ last_exc = None
175
+ break
176
+ except Exception as exc:
177
+ last_exc = exc
178
+ if _is_retryable(exc) and attempt < _MAX_ATTEMPTS - 1:
179
+ await asyncio.sleep(_BASE_BACKOFF_S * (2**attempt))
180
+ continue
181
+ # Non-retryable, or retries exhausted: stop the loop and
182
+ # surface below — do not swallow silently.
183
+ break
184
+
185
+ if last_exc is None:
186
+ results[name] = "ok"
187
+ if not quiet:
188
+ print(
189
+ f"java-codebase-rag: optimize: {name} ok",
190
+ file=sys.stderr,
191
+ )
192
+ else:
193
+ results[name] = f"error: {last_exc}"
194
+ failed = True
195
+ print(
196
+ f"java-codebase-rag: optimize: {name} failed: {last_exc}",
197
+ file=sys.stderr,
198
+ )
199
+ finally:
200
+ # ``AsyncConnection.close`` is a *sync* method in lancedb 0.30.x.
201
+ db.close()
202
+ return results
203
+ except Exception:
204
+ # An unexpected exception (e.g. ``connect_async`` raised, or a table-
205
+ # independent failure) must still flip the terminal event to failed so
206
+ # the renderer's task doesn't render a green check on a crash. Re-raise
207
+ # after marking — the caller (``_maybe_run_serialized_optimize`` /
208
+ # ``run_refresh_pipeline``) treats optimize failure as non-fatal and
209
+ # logs it, but the renderer must reflect the truth.
210
+ failed = True
211
+ raise
145
212
  finally:
146
- # ``AsyncConnection.close`` is a *sync* method in lancedb 0.30.x.
147
- db.close()
148
- return results
213
+ # Always emit a terminal optimize event so the renderer's task never
214
+ # hangs at "running" — even on exception (the parent treats a failed
215
+ # optimize as non-fatal: the index is still searchable un-compacted).
216
+ if on_progress is not None:
217
+ on_progress(
218
+ _make_optimize_event(
219
+ status="failed" if failed else "done",
220
+ elapsed_s=time.perf_counter() - t0,
221
+ )
222
+ )