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
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,7 @@ Full schemas, `NodeFilter` / `EdgeFilter` semantics, and the hints contract live
jrag status # index health (ontology version, freshness, counts)
jrag microservices # microservices with resolved type counts
jrag map # counts per kind per service/module
jrag map --module # group by module instead
jrag map --by module # group by module instead (--module filters)
jrag conventions # dominant roles + framework tallies
jrag overview chat-core # bundle for a microservice
jrag overview /chat/assign # route flow (inbound callers + outbound CALLS)
Expand Down
95 changes: 93 additions & 2 deletions docs/JAVA-CODEBASE-RAG-CLI.md
Original file line number Diff line number Diff line change
Expand Up @@ -355,9 +355,100 @@ Prefer **`java-codebase-rag reprocess --graph-only`** when you only need Ladybug
- [CODEBASE_REQUIREMENTS.md](./CODEBASE_REQUIREMENTS.md) — repo layout, brownfield, when to rebuild.
- [MANUAL-VERIFICATION-CHECKLIST.md](./MANUAL-VERIFICATION-CHECKLIST.md) — phased checks that mix CLI + MCP.

## `jrag` command — search CLI
## `jrag` command — agent CLI

The **`jrag`** command provides semantic search over the LanceDB index. This is the CLI surface for search (see MCP `search` tool in AGENT-GUIDE.md for the programmatic interface).
`jrag` is the **agent-facing** CLI — a separate console script alongside `java-codebase-rag`. It exposes **one command per engineering intent** over the same LanceDB vectors + LadybugDB graph as the MCP surface, and takes human-readable identifiers (FQN / simple name / route path / topic) — never raw node IDs. Every `<query>` command resolves the identifier as its first step; on `many` candidates it returns them and stops, on `none` it returns `not_found` (auto-pick is forbidden).

