java-codebase-rag 0.6.1__py3-none-any.whl → 0.6.3__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
@@ -759,6 +760,11 @@ def generate_yaml_config(
759
760
  else:
760
761
  config["embedding"].pop("model", None)
761
762
 
763
+ # Seed cross-service resolution safe-by-default: only evidence-backed cross-service
764
+ # edges survive (see _is_brownfield_sourced in build_ast_graph). setdefault preserves
765
+ # an explicit user choice (e.g. `auto`) on re-run update.
766
+ config.setdefault("cross_service_resolution", "brownfield_only")
767
+
762
768
  # Keys NOT written by installer (preserved if present):
763
769
  # - source_root (config.py resolves from walk-up discovery)
764
770
  # - index_dir (config.py defaults to <source_root>/.java-codebase-rag)
@@ -801,6 +807,38 @@ def update_gitignore(cwd: Path) -> None:
801
807
  gitignore_path.write_text("\n".join(lines), encoding="utf-8")
802
808
 
803
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
+
804
842
  def run_init_if_needed(
805
843
  source_root: Path,
806
844
  index_dir: Path,
@@ -808,15 +846,25 @@ def run_init_if_needed(
808
846
  *,
809
847
  non_interactive: bool,
810
848
  quiet: bool,
849
+ verbose: bool = False,
811
850
  ) -> bool:
812
851
  """Run init if index directory has no artifacts. Return True if init was run.
813
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
+
814
861
  Args:
815
862
  source_root: Source root directory
816
863
  index_dir: Index directory path
817
864
  model: Embedding model path or "auto"
818
865
  non_interactive: If True, suppress prompts
819
- quiet: If True, suppress output
866
+ quiet: If True, suppress progress output
867
+ verbose: If True, raw-relay subprocess output (no Live region)
820
868
 
821
869
  Returns:
822
870
  True if init was run, False if skipped
@@ -832,36 +880,71 @@ def run_init_if_needed(
832
880
  print("Index already exists. Run `java-codebase-rag reprocess` to rebuild.")
833
881
  return False
834
882
 
835
- print("Creating index...")
836
883
  cfg = resolve_operator_config(
837
884
  source_root=source_root,
838
885
  cli_index_dir=None, # use default (<source_root>/.java-codebase-rag)
839
886
  cli_embedding_model=model if model != "auto" else None,
840
887
  )
841
888
  cfg.apply_to_os_environ()
842
-
843
889
  env = cfg.subprocess_env()
844
890
 
845
- # Run CocoIndex update
846
- coco = run_cocoindex_update(env, full_reprocess=False, quiet=quiet)
847
- if coco.returncode != 0:
848
- print(f"Error: CocoIndex update failed with code {coco.returncode}")
849
- return False
850
-
851
- # Run AST graph build
852
- g = run_build_ast_graph(
853
- source_root=cfg.source_root,
854
- ladybug_path=cfg.ladybug_path,
855
- verbose=not quiet,
856
- quiet=quiet,
857
- env=env,
858
- )
859
- if g.returncode != 0:
860
- print(f"Error: AST graph build failed with code {g.returncode}")
861
- return False
862
-
863
- print("Index created successfully.")
864
- 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
865
948
 
866
949
 
867
950
  def handle_rerun(cwd: Path, *, non_interactive: bool) -> dict | None:
@@ -1196,13 +1279,25 @@ def run_update(
1196
1279
  force: bool,
1197
1280
  dry_run: bool,
1198
1281
  cwd: Path | None = None,
1282
+ quiet: bool = False,
1283
+ verbose: bool = False,
1199
1284
  ) -> int:
1200
1285
  """Run the update pipeline. Returns exit code.
1201
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
+
1202
1295
  Args:
1203
1296
  force: If True, overwrite all artifacts even if matching
1204
1297
  dry_run: If True, print changes without writing
1205
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)
1206
1301
 
1207
1302
  Returns:
1208
1303
  Exit code (0=success, 1=partial, 2=fatal)
@@ -1250,9 +1345,14 @@ def run_update(
1250
1345
  print("Skipping index update.")
1251
1346
  return EXIT_PARTIAL if has_artifact_failures else EXIT_SUCCESS
1252
1347
 
1253
- # Resolve configuration
1348
+ # Resolve configuration. Pass source_root=None so the YAML ``source_root``
1349
+ # field is honored exactly like increment/init/reprocess — passing the
1350
+ # discovered config dir here routes resolve_operator_config into the
1351
+ # explicit-override branch that SKIPS the YAML field, which made `update`
1352
+ # point cocoindex at the config dir (no Java) against the real index and
1353
+ # mass-delete it. Discovery still runs against the CLI's cwd.
1254
1354
  try:
1255
- cfg = resolve_operator_config(source_root=project_root, cli_index_dir=None)
1355
+ cfg = resolve_operator_config(source_root=None, cli_index_dir=None)
1256
1356
  index_dir = cfg.index_dir
1257
1357
  except Exception as e:
1258
1358
  print(f"\nWarning: Failed to resolve configuration: {e}")
@@ -1272,30 +1372,74 @@ def run_update(
1272
1372
  # The "graph not implemented" warning belongs only on the vectors-only path
1273
1373
  # (increment --vectors-only), where the graph step is deliberately skipped.
1274
1374
  if not dry_run:
1275
- print("\nUpdating index (Lance + graph)...")
1276
1375
  cfg.apply_to_os_environ()
1277
1376
  env = cfg.subprocess_env()
1278
1377
 
1279
- coco = run_cocoindex_update(env, full_reprocess=False, quiet=True)
1280
- if coco.returncode != 0:
1281
- print(f"Error: Lance index update failed with code {coco.returncode}")
1282
- return 1
1283
-
1284
- g = run_incremental_graph(
1285
- source_root=cfg.source_root,
1286
- ladybug_path=cfg.ladybug_path,
1287
- verbose=False,
1288
- quiet=True,
1289
- env=env,
1290
- )
1291
- if g.returncode != 0:
1292
- # Artifacts above already refreshed; the graph catch-up is best-effort
1293
- # here. Surface a truthful, actionable message instead of leaving the
1294
- # graph silently stale or claiming the feature is unimplemented.
1295
- print(
1296
- f"\nWarning: incremental graph update failed (exit {g.returncode}). "
1297
- "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,
1298
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
1299
1443
  else:
1300
1444
  print("\nWould run incremental index update (Lance + graph).")
1301
1445
 
@@ -1315,6 +1459,7 @@ def run_install(
1315
1459
  model: str | None,
1316
1460
  source_root: Path | None = None,
1317
1461
  quiet: bool = False,
1462
+ verbose: bool = False,
1318
1463
  ) -> int:
1319
1464
  """Run the install pipeline. Returns exit code.
1320
1465
 
@@ -1325,6 +1470,7 @@ def run_install(
1325
1470
  model: Model from CLI flag
1326
1471
  source_root: Source root path (defaults to cwd if None)
1327
1472
  quiet: If True, suppress output
1473
+ verbose: If True, raw-relay subprocess indexing output (no Live region)
1328
1474
 
1329
1475
  Returns:
1330
1476
  Exit code (0=success, 1=partial, 2=fatal)
@@ -1423,6 +1569,7 @@ def run_install(
1423
1569
  resolved_model,
1424
1570
  non_interactive=non_interactive,
1425
1571
  quiet=quiet,
1572
+ verbose=verbose,
1426
1573
  )
1427
1574
 
1428
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
+ )