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.
- java_codebase_rag-0.12.2.dist-info/METADATA +35 -0
- java_codebase_rag-0.12.2.dist-info/RECORD +4 -0
- {java_codebase_rag-0.12.0.dist-info → java_codebase_rag-0.12.2.dist-info}/WHEEL +1 -1
- java_codebase_rag/_deprecation.py +0 -103
- java_codebase_rag/_fdlimit.py +0 -56
- java_codebase_rag/_stdio.py +0 -32
- java_codebase_rag/_version.py +0 -35
- java_codebase_rag/absence/__init__.py +0 -0
- java_codebase_rag/absence/absence_diagnosis.py +0 -700
- java_codebase_rag/absence/absence_types.py +0 -124
- java_codebase_rag/absence/absence_vocab.py +0 -460
- java_codebase_rag/analysis/__init__.py +0 -0
- java_codebase_rag/analysis/pr_analysis.py +0 -563
- java_codebase_rag/analysis/resolve_service.py +0 -740
- java_codebase_rag/ast/__init__.py +0 -0
- java_codebase_rag/ast/ast_java.py +0 -2847
- java_codebase_rag/ast/ast_kotlin.py +0 -1794
- java_codebase_rag/ast/brownfield_events.py +0 -58
- java_codebase_rag/ast/chunk_heuristics.py +0 -83
- java_codebase_rag/ast/language.py +0 -117
- java_codebase_rag/cli.py +0 -1215
- java_codebase_rag/cli_dispatch.py +0 -251
- java_codebase_rag/cli_format.py +0 -85
- java_codebase_rag/cli_progress.py +0 -94
- java_codebase_rag/config.py +0 -833
- java_codebase_rag/eval/__init__.py +0 -1
- java_codebase_rag/eval/ground_truth.py +0 -100
- java_codebase_rag/eval/metrics.py +0 -107
- java_codebase_rag/eval/runner.py +0 -556
- java_codebase_rag/graph/__init__.py +0 -0
- java_codebase_rag/graph/build_ast_graph.py +0 -4593
- java_codebase_rag/graph/graph_enrich.py +0 -1940
- java_codebase_rag/graph/graph_types.py +0 -224
- java_codebase_rag/graph/java_ontology.py +0 -465
- java_codebase_rag/graph/ladybug_queries.py +0 -2213
- java_codebase_rag/graph/path_filtering.py +0 -509
- java_codebase_rag/index/__init__.py +0 -0
- java_codebase_rag/index/java_index_flow_lancedb.py +0 -879
- java_codebase_rag/index/java_index_v1_common.py +0 -33
- java_codebase_rag/install_data/__init__.py +0 -0
- java_codebase_rag/install_data/agents/explorer-rag-cli.md +0 -110
- java_codebase_rag/install_data/agents/explorer-rag-enhanced.md +0 -152
- java_codebase_rag/install_data/skills/explore-codebase/SKILL.md +0 -165
- java_codebase_rag/install_data/skills/explore-codebase-cli/SKILL.md +0 -107
- java_codebase_rag/installer.py +0 -2188
- java_codebase_rag/jrag.py +0 -4545
- java_codebase_rag/jrag_envelope.py +0 -1107
- java_codebase_rag/jrag_hints.py +0 -204
- java_codebase_rag/jrag_render.py +0 -926
- java_codebase_rag/lance_optimize.py +0 -264
- java_codebase_rag/mcp/__init__.py +0 -0
- java_codebase_rag/mcp/mcp_hints.py +0 -932
- java_codebase_rag/mcp/mcp_v2.py +0 -1916
- java_codebase_rag/mcp/server.py +0 -886
- java_codebase_rag/pipeline.py +0 -531
- java_codebase_rag/progress.py +0 -570
- java_codebase_rag/read_payloads.py +0 -781
- java_codebase_rag/search/__init__.py +0 -0
- java_codebase_rag/search/index_common.py +0 -10
- java_codebase_rag/search/search_lancedb.py +0 -1296
- java_codebase_rag/search/search_lexical.py +0 -449
- java_codebase_rag/search/search_scoring.py +0 -537
- java_codebase_rag/watch/__init__.py +0 -0
- java_codebase_rag/watch/client.py +0 -230
- java_codebase_rag/watch/daemon.py +0 -396
- java_codebase_rag/watch/lock.py +0 -201
- java_codebase_rag/watch/paths.py +0 -76
- java_codebase_rag/watch/protocol.py +0 -122
- java_codebase_rag/watch/server.py +0 -273
- java_codebase_rag/watch/warm.py +0 -105
- java_codebase_rag/watch/watcher.py +0 -394
- java_codebase_rag-0.12.0.dist-info/METADATA +0 -340
- java_codebase_rag-0.12.0.dist-info/RECORD +0 -75
- java_codebase_rag-0.12.0.dist-info/entry_points.txt +0 -5
- java_codebase_rag-0.12.0.dist-info/licenses/LICENSE +0 -21
- java_codebase_rag-0.12.0.dist-info/top_level.txt +0 -1
- /java_codebase_rag/__init__.py → /java_codebase_rag-0.12.2.dist-info/top_level.txt +0 -0
|
@@ -1,1940 +0,0 @@
|
|
|
1
|
-
"""Helpers that bridge `ast_java` output with chunk- and graph-level metadata.
|
|
2
|
-
|
|
3
|
-
Used both by the CocoIndex indexer (for per-chunk enrichment) and by
|
|
4
|
-
`build_ast_graph.py` (for module / microservice inference and deterministic
|
|
5
|
-
node ids).
|
|
6
|
-
|
|
7
|
-
Two location concepts are tracked per file:
|
|
8
|
-
|
|
9
|
-
- **module** — the *innermost* build-marker ancestor (Maven / Gradle /
|
|
10
|
-
SBT). Same as the legacy `service` field. Useful for module-scoped
|
|
11
|
-
search inside a microservice.
|
|
12
|
-
- **microservice** — the *outermost* build-marker ancestor under
|
|
13
|
-
`project_root`. Represents one deployable / repo. Resolution order:
|
|
14
|
-
1. explicit override list (YAML at project root);
|
|
15
|
-
2. outermost build marker between `project_root` and the file;
|
|
16
|
-
3. first path segment under `project_root`;
|
|
17
|
-
4. empty.
|
|
18
|
-
"""
|
|
19
|
-
from __future__ import annotations
|
|
20
|
-
|
|
21
|
-
import hashlib
|
|
22
|
-
import re
|
|
23
|
-
import sys
|
|
24
|
-
from dataclasses import dataclass, field, replace
|
|
25
|
-
from functools import lru_cache
|
|
26
|
-
from pathlib import Path
|
|
27
|
-
from typing import Any, TypeVar
|
|
28
|
-
from java_codebase_rag.ast.ast_java import (
|
|
29
|
-
AnnotationRef,
|
|
30
|
-
JavaFileAst,
|
|
31
|
-
MethodDecl,
|
|
32
|
-
OutgoingCallDecl,
|
|
33
|
-
RouteDecl,
|
|
34
|
-
ROUTE_META_ANNOTATION_NAMES,
|
|
35
|
-
TypeDecl,
|
|
36
|
-
_ROUTE_HTTP_MAPPING_NAMES,
|
|
37
|
-
CODEBASE_HTTP_CLIENT_ANNOTATIONS,
|
|
38
|
-
CODEBASE_PRODUCER_ANNOTATIONS,
|
|
39
|
-
infer_capabilities_for_type,
|
|
40
|
-
infer_role_for_type,
|
|
41
|
-
ROLE_ANNOTATIONS,
|
|
42
|
-
_METHOD_ANN_TO_CAPABILITY,
|
|
43
|
-
_TYPE_ANN_TO_CAPABILITY,
|
|
44
|
-
)
|
|
45
|
-
from java_codebase_rag.ast.language import backend_for
|
|
46
|
-
from java_codebase_rag.graph.java_ontology import (
|
|
47
|
-
CLIENT_KIND_REST_TEMPLATE,
|
|
48
|
-
VALID_CAPABILITIES,
|
|
49
|
-
VALID_CLIENT_KINDS,
|
|
50
|
-
VALID_PRODUCER_KINDS,
|
|
51
|
-
VALID_ROLES,
|
|
52
|
-
VALID_ROUTE_FRAMEWORKS,
|
|
53
|
-
VALID_ROUTE_KINDS,
|
|
54
|
-
)
|
|
55
|
-
from java_codebase_rag.graph.path_filtering import LayeredIgnore, iter_source_files
|
|
56
|
-
|
|
57
|
-
__all__ = [
|
|
58
|
-
"AnnotationDecl",
|
|
59
|
-
"BrownfieldOverrides",
|
|
60
|
-
"ChunkEnrichment",
|
|
61
|
-
"annotation_meta_decls_from_graph_tables",
|
|
62
|
-
"collect_annotation_meta_chain",
|
|
63
|
-
"compute_meta_chains_from_decls",
|
|
64
|
-
"enrich_chunk",
|
|
65
|
-
"load_brownfield_overrides",
|
|
66
|
-
"load_microservice_overrides",
|
|
67
|
-
"module_for_path",
|
|
68
|
-
"microservice_for_path",
|
|
69
|
-
"resolve_role_and_capabilities",
|
|
70
|
-
"resolve_routes_for_method",
|
|
71
|
-
"resolve_http_client_for_method",
|
|
72
|
-
"resolve_async_producer_for_method",
|
|
73
|
-
"RouteHint",
|
|
74
|
-
"HttpClientHint",
|
|
75
|
-
"AsyncProducerHint",
|
|
76
|
-
"symbol_id",
|
|
77
|
-
"phantom_id",
|
|
78
|
-
"BUILD_MARKERS",
|
|
79
|
-
"CONFIG_FILENAMES",
|
|
80
|
-
]
|
|
81
|
-
|
|
82
|
-
BUILD_MARKERS = (
|
|
83
|
-
"pom.xml",
|
|
84
|
-
"build.gradle",
|
|
85
|
-
"build.gradle.kts",
|
|
86
|
-
"build.sbt",
|
|
87
|
-
)
|
|
88
|
-
|
|
89
|
-
# Recognised config filenames at `project_root` (first match wins).
|
|
90
|
-
CONFIG_FILENAMES = (".java-codebase-rag.yml", ".java-codebase-rag.yaml")
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
@dataclass
|
|
94
|
-
class ChunkEnrichment:
|
|
95
|
-
package: str
|
|
96
|
-
module: str
|
|
97
|
-
microservice: str
|
|
98
|
-
primary_type_fqn: str
|
|
99
|
-
primary_type_kind: str
|
|
100
|
-
role: str
|
|
101
|
-
annotations_on_type: list[str]
|
|
102
|
-
symbols: list[str]
|
|
103
|
-
capabilities: list[str] = field(default_factory=list)
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
# ---------- microservice override loading ----------
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
def _parse_csv(raw: str) -> list[str]:
|
|
110
|
-
return [s.strip() for s in raw.split(",") if s.strip()]
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
@lru_cache(maxsize=64)
|
|
114
|
-
def _load_config_microservice_roots(project_root_str: str) -> tuple[str, ...]:
|
|
115
|
-
"""Read `microservice_roots` from `.java-codebase-rag.yml` at project_root.
|
|
116
|
-
|
|
117
|
-
Cached per project_root to avoid re-reading on every chunk. Failures
|
|
118
|
-
(file missing, malformed YAML, missing key) silently return an empty
|
|
119
|
-
tuple — config is strictly opt-in.
|
|
120
|
-
"""
|
|
121
|
-
root = Path(project_root_str)
|
|
122
|
-
for name in CONFIG_FILENAMES:
|
|
123
|
-
candidate = root / name
|
|
124
|
-
if not candidate.is_file():
|
|
125
|
-
continue
|
|
126
|
-
try:
|
|
127
|
-
import yaml # PyYAML; already a transitive dep of cocoindex
|
|
128
|
-
except ImportError:
|
|
129
|
-
return ()
|
|
130
|
-
try:
|
|
131
|
-
data = yaml.safe_load(candidate.read_text(encoding="utf-8"))
|
|
132
|
-
except Exception:
|
|
133
|
-
return ()
|
|
134
|
-
if not isinstance(data, dict):
|
|
135
|
-
return ()
|
|
136
|
-
raw = data.get("microservice_roots")
|
|
137
|
-
if isinstance(raw, str):
|
|
138
|
-
return tuple(_parse_csv(raw))
|
|
139
|
-
if isinstance(raw, list):
|
|
140
|
-
return tuple(str(x).strip() for x in raw if str(x).strip())
|
|
141
|
-
return ()
|
|
142
|
-
return ()
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
@lru_cache(maxsize=64)
|
|
146
|
-
def load_generated_detection(project_root_str: str | None) -> GeneratedDetectionConfig:
|
|
147
|
-
"""Read `generated_detection` from `.java-codebase-rag.yml` at project_root.
|
|
148
|
-
|
|
149
|
-
Cached per project_root to avoid re-reading on every chunk. Returns empty
|
|
150
|
-
config when section absent or project_root is None. Malformed entries
|
|
151
|
-
(wrong types, non-string values) are dropped with a stderr warning.
|
|
152
|
-
"""
|
|
153
|
-
if project_root_str is None:
|
|
154
|
-
return GeneratedDetectionConfig()
|
|
155
|
-
|
|
156
|
-
root = Path(project_root_str)
|
|
157
|
-
for name in CONFIG_FILENAMES:
|
|
158
|
-
candidate = root / name
|
|
159
|
-
if not candidate.is_file():
|
|
160
|
-
continue
|
|
161
|
-
try:
|
|
162
|
-
import yaml # PyYAML; already a transitive dep of cocoindex
|
|
163
|
-
except ImportError:
|
|
164
|
-
return GeneratedDetectionConfig()
|
|
165
|
-
try:
|
|
166
|
-
data = yaml.safe_load(candidate.read_text(encoding="utf-8"))
|
|
167
|
-
except Exception:
|
|
168
|
-
return GeneratedDetectionConfig()
|
|
169
|
-
if not isinstance(data, dict):
|
|
170
|
-
return GeneratedDetectionConfig()
|
|
171
|
-
|
|
172
|
-
raw = data.get("generated_detection")
|
|
173
|
-
if raw is None:
|
|
174
|
-
return GeneratedDetectionConfig()
|
|
175
|
-
|
|
176
|
-
if not isinstance(raw, dict):
|
|
177
|
-
import sys
|
|
178
|
-
print("[warn] generated_detection must be a dict; skipping",
|
|
179
|
-
file=sys.stderr)
|
|
180
|
-
return GeneratedDetectionConfig()
|
|
181
|
-
|
|
182
|
-
result = GeneratedDetectionConfig()
|
|
183
|
-
|
|
184
|
-
# Spec table: (config_key, field_name, type_converter, is_list_type)
|
|
185
|
-
# is_list_type: True = keep as list, False = convert to set
|
|
186
|
-
spec_table = [
|
|
187
|
-
("header_patterns", "header_patterns", lambda x: x, True),
|
|
188
|
-
("annotation_patterns", "annotation_patterns", lambda x: x, True),
|
|
189
|
-
("force_fqns", "force_fqns", set, False),
|
|
190
|
-
("exclude_fqns", "exclude_fqns", set, False),
|
|
191
|
-
]
|
|
192
|
-
|
|
193
|
-
for config_key, field_name, type_conv, is_list_type in spec_table:
|
|
194
|
-
value = raw.get(config_key)
|
|
195
|
-
if value is not None:
|
|
196
|
-
if isinstance(value, list):
|
|
197
|
-
if is_list_type:
|
|
198
|
-
valid = [s for s in value if isinstance(s, str)]
|
|
199
|
-
else:
|
|
200
|
-
valid = {s for s in value if isinstance(s, str)}
|
|
201
|
-
|
|
202
|
-
if len(valid) != len(value):
|
|
203
|
-
import sys
|
|
204
|
-
print(f"[warn] generated_detection.{config_key}: "
|
|
205
|
-
"non-string entries dropped", file=sys.stderr)
|
|
206
|
-
|
|
207
|
-
# Update result with converted value
|
|
208
|
-
kwargs = {field_name: type_conv(valid)}
|
|
209
|
-
result = GeneratedDetectionConfig(
|
|
210
|
-
header_patterns=kwargs.get("header_patterns", result.header_patterns),
|
|
211
|
-
annotation_patterns=kwargs.get("annotation_patterns", result.annotation_patterns),
|
|
212
|
-
force_fqns=kwargs.get("force_fqns", result.force_fqns),
|
|
213
|
-
exclude_fqns=kwargs.get("exclude_fqns", result.exclude_fqns)
|
|
214
|
-
)
|
|
215
|
-
else:
|
|
216
|
-
import sys
|
|
217
|
-
print(f"[warn] generated_detection.{config_key}: "
|
|
218
|
-
"must be a list; skipping", file=sys.stderr)
|
|
219
|
-
|
|
220
|
-
return result
|
|
221
|
-
|
|
222
|
-
# No config file found → return empty config
|
|
223
|
-
return GeneratedDetectionConfig()
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
@lru_cache(maxsize=64)
|
|
227
|
-
def _load_config_cross_service_resolution(project_root_str: str) -> str:
|
|
228
|
-
"""Read `cross_service_resolution` from `.java-codebase-rag.yml` at project_root.
|
|
229
|
-
|
|
230
|
-
Returns "auto" or "brownfield_only". Defaults to "auto" when the key is absent
|
|
231
|
-
or the file is missing / malformed. Unknown values warn on stderr and fall back
|
|
232
|
-
to "auto".
|
|
233
|
-
"""
|
|
234
|
-
root = Path(project_root_str)
|
|
235
|
-
for name in CONFIG_FILENAMES:
|
|
236
|
-
candidate = root / name
|
|
237
|
-
if not candidate.is_file():
|
|
238
|
-
continue
|
|
239
|
-
try:
|
|
240
|
-
import yaml # PyYAML; already a transitive dep of cocoindex
|
|
241
|
-
except ImportError:
|
|
242
|
-
return "auto"
|
|
243
|
-
try:
|
|
244
|
-
data = yaml.safe_load(candidate.read_text(encoding="utf-8"))
|
|
245
|
-
except Exception:
|
|
246
|
-
return "auto"
|
|
247
|
-
if not isinstance(data, dict):
|
|
248
|
-
return "auto"
|
|
249
|
-
val = data.get("cross_service_resolution", "auto")
|
|
250
|
-
if val not in {"auto", "brownfield_only"}:
|
|
251
|
-
print(
|
|
252
|
-
f"[lancedb-mcp] cross_service_resolution: unknown value "
|
|
253
|
-
f"{val!r}, falling back to 'auto'",
|
|
254
|
-
file=sys.stderr,
|
|
255
|
-
)
|
|
256
|
-
return "auto"
|
|
257
|
-
return val
|
|
258
|
-
return "auto"
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
def load_microservice_overrides(project_root: str | Path | None) -> tuple[str, ...]:
|
|
262
|
-
"""Microservice root overrides from project YAML only (`microservice_roots:`)."""
|
|
263
|
-
out: list[str] = []
|
|
264
|
-
seen: set[str] = set()
|
|
265
|
-
|
|
266
|
-
if project_root is not None:
|
|
267
|
-
try:
|
|
268
|
-
root_str = str(Path(project_root).resolve())
|
|
269
|
-
except OSError:
|
|
270
|
-
root_str = str(project_root)
|
|
271
|
-
for name in _load_config_microservice_roots(root_str):
|
|
272
|
-
if name not in seen:
|
|
273
|
-
seen.add(name)
|
|
274
|
-
out.append(name)
|
|
275
|
-
|
|
276
|
-
return tuple(out)
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
# ---------- brownfield role / capability overrides ----------
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
@dataclass(frozen=True)
|
|
283
|
-
class RouteHint:
|
|
284
|
-
"""YAML `route_overrides` entry: maps to `RouteDecl` fields (B2a brownfield)."""
|
|
285
|
-
|
|
286
|
-
framework: str
|
|
287
|
-
kind: str
|
|
288
|
-
path: str = ""
|
|
289
|
-
method: str = ""
|
|
290
|
-
topic: str = ""
|
|
291
|
-
broker: str = ""
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
@dataclass(frozen=True)
|
|
295
|
-
class HttpClientHint:
|
|
296
|
-
client_kind: str
|
|
297
|
-
target_service: str = ""
|
|
298
|
-
path: str = ""
|
|
299
|
-
method: str = ""
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
@dataclass(frozen=True)
|
|
303
|
-
class AsyncProducerHint:
|
|
304
|
-
client_kind: str
|
|
305
|
-
topic: str = ""
|
|
306
|
-
broker: str = ""
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
@dataclass(frozen=True)
|
|
310
|
-
class BrownfieldOverrides:
|
|
311
|
-
annotation_to_role: dict[str, str] = field(default_factory=dict)
|
|
312
|
-
annotation_to_capabilities: dict[str, tuple[str, ...]] = field(default_factory=dict)
|
|
313
|
-
fqn_role: dict[str, str] = field(default_factory=dict)
|
|
314
|
-
fqn_capabilities: dict[str, tuple[str, ...]] = field(default_factory=dict)
|
|
315
|
-
annotation_to_route_hint: dict[str, RouteHint] = field(default_factory=dict)
|
|
316
|
-
fqn_to_route_hint: dict[str, RouteHint] = field(default_factory=dict)
|
|
317
|
-
annotation_to_http_client_hint: dict[str, HttpClientHint] = field(default_factory=dict)
|
|
318
|
-
fqn_to_http_client_hint: dict[str, HttpClientHint] = field(default_factory=dict)
|
|
319
|
-
annotation_to_async_producer_hint: dict[str, AsyncProducerHint] = field(default_factory=dict)
|
|
320
|
-
fqn_to_async_producer_hint: dict[str, AsyncProducerHint] = field(default_factory=dict)
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
@dataclass(frozen=True)
|
|
324
|
-
class GeneratedDetectionConfig:
|
|
325
|
-
"""Config for generated-source detection.
|
|
326
|
-
|
|
327
|
-
Mirrors brownfield override pattern: frozen dataclass with
|
|
328
|
-
field(default_factory=...) for mutable defaults.
|
|
329
|
-
"""
|
|
330
|
-
header_patterns: list[str] = field(default_factory=list)
|
|
331
|
-
annotation_patterns: list[str] = field(default_factory=list)
|
|
332
|
-
force_fqns: set[str] = field(default_factory=set)
|
|
333
|
-
exclude_fqns: set[str] = field(default_factory=set)
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
def _meta_builtins() -> frozenset[str]:
|
|
337
|
-
return (
|
|
338
|
-
frozenset(ROLE_ANNOTATIONS)
|
|
339
|
-
| frozenset(_METHOD_ANN_TO_CAPABILITY)
|
|
340
|
-
| frozenset(_TYPE_ANN_TO_CAPABILITY)
|
|
341
|
-
| ROUTE_META_ANNOTATION_NAMES
|
|
342
|
-
| CODEBASE_HTTP_CLIENT_ANNOTATIONS
|
|
343
|
-
| CODEBASE_PRODUCER_ANNOTATIONS
|
|
344
|
-
)
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
# Rounds in the iterative closure; `max_depth` of 4 = at most four hops
|
|
348
|
-
# from any annotation to a built-in in the plan's `_build_meta_chain` sketch
|
|
349
|
-
# (e.g. six linear wrappers to `@Service` leaves the outer name without a role
|
|
350
|
-
# from Layer A).
|
|
351
|
-
_META_PATH_DEPTH_CAP = 4
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
@dataclass(frozen=True)
|
|
355
|
-
class AnnotationDecl:
|
|
356
|
-
fqn: str
|
|
357
|
-
simple: str
|
|
358
|
-
meta_annotations: tuple[str, ...]
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
def _build_meta_chain(
|
|
362
|
-
decls: dict[str, AnnotationDecl],
|
|
363
|
-
builtins: frozenset[str],
|
|
364
|
-
*,
|
|
365
|
-
max_depth: int,
|
|
366
|
-
) -> dict[str, frozenset[str]]:
|
|
367
|
-
"""Iterative fixed-point over the meta-annotation graph (PLAN-BROWNFIELD, Pass A2)."""
|
|
368
|
-
chain: dict[str, set[str]] = {b: {b} for b in builtins}
|
|
369
|
-
for _ in range(max_depth):
|
|
370
|
-
changed = False
|
|
371
|
-
for _sk, decl in sorted(decls.items(), key=lambda kv: kv[0]):
|
|
372
|
-
reach: set[str] = set()
|
|
373
|
-
for parent in decl.meta_annotations:
|
|
374
|
-
reach |= chain.get(parent, set())
|
|
375
|
-
if reach and not reach.issubset(chain.get(decl.simple, set())):
|
|
376
|
-
chain.setdefault(decl.simple, set()).update(reach)
|
|
377
|
-
changed = True
|
|
378
|
-
if not changed:
|
|
379
|
-
break
|
|
380
|
-
return {k: frozenset(chain.get(k, set())) for k in decls}
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
def _collect_annotation_decl_index(project_root_str: str) -> dict[str, AnnotationDecl]:
|
|
384
|
-
"""File scan for `@interface` declarations; sorted paths for stable first-wins (Fix 5/6)."""
|
|
385
|
-
root = Path(project_root_str)
|
|
386
|
-
if not root.is_dir():
|
|
387
|
-
return {}
|
|
388
|
-
ignore = LayeredIgnore(root)
|
|
389
|
-
decls: dict[str, AnnotationDecl] = {}
|
|
390
|
-
for p in sorted(iter_source_files(root, ignore=ignore), key=str):
|
|
391
|
-
try:
|
|
392
|
-
content = p.read_bytes()
|
|
393
|
-
except OSError as exc:
|
|
394
|
-
print(
|
|
395
|
-
f"[lancedb-mcp] skipped unreadable {p}: {exc}",
|
|
396
|
-
file=sys.stderr,
|
|
397
|
-
)
|
|
398
|
-
continue
|
|
399
|
-
if not content.strip():
|
|
400
|
-
continue
|
|
401
|
-
backend = backend_for(p)
|
|
402
|
-
if backend is None:
|
|
403
|
-
continue
|
|
404
|
-
try:
|
|
405
|
-
jast = backend.parse(content, filename=str(p))
|
|
406
|
-
except Exception as exc:
|
|
407
|
-
print(
|
|
408
|
-
f"[lancedb-mcp] parse error in {p}: {exc}",
|
|
409
|
-
file=sys.stderr,
|
|
410
|
-
)
|
|
411
|
-
continue
|
|
412
|
-
for t in jast.all_types:
|
|
413
|
-
if t.kind != "annotation":
|
|
414
|
-
continue
|
|
415
|
-
if t.name in decls:
|
|
416
|
-
print(
|
|
417
|
-
f"[lancedb-mcp] duplicate @interface simple name {t.name!r} — "
|
|
418
|
-
f"keeping {decls[t.name].fqn!r}, ignoring {t.fqn!r}",
|
|
419
|
-
file=sys.stderr,
|
|
420
|
-
)
|
|
421
|
-
continue
|
|
422
|
-
decls[t.name] = AnnotationDecl(
|
|
423
|
-
fqn=t.fqn,
|
|
424
|
-
simple=t.name,
|
|
425
|
-
meta_annotations=tuple(a.name for a in t.annotations),
|
|
426
|
-
)
|
|
427
|
-
return decls
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
@lru_cache(maxsize=4)
|
|
431
|
-
def collect_annotation_meta_chain(
|
|
432
|
-
project_root_str: str,
|
|
433
|
-
) -> dict[str, frozenset[str]]:
|
|
434
|
-
"""Map annotation simple name → built-in simple names reachable via meta-annotations.
|
|
435
|
-
|
|
436
|
-
Single source of truth for Layer A: both the LadybugDB writer and Lance chunk
|
|
437
|
-
enrichment must use this; they must not derive `meta_chain` from separate
|
|
438
|
-
filesystem walks. See ``PLAN-BROWNFIELD-ROLE-OVERRIDES`` §
|
|
439
|
-
*Single source of truth (REQUIRED — read before implementation)*.
|
|
440
|
-
"""
|
|
441
|
-
decls = _collect_annotation_decl_index(project_root_str)
|
|
442
|
-
b = _meta_builtins()
|
|
443
|
-
return _build_meta_chain(decls, b, max_depth=_META_PATH_DEPTH_CAP)
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
def annotation_meta_decls_from_graph_tables(
|
|
447
|
-
types: dict[str, Any],
|
|
448
|
-
) -> dict[str, tuple[str, ...]]:
|
|
449
|
-
"""From `build_ast_graph.GraphTables.types`, map @interface simple name -> meta anns.
|
|
450
|
-
|
|
451
|
-
Used for diagnostics; Layer A in production uses `collect_annotation_meta_chain`
|
|
452
|
-
(disk) so LadybugDB and Lance share one index.
|
|
453
|
-
"""
|
|
454
|
-
decls: dict[str, tuple[str, ...]] = {}
|
|
455
|
-
first_fqn: dict[str, str] = {}
|
|
456
|
-
for e in types.values():
|
|
457
|
-
d = e.decl
|
|
458
|
-
if d.kind != "annotation":
|
|
459
|
-
continue
|
|
460
|
-
if d.name in decls:
|
|
461
|
-
print(
|
|
462
|
-
f"[lancedb-mcp] duplicate @interface simple name {d.name!r} — "
|
|
463
|
-
f"keeping {first_fqn[d.name]!r}, ignoring {d.fqn!r}",
|
|
464
|
-
file=sys.stderr,
|
|
465
|
-
)
|
|
466
|
-
continue
|
|
467
|
-
first_fqn[d.name] = d.fqn
|
|
468
|
-
decls[d.name] = tuple(a.name for a in d.annotations)
|
|
469
|
-
return decls
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
def compute_meta_chains_from_decls(
|
|
473
|
-
decls: dict[str, tuple[str, ...]],
|
|
474
|
-
) -> dict[str, frozenset[str]]:
|
|
475
|
-
"""Map annotation simple name → transitive built-in simple names (Layer A), tests/legacy.
|
|
476
|
-
|
|
477
|
-
Shape-only callers use placeholder FQNs; use `collect_annotation_meta_chain` for
|
|
478
|
-
a stable project index.
|
|
479
|
-
"""
|
|
480
|
-
adecls: dict[str, AnnotationDecl] = {
|
|
481
|
-
s: AnnotationDecl(
|
|
482
|
-
fqn=f"::{s}",
|
|
483
|
-
simple=s,
|
|
484
|
-
meta_annotations=meta,
|
|
485
|
-
)
|
|
486
|
-
for s, meta in decls.items()
|
|
487
|
-
}
|
|
488
|
-
b = _meta_builtins()
|
|
489
|
-
return _build_meta_chain(adecls, b, max_depth=_META_PATH_DEPTH_CAP)
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
@lru_cache(maxsize=64)
|
|
493
|
-
def _load_brownfield_overrides(project_root_str: str) -> BrownfieldOverrides:
|
|
494
|
-
"""Read `role_overrides` from `.java-codebase-rag.yml` at project_root. Cached per root."""
|
|
495
|
-
root = Path(project_root_str)
|
|
496
|
-
valid_roles = VALID_ROLES
|
|
497
|
-
valid_caps = VALID_CAPABILITIES
|
|
498
|
-
for name in CONFIG_FILENAMES:
|
|
499
|
-
candidate = root / name
|
|
500
|
-
if not candidate.is_file():
|
|
501
|
-
continue
|
|
502
|
-
try:
|
|
503
|
-
import yaml # PyYAML; already a transitive dep of cocoindex
|
|
504
|
-
except ImportError:
|
|
505
|
-
return BrownfieldOverrides({}, {}, {}, {}, {}, {}, {}, {}, {}, {})
|
|
506
|
-
try:
|
|
507
|
-
data = yaml.safe_load(candidate.read_text(encoding="utf-8"))
|
|
508
|
-
except Exception:
|
|
509
|
-
return BrownfieldOverrides({}, {}, {}, {}, {}, {}, {}, {}, {}, {})
|
|
510
|
-
if not isinstance(data, dict):
|
|
511
|
-
return BrownfieldOverrides({}, {}, {}, {}, {}, {}, {}, {}, {}, {})
|
|
512
|
-
ro = data.get("role_overrides")
|
|
513
|
-
if not isinstance(ro, dict):
|
|
514
|
-
ro = {}
|
|
515
|
-
a_to_r: dict[str, str] = {}
|
|
516
|
-
a_to_c: dict[str, tuple[str, ...]] = {}
|
|
517
|
-
fqn_r: dict[str, str] = {}
|
|
518
|
-
fqn_c: dict[str, tuple[str, ...]] = {}
|
|
519
|
-
|
|
520
|
-
ann = ro.get("annotations")
|
|
521
|
-
if isinstance(ann, dict):
|
|
522
|
-
for k, v in ann.items():
|
|
523
|
-
ks = str(k).strip()
|
|
524
|
-
if not ks:
|
|
525
|
-
continue
|
|
526
|
-
vs = str(v).strip()
|
|
527
|
-
if not vs:
|
|
528
|
-
continue
|
|
529
|
-
if vs not in valid_roles:
|
|
530
|
-
print(
|
|
531
|
-
f"[lancedb-mcp] role_overrides.annotations: unknown role {vs!r} for {ks!r} — dropped",
|
|
532
|
-
file=sys.stderr,
|
|
533
|
-
)
|
|
534
|
-
continue
|
|
535
|
-
a_to_r[ks] = vs
|
|
536
|
-
|
|
537
|
-
caps_block = ro.get("capabilities")
|
|
538
|
-
if isinstance(caps_block, dict):
|
|
539
|
-
for k, v in caps_block.items():
|
|
540
|
-
ks = str(k).strip()
|
|
541
|
-
if not ks or not isinstance(v, (list, tuple)):
|
|
542
|
-
continue
|
|
543
|
-
out_cp: list[str] = []
|
|
544
|
-
for item in v:
|
|
545
|
-
cap = str(item).strip()
|
|
546
|
-
if not cap:
|
|
547
|
-
continue
|
|
548
|
-
if cap not in valid_caps:
|
|
549
|
-
print(
|
|
550
|
-
f"[lancedb-mcp] role_overrides.capabilities: unknown capability {cap!r} for {ks!r} — dropped",
|
|
551
|
-
file=sys.stderr,
|
|
552
|
-
)
|
|
553
|
-
continue
|
|
554
|
-
out_cp.append(cap)
|
|
555
|
-
if out_cp:
|
|
556
|
-
a_to_c[ks] = tuple(out_cp)
|
|
557
|
-
|
|
558
|
-
fqn = ro.get("fqn")
|
|
559
|
-
if isinstance(fqn, dict):
|
|
560
|
-
for fqn_key, v in fqn.items():
|
|
561
|
-
fk = str(fqn_key).strip()
|
|
562
|
-
if not fk or not isinstance(v, dict):
|
|
563
|
-
continue
|
|
564
|
-
r = v.get("role")
|
|
565
|
-
if r is not None and str(r).strip():
|
|
566
|
-
rs = str(r).strip()
|
|
567
|
-
if rs in valid_roles:
|
|
568
|
-
fqn_r[fk] = rs
|
|
569
|
-
else:
|
|
570
|
-
print(
|
|
571
|
-
f"[lancedb-mcp] role_overrides.fqn: unknown role {rs!r} for {fk!r} — dropped",
|
|
572
|
-
file=sys.stderr,
|
|
573
|
-
)
|
|
574
|
-
cap_list = v.get("capabilities")
|
|
575
|
-
if isinstance(cap_list, (list, tuple)):
|
|
576
|
-
out_c: list[str] = []
|
|
577
|
-
for item in cap_list:
|
|
578
|
-
cap = str(item).strip()
|
|
579
|
-
if not cap:
|
|
580
|
-
continue
|
|
581
|
-
if cap not in valid_caps:
|
|
582
|
-
print(
|
|
583
|
-
f"[lancedb-mcp] role_overrides.fqn: unknown capability {cap!r} for {fk!r} — dropped",
|
|
584
|
-
file=sys.stderr,
|
|
585
|
-
)
|
|
586
|
-
continue
|
|
587
|
-
out_c.append(cap)
|
|
588
|
-
if out_c:
|
|
589
|
-
fqn_c[fk] = tuple(out_c)
|
|
590
|
-
|
|
591
|
-
a_route: dict[str, RouteHint] = {}
|
|
592
|
-
f_route: dict[str, RouteHint] = {}
|
|
593
|
-
a_http: dict[str, HttpClientHint] = {}
|
|
594
|
-
f_http: dict[str, HttpClientHint] = {}
|
|
595
|
-
a_async: dict[str, AsyncProducerHint] = {}
|
|
596
|
-
f_async: dict[str, AsyncProducerHint] = {}
|
|
597
|
-
r_ov = data.get("route_overrides")
|
|
598
|
-
if isinstance(r_ov, dict):
|
|
599
|
-
ann_rt = r_ov.get("annotations")
|
|
600
|
-
if isinstance(ann_rt, dict):
|
|
601
|
-
for key, val in ann_rt.items():
|
|
602
|
-
ks = str(key).strip()
|
|
603
|
-
if not ks or not isinstance(val, dict):
|
|
604
|
-
continue
|
|
605
|
-
fw = str(val.get("framework", "") or "").strip()
|
|
606
|
-
kd = str(val.get("kind", "") or "").strip()
|
|
607
|
-
if fw not in VALID_ROUTE_FRAMEWORKS:
|
|
608
|
-
print(
|
|
609
|
-
f"[lancedb-mcp] route_overrides.annotations: unknown framework {fw!r} "
|
|
610
|
-
f"for key {ks!r} — entry dropped",
|
|
611
|
-
file=sys.stderr,
|
|
612
|
-
)
|
|
613
|
-
continue
|
|
614
|
-
if kd not in VALID_ROUTE_KINDS:
|
|
615
|
-
print(
|
|
616
|
-
f"[lancedb-mcp] route_overrides.annotations: unknown kind {kd!r} "
|
|
617
|
-
f"for key {ks!r} — entry dropped",
|
|
618
|
-
file=sys.stderr,
|
|
619
|
-
)
|
|
620
|
-
continue
|
|
621
|
-
a_route[ks] = RouteHint(
|
|
622
|
-
framework=fw,
|
|
623
|
-
kind=kd,
|
|
624
|
-
path=str(val.get("path", "") or "").strip(),
|
|
625
|
-
method=str(val.get("method", "") or "").strip().upper(),
|
|
626
|
-
topic=str(val.get("topic", "") or "").strip(),
|
|
627
|
-
broker=str(val.get("broker", "") or "").strip(),
|
|
628
|
-
)
|
|
629
|
-
fqn_rt = r_ov.get("fqn")
|
|
630
|
-
if isinstance(fqn_rt, dict):
|
|
631
|
-
for fqn_key, val in fqn_rt.items():
|
|
632
|
-
fk = str(fqn_key).strip()
|
|
633
|
-
if not fk or not isinstance(val, dict):
|
|
634
|
-
continue
|
|
635
|
-
fw = str(val.get("framework", "") or "").strip()
|
|
636
|
-
kd = str(val.get("kind", "") or "").strip()
|
|
637
|
-
if fw not in VALID_ROUTE_FRAMEWORKS:
|
|
638
|
-
print(
|
|
639
|
-
f"[lancedb-mcp] route_overrides.fqn: unknown framework {fw!r} "
|
|
640
|
-
f"for key {fk!r} — entry dropped",
|
|
641
|
-
file=sys.stderr,
|
|
642
|
-
)
|
|
643
|
-
continue
|
|
644
|
-
if kd not in VALID_ROUTE_KINDS:
|
|
645
|
-
print(
|
|
646
|
-
f"[lancedb-mcp] route_overrides.fqn: unknown kind {kd!r} "
|
|
647
|
-
f"for key {fk!r} — entry dropped",
|
|
648
|
-
file=sys.stderr,
|
|
649
|
-
)
|
|
650
|
-
continue
|
|
651
|
-
f_route[fk] = RouteHint(
|
|
652
|
-
framework=fw,
|
|
653
|
-
kind=kd,
|
|
654
|
-
path=str(val.get("path", "") or "").strip(),
|
|
655
|
-
method=str(val.get("method", "") or "").strip().upper(),
|
|
656
|
-
topic=str(val.get("topic", "") or "").strip(),
|
|
657
|
-
broker=str(val.get("broker", "") or "").strip(),
|
|
658
|
-
)
|
|
659
|
-
|
|
660
|
-
http_ov = data.get("http_client_overrides")
|
|
661
|
-
if isinstance(http_ov, dict):
|
|
662
|
-
ann_http = http_ov.get("annotations")
|
|
663
|
-
if isinstance(ann_http, dict):
|
|
664
|
-
for key, val in ann_http.items():
|
|
665
|
-
ks = str(key).strip()
|
|
666
|
-
if not ks or not isinstance(val, dict):
|
|
667
|
-
continue
|
|
668
|
-
ck = str(val.get("client_kind", "") or "").strip()
|
|
669
|
-
if ck not in VALID_CLIENT_KINDS:
|
|
670
|
-
print(
|
|
671
|
-
f"[lancedb-mcp] http_client_overrides.annotations: unknown client_kind {ck!r} "
|
|
672
|
-
f"for key {ks!r} — entry dropped",
|
|
673
|
-
file=sys.stderr,
|
|
674
|
-
)
|
|
675
|
-
continue
|
|
676
|
-
a_http[ks] = HttpClientHint(
|
|
677
|
-
client_kind=ck,
|
|
678
|
-
target_service=str(val.get("target_service", "") or "").strip(),
|
|
679
|
-
path=str(val.get("path", "") or "").strip(),
|
|
680
|
-
method=str(val.get("method", "") or "").strip().upper(),
|
|
681
|
-
)
|
|
682
|
-
fqn_http = http_ov.get("fqn")
|
|
683
|
-
if isinstance(fqn_http, dict):
|
|
684
|
-
for fqn_key, val in fqn_http.items():
|
|
685
|
-
fk = str(fqn_key).strip()
|
|
686
|
-
if not fk or not isinstance(val, dict):
|
|
687
|
-
continue
|
|
688
|
-
ck = str(val.get("client_kind", "") or "").strip()
|
|
689
|
-
if ck not in VALID_CLIENT_KINDS:
|
|
690
|
-
print(
|
|
691
|
-
f"[lancedb-mcp] http_client_overrides.fqn: unknown client_kind {ck!r} "
|
|
692
|
-
f"for key {fk!r} — entry dropped",
|
|
693
|
-
file=sys.stderr,
|
|
694
|
-
)
|
|
695
|
-
continue
|
|
696
|
-
f_http[fk] = HttpClientHint(
|
|
697
|
-
client_kind=ck,
|
|
698
|
-
target_service=str(val.get("target_service", "") or "").strip(),
|
|
699
|
-
path=str(val.get("path", "") or "").strip(),
|
|
700
|
-
method=str(val.get("method", "") or "").strip().upper(),
|
|
701
|
-
)
|
|
702
|
-
|
|
703
|
-
async_ov = data.get("async_producer_overrides")
|
|
704
|
-
if isinstance(async_ov, dict):
|
|
705
|
-
ann_async = async_ov.get("annotations")
|
|
706
|
-
if isinstance(ann_async, dict):
|
|
707
|
-
for key, val in ann_async.items():
|
|
708
|
-
ks = str(key).strip()
|
|
709
|
-
if not ks or not isinstance(val, dict):
|
|
710
|
-
continue
|
|
711
|
-
ck = str(val.get("client_kind", "") or "").strip()
|
|
712
|
-
if ck not in VALID_PRODUCER_KINDS:
|
|
713
|
-
print(
|
|
714
|
-
f"[lancedb-mcp] async_producer_overrides.annotations: unknown client_kind {ck!r} "
|
|
715
|
-
f"for key {ks!r} — entry dropped",
|
|
716
|
-
file=sys.stderr,
|
|
717
|
-
)
|
|
718
|
-
continue
|
|
719
|
-
a_async[ks] = AsyncProducerHint(
|
|
720
|
-
client_kind=ck,
|
|
721
|
-
topic=str(val.get("topic", "") or "").strip(),
|
|
722
|
-
broker=str(val.get("broker", "") or "").strip(),
|
|
723
|
-
)
|
|
724
|
-
fqn_async = async_ov.get("fqn")
|
|
725
|
-
if isinstance(fqn_async, dict):
|
|
726
|
-
for fqn_key, val in fqn_async.items():
|
|
727
|
-
fk = str(fqn_key).strip()
|
|
728
|
-
if not fk or not isinstance(val, dict):
|
|
729
|
-
continue
|
|
730
|
-
ck = str(val.get("client_kind", "") or "").strip()
|
|
731
|
-
if ck not in VALID_PRODUCER_KINDS:
|
|
732
|
-
print(
|
|
733
|
-
f"[lancedb-mcp] async_producer_overrides.fqn: unknown client_kind {ck!r} "
|
|
734
|
-
f"for key {fk!r} — entry dropped",
|
|
735
|
-
file=sys.stderr,
|
|
736
|
-
)
|
|
737
|
-
continue
|
|
738
|
-
f_async[fk] = AsyncProducerHint(
|
|
739
|
-
client_kind=ck,
|
|
740
|
-
topic=str(val.get("topic", "") or "").strip(),
|
|
741
|
-
broker=str(val.get("broker", "") or "").strip(),
|
|
742
|
-
)
|
|
743
|
-
|
|
744
|
-
return BrownfieldOverrides(
|
|
745
|
-
a_to_r,
|
|
746
|
-
a_to_c,
|
|
747
|
-
fqn_r,
|
|
748
|
-
fqn_c,
|
|
749
|
-
a_route,
|
|
750
|
-
f_route,
|
|
751
|
-
a_http,
|
|
752
|
-
f_http,
|
|
753
|
-
a_async,
|
|
754
|
-
f_async,
|
|
755
|
-
)
|
|
756
|
-
return BrownfieldOverrides({}, {}, {}, {}, {}, {}, {}, {}, {}, {})
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
def load_brownfield_overrides(
|
|
760
|
-
project_root: str | Path | None,
|
|
761
|
-
) -> BrownfieldOverrides:
|
|
762
|
-
if project_root is None:
|
|
763
|
-
return BrownfieldOverrides({}, {}, {}, {}, {}, {}, {}, {}, {}, {})
|
|
764
|
-
try:
|
|
765
|
-
r = str(Path(project_root).resolve())
|
|
766
|
-
except OSError:
|
|
767
|
-
r = str(project_root)
|
|
768
|
-
return _load_brownfield_overrides(r)
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
def resolve_role_and_capabilities(
|
|
772
|
-
type_decl: TypeDecl,
|
|
773
|
-
*,
|
|
774
|
-
overrides: BrownfieldOverrides,
|
|
775
|
-
meta_chain: dict[str, frozenset[str]] | None = None,
|
|
776
|
-
) -> tuple[str, list[str]]:
|
|
777
|
-
"""Compose AST inference with brownfield overrides (single execution order).
|
|
778
|
-
|
|
779
|
-
The resolver runs the steps **below in order**; each step mutates the same
|
|
780
|
-
working ``(role, caps)``. Steps listed later in this docstring *override
|
|
781
|
-
or extend* the result of earlier steps when they apply. There is no second
|
|
782
|
-
"priority" axis: "last to run" in this list is the strongest.
|
|
783
|
-
|
|
784
|
-
1. Built-in inference (``infer_role_for_type`` / ``infer_capabilities_for_type``)
|
|
785
|
-
2. Layer B — config annotation map (``role_overrides.annotations`` / ``capabilities``)
|
|
786
|
-
3. Layer A — meta-annotation walk (``meta_chain``; Phase 2; no-op if None)
|
|
787
|
-
4. Layer C — ``@CodebaseRole`` / ``@CodebaseCapability`` in source
|
|
788
|
-
5. Layer B — per-FQN map (``role_overrides.fqn``)
|
|
789
|
-
|
|
790
|
-
Role rule: steps 2 and 3 that change *role* use ``if role == "OTHER"`` on the
|
|
791
|
-
*current* role, so step 2 (user config) runs before step 3: explicit config
|
|
792
|
-
wins over automatic meta (see `PLAN-BROWNFIELD-ROLE-OVERRIDES` §
|
|
793
|
-
*Resolver execution order*). Steps 4 and 5 apply to role without that guard.
|
|
794
|
-
Capability rule: every layer is additively unioned; return value is
|
|
795
|
-
``sorted(caps)`` for a stable on-disk form.
|
|
796
|
-
|
|
797
|
-
See ``PLAN-BROWNFIELD-ROLE-OVERRIDES`` § *Resolver execution order* for the
|
|
798
|
-
side-by-side table.
|
|
799
|
-
"""
|
|
800
|
-
# ----- Step 1: built-in inference (runs first) -----
|
|
801
|
-
role = infer_role_for_type(type_decl)
|
|
802
|
-
caps: set[str] = set(infer_capabilities_for_type(type_decl))
|
|
803
|
-
type_ann_names = [a.name for a in type_decl.annotations]
|
|
804
|
-
|
|
805
|
-
# ----- Step 2: Layer B — annotation name map (before meta-walk) -----
|
|
806
|
-
if role == "OTHER":
|
|
807
|
-
for ann in type_ann_names:
|
|
808
|
-
mapped = overrides.annotation_to_role.get(ann)
|
|
809
|
-
if mapped:
|
|
810
|
-
role = mapped
|
|
811
|
-
break
|
|
812
|
-
for ann in type_ann_names:
|
|
813
|
-
for c in overrides.annotation_to_capabilities.get(ann, ()):
|
|
814
|
-
caps.add(c)
|
|
815
|
-
for m in type_decl.methods:
|
|
816
|
-
for ann in m.annotations:
|
|
817
|
-
for c in overrides.annotation_to_capabilities.get(ann.name, ()):
|
|
818
|
-
caps.add(c)
|
|
819
|
-
|
|
820
|
-
# ----- Step 3: Layer A — meta-annotation chain -----
|
|
821
|
-
if meta_chain is not None:
|
|
822
|
-
if role == "OTHER":
|
|
823
|
-
for ann in type_ann_names:
|
|
824
|
-
for builtin in meta_chain.get(ann, ()):
|
|
825
|
-
mapped = ROLE_ANNOTATIONS.get(builtin)
|
|
826
|
-
if mapped:
|
|
827
|
-
role = mapped
|
|
828
|
-
break
|
|
829
|
-
if role != "OTHER":
|
|
830
|
-
break
|
|
831
|
-
for ann in type_ann_names:
|
|
832
|
-
for builtin in meta_chain.get(ann, ()):
|
|
833
|
-
c = _TYPE_ANN_TO_CAPABILITY.get(builtin)
|
|
834
|
-
if c:
|
|
835
|
-
caps.add(c)
|
|
836
|
-
for m in type_decl.methods:
|
|
837
|
-
for ann in m.annotations:
|
|
838
|
-
for builtin in meta_chain.get(ann.name, ()):
|
|
839
|
-
c = _METHOD_ANN_TO_CAPABILITY.get(builtin)
|
|
840
|
-
if c:
|
|
841
|
-
caps.add(c)
|
|
842
|
-
|
|
843
|
-
# ----- Step 4: Layer C — in-source @CodebaseRole / @CodebaseCapability -----
|
|
844
|
-
for ann in type_decl.annotations:
|
|
845
|
-
if ann.name == "CodebaseRole":
|
|
846
|
-
v = ann.arguments.get("value")
|
|
847
|
-
vk = ann.argument_kinds.get("value")
|
|
848
|
-
if vk == "string" and v is not None:
|
|
849
|
-
print(
|
|
850
|
-
f"[lancedb-mcp] CodebaseRole: string literal value {v!r} is no longer supported; "
|
|
851
|
-
"use CodebaseRoleKind.*",
|
|
852
|
-
file=sys.stderr,
|
|
853
|
-
)
|
|
854
|
-
elif vk == "enum" and v in VALID_ROLES:
|
|
855
|
-
role = v
|
|
856
|
-
elif vk == "enum" and v is not None and v not in VALID_ROLES:
|
|
857
|
-
print(
|
|
858
|
-
f"[lancedb-mcp] CodebaseRole: invalid value {v!r} — ignored",
|
|
859
|
-
file=sys.stderr,
|
|
860
|
-
)
|
|
861
|
-
elif ann.name == "CodebaseCapability":
|
|
862
|
-
v = ann.arguments.get("value")
|
|
863
|
-
vk = ann.argument_kinds.get("value")
|
|
864
|
-
if vk == "string" and v is not None:
|
|
865
|
-
print(
|
|
866
|
-
f"[lancedb-mcp] CodebaseCapability: string literal value {v!r} is no longer supported; "
|
|
867
|
-
"use CodebaseCapabilityKind.*",
|
|
868
|
-
file=sys.stderr,
|
|
869
|
-
)
|
|
870
|
-
elif vk == "enum" and v in VALID_CAPABILITIES:
|
|
871
|
-
caps.add(v)
|
|
872
|
-
elif vk == "enum" and v is not None and v not in VALID_CAPABILITIES:
|
|
873
|
-
print(
|
|
874
|
-
f"[lancedb-mcp] CodebaseCapability: invalid value {v!r} — ignored",
|
|
875
|
-
file=sys.stderr,
|
|
876
|
-
)
|
|
877
|
-
elif ann.name == "CodebaseCapabilities":
|
|
878
|
-
for v, vk in zip(
|
|
879
|
-
ann.container_capability_values,
|
|
880
|
-
ann.container_capability_kinds,
|
|
881
|
-
strict=True,
|
|
882
|
-
):
|
|
883
|
-
if vk == "string" and v:
|
|
884
|
-
print(
|
|
885
|
-
f"[lancedb-mcp] CodebaseCapabilities: string literal value {v!r} is no longer supported; "
|
|
886
|
-
"use CodebaseCapabilityKind.*",
|
|
887
|
-
file=sys.stderr,
|
|
888
|
-
)
|
|
889
|
-
elif vk == "enum" and v in VALID_CAPABILITIES:
|
|
890
|
-
caps.add(v)
|
|
891
|
-
elif vk == "enum" and v:
|
|
892
|
-
print(
|
|
893
|
-
f"[lancedb-mcp] CodebaseCapabilities: invalid value {v!r} — ignored",
|
|
894
|
-
file=sys.stderr,
|
|
895
|
-
)
|
|
896
|
-
|
|
897
|
-
# ----- Step 5: Layer B — per-FQN (runs last; can override role / add caps) -----
|
|
898
|
-
if type_decl.fqn in overrides.fqn_role:
|
|
899
|
-
role = overrides.fqn_role[type_decl.fqn]
|
|
900
|
-
for c in overrides.fqn_capabilities.get(type_decl.fqn, ()):
|
|
901
|
-
caps.add(c)
|
|
902
|
-
|
|
903
|
-
return role, sorted(caps)
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
_HTTP_ROUTE_KINDS = frozenset({"http_endpoint", "http_consumer"})
|
|
907
|
-
# Layer C `@CodebaseAsyncRoute` replaces same-method auto messaging of these kinds.
|
|
908
|
-
_LAYER_C_ASYNC_REPLACES_BUILTIN_KINDS = frozenset({"kafka_topic"})
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
def _route_path_atom(raw_value: str, value_kind: str | None) -> tuple[str, str, float, bool]:
|
|
912
|
-
# Canonical ladder for route path hints: annotation -> spel -> constant_ref.
|
|
913
|
-
# Note: an empty string literal is still an explicit annotation value (`annotation`).
|
|
914
|
-
# "No value present" is handled by caller fallback, not by a separate value_kind.
|
|
915
|
-
if value_kind == "string":
|
|
916
|
-
if "${" in raw_value:
|
|
917
|
-
return "", "spel", 0.85, False
|
|
918
|
-
return raw_value, "annotation", 1.0, True
|
|
919
|
-
return "", "constant_ref", 0.7, False
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
_HINT = TypeVar("_HINT")
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
def _hint_lookup(ann: AnnotationRef, hints: dict[str, _HINT]) -> _HINT | None:
|
|
926
|
-
"""Resolve a brownfield hint by qualified name, then simple name, then suffix.
|
|
927
|
-
|
|
928
|
-
Shared by route / http-client / async-producer hint resolution; the three
|
|
929
|
-
former copies differed only in the hint value type.
|
|
930
|
-
"""
|
|
931
|
-
q = ann.qualified.strip()
|
|
932
|
-
if q in hints:
|
|
933
|
-
return hints[q]
|
|
934
|
-
if ann.name in hints:
|
|
935
|
-
return hints[ann.name]
|
|
936
|
-
for k, h in sorted(hints.items(), key=lambda kv: kv[0]):
|
|
937
|
-
if k.endswith("." + ann.name):
|
|
938
|
-
return h
|
|
939
|
-
return None
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
def _route_decl_from_route_hint(
|
|
943
|
-
hint: RouteHint,
|
|
944
|
-
*,
|
|
945
|
-
method_fqn: str,
|
|
946
|
-
method_sig: str,
|
|
947
|
-
filename: str,
|
|
948
|
-
start_line: int,
|
|
949
|
-
end_line: int,
|
|
950
|
-
source_layer: str,
|
|
951
|
-
) -> RouteDecl:
|
|
952
|
-
return RouteDecl(
|
|
953
|
-
method_fqn=method_fqn,
|
|
954
|
-
method_sig=method_sig,
|
|
955
|
-
kind=hint.kind,
|
|
956
|
-
framework=hint.framework,
|
|
957
|
-
http_method=hint.method,
|
|
958
|
-
path=hint.path,
|
|
959
|
-
topic=hint.topic,
|
|
960
|
-
broker=hint.broker,
|
|
961
|
-
feign_name="",
|
|
962
|
-
feign_url="",
|
|
963
|
-
resolution_strategy="annotation",
|
|
964
|
-
confidence=1.0,
|
|
965
|
-
resolved=True,
|
|
966
|
-
filename=filename,
|
|
967
|
-
start_line=start_line,
|
|
968
|
-
end_line=end_line,
|
|
969
|
-
route_source_layer=source_layer,
|
|
970
|
-
)
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
def _http_paths_from_ann_ref(ann: AnnotationRef) -> list[tuple[str, str, float, bool]]:
|
|
974
|
-
"""Path atoms for a custom mapping annotation (AnnotationRef only; Layer A)."""
|
|
975
|
-
out: list[tuple[str, str, float, bool]] = []
|
|
976
|
-
for key in ("path", "value"):
|
|
977
|
-
if key not in ann.arguments:
|
|
978
|
-
continue
|
|
979
|
-
v = ann.arguments[key]
|
|
980
|
-
vk = ann.argument_kinds.get(key)
|
|
981
|
-
if not v:
|
|
982
|
-
continue
|
|
983
|
-
out.append(_route_path_atom(v, vk))
|
|
984
|
-
if not out:
|
|
985
|
-
out.append(_route_path_atom("", "string"))
|
|
986
|
-
return out
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
def _http_methods_for_ann_ref(ann: AnnotationRef, template: str) -> list[str]:
|
|
990
|
-
if template == "GetMapping":
|
|
991
|
-
return ["GET"]
|
|
992
|
-
if template == "PostMapping":
|
|
993
|
-
return ["POST"]
|
|
994
|
-
if template == "PutMapping":
|
|
995
|
-
return ["PUT"]
|
|
996
|
-
if template == "DeleteMapping":
|
|
997
|
-
return ["DELETE"]
|
|
998
|
-
if template == "PatchMapping":
|
|
999
|
-
return ["PATCH"]
|
|
1000
|
-
if template == "RequestMapping":
|
|
1001
|
-
raw = ann.arguments.get("method")
|
|
1002
|
-
mk = ann.argument_kinds.get("method")
|
|
1003
|
-
if raw and mk == "enum":
|
|
1004
|
-
return [raw.rsplit(".", 1)[-1].upper()]
|
|
1005
|
-
return [""]
|
|
1006
|
-
return [""]
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
def _layer_a_route_decls_from_ann(
|
|
1010
|
-
ann: AnnotationRef,
|
|
1011
|
-
meta_chain: dict[str, frozenset[str]],
|
|
1012
|
-
*,
|
|
1013
|
-
method_fqn: str,
|
|
1014
|
-
method_sig: str,
|
|
1015
|
-
filename: str,
|
|
1016
|
-
start_line: int,
|
|
1017
|
-
end_line: int,
|
|
1018
|
-
) -> list[RouteDecl]:
|
|
1019
|
-
"""Synthetic HTTP routes from custom annotations whose meta-chain hits Spring mappings."""
|
|
1020
|
-
if ann.name in _ROUTE_HTTP_MAPPING_NAMES:
|
|
1021
|
-
return []
|
|
1022
|
-
chain = meta_chain.get(ann.name, frozenset())
|
|
1023
|
-
http_hits = sorted(chain & _ROUTE_HTTP_MAPPING_NAMES)
|
|
1024
|
-
if not http_hits:
|
|
1025
|
-
return []
|
|
1026
|
-
template = http_hits[0]
|
|
1027
|
-
path_atoms = _http_paths_from_ann_ref(ann)
|
|
1028
|
-
methods = _http_methods_for_ann_ref(ann, template)
|
|
1029
|
-
out: list[RouteDecl] = []
|
|
1030
|
-
for raw_path, strat, conf, res in path_atoms:
|
|
1031
|
-
for hm in methods:
|
|
1032
|
-
out.append(
|
|
1033
|
-
RouteDecl(
|
|
1034
|
-
method_fqn=method_fqn,
|
|
1035
|
-
method_sig=method_sig,
|
|
1036
|
-
kind="http_endpoint",
|
|
1037
|
-
framework="spring_mvc",
|
|
1038
|
-
http_method=hm,
|
|
1039
|
-
path=raw_path,
|
|
1040
|
-
topic="",
|
|
1041
|
-
broker="",
|
|
1042
|
-
feign_name="",
|
|
1043
|
-
feign_url="",
|
|
1044
|
-
resolution_strategy=strat,
|
|
1045
|
-
confidence=conf,
|
|
1046
|
-
resolved=res,
|
|
1047
|
-
filename=filename,
|
|
1048
|
-
start_line=start_line,
|
|
1049
|
-
end_line=end_line,
|
|
1050
|
-
route_source_layer="layer_a_meta",
|
|
1051
|
-
),
|
|
1052
|
-
)
|
|
1053
|
-
return out
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
def _merge_layer_c_codebase_routes(
|
|
1057
|
-
working: list[RouteDecl],
|
|
1058
|
-
layer_c: list[RouteDecl],
|
|
1059
|
-
) -> list[RouteDecl]:
|
|
1060
|
-
"""Layer C — brownfield in-source routes win over same-method auto extraction.
|
|
1061
|
-
|
|
1062
|
-
HTTP: any `@CodebaseHttpRoute` for a method drops same-method **built-in** HTTP
|
|
1063
|
-
rows (typically `@GetMapping`), then layer C HTTP rows are appended so the
|
|
1064
|
-
brownfield path/method is authoritative (no field merge onto surviving built-ins).
|
|
1065
|
-
Async: any `@CodebaseAsyncRoute` (`kafka_topic`) for a method drops same-method
|
|
1066
|
-
**built-in** `kafka_topic` rows (typically `@KafkaListener`), then layer C rows
|
|
1067
|
-
are merged/appended so the brownfield topic is authoritative over auto extraction.
|
|
1068
|
-
"""
|
|
1069
|
-
if not layer_c:
|
|
1070
|
-
return working
|
|
1071
|
-
merged = [replace(r) for r in working]
|
|
1072
|
-
async_override_mf = {
|
|
1073
|
-
cr.method_fqn
|
|
1074
|
-
for cr in layer_c
|
|
1075
|
-
if cr.kind in _LAYER_C_ASYNC_REPLACES_BUILTIN_KINDS
|
|
1076
|
-
}
|
|
1077
|
-
if async_override_mf:
|
|
1078
|
-
merged = [
|
|
1079
|
-
r
|
|
1080
|
-
for r in merged
|
|
1081
|
-
if not (
|
|
1082
|
-
r.method_fqn in async_override_mf
|
|
1083
|
-
and r.kind in _LAYER_C_ASYNC_REPLACES_BUILTIN_KINDS
|
|
1084
|
-
and r.route_source_layer == "builtin"
|
|
1085
|
-
)
|
|
1086
|
-
]
|
|
1087
|
-
http_override_mf = {
|
|
1088
|
-
cr.method_fqn for cr in layer_c if cr.kind in _HTTP_ROUTE_KINDS
|
|
1089
|
-
}
|
|
1090
|
-
if http_override_mf:
|
|
1091
|
-
merged = [
|
|
1092
|
-
r
|
|
1093
|
-
for r in merged
|
|
1094
|
-
if not (
|
|
1095
|
-
r.method_fqn in http_override_mf
|
|
1096
|
-
and r.kind in _HTTP_ROUTE_KINDS
|
|
1097
|
-
and r.route_source_layer == "builtin"
|
|
1098
|
-
)
|
|
1099
|
-
]
|
|
1100
|
-
for cr in sorted(layer_c, key=lambda x: (x.path, x.http_method, x.topic)):
|
|
1101
|
-
if cr.kind in _HTTP_ROUTE_KINDS:
|
|
1102
|
-
merged.append(replace(cr))
|
|
1103
|
-
continue
|
|
1104
|
-
placed = False
|
|
1105
|
-
for i, r in enumerate(merged):
|
|
1106
|
-
if (
|
|
1107
|
-
r.kind in _HTTP_ROUTE_KINDS
|
|
1108
|
-
and cr.kind in _HTTP_ROUTE_KINDS
|
|
1109
|
-
and r.method_fqn == cr.method_fqn
|
|
1110
|
-
):
|
|
1111
|
-
merged[i] = replace(
|
|
1112
|
-
r,
|
|
1113
|
-
path=cr.path if cr.path else r.path,
|
|
1114
|
-
http_method=cr.http_method if cr.http_method else r.http_method,
|
|
1115
|
-
framework=cr.framework if cr.framework else r.framework,
|
|
1116
|
-
kind=cr.kind if cr.kind else r.kind,
|
|
1117
|
-
topic=cr.topic if cr.topic else r.topic,
|
|
1118
|
-
broker=cr.broker if cr.broker else r.broker,
|
|
1119
|
-
resolution_strategy="codebase_route",
|
|
1120
|
-
confidence=cr.confidence,
|
|
1121
|
-
resolved=cr.resolved,
|
|
1122
|
-
route_source_layer="layer_c_source",
|
|
1123
|
-
)
|
|
1124
|
-
placed = True
|
|
1125
|
-
break
|
|
1126
|
-
if not placed:
|
|
1127
|
-
merged.append(replace(cr))
|
|
1128
|
-
return merged
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
def _apply_layer_b_fqn(
|
|
1132
|
-
working: list[RouteDecl],
|
|
1133
|
-
hint: RouteHint,
|
|
1134
|
-
*,
|
|
1135
|
-
method_fqn: str,
|
|
1136
|
-
method_sig: str,
|
|
1137
|
-
filename: str,
|
|
1138
|
-
start_line: int,
|
|
1139
|
-
end_line: int,
|
|
1140
|
-
) -> list[RouteDecl]:
|
|
1141
|
-
"""Layer B fqn — last writer; merges onto existing routes or seeds one."""
|
|
1142
|
-
if not working:
|
|
1143
|
-
return [
|
|
1144
|
-
_route_decl_from_route_hint(
|
|
1145
|
-
hint,
|
|
1146
|
-
method_fqn=method_fqn,
|
|
1147
|
-
method_sig=method_sig,
|
|
1148
|
-
filename=filename,
|
|
1149
|
-
start_line=start_line,
|
|
1150
|
-
end_line=end_line,
|
|
1151
|
-
source_layer="layer_b_fqn",
|
|
1152
|
-
),
|
|
1153
|
-
]
|
|
1154
|
-
out: list[RouteDecl] = []
|
|
1155
|
-
for r in working:
|
|
1156
|
-
out.append(
|
|
1157
|
-
replace(
|
|
1158
|
-
r,
|
|
1159
|
-
framework=hint.framework or r.framework,
|
|
1160
|
-
kind=hint.kind or r.kind,
|
|
1161
|
-
path=hint.path or r.path,
|
|
1162
|
-
http_method=hint.method or r.http_method,
|
|
1163
|
-
topic=hint.topic or r.topic,
|
|
1164
|
-
broker=hint.broker or r.broker,
|
|
1165
|
-
route_source_layer="layer_b_fqn",
|
|
1166
|
-
),
|
|
1167
|
-
)
|
|
1168
|
-
return out
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
def resolve_routes_for_method(
|
|
1172
|
-
*,
|
|
1173
|
-
method_decl: MethodDecl,
|
|
1174
|
-
enclosing_type: TypeDecl,
|
|
1175
|
-
overrides: BrownfieldOverrides,
|
|
1176
|
-
meta_chain: dict[str, frozenset[str]] | None,
|
|
1177
|
-
builtin_routes: list[RouteDecl],
|
|
1178
|
-
) -> list[RouteDecl]:
|
|
1179
|
-
"""Compose built-in route extraction with brownfield overrides (single execution order).
|
|
1180
|
-
|
|
1181
|
-
Mirrors ``resolve_role_and_capabilities`` layering; see ``PLAN-TIER1-COMPLETION``
|
|
1182
|
-
§ PR-A3. Steps run **in order**; later steps override per field on the same
|
|
1183
|
-
route where applicable.
|
|
1184
|
-
|
|
1185
|
-
1. Built-in routes from ``_collect_routes`` (excluding ``@CodebaseRoute`` stubs)
|
|
1186
|
-
2. Layer B — ``route_overrides.annotations`` (annotation FQN or simple name)
|
|
1187
|
-
3. Layer A — meta-annotation walk via ``collect_annotation_meta_chain``
|
|
1188
|
-
4. Layer C — in-source ``@CodebaseHttpRoute`` / ``@CodebaseAsyncRoute`` (and
|
|
1189
|
-
legacy ``@CodebaseRoute``) from parse; async layer C drops built-in
|
|
1190
|
-
``kafka_topic`` rows for the same method before merge
|
|
1191
|
-
5. Layer B — ``route_overrides.fqn`` (outermost; merges onto every route)
|
|
1192
|
-
"""
|
|
1193
|
-
method_fqn = f"{enclosing_type.fqn}#{method_decl.signature}"
|
|
1194
|
-
filename = builtin_routes[0].filename if builtin_routes else ""
|
|
1195
|
-
sl, el = method_decl.start_line, method_decl.end_line
|
|
1196
|
-
|
|
1197
|
-
# In-source brownfield: `@CodebaseHttpRoute` marks `codebase_route`; async uses
|
|
1198
|
-
# the topic atom strategy but always sets `route_source_layer=layer_c_source`.
|
|
1199
|
-
builtins_only = [
|
|
1200
|
-
r
|
|
1201
|
-
for r in builtin_routes
|
|
1202
|
-
if r.route_source_layer != "layer_c_source" and r.resolution_strategy != "codebase_route"
|
|
1203
|
-
]
|
|
1204
|
-
layer_c_src = [
|
|
1205
|
-
r
|
|
1206
|
-
for r in builtin_routes
|
|
1207
|
-
if r.route_source_layer == "layer_c_source" or r.resolution_strategy == "codebase_route"
|
|
1208
|
-
]
|
|
1209
|
-
|
|
1210
|
-
working: list[RouteDecl] = [
|
|
1211
|
-
replace(r, route_source_layer="builtin") for r in builtins_only
|
|
1212
|
-
]
|
|
1213
|
-
|
|
1214
|
-
combined_anns: list[tuple[bool, AnnotationRef]] = sorted(
|
|
1215
|
-
[(False, a) for a in enclosing_type.annotations]
|
|
1216
|
-
+ [(True, a) for a in method_decl.annotations],
|
|
1217
|
-
key=lambda t: (t[1].name, t[1].qualified, t[0]),
|
|
1218
|
-
)
|
|
1219
|
-
if any(a.name in {"CodebaseRoute", "CodebaseRoutes"} for _m, a in combined_anns):
|
|
1220
|
-
print(
|
|
1221
|
-
"[lancedb-mcp] v1 brownfield annotation detected; migrate to "
|
|
1222
|
-
"CodebaseHttpRoute / CodebaseAsyncRoute / CodebaseHttpClient",
|
|
1223
|
-
file=sys.stderr,
|
|
1224
|
-
)
|
|
1225
|
-
|
|
1226
|
-
# ----- Step 2: Layer B — annotation route hints -----
|
|
1227
|
-
for _is_m, ann in combined_anns:
|
|
1228
|
-
hint = _hint_lookup(ann, overrides.annotation_to_route_hint)
|
|
1229
|
-
if hint is None:
|
|
1230
|
-
continue
|
|
1231
|
-
working.append(
|
|
1232
|
-
_route_decl_from_route_hint(
|
|
1233
|
-
hint,
|
|
1234
|
-
method_fqn=method_fqn,
|
|
1235
|
-
method_sig=method_decl.signature,
|
|
1236
|
-
filename=filename,
|
|
1237
|
-
start_line=sl,
|
|
1238
|
-
end_line=el,
|
|
1239
|
-
source_layer="layer_b_ann",
|
|
1240
|
-
),
|
|
1241
|
-
)
|
|
1242
|
-
|
|
1243
|
-
# ----- Step 3: Layer A — meta-linked custom mapping annotations -----
|
|
1244
|
-
if meta_chain is not None:
|
|
1245
|
-
seen_a: set[tuple[str, str]] = set()
|
|
1246
|
-
for _is_m, ann in combined_anns:
|
|
1247
|
-
key = (ann.name, ann.qualified)
|
|
1248
|
-
if key in seen_a:
|
|
1249
|
-
continue
|
|
1250
|
-
extra = _layer_a_route_decls_from_ann(
|
|
1251
|
-
ann,
|
|
1252
|
-
meta_chain,
|
|
1253
|
-
method_fqn=method_fqn,
|
|
1254
|
-
method_sig=method_decl.signature,
|
|
1255
|
-
filename=filename,
|
|
1256
|
-
start_line=sl,
|
|
1257
|
-
end_line=el,
|
|
1258
|
-
)
|
|
1259
|
-
if extra:
|
|
1260
|
-
seen_a.add(key)
|
|
1261
|
-
working.extend(extra)
|
|
1262
|
-
|
|
1263
|
-
# ----- Step 4: Layer C — in-source @CodebaseRoute -----
|
|
1264
|
-
working = _merge_layer_c_codebase_routes(working, layer_c_src)
|
|
1265
|
-
|
|
1266
|
-
# ----- Step 5: Layer B — per-type FQN route hint -----
|
|
1267
|
-
fh = overrides.fqn_to_route_hint.get(enclosing_type.fqn)
|
|
1268
|
-
if fh is not None:
|
|
1269
|
-
working = _apply_layer_b_fqn(
|
|
1270
|
-
working,
|
|
1271
|
-
fh,
|
|
1272
|
-
method_fqn=method_fqn,
|
|
1273
|
-
method_sig=method_decl.signature,
|
|
1274
|
-
filename=filename,
|
|
1275
|
-
start_line=sl,
|
|
1276
|
-
end_line=el,
|
|
1277
|
-
)
|
|
1278
|
-
|
|
1279
|
-
return working
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
def _call_from_http_hint(
|
|
1283
|
-
*,
|
|
1284
|
-
hint: HttpClientHint,
|
|
1285
|
-
base_call: OutgoingCallDecl | None,
|
|
1286
|
-
method_decl: MethodDecl,
|
|
1287
|
-
enclosing_type: TypeDecl,
|
|
1288
|
-
source_layer: str,
|
|
1289
|
-
) -> OutgoingCallDecl:
|
|
1290
|
-
filename = base_call.filename if base_call is not None else ""
|
|
1291
|
-
start_line = base_call.start_line if base_call is not None else method_decl.start_line
|
|
1292
|
-
end_line = base_call.end_line if base_call is not None else method_decl.end_line
|
|
1293
|
-
method_fqn = (
|
|
1294
|
-
base_call.method_fqn if base_call is not None else f"{enclosing_type.fqn}#{method_decl.signature}"
|
|
1295
|
-
)
|
|
1296
|
-
method_sig = base_call.method_sig if base_call is not None else method_decl.signature
|
|
1297
|
-
return OutgoingCallDecl(
|
|
1298
|
-
method_fqn=method_fqn,
|
|
1299
|
-
method_sig=method_sig,
|
|
1300
|
-
client_kind=hint.client_kind or (base_call.client_kind if base_call else ""),
|
|
1301
|
-
channel="http",
|
|
1302
|
-
feign_target_name=hint.target_service or (base_call.feign_target_name if base_call else ""),
|
|
1303
|
-
feign_target_url=base_call.feign_target_url if base_call else "",
|
|
1304
|
-
path_template_call=hint.path or (base_call.path_template_call if base_call else ""),
|
|
1305
|
-
method_call=hint.method or (base_call.method_call if base_call else ""),
|
|
1306
|
-
topic_call="",
|
|
1307
|
-
broker_call="",
|
|
1308
|
-
raw_uri=(base_call.raw_uri if base_call else (hint.path or "")),
|
|
1309
|
-
raw_topic="",
|
|
1310
|
-
resolution_strategy=source_layer,
|
|
1311
|
-
confidence_base=1.0,
|
|
1312
|
-
resolved=True,
|
|
1313
|
-
filename=filename,
|
|
1314
|
-
start_line=start_line,
|
|
1315
|
-
end_line=end_line,
|
|
1316
|
-
)
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
def _call_from_async_hint(
|
|
1320
|
-
*,
|
|
1321
|
-
hint: AsyncProducerHint,
|
|
1322
|
-
base_call: OutgoingCallDecl | None,
|
|
1323
|
-
method_decl: MethodDecl,
|
|
1324
|
-
enclosing_type: TypeDecl,
|
|
1325
|
-
source_layer: str,
|
|
1326
|
-
) -> OutgoingCallDecl:
|
|
1327
|
-
filename = base_call.filename if base_call is not None else ""
|
|
1328
|
-
start_line = base_call.start_line if base_call is not None else method_decl.start_line
|
|
1329
|
-
end_line = base_call.end_line if base_call is not None else method_decl.end_line
|
|
1330
|
-
method_fqn = (
|
|
1331
|
-
base_call.method_fqn if base_call is not None else f"{enclosing_type.fqn}#{method_decl.signature}"
|
|
1332
|
-
)
|
|
1333
|
-
method_sig = base_call.method_sig if base_call is not None else method_decl.signature
|
|
1334
|
-
return OutgoingCallDecl(
|
|
1335
|
-
method_fqn=method_fqn,
|
|
1336
|
-
method_sig=method_sig,
|
|
1337
|
-
client_kind=hint.client_kind or (base_call.client_kind if base_call else ""),
|
|
1338
|
-
channel="async",
|
|
1339
|
-
feign_target_name="",
|
|
1340
|
-
feign_target_url="",
|
|
1341
|
-
path_template_call="",
|
|
1342
|
-
method_call="",
|
|
1343
|
-
topic_call=hint.topic or (base_call.topic_call if base_call else ""),
|
|
1344
|
-
broker_call=hint.broker or (base_call.broker_call if base_call else ""),
|
|
1345
|
-
raw_uri="",
|
|
1346
|
-
raw_topic=(base_call.raw_topic if base_call else (hint.topic or "")),
|
|
1347
|
-
resolution_strategy=source_layer,
|
|
1348
|
-
confidence_base=1.0,
|
|
1349
|
-
resolved=True,
|
|
1350
|
-
filename=filename,
|
|
1351
|
-
start_line=start_line,
|
|
1352
|
-
end_line=end_line,
|
|
1353
|
-
)
|
|
1354
|
-
|
|
1355
|
-
|
|
1356
|
-
def resolve_http_client_for_method(
|
|
1357
|
-
*,
|
|
1358
|
-
method_decl: MethodDecl,
|
|
1359
|
-
enclosing_type: TypeDecl,
|
|
1360
|
-
overrides: BrownfieldOverrides,
|
|
1361
|
-
meta_chain: dict[str, frozenset[str]] | None,
|
|
1362
|
-
builtin_calls: list[OutgoingCallDecl],
|
|
1363
|
-
) -> list[OutgoingCallDecl]:
|
|
1364
|
-
builtins_only = [c for c in builtin_calls if c.resolution_strategy != "codebase_client"]
|
|
1365
|
-
layer_c_src = [c for c in builtin_calls if c.resolution_strategy == "codebase_client"]
|
|
1366
|
-
combined_anns: list[tuple[bool, AnnotationRef]] = sorted(
|
|
1367
|
-
[(False, a) for a in enclosing_type.annotations]
|
|
1368
|
-
+ [(True, a) for a in method_decl.annotations],
|
|
1369
|
-
key=lambda t: (t[1].name, t[1].qualified, t[0]),
|
|
1370
|
-
)
|
|
1371
|
-
builtin_http = [c for c in builtins_only if c.channel == "http"]
|
|
1372
|
-
brownfield_calls: list[OutgoingCallDecl] = []
|
|
1373
|
-
anchor = builtin_http[0] if builtin_http else (layer_c_src[0] if layer_c_src else None)
|
|
1374
|
-
|
|
1375
|
-
for _is_m, ann in combined_anns:
|
|
1376
|
-
hint = _hint_lookup(ann, overrides.annotation_to_http_client_hint)
|
|
1377
|
-
if hint is None:
|
|
1378
|
-
continue
|
|
1379
|
-
brownfield_calls.append(
|
|
1380
|
-
_call_from_http_hint(
|
|
1381
|
-
hint=hint,
|
|
1382
|
-
base_call=anchor,
|
|
1383
|
-
method_decl=method_decl,
|
|
1384
|
-
enclosing_type=enclosing_type,
|
|
1385
|
-
source_layer="layer_b_ann",
|
|
1386
|
-
),
|
|
1387
|
-
)
|
|
1388
|
-
|
|
1389
|
-
if meta_chain is not None:
|
|
1390
|
-
seen_a: set[tuple[str, str]] = set()
|
|
1391
|
-
for _is_m, ann in combined_anns:
|
|
1392
|
-
key = (ann.name, ann.qualified)
|
|
1393
|
-
if key in seen_a:
|
|
1394
|
-
continue
|
|
1395
|
-
if ann.name in CODEBASE_HTTP_CLIENT_ANNOTATIONS:
|
|
1396
|
-
continue
|
|
1397
|
-
chain = meta_chain.get(ann.name, frozenset())
|
|
1398
|
-
if "CodebaseHttpClient" not in chain and "CodebaseHttpClients" not in chain:
|
|
1399
|
-
continue
|
|
1400
|
-
hint = overrides.annotation_to_http_client_hint.get("CodebaseHttpClient")
|
|
1401
|
-
if hint is None:
|
|
1402
|
-
hint = HttpClientHint(
|
|
1403
|
-
client_kind=anchor.client_kind if anchor else CLIENT_KIND_REST_TEMPLATE,
|
|
1404
|
-
target_service=anchor.feign_target_name if anchor else "",
|
|
1405
|
-
path=anchor.path_template_call if anchor else "",
|
|
1406
|
-
method=anchor.method_call if anchor else "",
|
|
1407
|
-
)
|
|
1408
|
-
seen_a.add(key)
|
|
1409
|
-
brownfield_calls.append(
|
|
1410
|
-
_call_from_http_hint(
|
|
1411
|
-
hint=hint,
|
|
1412
|
-
base_call=anchor,
|
|
1413
|
-
method_decl=method_decl,
|
|
1414
|
-
enclosing_type=enclosing_type,
|
|
1415
|
-
source_layer="layer_a_meta",
|
|
1416
|
-
),
|
|
1417
|
-
)
|
|
1418
|
-
|
|
1419
|
-
for c in layer_c_src:
|
|
1420
|
-
if c.channel == "http":
|
|
1421
|
-
brownfield_calls.append(replace(c, resolution_strategy="layer_c_source"))
|
|
1422
|
-
|
|
1423
|
-
fh = overrides.fqn_to_http_client_hint.get(enclosing_type.fqn)
|
|
1424
|
-
if fh is not None:
|
|
1425
|
-
if not brownfield_calls:
|
|
1426
|
-
brownfield_calls.append(
|
|
1427
|
-
_call_from_http_hint(
|
|
1428
|
-
hint=fh,
|
|
1429
|
-
base_call=anchor,
|
|
1430
|
-
method_decl=method_decl,
|
|
1431
|
-
enclosing_type=enclosing_type,
|
|
1432
|
-
source_layer="layer_b_fqn",
|
|
1433
|
-
),
|
|
1434
|
-
)
|
|
1435
|
-
else:
|
|
1436
|
-
brownfield_calls = [
|
|
1437
|
-
_call_from_http_hint(
|
|
1438
|
-
hint=fh,
|
|
1439
|
-
base_call=c,
|
|
1440
|
-
method_decl=method_decl,
|
|
1441
|
-
enclosing_type=enclosing_type,
|
|
1442
|
-
source_layer="layer_b_fqn",
|
|
1443
|
-
) for c in brownfield_calls
|
|
1444
|
-
]
|
|
1445
|
-
return brownfield_calls if brownfield_calls else builtin_http
|
|
1446
|
-
|
|
1447
|
-
|
|
1448
|
-
def resolve_async_producer_for_method(
|
|
1449
|
-
*,
|
|
1450
|
-
method_decl: MethodDecl,
|
|
1451
|
-
enclosing_type: TypeDecl,
|
|
1452
|
-
overrides: BrownfieldOverrides,
|
|
1453
|
-
meta_chain: dict[str, frozenset[str]] | None,
|
|
1454
|
-
builtin_calls: list[OutgoingCallDecl],
|
|
1455
|
-
) -> list[OutgoingCallDecl]:
|
|
1456
|
-
builtins_only = [c for c in builtin_calls if c.resolution_strategy != "codebase_producer"]
|
|
1457
|
-
layer_c_src = [c for c in builtin_calls if c.resolution_strategy == "codebase_producer"]
|
|
1458
|
-
combined_anns: list[tuple[bool, AnnotationRef]] = sorted(
|
|
1459
|
-
[(False, a) for a in enclosing_type.annotations]
|
|
1460
|
-
+ [(True, a) for a in method_decl.annotations],
|
|
1461
|
-
key=lambda t: (t[1].name, t[1].qualified, t[0]),
|
|
1462
|
-
)
|
|
1463
|
-
builtin_async = [c for c in builtins_only if c.channel == "async"]
|
|
1464
|
-
brownfield_calls: list[OutgoingCallDecl] = []
|
|
1465
|
-
anchor = builtin_async[0] if builtin_async else (layer_c_src[0] if layer_c_src else None)
|
|
1466
|
-
|
|
1467
|
-
for _is_m, ann in combined_anns:
|
|
1468
|
-
hint = _hint_lookup(ann, overrides.annotation_to_async_producer_hint)
|
|
1469
|
-
if hint is None:
|
|
1470
|
-
continue
|
|
1471
|
-
brownfield_calls.append(
|
|
1472
|
-
_call_from_async_hint(
|
|
1473
|
-
hint=hint,
|
|
1474
|
-
base_call=anchor,
|
|
1475
|
-
method_decl=method_decl,
|
|
1476
|
-
enclosing_type=enclosing_type,
|
|
1477
|
-
source_layer="layer_b_ann",
|
|
1478
|
-
),
|
|
1479
|
-
)
|
|
1480
|
-
|
|
1481
|
-
if meta_chain is not None:
|
|
1482
|
-
seen_a: set[tuple[str, str]] = set()
|
|
1483
|
-
for _is_m, ann in combined_anns:
|
|
1484
|
-
key = (ann.name, ann.qualified)
|
|
1485
|
-
if key in seen_a:
|
|
1486
|
-
continue
|
|
1487
|
-
if ann.name in CODEBASE_PRODUCER_ANNOTATIONS:
|
|
1488
|
-
continue
|
|
1489
|
-
chain = meta_chain.get(ann.name, frozenset())
|
|
1490
|
-
if "CodebaseProducer" not in chain and "CodebaseProducers" not in chain:
|
|
1491
|
-
continue
|
|
1492
|
-
hint = overrides.annotation_to_async_producer_hint.get("CodebaseProducer")
|
|
1493
|
-
if hint is None:
|
|
1494
|
-
hint = AsyncProducerHint(
|
|
1495
|
-
client_kind=anchor.client_kind if anchor else "kafka_send",
|
|
1496
|
-
topic=anchor.topic_call if anchor else "",
|
|
1497
|
-
broker=anchor.broker_call if anchor else "",
|
|
1498
|
-
)
|
|
1499
|
-
seen_a.add(key)
|
|
1500
|
-
brownfield_calls.append(
|
|
1501
|
-
_call_from_async_hint(
|
|
1502
|
-
hint=hint,
|
|
1503
|
-
base_call=anchor,
|
|
1504
|
-
method_decl=method_decl,
|
|
1505
|
-
enclosing_type=enclosing_type,
|
|
1506
|
-
source_layer="layer_a_meta",
|
|
1507
|
-
),
|
|
1508
|
-
)
|
|
1509
|
-
|
|
1510
|
-
for c in layer_c_src:
|
|
1511
|
-
if c.channel == "async":
|
|
1512
|
-
brownfield_calls.append(replace(c, resolution_strategy="layer_c_source"))
|
|
1513
|
-
|
|
1514
|
-
fh = overrides.fqn_to_async_producer_hint.get(enclosing_type.fqn)
|
|
1515
|
-
if fh is not None:
|
|
1516
|
-
if not brownfield_calls:
|
|
1517
|
-
brownfield_calls.append(
|
|
1518
|
-
_call_from_async_hint(
|
|
1519
|
-
hint=fh,
|
|
1520
|
-
base_call=anchor,
|
|
1521
|
-
method_decl=method_decl,
|
|
1522
|
-
enclosing_type=enclosing_type,
|
|
1523
|
-
source_layer="layer_b_fqn",
|
|
1524
|
-
),
|
|
1525
|
-
)
|
|
1526
|
-
else:
|
|
1527
|
-
brownfield_calls = [
|
|
1528
|
-
_call_from_async_hint(
|
|
1529
|
-
hint=fh,
|
|
1530
|
-
base_call=c,
|
|
1531
|
-
method_decl=method_decl,
|
|
1532
|
-
enclosing_type=enclosing_type,
|
|
1533
|
-
source_layer="layer_b_fqn",
|
|
1534
|
-
) for c in brownfield_calls
|
|
1535
|
-
]
|
|
1536
|
-
return brownfield_calls if brownfield_calls else builtin_async
|
|
1537
|
-
|
|
1538
|
-
|
|
1539
|
-
def _resolve_with_root(
|
|
1540
|
-
file_path: str, project_root: str | Path | None,
|
|
1541
|
-
) -> tuple[Path, Path | None]:
|
|
1542
|
-
p = Path(file_path)
|
|
1543
|
-
if project_root is None:
|
|
1544
|
-
try:
|
|
1545
|
-
return p.resolve(), None
|
|
1546
|
-
except OSError:
|
|
1547
|
-
return p, None
|
|
1548
|
-
root = Path(project_root).resolve()
|
|
1549
|
-
try:
|
|
1550
|
-
p_abs = (root / p).resolve() if not p.is_absolute() else p.resolve()
|
|
1551
|
-
except OSError:
|
|
1552
|
-
p_abs = p
|
|
1553
|
-
return p_abs, root
|
|
1554
|
-
|
|
1555
|
-
|
|
1556
|
-
def _bounded_parents(p: Path, root: Path | None) -> list[Path]:
|
|
1557
|
-
"""Parents of `p`, stopping at (and not crossing above) `root`."""
|
|
1558
|
-
try:
|
|
1559
|
-
parents = list(p.parents)
|
|
1560
|
-
except OSError:
|
|
1561
|
-
return []
|
|
1562
|
-
if root is None:
|
|
1563
|
-
return parents
|
|
1564
|
-
bounded: list[Path] = []
|
|
1565
|
-
for parent in parents:
|
|
1566
|
-
bounded.append(parent)
|
|
1567
|
-
if parent == root:
|
|
1568
|
-
break
|
|
1569
|
-
return bounded
|
|
1570
|
-
|
|
1571
|
-
|
|
1572
|
-
def _has_build_marker(directory: Path) -> bool:
|
|
1573
|
-
for marker in BUILD_MARKERS:
|
|
1574
|
-
if (directory / marker).is_file():
|
|
1575
|
-
return True
|
|
1576
|
-
return False
|
|
1577
|
-
|
|
1578
|
-
|
|
1579
|
-
def module_for_path(file_path: str, project_root: str | Path | None = None) -> str:
|
|
1580
|
-
"""Innermost build-marker ancestor's directory name.
|
|
1581
|
-
|
|
1582
|
-
Returns "" when no build marker is found between the file and
|
|
1583
|
-
`project_root` (inclusive).
|
|
1584
|
-
"""
|
|
1585
|
-
p, root = _resolve_with_root(file_path, project_root)
|
|
1586
|
-
for parent in _bounded_parents(p, root):
|
|
1587
|
-
if _has_build_marker(parent):
|
|
1588
|
-
return parent.name
|
|
1589
|
-
return ""
|
|
1590
|
-
|
|
1591
|
-
|
|
1592
|
-
def microservice_for_path(
|
|
1593
|
-
file_path: str, project_root: str | Path | None = None,
|
|
1594
|
-
) -> str:
|
|
1595
|
-
"""Outermost build-marker ancestor under `project_root`.
|
|
1596
|
-
|
|
1597
|
-
Resolution order, first hit wins:
|
|
1598
|
-
|
|
1599
|
-
1. Explicit override (env var + config file). The override is a list
|
|
1600
|
-
of directory names; the first one that appears in the file's
|
|
1601
|
-
ancestry (under `project_root`) wins.
|
|
1602
|
-
2. Outermost build-marker ancestor between `project_root` and `file`
|
|
1603
|
-
(i.e. the build marker closest to `project_root`).
|
|
1604
|
-
3. First path segment under `project_root`.
|
|
1605
|
-
4. "" — when none of the above apply (typically: file *is*
|
|
1606
|
-
`project_root`, or `project_root` is None and the file path
|
|
1607
|
-
has no parents).
|
|
1608
|
-
"""
|
|
1609
|
-
p, root = _resolve_with_root(file_path, project_root)
|
|
1610
|
-
parents = _bounded_parents(p, root)
|
|
1611
|
-
|
|
1612
|
-
overrides = load_microservice_overrides(project_root)
|
|
1613
|
-
if overrides:
|
|
1614
|
-
# Walk from outermost to innermost so a nested override (rare)
|
|
1615
|
-
# still works when the user lists a deeper directory.
|
|
1616
|
-
override_set = set(overrides)
|
|
1617
|
-
for parent in reversed(parents):
|
|
1618
|
-
if parent.name in override_set:
|
|
1619
|
-
return parent.name
|
|
1620
|
-
# Fall through to structural inference if no override matched.
|
|
1621
|
-
|
|
1622
|
-
outermost_marker: Path | None = None
|
|
1623
|
-
for parent in parents:
|
|
1624
|
-
if _has_build_marker(parent):
|
|
1625
|
-
outermost_marker = parent
|
|
1626
|
-
if outermost_marker is not None and (root is None or outermost_marker != root):
|
|
1627
|
-
return outermost_marker.name
|
|
1628
|
-
|
|
1629
|
-
if root is not None:
|
|
1630
|
-
# First path segment under `root`. parents are ordered
|
|
1631
|
-
# innermost-first; the candidate is the parent immediately
|
|
1632
|
-
# below `root`.
|
|
1633
|
-
for parent in parents:
|
|
1634
|
-
try:
|
|
1635
|
-
rel = parent.relative_to(root)
|
|
1636
|
-
except ValueError:
|
|
1637
|
-
continue
|
|
1638
|
-
parts = rel.parts
|
|
1639
|
-
if len(parts) == 1:
|
|
1640
|
-
return parts[0]
|
|
1641
|
-
|
|
1642
|
-
return ""
|
|
1643
|
-
|
|
1644
|
-
|
|
1645
|
-
# ---------- generated-source detection ----------
|
|
1646
|
-
|
|
1647
|
-
|
|
1648
|
-
# Built-in generator markers (v1 set). Verified against real generator output.
|
|
1649
|
-
# Annotation FQNs that mark generated code (any annotation with these FQNs
|
|
1650
|
-
# or simple names is considered a marker).
|
|
1651
|
-
_GENERATED_ANNOTATION_FQNS = {
|
|
1652
|
-
"javax.annotation.processing.Generated", # Standard Java (pre-Jakarta)
|
|
1653
|
-
"jakarta.annotation.processing.Generated", # Jakarta EE
|
|
1654
|
-
"org.immutables.value.Generated", # Immutables
|
|
1655
|
-
"lombok.Generated", # Lombok
|
|
1656
|
-
"com.squareup.javapoet.Generated", # JavaPoet
|
|
1657
|
-
}
|
|
1658
|
-
|
|
1659
|
-
# Header patterns for generators that emit banners (checked against first 4KB).
|
|
1660
|
-
# Patterns are compiled as case-insensitive regexes.
|
|
1661
|
-
_GENERATED_HEADER_PATTERNS = {
|
|
1662
|
-
re.compile(r"This file was generated by the OpenAPI Generator", re.IGNORECASE): "openapi",
|
|
1663
|
-
re.compile(r"Generated by the protocol buffer compiler", re.IGNORECASE): "protobuf",
|
|
1664
|
-
re.compile(r"This file was generated by jsonschema2pojo", re.IGNORECASE): "jsonschema2pojo",
|
|
1665
|
-
re.compile(r"generated by wsimport", re.IGNORECASE): "wsimport", # JAX-WS wsimport
|
|
1666
|
-
re.compile(r"WARNING: DO NOT EDIT.*generated by MapStruct", re.IGNORECASE): "mapstruct",
|
|
1667
|
-
}
|
|
1668
|
-
|
|
1669
|
-
# @Generated(value="...") patterns that identify the generator family.
|
|
1670
|
-
# These are matched against annotation arguments["value"] or arguments["comments"].
|
|
1671
|
-
_GENERATED_VALUE_PATTERNS = {
|
|
1672
|
-
re.compile(r"org\.openapitools\.codegen\."): "openapi",
|
|
1673
|
-
re.compile(r"org\.mapstruct\.ap\.MappingProcessor"): "mapstruct",
|
|
1674
|
-
re.compile(r"com\.google\.auto\.value\.processor\.AutoValueProcessor"): "autovalue",
|
|
1675
|
-
re.compile(r"org\.jooq\."): "jooq",
|
|
1676
|
-
re.compile(r"com\.querydsl\."): "querydsl",
|
|
1677
|
-
re.compile(r"org\.immutables\."): "immutables",
|
|
1678
|
-
}
|
|
1679
|
-
|
|
1680
|
-
|
|
1681
|
-
def _infer_family_from_annotation(annotation: AnnotationRef) -> str | None:
|
|
1682
|
-
"""Infer generator family from @Generated annotation arguments.
|
|
1683
|
-
|
|
1684
|
-
Returns lowercased family slug or None if no match.
|
|
1685
|
-
"""
|
|
1686
|
-
# Check value/comments arguments for generator identifiers FIRST
|
|
1687
|
-
# (handles javax.annotation.processing.Generated(value="org.mapstruct.ap.MappingProcessor"))
|
|
1688
|
-
value = annotation.arguments.get("value", "")
|
|
1689
|
-
comments = annotation.arguments.get("comments", "")
|
|
1690
|
-
|
|
1691
|
-
for pattern, family in _GENERATED_VALUE_PATTERNS.items():
|
|
1692
|
-
if pattern.search(value) or pattern.search(comments):
|
|
1693
|
-
return family
|
|
1694
|
-
|
|
1695
|
-
# Check annotation FQN for families identifiable by FQN itself
|
|
1696
|
-
if annotation.qualified in _GENERATED_ANNOTATION_FQNS:
|
|
1697
|
-
# Extract family from qualified name if possible
|
|
1698
|
-
if "lombok.Generated" in annotation.qualified:
|
|
1699
|
-
return "lombok"
|
|
1700
|
-
if "immutables" in annotation.qualified:
|
|
1701
|
-
return "immutables"
|
|
1702
|
-
# Generic javax/jakarta or JavaPoet -> unknown family
|
|
1703
|
-
return None
|
|
1704
|
-
|
|
1705
|
-
return None
|
|
1706
|
-
|
|
1707
|
-
|
|
1708
|
-
def _check_header_banners(header_text: str) -> str | None:
|
|
1709
|
-
"""Check header (first 4KB) for generator banners.
|
|
1710
|
-
|
|
1711
|
-
Args:
|
|
1712
|
-
header_text: Decoded header text (first 4KB of source file).
|
|
1713
|
-
|
|
1714
|
-
Returns family slug if matched, None otherwise.
|
|
1715
|
-
"""
|
|
1716
|
-
for pattern, family in _GENERATED_HEADER_PATTERNS.items():
|
|
1717
|
-
if pattern.search(header_text):
|
|
1718
|
-
return family
|
|
1719
|
-
|
|
1720
|
-
return None
|
|
1721
|
-
|
|
1722
|
-
|
|
1723
|
-
def classify_java_file(
|
|
1724
|
-
source: bytes,
|
|
1725
|
-
ast: "JavaFileAst",
|
|
1726
|
-
*,
|
|
1727
|
-
config: GeneratedDetectionConfig | None = None,
|
|
1728
|
-
project_root: str | Path | None = None,
|
|
1729
|
-
) -> tuple[bool, str | None]:
|
|
1730
|
-
"""Classify whether a Java source file is generated.
|
|
1731
|
-
|
|
1732
|
-
Args:
|
|
1733
|
-
source: Raw file bytes (required for header-banner detection).
|
|
1734
|
-
ast: Parsed Java AST (from ast_java.parse_java).
|
|
1735
|
-
config: Optional detection config (defaults to empty config).
|
|
1736
|
-
project_root: Optional project root for loading default config.
|
|
1737
|
-
|
|
1738
|
-
Returns:
|
|
1739
|
-
(generated, generated_by) tuple:
|
|
1740
|
-
- generated: True if file is detected as generated code.
|
|
1741
|
-
- generated_by: Lowercased family slug (openapi, jsonschema2pojo,
|
|
1742
|
-
protobuf, mapstruct, wsimport, querydsl, jooq, immutables,
|
|
1743
|
-
autovalue, lombok) or None if family unknown.
|
|
1744
|
-
"""
|
|
1745
|
-
# Load config if not provided
|
|
1746
|
-
if config is None:
|
|
1747
|
-
config = load_generated_detection(
|
|
1748
|
-
str(project_root) if project_root is not None else None
|
|
1749
|
-
)
|
|
1750
|
-
|
|
1751
|
-
# Decode header once for banner detection (first 4KB)
|
|
1752
|
-
HEADER_PREFIX_SIZE = 4096
|
|
1753
|
-
header_prefix = source[:HEADER_PREFIX_SIZE].decode("utf-8", errors="ignore")
|
|
1754
|
-
|
|
1755
|
-
# Collect all type FQNs for config-based checks
|
|
1756
|
-
all_type_fqns = {t.fqn for t in ast.all_types}
|
|
1757
|
-
|
|
1758
|
-
# Priority 1: exclude_fqns (override, wins even with markers)
|
|
1759
|
-
if all_type_fqns & config.exclude_fqns:
|
|
1760
|
-
return False, None
|
|
1761
|
-
|
|
1762
|
-
# Priority 2: force_fqns (forced generated, no markers needed)
|
|
1763
|
-
if all_type_fqns & config.force_fqns:
|
|
1764
|
-
return True, None
|
|
1765
|
-
|
|
1766
|
-
# Priority 3: annotation-based detection
|
|
1767
|
-
for typ in ast.all_types:
|
|
1768
|
-
for ann in typ.annotations:
|
|
1769
|
-
# Check simple name "Generated" first
|
|
1770
|
-
if ann.name == "Generated":
|
|
1771
|
-
family = _infer_family_from_annotation(ann)
|
|
1772
|
-
if family is not None:
|
|
1773
|
-
return True, family
|
|
1774
|
-
# Generic @Generated without identifiable value
|
|
1775
|
-
return True, None
|
|
1776
|
-
|
|
1777
|
-
# Check configured annotation patterns
|
|
1778
|
-
for pattern in config.annotation_patterns:
|
|
1779
|
-
if re.search(pattern, ann.qualified) or re.search(pattern, ann.name):
|
|
1780
|
-
return True, None
|
|
1781
|
-
|
|
1782
|
-
# Priority 4: header-banner detection
|
|
1783
|
-
family = _check_header_banners(header_prefix)
|
|
1784
|
-
if family is not None:
|
|
1785
|
-
return True, family
|
|
1786
|
-
|
|
1787
|
-
# Check configured header patterns
|
|
1788
|
-
for pattern in config.header_patterns:
|
|
1789
|
-
if re.search(pattern, header_prefix, re.IGNORECASE):
|
|
1790
|
-
return True, None
|
|
1791
|
-
|
|
1792
|
-
return False, None
|
|
1793
|
-
|
|
1794
|
-
|
|
1795
|
-
def detect_microservice_from_path(cwd: Path, source_root: Path) -> str | None:
|
|
1796
|
-
"""Detect microservice from cwd for query-time auto-scope.
|
|
1797
|
-
|
|
1798
|
-
Returns None if cwd is outside source_root, cwd IS source_root (system level),
|
|
1799
|
-
or no microservice is detected. Otherwise returns the microservice name.
|
|
1800
|
-
"""
|
|
1801
|
-
cwd_resolved = cwd.resolve()
|
|
1802
|
-
source_resolved = source_root.resolve()
|
|
1803
|
-
|
|
1804
|
-
# Check if cwd is outside source_root
|
|
1805
|
-
try:
|
|
1806
|
-
cwd_resolved.relative_to(source_resolved)
|
|
1807
|
-
except ValueError:
|
|
1808
|
-
return None
|
|
1809
|
-
|
|
1810
|
-
# Check if cwd IS source_root (at system level, no specific scope)
|
|
1811
|
-
if cwd_resolved == source_resolved:
|
|
1812
|
-
return None
|
|
1813
|
-
|
|
1814
|
-
# Check if cwd itself matches a YAML override (directory name matches microservice_roots)
|
|
1815
|
-
overrides = load_microservice_overrides(source_resolved)
|
|
1816
|
-
if overrides and cwd_resolved.name in overrides:
|
|
1817
|
-
return cwd_resolved.name
|
|
1818
|
-
|
|
1819
|
-
# microservice_for_path walks _bounded_parents which excludes the path itself.
|
|
1820
|
-
# For query-time detection we need cwd included in the walk, so pass a synthetic
|
|
1821
|
-
# child path so that cwd appears as a parent in the build-marker scan.
|
|
1822
|
-
synthetic = cwd_resolved / "__scope_probe__"
|
|
1823
|
-
ms = microservice_for_path(str(synthetic), source_resolved)
|
|
1824
|
-
return ms if ms else None
|
|
1825
|
-
|
|
1826
|
-
|
|
1827
|
-
# ---------- chunk enrichment ----------
|
|
1828
|
-
|
|
1829
|
-
|
|
1830
|
-
def _flatten_types(ast: JavaFileAst) -> list[TypeDecl]:
|
|
1831
|
-
return list(ast.all_types)
|
|
1832
|
-
|
|
1833
|
-
|
|
1834
|
-
def _enclosing_type(ast: JavaFileAst, start: int, end: int) -> TypeDecl | None:
|
|
1835
|
-
"""Smallest TypeDecl whose [start_byte, end_byte] contains chunk range.
|
|
1836
|
-
|
|
1837
|
-
Falls back to largest overlap if nothing fully encloses.
|
|
1838
|
-
"""
|
|
1839
|
-
best: TypeDecl | None = None
|
|
1840
|
-
best_span = -1
|
|
1841
|
-
for t in _flatten_types(ast):
|
|
1842
|
-
if t.start_byte <= start and end <= t.end_byte:
|
|
1843
|
-
span = t.end_byte - t.start_byte
|
|
1844
|
-
if best is None or span < best_span or best_span < 0:
|
|
1845
|
-
best = t
|
|
1846
|
-
best_span = span
|
|
1847
|
-
if best is not None:
|
|
1848
|
-
return best
|
|
1849
|
-
|
|
1850
|
-
overlap_best: TypeDecl | None = None
|
|
1851
|
-
overlap_size = 0
|
|
1852
|
-
for t in _flatten_types(ast):
|
|
1853
|
-
o = max(0, min(end, t.end_byte) - max(start, t.start_byte))
|
|
1854
|
-
if o > overlap_size:
|
|
1855
|
-
overlap_size = o
|
|
1856
|
-
overlap_best = t
|
|
1857
|
-
return overlap_best
|
|
1858
|
-
|
|
1859
|
-
|
|
1860
|
-
def _symbols_in_range(ast: JavaFileAst, start: int, end: int) -> list[str]:
|
|
1861
|
-
out: list[str] = []
|
|
1862
|
-
seen: set[str] = set()
|
|
1863
|
-
for t in _flatten_types(ast):
|
|
1864
|
-
if t.end_byte < start or t.start_byte > end:
|
|
1865
|
-
continue
|
|
1866
|
-
if start <= t.start_byte <= end and t.name not in seen:
|
|
1867
|
-
out.append(t.name)
|
|
1868
|
-
seen.add(t.name)
|
|
1869
|
-
for f in t.fields:
|
|
1870
|
-
if start <= f.start_byte <= end and f.name not in seen:
|
|
1871
|
-
out.append(f.name)
|
|
1872
|
-
seen.add(f.name)
|
|
1873
|
-
for m in t.methods:
|
|
1874
|
-
if start <= m.start_byte <= end and m.name not in seen:
|
|
1875
|
-
out.append(m.name)
|
|
1876
|
-
seen.add(m.name)
|
|
1877
|
-
return out
|
|
1878
|
-
|
|
1879
|
-
|
|
1880
|
-
def enrich_chunk(
|
|
1881
|
-
ast: JavaFileAst,
|
|
1882
|
-
*,
|
|
1883
|
-
chunk_start_byte: int,
|
|
1884
|
-
chunk_end_byte: int,
|
|
1885
|
-
file_path: str,
|
|
1886
|
-
project_root: str | Path | None = None,
|
|
1887
|
-
) -> ChunkEnrichment:
|
|
1888
|
-
"""Compute enrichment metadata for a single chunk of a parsed Java file."""
|
|
1889
|
-
module = module_for_path(file_path, project_root)
|
|
1890
|
-
microservice = microservice_for_path(file_path, project_root)
|
|
1891
|
-
encl = _enclosing_type(ast, chunk_start_byte, chunk_end_byte)
|
|
1892
|
-
if encl is not None:
|
|
1893
|
-
ann_names = [a.name for a in encl.annotations]
|
|
1894
|
-
prs: str | None = None
|
|
1895
|
-
if project_root is not None:
|
|
1896
|
-
try:
|
|
1897
|
-
prs = str(Path(project_root).resolve())
|
|
1898
|
-
except OSError:
|
|
1899
|
-
prs = str(project_root)
|
|
1900
|
-
bov = load_brownfield_overrides(project_root)
|
|
1901
|
-
mchain = collect_annotation_meta_chain(prs) if prs else None
|
|
1902
|
-
role, cap_list = resolve_role_and_capabilities(
|
|
1903
|
-
encl,
|
|
1904
|
-
overrides=bov,
|
|
1905
|
-
meta_chain=mchain,
|
|
1906
|
-
)
|
|
1907
|
-
return ChunkEnrichment(
|
|
1908
|
-
package=ast.package,
|
|
1909
|
-
module=module,
|
|
1910
|
-
microservice=microservice,
|
|
1911
|
-
primary_type_fqn=encl.fqn,
|
|
1912
|
-
primary_type_kind=encl.kind,
|
|
1913
|
-
role=role,
|
|
1914
|
-
annotations_on_type=ann_names,
|
|
1915
|
-
symbols=_symbols_in_range(ast, chunk_start_byte, chunk_end_byte),
|
|
1916
|
-
capabilities=cap_list,
|
|
1917
|
-
)
|
|
1918
|
-
return ChunkEnrichment(
|
|
1919
|
-
package=ast.package,
|
|
1920
|
-
module=module,
|
|
1921
|
-
microservice=microservice,
|
|
1922
|
-
primary_type_fqn="",
|
|
1923
|
-
primary_type_kind="",
|
|
1924
|
-
role="OTHER",
|
|
1925
|
-
annotations_on_type=[],
|
|
1926
|
-
symbols=_symbols_in_range(ast, chunk_start_byte, chunk_end_byte),
|
|
1927
|
-
capabilities=[],
|
|
1928
|
-
)
|
|
1929
|
-
|
|
1930
|
-
|
|
1931
|
-
def symbol_id(kind: str, fqn: str, file_path: str = "", start_byte: int = 0) -> str:
|
|
1932
|
-
"""Deterministic SHA1-based id for LadybugDB Symbol nodes."""
|
|
1933
|
-
key = f"{kind}|{fqn}|{file_path}|{start_byte}".encode("utf-8")
|
|
1934
|
-
return hashlib.sha1(key).hexdigest()
|
|
1935
|
-
|
|
1936
|
-
|
|
1937
|
-
def phantom_id(simple_or_fqn: str) -> str:
|
|
1938
|
-
"""Id for unresolved/external type targets (phantom Symbol rows)."""
|
|
1939
|
-
key = f"class|__phantom.{simple_or_fqn}|".encode("utf-8")
|
|
1940
|
-
return hashlib.sha1(key).hexdigest()
|