Output defaults to compact text; `--format json` emits the shared envelope verbatim. Shared flags apply to most commands: `--service`, `--module`, `--limit`, `--format`, `--detail {brief,normal,full}`, `--index-dir`. `<query>` commands also take resolve hints (`--kind`, `--role`, `--fqn-contains`). Run `jrag <command> --help` for the per-command synopsis; for the full envelope contract and flag table, see [`jrag` — agent CLI](../README.md#jrag--agent-cli) in the README.

A missing or stale index produces an actionable `status: error` envelope (exit **2**) rather than a traceback.

### Command reference

Commands are grouped by engineering intent. Every `<query>` argument is a human-readable identifier (FQN, `Symbol#method(args)`, route path, or topic), resolved before any graph walk.

#### Orientation

Index health and coarse-grained maps — no identifier needed.

```bash
jrag status # index freshness, ontology version, counts
jrag microservices # microservices with resolved type counts
jrag map # symbol counts per kind, grouped by microservice
jrag map --by module # ...grouped by module instead
jrag conventions # dominant roles + framework tallies
jrag overview chat-core # bundle for a microservice
jrag overview /chat/assign # route flow (inbound callers + outbound CALLS)
jrag overview banking.chat # topic: producers + consumers
jrag overview chat-core --as microservice # override auto-detection
```

`overview` dispatches on its subject: a `/`-prefixed string is a route (same as `flow`), a known microservice name yields its routes + clients + producers, otherwise it is treated as a topic. `--as {microservice,route,topic}` overrides the auto-detection.

#### Locate

Find a specific node by name, or inspect one in full. `<query>` commands resolve first.

```bash
jrag find ChatService # exact name / FQN lookup (symbols only)
jrag find --role CONTROLLER # filter mode (structured NodeFilter flags)
jrag find --framework spring_mvc --capability HTTP_CLIENT
jrag inspect ChatService # full node record + edge summary
jrag outline src/main/.../Foo.java # symbols declared in a file
jrag imports src/main/.../Foo.java # imports resolved to graph nodes
```

`find` has two modes: a positional `<query>` for exact name/FQN lookup (symbols only), or **filter mode** (no positional) using structured flags (`--role`, `--java-kind`, `--annotation`, `--capability`, `--framework`, `--http-method`, `--client-kind`, `--producer-kind`, `--topic-contains`, …). Domain flags imply `--kind` when omitted; `--offset` paginates in filter mode only.

#### Listings

Surface every node of a given Java role. No `<query>`; pair with `--service` / `--module` to scope.

```bash
jrag http-routes # HTTP routes
jrag http-clients # HTTP clients (Feign / RestTemplate / WebClient)
jrag producers # async message producers (Kafka / StreamBridge)
jrag topics # message topics, grouped by producer
jrag jobs # scheduled tasks (@Scheduled)
jrag listeners # message listeners (@KafkaListener etc.)
jrag entities # JPA entities
```

#### Traversals

One-hop and multi-hop walks. **All resolve-first** — pass a human-readable identifier.

```bash
jrag callers ChatService#assign(Request) # who calls me?
jrag callers ChatIngressController # a controller also lists its EXPOSES routes
jrag callees ChatService#assign(Request) # what do I call?
jrag dependencies ChatService # types this Symbol injects (INJECTS out)
jrag dependents PaymentGateway # who injects this type?
jrag hierarchy AbstractBase # type tree (parents + children)
jrag implementations PaymentProcessor # classes implementing an interface
jrag subclasses AbstractRepository # classes extending a type
jrag overrides Impl#run() # methods this overrides (dispatch UP)
jrag overridden-by Iface#run() # methods overriding this (dispatch DOWN)
jrag impact PaymentGateway # fleet-wide blast radius (reverse closure)
jrag decompose ChatIngressController#assign # role-waterfall flow from an entrypoint
jrag flow /chat/assign # request flow through a route
jrag connection chat-core # cross-service connections (inbound/outbound)
```

#### Semantic search

```bash
jrag search "assign a chat agent" # semantic over Lance (java table)
jrag search "kafka" --table all # java + sql + yaml tables
```

See [`jrag search`](#jrag-search) below for the full flag reference (hybrid, explain, dedup, pagination, role/framework filters, generated-source filtering).

#### Maintenance

```bash
jrag vocab-index # rebuild the vocabulary sidecar (did-you-mean / absence diagnosis)
```

### `jrag search`

Expand Down
36 changes: 33 additions & 3 deletions src/java_codebase_rag/analysis/pr_analysis.py
Original file line number Diff line number Diff line change
Expand Up @@ -377,6 +377,31 @@ def _route_ids_for_symbol(graph: Any, symbol_id: str) -> list[str]:
return out


def _route_natural_id(graph: Any, rid: str) -> str:
"""Map a raw Route node id to its agent-facing natural identifier.

Mirrors the envelope contract (``METHOD path`` for HTTP endpoints,
``topic:<name>`` for kafka topics surfaced as :Route) so ``routes_touched``
in the PR risk report is readable instead of leaking raw graph ids like
``r:970ffaa960a4f65d``. Falls back to ``rid`` only if the route vanished.
"""
rows = graph._rows(
"MATCH (r:Route {id: $rid}) "
"RETURN r.method AS method, r.path_template AS path_template, "
"r.path AS path, r.topic AS topic LIMIT 1",
{"rid": rid},
)
if not rows:
return rid
r = rows[0]
method = str(r.get("method") or "")
path = str(r.get("path_template") or r.get("path") or "")
if method or path:
return f"{method} {path}".strip()
topic = str(r.get("topic") or "")
return f"topic:{topic}" if topic else rid


def compute_risk(graph: Any, changed: list[ChangedSymbol]) -> PrRiskReport:
"""Aggregate blast radius, routes, cross-service callers, and v1 risk score.

Expand Down Expand Up @@ -415,7 +440,9 @@ def compute_risk(graph: Any, changed: list[ChangedSymbol]) -> PrRiskReport:
needle = _impact_needle_for_changed(graph, fqn, cs.kind)
ia = graph.impact_analysis(needle, depth=2, limit=400)
n = len(ia)
blast_by[cs.symbol_id] = n
# Key blast radius by the symbol's FQN (agent-facing identifier), not
# its raw graph id — the report is operator-facing JSON.
blast_by[fqn or cs.symbol_id] = n
blast_total += n

for e in graph.find_callers(cs.fqn, depth=2, limit=400):
Expand All @@ -429,8 +456,11 @@ def compute_risk(graph: Any, changed: list[ChangedSymbol]) -> PrRiskReport:
cs_cross_service = 0
route_ids = _route_ids_for_symbol(graph, cs.symbol_id)
for rid in route_ids:
if rid not in routes:
routes.append(rid)
# Record the route by its natural identifier (METHOD path /
# topic:name), not the raw graph id.
label = _route_natural_id(graph, rid)
if label and label not in routes:
routes.append(label)
callers = graph._rows(
"MATCH (s:Symbol)-[:DECLARES_CLIENT]->(c:Client)-[e:HTTP_CALLS]->(r:Route {id: $rid}) "
"WHERE e.match = 'cross_service' "
Expand Down
17 changes: 10 additions & 7 deletions src/java_codebase_rag/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -846,7 +846,15 @@ def _cmd_unresolved_calls_list(args: argparse.Namespace) -> int:
callee_simple=args.callee_simple,
limit=int(args.limit),
)
_emit({"success": True, "count": len(rows), "sites": rows})
# Drop the raw caller symbol id: the row already carries the agent-facing
# ``caller_fqn``, so ``caller_id`` is redundant noise in an operator-facing
# report. The call-site ``id`` (``ucs:``) is kept — it's each site's
# primary key, not a caller reference.
sites = [
{k: v for k, v in row.items() if k != "caller_id"}
for row in rows
]
_emit({"success": True, "count": len(sites), "sites": sites})
return 0


Expand Down Expand Up @@ -1060,12 +1068,7 @@ def build_parser() -> argparse.ArgumentParser:
)
_add_index_embedding_flags(erase)
erase.add_argument("--yes", action="store_true", help="Confirm destructive deletion (required in CI)")
erase.add_argument(
"--quiet", "-q",
action="store_true",
dest="quiet",
help="Suppress stderr progress relay; stdout payload unchanged.",
)
_add_verbosity_flags(erase)
erase.set_defaults(handler=_cmd_erase)

meta = subparsers.add_parser("meta", help="Print graph meta and embedding resolution.")
Expand Down
131 changes: 109 additions & 22 deletions src/java_codebase_rag/graph/graph_types.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,24 @@ class NodeRef(BaseModel):
role: str | None = None
generated: bool | None = None
generated_by: str | None = None
# Identity-adjacent fields consumed by ``jrag_envelope.node_key`` (keying)
# and ``project_node`` allow-lists (rendering) so find filter-mode nodes
# carry the same rich shape as the dedicated listings (http-routes /
# http-clients / producers). All optional with default None: existing
# NodeRef constructors are unaffected and ``_drop_empty`` strips unset
# fields at the JSON boundary.
method: str | None = None
path: str | None = None
framework: str | None = None
member_fqn: str | None = None
target_service: str | None = None
client_kind: str | None = None
topic: str | None = None
broker: str | None = None
producer_kind: str | None = None
filename: str | None = None
start_line: int | None = None
resolved: bool | None = None


class StructuredHint(BaseModel):
Expand Down Expand Up @@ -98,38 +116,107 @@ def _resolve_node_kind(


def _node_ref_from_row(kind: Literal["symbol", "route", "client", "producer"], row: dict[str, Any]) -> NodeRef:
symbol_kind: str | None = None
"""Map a graph store row to a :class:`NodeRef`.

``fqn`` is set to each kind's documented natural identifier (README
§"jrag — agent CLI") so ``jrag_envelope.node_key`` (which checks ``fqn``
first) keys nodes correctly and no raw graph id leaks:

* symbol -> symbol fqn
* route -> ``"METHOD path"`` (HTTP endpoint); ``"topic:<name>"`` when a
kafka topic surfaces as :Route; ``""`` for a phantom route
(node_key then falls through to the composed ``file``)
* client -> ``"member_fqn->target_service"``
* producer-> ``"topic:<name>"``

Rich detail (member_fqn / target_service / method / path / topic /
framework / filename / start_line / resolved) is populated alongside so
find filter-mode nodes render with the same shape as the dedicated
listings (``http-routes`` / ``http-clients`` / ``producers``) instead of
collapsing to ``{"kind": ...}`` after the envelope's projection+drop-empty.
"""
microservice = str(row.get("microservice") or "") or None
module = str(row.get("module") or "") or None
filename = str(row.get("filename") or "") or None
start_line = row.get("start_line")
try:
start_line = int(start_line) if start_line not in (None, "") else None
except (TypeError, ValueError):
start_line = None
resolved_raw = row.get("resolved")
resolved = bool(resolved_raw) if resolved_raw is not None else None
nid = str(row.get("id") or "")

if kind == "symbol":
fqn = str(row.get("fqn") or "")
role = str(row.get("role") or "") or None
symbol_kind_val = str(row.get("symbol_kind") or row.get("kind") or "").strip()
symbol_kind = symbol_kind_val or None
elif kind == "route":
return NodeRef(
id=nid, kind="symbol", fqn=fqn,
name=str(row.get("name") or "") or None,
symbol_kind=symbol_kind,
microservice=microservice, module=module, role=role,
filename=filename, start_line=start_line,
generated=bool(row.get("generated")) if row.get("generated") is not None else None,
generated_by=str(row.get("generated_by")) if row.get("generated_by") else None,
)

if kind == "route":
method = str(row.get("method") or "")
path = str(row.get("path_template") or row.get("path") or "")
fqn = f"{method} {path}".strip()
role = None
elif kind == "client":
topic = str(row.get("topic") or "") or None
if method or path:
fqn = f"{method} {path}".strip()
elif topic:
fqn = f"topic:{topic}"
else:
fqn = ""
return NodeRef(
id=nid, kind="route", fqn=fqn, name=(path or topic),
method=method or None, path=path or None, topic=topic,
framework=str(row.get("framework") or "") or None,
microservice=microservice, module=module,
filename=filename, start_line=start_line, resolved=resolved,
)

if kind == "client":
mfqn = str(row.get("member_fqn") or "")
tgt = str(row.get("target_service") or "")
method = str(row.get("method") or "")
target = str(row.get("target_service") or "")
path = str(row.get("path_template") or row.get("path") or "")
fqn = f"{target} {method} {path}".strip()
role = None
else:
topic = str(row.get("topic") or "")
broker = str(row.get("broker") or "")
fqn = f"{topic} {broker}".strip()
role = None
member_fqn = mfqn or None
target_service = tgt or None
member_simple = (mfqn.rsplit(".", 1)[-1] if mfqn else "") or None
# Build the contract id ``member_fqn->target_service`` from the RAW
# strings: ``member_fqn`` was normalized to None above, so interpolating
# it would emit ``"None-><target>"`` for member-less (brownfield/meta)
# clients. Fall back to whichever half is present; never a dangling arrow.
fqn = f"{mfqn}->{tgt}" if (mfqn and tgt) else (mfqn or tgt)
return NodeRef(
id=nid, kind="client",
fqn=fqn,
name=member_simple,
member_fqn=member_fqn, target_service=target_service,
method=method or None, path=path or None,
client_kind=str(row.get("client_kind") or "") or None,
microservice=microservice, module=module,
filename=filename, start_line=start_line, resolved=resolved,
)

# producer
topic = str(row.get("topic") or "") or None
member_fqn = str(row.get("member_fqn") or "") or None
member_simple = (member_fqn.rsplit(".", 1)[-1] if member_fqn else "") or None
return NodeRef(
id=str(row.get("id") or ""),
kind=kind,
fqn=fqn,
symbol_kind=symbol_kind,
microservice=str(row.get("microservice") or "") or None,
module=str(row.get("module") or "") or None,
role=role,
generated=bool(row.get("generated")) if row.get("generated") is not None else None,
generated_by=str(row.get("generated_by")) if row.get("generated_by") else None,
id=nid, kind="producer",
fqn=(f"topic:{topic}" if topic else ""),
name=(topic or member_simple),
topic=topic, broker=str(row.get("broker") or "") or None,
producer_kind=str(row.get("producer_kind") or "") or None,
member_fqn=member_fqn,
microservice=microservice, module=module,
filename=filename, start_line=start_line, resolved=resolved,
)


Expand Down
Loading
Loading