java-codebase-rag 0.9.0__tar.gz → 0.9.2__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.
- {java_codebase_rag-0.9.0/java_codebase_rag.egg-info → java_codebase_rag-0.9.2}/PKG-INFO +1 -1
- {java_codebase_rag-0.9.0 → java_codebase_rag-0.9.2}/java_codebase_rag/config.py +27 -3
- {java_codebase_rag-0.9.0 → java_codebase_rag-0.9.2}/java_codebase_rag/jrag.py +20 -4
- {java_codebase_rag-0.9.0 → java_codebase_rag-0.9.2}/java_codebase_rag/lance_optimize.py +25 -0
- {java_codebase_rag-0.9.0 → java_codebase_rag-0.9.2}/java_codebase_rag/pipeline.py +20 -0
- {java_codebase_rag-0.9.0 → java_codebase_rag-0.9.2/java_codebase_rag.egg-info}/PKG-INFO +1 -1
- {java_codebase_rag-0.9.0 → java_codebase_rag-0.9.2}/java_codebase_rag.egg-info/SOURCES.txt +1 -0
- {java_codebase_rag-0.9.0 → java_codebase_rag-0.9.2}/java_index_flow_lancedb.py +66 -6
- {java_codebase_rag-0.9.0 → java_codebase_rag-0.9.2}/java_ontology.py +4 -0
- {java_codebase_rag-0.9.0 → java_codebase_rag-0.9.2}/pyproject.toml +1 -1
- {java_codebase_rag-0.9.0 → java_codebase_rag-0.9.2}/resolve_service.py +24 -0
- {java_codebase_rag-0.9.0 → java_codebase_rag-0.9.2}/server.py +25 -0
- {java_codebase_rag-0.9.0 → java_codebase_rag-0.9.2}/tests/test_config.py +4 -0
- {java_codebase_rag-0.9.0 → java_codebase_rag-0.9.2}/tests/test_jrag_traversal_direct.py +133 -2
- {java_codebase_rag-0.9.0 → java_codebase_rag-0.9.2}/tests/test_lance_optimize.py +75 -14
- {java_codebase_rag-0.9.0 → java_codebase_rag-0.9.2}/tests/test_mcp_v2.py +29 -0
- java_codebase_rag-0.9.2/tests/test_pipeline.py +103 -0
- {java_codebase_rag-0.9.0 → java_codebase_rag-0.9.2}/tests/test_resolve_service.py +47 -0
- {java_codebase_rag-0.9.0 → java_codebase_rag-0.9.2}/LICENSE +0 -0
- {java_codebase_rag-0.9.0 → java_codebase_rag-0.9.2}/README.md +0 -0
- {java_codebase_rag-0.9.0 → java_codebase_rag-0.9.2}/ast_java.py +0 -0
- {java_codebase_rag-0.9.0 → java_codebase_rag-0.9.2}/brownfield_events.py +0 -0
- {java_codebase_rag-0.9.0 → java_codebase_rag-0.9.2}/build_ast_graph.py +0 -0
- {java_codebase_rag-0.9.0 → java_codebase_rag-0.9.2}/chunk_heuristics.py +0 -0
- {java_codebase_rag-0.9.0 → java_codebase_rag-0.9.2}/graph_enrich.py +0 -0
- {java_codebase_rag-0.9.0 → java_codebase_rag-0.9.2}/graph_types.py +0 -0
- {java_codebase_rag-0.9.0 → java_codebase_rag-0.9.2}/index_common.py +0 -0
- {java_codebase_rag-0.9.0 → java_codebase_rag-0.9.2}/java_codebase_rag/__init__.py +0 -0
- {java_codebase_rag-0.9.0 → java_codebase_rag-0.9.2}/java_codebase_rag/_fdlimit.py +0 -0
- {java_codebase_rag-0.9.0 → java_codebase_rag-0.9.2}/java_codebase_rag/_stdio.py +0 -0
- {java_codebase_rag-0.9.0 → java_codebase_rag-0.9.2}/java_codebase_rag/cli.py +0 -0
- {java_codebase_rag-0.9.0 → java_codebase_rag-0.9.2}/java_codebase_rag/cli_format.py +0 -0
- {java_codebase_rag-0.9.0 → java_codebase_rag-0.9.2}/java_codebase_rag/cli_progress.py +0 -0
- {java_codebase_rag-0.9.0 → java_codebase_rag-0.9.2}/java_codebase_rag/install_data/agents/explorer-rag-cli.md +0 -0
- {java_codebase_rag-0.9.0 → java_codebase_rag-0.9.2}/java_codebase_rag/install_data/agents/explorer-rag-enhanced.md +0 -0
- {java_codebase_rag-0.9.0 → java_codebase_rag-0.9.2}/java_codebase_rag/install_data/skills/explore-codebase/SKILL.md +0 -0
- {java_codebase_rag-0.9.0 → java_codebase_rag-0.9.2}/java_codebase_rag/install_data/skills/explore-codebase-cli/SKILL.md +0 -0
- {java_codebase_rag-0.9.0 → java_codebase_rag-0.9.2}/java_codebase_rag/installer.py +0 -0
- {java_codebase_rag-0.9.0 → java_codebase_rag-0.9.2}/java_codebase_rag/jrag_envelope.py +0 -0
- {java_codebase_rag-0.9.0 → java_codebase_rag-0.9.2}/java_codebase_rag/jrag_hints.py +0 -0
- {java_codebase_rag-0.9.0 → java_codebase_rag-0.9.2}/java_codebase_rag/jrag_render.py +0 -0
- {java_codebase_rag-0.9.0 → java_codebase_rag-0.9.2}/java_codebase_rag/progress.py +0 -0
- {java_codebase_rag-0.9.0 → java_codebase_rag-0.9.2}/java_codebase_rag.egg-info/dependency_links.txt +0 -0
- {java_codebase_rag-0.9.0 → java_codebase_rag-0.9.2}/java_codebase_rag.egg-info/entry_points.txt +0 -0
- {java_codebase_rag-0.9.0 → java_codebase_rag-0.9.2}/java_codebase_rag.egg-info/requires.txt +0 -0
- {java_codebase_rag-0.9.0 → java_codebase_rag-0.9.2}/java_codebase_rag.egg-info/top_level.txt +0 -0
- {java_codebase_rag-0.9.0 → java_codebase_rag-0.9.2}/java_index_v1_common.py +0 -0
- {java_codebase_rag-0.9.0 → java_codebase_rag-0.9.2}/ladybug_queries.py +0 -0
- {java_codebase_rag-0.9.0 → java_codebase_rag-0.9.2}/mcp_hints.py +0 -0
- {java_codebase_rag-0.9.0 → java_codebase_rag-0.9.2}/mcp_v2.py +0 -0
- {java_codebase_rag-0.9.0 → java_codebase_rag-0.9.2}/path_filtering.py +0 -0
- {java_codebase_rag-0.9.0 → java_codebase_rag-0.9.2}/pr_analysis.py +0 -0
- {java_codebase_rag-0.9.0 → java_codebase_rag-0.9.2}/search_lancedb.py +0 -0
- {java_codebase_rag-0.9.0 → java_codebase_rag-0.9.2}/setup.cfg +0 -0
- {java_codebase_rag-0.9.0 → java_codebase_rag-0.9.2}/tests/test_agent_skills_static.py +0 -0
- {java_codebase_rag-0.9.0 → java_codebase_rag-0.9.2}/tests/test_assign_endpoint_client_extraction.py +0 -0
- {java_codebase_rag-0.9.0 → java_codebase_rag-0.9.2}/tests/test_ast_graph_build.py +0 -0
- {java_codebase_rag-0.9.0 → java_codebase_rag-0.9.2}/tests/test_ast_java_calls.py +0 -0
- {java_codebase_rag-0.9.0 → java_codebase_rag-0.9.2}/tests/test_ast_java_capabilities.py +0 -0
- {java_codebase_rag-0.9.0 → java_codebase_rag-0.9.2}/tests/test_ast_java_thread_safety.py +0 -0
- {java_codebase_rag-0.9.0 → java_codebase_rag-0.9.2}/tests/test_bank_chat_brownfield_integration.py +0 -0
- {java_codebase_rag-0.9.0 → java_codebase_rag-0.9.2}/tests/test_brownfield_clients.py +0 -0
- {java_codebase_rag-0.9.0 → java_codebase_rag-0.9.2}/tests/test_brownfield_events.py +0 -0
- {java_codebase_rag-0.9.0 → java_codebase_rag-0.9.2}/tests/test_brownfield_overrides.py +0 -0
- {java_codebase_rag-0.9.0 → java_codebase_rag-0.9.2}/tests/test_brownfield_routes.py +0 -0
- {java_codebase_rag-0.9.0 → java_codebase_rag-0.9.2}/tests/test_call_edge_matching.py +0 -0
- {java_codebase_rag-0.9.0 → java_codebase_rag-0.9.2}/tests/test_call_edges_e2e.py +0 -0
- {java_codebase_rag-0.9.0 → java_codebase_rag-0.9.2}/tests/test_call_graph_receiver_resolution.py +0 -0
- {java_codebase_rag-0.9.0 → java_codebase_rag-0.9.2}/tests/test_call_graph_smoke_roundtrip.py +0 -0
- {java_codebase_rag-0.9.0 → java_codebase_rag-0.9.2}/tests/test_call_invariant.py +0 -0
- {java_codebase_rag-0.9.0 → java_codebase_rag-0.9.2}/tests/test_cli_progress_stdout_invariant.py +0 -0
- {java_codebase_rag-0.9.0 → java_codebase_rag-0.9.2}/tests/test_cli_quiet_parity.py +0 -0
- {java_codebase_rag-0.9.0 → java_codebase_rag-0.9.2}/tests/test_client_hint_recovery.py +0 -0
- {java_codebase_rag-0.9.0 → java_codebase_rag-0.9.2}/tests/test_client_node_extraction.py +0 -0
- {java_codebase_rag-0.9.0 → java_codebase_rag-0.9.2}/tests/test_client_role_rename.py +0 -0
- {java_codebase_rag-0.9.0 → java_codebase_rag-0.9.2}/tests/test_cross_service_resolution_flag.py +0 -0
- {java_codebase_rag-0.9.0 → java_codebase_rag-0.9.2}/tests/test_edge_navigation_doc.py +0 -0
- {java_codebase_rag-0.9.0 → java_codebase_rag-0.9.2}/tests/test_fd_limit.py +0 -0
- {java_codebase_rag-0.9.0 → java_codebase_rag-0.9.2}/tests/test_feign_not_exposer.py +0 -0
- {java_codebase_rag-0.9.0 → java_codebase_rag-0.9.2}/tests/test_graph_enrich.py +0 -0
- {java_codebase_rag-0.9.0 → java_codebase_rag-0.9.2}/tests/test_graph_only_boot.py +0 -0
- {java_codebase_rag-0.9.0 → java_codebase_rag-0.9.2}/tests/test_incremental_graph.py +0 -0
- {java_codebase_rag-0.9.0 → java_codebase_rag-0.9.2}/tests/test_install_data_sync.py +0 -0
- {java_codebase_rag-0.9.0 → java_codebase_rag-0.9.2}/tests/test_installer.py +0 -0
- {java_codebase_rag-0.9.0 → java_codebase_rag-0.9.2}/tests/test_installer_integration.py +0 -0
- {java_codebase_rag-0.9.0 → java_codebase_rag-0.9.2}/tests/test_installer_surface.py +0 -0
- {java_codebase_rag-0.9.0 → java_codebase_rag-0.9.2}/tests/test_java_codebase_rag_cli.py +0 -0
- {java_codebase_rag-0.9.0 → java_codebase_rag-0.9.2}/tests/test_jrag_auto_scope.py +0 -0
- {java_codebase_rag-0.9.0 → java_codebase_rag-0.9.2}/tests/test_jrag_envelope.py +0 -0
- {java_codebase_rag-0.9.0 → java_codebase_rag-0.9.2}/tests/test_jrag_listing.py +0 -0
- {java_codebase_rag-0.9.0 → java_codebase_rag-0.9.2}/tests/test_jrag_locate.py +0 -0
- {java_codebase_rag-0.9.0 → java_codebase_rag-0.9.2}/tests/test_jrag_orientation.py +0 -0
- {java_codebase_rag-0.9.0 → java_codebase_rag-0.9.2}/tests/test_jrag_render.py +0 -0
- {java_codebase_rag-0.9.0 → java_codebase_rag-0.9.2}/tests/test_jrag_status.py +0 -0
- {java_codebase_rag-0.9.0 → java_codebase_rag-0.9.2}/tests/test_jrag_token_budget.py +0 -0
- {java_codebase_rag-0.9.0 → java_codebase_rag-0.9.2}/tests/test_jrag_traversal_compose.py +0 -0
- {java_codebase_rag-0.9.0 → java_codebase_rag-0.9.2}/tests/test_ladybug_queries.py +0 -0
- {java_codebase_rag-0.9.0 → java_codebase_rag-0.9.2}/tests/test_lancedb_e2e.py +0 -0
- {java_codebase_rag-0.9.0 → java_codebase_rag-0.9.2}/tests/test_mcp_hints.py +0 -0
- {java_codebase_rag-0.9.0 → java_codebase_rag-0.9.2}/tests/test_mcp_server_project_root.py +0 -0
- {java_codebase_rag-0.9.0 → java_codebase_rag-0.9.2}/tests/test_mcp_tools.py +0 -0
- {java_codebase_rag-0.9.0 → java_codebase_rag-0.9.2}/tests/test_mcp_v2_compose.py +0 -0
- {java_codebase_rag-0.9.0 → java_codebase_rag-0.9.2}/tests/test_meta_chain_core.py +0 -0
- {java_codebase_rag-0.9.0 → java_codebase_rag-0.9.2}/tests/test_microservice_scope.py +0 -0
- {java_codebase_rag-0.9.0 → java_codebase_rag-0.9.2}/tests/test_outgoing_call_extraction.py +0 -0
- {java_codebase_rag-0.9.0 → java_codebase_rag-0.9.2}/tests/test_packaging_metadata.py +0 -0
- {java_codebase_rag-0.9.0 → java_codebase_rag-0.9.2}/tests/test_path_filtering.py +0 -0
- {java_codebase_rag-0.9.0 → java_codebase_rag-0.9.2}/tests/test_pr_analysis.py +0 -0
- {java_codebase_rag-0.9.0 → java_codebase_rag-0.9.2}/tests/test_progress.py +0 -0
- {java_codebase_rag-0.9.0 → java_codebase_rag-0.9.2}/tests/test_resolve_routes_messaging_layer_c.py +0 -0
- {java_codebase_rag-0.9.0 → java_codebase_rag-0.9.2}/tests/test_route_extraction.py +0 -0
- {java_codebase_rag-0.9.0 → java_codebase_rag-0.9.2}/tests/test_schema_consistency.py +0 -0
- {java_codebase_rag-0.9.0 → java_codebase_rag-0.9.2}/tests/test_search_lancedb.py +0 -0
- {java_codebase_rag-0.9.0 → java_codebase_rag-0.9.2}/tests/test_search_lancedb_capability.py +0 -0
- {java_codebase_rag-0.9.0 → java_codebase_rag-0.9.2}/tests/test_string_value_atoms.py +0 -0
- {java_codebase_rag-0.9.0 → java_codebase_rag-0.9.2}/tests/test_vectors_progress.py +0 -0
|
@@ -47,15 +47,39 @@ ENV_RUN_HEAVY = "JAVA_CODEBASE_RAG_RUN_HEAVY"
|
|
|
47
47
|
COCOINDEX_MAX_INFLIGHT_COMPONENTS_ENV = "COCOINDEX_MAX_INFLIGHT_COMPONENTS"
|
|
48
48
|
COCOINDEX_DEFAULT_MAX_INFLIGHT_COMPONENTS = "256"
|
|
49
49
|
|
|
50
|
+
# Lance native DataFusion hash-join memory pool ceiling (FairSpillPool). The
|
|
51
|
+
# lance default is ~100 MiB, tuned for query workloads — too small for the
|
|
52
|
+
# single big ``merge_insert`` cocoindex emits at the end of a flow component.
|
|
53
|
+
# On ``--full-reprocess`` (all rows match the existing table → bulk-update
|
|
54
|
+
# path) the hash join builds on a large side and exhausts the pool somewhere
|
|
55
|
+
# around 75k-100k chunks: "Resources exhausted: Failed to allocate ... for
|
|
56
|
+
# HashJoinInput ... N MiB remain available for the total pool". cocoindex is a
|
|
57
|
+
# bare pass-through to lancedb (it never sets a Session/memory_limit), so it
|
|
58
|
+
# inherits this default — we raise it here. FairSpillPool is a *reservation
|
|
59
|
+
# ceiling*, not a pre-allocation: setting 1 GiB does not reserve 1 GiB upfront,
|
|
60
|
+
# it just allows the join to grow before spilling/erroring, so it is safe on
|
|
61
|
+
# memory-constrained hosts. An operator can still override via their own
|
|
62
|
+
# ``LANCE_MEM_POOL_SIZE`` (subprocess_env copies os.environ, and apply is via
|
|
63
|
+
# ``setdefault`` so the operator value wins). Increment is unaffected (tiny
|
|
64
|
+
# batch → tiny hash table); only the full-reprocess write path is at risk.
|
|
65
|
+
LANCE_MEM_POOL_SIZE_ENV = "LANCE_MEM_POOL_SIZE"
|
|
66
|
+
LANCE_DEFAULT_MEM_POOL_SIZE = "1073741824" # 1 GiB
|
|
67
|
+
|
|
50
68
|
|
|
51
69
|
def cocoindex_subprocess_env_defaults() -> dict[str, str]:
|
|
52
|
-
"""Env defaults applied to every CocoIndex subprocess
|
|
70
|
+
"""Env defaults applied to every CocoIndex subprocess.
|
|
71
|
+
|
|
72
|
+
Bounds CocoIndex concurrency (``COCOINDEX_MAX_INFLIGHT_COMPONENTS``; see
|
|
73
|
+
:issue:`306`) and raises the Lance hash-join memory ceiling
|
|
74
|
+
(``LANCE_MEM_POOL_SIZE``) so a large full-reprocess does not exhaust the
|
|
75
|
+
default ~100 MiB pool mid-``merge_insert``.
|
|
53
76
|
|
|
54
77
|
Apply with ``env.setdefault(...)`` so a caller-provided (operator) value
|
|
55
|
-
always wins.
|
|
78
|
+
always wins.
|
|
56
79
|
"""
|
|
57
80
|
return {
|
|
58
|
-
COCOINDEX_MAX_INFLIGHT_COMPONENTS_ENV: COCOINDEX_DEFAULT_MAX_INFLIGHT_COMPONENTS
|
|
81
|
+
COCOINDEX_MAX_INFLIGHT_COMPONENTS_ENV: COCOINDEX_DEFAULT_MAX_INFLIGHT_COMPONENTS,
|
|
82
|
+
LANCE_MEM_POOL_SIZE_ENV: LANCE_DEFAULT_MEM_POOL_SIZE,
|
|
59
83
|
}
|
|
60
84
|
|
|
61
85
|
_DEFAULT_EMBEDDING_MODEL = "sentence-transformers/all-MiniLM-L6-v2"
|
|
@@ -812,11 +812,27 @@ def build_parser() -> argparse.ArgumentParser:
|
|
|
812
812
|
"CALLS hops from the route handler. Intra-service is an INDEX-TIME data "
|
|
813
813
|
"property: CALLS edges are intra-codebase by construction, and the query "
|
|
814
814
|
"carries no microservice predicate, so the result reflects whatever the "
|
|
815
|
-
"fixture indexed (no query-time constraint). --
|
|
815
|
+
"fixture indexed (no query-time constraint). --depth clamped to 1..8."
|
|
816
816
|
),
|
|
817
817
|
)
|
|
818
|
-
flow.add_argument(
|
|
819
|
-
|
|
818
|
+
flow.add_argument(
|
|
819
|
+
"query",
|
|
820
|
+
help=(
|
|
821
|
+
"Route path (e.g. '/chat/assign') or Kafka topic name (e.g. "
|
|
822
|
+
"'banking.chat.compliance.review'). Resolved with hint_kind=route; "
|
|
823
|
+
"kafka_topic Routes match on topic."
|
|
824
|
+
),
|
|
825
|
+
)
|
|
826
|
+
# Primary flag is --depth (consistent with callers/callees/impact/decompose).
|
|
827
|
+
# --max-hops is kept as a hidden back-compat alias (same dest).
|
|
828
|
+
flow.add_argument(
|
|
829
|
+
"--depth", type=int, default=5, dest="depth",
|
|
830
|
+
help="Max CALLS hops (clamped 1..8, default 5).",
|
|
831
|
+
)
|
|
832
|
+
flow.add_argument(
|
|
833
|
+
"--max-hops", type=int, dest="depth",
|
|
834
|
+
default=argparse.SUPPRESS, help=argparse.SUPPRESS,
|
|
835
|
+
)
|
|
820
836
|
flow.set_defaults(handler=_cmd_flow)
|
|
821
837
|
|
|
822
838
|
# ---- Compose traversals + file inspection (PR-JRAG-3b) ----
|
|
@@ -3001,7 +3017,7 @@ def _cmd_flow(args: argparse.Namespace) -> int:
|
|
|
3001
3017
|
args, reason="trace_request_flow carries no microservice predicate; intra-codebase is an index-time data property"
|
|
3002
3018
|
)
|
|
3003
3019
|
|
|
3004
|
-
max_hops = max(1, min(8, getattr(args, "
|
|
3020
|
+
max_hops = max(1, min(8, getattr(args, "depth", 5)))
|
|
3005
3021
|
flow_data = graph.trace_request_flow(entry_route_id=node.id, max_hops=max_hops)
|
|
3006
3022
|
|
|
3007
3023
|
root_id = node.id
|
|
@@ -184,6 +184,31 @@ async def optimize_lance_tables(
|
|
|
184
184
|
|
|
185
185
|
if last_exc is None:
|
|
186
186
|
results[name] = "ok"
|
|
187
|
+
# Best-effort BTREE scalar index on the primary key ("id").
|
|
188
|
+
# cocoindex's merge_insert defaults to use_index=True but
|
|
189
|
+
# never creates a scalar PK index itself (declaring
|
|
190
|
+
# primary_key in the schema does NOT auto-build a lance
|
|
191
|
+
# index), so without this every merge_insert — increment
|
|
192
|
+
# included — is a forced full scan of the PK column,
|
|
193
|
+
# O(existing rows). On a large repo that scan dominates
|
|
194
|
+
# increment wall-clock; with the index present the join does
|
|
195
|
+
# lookups (~O(batch*log N)). Failure is non-fatal (the table
|
|
196
|
+
# is still correct, just un-indexed) and never alters the
|
|
197
|
+
# "ok" status, mirroring the FTS block below. ``replace=True``
|
|
198
|
+
# keeps it idempotent across runs; table.optimize() above
|
|
199
|
+
# maintains it on subsequent runs.
|
|
200
|
+
try:
|
|
201
|
+
from lancedb.index import BTree
|
|
202
|
+
await table.create_index("id", config=BTree(), replace=True)
|
|
203
|
+
except Exception as exc:
|
|
204
|
+
low = str(exc).lower()
|
|
205
|
+
if not any(
|
|
206
|
+
w in low for w in ("exist", "duplicate", "already", "same name")
|
|
207
|
+
) and not quiet:
|
|
208
|
+
print(
|
|
209
|
+
f"java-codebase-rag: optimize: {name} id-index skipped: {exc}",
|
|
210
|
+
file=sys.stderr,
|
|
211
|
+
)
|
|
187
212
|
# Best-effort FTS index at index time (PR-SEARCH-3) so hybrid
|
|
188
213
|
# search works on all tables (java/sql/yaml) without a
|
|
189
214
|
# first-query race. Failure is non-fatal — the lazy
|
|
@@ -132,6 +132,26 @@ def run_cocoindex_update(
|
|
|
132
132
|
on_progress: Callable[[ProgressEvent], None] | None = None,
|
|
133
133
|
on_progress_console: object | None = None,
|
|
134
134
|
) -> subprocess.CompletedProcess[str]:
|
|
135
|
+
if full_reprocess:
|
|
136
|
+
# A full reprocess rebuilds every row, so DROP the Lance target tables
|
|
137
|
+
# first and let cocoindex recreate them via the fast INSERT path. The
|
|
138
|
+
# in-place alternative (cocoindex's bulk-update merge_insert) emits
|
|
139
|
+
# ~one deletion-vector + version commit PER matched row — O(rows) of
|
|
140
|
+
# tiny file IO that scales to multi-minute hangs on large repos
|
|
141
|
+
# (measured ~83s sys time / 3474 deletion files for 3475 chunks on
|
|
142
|
+
# Shopizer; drop+recreate is ~3.6s sys / 0 deletions, ~3.7x faster and
|
|
143
|
+
# hang-free). Output is identical either way (full recompute); only the
|
|
144
|
+
# write path differs. Drop failure is non-fatal — if it somehow fails,
|
|
145
|
+
# the update falls back to the slow in-place path. The same fix is
|
|
146
|
+
# applied on the async server path (``server.run_refresh_pipeline``).
|
|
147
|
+
drop = run_cocoindex_drop(env, quiet=quiet)
|
|
148
|
+
if drop.returncode != 0 and not is_cocoindex_preflight_blocker(drop):
|
|
149
|
+
print(
|
|
150
|
+
"java-codebase-rag: drop-before-reprocess failed "
|
|
151
|
+
f"(exit {drop.returncode}); falling back to in-place update: "
|
|
152
|
+
f"{(drop.stderr or '').strip()[:200]}",
|
|
153
|
+
file=sys.stderr,
|
|
154
|
+
)
|
|
135
155
|
result = _run_cocoindex_update_impl(
|
|
136
156
|
env,
|
|
137
157
|
full_reprocess=full_reprocess,
|
|
@@ -101,6 +101,7 @@ tests/test_microservice_scope.py
|
|
|
101
101
|
tests/test_outgoing_call_extraction.py
|
|
102
102
|
tests/test_packaging_metadata.py
|
|
103
103
|
tests/test_path_filtering.py
|
|
104
|
+
tests/test_pipeline.py
|
|
104
105
|
tests/test_pr_analysis.py
|
|
105
106
|
tests/test_progress.py
|
|
106
107
|
tests/test_resolve_routes_messaging_layer_c.py
|
|
@@ -112,6 +112,38 @@ _NUM_TXN_BEFORE_OPTIMIZE = 10**12
|
|
|
112
112
|
# parent clamps to total on the terminal event anyway).
|
|
113
113
|
_VECTORS_TICK_EVERY = 25
|
|
114
114
|
|
|
115
|
+
# Bounded concurrency for the per-file drain in app_main. cocoindex's embedder
|
|
116
|
+
# is ``@coco.fn.as_async(batching=True, runner=GPU, max_batch_size=64)`` — but
|
|
117
|
+
# its batching layer only coalesces calls that are in flight SIMULTANEOUSLY. A
|
|
118
|
+
# serial ``async for … await`` loop keeps just one file's chunks (avg 1–3) in
|
|
119
|
+
# flight, so real batches stay tiny and MPS idles between them (measured ~138
|
|
120
|
+
# chunks/s vs the ~235 chunks/s ceiling at batch=64 for all-MiniLM-L6-v2).
|
|
121
|
+
# Draining many files at once with a semaphore puts their chunks in flight
|
|
122
|
+
# together → the embedder coalesces them into full batches → MPS climbs toward
|
|
123
|
+
# the ceiling. Measured on Shopizer (1167 files / 3475 chunks): full init drops
|
|
124
|
+
# from ~46.7s (serial) to ~36.0s (32) / ~34.3s (64), with identical row output.
|
|
125
|
+
#
|
|
126
|
+
# This stays inside ONE component, so the earlier mount_each→app_main win is
|
|
127
|
+
# preserved: still exactly ONE merge_insert per table at commit. Memoization
|
|
128
|
+
# (``@coco.fn(memo=True)``) and the lock-guarded tick counter are safe under
|
|
129
|
+
# concurrency; ``parse_java`` uses a per-thread tree-sitter Parser (already
|
|
130
|
+
# routed via ``asyncio.to_thread``) and ``splitter.split`` is synchronous so the
|
|
131
|
+
# event loop cannot reenter it.
|
|
132
|
+
#
|
|
133
|
+
# Default 64 is sized to MATCH the embedder's hardcoded max_batch_size=64 (the
|
|
134
|
+
# decorator above; not a constructor arg, so not raisable from the flow): ~64
|
|
135
|
+
# files in flight reliably fills a 64-chunk batch and saturates MPS. Going higher
|
|
136
|
+
# buys nothing — the batch is already capped — and lower underfills it. Memory
|
|
137
|
+
# is NOT the limiting factor here: cocoindex buffers ALL staged rows until the
|
|
138
|
+
# single final merge_insert regardless of concurrency, so peak RSS is set by
|
|
139
|
+
# total chunk count (the commit buffer), not by how many files process at once.
|
|
140
|
+
# Set to ``1`` for the old serial behavior; raise/lower only if you have also
|
|
141
|
+
# changed the effective batch size or are constraining the commit buffer itself.
|
|
142
|
+
_FILE_CONCURRENCY = max(
|
|
143
|
+
1,
|
|
144
|
+
int(os.environ.get("JAVA_CODEBASE_RAG_FILE_CONCURRENCY", "64") or "64"),
|
|
145
|
+
)
|
|
146
|
+
|
|
115
147
|
# Thread-safe counter: cocoindex may call process_*_file concurrently
|
|
116
148
|
# (mount_each parallelism is implementation-defined). A module-level lock guards
|
|
117
149
|
# both the counter and the emission so two threads never interleave a tick.
|
|
@@ -533,6 +565,29 @@ async def process_yaml_file(
|
|
|
533
565
|
)
|
|
534
566
|
|
|
535
567
|
|
|
568
|
+
async def _drain_files_concurrently(
|
|
569
|
+
files: Any, process_fn: Any, table: Any, sem: asyncio.Semaphore
|
|
570
|
+
) -> None:
|
|
571
|
+
"""Run ``process_fn(file, table)`` over every file with bounded concurrency.
|
|
572
|
+
|
|
573
|
+
Replaces the serial ``async for … await process_*_file`` loop so the
|
|
574
|
+
embedder's batching layer sees many files' chunks in flight at once (see
|
|
575
|
+
``_FILE_CONCURRENCY``). Materializes the async iterable up front — file
|
|
576
|
+
handles are lightweight and cocoindex already realized the collection when
|
|
577
|
+
the walker mounted, so this is not a second walk. An empty collection is a
|
|
578
|
+
no-op (e.g. SQL/YAML tables on a repo with none).
|
|
579
|
+
"""
|
|
580
|
+
items = [f async for _, f in files.items()]
|
|
581
|
+
if not items:
|
|
582
|
+
return
|
|
583
|
+
|
|
584
|
+
async def _one(_file: Any) -> None:
|
|
585
|
+
async with sem:
|
|
586
|
+
await process_fn(_file, table)
|
|
587
|
+
|
|
588
|
+
await asyncio.gather(*(_one(f) for f in items))
|
|
589
|
+
|
|
590
|
+
|
|
536
591
|
@coco.fn
|
|
537
592
|
async def app_main() -> None:
|
|
538
593
|
java_schema = await lancedb.TableSchema.from_class(
|
|
@@ -646,12 +701,17 @@ async def app_main() -> None:
|
|
|
646
701
|
# unchanged files still skip re-embedding on incremental; _RowHandler.
|
|
647
702
|
# reconcile skips rows whose fingerprint is unchanged → increment carries
|
|
648
703
|
# only changed rows in its single merge_insert.
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
async for
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
704
|
+
#
|
|
705
|
+
# PERF (concurrency): drain files with a bounded semaphore instead of a
|
|
706
|
+
# serial ``async for … await``. See ``_FILE_CONCURRENCY`` — this is what
|
|
707
|
+
# lets the embedder's batching layer fill real batches (embedding dominates
|
|
708
|
+
# init cost, and serial files starve it). One shared semaphore bounds total
|
|
709
|
+
# in-flight work; tables are drained in order (java dominates, sql/yaml are
|
|
710
|
+
# usually near-empty).
|
|
711
|
+
_sem = asyncio.Semaphore(_FILE_CONCURRENCY)
|
|
712
|
+
await _drain_files_concurrently(java_files, process_java_file, java_table, _sem)
|
|
713
|
+
await _drain_files_concurrently(sql_files, process_sql_file, sql_table, _sem)
|
|
714
|
+
await _drain_files_concurrently(yaml_files, process_yaml_file, yaml_table, _sem)
|
|
655
715
|
|
|
656
716
|
|
|
657
717
|
app = coco.App(
|
|
@@ -90,6 +90,8 @@ VALID_RESOLVE_REASONS: frozenset[str] = frozenset((
|
|
|
90
90
|
"short_name",
|
|
91
91
|
"route_template",
|
|
92
92
|
"route_method_path",
|
|
93
|
+
"route_topic",
|
|
94
|
+
"route_topic_prefix",
|
|
93
95
|
"client_target",
|
|
94
96
|
"client_target_path",
|
|
95
97
|
"client_name",
|
|
@@ -428,6 +430,8 @@ ResolveReason = Literal[
|
|
|
428
430
|
"short_name",
|
|
429
431
|
"route_template",
|
|
430
432
|
"route_method_path",
|
|
433
|
+
"route_topic",
|
|
434
|
+
"route_topic_prefix",
|
|
431
435
|
"client_target",
|
|
432
436
|
"client_target_path",
|
|
433
437
|
"client_name",
|
|
@@ -43,11 +43,13 @@ _RESOLVE_REASON_PRIORITY: dict[ResolveReason, int] = {
|
|
|
43
43
|
"producer_topic_prefix": 1,
|
|
44
44
|
"fqn_suffix": 2,
|
|
45
45
|
"route_template": 2,
|
|
46
|
+
"route_topic": 2,
|
|
46
47
|
"client_fqn": 2,
|
|
47
48
|
"short_name": 3,
|
|
48
49
|
"client_target": 3,
|
|
49
50
|
"client_name": 3,
|
|
50
51
|
"producer_topic": 3,
|
|
52
|
+
"route_topic_prefix": 3,
|
|
51
53
|
}
|
|
52
54
|
|
|
53
55
|
_SYMBOL_RESOLVE_RETURN = (
|
|
@@ -295,6 +297,28 @@ def _resolve_route_candidates(
|
|
|
295
297
|
path_val = str(row.get("path_template") or row.get("path") or "")
|
|
296
298
|
out.append((_node_ref_from_row("route", row), "route_template", len(path_val)))
|
|
297
299
|
|
|
300
|
+
# Kafka/topic routes carry their name in ``topic`` (``path``/``path_template``
|
|
301
|
+
# are empty), so path-based matching above cannot reach them. Match on
|
|
302
|
+
# ``r.topic`` the same way ``_resolve_producer_candidates`` matches
|
|
303
|
+
# ``p.topic`` — this lets ``flow``/``callers``/``overview`` resolve a
|
|
304
|
+
# ``kafka_topic`` Route by topic name. ``_drop_route_mirrors`` below then
|
|
305
|
+
# discards the no-EXPOSES producer phantom in favour of the server route.
|
|
306
|
+
rows = g._rows( # noqa: SLF001
|
|
307
|
+
f"MATCH (r:Route) WHERE r.topic = $topic{scope} RETURN {_ROUTE_RESOLVE_RETURN} LIMIT $lim",
|
|
308
|
+
{"topic": identifier, "lim": lim, **scope_params},
|
|
309
|
+
)
|
|
310
|
+
for row in rows:
|
|
311
|
+
out.append((_node_ref_from_row("route", row), "route_topic", len(identifier)))
|
|
312
|
+
|
|
313
|
+
if not identifier.startswith("/"):
|
|
314
|
+
rows = g._rows( # noqa: SLF001
|
|
315
|
+
f"MATCH (r:Route) WHERE r.topic STARTS WITH $topic{scope} "
|
|
316
|
+
f"RETURN {_ROUTE_RESOLVE_RETURN} LIMIT $lim",
|
|
317
|
+
{"topic": identifier, "lim": lim, **scope_params},
|
|
318
|
+
)
|
|
319
|
+
for row in rows:
|
|
320
|
+
out.append((_node_ref_from_row("route", row), "route_topic_prefix", len(identifier)))
|
|
321
|
+
|
|
298
322
|
return _drop_route_mirrors(g, out)
|
|
299
323
|
|
|
300
324
|
|
|
@@ -349,6 +349,31 @@ async def run_refresh_pipeline(
|
|
|
349
349
|
)
|
|
350
350
|
proc: asyncio.subprocess.Process | None = None
|
|
351
351
|
out_b, err_b = b"", b""
|
|
352
|
+
# DROP the Lance target tables so the update takes the fast INSERT path
|
|
353
|
+
# instead of cocoindex's in-place bulk-update, which emits ~one deletion-
|
|
354
|
+
# vector + version commit PER matched row — O(rows) of tiny file IO that
|
|
355
|
+
# hangs for many minutes on large repos. Drop+recreate is identical output
|
|
356
|
+
# for a full rebuild (the very thing --full-reprocess means). Same fix on
|
|
357
|
+
# the sync path: pipeline.run_cocoindex_update. Drop failure is non-fatal:
|
|
358
|
+
# the update falls back to the slow in-place path.
|
|
359
|
+
try:
|
|
360
|
+
drop_proc = await asyncio.create_subprocess_exec(
|
|
361
|
+
str(cocoindex_bin),
|
|
362
|
+
"drop",
|
|
363
|
+
_COCOINDEX_TARGET,
|
|
364
|
+
"-f",
|
|
365
|
+
cwd=str(flow_path.parent),
|
|
366
|
+
env=_cocoindex_subprocess_env(root),
|
|
367
|
+
stdout=asyncio.subprocess.PIPE,
|
|
368
|
+
stderr=asyncio.subprocess.PIPE,
|
|
369
|
+
)
|
|
370
|
+
await drop_proc.communicate()
|
|
371
|
+
except Exception as exc:
|
|
372
|
+
print(
|
|
373
|
+
f"java-codebase-rag: drop-before-reprocess failed ({exc!s}); "
|
|
374
|
+
"falling back to in-place update",
|
|
375
|
+
file=sys.stderr,
|
|
376
|
+
)
|
|
352
377
|
if quiet:
|
|
353
378
|
try:
|
|
354
379
|
proc = await asyncio.create_subprocess_exec(
|
|
@@ -506,6 +506,10 @@ def test_cocoindex_subprocess_env_defaults_uses_real_inflight_env_var() -> None:
|
|
|
506
506
|
assert defaults["COCOINDEX_MAX_INFLIGHT_COMPONENTS"] == "256"
|
|
507
507
|
# The bogus name from the broken #293 fix must NOT leak back in.
|
|
508
508
|
assert "COCOINDEX_SOURCE_MAX_INFLIGHT_ROWS" not in defaults
|
|
509
|
+
# Lance native hash-join pool ceiling, raised from the ~100 MiB default so a
|
|
510
|
+
# large full-reprocess merge_insert does not exhaust it mid-commit. Applied
|
|
511
|
+
# via setdefault, so an operator's own value still wins.
|
|
512
|
+
assert defaults["LANCE_MEM_POOL_SIZE"] == "1073741824"
|
|
509
513
|
|
|
510
514
|
|
|
511
515
|
class TestConfigSourcePointer:
|
|
@@ -22,8 +22,11 @@ Tests (bank-chat fixture):
|
|
|
22
22
|
13. test_impact_service_post_filter_emits_warning
|
|
23
23
|
14. test_decompose_renders_role_waterfall
|
|
24
24
|
15. test_flow_outbound_intra_service_on_fixture
|
|
25
|
-
16.
|
|
26
|
-
17.
|
|
25
|
+
16. test_flow_follows_kafka_topic_on_fixture
|
|
26
|
+
17. test_flow_depth_flag_and_max_hops_alias
|
|
27
|
+
18. test_callers_topic_disambiguates_with_kind
|
|
28
|
+
19. test_traversal_resolve_ambiguous_stops
|
|
29
|
+
20. test_traversal_rejects_offset
|
|
27
30
|
"""
|
|
28
31
|
from __future__ import annotations
|
|
29
32
|
|
|
@@ -620,6 +623,134 @@ def test_flow_outbound_intra_service_on_fixture(
|
|
|
620
623
|
)
|
|
621
624
|
|
|
622
625
|
|
|
626
|
+
def test_flow_follows_kafka_topic_on_fixture(
|
|
627
|
+
corpus_root: Path, ladybug_db_path: Path
|
|
628
|
+
) -> None:
|
|
629
|
+
"""flow resolves a Kafka topic to its Route AND follows it (async edges).
|
|
630
|
+
|
|
631
|
+
Regression: ``_resolve_route_candidates`` matched only on path, so a
|
|
632
|
+
``kafka_topic`` Route (name in ``topic``, ``path=''``) was unresolvable and
|
|
633
|
+
``jrag flow <topic>`` returned 'none'. Resolution alone is not enough — this
|
|
634
|
+
test also asserts the follow graph is non-empty and carries an
|
|
635
|
+
``ASYNC_CALLS`` edge, proving the kafka-specific inbound arm (topic-matched
|
|
636
|
+
Producer) actually fires once the route resolves.
|
|
637
|
+
|
|
638
|
+
``banking.chat.incoming`` is the canonical dual-sided topic: produced by
|
|
639
|
+
``FollowUpKafkaPublisher`` (inbound ``ASYNC_CALLS``) and consumed by
|
|
640
|
+
``ChatKafkaListener`` (outbound ``CALLS`` to ``orchestrationService.handle``).
|
|
641
|
+
"""
|
|
642
|
+
env = _env_for(corpus_root, ladybug_db_path)
|
|
643
|
+
proc = _run_jrag(
|
|
644
|
+
["flow", "banking.chat.incoming", "--format", "json"], env=env
|
|
645
|
+
)
|
|
646
|
+
assert proc.returncode == 0, (
|
|
647
|
+
f"flow on kafka topic failed: rc={proc.returncode}\nstdout={proc.stdout}\nstderr={proc.stderr}"
|
|
648
|
+
)
|
|
649
|
+
payload = json.loads(proc.stdout)
|
|
650
|
+
assert payload["status"] == "ok", f"expected ok, got {payload}"
|
|
651
|
+
assert payload.get("root"), "expected root id (the kafka_topic Route)"
|
|
652
|
+
root_node = payload.get("nodes", {}).get(payload["root"], {})
|
|
653
|
+
assert root_node.get("kind") == "route", f"expected route root, got {root_node}"
|
|
654
|
+
# The follow graph must be non-empty AND carry a kafka async inbound edge —
|
|
655
|
+
# this is the part that proves "flow follows kafka", not just "resolves it".
|
|
656
|
+
edges = payload.get("edges", [])
|
|
657
|
+
assert edges, f"expected non-empty follow graph for a kafka topic, got edges={edges}"
|
|
658
|
+
assert any(e.get("edge_type") == "ASYNC_CALLS" for e in edges), (
|
|
659
|
+
f"expected an ASYNC_CALLS (topic-matched Producer) inbound edge, got {edges}"
|
|
660
|
+
)
|
|
661
|
+
|
|
662
|
+
|
|
663
|
+
def test_flow_depth_flag_and_max_hops_alias(
|
|
664
|
+
corpus_root: Path, ladybug_db_path: Path
|
|
665
|
+
) -> None:
|
|
666
|
+
"""flow uses --depth (consistent with callers/callees/impact/decompose).
|
|
667
|
+
|
|
668
|
+
--max-hops remains as a hidden back-compat alias (same dest). Both must be
|
|
669
|
+
accepted, produce the same traversal, and --depth must actually change the
|
|
670
|
+
traversal depth (more hops => strictly more CALLS edges).
|
|
671
|
+
"""
|
|
672
|
+
env = _env_for(corpus_root, ladybug_db_path)
|
|
673
|
+
|
|
674
|
+
# --depth is the primary flag now.
|
|
675
|
+
proc_depth = _run_jrag(
|
|
676
|
+
["flow", "/chat/assign", "--depth", "2", "--format", "json"], env=env
|
|
677
|
+
)
|
|
678
|
+
assert proc_depth.returncode == 0, (
|
|
679
|
+
f"flow --depth failed: rc={proc_depth.returncode}\nstdout={proc_depth.stdout}"
|
|
680
|
+
)
|
|
681
|
+
payload_depth = json.loads(proc_depth.stdout)
|
|
682
|
+
assert payload_depth["status"] == "ok", f"expected ok, got {payload_depth}"
|
|
683
|
+
|
|
684
|
+
# --max-hops is the hidden alias; must still be accepted (back-compat).
|
|
685
|
+
proc_alias = _run_jrag(
|
|
686
|
+
["flow", "/chat/assign", "--max-hops", "2", "--format", "json"], env=env
|
|
687
|
+
)
|
|
688
|
+
assert proc_alias.returncode == 0, (
|
|
689
|
+
f"flow --max-hops alias failed: rc={proc_alias.returncode}\nstdout={proc_alias.stdout}"
|
|
690
|
+
)
|
|
691
|
+
payload_alias = json.loads(proc_alias.stdout)
|
|
692
|
+
assert payload_alias["status"] == "ok", f"expected ok, got {payload_alias}"
|
|
693
|
+
# Same dest -> same traversal (depth 2 in both cases).
|
|
694
|
+
assert payload_alias.get("root") == payload_depth.get("root"), (
|
|
695
|
+
"expected identical root id for --depth and --max-hops"
|
|
696
|
+
)
|
|
697
|
+
|
|
698
|
+
# --depth must actually affect the traversal: depth 1 yields strictly fewer
|
|
699
|
+
# outbound CALLS edges than depth 5 on this fixture (~3 vs ~20).
|
|
700
|
+
proc_shallow = _run_jrag(
|
|
701
|
+
["flow", "/chat/assign", "--depth", "1", "--format", "json"], env=env
|
|
702
|
+
)
|
|
703
|
+
proc_deep = _run_jrag(
|
|
704
|
+
["flow", "/chat/assign", "--depth", "5", "--format", "json"], env=env
|
|
705
|
+
)
|
|
706
|
+
shallow = json.loads(proc_shallow.stdout)
|
|
707
|
+
deep = json.loads(proc_deep.stdout)
|
|
708
|
+
assert len(shallow.get("edges", [])) < len(deep.get("edges", [])), (
|
|
709
|
+
f"--depth should change traversal size; depth=1 -> {len(shallow.get('edges', []))}, "
|
|
710
|
+
f"depth=5 -> {len(deep.get('edges', []))}"
|
|
711
|
+
)
|
|
712
|
+
|
|
713
|
+
|
|
714
|
+
def test_callers_topic_disambiguates_with_kind(
|
|
715
|
+
corpus_root: Path, ladybug_db_path: Path
|
|
716
|
+
) -> None:
|
|
717
|
+
"""A dual-sided topic resolves to BOTH a Producer and a Route -> ambiguous
|
|
718
|
+
without --kind; --kind route collapses to one and runs find_route_callers.
|
|
719
|
+
|
|
720
|
+
Behavior note (from enabling r.topic resolution): ``callers <topic>`` with no
|
|
721
|
+
--kind now searches all kinds, and a topic with both a Producer and a server
|
|
722
|
+
Route is genuinely ambiguous (previously only the Producer matched, which
|
|
723
|
+
then errored because callers only accepts Symbol/Route roots — jrag.py:2371).
|
|
724
|
+
``--kind route`` is the disambiguation that yields a working callers run.
|
|
725
|
+
(--kind producer resolves to the Producer but callers rejects a Producer
|
|
726
|
+
root; use the ``producers`` command for the producer view.)
|
|
727
|
+
"""
|
|
728
|
+
env = _env_for(corpus_root, ladybug_db_path)
|
|
729
|
+
|
|
730
|
+
# No --kind: Producer + Route both match -> ambiguous (no traversal).
|
|
731
|
+
proc_both = _run_jrag(
|
|
732
|
+
["callers", "banking.chat.incoming", "--format", "json"], env=env
|
|
733
|
+
)
|
|
734
|
+
payload_both = json.loads(proc_both.stdout)
|
|
735
|
+
assert payload_both["status"] == "ambiguous", (
|
|
736
|
+
f"expected ambiguous for dual-sided topic without --kind, got {payload_both.get('status')}"
|
|
737
|
+
)
|
|
738
|
+
assert len(payload_both.get("candidates", [])) >= 2, (
|
|
739
|
+
f"expected >=2 candidates (producer + route), got {payload_both.get('candidates')}"
|
|
740
|
+
)
|
|
741
|
+
|
|
742
|
+
# --kind route: resolves to the single server Route and runs find_route_callers.
|
|
743
|
+
proc_route = _run_jrag(
|
|
744
|
+
["callers", "banking.chat.incoming", "--kind", "route", "--format", "json"],
|
|
745
|
+
env=env,
|
|
746
|
+
)
|
|
747
|
+
payload_route = json.loads(proc_route.stdout)
|
|
748
|
+
assert payload_route["status"] == "ok", (
|
|
749
|
+
f"expected --kind route to resolve and run, got {payload_route.get('status')}"
|
|
750
|
+
)
|
|
751
|
+
assert payload_route.get("root"), "expected a resolved root for --kind route"
|
|
752
|
+
|
|
753
|
+
|
|
623
754
|
# ----- Test 16: traversal resolve-ambiguous stops (no auto-pick) -----
|
|
624
755
|
|
|
625
756
|
|
|
@@ -123,31 +123,92 @@ async def test_optimize_builds_fts_index_after_success(monkeypatch, tmp_path) ->
|
|
|
123
123
|
table = _FakeTable(name, [None]) # optimize succeeds on first try
|
|
124
124
|
conn = _FakeConnection(table_names={name}, tables={name: table})
|
|
125
125
|
_install_fake_lancedb(monkeypatch, conn)
|
|
126
|
-
|
|
127
|
-
index_mod = types.ModuleType("lancedb.index")
|
|
128
|
-
|
|
129
|
-
class FTS: # stands in for lancedb.index.FTS config object
|
|
130
|
-
pass
|
|
131
|
-
|
|
132
|
-
index_mod.FTS = FTS
|
|
133
|
-
monkeypatch.setitem(sys.modules, "lancedb.index", index_mod)
|
|
126
|
+
fts_cls, _btree_cls = _install_fake_lancedb_index(monkeypatch)
|
|
134
127
|
|
|
135
128
|
results = await lance_optimize.optimize_lance_tables(tmp_path, quiet=True)
|
|
136
129
|
|
|
137
130
|
assert results[name] == "ok"
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
131
|
+
# optimize now builds TWO indices (BTREE on "id" + FTS on "text"); find the
|
|
132
|
+
# FTS one by column rather than asserting on call count / order.
|
|
133
|
+
fts_calls = [
|
|
134
|
+
c for c in table.create_index_calls if c["args"] and c["args"][0] == "text"
|
|
135
|
+
]
|
|
136
|
+
assert len(fts_calls) == 1, (
|
|
137
|
+
f"expected one FTS create_index on 'text', got {table.create_index_calls}"
|
|
144
138
|
)
|
|
139
|
+
call = fts_calls[0]
|
|
145
140
|
assert call["kwargs"].get("replace") is True, (
|
|
146
141
|
f"FTS index must be built with replace=True, got kwargs={call['kwargs']}"
|
|
147
142
|
)
|
|
143
|
+
assert isinstance(call["kwargs"].get("config"), fts_cls), (
|
|
144
|
+
f"FTS index config must be a lancedb.index.FTS, got kwargs={call['kwargs']}"
|
|
145
|
+
)
|
|
148
146
|
assert conn.closed is True
|
|
149
147
|
|
|
150
148
|
|
|
149
|
+
async def test_optimize_builds_btree_pk_index_after_success(monkeypatch, tmp_path) -> None:
|
|
150
|
+
"""A successful optimize builds a BTREE scalar index on the ``id`` PK.
|
|
151
|
+
|
|
152
|
+
cocoindex's ``merge_insert`` defaults to ``use_index=True`` but never creates
|
|
153
|
+
a scalar PK index itself (declaring ``primary_key`` does NOT auto-build a
|
|
154
|
+
lance index), so without this every ``merge_insert`` — increment included —
|
|
155
|
+
is a forced full scan of the PK column (O(existing rows)). The BTREE index
|
|
156
|
+
flips that to ~O(batch*log N) lookups. Guards the path against silent
|
|
157
|
+
regression under the broad ``except`` that swallows index-build failures.
|
|
158
|
+
"""
|
|
159
|
+
import types
|
|
160
|
+
|
|
161
|
+
from java_codebase_rag import lance_optimize
|
|
162
|
+
|
|
163
|
+
name = lance_optimize.LANCE_TABLE_NAMES[0]
|
|
164
|
+
table = _FakeTable(name, [None]) # optimize succeeds on first try
|
|
165
|
+
conn = _FakeConnection(table_names={name}, tables={name: table})
|
|
166
|
+
_install_fake_lancedb(monkeypatch, conn)
|
|
167
|
+
_fts_cls, btree_cls = _install_fake_lancedb_index(monkeypatch)
|
|
168
|
+
|
|
169
|
+
results = await lance_optimize.optimize_lance_tables(tmp_path, quiet=True)
|
|
170
|
+
|
|
171
|
+
assert results[name] == "ok"
|
|
172
|
+
id_calls = [
|
|
173
|
+
c for c in table.create_index_calls if c["args"] and c["args"][0] == "id"
|
|
174
|
+
]
|
|
175
|
+
assert len(id_calls) == 1, (
|
|
176
|
+
f"expected one BTREE create_index on 'id', got {table.create_index_calls}"
|
|
177
|
+
)
|
|
178
|
+
call = id_calls[0]
|
|
179
|
+
assert call["kwargs"].get("replace") is True, (
|
|
180
|
+
f"BTREE PK index must be built with replace=True, got kwargs={call['kwargs']}"
|
|
181
|
+
)
|
|
182
|
+
assert isinstance(call["kwargs"].get("config"), btree_cls), (
|
|
183
|
+
f"BTREE PK index config must be a lancedb.index.BTree, got kwargs={call['kwargs']}"
|
|
184
|
+
)
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
def _install_fake_lancedb_index(monkeypatch):
|
|
188
|
+
"""Make ``from lancedb.index import FTS, BTree`` resolve to stand-in classes.
|
|
189
|
+
|
|
190
|
+
Returns the ``(FTS, BTree)`` config stand-ins so a test can assert the right
|
|
191
|
+
config object was passed. Both the FTS path (PR-SEARCH-3) and the BTREE PK
|
|
192
|
+
path build their index config via a local ``from lancedb.index import ...``;
|
|
193
|
+
without a stand-in those imports hit the broad ``except`` and silently no-op,
|
|
194
|
+
leaving the path unexercised.
|
|
195
|
+
"""
|
|
196
|
+
import types
|
|
197
|
+
|
|
198
|
+
index_mod = types.ModuleType("lancedb.index")
|
|
199
|
+
|
|
200
|
+
class FTS: # stands in for lancedb.index.FTS config object
|
|
201
|
+
pass
|
|
202
|
+
|
|
203
|
+
class BTree: # stands in for lancedb.index.BTree config object
|
|
204
|
+
pass
|
|
205
|
+
|
|
206
|
+
index_mod.FTS = FTS
|
|
207
|
+
index_mod.BTree = BTree
|
|
208
|
+
monkeypatch.setitem(sys.modules, "lancedb.index", index_mod)
|
|
209
|
+
return FTS, BTree
|
|
210
|
+
|
|
211
|
+
|
|
151
212
|
async def test_optimize_does_not_retry_non_conflict_error(monkeypatch, tmp_path) -> None:
|
|
152
213
|
"""A non-conflict exception is re-raised (captured per-table), never retried."""
|
|
153
214
|
from java_codebase_rag import lance_optimize
|