java-codebase-rag 0.6.7__py3-none-any.whl → 0.8.0__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.
- ast_java.py +8 -3
- build_ast_graph.py +72 -16
- graph_enrich.py +2 -1
- graph_types.py +133 -0
- java_codebase_rag/_fdlimit.py +10 -2
- java_codebase_rag/_stdio.py +32 -0
- java_codebase_rag/cli.py +135 -24
- java_codebase_rag/config.py +128 -9
- java_codebase_rag/install_data/agents/explorer-rag-cli.md +291 -0
- java_codebase_rag/install_data/agents/explorer-rag-enhanced.md +8 -8
- java_codebase_rag/install_data/skills/explore-codebase/SKILL.md +8 -8
- java_codebase_rag/install_data/skills/explore-codebase-cli/SKILL.md +251 -0
- java_codebase_rag/installer.py +438 -103
- java_codebase_rag/jrag.py +4300 -0
- java_codebase_rag/jrag_envelope.py +1085 -0
- java_codebase_rag/jrag_hints.py +204 -0
- java_codebase_rag/jrag_render.py +688 -0
- java_codebase_rag/pipeline.py +20 -0
- {java_codebase_rag-0.6.7.dist-info → java_codebase_rag-0.8.0.dist-info}/METADATA +137 -94
- java_codebase_rag-0.8.0.dist-info/RECORD +43 -0
- {java_codebase_rag-0.6.7.dist-info → java_codebase_rag-0.8.0.dist-info}/WHEEL +1 -1
- {java_codebase_rag-0.6.7.dist-info → java_codebase_rag-0.8.0.dist-info}/entry_points.txt +1 -0
- {java_codebase_rag-0.6.7.dist-info → java_codebase_rag-0.8.0.dist-info}/top_level.txt +2 -0
- java_index_flow_lancedb.py +34 -19
- java_ontology.py +12 -0
- ladybug_queries.py +233 -52
- mcp_hints.py +6 -6
- mcp_v2.py +205 -617
- resolve_service.py +649 -0
- search_lancedb.py +10 -1
- server.py +20 -12
- java_codebase_rag-0.6.7.dist-info/RECORD +0 -34
- {java_codebase_rag-0.6.7.dist-info → java_codebase_rag-0.8.0.dist-info}/licenses/LICENSE +0 -0
java_codebase_rag/cli.py
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
-
# Heavy imports (`server`, `pr_analysis`, `path_filtering.LayeredIgnore
|
|
4
|
-
# inside handlers so `java-codebase-rag --help` stays fast.
|
|
3
|
+
# Heavy imports (`server`, `pr_analysis`, `path_filtering.LayeredIgnore`,
|
|
4
|
+
# `build_ast_graph`) stay lazy inside handlers so `java-codebase-rag --help` stays fast.
|
|
5
5
|
|
|
6
6
|
import argparse
|
|
7
7
|
import asyncio
|
|
@@ -15,15 +15,25 @@ from pathlib import Path
|
|
|
15
15
|
from typing import Any, Callable
|
|
16
16
|
|
|
17
17
|
from java_codebase_rag.config import (
|
|
18
|
+
OPERATOR_OWNED_INDEX_FILES,
|
|
18
19
|
ResolvedOperatorConfig,
|
|
19
20
|
describe_path_sizes,
|
|
20
21
|
emit_legacy_env_hints_if_present,
|
|
21
22
|
emit_legacy_yaml_hint_if_needed,
|
|
22
23
|
index_dir_has_existing_artifacts,
|
|
23
24
|
resolve_operator_config,
|
|
25
|
+
write_config_source_pointer,
|
|
24
26
|
)
|
|
25
27
|
from java_codebase_rag._fdlimit import raise_fd_limit
|
|
26
|
-
from java_codebase_rag.pipeline import
|
|
28
|
+
from java_codebase_rag.pipeline import (
|
|
29
|
+
clip,
|
|
30
|
+
is_cocoindex_preflight_blocker,
|
|
31
|
+
is_graph_preflight_blocker,
|
|
32
|
+
run_build_ast_graph,
|
|
33
|
+
run_cocoindex_drop,
|
|
34
|
+
run_cocoindex_update,
|
|
35
|
+
run_incremental_graph,
|
|
36
|
+
)
|
|
27
37
|
from java_ontology import VALID_UNRESOLVED_CALL_REASONS
|
|
28
38
|
|
|
29
39
|
LADYBUG_INCREMENTAL_TRACKING_ISSUE_URL = "https://github.com/HumanBean17/java-codebase-rag/issues/73"
|
|
@@ -61,6 +71,12 @@ def _reprocess_drift_graph_only_line(index_dir: Path) -> str:
|
|
|
61
71
|
)
|
|
62
72
|
|
|
63
73
|
|
|
74
|
+
_VECTORS_SKIPPED_GRAPH_ONLY = (
|
|
75
|
+
"java-codebase-rag: vectors skipped — vector stack not installed on this platform "
|
|
76
|
+
"(graph-only mode). The graph is built/refreshed; semantic search is unavailable."
|
|
77
|
+
)
|
|
78
|
+
|
|
79
|
+
|
|
64
80
|
def _reprocess_exit_code(payload: dict[str, Any]) -> int:
|
|
65
81
|
if payload.get("success"):
|
|
66
82
|
return 0
|
|
@@ -70,16 +86,17 @@ def _reprocess_exit_code(payload: dict[str, Any]) -> int:
|
|
|
70
86
|
return 1
|
|
71
87
|
|
|
72
88
|
|
|
73
|
-
# Preflight detection
|
|
74
|
-
#
|
|
89
|
+
# Preflight detection delegates to pipeline.is_cocoindex_preflight_blocker /
|
|
90
|
+
# is_graph_preflight_blocker, which are co-located with the stub CompletedProcess shapes
|
|
91
|
+
# they must match (missing cocoindex / flow / build_ast_graph.py).
|
|
75
92
|
def _is_cocoindex_preflight_blocker(coco: Any) -> bool:
|
|
76
93
|
"""True when ``run_cocoindex_update`` returned without spawning cocoindex."""
|
|
77
|
-
return
|
|
94
|
+
return is_cocoindex_preflight_blocker(coco)
|
|
78
95
|
|
|
79
96
|
|
|
80
97
|
def _is_graph_preflight_blocker(g: Any) -> bool:
|
|
81
98
|
"""True when ``run_build_ast_graph`` returned without spawning the builder."""
|
|
82
|
-
return
|
|
99
|
+
return is_graph_preflight_blocker(g)
|
|
83
100
|
|
|
84
101
|
|
|
85
102
|
def _emit_reprocess_selective_tty(*, mode: str) -> None:
|
|
@@ -126,6 +143,21 @@ def _pipeline_footer(subcommand: str, started: float, exit_code: int) -> None:
|
|
|
126
143
|
)
|
|
127
144
|
|
|
128
145
|
|
|
146
|
+
# Subcommands that build/refresh an index and therefore should record which YAML
|
|
147
|
+
# was used (so a later discovery run from a sibling/cwd can relocate it).
|
|
148
|
+
_CONFIG_SOURCE_RECORDING_SUBCOMMANDS = frozenset({"init", "increment", "reprocess"})
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
def _maybe_record_config_source(
|
|
152
|
+
subcommand: str, cfg: ResolvedOperatorConfig, code: int
|
|
153
|
+
) -> None:
|
|
154
|
+
"""On a successful index build, remember the YAML path inside the index dir."""
|
|
155
|
+
if code == 0 and subcommand in _CONFIG_SOURCE_RECORDING_SUBCOMMANDS:
|
|
156
|
+
write_config_source_pointer(
|
|
157
|
+
index_dir=cfg.index_dir, yaml_config_path=cfg.yaml_config_path
|
|
158
|
+
)
|
|
159
|
+
|
|
160
|
+
|
|
129
161
|
def _run_with_pipeline_progress(
|
|
130
162
|
subcommand: str,
|
|
131
163
|
cfg: ResolvedOperatorConfig,
|
|
@@ -143,7 +175,9 @@ def _run_with_pipeline_progress(
|
|
|
143
175
|
raw-relays subprocess output).
|
|
144
176
|
"""
|
|
145
177
|
if quiet or verbose:
|
|
146
|
-
|
|
178
|
+
code = int(work(None))
|
|
179
|
+
_maybe_record_config_source(subcommand, cfg, code)
|
|
180
|
+
return code
|
|
147
181
|
from java_codebase_rag.progress import build_index_progress_context
|
|
148
182
|
|
|
149
183
|
# PR-3 owns all three tasks in order: Vectors → Optimize → Graph. The vectors
|
|
@@ -183,6 +217,7 @@ def _run_with_pipeline_progress(
|
|
|
183
217
|
finally:
|
|
184
218
|
renderer.stop()
|
|
185
219
|
_pipeline_footer(subcommand, t0, code)
|
|
220
|
+
_maybe_record_config_source(subcommand, cfg, code)
|
|
186
221
|
|
|
187
222
|
|
|
188
223
|
class PipelineProgress:
|
|
@@ -321,7 +356,11 @@ def _cmd_init(args: argparse.Namespace) -> int:
|
|
|
321
356
|
on_progress=progress.on_progress if progress is not None else None,
|
|
322
357
|
on_progress_console=progress.console if progress is not None else None,
|
|
323
358
|
)
|
|
324
|
-
|
|
359
|
+
# Graph-only install (cocoindex absent, e.g. macOS Intel): skip the vectors phase
|
|
360
|
+
# and proceed to the graph build rather than failing — the graph layer is the
|
|
361
|
+
# supported surface there. A genuine non-zero cocoindex exit still fails.
|
|
362
|
+
vectors_skipped = _is_cocoindex_preflight_blocker(coco)
|
|
363
|
+
if coco.returncode != 0 and not vectors_skipped:
|
|
325
364
|
_emit(
|
|
326
365
|
{
|
|
327
366
|
"success": False,
|
|
@@ -332,6 +371,8 @@ def _cmd_init(args: argparse.Namespace) -> int:
|
|
|
332
371
|
}
|
|
333
372
|
)
|
|
334
373
|
return 1
|
|
374
|
+
if vectors_skipped:
|
|
375
|
+
print(_VECTORS_SKIPPED_GRAPH_ONLY, file=sys.stderr, flush=True)
|
|
335
376
|
if not args.quiet:
|
|
336
377
|
print(file=sys.stderr, flush=True)
|
|
337
378
|
g = run_build_ast_graph(
|
|
@@ -354,7 +395,16 @@ def _cmd_init(args: argparse.Namespace) -> int:
|
|
|
354
395
|
}
|
|
355
396
|
)
|
|
356
397
|
return 1
|
|
357
|
-
_emit(
|
|
398
|
+
_emit(
|
|
399
|
+
{
|
|
400
|
+
"success": True,
|
|
401
|
+
"message": (
|
|
402
|
+
"init completed (graph-only; vectors skipped — vector stack not installed)"
|
|
403
|
+
if vectors_skipped
|
|
404
|
+
else "init completed"
|
|
405
|
+
),
|
|
406
|
+
}
|
|
407
|
+
)
|
|
358
408
|
return 0
|
|
359
409
|
|
|
360
410
|
return _run_with_pipeline_progress(
|
|
@@ -383,7 +433,8 @@ def _cmd_increment(args: argparse.Namespace) -> int:
|
|
|
383
433
|
on_progress=progress.on_progress if progress is not None else None,
|
|
384
434
|
on_progress_console=progress.console if progress is not None else None,
|
|
385
435
|
)
|
|
386
|
-
|
|
436
|
+
vectors_skipped = _is_cocoindex_preflight_blocker(coco)
|
|
437
|
+
if coco.returncode != 0 and not vectors_skipped:
|
|
387
438
|
_emit(
|
|
388
439
|
{
|
|
389
440
|
"success": False,
|
|
@@ -394,9 +445,19 @@ def _cmd_increment(args: argparse.Namespace) -> int:
|
|
|
394
445
|
}
|
|
395
446
|
)
|
|
396
447
|
return 1
|
|
448
|
+
if vectors_skipped:
|
|
449
|
+
print(_VECTORS_SKIPPED_GRAPH_ONLY, file=sys.stderr, flush=True)
|
|
397
450
|
|
|
398
451
|
# If --vectors-only is set, skip graph update
|
|
399
452
|
if vectors_only:
|
|
453
|
+
if vectors_skipped:
|
|
454
|
+
_emit(
|
|
455
|
+
{
|
|
456
|
+
"success": True,
|
|
457
|
+
"message": "increment skipped: vector stack not installed (graph-only mode)",
|
|
458
|
+
}
|
|
459
|
+
)
|
|
460
|
+
return 0
|
|
400
461
|
_emit({"success": True, "message": "increment completed (Lance only; graph may be stale — see stderr)"})
|
|
401
462
|
return 0
|
|
402
463
|
|
|
@@ -439,7 +500,16 @@ def _cmd_increment(args: argparse.Namespace) -> int:
|
|
|
439
500
|
)
|
|
440
501
|
return 1
|
|
441
502
|
|
|
442
|
-
_emit(
|
|
503
|
+
_emit(
|
|
504
|
+
{
|
|
505
|
+
"success": True,
|
|
506
|
+
"message": (
|
|
507
|
+
"increment completed (graph only; vectors skipped — vector stack not installed)"
|
|
508
|
+
if vectors_skipped
|
|
509
|
+
else "increment completed (Lance + graph updated)"
|
|
510
|
+
),
|
|
511
|
+
}
|
|
512
|
+
)
|
|
443
513
|
return 0
|
|
444
514
|
|
|
445
515
|
return _run_with_pipeline_progress(
|
|
@@ -563,6 +633,7 @@ def _cmd_install(args: argparse.Namespace) -> int:
|
|
|
563
633
|
agents=args.agent, # list of str (may be empty)
|
|
564
634
|
scope=args.scope,
|
|
565
635
|
model=args.model,
|
|
636
|
+
surface=args.surface,
|
|
566
637
|
source_root=None, # None means cwd; installer confirms interactively
|
|
567
638
|
quiet=bool(args.quiet),
|
|
568
639
|
verbose=bool(args.verbose),
|
|
@@ -605,14 +676,25 @@ def _cmd_erase(args: argparse.Namespace) -> int:
|
|
|
605
676
|
cfg = _resolved_from_ns(args)
|
|
606
677
|
_startup_hints(cfg)
|
|
607
678
|
cfg.apply_to_os_environ()
|
|
608
|
-
|
|
609
|
-
|
|
679
|
+
# Lazy import: build_ast_graph transitively pulls numpy/ladybug/pyarrow/
|
|
680
|
+
# tree_sitter (~54ms), and these filenames are only needed on the erase path.
|
|
681
|
+
# Keeping it out of the top-level import lets `java-codebase-rag --help` (and
|
|
682
|
+
# every other command) stay fast -- see the lazy-import invariant atop this file.
|
|
683
|
+
from build_ast_graph import BUILDER_OWNED_INDEX_FILES
|
|
684
|
+
builder_paths = [cfg.ladybug_path.parent / name for name in BUILDER_OWNED_INDEX_FILES]
|
|
685
|
+
operator_paths = [cfg.index_dir / name for name in OPERATOR_OWNED_INDEX_FILES]
|
|
686
|
+
to_describe: list[Path] = [
|
|
687
|
+
cfg.ladybug_path,
|
|
688
|
+
cfg.cocoindex_db,
|
|
689
|
+
*builder_paths,
|
|
690
|
+
*operator_paths,
|
|
691
|
+
]
|
|
610
692
|
if cfg.index_dir.is_dir():
|
|
611
693
|
try:
|
|
612
694
|
import lancedb
|
|
613
695
|
|
|
614
696
|
db = lancedb.connect(str(cfg.index_dir.resolve()))
|
|
615
|
-
for name in db.
|
|
697
|
+
for name in db.list_tables():
|
|
616
698
|
to_describe.append(cfg.index_dir / name)
|
|
617
699
|
except Exception:
|
|
618
700
|
pass
|
|
@@ -627,7 +709,17 @@ def _cmd_erase(args: argparse.Namespace) -> int:
|
|
|
627
709
|
file=sys.stderr,
|
|
628
710
|
)
|
|
629
711
|
return 2
|
|
630
|
-
|
|
712
|
+
try:
|
|
713
|
+
ans = input("Delete these paths? [y/N]: ").strip().lower()
|
|
714
|
+
except EOFError:
|
|
715
|
+
# Non-interactive stdin that nonetheless reported isatty() == True
|
|
716
|
+
# (the Windows NUL device is a character device, so isatty() lies).
|
|
717
|
+
# Treat it as a refusal instead of crashing with an EOF traceback.
|
|
718
|
+
print(
|
|
719
|
+
"java-codebase-rag erase: non-interactive stdin; pass --yes to confirm.",
|
|
720
|
+
file=sys.stderr,
|
|
721
|
+
)
|
|
722
|
+
return 2
|
|
631
723
|
if ans not in ("y", "yes"):
|
|
632
724
|
print("Aborted.", file=sys.stderr)
|
|
633
725
|
return 2
|
|
@@ -643,21 +735,29 @@ def _cmd_erase(args: argparse.Namespace) -> int:
|
|
|
643
735
|
)
|
|
644
736
|
elif drop.returncode != 0:
|
|
645
737
|
print(clip(drop.stderr, 4000), file=sys.stderr)
|
|
646
|
-
# Remove the LadybugDB graph, the cocoindex state store, and
|
|
647
|
-
# builder
|
|
648
|
-
#
|
|
649
|
-
#
|
|
650
|
-
#
|
|
651
|
-
#
|
|
738
|
+
# Remove the LadybugDB graph, the cocoindex state store, and every
|
|
739
|
+
# builder-owned bookkeeping file next to code_graph.lbug (the content-hash
|
|
740
|
+
# store, its atomic-write temp, and the incremental crash marker). Each is
|
|
741
|
+
# removed by type (see _rm_any): code_graph.lbug is a file here but may be
|
|
742
|
+
# a dir under kuzu, while cocoindex.db is a directory — a type-blind delete
|
|
743
|
+
# silently no-oped on one or the other, and the builder files were never
|
|
744
|
+
# targeted at all (issues #346 / #349 / #350). The list comes from
|
|
745
|
+
# build_ast_graph.BUILDER_OWNED_INDEX_FILES so erase and the builder cannot drift.
|
|
652
746
|
_rm_any(cfg.ladybug_path)
|
|
653
747
|
_rm_any(cfg.cocoindex_db)
|
|
654
|
-
|
|
748
|
+
for builder_path in builder_paths:
|
|
749
|
+
_rm_any(builder_path)
|
|
750
|
+
# Operator-owned index files (the config_source pointer recording which
|
|
751
|
+
# YAML built this index) — owned by the CLI/installer, not the graph
|
|
752
|
+
# builder, so removed here rather than via BUILDER_OWNED_INDEX_FILES.
|
|
753
|
+
for operator_path in operator_paths:
|
|
754
|
+
_rm_any(operator_path)
|
|
655
755
|
if cfg.index_dir.is_dir():
|
|
656
756
|
try:
|
|
657
757
|
import lancedb
|
|
658
758
|
|
|
659
759
|
db = lancedb.connect(str(cfg.index_dir.resolve()))
|
|
660
|
-
for name in list(db.
|
|
760
|
+
for name in list(db.list_tables()):
|
|
661
761
|
try:
|
|
662
762
|
db.drop_table(name)
|
|
663
763
|
except Exception as exc:
|
|
@@ -865,6 +965,17 @@ def build_parser() -> argparse.ArgumentParser:
|
|
|
865
965
|
default=None,
|
|
866
966
|
help="Embedding model path or 'auto' (default: auto).",
|
|
867
967
|
)
|
|
968
|
+
install.add_argument(
|
|
969
|
+
"--surface",
|
|
970
|
+
choices=["mcp", "cli"],
|
|
971
|
+
default=None,
|
|
972
|
+
help=(
|
|
973
|
+
"Agent surface to install: 'mcp' (stdio MCP server + explore-codebase "
|
|
974
|
+
"skill + explorer-rag-enhanced subagent) or 'cli' (jrag console-script "
|
|
975
|
+
"skill + explorer-rag-cli subagent, no MCP entry). Omit to choose "
|
|
976
|
+
"interactively; non-interactive mode defaults to 'mcp'."
|
|
977
|
+
),
|
|
978
|
+
)
|
|
868
979
|
_add_verbosity_flags(install)
|
|
869
980
|
install.set_defaults(handler=_cmd_install)
|
|
870
981
|
|
java_codebase_rag/config.py
CHANGED
|
@@ -18,6 +18,18 @@ SettingSource = Literal["cli", "env", "yaml", "default"]
|
|
|
18
18
|
YAML_CONFIG_FILENAMES = (".java-codebase-rag.yml", ".java-codebase-rag.yaml")
|
|
19
19
|
LEGACY_YAML_FILENAMES = (".lancedb-mcp.yml", ".lancedb-mcp.yaml")
|
|
20
20
|
|
|
21
|
+
# Pointer file written into the index dir at index time so discovery can locate
|
|
22
|
+
# a YAML that does not sit beside the index-dir anchor — e.g. a config living in
|
|
23
|
+
# a sibling ``project-context/`` dir when the agent's cwd is inside a
|
|
24
|
+
# microservice (a descendant of the index anchor, a sibling of the config).
|
|
25
|
+
# Contains one line: the absolute path of the YAML used to build the index. A
|
|
26
|
+
# direct YAML at the anchor always wins; the pointer only fires when the anchor
|
|
27
|
+
# has no YAML beside it (see ``_effective_config_dir``).
|
|
28
|
+
CONFIG_SOURCE_FILENAME = "config_source"
|
|
29
|
+
# Operator-owned files inside the index dir that ``erase`` removes. Kept separate
|
|
30
|
+
# from ``build_ast_graph.BUILDER_OWNED_INDEX_FILES`` (builder-owned artifacts).
|
|
31
|
+
OPERATOR_OWNED_INDEX_FILES = (CONFIG_SOURCE_FILENAME,)
|
|
32
|
+
|
|
21
33
|
ENV_INDEX_DIR = "JAVA_CODEBASE_RAG_INDEX_DIR"
|
|
22
34
|
# Public operator contract is six names: INDEX_DIR, DEBUG_CONTEXT, RUN_HEAVY, SBERT_MODEL, SBERT_DEVICE, HINTS_ENABLED.
|
|
23
35
|
# SOURCE_ROOT is still required for MCP / subprocess Java tree resolution (see mcp.json.example); it is not folded into the headline "5".
|
|
@@ -202,20 +214,27 @@ def discover_project_root(start: Path) -> Path | None:
|
|
|
202
214
|
First match wins (closest to start). Config file takes priority over index
|
|
203
215
|
directory at the same level. Stops at $HOME inclusive — checks $HOME itself
|
|
204
216
|
but does not walk past it. Returns None if no marker found.
|
|
217
|
+
|
|
218
|
+
A bare ``.java-codebase-rag/`` index directory at ``$HOME`` is intentionally
|
|
219
|
+
NOT treated as an anchor (issue #357): a stray home-level index (e.g. an
|
|
220
|
+
accidental ``init`` run from home) would otherwise hijack resolution for any
|
|
221
|
+
command run from a ``$HOME`` subdir without its own marker, silently reading
|
|
222
|
+
and writing the home-level index. A config file at ``$HOME`` still anchors.
|
|
205
223
|
"""
|
|
206
224
|
start = start.resolve()
|
|
207
225
|
home = Path.home().resolve()
|
|
208
226
|
|
|
209
227
|
current = start
|
|
210
228
|
while True:
|
|
211
|
-
# Config file is the primary anchor
|
|
229
|
+
# Config file is the primary anchor (valid at every level, including $HOME).
|
|
212
230
|
if find_yaml_config_file(current) is not None:
|
|
213
231
|
return current
|
|
214
|
-
# Index directory is the secondary anchor (supports indexes without config)
|
|
215
|
-
|
|
232
|
+
# Index directory is the secondary anchor (supports indexes without config),
|
|
233
|
+
# but NOT at $HOME — see the docstring for the cross-project hijack rationale.
|
|
234
|
+
if current != home and _has_index_dir(current):
|
|
216
235
|
return current
|
|
217
236
|
|
|
218
|
-
# Stop if we've reached home (check
|
|
237
|
+
# Stop if we've reached home (config-file check above already handled home)
|
|
219
238
|
if current == home:
|
|
220
239
|
return None
|
|
221
240
|
|
|
@@ -227,6 +246,61 @@ def discover_project_root(start: Path) -> Path | None:
|
|
|
227
246
|
current = parent
|
|
228
247
|
|
|
229
248
|
|
|
249
|
+
_stale_pointer_seen: set[str] = set()
|
|
250
|
+
|
|
251
|
+
|
|
252
|
+
def _config_dir_from_pointer(anchor: Path) -> Path | None:
|
|
253
|
+
"""Return the YAML config dir recorded in the index dir's ``config_source`` pointer.
|
|
254
|
+
|
|
255
|
+
Reads ``<anchor>/.java-codebase-rag/config_source`` (one absolute path). If it
|
|
256
|
+
names an existing ``.java-codebase-rag.yml`` / ``.yaml``, returns that file's
|
|
257
|
+
parent directory; otherwise (missing/blank/stale) returns ``None``. Used only
|
|
258
|
+
when the anchor has no direct YAML — see :func:`_effective_config_dir`.
|
|
259
|
+
"""
|
|
260
|
+
pointer = anchor / ".java-codebase-rag" / CONFIG_SOURCE_FILENAME
|
|
261
|
+
if not pointer.is_file():
|
|
262
|
+
return None
|
|
263
|
+
try:
|
|
264
|
+
raw = pointer.read_text(encoding="utf-8").strip()
|
|
265
|
+
except OSError:
|
|
266
|
+
return None
|
|
267
|
+
if not raw:
|
|
268
|
+
return None
|
|
269
|
+
target = Path(raw).expanduser()
|
|
270
|
+
if not target.is_absolute():
|
|
271
|
+
# Relative to the anchor (the index-dir parent), not the pointer file.
|
|
272
|
+
target = (anchor / target).resolve()
|
|
273
|
+
if not target.is_file() or target.name not in YAML_CONFIG_FILENAMES:
|
|
274
|
+
key = str(pointer.resolve())
|
|
275
|
+
if key not in _stale_pointer_seen:
|
|
276
|
+
_stale_pointer_seen.add(key)
|
|
277
|
+
print(
|
|
278
|
+
"java-codebase-rag: ignoring stale index pointer "
|
|
279
|
+
f"{pointer} -> {raw} (target missing or not a config file).",
|
|
280
|
+
file=sys.stderr,
|
|
281
|
+
)
|
|
282
|
+
return None
|
|
283
|
+
return target.parent
|
|
284
|
+
|
|
285
|
+
|
|
286
|
+
def _effective_config_dir(config_dir: Path) -> Path:
|
|
287
|
+
"""Resolve the directory YAML config fields are relative to.
|
|
288
|
+
|
|
289
|
+
A direct ``.java-codebase-rag.yml`` / ``.yaml`` in ``config_dir`` always wins.
|
|
290
|
+
Otherwise, if ``config_dir`` hosts the ``.java-codebase-rag/`` index dir and
|
|
291
|
+
that index remembers its config via a ``config_source`` pointer, follow it to
|
|
292
|
+
the YAML's directory. This lets a config in a sibling dir (e.g.
|
|
293
|
+
``project-context/`` beside the Java tree) be found when discovery anchors on
|
|
294
|
+
the index dir from inside a microservice — without an env var or flag, and
|
|
295
|
+
with YAML-relative fields (``index_dir``, ``source_root``, ``embedding.model``)
|
|
296
|
+
resolving against the YAML's home rather than the index anchor. Falls back to
|
|
297
|
+
``config_dir`` unchanged when neither applies.
|
|
298
|
+
"""
|
|
299
|
+
if find_yaml_config_file(config_dir) is not None:
|
|
300
|
+
return config_dir
|
|
301
|
+
return _config_dir_from_pointer(config_dir) or config_dir
|
|
302
|
+
|
|
303
|
+
|
|
230
304
|
def load_yaml_mapping(source_root: Path) -> dict[str, Any]:
|
|
231
305
|
path = find_yaml_config_file(source_root)
|
|
232
306
|
if path is None:
|
|
@@ -237,7 +311,17 @@ def load_yaml_mapping(source_root: Path) -> dict[str, Any]:
|
|
|
237
311
|
return {}
|
|
238
312
|
try:
|
|
239
313
|
data = yaml.safe_load(path.read_text(encoding="utf-8"))
|
|
240
|
-
except
|
|
314
|
+
except (yaml.YAMLError, OSError, UnicodeDecodeError) as exc:
|
|
315
|
+
# Best-effort loader: a missing/unreadable/malformed config must NOT abort
|
|
316
|
+
# startup — return {} and proceed with defaults. Narrowing this to
|
|
317
|
+
# ``yaml.YAMLError`` alone let OSError (chmod 000, stat/read TOCTOU) and
|
|
318
|
+
# UnicodeDecodeError (non-UTF-8 config) propagate to the caller; the broader
|
|
319
|
+
# tuple restores the graceful-degradation contract while still surfacing the
|
|
320
|
+
# problem on stderr.
|
|
321
|
+
print(
|
|
322
|
+
f"java-codebase-rag: could not load config {path}: {exc}; ignoring config.",
|
|
323
|
+
file=sys.stderr,
|
|
324
|
+
)
|
|
241
325
|
return {}
|
|
242
326
|
return data if isinstance(data, dict) else {}
|
|
243
327
|
|
|
@@ -255,6 +339,10 @@ class ResolvedOperatorConfig:
|
|
|
255
339
|
embedding_model_source: SettingSource
|
|
256
340
|
embedding_device_source: SettingSource
|
|
257
341
|
hints_enabled_source: SettingSource
|
|
342
|
+
# Absolute path of the YAML actually loaded (None when built-in defaults were
|
|
343
|
+
# used with no config file). Recorded into the index dir at index time so a
|
|
344
|
+
# later discovery run from a sibling/cwd can relocate this config.
|
|
345
|
+
yaml_config_path: Path | None = None
|
|
258
346
|
|
|
259
347
|
def apply_to_os_environ(self) -> None:
|
|
260
348
|
"""Make downstream modules (server, ladybug_queries, flows) see a consistent environment.
|
|
@@ -394,21 +482,26 @@ def resolve_operator_config(
|
|
|
394
482
|
# Phase 1: Find the config file directory
|
|
395
483
|
if source_root is not None:
|
|
396
484
|
# CLI flag provided: use it as both config_dir and effective source_root
|
|
397
|
-
# (skip YAML source_root check - CLI wins)
|
|
485
|
+
# (skip YAML source_root check - CLI wins). ``_effective_config_dir`` may
|
|
486
|
+
# rebase config_dir to a YAML reached via the index-dir pointer; root is
|
|
487
|
+
# untouched (explicit source_root wins).
|
|
398
488
|
root = source_root.expanduser().resolve()
|
|
399
|
-
config_dir = root
|
|
489
|
+
config_dir = _effective_config_dir(root)
|
|
400
490
|
yaml_dict = load_yaml_mapping(config_dir)
|
|
401
491
|
else:
|
|
402
492
|
# Check env var first
|
|
403
493
|
env_raw = os.environ.get(ENV_SOURCE_ROOT, "").strip()
|
|
404
494
|
if env_raw:
|
|
405
495
|
root = Path(env_raw).expanduser().resolve()
|
|
406
|
-
config_dir = root
|
|
496
|
+
config_dir = _effective_config_dir(root)
|
|
407
497
|
yaml_dict = load_yaml_mapping(config_dir)
|
|
408
498
|
else:
|
|
409
499
|
# Walk up to find config dir
|
|
410
500
|
discovered = discover_project_root(Path.cwd())
|
|
411
501
|
config_dir = discovered if discovered is not None else Path.cwd().resolve()
|
|
502
|
+
# Follow an index-dir pointer to the real config dir when the anchor
|
|
503
|
+
# has no YAML beside it (e.g. config in a sibling dir).
|
|
504
|
+
config_dir = _effective_config_dir(config_dir)
|
|
412
505
|
# Load YAML from config dir
|
|
413
506
|
yaml_dict = load_yaml_mapping(config_dir)
|
|
414
507
|
|
|
@@ -462,9 +555,35 @@ def resolve_operator_config(
|
|
|
462
555
|
embedding_model_source=model_src,
|
|
463
556
|
embedding_device_source=device_src,
|
|
464
557
|
hints_enabled_source=hints_src,
|
|
558
|
+
yaml_config_path=find_yaml_config_file(config_dir),
|
|
465
559
|
)
|
|
466
560
|
|
|
467
561
|
|
|
562
|
+
def write_config_source_pointer(
|
|
563
|
+
*, index_dir: Path, yaml_config_path: Path | None
|
|
564
|
+
) -> None:
|
|
565
|
+
"""Record the YAML config path inside the index dir (best-effort).
|
|
566
|
+
|
|
567
|
+
Writes ``<index_dir>/config_source`` with the YAML's absolute path so a later
|
|
568
|
+
discovery run that anchors on the index dir (but has no YAML beside it) can
|
|
569
|
+
relocate the config via :func:`_effective_config_dir`. No-op when
|
|
570
|
+
``yaml_config_path`` is None (pure-default build — nothing to remember). Never
|
|
571
|
+
raises: the pointer is an optimization, not a correctness requirement — a
|
|
572
|
+
missing/unreadable pointer just falls back to built-in defaults.
|
|
573
|
+
"""
|
|
574
|
+
if yaml_config_path is None:
|
|
575
|
+
return
|
|
576
|
+
try:
|
|
577
|
+
index_dir.mkdir(parents=True, exist_ok=True)
|
|
578
|
+
content = str(yaml_config_path.resolve()) + "\n"
|
|
579
|
+
target = index_dir / CONFIG_SOURCE_FILENAME
|
|
580
|
+
tmp = index_dir / (CONFIG_SOURCE_FILENAME + ".tmp")
|
|
581
|
+
tmp.write_text(content, encoding="utf-8")
|
|
582
|
+
os.replace(tmp, target)
|
|
583
|
+
except OSError:
|
|
584
|
+
pass
|
|
585
|
+
|
|
586
|
+
|
|
468
587
|
def index_dir_has_existing_artifacts(index_dir: Path) -> tuple[bool, list[str]]:
|
|
469
588
|
"""True if graph dir or any Lance table already exists under index_dir."""
|
|
470
589
|
paths: list[str] = []
|
|
@@ -476,7 +595,7 @@ def index_dir_has_existing_artifacts(index_dir: Path) -> tuple[bool, list[str]]:
|
|
|
476
595
|
import lancedb
|
|
477
596
|
|
|
478
597
|
db = lancedb.connect(str(index_dir.resolve()))
|
|
479
|
-
for name in db.
|
|
598
|
+
for name in db.list_tables():
|
|
480
599
|
paths.append(str((index_dir / name).resolve()) + " (Lance table)")
|
|
481
600
|
except Exception:
|
|
482
601
|
pass
|