Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 7 additions & 2 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,12 @@ jobs:
# kuzu/ladybug graph layer, which had never been run on these OSes
# before this matrix. Promote an OS to a hard gate by dropping it from
# the continue-on-error expression once it is reliably green.
os: [ubuntu-latest, macos-latest, windows-latest]
#
# macos-13 is the only Intel (x86_64) runner: PEP 508 markers gate the
# vector trio (cocoindex[lancedb]/lancedb/sentence-transformers) off
# there, so it installs graph-only and proves the graph-only boot path
# (tests/test_graph_only_boot.py) plus clean skips of vector tests.
os: [ubuntu-latest, macos-latest, macos-13, windows-latest]
continue-on-error: ${{ matrix.os != 'ubuntu-latest' }}
runs-on: ${{ matrix.os }}
steps:
Expand Down Expand Up @@ -64,7 +69,7 @@ jobs:
if: steps.changes.outputs.code == 'true'
run: python scripts/generate_edge_navigation.py --check
- name: Cache HuggingFace models
if: steps.changes.outputs.code == 'true'
if: steps.changes.outputs.code == 'true' && matrix.os != 'macos-13'
uses: actions/cache@v4
with:
path: ~/.cache/huggingface
Expand Down
6 changes: 3 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,8 +41,8 @@ The rest of this README is the install, the tool/command orientation, and the re
pip install java-codebase-rag
```

Python **3.11+** required, on **Linux, macOS, and Windows**every native dependency (LanceDB, LadybugDB/kuzu, CocoIndex) ships a wheel for each platform. After install, `java-codebase-rag --help` should print the CLI groups.
The package includes the CocoIndex lifecycle dependency used by `init`, `increment`, `reprocess`, and `erase`.
Python **3.11+** required, on **Linux, macOS, and Windows**. On Linux, Windows, and **Apple Silicon** Macs every native dependency (LanceDB, LadybugDB/kuzu, CocoIndex) ships a wheel and you get the full semantic + graph search. **Intel Macs (x86_64) install graph-only**: PyTorch ≥2.3 and LanceDB ≥0.26 dropped macOS Intel wheels, so the vector stack is auto-excluded via PEP 508 markers — `pip install java-codebase-rag` works out of the box, the graph layer (`find` / `describe` / `neighbors` / `resolve`) is fully usable, and the `search` (semantic) tool reports it is unavailable rather than failing. After install, `java-codebase-rag --help` should print the CLI groups.
The package includes the CocoIndex lifecycle dependency used by `init`, `increment`, `reprocess`, and `erase` on platforms that have it (it is absent on Intel Mac).

### Interactive setup (recommended)

Expand Down Expand Up @@ -270,7 +270,7 @@ full design and per-PR breakdown.
git clone https://github.com/HumanBean17/java-codebase-rag
cd java-codebase-rag
python3 -m venv .venv
.venv/bin/pip install -r requirements.txt
.venv/bin/pip install -e ".[dev]"
```

The `cocoindex` package powers lifecycle commands that run the indexer (`init`, `increment`, `reprocess`, `erase`). Search and MCP navigation do not invoke it directly.
Expand Down
68 changes: 59 additions & 9 deletions java_codebase_rag/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,15 @@
resolve_operator_config,
)
from java_codebase_rag._fdlimit import raise_fd_limit
from java_codebase_rag.pipeline import clip, run_build_ast_graph, run_cocoindex_drop, run_cocoindex_update, run_incremental_graph
from java_codebase_rag.pipeline import (
clip,
is_cocoindex_preflight_blocker,
is_graph_preflight_blocker,
run_build_ast_graph,
run_cocoindex_drop,
run_cocoindex_update,
run_incremental_graph,
)
from java_ontology import VALID_UNRESOLVED_CALL_REASONS

LADYBUG_INCREMENTAL_TRACKING_ISSUE_URL = "https://github.com/HumanBean17/java-codebase-rag/issues/73"
Expand Down Expand Up @@ -61,6 +69,12 @@ def _reprocess_drift_graph_only_line(index_dir: Path) -> str:
)


_VECTORS_SKIPPED_GRAPH_ONLY = (
"java-codebase-rag: vectors skipped — vector stack not installed on this platform "
"(graph-only mode). The graph is built/refreshed; semantic search is unavailable."
)


def _reprocess_exit_code(payload: dict[str, Any]) -> int:
if payload.get("success"):
return 0
Expand All @@ -70,16 +84,17 @@ def _reprocess_exit_code(payload: dict[str, Any]) -> int:
return 1


