java-codebase-rag 0.9.7__py3-none-any.whl → 0.10.1__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.
- java_codebase_rag/analysis/pr_analysis.py +33 -3
- java_codebase_rag/ast/ast_java.py +2 -1
- java_codebase_rag/cli.py +23 -8
- java_codebase_rag/config.py +68 -1
- java_codebase_rag/graph/build_ast_graph.py +123 -4
- java_codebase_rag/graph/graph_types.py +109 -22
- java_codebase_rag/graph/ladybug_queries.py +45 -2
- java_codebase_rag/index/java_index_flow_lancedb.py +10 -16
- java_codebase_rag/install_data/agents/explorer-rag-cli.md +3 -1
- java_codebase_rag/install_data/skills/explore-codebase-cli/SKILL.md +3 -1
- java_codebase_rag/jrag.py +627 -661
- java_codebase_rag/jrag_render.py +160 -3
- java_codebase_rag/lance_optimize.py +11 -12
- java_codebase_rag/mcp/mcp_v2.py +2 -1
- java_codebase_rag/pipeline.py +47 -1
- java_codebase_rag/read_payloads.py +781 -0
- java_codebase_rag/search/search_lancedb.py +138 -6
- java_codebase_rag/search/search_lexical.py +140 -30
- java_codebase_rag/search/search_scoring.py +108 -0
- java_codebase_rag/watch/__init__.py +0 -0
- java_codebase_rag/watch/client.py +230 -0
- java_codebase_rag/watch/daemon.py +368 -0
- java_codebase_rag/watch/lock.py +201 -0
- java_codebase_rag/watch/paths.py +76 -0
- java_codebase_rag/watch/protocol.py +122 -0
- java_codebase_rag/watch/server.py +273 -0
- java_codebase_rag/watch/warm.py +105 -0
- java_codebase_rag/watch/watcher.py +352 -0
- {java_codebase_rag-0.9.7.dist-info → java_codebase_rag-0.10.1.dist-info}/METADATA +30 -31
- {java_codebase_rag-0.9.7.dist-info → java_codebase_rag-0.10.1.dist-info}/RECORD +34 -24
- {java_codebase_rag-0.9.7.dist-info → java_codebase_rag-0.10.1.dist-info}/WHEEL +0 -0
- {java_codebase_rag-0.9.7.dist-info → java_codebase_rag-0.10.1.dist-info}/entry_points.txt +0 -0
- {java_codebase_rag-0.9.7.dist-info → java_codebase_rag-0.10.1.dist-info}/licenses/LICENSE +0 -0
- {java_codebase_rag-0.9.7.dist-info → java_codebase_rag-0.10.1.dist-info}/top_level.txt +0 -0
java_codebase_rag/jrag_render.py
CHANGED
|
@@ -10,12 +10,13 @@ invariant.
|
|
|
10
10
|
"""
|
|
11
11
|
from __future__ import annotations
|
|
12
12
|
|
|
13
|
+
import json
|
|
13
14
|
from typing import Any
|
|
14
15
|
|
|
15
16
|
from java_codebase_rag.absence.absence_types import AbsenceDiagnosis
|
|
16
17
|
from java_codebase_rag.jrag_envelope import Envelope, project_envelope, simple_name
|
|
17
18
|
|
|
18
|
-
__all__ = ["render", "tiered_name", "display_name"]
|
|
19
|
+
__all__ = ["render", "tiered_name", "display_name", "count_results", "has_results"]
|
|
19
20
|
|
|
20
21
|
|
|
21
22
|
# Edge labels that carry a ``confidence`` column (CALLS-family). ``conf:`` is
|
|
@@ -125,6 +126,23 @@ def _next_action_lines(envelope: Envelope) -> list[str]:
|
|
|
125
126
|
return [f"next: {hint}" for hint in envelope.agent_next_actions[:2]]
|
|
126
127
|
|
|
127
128
|
|
|
129
|
+
def _is_dynamic_topic_ref(topic: str) -> bool:
|
|
130
|
+
"""True when a producer ``topic`` string is a bare Java identifier — a
|
|
131
|
+
variable or method-call name the indexer captured because it could not
|
|
132
|
+
resolve the destination at index time — rather than a real topic name.
|
|
133
|
+
|
|
134
|
+
Real topics are dotted (``banking.chat.audit``) or CONSTANT references
|
|
135
|
+
(``ChatTopics.ESCALATION``, ``OPERATOR_NOTIFICATIONS``); both contain a
|
|
136
|
+
``.`` or ``_``. A single lowercase/identifier token (``topic``,
|
|
137
|
+
``distributionTopic``) is a runtime reference. Used by :func:`display_name`
|
|
138
|
+
only when the producer is also ``resolved=False`` — a resolved bare token is
|
|
139
|
+
treated as a (rare) real single-word topic.
|
|
140
|
+
"""
|
|
141
|
+
if "." in topic or "_" in topic:
|
|
142
|
+
return False
|
|
143
|
+
return bool(topic) and topic[0].islower() and topic.isalnum()
|
|
144
|
+
|
|
145
|
+
|
|
128
146
|
def display_name(node: dict[str, Any]) -> str:
|
|
129
147
|
"""Best short label for a node across all kinds (symbol + route/client/producer).
|
|
130
148
|
|
|
@@ -175,6 +193,15 @@ def display_name(node: dict[str, Any]) -> str:
|
|
|
175
193
|
base = member_fqn.rsplit(".", 1)[-1]
|
|
176
194
|
topic = str(node.get("topic") or "").strip()
|
|
177
195
|
if topic:
|
|
196
|
+
# An unresolved topic that is a bare Java identifier is a runtime
|
|
197
|
+
# reference the indexer could not resolve (e.g. the variable
|
|
198
|
+
# ``topic``, or ``getKafka().getDistributionTopic()`` reduced to
|
|
199
|
+
# ``distributionTopic``). Printing it verbatim would show
|
|
200
|
+
# ``→ topic`` and mislead the agent into treating the variable name
|
|
201
|
+
# as the Kafka destination; surface it as dynamic instead. Real
|
|
202
|
+
# topic names (dotted / CONSTANT) stay verbatim.
|
|
203
|
+
if node.get("resolved") is False and _is_dynamic_topic_ref(topic):
|
|
204
|
+
return f"{base} → (dynamic topic)"
|
|
178
205
|
return f"{base} → {topic}"
|
|
179
206
|
target = str(node.get("target_service") or "").strip()
|
|
180
207
|
if target:
|
|
@@ -619,8 +646,24 @@ def _render_inspect_block(node: dict[str, Any], indent: int) -> list[str]:
|
|
|
619
646
|
out.extend(_render_inspect_block(val, indent + 1))
|
|
620
647
|
elif _is_dict_list(val):
|
|
621
648
|
out.append(f"{pad}{key}:")
|
|
649
|
+
item_pad = " " * (indent + 1)
|
|
650
|
+
# Rich items (>2 keys: e.g. ``topics`` producers) render one kv per
|
|
651
|
+
# line for readability — a wall of comma-joined keys is unreadable
|
|
652
|
+
# and hides the composed ``file`` location. Short sample items
|
|
653
|
+
# (``route_sample`` / ``client_sample``) stay on a single line.
|
|
654
|
+
rich = any(len(it) > 2 for it in val)
|
|
622
655
|
for item in val:
|
|
623
|
-
|
|
656
|
+
if rich:
|
|
657
|
+
# Render at indent+2 so continuation kvs align under the
|
|
658
|
+
# first kv (which sits after the `- ` marker at indent+1).
|
|
659
|
+
item_lines = _render_inspect_block(item, indent + 2)
|
|
660
|
+
if not item_lines:
|
|
661
|
+
out.append(f"{item_pad}- {{}}")
|
|
662
|
+
else:
|
|
663
|
+
out.append(f"{item_pad}- {item_lines[0].lstrip()}")
|
|
664
|
+
out.extend(item_lines[1:])
|
|
665
|
+
else:
|
|
666
|
+
out.append(f"{item_pad}- {_inspect_inline(item)}")
|
|
624
667
|
else:
|
|
625
668
|
out.append(f"{pad}{key}: {_inspect_inline(val)}")
|
|
626
669
|
return out
|
|
@@ -717,6 +760,92 @@ def _render_text_shape(envelope: Envelope, *, noun: str, shape: str | None, deta
|
|
|
717
760
|
return _render_scalar(envelope)
|
|
718
761
|
|
|
719
762
|
|
|
763
|
+
def count_results(envelope: Envelope, shape: str | None) -> int:
|
|
764
|
+
"""How many result items ``envelope`` represents, per its render shape.
|
|
765
|
+
|
|
766
|
+
The single source of truth shared by ``--count`` (output) and ``--exists``
|
|
767
|
+
(output + exit code). Counts the collection the agent thinks of as the
|
|
768
|
+
result, not the raw node dict (a traversal envelope includes the root
|
|
769
|
+
subject in ``nodes`` — counting nodes would report N+1, not N):
|
|
770
|
+
|
|
771
|
+
* ``shape="inspect"`` -> 1 when a subject resolved (else 0). ``inspect``
|
|
772
|
+
/``status``/``map``/``conventions``/``overview`` declare this shape.
|
|
773
|
+
* ``root is not None`` (traversal) -> ``len(edges)``: the callers/callees/
|
|
774
|
+
hierarchy members are the result; the root is the resolved subject.
|
|
775
|
+
* otherwise (listing) -> ``len(nodes)``.
|
|
776
|
+
|
|
777
|
+
Callers that only care about emptiness should use :func:`has_results`, which
|
|
778
|
+
also gates on ``status == "ok"`` (a ``not_found`` / ``error`` envelope has no
|
|
779
|
+
result regardless of any carried nodes/edges).
|
|
780
|
+
"""
|
|
781
|
+
if shape == "inspect":
|
|
782
|
+
return 1 if envelope.nodes else 0
|
|
783
|
+
if envelope.root is not None:
|
|
784
|
+
return len(envelope.edges)
|
|
785
|
+
return len(envelope.nodes)
|
|
786
|
+
|
|
787
|
+
|
|
788
|
+
def has_results(envelope: Envelope, shape: str | None) -> bool:
|
|
789
|
+
"""True only when the envelope is a non-empty ``ok`` result.
|
|
790
|
+
|
|
791
|
+
``not_found`` / ``ambiguous`` / ``error`` carry no result even when they
|
|
792
|
+
carry nodes/candidates (e.g. ambiguous candidates are narrowing hints, not
|
|
793
|
+
hits). Used by ``--exists`` for both its output and its exit code.
|
|
794
|
+
"""
|
|
795
|
+
return envelope.status == "ok" and count_results(envelope, shape) > 0
|
|
796
|
+
|
|
797
|
+
|
|
798
|
+
def _field_set(fields: str) -> set[str]:
|
|
799
|
+
"""Parse a comma-separated ``--fields`` allowlist into a set of trimmed names."""
|
|
800
|
+
return {name.strip() for name in fields.split(",") if name.strip()}
|
|
801
|
+
|
|
802
|
+
|
|
803
|
+
def _project_to_fields(envelope: Envelope, fields: str) -> Envelope:
|
|
804
|
+
"""Return a copy of ``envelope`` (at FULL detail) whose nodes keep only the
|
|
805
|
+
requested field names.
|
|
806
|
+
|
|
807
|
+
``--fields`` is an explicit allowlist that OVERRIDES ``--detail``: project to
|
|
808
|
+
full (so a ``full``-tier field like ``signature`` is available even at the
|
|
809
|
+
default detail), then keep only the requested keys per node. Names absent on
|
|
810
|
+
a node are simply not present. Graph-id fields stay stripped
|
|
811
|
+
(``project_envelope(full)`` already strips them). Edges/candidates are left
|
|
812
|
+
at full; ``--fields`` is documented as a node projection.
|
|
813
|
+
|
|
814
|
+
Delegates the envelope copy to :func:`project_envelope` (the single
|
|
815
|
+
projection seam) and only rewrites ``nodes`` on the already-copied result,
|
|
816
|
+
so the per-field Envelope construction can't drift out of sync as fields are
|
|
817
|
+
added. ``project_envelope`` returns an independent copy and the node dicts
|
|
818
|
+
are rebuilt by the comprehension, so the caller's ``envelope`` is untouched.
|
|
819
|
+
"""
|
|
820
|
+
wanted = _field_set(fields)
|
|
821
|
+
projected = project_envelope(envelope, "full")
|
|
822
|
+
projected.nodes = {
|
|
823
|
+
nid: {k: v for k, v in node.items() if k in wanted}
|
|
824
|
+
for nid, node in projected.nodes.items()
|
|
825
|
+
}
|
|
826
|
+
return projected
|
|
827
|
+
|
|
828
|
+
|
|
829
|
+
def _render_count(envelope: Envelope, *, fmt: str, shape: str | None) -> str:
|
|
830
|
+
"""``--count`` output: bare integer (text) or ``{"status","count"}`` (json).
|
|
831
|
+
|
|
832
|
+
Non-``ok`` envelopes count as 0 (a miss / error has no result items). Text is
|
|
833
|
+
the bare count — "only the result count", script-friendly for ``$(...)``.
|
|
834
|
+
"""
|
|
835
|
+
n = count_results(envelope, shape) if envelope.status == "ok" else 0
|
|
836
|
+
if fmt == "json":
|
|
837
|
+
return json.dumps({"status": envelope.status, "count": n})
|
|
838
|
+
return str(n)
|
|
839
|
+
|
|
840
|
+
|
|
841
|
+
def _render_exists(envelope: Envelope, *, fmt: str, shape: str | None) -> str:
|
|
842
|
+
"""``--exists`` output: ``true``/``false`` (text) or ``{"status","exists"}`` (json)."""
|
|
843
|
+
exists = has_results(envelope, shape)
|
|
844
|
+
if fmt == "json":
|
|
845
|
+
return json.dumps({"status": envelope.status, "exists": exists})
|
|
846
|
+
return "true" if exists else "false"
|
|
847
|
+
|
|
848
|
+
|
|
720
849
|
def render(
|
|
721
850
|
envelope: Envelope,
|
|
722
851
|
*,
|
|
@@ -725,6 +854,9 @@ def render(
|
|
|
725
854
|
noun: str = "",
|
|
726
855
|
next_offset: int | None = None,
|
|
727
856
|
shape: str | None = None,
|
|
857
|
+
count: bool = False,
|
|
858
|
+
exists: bool = False,
|
|
859
|
+
fields: str | None = None,
|
|
728
860
|
) -> str:
|
|
729
861
|
"""Dispatch on ``fmt`` (text default; json emits the projected envelope).
|
|
730
862
|
|
|
@@ -750,8 +882,33 @@ def render(
|
|
|
750
882
|
``nodes``/``noun`` -> listing, else scalar. Listing nodes frequently carry
|
|
751
883
|
dict-valued fields after ``.model_dump()``, so inspect is NEVER inferred
|
|
752
884
|
from node contents - only an explicit ``shape="inspect"`` routes there.
|
|
885
|
+
|
|
886
|
+
Output-shaping flags (issue #376), orthogonal to the command that produced
|
|
887
|
+
the envelope and honored on both text and json:
|
|
888
|
+
|
|
889
|
+
* ``exists=True`` -> ``true``/``false`` (or ``{"status","exists"}``). Takes
|
|
890
|
+
precedence over ``count`` (a gate is more specific than a tally).
|
|
891
|
+
* ``count=True`` -> the bare result count (or ``{"status","count"}``); see
|
|
892
|
+
:func:`count_results` for what is counted per shape.
|
|
893
|
+
* ``fields`` -> comma-separated node-field allowlist that OVERRIDES
|
|
894
|
+
``--detail`` (project to full, then keep only the named fields). Applies
|
|
895
|
+
only to ``status="ok"`` output; composes with the normal render, not with
|
|
896
|
+
``count``/``exists``. A whitespace/comma-only allowlist is treated as
|
|
897
|
+
not given (falls back to the normal projection). Primarily a JSON lever
|
|
898
|
+
(text rendering still labels rows from whatever identity fields survive
|
|
899
|
+
the allowlist).
|
|
900
|
+
|
|
901
|
+
The exit-code side of ``--exists`` is decided by the caller (``jrag._emit``)
|
|
902
|
+
via :func:`has_results` — render() only shapes output.
|
|
753
903
|
"""
|
|
754
|
-
|
|
904
|
+
if exists:
|
|
905
|
+
return _render_exists(envelope, fmt=fmt, shape=shape)
|
|
906
|
+
if count:
|
|
907
|
+
return _render_count(envelope, fmt=fmt, shape=shape)
|
|
908
|
+
if fields and envelope.status == "ok" and _field_set(fields):
|
|
909
|
+
projected = _project_to_fields(envelope, fields)
|
|
910
|
+
else:
|
|
911
|
+
projected = project_envelope(envelope, detail)
|
|
755
912
|
if fmt == "json":
|
|
756
913
|
return projected.to_json()
|
|
757
914
|
body = _render_text_shape(projected, noun=noun, shape=shape, detail=detail)
|
|
@@ -1,21 +1,20 @@
|
|
|
1
1
|
"""Serialized post-flow LanceDB optimize with commit-conflict retry.
|
|
2
2
|
|
|
3
|
-
cocoindex 1.0.7
|
|
4
|
-
|
|
5
|
-
``table.delete()`` (**Delete**)
|
|
6
|
-
LanceDB does not allow a Rewrite to commit concurrently with a
|
|
7
|
-
(upstream lancedb#1504
|
|
8
|
-
which surfaces as a flood of::
|
|
3
|
+
Historically (cocoindex 1.0.7) this existed because cocoindex scheduled
|
|
4
|
+
``table.optimize()`` (a LanceDB **Rewrite**/compaction) as a *background*
|
|
5
|
+
``asyncio`` task that raced concurrent ``table.delete()`` (**Delete**)
|
|
6
|
+
transactions — LanceDB does not allow a Rewrite to commit concurrently with a
|
|
7
|
+
Delete (upstream lancedb#1504), surfacing as a flood of::
|
|
9
8
|
|
|
10
9
|
RuntimeError: lance error: Retryable commit conflict for version N: \
|
|
11
10
|
This Rewrite transaction was preempted by concurrent transaction Delete ...
|
|
12
11
|
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
commit conflict that two internal compaction passes can still produce.
|
|
12
|
+
cocoindex >=1.0.15 made optimize **inline and stats-driven** (only compacts
|
|
13
|
+
when small fragments accumulate, inside the merge_insert commit path), so the
|
|
14
|
+
race is gone and the flow no longer disables anything. This module still runs a
|
|
15
|
+
*single*, serialized optimize after the flow returns (exit 0 → no concurrent
|
|
16
|
+
writers) as a clean final compaction + scalar/FTS index build, retrying the rare
|
|
17
|
+
residual commit conflict that two internal compaction passes can still produce.
|
|
19
18
|
"""
|
|
20
19
|
from __future__ import annotations
|
|
21
20
|
|
java_codebase_rag/mcp/mcp_v2.py
CHANGED
|
@@ -1136,7 +1136,8 @@ def find_v2(
|
|
|
1136
1136
|
)
|
|
1137
1137
|
params["lim"] = fetch_cap
|
|
1138
1138
|
rows = g._rows( # noqa: SLF001
|
|
1139
|
-
f"MATCH (s:Symbol) {where} RETURN s.id AS id, s.fqn AS fqn, s.
|
|
1139
|
+
f"MATCH (s:Symbol) {where} RETURN s.id AS id, s.fqn AS fqn, s.name AS name, "
|
|
1140
|
+
"s.filename AS filename, s.start_line AS start_line, s.microservice AS microservice, "
|
|
1140
1141
|
"s.module AS module, s.role AS role, s.kind AS symbol_kind, s.generated AS generated, s.generated_by AS generated_by ORDER BY s.fqn LIMIT $lim",
|
|
1141
1142
|
params,
|
|
1142
1143
|
)
|
java_codebase_rag/pipeline.py
CHANGED
|
@@ -133,14 +133,60 @@ def _popen_capturing_stderr(
|
|
|
133
133
|
t_err = threading.Thread(target=drain_err, name="stream-stderr", daemon=True)
|
|
134
134
|
t_out.start()
|
|
135
135
|
t_err.start()
|
|
136
|
+
# Wait on the CHILD before joining the drain threads. ``Popen.wait()`` is
|
|
137
|
+
# interruptible by Ctrl+C — the underlying ``os.waitpid`` returns EINTR and
|
|
138
|
+
# CPython raises ``KeyboardInterrupt`` — whereas ``Thread.join()`` blocks on
|
|
139
|
+
# an internal lock whose infinite-timeout acquire CPython never polls for
|
|
140
|
+
# signals. Joining *first* therefore made the whole indexing step ignore
|
|
141
|
+
# Ctrl+C until the child happened to close its pipes; cocoindex's teardown
|
|
142
|
+
# (and any flow-server grandchild it spawned) can hold them open for a long
|
|
143
|
+
# time, so an install/reprocess could not be aborted. The daemon drain
|
|
144
|
+
# threads keep the child's stdout/stderr pipes empty while we wait, so the
|
|
145
|
+
# child never blocks on a full pipe.
|
|
146
|
+
try:
|
|
147
|
+
code = proc.wait()
|
|
148
|
+
except BaseException:
|
|
149
|
+
# Ctrl+C or any other abort: best-effort, NON-BLOCKING child teardown
|
|
150
|
+
# so a process we spawned does not outlive us, then re-raise WITHOUT
|
|
151
|
+
# joining the drain threads. They may be blocked on a pipe the child
|
|
152
|
+
# still owns, and a join here would re-introduce the very hang this
|
|
153
|
+
# reordering fixes. The threads are daemons, so they vanish at exit.
|
|
154
|
+
_abort_child(proc)
|
|
155
|
+
raise
|
|
156
|
+
# Normal exit: the child is gone, its pipes hit EOF, and the drain threads
|
|
157
|
+
# return promptly — safe to join here. (If a pipe-inheriting grandchild
|
|
158
|
+
# lingered past the child's exit, these joins would block until it too
|
|
159
|
+
# closed its write ends — on Ctrl+C the shared-process-group SIGINT reaches
|
|
160
|
+
# it and closes them. This window is the short teardown-after-indexing phase,
|
|
161
|
+
# not the long indexing phase the wait-first reorder already made
|
|
162
|
+
# interruptible.)
|
|
136
163
|
t_out.join()
|
|
137
164
|
t_err.join()
|
|
138
165
|
if filt is not None:
|
|
139
166
|
filt.flush()
|
|
140
|
-
code = proc.wait()
|
|
141
167
|
return out_buf.decode(errors="replace"), err_buf.decode(errors="replace"), code
|
|
142
168
|
|
|
143
169
|
|
|
170
|
+
def _abort_child(proc: subprocess.Popen[bytes]) -> None:
|
|
171
|
+
"""Best-effort, NON-BLOCKING teardown of a spawned child on an abort path.
|
|
172
|
+
|
|
173
|
+
Runs when ``proc.wait()`` raised (Ctrl+C, or any other exception). Sends
|
|
174
|
+
SIGTERM and returns immediately — this path exists to let the operator exit
|
|
175
|
+
*promptly*, and waiting for the child would defeat that. On Ctrl+C the child
|
|
176
|
+
already received SIGINT (same process group); this just guarantees teardown
|
|
177
|
+
for non-signal aborts, after which the child finishes shutting down on its
|
|
178
|
+
own. ``OSError`` (incl. ``ProcessLookupError`` — already-dead / zombie /
|
|
179
|
+
reaped) is swallowed — the only caller re-raises regardless.
|
|
180
|
+
"""
|
|
181
|
+
fn = getattr(proc, "terminate", None)
|
|
182
|
+
if fn is None:
|
|
183
|
+
return
|
|
184
|
+
try:
|
|
185
|
+
fn()
|
|
186
|
+
except OSError:
|
|
187
|
+
pass
|
|
188
|
+
|
|
189
|
+
|
|
144
190
|
def run_cocoindex_update(
|
|
145
191
|
env: dict[str, str],
|
|
146
192
|
*,
|