java-codebase-rag 0.12.0__py3-none-any.whl → 0.12.2__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.
Files changed (77) hide show
  1. java_codebase_rag-0.12.2.dist-info/METADATA +35 -0
  2. java_codebase_rag-0.12.2.dist-info/RECORD +4 -0
  3. {java_codebase_rag-0.12.0.dist-info → java_codebase_rag-0.12.2.dist-info}/WHEEL +1 -1
  4. java_codebase_rag/_deprecation.py +0 -103
  5. java_codebase_rag/_fdlimit.py +0 -56
  6. java_codebase_rag/_stdio.py +0 -32
  7. java_codebase_rag/_version.py +0 -35
  8. java_codebase_rag/absence/__init__.py +0 -0
  9. java_codebase_rag/absence/absence_diagnosis.py +0 -700
  10. java_codebase_rag/absence/absence_types.py +0 -124
  11. java_codebase_rag/absence/absence_vocab.py +0 -460
  12. java_codebase_rag/analysis/__init__.py +0 -0
  13. java_codebase_rag/analysis/pr_analysis.py +0 -563
  14. java_codebase_rag/analysis/resolve_service.py +0 -740
  15. java_codebase_rag/ast/__init__.py +0 -0
  16. java_codebase_rag/ast/ast_java.py +0 -2847
  17. java_codebase_rag/ast/ast_kotlin.py +0 -1794
  18. java_codebase_rag/ast/brownfield_events.py +0 -58
  19. java_codebase_rag/ast/chunk_heuristics.py +0 -83
  20. java_codebase_rag/ast/language.py +0 -117
  21. java_codebase_rag/cli.py +0 -1215
  22. java_codebase_rag/cli_dispatch.py +0 -251
  23. java_codebase_rag/cli_format.py +0 -85
  24. java_codebase_rag/cli_progress.py +0 -94
  25. java_codebase_rag/config.py +0 -833
  26. java_codebase_rag/eval/__init__.py +0 -1
  27. java_codebase_rag/eval/ground_truth.py +0 -100
  28. java_codebase_rag/eval/metrics.py +0 -107
  29. java_codebase_rag/eval/runner.py +0 -556
  30. java_codebase_rag/graph/__init__.py +0 -0
  31. java_codebase_rag/graph/build_ast_graph.py +0 -4593
  32. java_codebase_rag/graph/graph_enrich.py +0 -1940
  33. java_codebase_rag/graph/graph_types.py +0 -224
  34. java_codebase_rag/graph/java_ontology.py +0 -465
  35. java_codebase_rag/graph/ladybug_queries.py +0 -2213
  36. java_codebase_rag/graph/path_filtering.py +0 -509
  37. java_codebase_rag/index/__init__.py +0 -0
  38. java_codebase_rag/index/java_index_flow_lancedb.py +0 -879
  39. java_codebase_rag/index/java_index_v1_common.py +0 -33
  40. java_codebase_rag/install_data/__init__.py +0 -0
  41. java_codebase_rag/install_data/agents/explorer-rag-cli.md +0 -110
  42. java_codebase_rag/install_data/agents/explorer-rag-enhanced.md +0 -152
  43. java_codebase_rag/install_data/skills/explore-codebase/SKILL.md +0 -165
  44. java_codebase_rag/install_data/skills/explore-codebase-cli/SKILL.md +0 -107
  45. java_codebase_rag/installer.py +0 -2188
  46. java_codebase_rag/jrag.py +0 -4545
  47. java_codebase_rag/jrag_envelope.py +0 -1107
  48. java_codebase_rag/jrag_hints.py +0 -204
  49. java_codebase_rag/jrag_render.py +0 -926
  50. java_codebase_rag/lance_optimize.py +0 -264
  51. java_codebase_rag/mcp/__init__.py +0 -0
  52. java_codebase_rag/mcp/mcp_hints.py +0 -932
  53. java_codebase_rag/mcp/mcp_v2.py +0 -1916
  54. java_codebase_rag/mcp/server.py +0 -886
  55. java_codebase_rag/pipeline.py +0 -531
  56. java_codebase_rag/progress.py +0 -570
  57. java_codebase_rag/read_payloads.py +0 -781
  58. java_codebase_rag/search/__init__.py +0 -0
  59. java_codebase_rag/search/index_common.py +0 -10
  60. java_codebase_rag/search/search_lancedb.py +0 -1296
  61. java_codebase_rag/search/search_lexical.py +0 -449
  62. java_codebase_rag/search/search_scoring.py +0 -537
  63. java_codebase_rag/watch/__init__.py +0 -0
  64. java_codebase_rag/watch/client.py +0 -230
  65. java_codebase_rag/watch/daemon.py +0 -396
  66. java_codebase_rag/watch/lock.py +0 -201
  67. java_codebase_rag/watch/paths.py +0 -76
  68. java_codebase_rag/watch/protocol.py +0 -122
  69. java_codebase_rag/watch/server.py +0 -273
  70. java_codebase_rag/watch/warm.py +0 -105
  71. java_codebase_rag/watch/watcher.py +0 -394
  72. java_codebase_rag-0.12.0.dist-info/METADATA +0 -340
  73. java_codebase_rag-0.12.0.dist-info/RECORD +0 -75
  74. java_codebase_rag-0.12.0.dist-info/entry_points.txt +0 -5
  75. java_codebase_rag-0.12.0.dist-info/licenses/LICENSE +0 -21
  76. java_codebase_rag-0.12.0.dist-info/top_level.txt +0 -1
  77. /java_codebase_rag/__init__.py → /java_codebase_rag-0.12.2.dist-info/top_level.txt +0 -0