# Preflight detection must stay aligned with stub CompletedProcess shapes in
# java_codebase_rag/pipeline.py (missing cocoindex / flow / build_ast_graph.py).
# Preflight detection delegates to pipeline.is_cocoindex_preflight_blocker /
# is_graph_preflight_blocker, which are co-located with the stub CompletedProcess shapes
# they must match (missing cocoindex / flow / build_ast_graph.py).
def _is_cocoindex_preflight_blocker(coco: Any) -> bool:
"""True when ``run_cocoindex_update`` returned without spawning cocoindex."""
return bool(coco.returncode in (126, 127) and len(getattr(coco, "args", ()) or ()) <= 1)
return is_cocoindex_preflight_blocker(coco)


def _is_graph_preflight_blocker(g: Any) -> bool:
"""True when ``run_build_ast_graph`` returned without spawning the builder."""
return bool(g.returncode in (126, 127) and len(getattr(g, "args", ()) or ()) <= 1)
return is_graph_preflight_blocker(g)


def _emit_reprocess_selective_tty(*, mode: str) -> None:
Expand Down Expand Up @@ -321,7 +336,11 @@ def work(progress: "PipelineProgress | None") -> int:
on_progress=progress.on_progress if progress is not None else None,
on_progress_console=progress.console if progress is not None else None,
)
if coco.returncode != 0:
# Graph-only install (cocoindex absent, e.g. macOS Intel): skip the vectors phase
# and proceed to the graph build rather than failing — the graph layer is the
# supported surface there. A genuine non-zero cocoindex exit still fails.
vectors_skipped = _is_cocoindex_preflight_blocker(coco)
if coco.returncode != 0 and not vectors_skipped:
_emit(
{
"success": False,
Expand All @@ -332,6 +351,8 @@ def work(progress: "PipelineProgress | None") -> int:
}
)
return 1
if vectors_skipped:
print(_VECTORS_SKIPPED_GRAPH_ONLY, file=sys.stderr, flush=True)
if not args.quiet:
print(file=sys.stderr, flush=True)
g = run_build_ast_graph(
Expand All @@ -354,7 +375,16 @@ def work(progress: "PipelineProgress | None") -> int:
}
)
return 1
_emit({"success": True, "message": "init completed"})
_emit(
{
"success": True,
"message": (
"init completed (graph-only; vectors skipped — vector stack not installed)"
if vectors_skipped
else "init completed"
),
}
)
return 0

return _run_with_pipeline_progress(
Expand Down Expand Up @@ -383,7 +413,8 @@ def work(progress: "PipelineProgress | None") -> int:
on_progress=progress.on_progress if progress is not None else None,
on_progress_console=progress.console if progress is not None else None,
)
if coco.returncode != 0:
vectors_skipped = _is_cocoindex_preflight_blocker(coco)
if coco.returncode != 0 and not vectors_skipped:
_emit(
{
"success": False,
Expand All @@ -394,9 +425,19 @@ def work(progress: "PipelineProgress | None") -> int:
}
)
return 1
if vectors_skipped:
print(_VECTORS_SKIPPED_GRAPH_ONLY, file=sys.stderr, flush=True)

# If --vectors-only is set, skip graph update
if vectors_only:
if vectors_skipped:
_emit(
{
"success": True,
"message": "increment skipped: vector stack not installed (graph-only mode)",
}
)
return 0
_emit({"success": True, "message": "increment completed (Lance only; graph may be stale — see stderr)"})
return 0

Expand Down Expand Up @@ -439,7 +480,16 @@ def work(progress: "PipelineProgress | None") -> int:
)
return 1

_emit({"success": True, "message": "increment completed (Lance + graph updated)"})
_emit(
{
"success": True,
"message": (
"increment completed (graph only; vectors skipped — vector stack not installed)"
if vectors_skipped
else "increment completed (Lance + graph updated)"
),
}
)
return 0

