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,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()