@@ -1,740 +0,0 @@
1
- """Resolve service for mapping identifiers to graph nodes.
2
-
3
- Transport-agnostic resolve pipeline extracted from mcp_v2.py for reuse
4
- by the CLI layer. Provides resolve_v2(identifier, hint_kind, graph) -> ResolveOutput.
5
- """
6
-
7
- from __future__ import annotations
8
-
9
- from typing import Any, Literal
10
-
11
- from pydantic import BaseModel, ConfigDict, Field
12
-
13
- from java_codebase_rag.absence.absence_types import AbsenceDiagnosis
14
- from java_codebase_rag.absence.absence_diagnosis import diagnose
15
- from java_codebase_rag.absence.absence_vocab import get_vocabulary_index
16
- from java_codebase_rag.graph.graph_types import (
17
- NodeRef,
18
- StructuredHint,
19
- _hints_or_skip,
20
- _node_ref_from_row,
21
- _to_structured_hints,
22
- set_hints_enabled,
23
- )
24
- from java_codebase_rag.graph.java_ontology import ResolveReason
25
- from java_codebase_rag.graph.ladybug_queries import LadybugGraph
26
- from java_codebase_rag.mcp.mcp_hints import MCP_HINTS_STRUCTURED_FIELD_DESCRIPTION
27
-
28
- __all__ = [
29
- "resolve_v2",
30
- "ResolveOutput",
31
- "ResolveCandidate",
32
- "ResolveStatus",
33
- "set_hints_enabled",
34
- "set_absence_config",
35
- ]
36
-
37
-
38
- ResolveStatus = Literal["one", "many", "none"]
39
-
40
- _RESOLVE_CANDIDATE_CAP = 10
41
-
42
- # Module-level holder for absence diagnosis config (set by server.py)
43
- _absence_config: Any = None
44
-
45
-
46
- def set_absence_config(cfg: Any) -> None:
47
- """Set the global absence diagnosis config for resolve service.
48
-
49
- Mirrors set_hints_enabled: called from server.py to make cfg reachable
50
- in resolve functions without threading it through every signature.
51
- """
52
- global _absence_config
53
- _absence_config = cfg
54
-
55
-
56
- def _get_absence_config() -> Any:
57
- """Get the absence diagnosis config, with safe fallback.
58
-
59
- Returns the module-level config if set; otherwise falls back to a
60
- default config (for direct tool calls in tests without server init).
61
- """
62
- if _absence_config is not None:
63
- return _absence_config
64
-
65
- # Fallback: resolve from cwd for direct test calls
66
- from java_codebase_rag.config import resolve_operator_config
67
- from pathlib import Path
68
-
69
- return resolve_operator_config(source_root=Path.cwd())
70
-
71
- _RESOLVE_REASON_PRIORITY: dict[ResolveReason, int] = {
72
- "exact_id": 0,
73
- "exact_fqn": 1,
74
- "route_method_path": 1,
75
- "client_target_path": 1,
76
- "producer_topic_prefix": 1,
77
- "fqn_suffix": 2,
78
- "route_template": 2,
79
- "route_topic": 2,
80
- "client_fqn": 2,
81
- "short_name": 3,
82
- "client_target": 3,
83
- "client_name": 3,
84
- "producer_topic": 3,
85
- "route_topic_prefix": 3,
86
- }
87
-
88
- _SYMBOL_RESOLVE_RETURN = (
89
- "s.id AS id, s.fqn AS fqn, s.microservice AS microservice, "
90
- "s.module AS module, s.role AS role, s.kind AS symbol_kind"
91
- )
92
-
93
- _ROUTE_RESOLVE_RETURN = (
94
- "r.id AS id, r.kind AS kind, r.framework AS framework, r.method AS method, "
95
- "r.path AS path, r.path_template AS path_template, r.path_regex AS path_regex, "
96
- "r.topic AS topic, r.broker AS broker, r.feign_name AS feign_name, r.feign_url AS feign_url, "
97
- "r.microservice AS microservice, r.module AS module, r.filename AS filename, "
98
- "r.start_line AS start_line, r.end_line AS end_line, r.resolved AS resolved"
99
- )
100
-
101
- _CLIENT_RESOLVE_RETURN = (
102
- "c.id AS id, c.client_kind AS client_kind, c.target_service AS target_service, "
103
- "c.method AS method, c.path AS path, c.path_template AS path_template, "
104
- "c.path_regex AS path_regex, c.member_fqn AS member_fqn, c.member_id AS member_id, "
105
- "c.microservice AS microservice, c.module AS module, c.filename AS filename, "
106
- "c.start_line AS start_line, c.end_line AS end_line, c.resolved AS resolved, "
107
- "c.source_layer AS source_layer"
108
- )
109
-
110
- _PRODUCER_RESOLVE_RETURN = (
111
- "p.id AS id, p.producer_kind AS producer_kind, p.topic AS topic, p.broker AS broker, "
112
- "p.direction AS direction, p.member_fqn AS member_fqn, p.member_id AS member_id, "
113
- "p.microservice AS microservice, p.module AS module, p.filename AS filename, "
114
- "p.start_line AS start_line, p.end_line AS end_line, p.resolved AS resolved, "
115
- "p.source_layer AS source_layer"
116
- )
117
-
118
- _RESOLVE_PRE_DEDUP_LIMIT = 50
119
-
120
-
121
- def _scope_clause(
122
- alias: str,
123
- microservice: str = "",
124
- module: str = "",
125
- ) -> tuple[str, dict[str, str]]:
126
- """Build a Cypher AND-clause scoping a node alias by microservice/module.
127
-
128
- Returns ``(clause, params)`` where ``clause`` is ``""`` or
129
- ``" AND <alias>.microservice = $ms AND <alias>.module = $mod"`` and
130
- ``params`` carries only the bound scope values. Used by the candidate
131
- matchers to push ``--service``/``--module`` down into resolve so they act
132
- as resolve-time filters (not just traversal post-filters).
133
- """
134
- preds: list[str] = []
135
- params: dict[str, str] = {}
136
- if microservice:
137
- preds.append(f"{alias}.microservice = $ms")
138
- params["ms"] = microservice
139
- if module:
140
- preds.append(f"{alias}.module = $mod")
141
- params["mod"] = module
142
- clause = (" AND " + " AND ".join(preds)) if preds else ""
143
- return clause, params
144
-
145
-
146
- class ResolveCandidate(BaseModel):
147
- model_config = ConfigDict(extra="forbid")
148
-
149
- node: NodeRef
150
- score: float
151
- reason: ResolveReason
152
-
153
-
154
- class ResolveOutput(BaseModel):
155
- model_config = ConfigDict(extra="forbid")
156
-
157
- success: bool
158
- status: ResolveStatus
159
- node: NodeRef | None = None
160
- candidates: list[ResolveCandidate] = Field(default_factory=list)
161
- message: str | None = None
162
- resolved_identifier: str | None = None
163
- advisories: list[str] = Field(default_factory=list, description="Pure informational text with no tool call suggestion")
164
- hints_structured: list[StructuredHint] = Field(default_factory=list, description=MCP_HINTS_STRUCTURED_FIELD_DESCRIPTION)
165
- absence: AbsenceDiagnosis | None = None
166
-
167
-
168
- def _resolve_validate_identifier(raw: str) -> tuple[str | None, str | None]:
169
- trimmed = raw.strip()
170
- if not trimmed:
171
- detail = "empty string" if raw == "" else "whitespace only"
172
- return None, f"Invalid identifier: {detail}"
173
- return trimmed, None
174
-
175
-
176
- def _resolve_kinds_to_search(
177
- hint_kind: Literal["symbol", "route", "client", "producer"] | None,
178
- ) -> list[Literal["symbol", "route", "client", "producer"]]:
179
- if hint_kind is None:
180
- return ["symbol", "route", "client", "producer"]
181
- return [hint_kind]
182
-
183
-
184
- def _resolve_parse_route_method_path(identifier: str) -> tuple[str, str] | None:
185
- parts = identifier.split(None, 1)
186
- if len(parts) != 2:
187
- return None
188
- method, path = parts[0].upper(), parts[1].strip()
189
- if not method.isalpha() or not path.startswith("/"):
190
- return None
191
- return method, path
192
-
193
-
194
- def _resolve_parse_microservice_route(identifier: str) -> tuple[str, str, str] | None:
195
- parts = identifier.split(None, 2)
196
- if len(parts) != 3:
197
- return None
198
- microservice, method, path = parts[0], parts[1].upper(), parts[2].strip()
199
- if not method.isalpha() or not path.startswith("/"):
200
- return None
201
- return microservice, method, path
202
-
203
-
204
- def _resolve_symbol_candidates(
205
- g: LadybugGraph,
206
- identifier: str,
207
- *,
208
- microservice: str = "",
209
- module: str = "",
210
- ) -> list[tuple[NodeRef, ResolveReason, int]]:
211
- out: list[tuple[NodeRef, ResolveReason, int]] = []
212
- lim = _RESOLVE_PRE_DEDUP_LIMIT
213
- scope, scope_params = _scope_clause("s", microservice, module)
214
-
215
- rows = g._rows( # noqa: SLF001
216
- f"MATCH (s:Symbol) WHERE s.id = $id{scope} RETURN {_SYMBOL_RESOLVE_RETURN} LIMIT $lim",
217
- {"id": identifier, "lim": lim, **scope_params},
218
- )
219
- for row in rows:
220
- out.append((_node_ref_from_row("symbol", row), "exact_id", len(identifier)))
221
-
222
- rows = g._rows( # noqa: SLF001
223
- f"MATCH (s:Symbol) WHERE s.fqn = $fqn{scope} RETURN {_SYMBOL_RESOLVE_RETURN} LIMIT $lim",
224
- {"fqn": identifier, "lim": lim, **scope_params},
225
- )
226
- for row in rows:
227
- out.append((_node_ref_from_row("symbol", row), "exact_fqn", len(identifier)))
228
-
229
- # Method FQN without arg signature (e.g. "pkg.Cls#method"): the stored method
230
- # fqn is "pkg.Cls#method(Type,Type)", so an argless identifier misses the
231
- # exact match above. Prefix-match on "<identifier>(" so the agent doesn't
232
- # have to type the exact "(Type,Type)" signature. Multiple overloads → the
233
- # resolve "many" path surfaces them honestly as ambiguous candidates.
234
- if "#" in identifier and "(" not in identifier:
235
- rows = g._rows( # noqa: SLF001
236
- f"MATCH (s:Symbol) WHERE s.fqn STARTS WITH $mp{scope} "
237
- f"RETURN {_SYMBOL_RESOLVE_RETURN} LIMIT $lim",
238
- {"mp": identifier + "(", "lim": lim, **scope_params},
239
- )
240
- for row in rows:
241
- out.append((_node_ref_from_row("symbol", row), "fqn_suffix", len(identifier) + 1))
242
- # Short "Cls#method" form (no package): the identifier is "<Class>#<method>"
243
- # with no dot in the class part. Match symbols whose fqn contains
244
- # "<Class>#<method>(" so overloads surface honestly as ambiguous.
245
- if "." not in identifier.split("#", 1)[0]:
246
- class_part, _, method_part = identifier.partition("#")
247
- if class_part and method_part:
248
- contains = f".{class_part}#{method_part}("
249
- rows = g._rows( # noqa: SLF001
250
- f"MATCH (s:Symbol) WHERE s.fqn CONTAINS $mp{scope} "
251
- f"RETURN {_SYMBOL_RESOLVE_RETURN} LIMIT $lim",
252
- {"mp": contains, "lim": lim, **scope_params},
253
- )
254
- for row in rows:
255
- fqn = str(row.get("fqn") or "")
256
- out.append(
257
- (_node_ref_from_row("symbol", row), "fqn_suffix", len(identifier) + 1)
258
- )
259
-
260
- suffix = f".{identifier}"
261
- rows = g._rows( # noqa: SLF001
262
- f"MATCH (s:Symbol) WHERE s.fqn = $ident OR s.fqn ENDS WITH $suffix{scope} "
263
- f"RETURN {_SYMBOL_RESOLVE_RETURN} LIMIT $lim",
264
- {"ident": identifier, "suffix": suffix, "lim": lim, **scope_params},
265
- )
266
- for row in rows:
267
- fqn = str(row.get("fqn") or "")
268
- spec = len(fqn)
269
- out.append((_node_ref_from_row("symbol", row), "fqn_suffix", spec))
270
-
271
- rows = g._rows( # noqa: SLF001
272
- f"MATCH (s:Symbol) WHERE s.name = $name{scope} RETURN {_SYMBOL_RESOLVE_RETURN} LIMIT $lim",
273
- {"name": identifier, "lim": lim, **scope_params},
274
- )
275
- for row in rows:
276
- out.append((_node_ref_from_row("symbol", row), "short_name", len(identifier)))
277
-
278
- return out
279
-
280
-
281
- def _resolve_route_candidates(
282
- g: LadybugGraph,
283
- identifier: str,
284
- *,
285
- microservice: str = "",
286
- module: str = "",
287
- ) -> list[tuple[NodeRef, ResolveReason, int]]:
288
- out: list[tuple[NodeRef, ResolveReason, int]] = []
289
- lim = _RESOLVE_PRE_DEDUP_LIMIT
290
- scope, scope_params = _scope_clause("r", microservice, module)
291
-
292
- rows = g._rows( # noqa: SLF001
293
- f"MATCH (r:Route) WHERE r.id = $id{scope} RETURN {_ROUTE_RESOLVE_RETURN} LIMIT $lim",
294
- {"id": identifier, "lim": lim, **scope_params},
295
- )
296
- for row in rows:
297
- out.append((_node_ref_from_row("route", row), "exact_id", len(identifier)))
298
-
299
- ms_route = _resolve_parse_microservice_route(identifier)
300
- if ms_route is not None:
301
- microservice_ms, method, path = ms_route
302
- rows = g._rows( # noqa: SLF001
303
- f"MATCH (r:Route) WHERE r.microservice = $ms AND r.method = $method "
304
- f"AND (r.path = $path OR r.path_template = $path){scope} "
305
- f"RETURN {_ROUTE_RESOLVE_RETURN} LIMIT $lim",
306
- {"ms": microservice_ms, "method": method, "path": path, "lim": lim, **scope_params},
307
- )
308
- for row in rows:
309
- spec = len(path)
310
- out.append((_node_ref_from_row("route", row), "route_method_path", spec))
311
-
312
- method_path = _resolve_parse_route_method_path(identifier)
313
- if method_path is not None:
314
- method, path = method_path
315
- rows = g._rows( # noqa: SLF001
316
- f"MATCH (r:Route) WHERE r.method = $method "
317
- f"AND (r.path = $path OR r.path_template = $path){scope} "
318
- f"RETURN {_ROUTE_RESOLVE_RETURN} LIMIT $lim",
319
- {"method": method, "path": path, "lim": lim, **scope_params},
320
- )
321
- for row in rows:
322
- out.append((_node_ref_from_row("route", row), "route_method_path", len(path)))
323
-
324
- if identifier.startswith("/"):
325
- rows = g._rows( # noqa: SLF001
326
- f"MATCH (r:Route) WHERE r.path = $path OR r.path_template = $path{scope} "
327
- f"RETURN {_ROUTE_RESOLVE_RETURN} LIMIT $lim",
328
- {"path": identifier, "lim": lim, **scope_params},
329
- )
330
- for row in rows:
331
- path_val = str(row.get("path_template") or row.get("path") or "")
332
- out.append((_node_ref_from_row("route", row), "route_template", len(path_val)))
333
-
334
- # Kafka/topic routes carry their name in ``topic`` (``path``/``path_template``
335
- # are empty), so path-based matching above cannot reach them. Match on
336
- # ``r.topic`` the same way ``_resolve_producer_candidates`` matches
337
- # ``p.topic`` — this lets ``flow``/``callers``/``overview`` resolve a
338
- # ``kafka_topic`` Route by topic name. ``_drop_route_mirrors`` below then
339
- # discards the no-EXPOSES producer phantom in favour of the server route.
340
- rows = g._rows( # noqa: SLF001
341
- f"MATCH (r:Route) WHERE r.topic = $topic{scope} RETURN {_ROUTE_RESOLVE_RETURN} LIMIT $lim",
342
- {"topic": identifier, "lim": lim, **scope_params},
343
- )
344
- for row in rows:
345
- out.append((_node_ref_from_row("route", row), "route_topic", len(identifier)))
346
-
347
- if not identifier.startswith("/"):
348
- rows = g._rows( # noqa: SLF001
349
- f"MATCH (r:Route) WHERE r.topic STARTS WITH $topic{scope} "
350
- f"RETURN {_ROUTE_RESOLVE_RETURN} LIMIT $lim",
351
- {"topic": identifier, "lim": lim, **scope_params},
352
- )
353
- for row in rows:
354
- out.append((_node_ref_from_row("route", row), "route_topic_prefix", len(identifier)))
355
-
356
- return _drop_route_mirrors(g, out)
357
-
358
-
359
- def _drop_route_mirrors(
360
- g: LadybugGraph,
361
- cands: list[tuple[NodeRef, ResolveReason, int]],
362
- ) -> list[tuple[NodeRef, ResolveReason, int]]:
363
- """Drop client-side Route mirrors that collide with a server-exposed route.
364
-
365
- A path can resolve to TWO Route nodes: the server route (exposed by a
366
- controller via an inbound ``EXPOSES`` edge, ``microservice`` set) and a
367
- client-side mirror (no ``EXPOSES``, often ``microservice=''``) created when
368
- a Client's HTTP call couldn't be linked to the server route. The mirror is
369
- an artifact — drop it when a server-exposed route shares the same
370
- ``(method, path_template)`` so the no-flags ``jrag callers '/path'`` flow
371
- resolves to the single server route instead of stalling on "ambiguous".
372
- GENUINE ambiguity (two server-exposed routes in different microservices
373
- sharing a path) is preserved — both have ``EXPOSES`` and survive.
374
- """
375
- if len(cands) < 2:
376
- return cands
377
- ids = [c[0].id for c in cands if c[0].id]
378
- if not ids:
379
- return cands
380
- id_list = ", ".join("'" + cid.replace("'", "''") + "'" for cid in ids)
381
- exposed_rows = g._rows( # noqa: SLF001
382
- "MATCH (s:Symbol)-[:EXPOSES]->(r:Route) "
383
- f"WHERE r.id IN [{id_list}] "
384
- "RETURN r.id AS rid",
385
- {},
386
- )
387
- exposed_ids = {str(r.get("rid") or "") for r in exposed_rows}
388
- if not exposed_ids:
389
- return cands
390
-
391
- # Group candidates by their route fqn ("METHOD path"); within a colliding
392
- # group, drop non-exposed (mirror) entries only when an exposed entry exists.
393
- groups: dict[str, list[tuple[NodeRef, ResolveReason, int]]] = {}
394
- for node, reason, spec in cands:
395
- groups.setdefault(str(node.fqn or ""), []).append((node, reason, spec))
396
-
397
- keep: list[tuple[NodeRef, ResolveReason, int]] = []
398
- for group in groups.values():
399
- has_exposed = any(c[0].id in exposed_ids for c in group)
400
- for node, reason, spec in group:
401
- if has_exposed and node.id not in exposed_ids:
402
- continue # mirror colliding with a server route — drop
403
- keep.append((node, reason, spec))
404
- return keep
405
-
406
-
407
- def _resolve_client_candidates(
408
- g: LadybugGraph,
409
- identifier: str,
410
- *,
411
- microservice: str = "",
412
- module: str = "",
413
- ) -> list[tuple[NodeRef, ResolveReason, int]]:
414
- out: list[tuple[NodeRef, ResolveReason, int]] = []
415
- lim = _RESOLVE_PRE_DEDUP_LIMIT
416
- scope, scope_params = _scope_clause("c", microservice, module)
417
-
418
- rows = g._rows( # noqa: SLF001
419
- f"MATCH (c:Client) WHERE c.id = $id{scope} RETURN {_CLIENT_RESOLVE_RETURN} LIMIT $lim",
420
- {"id": identifier, "lim": lim, **scope_params},
421
- )
422
- for row in rows:
423
- out.append((_node_ref_from_row("client", row), "exact_id", len(identifier)))
424
-
425
- if " " in identifier:
426
- target, path_prefix = identifier.split(" ", 1)
427
- target = target.strip()
428
- path_prefix = path_prefix.strip()
429
- if target and path_prefix:
430
- rows = g._rows( # noqa: SLF001
431
- f"MATCH (c:Client) WHERE c.target_service = $target "
432
- f"AND (c.path STARTS WITH $path OR c.path_template STARTS WITH $path){scope} "
433
- f"RETURN {_CLIENT_RESOLVE_RETURN} LIMIT $lim",
434
- {"target": target, "path": path_prefix, "lim": lim, **scope_params},
435
- )
436
- for row in rows:
437
- spec = len(path_prefix)
438
- out.append((_node_ref_from_row("client", row), "client_target_path", spec))
439
- elif not identifier.startswith("/"):
440
- rows = g._rows( # noqa: SLF001
441
- f"MATCH (c:Client) WHERE c.target_service = $target{scope} "
442
- f"RETURN {_CLIENT_RESOLVE_RETURN} LIMIT $lim",
443
- {"target": identifier, "lim": lim, **scope_params},
444
- )
445
- for row in rows:
446
- out.append((_node_ref_from_row("client", row), "client_target", len(identifier)))
447
-
448
- # Client-by-name/FQN: reach a Client root via its declaring Symbol (the
449
- # method that declares the client). Only SUFFIX/NAME matches are used — a
450
- # full method FQN identifier (e.g. 'pkg.Cls#method(Arg)') is intentionally
451
- # left to resolve to the method Symbol (exact_fqn) rather than ALSO matching
452
- # the Client declared by that same method, which would surface a spurious
453
- # "ambiguous" result. A bare class name (no '#') does not suffix-match a
454
- # Client's member_fqn (which carries '#method(args)'), so it resolves to the
455
- # type Symbol; a bare method name ('joinOperator') matches Client(s) via the
456
- # declaring symbol name and surfaces honest ambiguity across clients.
457
- if " " not in identifier and not identifier.startswith("/"):
458
- sym_scope, sym_scope_params = _scope_clause("s", microservice, module)
459
- # Declaring symbol name match (e.g. 'joinOperator').
460
- rows = g._rows( # noqa: SLF001
461
- "MATCH (s:Symbol)-[:DECLARES_CLIENT]->(c:Client) "
462
- f"WHERE s.name = $name{sym_scope} "
463
- f"RETURN {_CLIENT_RESOLVE_RETURN} LIMIT $lim",
464
- {"name": identifier, "lim": lim, **sym_scope_params},
465
- )
466
- for row in rows:
467
- out.append((_node_ref_from_row("client", row), "client_name", len(identifier)))
468
- # Declaring symbol FQN / member_fqn SUFFIX match (safe: a full method
469
- # FQN with args never suffix-matches because stored fqns carry the arg
470
- # suffix; '.<identifier>' only matches a class-level identifier).
471
- suffix = "." + identifier
472
- rows = g._rows( # noqa: SLF001
473
- "MATCH (s:Symbol)-[:DECLARES_CLIENT]->(c:Client) "
474
- f"WHERE s.fqn ENDS WITH $suffix OR c.member_fqn ENDS WITH $suffix{sym_scope} "
475
- f"RETURN {_CLIENT_RESOLVE_RETURN} LIMIT $lim",
476
- {"suffix": suffix, "lim": lim, **sym_scope_params},
477
- )
478
- for row in rows:
479
- fqn = str(row.get("member_fqn") or "")
480
- out.append((_node_ref_from_row("client", row), "client_fqn", len(fqn) or len(identifier)))
481
-
482
- return out
483
-
484
-
485
- def _resolve_producer_candidates(
486
- g: LadybugGraph,
487
- identifier: str,
488
- *,
489
- microservice: str = "",
490
- module: str = "",
491
- ) -> list[tuple[NodeRef, ResolveReason, int]]:
492
- out: list[tuple[NodeRef, ResolveReason, int]] = []
493
- lim = _RESOLVE_PRE_DEDUP_LIMIT
494
- scope, scope_params = _scope_clause("p", microservice, module)
495
-
496
- rows = g._rows( # noqa: SLF001
497
- f"MATCH (p:Producer) WHERE p.id = $id{scope} RETURN {_PRODUCER_RESOLVE_RETURN} LIMIT $lim",
498
- {"id": identifier, "lim": lim, **scope_params},
499
- )
500
- for row in rows:
501
- out.append((_node_ref_from_row("producer", row), "exact_id", len(identifier)))
502
-
503
- rows = g._rows( # noqa: SLF001
504
- f"MATCH (p:Producer) WHERE p.topic = $topic{scope} RETURN {_PRODUCER_RESOLVE_RETURN} LIMIT $lim",
505
- {"topic": identifier, "lim": lim, **scope_params},
506
- )
507
- for row in rows:
508
- out.append((_node_ref_from_row("producer", row), "producer_topic", len(identifier)))
509
-
510
- if not identifier.startswith("/"):
511
- rows = g._rows( # noqa: SLF001
512
- f"MATCH (p:Producer) WHERE p.topic STARTS WITH $topic{scope} "
513
- f"RETURN {_PRODUCER_RESOLVE_RETURN} LIMIT $lim",
514
- {"topic": identifier, "lim": lim, **scope_params},
515
- )
516
- for row in rows:
517
- out.append((_node_ref_from_row("producer", row), "producer_topic_prefix", len(identifier)))
518
-
519
- return out
520
-
521
-
522
- def _resolve_dedupe_candidates(
523
- raw: list[tuple[NodeRef, ResolveReason, int]],
524
- ) -> list[tuple[NodeRef, ResolveReason, int]]:
525
- best: dict[str, tuple[NodeRef, ResolveReason, int]] = {}
526
- for node, reason, specificity in raw:
527
- prev = best.get(node.id)
528
- if prev is None:
529
- best[node.id] = (node, reason, specificity)
530
- continue
531
- prev_pri = _RESOLVE_REASON_PRIORITY[prev[1]]
532
- new_pri = _RESOLVE_REASON_PRIORITY[reason]
533
- if new_pri < prev_pri or (new_pri == prev_pri and specificity > prev[2]):
534
- best[node.id] = (node, reason, specificity)
535
- return list(best.values())
536
-
537
-
538
- def _resolve_rank_candidates(
539
- deduped: list[tuple[NodeRef, ResolveReason, int]],
540
- ) -> list[ResolveCandidate]:
541
- ordered = sorted(
542
- deduped,
543
- key=lambda item: (_RESOLVE_REASON_PRIORITY[item[1]], -item[2], item[0].id),
544
- )
545
- total = len(ordered)
546
- return [
547
- ResolveCandidate(
548
- node=node,
549
- reason=reason,
550
- score=(1.0 - (idx / total)) if total else 0.0,
551
- )
552
- for idx, (node, reason, _spec) in enumerate(ordered)
553
- ]
554
-
555
-
556
- def _resolve_assert_invariants(out: ResolveOutput) -> None:
557
- if not out.success:
558
- assert out.status == "none"
559
- assert out.node is None
560
- assert not out.candidates
561
- assert out.message
562
- return
563
- if out.status == "one":
564
- assert out.node is not None
565
- assert not out.candidates
566
- elif out.status == "many":
567
- assert out.node is None
568
- assert len(out.candidates) >= 2
569
- elif out.status == "none":
570
- assert out.node is None
571
- assert not out.candidates
572
- assert out.message
573
-
574
-
575
- def _resolve_seeds_for_hints(identifier: str) -> tuple[str | None, str | None]:
576
- path_prefix_seed: str | None = None
577
- method_path = _resolve_parse_route_method_path(identifier)
578
- if method_path is not None:
579
- path_prefix_seed = method_path[1]
580
- else:
581
- ms_route = _resolve_parse_microservice_route(identifier)
582
- if ms_route is not None:
583
- path_prefix_seed = ms_route[2]
584
- elif identifier.startswith("/"):
585
- path_prefix_seed = identifier
586
-
587
- target_service_seed: str | None = None
588
- if " " in identifier:
589
- target, _path_prefix = identifier.split(" ", 1)
590
- target = target.strip()
591
- if target:
592
- target_service_seed = target
593
- elif not identifier.startswith("/"):
594
- target_service_seed = identifier
595
-
596
- return path_prefix_seed, target_service_seed
597
-
598
-
599
- def _resolve_finalize_success(
600
- trimmed: str,
601
- hint_kind: Literal["symbol", "route", "client", "producer"] | None,
602
- matches: list[ResolveCandidate],
603
- graph: LadybugGraph | None = None,
604
- microservice: str = "",
605
- module: str = "",
606
- ) -> ResolveOutput:
607
- if not matches:
608
- out = ResolveOutput(
609
- success=True,
610
- status="none",
611
- message=(
612
- "No matches for identifier; use search(query=...) for ranked fuzzy lookup."
613
- ),
614
- resolved_identifier=trimmed,
615
- )
616
- elif len(matches) == 1:
617
- out = ResolveOutput(
618
- success=True,
619
- status="one",
620
- node=matches[0].node,
621
- resolved_identifier=trimmed,
622
- )
623
- else:
624
- out = ResolveOutput(
625
- success=True,
626
- status="many",
627
- candidates=matches,
628
- resolved_identifier=trimmed,
629
- )
630
-
631
- # Absence diagnosis for empty results (status="none")
632
- cfg = _get_absence_config()
633
- diag: AbsenceDiagnosis | None = None
634
- if not matches:
635
- g = graph
636
- vocab = get_vocabulary_index(g, cfg)
637
- # Map hint_kind to filter_kind (symbol -> "symbol", etc.)
638
- filter_kind = hint_kind if hint_kind in ("symbol", "route", "client", "producer") else None
639
- # Build scope from microservice/module params
640
- scope = {"microservice": microservice or "", "module": module or ""}
641
- diag = diagnose(
642
- tool="resolve",
643
- query=trimmed,
644
- filt=None,
645
- filter_kind=filter_kind,
646
- root_node=None,
647
- scope=scope,
648
- vocab=vocab,
649
- graph=g,
650
- cfg=cfg,
651
- )
652
-
653
- path_prefix_seed, target_service_seed = _resolve_seeds_for_hints(trimmed)
654
- hint_payload = {
655
- "status": out.status,
656
- "resolved_identifier": trimmed,
657
- "candidates": out.candidates,
658
- "hint_kind": hint_kind,
659
- "path_prefix_seed": path_prefix_seed,
660
- "target_service_seed": target_service_seed,
661
- }
662
- raw_struct, raw_advisories = _hints_or_skip("resolve", hint_payload)
663
- out = out.model_copy(update={
664
- "advisories": raw_advisories,
665
- "hints_structured": _to_structured_hints(raw_struct),
666
- "absence": diag,
667
- })
668
- _resolve_assert_invariants(out)
669
- return out
670
-
671
-
672
- def resolve_v2(
673
- identifier: str,
674
- hint_kind: Literal["symbol", "route", "client", "producer"] | None = None,
675
- graph: LadybugGraph | None = None,
676
- *,
677
- microservice: str = "",
678
- module: str = "",
679
- ) -> ResolveOutput:
680
- try:
681
- trimmed, err = _resolve_validate_identifier(identifier)
682
- if err is not None:
683
- out = ResolveOutput(
684
- success=False,
685
- status="none",
686
- message=err,
687
- advisories=[],
688
- resolved_identifier=None,
689
- )
690
- _resolve_assert_invariants(out)
691
- return out
692
-
693
- assert trimmed is not None
694
- if "*" in trimmed or "?" in trimmed:
695
- out = ResolveOutput(
696
- success=False,
697
- status="none",
698
- message=(
699
- "Wildcards (* and ?) are not supported in resolve; "
700
- "use search(query=...) for ranked text search."
701
- ),
702
- advisories=[],
703
- resolved_identifier=trimmed,
704
- )
705
- _resolve_assert_invariants(out)
706
- return out
707
-
708
- g = graph or LadybugGraph.get()
709
- raw: list[tuple[NodeRef, ResolveReason, int]] = []
710
- for kind in _resolve_kinds_to_search(hint_kind):
711
- if kind == "symbol":
712
- raw.extend(_resolve_symbol_candidates(g, trimmed, microservice=microservice, module=module))
713
- elif kind == "route":
714
- raw.extend(_resolve_route_candidates(g, trimmed, microservice=microservice, module=module))
715
- elif kind == "client":
716
- raw.extend(_resolve_client_candidates(g, trimmed, microservice=microservice, module=module))
717
- else:
718
- raw.extend(_resolve_producer_candidates(g, trimmed, microservice=microservice, module=module))
719
-
720
- deduped = _resolve_dedupe_candidates(raw)
721
- ranked = _resolve_rank_candidates(deduped)
722
- capped = ranked[:_RESOLVE_CANDIDATE_CAP]
723
- return _resolve_finalize_success(
724
- trimmed,
725
- hint_kind,
726
- capped,
727
- graph=g,
728
- microservice=microservice,
729
- module=module,
730
- )
731
- except Exception as exc:
732
- out = ResolveOutput(
733
- success=False,
734
- status="none",
735
- message=str(exc),
736
- advisories=[],
737
- resolved_identifier=None,
738
- )
739
- _resolve_assert_invariants(out)
740
- return out