java-codebase-rag 0.9.6__py3-none-any.whl → 0.10.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- java_codebase_rag/absence/absence_vocab.py +7 -2
- 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_envelope.py +13 -0
- java_codebase_rag/jrag_render.py +160 -3
- java_codebase_rag/lance_optimize.py +11 -12
- java_codebase_rag/mcp/mcp_v2.py +16 -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 +128 -30
- java_codebase_rag/search/search_scoring.py +82 -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.6.dist-info → java_codebase_rag-0.10.0.dist-info}/METADATA +30 -31
- java_codebase_rag-0.10.0.dist-info/RECORD +67 -0
- java_codebase_rag-0.9.6.dist-info/RECORD +0 -57
- {java_codebase_rag-0.9.6.dist-info → java_codebase_rag-0.10.0.dist-info}/WHEEL +0 -0
- {java_codebase_rag-0.9.6.dist-info → java_codebase_rag-0.10.0.dist-info}/entry_points.txt +0 -0
- {java_codebase_rag-0.9.6.dist-info → java_codebase_rag-0.10.0.dist-info}/licenses/LICENSE +0 -0
- {java_codebase_rag-0.9.6.dist-info → java_codebase_rag-0.10.0.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,781 @@
|
|
|
1
|
+
"""Payload-returning cores for the ``jrag`` read commands.
|
|
2
|
+
|
|
3
|
+
Each ``<cmd>_payload(args, cfg, graph)`` function assembles the
|
|
4
|
+
JSON-serializable payload the corresponding ``jrag`` handler renders, WITHOUT
|
|
5
|
+
rendering. This lets the ``jrag watch`` daemon (later tasks) serve these
|
|
6
|
+
commands over a socket by calling the payload function and serializing,
|
|
7
|
+
reusing the exact cold read path.
|
|
8
|
+
|
|
9
|
+
Behavior-preserving extraction (Task 5 of the jrag-watch plan): the compute
|
|
10
|
+
(graph calls + folds) is lifted verbatim from the handlers in ``jrag.py``;
|
|
11
|
+
rendering stays in the handlers. Each handler is restructured to::
|
|
12
|
+
|
|
13
|
+
payload = <cmd>_payload(args, cfg, graph) # may raise PayloadError
|
|
14
|
+
render(payload, args) # unchanged render call
|
|
15
|
+
|
|
16
|
+
On resolve / kind-guard / validation failure the payload raises
|
|
17
|
+
:class:`PayloadError` carrying the :class:`Envelope` and ``rc`` the handler
|
|
18
|
+
would have rendered, so the handler can render the error byte-identically.
|
|
19
|
+
|
|
20
|
+
Payload shapes:
|
|
21
|
+
* ``search_payload`` -> ``SearchOutput`` (pydantic; the raw ``search_v2`` result)
|
|
22
|
+
* ``find_payload`` -> ``{"mode": "query"|"filter", ...}`` carrying either the
|
|
23
|
+
``find_by_name_or_fqn`` post-filtered rows or a ``FindOutput``
|
|
24
|
+
* ``inspect_payload`` -> ``{"describe": DescribeOutput, "node_id", "node_fqn",
|
|
25
|
+
"file_location"}`` (inspect's renderer needs resolve-derived
|
|
26
|
+
``file_location`` which is not on ``DescribeOutput``)
|
|
27
|
+
* ``callers_payload`` / ``callees_payload`` / ``flow_payload`` -> traversal dict
|
|
28
|
+
``{"root_id", "nodes", "edges", "noun", "warnings", "truncated",
|
|
29
|
+
"is_external_entrypoint"}`` shaped exactly as ``_emit_traversal`` consumes.
|
|
30
|
+
|
|
31
|
+
All payloads round-trip through ``json.dumps``/``loads`` (pydantic via
|
|
32
|
+
``model_dump``; the rest are plain dict/list/scalar).
|
|
33
|
+
"""
|
|
34
|
+
from __future__ import annotations
|
|
35
|
+
|
|
36
|
+
import argparse
|
|
37
|
+
from typing import Any
|
|
38
|
+
|
|
39
|
+
# Top-level imports are safe here: ``jrag`` imports this module lazily (inside
|
|
40
|
+
# its handlers), so there is no import cycle -- by the time this module loads,
|
|
41
|
+
# ``jrag`` is already fully initialized, and these are stable module-level helpers.
|
|
42
|
+
from java_codebase_rag.jrag import (
|
|
43
|
+
_build_node_filter_or_error,
|
|
44
|
+
_check_kind_contradiction,
|
|
45
|
+
_clamped_limit,
|
|
46
|
+
_dedupe_traversal_edges,
|
|
47
|
+
_infer_kind,
|
|
48
|
+
_noderef_to_node_dict,
|
|
49
|
+
_symbol_hit_to_dict,
|
|
50
|
+
_warn_unapplied_scope,
|
|
51
|
+
)
|
|
52
|
+
from java_codebase_rag.jrag_envelope import Envelope, mark_truncated, normalize_enum, resolve_query
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
class PayloadError(Exception):
|
|
56
|
+
"""Raised by a ``*_payload`` function on resolve / kind-guard / validation
|
|
57
|
+
failure.
|
|
58
|
+
|
|
59
|
+
Carries the :class:`Envelope` the handler renders and the ``rc`` it returns,
|
|
60
|
+
so the handler renders the error byte-identically to before the extraction.
|
|
61
|
+
"""
|
|
62
|
+
|
|
63
|
+
def __init__(self, env: Envelope, rc: int) -> None:
|
|
64
|
+
self.env = env
|
|
65
|
+
self.rc = rc
|
|
66
|
+
super().__init__(env.message or env.status)
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
# ---------------------------------------------------------------------------
|
|
70
|
+
# Shared resolve frame (verbatim logic of jrag._resolve_traversal_node, minus
|
|
71
|
+
# the print — the handler renders the Envelope carried by PayloadError).
|
|
72
|
+
# ---------------------------------------------------------------------------
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def _resolve_traversal(args: argparse.Namespace, *, cfg, graph, hint_kind, apply_scope: bool):
|
|
76
|
+
"""Resolve the traversal root non-printingly.
|
|
77
|
+
|
|
78
|
+
Returns the resolved ``node`` (NodeRef). Raises :class:`PayloadError`
|
|
79
|
+
carrying the resolve ``Envelope`` + ``rc`` on failure (rc=2 on error,
|
|
80
|
+
0 on ambiguous/not_found — matches ``_resolve_traversal_node``).
|
|
81
|
+
"""
|
|
82
|
+
node, env = resolve_query(
|
|
83
|
+
args.query,
|
|
84
|
+
hint_kind=hint_kind,
|
|
85
|
+
java_kind=getattr(args, "java_kind", None),
|
|
86
|
+
role=getattr(args, "role", None),
|
|
87
|
+
fqn_contains=getattr(args, "fqn_contains", None),
|
|
88
|
+
cfg=cfg,
|
|
89
|
+
graph=graph,
|
|
90
|
+
microservice=(getattr(args, "service", None) or "") if apply_scope else "",
|
|
91
|
+
module=(getattr(args, "module", None) or "") if apply_scope else "",
|
|
92
|
+
)
|
|
93
|
+
if env.status != "ok":
|
|
94
|
+
raise PayloadError(env, 2 if env.status == "error" else 0)
|
|
95
|
+
return node
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def _kind_guard(node, *, args: argparse.Namespace, expected: str, kinds: tuple[str, ...], hint: str = "") -> None:
|
|
99
|
+
"""Non-printing kind guard (logic of jrag._require_kind, minus the print).
|
|
100
|
+
|
|
101
|
+
Raises :class:`PayloadError` with the same error message ``_require_kind``
|
|
102
|
+
builds when ``node.kind`` is not in ``kinds``.
|
|
103
|
+
"""
|
|
104
|
+
if node.kind not in kinds:
|
|
105
|
+
msg = f"{expected}; resolved kind is {node.kind!r}."
|
|
106
|
+
if hint:
|
|
107
|
+
msg = f"{msg} {hint}"
|
|
108
|
+
raise PayloadError(Envelope(status="error", message=msg), 2)
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
# ---------------------------------------------------------------------------
|
|
112
|
+
# callers_payload
|
|
113
|
+
# ---------------------------------------------------------------------------
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def callers_payload(args: argparse.Namespace, cfg, graph) -> dict[str, Any]:
|
|
117
|
+
"""Assemble the traversal payload for ``jrag callers``.
|
|
118
|
+
|
|
119
|
+
Route root -> ``find_route_callers`` (plus the external-entrypoint fold for
|
|
120
|
+
a zero-caller http_endpoint). Symbol root -> ``find_callers`` plus the
|
|
121
|
+
EXPOSES-inbound fold (DECLARES.EXPOSES routes surfaced as additional rows).
|
|
122
|
+
Both folds are transcribed verbatim from ``_cmd_callers``.
|
|
123
|
+
"""
|
|
124
|
+
limit = _clamped_limit(args)
|
|
125
|
+
# callers opts into resolve-time --service/--module narrowing (apply_scope).
|
|
126
|
+
node = _resolve_traversal(
|
|
127
|
+
args, cfg=cfg, graph=graph, hint_kind=args.kind, apply_scope=True
|
|
128
|
+
)
|
|
129
|
+
|
|
130
|
+
root_dict = _noderef_to_node_dict(node)
|
|
131
|
+
root_id = node.id
|
|
132
|
+
|
|
133
|
+
# ---- Route root -> find_route_callers (+ external-entrypoint fold) ----
|
|
134
|
+
if node.kind == "route":
|
|
135
|
+
route_callers = graph.find_route_callers(route_id=root_id)
|
|
136
|
+
warnings: list[str] = []
|
|
137
|
+
# No backend limit on find_route_callers; client-side slice for truncation.
|
|
138
|
+
truncated = len(route_callers) > limit
|
|
139
|
+
display = route_callers[:limit]
|
|
140
|
+
nodes: dict[str, dict] = {}
|
|
141
|
+
edges: list[dict] = []
|
|
142
|
+
for rc in display:
|
|
143
|
+
caller_id = rc.caller_node_id
|
|
144
|
+
if rc.caller_node_kind == "client":
|
|
145
|
+
edge_type = "HTTP_CALLS"
|
|
146
|
+
else:
|
|
147
|
+
edge_type = "ASYNC_CALLS"
|
|
148
|
+
node_row = {
|
|
149
|
+
"id": caller_id,
|
|
150
|
+
"kind": rc.caller_node_kind,
|
|
151
|
+
"fqn": rc.declaring_symbol_fqn or caller_id,
|
|
152
|
+
"microservice": rc.caller_microservice,
|
|
153
|
+
}
|
|
154
|
+
if rc.target_service:
|
|
155
|
+
node_row["target_service"] = rc.target_service
|
|
156
|
+
if rc.caller_node_kind == "client" and rc.raw_uri:
|
|
157
|
+
node_row["raw_uri"] = rc.raw_uri
|
|
158
|
+
elif rc.caller_node_kind != "client" and rc.topic:
|
|
159
|
+
node_row["topic"] = rc.topic
|
|
160
|
+
nodes[caller_id] = node_row
|
|
161
|
+
edges.append(
|
|
162
|
+
{"other_id": caller_id, "edge_type": edge_type, "confidence": rc.confidence}
|
|
163
|
+
)
|
|
164
|
+
# Include the root (Route) node so the zero-callers rendering surfaces
|
|
165
|
+
# the route path rather than a bare "0 callers" line.
|
|
166
|
+
nodes[root_id] = root_dict
|
|
167
|
+
# External-entrypoint detection (http_endpoint with an inbound EXPOSES
|
|
168
|
+
# edge from a controller Symbol genuinely has zero in-repo callers).
|
|
169
|
+
is_external_entrypoint = False
|
|
170
|
+
if not display:
|
|
171
|
+
kind_row = graph._rows( # noqa: SLF001 - same pattern as jrag_envelope._node_file_location
|
|
172
|
+
"MATCH (r:Route) WHERE r.id = $rid RETURN r.kind AS kind LIMIT 1",
|
|
173
|
+
{"rid": root_id},
|
|
174
|
+
)
|
|
175
|
+
route_kind = str(kind_row[0].get("kind") or "") if kind_row else ""
|
|
176
|
+
if route_kind == "http_endpoint" and graph.find_route_handlers(route_id=root_id):
|
|
177
|
+
is_external_entrypoint = True
|
|
178
|
+
return {
|
|
179
|
+
"root_id": root_id,
|
|
180
|
+
"nodes": nodes,
|
|
181
|
+
"edges": edges,
|
|
182
|
+
"noun": "callers",
|
|
183
|
+
"warnings": warnings,
|
|
184
|
+
"truncated": truncated,
|
|
185
|
+
"is_external_entrypoint": is_external_entrypoint,
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
# callers accepts Symbol OR Route; anything else is a usage error.
|
|
189
|
+
if node.kind != "symbol":
|
|
190
|
+
raise PayloadError(
|
|
191
|
+
Envelope(
|
|
192
|
+
status="error",
|
|
193
|
+
message=(
|
|
194
|
+
f"callers expects a Symbol or Route root; resolved node kind is "
|
|
195
|
+
f"{node.kind!r}. Use --kind to narrow resolve."
|
|
196
|
+
),
|
|
197
|
+
),
|
|
198
|
+
2,
|
|
199
|
+
)
|
|
200
|
+
|
|
201
|
+
# ---- Symbol root -> find_callers (+ EXPOSES-inbound fold) ----
|
|
202
|
+
depth = getattr(args, "depth", 1)
|
|
203
|
+
min_conf = getattr(args, "min_confidence", 0.0)
|
|
204
|
+
exclude_external = not getattr(args, "include_external", False)
|
|
205
|
+
call_edges = graph.find_callers(
|
|
206
|
+
node.fqn,
|
|
207
|
+
depth=depth,
|
|
208
|
+
limit=limit + 1,
|
|
209
|
+
min_confidence=min_conf,
|
|
210
|
+
exclude_external=exclude_external,
|
|
211
|
+
module=args.module,
|
|
212
|
+
microservice=args.service,
|
|
213
|
+
)
|
|
214
|
+
display, truncated = mark_truncated(call_edges, limit)
|
|
215
|
+
nodes = {}
|
|
216
|
+
edges = []
|
|
217
|
+
for ce in display:
|
|
218
|
+
nodes[ce.src.id] = _symbol_hit_to_dict(ce.src)
|
|
219
|
+
edges.append(
|
|
220
|
+
{"other_id": ce.src.id, "edge_type": "CALLS", "confidence": ce.confidence}
|
|
221
|
+
)
|
|
222
|
+
# EXPOSES-inbound fold: surface routes this type's methods EXPOSE as
|
|
223
|
+
# additional rows (controllers/listeners are entry points invoked via the
|
|
224
|
+
# framework dispatch, not via in-repo CALLS edges).
|
|
225
|
+
expose_rows = graph._rows( # noqa: SLF001 - one-shot aggregation, cf. _cmd_callees client path
|
|
226
|
+
"MATCH (t:Symbol {id: $tid})-[:DECLARES]->(m:Symbol)-[e:EXPOSES]->(r:Route) "
|
|
227
|
+
"RETURN r.id AS rid, r.method AS rmethod, r.path AS rpath, "
|
|
228
|
+
"r.path_template AS rpt, r.microservice AS rms, "
|
|
229
|
+
"m.fqn AS via_fqn, e.confidence AS conf",
|
|
230
|
+
{"tid": root_id},
|
|
231
|
+
)
|
|
232
|
+
for row in expose_rows:
|
|
233
|
+
rid = str(row.get("rid") or "")
|
|
234
|
+
if not rid or rid in nodes:
|
|
235
|
+
continue
|
|
236
|
+
rmethod = str(row.get("rmethod") or "")
|
|
237
|
+
rpath = str(row.get("rpt") or row.get("rpath") or "")
|
|
238
|
+
nodes[rid] = {
|
|
239
|
+
"id": rid,
|
|
240
|
+
"kind": "route",
|
|
241
|
+
"fqn": f"{rmethod} {rpath}".strip(),
|
|
242
|
+
"method": rmethod,
|
|
243
|
+
"path": rpath,
|
|
244
|
+
"microservice": str(row.get("rms") or ""),
|
|
245
|
+
}
|
|
246
|
+
edge_row: dict = {"other_id": rid, "edge_type": "EXPOSES"}
|
|
247
|
+
via_fqn = str(row.get("via_fqn") or "")
|
|
248
|
+
if via_fqn:
|
|
249
|
+
edge_row["from_fqn"] = via_fqn
|
|
250
|
+
edges.append(edge_row)
|
|
251
|
+
nodes[root_id] = root_dict
|
|
252
|
+
return {
|
|
253
|
+
"root_id": root_id,
|
|
254
|
+
"nodes": nodes,
|
|
255
|
+
"edges": edges,
|
|
256
|
+
"noun": "callers",
|
|
257
|
+
"warnings": [],
|
|
258
|
+
"truncated": truncated,
|
|
259
|
+
"is_external_entrypoint": False,
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
|
|
263
|
+
# ---------------------------------------------------------------------------
|
|
264
|
+
# callees_payload
|
|
265
|
+
# ---------------------------------------------------------------------------
|
|
266
|
+
|
|
267
|
+
|
|
268
|
+
def callees_payload(args: argparse.Namespace, cfg, graph) -> dict[str, Any]:
|
|
269
|
+
"""Assemble the traversal payload for ``jrag callees``.
|
|
270
|
+
|
|
271
|
+
Client/Producer root -> ``neighbors_v2`` (HTTP_CALLS / ASYNC_CALLS out).
|
|
272
|
+
CLIENT-role type Symbol -> the CLIENT-role HTTP_CALLS fold (declared Client
|
|
273
|
+
nodes -> HTTP_CALLS -> Route). Symbol root -> ``find_callees``. The
|
|
274
|
+
CLIENT-role fold is transcribed verbatim from ``_cmd_callees``.
|
|
275
|
+
"""
|
|
276
|
+
from java_codebase_rag.mcp import mcp_v2
|
|
277
|
+
from java_codebase_rag.jrag_envelope import Envelope as _Envelope # noqa: F811 (local alias for clarity)
|
|
278
|
+
|
|
279
|
+
limit = _clamped_limit(args)
|
|
280
|
+
node = _resolve_traversal(
|
|
281
|
+
args, cfg=cfg, graph=graph, hint_kind=args.kind, apply_scope=False
|
|
282
|
+
)
|
|
283
|
+
|
|
284
|
+
_kind_guard(
|
|
285
|
+
node,
|
|
286
|
+
args=args,
|
|
287
|
+
expected="callees expects a Symbol, Client, or Producer root",
|
|
288
|
+
kinds=("symbol", "client", "producer"),
|
|
289
|
+
hint="Use --kind to narrow resolve.",
|
|
290
|
+
)
|
|
291
|
+
|
|
292
|
+
# ---- Client/Producer root -> neighbors_v2 (HTTP_CALLS / ASYNC_CALLS out) ----
|
|
293
|
+
if node.kind in ("client", "producer"):
|
|
294
|
+
edge_types = ["HTTP_CALLS"] if node.kind == "client" else ["ASYNC_CALLS"]
|
|
295
|
+
out = mcp_v2.neighbors_v2(
|
|
296
|
+
[node.id], direction="out", edge_types=edge_types,
|
|
297
|
+
limit=limit + 1, graph=graph,
|
|
298
|
+
)
|
|
299
|
+
if not out.success:
|
|
300
|
+
raise PayloadError(
|
|
301
|
+
_Envelope(status="error", message=out.message or "neighbors_v2 failed"), 2
|
|
302
|
+
)
|
|
303
|
+
root_id = node.id
|
|
304
|
+
nodes: dict[str, dict] = {root_id: _noderef_to_node_dict(node)}
|
|
305
|
+
edges: list[dict] = []
|
|
306
|
+
for e in out.results:
|
|
307
|
+
nodes[e.other.id] = _noderef_to_node_dict(e.other)
|
|
308
|
+
edges.append(
|
|
309
|
+
{
|
|
310
|
+
"other_id": e.other.id,
|
|
311
|
+
"edge_type": e.edge_type,
|
|
312
|
+
"confidence": e.attrs.get("confidence"),
|
|
313
|
+
}
|
|
314
|
+
)
|
|
315
|
+
truncated = bool(out.has_more_results) or len(edges) > limit
|
|
316
|
+
if len(edges) > limit:
|
|
317
|
+
edges = edges[:limit]
|
|
318
|
+
# --include-external is accepted but does not apply on Client/Producer roots.
|
|
319
|
+
warnings: list[str] = []
|
|
320
|
+
if getattr(args, "include_external", False):
|
|
321
|
+
warnings.append(
|
|
322
|
+
"--include-external does not apply to Client/Producer roots "
|
|
323
|
+
"(HTTP_CALLS/ASYNC_CALLS reach :Route, which is always in-graph)"
|
|
324
|
+
)
|
|
325
|
+
edges = _dedupe_traversal_edges(edges)
|
|
326
|
+
truncated = truncated or len(edges) > limit
|
|
327
|
+
edges = edges[:limit]
|
|
328
|
+
return {
|
|
329
|
+
"root_id": root_id,
|
|
330
|
+
"nodes": nodes,
|
|
331
|
+
"edges": edges,
|
|
332
|
+
"noun": "callees",
|
|
333
|
+
"warnings": warnings,
|
|
334
|
+
"truncated": truncated,
|
|
335
|
+
"is_external_entrypoint": False,
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
# ---- CLIENT-role type Symbol -> HTTP_CALLS fold ----
|
|
339
|
+
if (node.role or "") == "CLIENT":
|
|
340
|
+
root_id = node.id
|
|
341
|
+
client_rows = graph._rows( # noqa: SLF001 - one-shot aggregation query
|
|
342
|
+
"MATCH (iface:Symbol {id: $sid})-[:DECLARES]->(m:Symbol)"
|
|
343
|
+
"-[:DECLARES_CLIENT]->(c:Client) "
|
|
344
|
+
"OPTIONAL MATCH (c)-[e:HTTP_CALLS]->(r:Route) "
|
|
345
|
+
"RETURN c.id AS cid, c.member_fqn AS cfqn, c.path AS cpath, "
|
|
346
|
+
"c.method AS cmethod, c.microservice AS cms, "
|
|
347
|
+
"r.id AS rid, r.method AS rmethod, r.path AS rpath, "
|
|
348
|
+
"r.path_template AS rpt, r.microservice AS rms, "
|
|
349
|
+
"e.confidence AS conf",
|
|
350
|
+
{"sid": root_id},
|
|
351
|
+
)
|
|
352
|
+
nodes = {root_id: _noderef_to_node_dict(node)}
|
|
353
|
+
edges = []
|
|
354
|
+
for row in client_rows:
|
|
355
|
+
rid = str(row.get("rid") or "")
|
|
356
|
+
if rid:
|
|
357
|
+
target_id = rid
|
|
358
|
+
rmethod = str(row.get("rmethod") or "")
|
|
359
|
+
rpath = str(row.get("rpt") or row.get("rpath") or "")
|
|
360
|
+
nodes[target_id] = {
|
|
361
|
+
"id": target_id,
|
|
362
|
+
"kind": "route",
|
|
363
|
+
"fqn": f"{rmethod} {rpath}".strip(),
|
|
364
|
+
"microservice": str(row.get("rms") or ""),
|
|
365
|
+
}
|
|
366
|
+
edge_type = "HTTP_CALLS"
|
|
367
|
+
else:
|
|
368
|
+
# Client with no resolved HTTP_CALLS edge: surface the client
|
|
369
|
+
# node + its declared path so the outbound intent is visible.
|
|
370
|
+
target_id = str(row.get("cid") or "")
|
|
371
|
+
if not target_id:
|
|
372
|
+
continue
|
|
373
|
+
cmethod = str(row.get("cmethod") or "")
|
|
374
|
+
cpath = str(row.get("cpath") or "")
|
|
375
|
+
nodes[target_id] = {
|
|
376
|
+
"id": target_id,
|
|
377
|
+
"kind": "client",
|
|
378
|
+
"fqn": f"{cmethod} {cpath}".strip() or str(row.get("cfqn") or ""),
|
|
379
|
+
"microservice": str(row.get("cms") or ""),
|
|
380
|
+
}
|
|
381
|
+
edge_type = "HTTP_CALLS"
|
|
382
|
+
edges.append({
|
|
383
|
+
"other_id": target_id,
|
|
384
|
+
"edge_type": edge_type,
|
|
385
|
+
"confidence": float(row.get("conf") or 0.0) or None,
|
|
386
|
+
})
|
|
387
|
+
edges = _dedupe_traversal_edges(edges)
|
|
388
|
+
truncated = len(edges) > limit
|
|
389
|
+
edges = edges[:limit]
|
|
390
|
+
return {
|
|
391
|
+
"root_id": root_id,
|
|
392
|
+
"nodes": nodes,
|
|
393
|
+
"edges": edges,
|
|
394
|
+
"noun": "callees",
|
|
395
|
+
"warnings": [],
|
|
396
|
+
"truncated": truncated,
|
|
397
|
+
"is_external_entrypoint": False,
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
# ---- Symbol root -> find_callees ----
|
|
401
|
+
depth = getattr(args, "depth", 1)
|
|
402
|
+
min_conf = getattr(args, "min_confidence", 0.0)
|
|
403
|
+
exclude_external = not getattr(args, "include_external", False)
|
|
404
|
+
call_edges = graph.find_callees(
|
|
405
|
+
node.fqn,
|
|
406
|
+
depth=depth,
|
|
407
|
+
limit=limit + 1,
|
|
408
|
+
min_confidence=min_conf,
|
|
409
|
+
exclude_external=exclude_external,
|
|
410
|
+
module=args.module,
|
|
411
|
+
microservice=args.service,
|
|
412
|
+
)
|
|
413
|
+
display, truncated = mark_truncated(call_edges, limit)
|
|
414
|
+
root_id = node.id
|
|
415
|
+
nodes = {root_id: _noderef_to_node_dict(node)}
|
|
416
|
+
edges = []
|
|
417
|
+
for ce in display:
|
|
418
|
+
nodes[ce.dst.id] = _symbol_hit_to_dict(ce.dst)
|
|
419
|
+
edges.append(
|
|
420
|
+
{"other_id": ce.dst.id, "edge_type": "CALLS", "confidence": ce.confidence}
|
|
421
|
+
)
|
|
422
|
+
edges = _dedupe_traversal_edges(edges)
|
|
423
|
+
truncated = truncated or len(edges) > limit
|
|
424
|
+
edges = edges[:limit]
|
|
425
|
+
return {
|
|
426
|
+
"root_id": root_id,
|
|
427
|
+
"nodes": nodes,
|
|
428
|
+
"edges": edges,
|
|
429
|
+
"noun": "callees",
|
|
430
|
+
"warnings": [],
|
|
431
|
+
"truncated": truncated,
|
|
432
|
+
"is_external_entrypoint": False,
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
|
|
436
|
+
# ---------------------------------------------------------------------------
|
|
437
|
+
# flow_payload
|
|
438
|
+
# ---------------------------------------------------------------------------
|
|
439
|
+
|
|
440
|
+
|
|
441
|
+
def flow_payload(args: argparse.Namespace, cfg, graph) -> dict[str, Any]:
|
|
442
|
+
"""Assemble the traversal payload for ``jrag flow``.
|
|
443
|
+
|
|
444
|
+
Route root -> ``trace_request_flow`` plus the inbound/outbound merge +
|
|
445
|
+
client-side truncation, transcribed verbatim from ``_cmd_flow``.
|
|
446
|
+
"""
|
|
447
|
+
node = _resolve_traversal(
|
|
448
|
+
args, cfg=cfg, graph=graph, hint_kind="route", apply_scope=False
|
|
449
|
+
)
|
|
450
|
+
|
|
451
|
+
_kind_guard(
|
|
452
|
+
node,
|
|
453
|
+
args=args,
|
|
454
|
+
expected="flow requires a Route root",
|
|
455
|
+
kinds=("route",),
|
|
456
|
+
hint="Pass a route path (e.g. /chat/assign).",
|
|
457
|
+
)
|
|
458
|
+
|
|
459
|
+
warnings = _warn_unapplied_scope(
|
|
460
|
+
args,
|
|
461
|
+
reason="trace_request_flow carries no microservice predicate; intra-codebase is an index-time data property",
|
|
462
|
+
)
|
|
463
|
+
|
|
464
|
+
limit = _clamped_limit(args)
|
|
465
|
+
max_hops = max(1, min(8, getattr(args, "depth", 5)))
|
|
466
|
+
flow_data = graph.trace_request_flow(entry_route_id=node.id, max_hops=max_hops)
|
|
467
|
+
|
|
468
|
+
root_id = node.id
|
|
469
|
+
nodes: dict[str, dict] = {root_id: _noderef_to_node_dict(node)}
|
|
470
|
+
edges: list[dict] = []
|
|
471
|
+
# Inbound: cross-service HTTP/async callers (Client/Producer two-hop).
|
|
472
|
+
for row in flow_data.get("inbound", []):
|
|
473
|
+
caller_id = str(row.get("caller_node_id") or "")
|
|
474
|
+
if not caller_id:
|
|
475
|
+
continue
|
|
476
|
+
kind = str(row.get("caller_node_kind") or "")
|
|
477
|
+
nodes[caller_id] = {
|
|
478
|
+
"id": caller_id,
|
|
479
|
+
"kind": kind,
|
|
480
|
+
"fqn": str(row.get("declaring_symbol_fqn") or ""),
|
|
481
|
+
"microservice": str(row.get("microservice") or ""),
|
|
482
|
+
}
|
|
483
|
+
edges.append(
|
|
484
|
+
{
|
|
485
|
+
"other_id": caller_id,
|
|
486
|
+
"edge_type": "HTTP_CALLS" if kind == "client" else "ASYNC_CALLS",
|
|
487
|
+
"confidence": float(row.get("confidence") or 0.0),
|
|
488
|
+
}
|
|
489
|
+
)
|
|
490
|
+
# Outbound: CALLS hops from the route handler (intra-service by construction).
|
|
491
|
+
for row in flow_data.get("outbound", []):
|
|
492
|
+
next_id = str(row.get("next_symbol_id") or "")
|
|
493
|
+
if not next_id:
|
|
494
|
+
continue
|
|
495
|
+
nodes[next_id] = {
|
|
496
|
+
"id": next_id,
|
|
497
|
+
"kind": "symbol",
|
|
498
|
+
"fqn": str(row.get("next_fqn") or ""),
|
|
499
|
+
"microservice": str(row.get("next_microservice") or ""),
|
|
500
|
+
}
|
|
501
|
+
edges.append({"other_id": next_id, "edge_type": "CALLS"})
|
|
502
|
+
|
|
503
|
+
# Client-side slice for truncation (trace_request_flow has no limit param).
|
|
504
|
+
truncated = len(edges) > limit
|
|
505
|
+
if truncated:
|
|
506
|
+
edges = edges[:limit]
|
|
507
|
+
return {
|
|
508
|
+
"root_id": root_id,
|
|
509
|
+
"nodes": nodes,
|
|
510
|
+
"edges": edges,
|
|
511
|
+
"noun": "flow",
|
|
512
|
+
"warnings": warnings,
|
|
513
|
+
"truncated": truncated,
|
|
514
|
+
"is_external_entrypoint": False,
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
|
|
518
|
+
# ---------------------------------------------------------------------------
|
|
519
|
+
# search_payload
|
|
520
|
+
# ---------------------------------------------------------------------------
|
|
521
|
+
|
|
522
|
+
|
|
523
|
+
def search_payload(args: argparse.Namespace, cfg, graph):
|
|
524
|
+
"""Return the ``SearchOutput`` for ``jrag search``.
|
|
525
|
+
|
|
526
|
+
Builds the ``NodeFilter`` from args (the same filter set the handler builds)
|
|
527
|
+
and calls ``mcp_v2.search_v2`` with ``limit+1`` for +1-fetch truncation.
|
|
528
|
+
Pre-validation (``--fuzzy``, ``limit==0`` short-circuit, ``--framework``
|
|
529
|
+
enum check) and post-processing (framework post-filter, ``--explain``,
|
|
530
|
+
``--min-score``, truncation, zero-result guidance) stay in the handler —
|
|
531
|
+
this is the clean ``search_v2`` core.
|
|
532
|
+
"""
|
|
533
|
+
from java_codebase_rag.mcp import mcp_v2
|
|
534
|
+
|
|
535
|
+
# Build NodeFilter from flags (same set as the handler / `find` filter mode).
|
|
536
|
+
# NOTE: --framework is intentionally NOT placed in the NodeFilter (the graph
|
|
537
|
+
# stores framework only on Route nodes; the handler applies it as a
|
|
538
|
+
# client-side POST-filter after the hits come back).
|
|
539
|
+
filter_dict: dict = {}
|
|
540
|
+
if args.service:
|
|
541
|
+
filter_dict["microservice"] = args.service
|
|
542
|
+
if args.module:
|
|
543
|
+
filter_dict["module"] = args.module
|
|
544
|
+
if args.role:
|
|
545
|
+
filter_dict["role"] = normalize_enum(args.role, kind="role")
|
|
546
|
+
if args.exclude_role:
|
|
547
|
+
filter_dict["exclude_roles"] = [normalize_enum(args.exclude_role, kind="role")]
|
|
548
|
+
if args.annotation:
|
|
549
|
+
filter_dict["annotation"] = args.annotation
|
|
550
|
+
if args.capability:
|
|
551
|
+
filter_dict["capability"] = args.capability
|
|
552
|
+
if args.fqn_contains:
|
|
553
|
+
filter_dict["fqn_contains"] = args.fqn_contains
|
|
554
|
+
if args.java_kind:
|
|
555
|
+
filter_dict["symbol_kind"] = normalize_enum(args.java_kind, kind="java_kind")
|
|
556
|
+
|
|
557
|
+
node_filter, err_env = _build_node_filter_or_error(filter_dict)
|
|
558
|
+
if err_env is not None:
|
|
559
|
+
raise PayloadError(err_env, 2)
|
|
560
|
+
|
|
561
|
+
limit = min(args.limit if args.limit is not None else 20, 499)
|
|
562
|
+
return mcp_v2.search_v2(
|
|
563
|
+
args.query,
|
|
564
|
+
table=args.table,
|
|
565
|
+
hybrid=args.hybrid,
|
|
566
|
+
limit=limit + 1, # +1 for truncated detection
|
|
567
|
+
offset=args.offset,
|
|
568
|
+
path_contains=args.path_contains,
|
|
569
|
+
filter=node_filter,
|
|
570
|
+
explain=args.explain,
|
|
571
|
+
graph=graph,
|
|
572
|
+
dedup=not getattr(args, "chunks", False),
|
|
573
|
+
)
|
|
574
|
+
|
|
575
|
+
|
|
576
|
+
# ---------------------------------------------------------------------------
|
|
577
|
+
# find_payload
|
|
578
|
+
# ---------------------------------------------------------------------------
|
|
579
|
+
|
|
580
|
+
|
|
581
|
+
def find_payload(args: argparse.Namespace, cfg, graph) -> dict[str, Any]:
|
|
582
|
+
"""Return the find payload, selecting query vs filter mode exactly as
|
|
583
|
+
``_cmd_find`` / ``_cmd_find_*`` do today.
|
|
584
|
+
|
|
585
|
+
* Query mode (positional ``<query>``): ``graph.find_by_name_or_fqn`` plus
|
|
586
|
+
the existing client-side post-filters (role/annotation/capability). Returns
|
|
587
|
+
``{"mode": "query", "rows", "raw_truncated", "post_filter_active", "limit",
|
|
588
|
+
"query", "kinds", "matched_mode", "identifier_matched"}``. With ``--fuzzy``,
|
|
589
|
+
an empty exact result widens to prefix then substring (issue #375);
|
|
590
|
+
``matched_mode`` is the tier that hit (``exact``/``prefix``/``contains``).
|
|
591
|
+
* Filter mode: ``mcp_v2.find_v2``. Returns
|
|
592
|
+
``{"mode": "filter", "kind", "out" (FindOutput), "limit"}``.
|
|
593
|
+
|
|
594
|
+
Kind-contradiction / query-mode-kind errors raise :class:`PayloadError`.
|
|
595
|
+
Rendering (nodes/warnings/empty-result hint/offset) stays in the handler.
|
|
596
|
+
"""
|
|
597
|
+
from java_codebase_rag.mcp import mcp_v2
|
|
598
|
+
|
|
599
|
+
inferred = _infer_kind(args)
|
|
600
|
+
is_contradiction, error_msg = _check_kind_contradiction(args, inferred)
|
|
601
|
+
if is_contradiction:
|
|
602
|
+
raise PayloadError(
|
|
603
|
+
Envelope(status="error", message=error_msg or "kind contradiction"), 2
|
|
604
|
+
)
|
|
605
|
+
|
|
606
|
+
# Cap at 499 so limit+1 <= 500 (backend clamp); default 20.
|
|
607
|
+
raw_limit = args.limit if args.limit is not None else 20
|
|
608
|
+
limit = min(raw_limit, 499)
|
|
609
|
+
|
|
610
|
+
# ---- Query mode: positional <query> present ----
|
|
611
|
+
if args.query:
|
|
612
|
+
effective_kind = inferred or "symbol"
|
|
613
|
+
if effective_kind != "symbol":
|
|
614
|
+
raise PayloadError(
|
|
615
|
+
Envelope(
|
|
616
|
+
status="error",
|
|
617
|
+
message=(
|
|
618
|
+
f"query mode (positional <query>) only searches Symbols, but kind "
|
|
619
|
+
f"'{effective_kind}' was {'inferred from domain flags' if args.kind is None else 'set via --kind'}. "
|
|
620
|
+
"Drop the positional <query> and use filter mode (the domain flags) "
|
|
621
|
+
"for route/client/producer searches."
|
|
622
|
+
),
|
|
623
|
+
),
|
|
624
|
+
2,
|
|
625
|
+
)
|
|
626
|
+
query = args.query
|
|
627
|
+
# find_by_name_or_fqn is always Symbol; the only valid kinds filter is
|
|
628
|
+
# the symbol sub-kind derived from --java-kind.
|
|
629
|
+
if args.java_kind:
|
|
630
|
+
java_kind_norm = normalize_enum(args.java_kind, kind="java_kind")
|
|
631
|
+
kinds = [java_kind_norm.lower()]
|
|
632
|
+
else:
|
|
633
|
+
kinds = None
|
|
634
|
+
|
|
635
|
+
rows = graph.find_by_name_or_fqn(
|
|
636
|
+
query,
|
|
637
|
+
kinds=kinds,
|
|
638
|
+
module=args.module,
|
|
639
|
+
microservice=args.service,
|
|
640
|
+
limit=limit + 1, # +1 for truncated detection
|
|
641
|
+
)
|
|
642
|
+
# --fuzzy: widen an empty exact result to prefix (STARTS WITH) then
|
|
643
|
+
# substring (CONTAINS) on name/FQN (issue #375). Fuzzy modes exclude
|
|
644
|
+
# file/package Symbol nodes (their fqn is a filesystem path) — enforced
|
|
645
|
+
# in find_by_name_or_fqn's ``mode`` handling.
|
|
646
|
+
matched_mode = "exact"
|
|
647
|
+
if not rows and getattr(args, "fuzzy", False) and query:
|
|
648
|
+
for fb_mode in ("prefix", "contains"):
|
|
649
|
+
rows = graph.find_by_name_or_fqn(
|
|
650
|
+
query, kinds=kinds, module=args.module, microservice=args.service,
|
|
651
|
+
limit=limit + 1, mode=fb_mode,
|
|
652
|
+
)
|
|
653
|
+
if rows:
|
|
654
|
+
matched_mode = fb_mode
|
|
655
|
+
break
|
|
656
|
+
# Did any tier (exact or fallback) return rows BEFORE post-filters? Used
|
|
657
|
+
# to distinguish "identifier genuinely matched nothing" from "matched but
|
|
658
|
+
# --role/--annotation/--capability removed all hits" in the empty-result hint.
|
|
659
|
+
identifier_matched = bool(rows)
|
|
660
|
+
# Truncation is decided by the RAW name/FQN fetch (limit+1), BEFORE
|
|
661
|
+
# post-filters reduce the set.
|
|
662
|
+
raw_truncated = len(rows) > limit
|
|
663
|
+
|
|
664
|
+
# Post-filter by role/annotation/capability (SymbolHit carries these).
|
|
665
|
+
post_filter_active = False
|
|
666
|
+
if args.role:
|
|
667
|
+
post_filter_active = True
|
|
668
|
+
role_norm = normalize_enum(args.role, kind="role")
|
|
669
|
+
rows = [r for r in rows if (r.role or "").upper().replace("-", "_") == role_norm.upper()]
|
|
670
|
+
if args.exclude_role:
|
|
671
|
+
post_filter_active = True
|
|
672
|
+
exclude_role_norm = normalize_enum(args.exclude_role, kind="role")
|
|
673
|
+
rows = [r for r in rows if (r.role or "").upper().replace("-", "_") != exclude_role_norm.upper()]
|
|
674
|
+
if args.annotation:
|
|
675
|
+
post_filter_active = True
|
|
676
|
+
rows = [r for r in rows if args.annotation in (r.annotations or [])]
|
|
677
|
+
if args.capability:
|
|
678
|
+
post_filter_active = True
|
|
679
|
+
rows = [r for r in rows if args.capability in (r.capabilities or [])]
|
|
680
|
+
|
|
681
|
+
return {
|
|
682
|
+
"mode": "query",
|
|
683
|
+
"rows": rows,
|
|
684
|
+
"raw_truncated": raw_truncated,
|
|
685
|
+
"post_filter_active": post_filter_active,
|
|
686
|
+
"limit": limit,
|
|
687
|
+
"query": query,
|
|
688
|
+
"kinds": kinds,
|
|
689
|
+
"matched_mode": matched_mode,
|
|
690
|
+
"identifier_matched": identifier_matched,
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
# ---- Filter mode: build NodeFilter and call find_v2 ----
|
|
694
|
+
kind = inferred or "symbol"
|
|
695
|
+
filter_dict: dict = {}
|
|
696
|
+
if args.service:
|
|
697
|
+
filter_dict["microservice"] = args.service
|
|
698
|
+
if args.module:
|
|
699
|
+
filter_dict["module"] = args.module
|
|
700
|
+
if args.role:
|
|
701
|
+
filter_dict["role"] = normalize_enum(args.role, kind="role")
|
|
702
|
+
if args.exclude_role:
|
|
703
|
+
filter_dict["exclude_roles"] = [normalize_enum(args.exclude_role, kind="role")]
|
|
704
|
+
if args.annotation:
|
|
705
|
+
filter_dict["annotation"] = args.annotation
|
|
706
|
+
if args.capability:
|
|
707
|
+
filter_dict["capability"] = args.capability
|
|
708
|
+
if args.fqn_contains:
|
|
709
|
+
filter_dict["fqn_contains"] = args.fqn_contains
|
|
710
|
+
if args.java_kind:
|
|
711
|
+
filter_dict["symbol_kind"] = normalize_enum(args.java_kind, kind="java_kind")
|
|
712
|
+
if args.framework:
|
|
713
|
+
filter_dict["framework"] = normalize_enum(args.framework, kind="framework")
|
|
714
|
+
if args.source_layer:
|
|
715
|
+
filter_dict["source_layer"] = normalize_enum(args.source_layer, kind="source_layer")
|
|
716
|
+
if args.http_method:
|
|
717
|
+
filter_dict["http_method"] = args.http_method.upper()
|
|
718
|
+
if args.path_contains:
|
|
719
|
+
filter_dict["path_contains"] = args.path_contains
|
|
720
|
+
if args.client_kind:
|
|
721
|
+
filter_dict["client_kind"] = normalize_enum(args.client_kind, kind="client_kind")
|
|
722
|
+
if args.calls_service:
|
|
723
|
+
filter_dict["target_service"] = args.calls_service
|
|
724
|
+
if args.calls_path_contains:
|
|
725
|
+
filter_dict["target_path_contains"] = args.calls_path_contains
|
|
726
|
+
if args.producer_kind:
|
|
727
|
+
filter_dict["producer_kind"] = normalize_enum(args.producer_kind, kind="producer_kind")
|
|
728
|
+
if args.topic_contains:
|
|
729
|
+
filter_dict["topic_contains"] = args.topic_contains
|
|
730
|
+
|
|
731
|
+
node_filter, err_env = _build_node_filter_or_error(filter_dict)
|
|
732
|
+
if err_env is not None:
|
|
733
|
+
raise PayloadError(err_env, 2)
|
|
734
|
+
|
|
735
|
+
out = mcp_v2.find_v2(
|
|
736
|
+
kind=kind,
|
|
737
|
+
filter=node_filter,
|
|
738
|
+
limit=limit + 1, # +1 for has_more_results detection
|
|
739
|
+
offset=args.offset,
|
|
740
|
+
graph=graph,
|
|
741
|
+
)
|
|
742
|
+
return {"mode": "filter", "kind": kind, "out": out, "limit": limit}
|
|
743
|
+
|
|
744
|
+
|
|
745
|
+
# ---------------------------------------------------------------------------
|
|
746
|
+
# inspect_payload
|
|
747
|
+
# ---------------------------------------------------------------------------
|
|
748
|
+
|
|
749
|
+
|
|
750
|
+
def inspect_payload(args: argparse.Namespace, cfg, graph) -> dict[str, Any]:
|
|
751
|
+
"""Return the inspect payload: resolve + ``describe_v2``.
|
|
752
|
+
|
|
753
|
+
inspect's renderer needs ``file_location`` from the resolve ``Envelope``
|
|
754
|
+
(not present on ``DescribeOutput``), so the payload carries the resolved
|
|
755
|
+
node id/fqn and file_location alongside the ``DescribeOutput``. The
|
|
756
|
+
NodeRecord -> envelope-node flatten + render stays in the handler.
|
|
757
|
+
"""
|
|
758
|
+
from java_codebase_rag.mcp import mcp_v2
|
|
759
|
+
|
|
760
|
+
# inspect forwards --service/--module into resolve (disambiguates by scope).
|
|
761
|
+
node, env = resolve_query(
|
|
762
|
+
args.query,
|
|
763
|
+
hint_kind=args.kind,
|
|
764
|
+
java_kind=args.java_kind,
|
|
765
|
+
role=args.role,
|
|
766
|
+
fqn_contains=args.fqn_contains,
|
|
767
|
+
cfg=cfg,
|
|
768
|
+
graph=graph,
|
|
769
|
+
microservice=getattr(args, "service", None) or "",
|
|
770
|
+
module=getattr(args, "module", None) or "",
|
|
771
|
+
)
|
|
772
|
+
if env.status != "ok":
|
|
773
|
+
raise PayloadError(env, 2 if env.status == "error" else 0)
|
|
774
|
+
|
|
775
|
+
desc_out = mcp_v2.describe_v2(id=node.id, graph=graph)
|
|
776
|
+
return {
|
|
777
|
+
"describe": desc_out,
|
|
778
|
+
"node_id": node.id,
|
|
779
|
+
"node_fqn": node.fqn,
|
|
780
|
+
"file_location": env.file_location,
|
|
781
|
+
}
|