return _run_with_pipeline_progress(
Expand Down
27 changes: 23 additions & 4 deletions java_codebase_rag/installer.py
Original file line number Diff line number Diff line change
Expand Up @@ -1022,7 +1022,7 @@ def run_init_if_needed(
index_dir_has_existing_artifacts,
resolve_operator_config,
)
from java_codebase_rag.pipeline import run_build_ast_graph, run_cocoindex_update
from java_codebase_rag.pipeline import is_cocoindex_preflight_blocker, run_build_ast_graph, run_cocoindex_update

has_existing, _ = index_dir_has_existing_artifacts(index_dir)
if has_existing:
Expand Down Expand Up @@ -1061,13 +1061,23 @@ def run_init_if_needed(
on_progress=on_progress,
on_progress_console=on_progress_console,
)
if coco.returncode != 0:
# Graph-only install (cocoindex absent, e.g. macOS Intel): skip the vectors phase
# and build the graph rather than failing install. A genuine non-zero cocoindex
# exit still fails.
vectors_skipped = is_cocoindex_preflight_blocker(coco)
if coco.returncode != 0 and not vectors_skipped:
print(
f"Error: CocoIndex update failed with code {coco.returncode}",
file=sys.stderr,
)
index_ok = False
else:
if vectors_skipped:
print(
"java-codebase-rag: vectors skipped — vector stack not installed on this "
"platform (graph-only mode). Building graph only; semantic search is unavailable.",
file=sys.stderr,
)
g = run_build_ast_graph(
source_root=cfg.source_root,
ladybug_path=cfg.ladybug_path,
Expand Down Expand Up @@ -1596,7 +1606,7 @@ def run_update(
index_dir_has_existing_artifacts,
resolve_operator_config,
)
from java_codebase_rag.pipeline import run_cocoindex_update, run_incremental_graph
from java_codebase_rag.pipeline import is_cocoindex_preflight_blocker, run_cocoindex_update, run_incremental_graph

project_root = discover_project_root(cwd)
if project_root is None:
Expand Down Expand Up @@ -1659,13 +1669,22 @@ def run_update(
on_progress=on_progress,
on_progress_console=on_progress_console,
)
if coco.returncode != 0:
# Graph-only install (cocoindex absent): skip the vectors catch-up and run the
# graph catch-up only. A genuine non-zero cocoindex exit still fails.
vectors_skipped = is_cocoindex_preflight_blocker(coco)
if coco.returncode != 0 and not vectors_skipped:
print(
f"Error: Lance index update failed with code {coco.returncode}",
file=sys.stderr,
)
index_ok = False
else:
if vectors_skipped:
print(
"java-codebase-rag: vectors skipped — vector stack not installed on this "
"platform (graph-only mode). Running graph catch-up only.",
file=sys.stderr,
)
g = run_incremental_graph(
source_root=cfg.source_root,
ladybug_path=cfg.ladybug_path,
Expand Down
20 changes: 20 additions & 0 deletions java_codebase_rag/pipeline.py
Original file line number Diff line number Diff line change
Expand Up @@ -180,6 +180,26 @@ def _maybe_run_serialized_optimize(
print(f"java-codebase-rag: optimize failed: {exc}", file=sys.stderr)


def is_cocoindex_preflight_blocker(proc: subprocess.CompletedProcess[str]) -> bool:
"""True when ``run_cocoindex_update`` returned a pre-spawn stub, not a real cocoindex run.

The stubs are emitted by ``_run_cocoindex_update_impl`` just below:
* returncode 127, args=[exe] -> cocoindex binary not installed (graph-only install,
e.g. macOS Intel where the vector extra is gated off).
* returncode 126, args=[] -> ``java_index_flow_lancedb.py`` missing from the bundle.
A real cocoindex run has ``args`` = the full command list (length > 1), so the
``len(args) <= 1`` guard distinguishes a stub from a genuine non-zero exit. This is the
single authoritative detector — ``cli.py`` and ``installer.py`` both call it so the
"treat as skip, not failure" decision stays aligned with the stub shapes here.
"""
return bool(proc.returncode in (126, 127) and len(getattr(proc, "args", ()) or ()) <= 1)


def is_graph_preflight_blocker(proc: subprocess.CompletedProcess[str]) -> bool:
"""True when ``run_build_ast_graph`` returned a pre-spawn stub (builder missing)."""
return bool(proc.returncode in (126, 127) and len(getattr(proc, "args", ()) or ()) <= 1)


def _run_cocoindex_update_impl(
env: dict[str, str],
*,
Expand Down
31 changes: 28 additions & 3 deletions mcp_v2.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,10 +22,14 @@
import sys
from pathlib import Path
import threading
from typing import Annotated, Any, Literal, get_args
from typing import Annotated, Any, Literal, TYPE_CHECKING, get_args

from pydantic import BaseModel, ConfigDict, Field, TypeAdapter, ValidationError, model_validator, validate_call
from sentence_transformers import SentenceTransformer

if TYPE_CHECKING:
# Eager import would pull torch at module load. The vector stack is optional (graph-only
# installs ship without torch/lancedb); it is imported lazily in _get_sentence_transformer.
from sentence_transformers import SentenceTransformer

from graph_types import (
NodeRef,
Expand All @@ -41,8 +45,17 @@
from java_ontology import EDGE_SCHEMA
from ladybug_queries import LadybugGraph, OVERRIDE_AXIS_COMPOSED_EDGE_TYPES
from mcp_hints import MCP_HINTS_STRUCTURED_FIELD_DESCRIPTION
from search_lancedb import TABLES, run_search

# The vector stack (lancedb/torch, reached via search_lancedb) is optional — it is absent on
# graph-only installs (macOS Intel). Import eagerly when available so ``run_search``/``TABLES``
# exist as module attributes (tests monkeypatch ``mcp_v2.run_search``; callers use ``TABLES``);
# fall back to sentinels on ImportError so importing this module never fails and ``search_v2``
# can return a clean "vector search unavailable" envelope instead of crashing.
try:
from search_lancedb import TABLES, run_search
except ImportError: # graph-only install: no torch/lancedb
TABLES = {}
run_search = None
__all__ = [
"search_v2",
"find_v2",
Expand Down Expand Up @@ -158,6 +171,8 @@ def filter_frame_counters() -> dict[str, int]:

def _get_sentence_transformer(model_name: str, device: str | None) -> SentenceTransformer:
global _st_model
from sentence_transformers import SentenceTransformer

with _st_lock:
if _st_model is None:
_st_model = SentenceTransformer(
Expand Down Expand Up @@ -827,6 +842,16 @@ def search_v2(
if nf and (err := _nodefilter_applicability_error("symbol", nf)):
_log_fail_loud("applicability")
return SearchOutput(success=False, message=err, advisories=[], limit=None, offset=None)
if run_search is None:
# Graph-only install (no torch/lancedb): the vector stack is absent. Return a
# clean failure rather than crashing so the server keeps serving graph tools.
return SearchOutput(
success=False,
message="Vector search unavailable: graph-only mode (vector stack not installed).",
advisories=[],
limit=None,
offset=None,
)
model_name = resolved_sbert_model_for_process_env(SBERT_MODEL)
device = os.environ.get("SBERT_DEVICE") or None
model = _get_sentence_transformer(model_name, device)
Expand Down
10 changes: 7 additions & 3 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -25,10 +25,14 @@ classifiers = [
"Operating System :: MacOS :: MacOS X",
"Operating System :: Microsoft :: Windows",
]
# The vector/semantic-search trio (cocoindex[lancedb], lancedb, sentence-transformers
# -> torch) is gated off on Intel Mac (darwin + x86_64): torch >=2.3 and lancedb >=0.26
# dropped macOS x86_64 wheels, so the full stack is uninstallable there. On Intel Mac the
# package installs graph-only and works out of the box; every other platform is unchanged.
dependencies = [
"cocoindex[lancedb]>=1.0.7,<2",
"cocoindex[lancedb]>=1.0.7,<2; sys_platform != 'darwin' or platform_machine != 'x86_64'",
"ladybug>=0.17.1,<0.18",
"lancedb>=0.25.3,<0.31",
"lancedb>=0.25.3,<0.31; sys_platform != 'darwin' or platform_machine != 'x86_64'",
"mcp>=1.27.0,<2",
"numpy>=1.26.4,<2.5",
"pathspec>=1.0.4,<2",
Expand All @@ -37,7 +41,7 @@ dependencies = [
"PyYAML>=6.0.3,<7",
"questionary>=2.0,<3",
"rich>=14,<15",
"sentence-transformers>=5.4.0,<6",
"sentence-transformers>=5.4.0,<6; sys_platform != 'darwin' or platform_machine != 'x86_64'",
"tree-sitter>=0.25.2,<0.26",
"tree-sitter-java>=0.23.5,<0.24",
"unidiff>=0.7.3,<1",
Expand Down
12 changes: 10 additions & 2 deletions server.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,8 @@
from ladybug_queries import LadybugGraph, resolve_ladybug_path
from mcp.server.fastmcp import FastMCP
from pydantic import BaseModel, Field
from search_lancedb import TABLES
# NOTE: search_lancedb.TABLES is imported lazily in list_code_index_tables_payload() — it
# pulls lancedb/torch and is unavailable on graph-only installs (macOS Intel).

_COCOINDEX_TARGET = "java_index_flow_lancedb.py:JavaCodeIndexLance"
_INSTRUCTIONS = (
Expand Down Expand Up @@ -296,12 +297,19 @@ def _graph_meta_output() -> GraphMetaOutput:


def list_code_index_tables_payload() -> IndexInfoOutput:
try:
from search_lancedb import TABLES

tables = dict(TABLES)
except ImportError:
# Graph-only install (no lancedb): no Lance vector tables exist.
tables = {}
return IndexInfoOutput(
lancedb_uri=_resolve_lancedb_uri(),
embedding_model=resolved_sbert_model_for_process_env(SBERT_MODEL),
project_root=str(_project_root()),
cocoindex_target=_COCOINDEX_TARGET,
tables=dict(TABLES),
tables=tables,
graph=_graph_meta_output(),
)

Expand Down
Loading
Loading