sourcecode 1.35.30__tar.gz → 1.35.32__tar.gz

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 (109) hide show
  1. {sourcecode-1.35.30 → sourcecode-1.35.32}/PKG-INFO +3 -3
  2. {sourcecode-1.35.30 → sourcecode-1.35.32}/README.md +2 -2
  3. {sourcecode-1.35.30 → sourcecode-1.35.32}/pyproject.toml +1 -1
  4. {sourcecode-1.35.30 → sourcecode-1.35.32}/src/sourcecode/__init__.py +1 -1
  5. {sourcecode-1.35.30 → sourcecode-1.35.32}/src/sourcecode/file_chunker.py +54 -24
  6. {sourcecode-1.35.30 → sourcecode-1.35.32}/src/sourcecode/repository_ir.py +45 -7
  7. {sourcecode-1.35.30 → sourcecode-1.35.32}/src/sourcecode/spring_impact.py +7 -1
  8. {sourcecode-1.35.30 → sourcecode-1.35.32}/src/sourcecode/spring_model.py +30 -3
  9. {sourcecode-1.35.30 → sourcecode-1.35.32}/src/sourcecode/spring_semantic.py +34 -7
  10. {sourcecode-1.35.30 → sourcecode-1.35.32}/.github/workflows/build-windows.yml +0 -0
  11. {sourcecode-1.35.30 → sourcecode-1.35.32}/.gitignore +0 -0
  12. {sourcecode-1.35.30 → sourcecode-1.35.32}/.ruff.toml +0 -0
  13. {sourcecode-1.35.30 → sourcecode-1.35.32}/CHANGELOG.md +0 -0
  14. {sourcecode-1.35.30 → sourcecode-1.35.32}/CONTRIBUTING.md +0 -0
  15. {sourcecode-1.35.30 → sourcecode-1.35.32}/LICENSE +0 -0
  16. {sourcecode-1.35.30 → sourcecode-1.35.32}/SECURITY.md +0 -0
  17. {sourcecode-1.35.30 → sourcecode-1.35.32}/raw +0 -0
  18. {sourcecode-1.35.30 → sourcecode-1.35.32}/src/sourcecode/adaptive_scanner.py +0 -0
  19. {sourcecode-1.35.30 → sourcecode-1.35.32}/src/sourcecode/architecture_analyzer.py +0 -0
  20. {sourcecode-1.35.30 → sourcecode-1.35.32}/src/sourcecode/architecture_summary.py +0 -0
  21. {sourcecode-1.35.30 → sourcecode-1.35.32}/src/sourcecode/ast_extractor.py +0 -0
  22. {sourcecode-1.35.30 → sourcecode-1.35.32}/src/sourcecode/cache.py +0 -0
  23. {sourcecode-1.35.30 → sourcecode-1.35.32}/src/sourcecode/canonical_ir.py +0 -0
  24. {sourcecode-1.35.30 → sourcecode-1.35.32}/src/sourcecode/cir_graphs.py +0 -0
  25. {sourcecode-1.35.30 → sourcecode-1.35.32}/src/sourcecode/classifier.py +0 -0
  26. {sourcecode-1.35.30 → sourcecode-1.35.32}/src/sourcecode/cli.py +0 -0
  27. {sourcecode-1.35.30 → sourcecode-1.35.32}/src/sourcecode/code_notes_analyzer.py +0 -0
  28. {sourcecode-1.35.30 → sourcecode-1.35.32}/src/sourcecode/confidence_analyzer.py +0 -0
  29. {sourcecode-1.35.30 → sourcecode-1.35.32}/src/sourcecode/context_scorer.py +0 -0
  30. {sourcecode-1.35.30 → sourcecode-1.35.32}/src/sourcecode/context_summarizer.py +0 -0
  31. {sourcecode-1.35.30 → sourcecode-1.35.32}/src/sourcecode/contract_model.py +0 -0
  32. {sourcecode-1.35.30 → sourcecode-1.35.32}/src/sourcecode/contract_pipeline.py +0 -0
  33. {sourcecode-1.35.30 → sourcecode-1.35.32}/src/sourcecode/coverage_parser.py +0 -0
  34. {sourcecode-1.35.30 → sourcecode-1.35.32}/src/sourcecode/dependency_analyzer.py +0 -0
  35. {sourcecode-1.35.30 → sourcecode-1.35.32}/src/sourcecode/detectors/__init__.py +0 -0
  36. {sourcecode-1.35.30 → sourcecode-1.35.32}/src/sourcecode/detectors/base.py +0 -0
  37. {sourcecode-1.35.30 → sourcecode-1.35.32}/src/sourcecode/detectors/csproj_parser.py +0 -0
  38. {sourcecode-1.35.30 → sourcecode-1.35.32}/src/sourcecode/detectors/dart.py +0 -0
  39. {sourcecode-1.35.30 → sourcecode-1.35.32}/src/sourcecode/detectors/dotnet.py +0 -0
  40. {sourcecode-1.35.30 → sourcecode-1.35.32}/src/sourcecode/detectors/elixir.py +0 -0
  41. {sourcecode-1.35.30 → sourcecode-1.35.32}/src/sourcecode/detectors/go.py +0 -0
  42. {sourcecode-1.35.30 → sourcecode-1.35.32}/src/sourcecode/detectors/heuristic.py +0 -0
  43. {sourcecode-1.35.30 → sourcecode-1.35.32}/src/sourcecode/detectors/hybrid.py +0 -0
  44. {sourcecode-1.35.30 → sourcecode-1.35.32}/src/sourcecode/detectors/java.py +0 -0
  45. {sourcecode-1.35.30 → sourcecode-1.35.32}/src/sourcecode/detectors/jvm_ext.py +0 -0
  46. {sourcecode-1.35.30 → sourcecode-1.35.32}/src/sourcecode/detectors/nodejs.py +0 -0
  47. {sourcecode-1.35.30 → sourcecode-1.35.32}/src/sourcecode/detectors/parsers.py +0 -0
  48. {sourcecode-1.35.30 → sourcecode-1.35.32}/src/sourcecode/detectors/php.py +0 -0
  49. {sourcecode-1.35.30 → sourcecode-1.35.32}/src/sourcecode/detectors/project.py +0 -0
  50. {sourcecode-1.35.30 → sourcecode-1.35.32}/src/sourcecode/detectors/python.py +0 -0
  51. {sourcecode-1.35.30 → sourcecode-1.35.32}/src/sourcecode/detectors/ruby.py +0 -0
  52. {sourcecode-1.35.30 → sourcecode-1.35.32}/src/sourcecode/detectors/rust.py +0 -0
  53. {sourcecode-1.35.30 → sourcecode-1.35.32}/src/sourcecode/detectors/systems.py +0 -0
  54. {sourcecode-1.35.30 → sourcecode-1.35.32}/src/sourcecode/detectors/terraform.py +0 -0
  55. {sourcecode-1.35.30 → sourcecode-1.35.32}/src/sourcecode/detectors/tooling.py +0 -0
  56. {sourcecode-1.35.30 → sourcecode-1.35.32}/src/sourcecode/doc_analyzer.py +0 -0
  57. {sourcecode-1.35.30 → sourcecode-1.35.32}/src/sourcecode/entrypoint_classifier.py +0 -0
  58. {sourcecode-1.35.30 → sourcecode-1.35.32}/src/sourcecode/env_analyzer.py +0 -0
  59. {sourcecode-1.35.30 → sourcecode-1.35.32}/src/sourcecode/error_schema.py +0 -0
  60. {sourcecode-1.35.30 → sourcecode-1.35.32}/src/sourcecode/explain.py +0 -0
  61. {sourcecode-1.35.30 → sourcecode-1.35.32}/src/sourcecode/file_classifier.py +0 -0
  62. {sourcecode-1.35.30 → sourcecode-1.35.32}/src/sourcecode/flow_analyzer.py +0 -0
  63. {sourcecode-1.35.30 → sourcecode-1.35.32}/src/sourcecode/fqn_utils.py +0 -0
  64. {sourcecode-1.35.30 → sourcecode-1.35.32}/src/sourcecode/git_analyzer.py +0 -0
  65. {sourcecode-1.35.30 → sourcecode-1.35.32}/src/sourcecode/graph_analyzer.py +0 -0
  66. {sourcecode-1.35.30 → sourcecode-1.35.32}/src/sourcecode/license.py +0 -0
  67. {sourcecode-1.35.30 → sourcecode-1.35.32}/src/sourcecode/mcp/__init__.py +0 -0
  68. {sourcecode-1.35.30 → sourcecode-1.35.32}/src/sourcecode/mcp/onboarding/__init__.py +0 -0
  69. {sourcecode-1.35.30 → sourcecode-1.35.32}/src/sourcecode/mcp/onboarding/applier.py +0 -0
  70. {sourcecode-1.35.30 → sourcecode-1.35.32}/src/sourcecode/mcp/onboarding/backup.py +0 -0
  71. {sourcecode-1.35.30 → sourcecode-1.35.32}/src/sourcecode/mcp/onboarding/detector.py +0 -0
  72. {sourcecode-1.35.30 → sourcecode-1.35.32}/src/sourcecode/mcp/onboarding/planner.py +0 -0
  73. {sourcecode-1.35.30 → sourcecode-1.35.32}/src/sourcecode/mcp/orchestrator.py +0 -0
  74. {sourcecode-1.35.30 → sourcecode-1.35.32}/src/sourcecode/mcp/registry.py +0 -0
  75. {sourcecode-1.35.30 → sourcecode-1.35.32}/src/sourcecode/mcp/runner.py +0 -0
  76. {sourcecode-1.35.30 → sourcecode-1.35.32}/src/sourcecode/mcp/server.py +0 -0
  77. {sourcecode-1.35.30 → sourcecode-1.35.32}/src/sourcecode/mcp_nudge.py +0 -0
  78. {sourcecode-1.35.30 → sourcecode-1.35.32}/src/sourcecode/metrics_analyzer.py +0 -0
  79. {sourcecode-1.35.30 → sourcecode-1.35.32}/src/sourcecode/migrate_check.py +0 -0
  80. {sourcecode-1.35.30 → sourcecode-1.35.32}/src/sourcecode/output_budget.py +0 -0
  81. {sourcecode-1.35.30 → sourcecode-1.35.32}/src/sourcecode/path_filters.py +0 -0
  82. {sourcecode-1.35.30 → sourcecode-1.35.32}/src/sourcecode/pr_comment_renderer.py +0 -0
  83. {sourcecode-1.35.30 → sourcecode-1.35.32}/src/sourcecode/pr_impact.py +0 -0
  84. {sourcecode-1.35.30 → sourcecode-1.35.32}/src/sourcecode/prepare_context.py +0 -0
  85. {sourcecode-1.35.30 → sourcecode-1.35.32}/src/sourcecode/progress.py +0 -0
  86. {sourcecode-1.35.30 → sourcecode-1.35.32}/src/sourcecode/ranking_engine.py +0 -0
  87. {sourcecode-1.35.30 → sourcecode-1.35.32}/src/sourcecode/redactor.py +0 -0
  88. {sourcecode-1.35.30 → sourcecode-1.35.32}/src/sourcecode/relevance_scorer.py +0 -0
  89. {sourcecode-1.35.30 → sourcecode-1.35.32}/src/sourcecode/rename_refactor.py +0 -0
  90. {sourcecode-1.35.30 → sourcecode-1.35.32}/src/sourcecode/repo_classifier.py +0 -0
  91. {sourcecode-1.35.30 → sourcecode-1.35.32}/src/sourcecode/ris.py +0 -0
  92. {sourcecode-1.35.30 → sourcecode-1.35.32}/src/sourcecode/runtime_classifier.py +0 -0
  93. {sourcecode-1.35.30 → sourcecode-1.35.32}/src/sourcecode/scanner.py +0 -0
  94. {sourcecode-1.35.30 → sourcecode-1.35.32}/src/sourcecode/schema.py +0 -0
  95. {sourcecode-1.35.30 → sourcecode-1.35.32}/src/sourcecode/semantic_analyzer.py +0 -0
  96. {sourcecode-1.35.30 → sourcecode-1.35.32}/src/sourcecode/serializer.py +0 -0
  97. {sourcecode-1.35.30 → sourcecode-1.35.32}/src/sourcecode/spring_event_topology.py +0 -0
  98. {sourcecode-1.35.30 → sourcecode-1.35.32}/src/sourcecode/spring_findings.py +0 -0
  99. {sourcecode-1.35.30 → sourcecode-1.35.32}/src/sourcecode/spring_security_audit.py +0 -0
  100. {sourcecode-1.35.30 → sourcecode-1.35.32}/src/sourcecode/spring_tx_analyzer.py +0 -0
  101. {sourcecode-1.35.30 → sourcecode-1.35.32}/src/sourcecode/summarizer.py +0 -0
  102. {sourcecode-1.35.30 → sourcecode-1.35.32}/src/sourcecode/telemetry/__init__.py +0 -0
  103. {sourcecode-1.35.30 → sourcecode-1.35.32}/src/sourcecode/telemetry/config.py +0 -0
  104. {sourcecode-1.35.30 → sourcecode-1.35.32}/src/sourcecode/telemetry/consent.py +0 -0
  105. {sourcecode-1.35.30 → sourcecode-1.35.32}/src/sourcecode/telemetry/events.py +0 -0
  106. {sourcecode-1.35.30 → sourcecode-1.35.32}/src/sourcecode/telemetry/filters.py +0 -0
  107. {sourcecode-1.35.30 → sourcecode-1.35.32}/src/sourcecode/telemetry/transport.py +0 -0
  108. {sourcecode-1.35.30 → sourcecode-1.35.32}/src/sourcecode/tree_utils.py +0 -0
  109. {sourcecode-1.35.30 → sourcecode-1.35.32}/src/sourcecode/workspace.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: sourcecode
3
- Version: 1.35.30
3
+ Version: 1.35.32
4
4
  Summary: Persistent structural context and ultra-fast repeated analysis for AI coding agents
5
5
  License-File: LICENSE
6
6
  Keywords: agents,ai,codebase,context,developer-tools,llm
@@ -40,7 +40,7 @@ Description-Content-Type: text/markdown
40
40
 
41
41
  **Persistent structural context and ultra-fast repeated analysis for AI coding agents.**
42
42
 
43
- ![Version](https://img.shields.io/badge/version-1.35.30-blue)
43
+ ![Version](https://img.shields.io/badge/version-1.35.32-blue)
44
44
  ![Python](https://img.shields.io/badge/python-3.10%2B-green)
45
45
 
46
46
  ---
@@ -114,7 +114,7 @@ pipx install sourcecode
114
114
 
115
115
  ```bash
116
116
  sourcecode version
117
- # sourcecode 1.35.30
117
+ # sourcecode 1.35.32
118
118
 
119
119
  **v1.35.28** — 7 bug fixes: `rename-class` cross-package disambiguation (BUG-4), `rename-class` collision detection (BUG-2), `find_java_files` false positive on `com/test/` package paths (BUG-1), `cold-start --compact` correct key names (BUG-6), `@EnableMethodSecurity` no longer suppresses SEC-001 (BUG-3), `explain` @Entity stereotype detection (BUG-5), XML+annotation mixed security retagging (BUG-7).
120
120
  ```
@@ -2,7 +2,7 @@
2
2
 
3
3
  **Persistent structural context and ultra-fast repeated analysis for AI coding agents.**
4
4
 
5
- ![Version](https://img.shields.io/badge/version-1.35.30-blue)
5
+ ![Version](https://img.shields.io/badge/version-1.35.32-blue)
6
6
  ![Python](https://img.shields.io/badge/python-3.10%2B-green)
7
7
 
8
8
  ---
@@ -76,7 +76,7 @@ pipx install sourcecode
76
76
 
77
77
  ```bash
78
78
  sourcecode version
79
- # sourcecode 1.35.30
79
+ # sourcecode 1.35.32
80
80
 
81
81
  **v1.35.28** — 7 bug fixes: `rename-class` cross-package disambiguation (BUG-4), `rename-class` collision detection (BUG-2), `find_java_files` false positive on `com/test/` package paths (BUG-1), `cold-start --compact` correct key names (BUG-6), `@EnableMethodSecurity` no longer suppresses SEC-001 (BUG-3), `explain` @Entity stereotype detection (BUG-5), XML+annotation mixed security retagging (BUG-7).
82
82
  ```
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "sourcecode"
7
- version = "1.35.30"
7
+ version = "1.35.32"
8
8
  description = "Persistent structural context and ultra-fast repeated analysis for AI coding agents"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.9"
@@ -1,3 +1,3 @@
1
1
  """sourcecode — Deterministic codebase context maps for AI coding agents."""
2
2
 
3
- __version__ = "1.35.30"
3
+ __version__ = "1.35.32"
@@ -94,6 +94,7 @@ _CLASS_RE = re.compile(
94
94
  )
95
95
  _METHOD_RE = re.compile(
96
96
  r'^\s*(?:(?:public|protected|private|static|final|synchronized|abstract|native|default|override)\s+)*'
97
+ r'(?:@\w+(?:\([^)]*\))?\s+)*' # optional return-type annotations (e.g. @ResponseBody)
97
98
  r'(?:<[^>]+>\s+)?' # optional generic return type
98
99
  r'(?:[\w<>\[\],?\s]+\s+)?' # return type (optional for constructors)
99
100
  r'(\w+)\s*\(' # method/constructor name + opening paren
@@ -288,6 +289,8 @@ def chunk_java_file(
288
289
  current_method_name = ""
289
290
  current_method_type = ""
290
291
  method_brace_start_depth = -1
292
+ # State for multi-line method signatures (name detected, waiting for opening '{')
293
+ _pending_method: Optional[tuple[str, str]] = None # (name, type)
291
294
 
292
295
  for line_no_0, raw_line in enumerate(all_lines):
293
296
  line_no = line_no_0 + 1 # 1-based
@@ -315,32 +318,16 @@ def chunk_java_file(
315
318
 
316
319
  # Check if this line starts a method/constructor AT class_depth+1
317
320
  if class_depth >= 0 and depth == class_depth + 1 and not current_method_name:
318
- is_method, mname, mtype = _is_method_or_constructor_start(
319
- stripped, class_name, depth, class_depth
320
- )
321
- if is_method and "{" in raw_line:
322
- # Flush anything accumulated as field_block / class_header
323
- # Include annotation lines in the new method chunk
324
- if pending_lines:
325
- # Check if last N pending lines are annotations for this method
326
- # Flush everything up to ann_buffer start
327
- if ann_buffer:
328
- ann_start_line = ann_buffer[0][0]
329
- pre_ann_lines = pending_lines[:ann_start_line - pending_start]
330
- if pre_ann_lines:
331
- _flush_chunk(ann_start_line - 1)
332
- # Move ann_buffer lines into the new method chunk
333
- pending_start = ann_start_line
334
- pending_lines = [al for _, al in ann_buffer]
335
- ann_buffer = []
336
- else:
337
- _flush_chunk(line_no - 1)
338
- pending_start = line_no
339
- pending_lines = []
340
-
321
+ # Multi-line signature: we already detected the method name; this line
322
+ # should contain the opening brace that starts the method body.
323
+ # Guard: opens > closes ensures a net depth increase (not a balanced
324
+ # annotation arg like @RequestMapping({"v1","v2"})).
325
+ if _pending_method and opens > closes:
326
+ mname, mtype = _pending_method
327
+ _pending_method = None
341
328
  current_method_name = mname
342
329
  current_method_type = mtype
343
- method_brace_start_depth = depth + opens - 1 # depth entering method body
330
+ method_brace_start_depth = depth + opens - 1
344
331
  pending_type = mtype
345
332
  pending_symbol = f"{class_name}#{mname}" if class_name else mname
346
333
  pending_lines.append(raw_line)
@@ -348,6 +335,49 @@ def chunk_java_file(
348
335
  ann_buffer = []
349
336
  continue
350
337
 
338
+ if not _pending_method:
339
+ is_method, mname, mtype = _is_method_or_constructor_start(
340
+ stripped, class_name, depth, class_depth
341
+ )
342
+ if is_method:
343
+ # Flush anything accumulated as field_block / class_header
344
+ # Include annotation lines in the new method chunk
345
+ if pending_lines:
346
+ if ann_buffer:
347
+ ann_start_line = ann_buffer[0][0]
348
+ pre_ann_lines = pending_lines[:ann_start_line - pending_start]
349
+ if pre_ann_lines:
350
+ _flush_chunk(ann_start_line - 1)
351
+ # Move ann_buffer lines into the new method chunk
352
+ pending_start = ann_start_line
353
+ pending_lines = [al for _, al in ann_buffer]
354
+ ann_buffer = []
355
+ else:
356
+ _flush_chunk(line_no - 1)
357
+ pending_start = line_no
358
+ pending_lines = []
359
+
360
+ if "{" in raw_line:
361
+ # Single-line signature: method body opens on same line
362
+ current_method_name = mname
363
+ current_method_type = mtype
364
+ method_brace_start_depth = depth + opens - 1
365
+ pending_type = mtype
366
+ pending_symbol = f"{class_name}#{mname}" if class_name else mname
367
+ pending_lines.append(raw_line)
368
+ depth += opens - closes
369
+ ann_buffer = []
370
+ continue
371
+ else:
372
+ # Multi-line signature: record name, wait for '{' on later line
373
+ _pending_method = (mname, mtype)
374
+ pending_type = mtype
375
+ pending_symbol = f"{class_name}#{mname}" if class_name else mname
376
+ pending_lines.append(raw_line)
377
+ depth += opens - closes
378
+ ann_buffer = []
379
+ continue
380
+
351
381
  # Update depth
352
382
  depth += opens - closes
353
383
 
@@ -2756,8 +2756,8 @@ def _build_route_surface(
2756
2756
  _route_entry["security_annotations"] = _sec
2757
2757
  routes.append(_route_entry)
2758
2758
 
2759
- # Phase 3: inheritance projection — subclasses with zero own endpoints
2760
- # but with a class-level @RequestMapping prefix inherit parent methods.
2759
+ # Phase 3: inheritance projection — subclasses with a class-level @RequestMapping
2760
+ # prefix inherit parent methods that they do not override (same HTTP verb + path suffix).
2761
2761
  if extends_map:
2762
2762
  fqn_to_simple: dict[str, str] = {d["fqn"]: s for s, d in class_info.items()}
2763
2763
  simple_extends: dict[str, str] = {
@@ -2771,11 +2771,14 @@ def _build_route_surface(
2771
2771
  }
2772
2772
 
2773
2773
  for cls_simple, data in class_info.items():
2774
- if data["own_endpoints"]:
2775
- continue
2776
2774
  if not any(data["prefixes"]):
2777
2775
  continue
2778
2776
 
2777
+ # (verb, suffix) pairs declared on this subclass — these shadow parent methods.
2778
+ own_override_set: set[tuple[str, str]] = {
2779
+ (verb, suffix) for verb, suffix, _, _ in data["own_endpoints"]
2780
+ }
2781
+
2779
2782
  chain = simple_extends.get(cls_simple)
2780
2783
  visited: set[str] = {cls_simple}
2781
2784
  depth = 1
@@ -2786,6 +2789,9 @@ def _build_route_surface(
2786
2789
  break
2787
2790
  if parent["own_endpoints"]:
2788
2791
  for verb, suffix, declaring_sym, stable_id in parent["own_endpoints"]:
2792
+ # Skip methods the subclass overrides (same verb + path suffix).
2793
+ if (verb, suffix) in own_override_set:
2794
+ continue
2789
2795
  for prefix in data["prefixes"]:
2790
2796
  # P1 fix: collapse any number of consecutive slashes
2791
2797
  full_path = re.sub(r"/+", "/", prefix + "/" + suffix).rstrip("/") or "/"
@@ -2903,6 +2909,24 @@ def build_repo_ir(
2903
2909
  # Spring Data
2904
2910
  '@Query', '@NamedQuery',
2905
2911
  )
2912
+ # Pre-pass: collect custom meta-annotation names from @interface definitions
2913
+ # that compose known Spring stereotypes (e.g. @DomainService = @Service + @Transactional).
2914
+ # These names must be added to the marker set so classes using them aren't
2915
+ # filtered out by the fast pre-scan below.
2916
+ _custom_meta_markers: set[str] = set()
2917
+ for _rp in sorted(file_paths):
2918
+ try:
2919
+ _src = (root / _rp).read_text(encoding="utf-8", errors="replace")
2920
+ except OSError:
2921
+ continue
2922
+ if "@interface" not in _src:
2923
+ continue
2924
+ if not any(m in _src for m in _ANNOTATION_MARKERS):
2925
+ continue
2926
+ for _m in re.finditer(r'@interface\s+(\w+)', _src):
2927
+ _custom_meta_markers.add(f"@{_m.group(1)}")
2928
+ _effective_markers = _ANNOTATION_MARKERS + tuple(_custom_meta_markers)
2929
+
2906
2930
  _per_file: list[tuple[str, str, str, list[str], list[SymbolRecord]]] = []
2907
2931
  for rel_path in sorted(file_paths):
2908
2932
  abs_path = root / rel_path
@@ -2915,7 +2939,7 @@ def build_repo_ir(
2915
2939
  _meta_chars_read += len(source)
2916
2940
  # Fast pre-scan: if file has no relevant annotations skip full extraction.
2917
2941
  # Still register package/class name for same-package resolution.
2918
- if not any(marker in source for marker in _ANNOTATION_MARKERS):
2942
+ if not any(marker in source for marker in _effective_markers):
2919
2943
  pkg_m = _PKG_RE.search(source)
2920
2944
  _pkg = pkg_m.group(1) if pkg_m else ""
2921
2945
  # Minimal class-name symbols for same-package map (no methods/fields)
@@ -4232,6 +4256,16 @@ def _resolve_target(
4232
4256
  return "not_found", set()
4233
4257
 
4234
4258
 
4259
+ _BLAST_SKIP_EDGE_TYPES: frozenset[str] = frozenset({"contained_in", "imports"})
4260
+ # 'contained_in': structural membership (method→enclosing class), not a caller.
4261
+ # 'imports': Java import statements — any class that references the type in
4262
+ # an import declaration, including those that only use it as a
4263
+ # method-return type or catch-block type. Import presence does NOT
4264
+ # imply a runtime dependency; including it produces false-positive
4265
+ # callers (e.g. sibling service classes that share a utility interface).
4266
+ # Consistent with spring_impact._SKIP_EDGE_TYPES.
4267
+
4268
+
4235
4269
  def _all_callers_from_rg(fqn: str, reverse_graph: dict[str, dict[str, list[str]]]) -> list[str]:
4236
4270
  """Return all callers of fqn from the reverse graph (all edge types).
4237
4271
 
@@ -4242,13 +4276,17 @@ def _all_callers_from_rg(fqn: str, reverse_graph: dict[str, dict[str, list[str]]
4242
4276
  CH-002 fix: for 'injects' edges, normalize field/constructor FQNs to their
4243
4277
  enclosing class. e.g. pkg.ConsolidacionService.calcularField → pkg.ConsolidacionService
4244
4278
  so BFS can continue through DI injection chains and find controllers.
4279
+
4280
+ FP-001 fix: skip 'imports' edges — import declarations are not runtime
4281
+ dependencies and produce false-positive callers (e.g. sibling classes that share
4282
+ a utility interface but don't call the target).
4245
4283
  """
4246
4284
  entry = reverse_graph.get(fqn) or {}
4247
4285
  callers: list[str] = []
4248
4286
  seen: set[str] = set()
4249
4287
  for edge_type, fqn_list in entry.items():
4250
- if edge_type == "contained_in":
4251
- continue # structural membership, not a caller
4288
+ if edge_type in _BLAST_SKIP_EDGE_TYPES:
4289
+ continue
4252
4290
  for c in fqn_list:
4253
4291
  normalized = _normalize_owner_fqn(c) if edge_type == "injects" else c
4254
4292
  if normalized not in seen:
@@ -570,6 +570,9 @@ class ImpactOrchestrator:
570
570
  # When the queried class is an interface, BFS should also start from
571
571
  # implementation symbols so TX boundaries and callers on the impl are found.
572
572
  impl_graph = getattr(cir, "implementation_graph", None)
573
+ # Track original seed classes BEFORE CH-001a expansion so CH-001b does not
574
+ # cascade through interfaces shared by impl classes added here (false positives).
575
+ original_seed_classes: set[str] = {_class_of(s) for s in seed_fqns}
573
576
  if impl_graph is not None:
574
577
  seed_classes_ch001 = {_class_of(s) for s in seed_fqns}
575
578
  impl_seeds: list[str] = []
@@ -598,11 +601,14 @@ class ImpactOrchestrator:
598
601
  # Callers typically inject the interface type, so reverse-graph edges live on
599
602
  # the interface node, not on the implementation node. Without this expansion,
600
603
  # querying 'OrderServiceImpl' finds 0 callers even though 36 classes inject it.
604
+ # IMPORTANT: only expand ORIGINAL user-query seeds, not classes added by CH-001a.
605
+ # Expanding CH-001a-added impls cascades through shared utility interfaces
606
+ # (e.g. RefByUuid) and produces false-positive callers from sibling implementors.
601
607
  if impl_graph is not None:
602
608
  current_seed_classes = {_class_of(s) for s in seed_fqns}
603
609
  iface_seeds: list[str] = []
604
610
  iface_classes_added: set[str] = set()
605
- for seed_class in sorted(current_seed_classes):
611
+ for seed_class in sorted(original_seed_classes):
606
612
  ifaces = impl_graph.interfaces_of(seed_class)
607
613
  for iface_class in ifaces:
608
614
  if iface_class in iface_classes_added or iface_class in current_seed_classes:
@@ -153,18 +153,45 @@ class BeanGraph:
153
153
  raw_ir = getattr(cir, "_raw_ir", {}) or {}
154
154
  nodes = (raw_ir.get("graph") or {}).get("nodes") or []
155
155
 
156
+ # Pass 1: build meta-bean-annotation map from annotation-type nodes.
157
+ # e.g. @DomainService (annotated with @Service) maps "@DomainService" → "service"
158
+ _meta_bean_stereotype: dict[str, str] = {}
156
159
  for node in nodes:
157
160
  if not isinstance(node, dict):
158
161
  continue
162
+ if (node.get("symbol_kind") or node.get("type") or "") != "annotation":
163
+ continue
164
+ _ann_set = set(node.get("annotations") or [])
165
+ _match = _ann_set & _BEAN_ANNOTATIONS
166
+ if not _match:
167
+ continue
168
+ _fqn = node.get("fqn") or ""
169
+ if not _fqn:
170
+ continue
171
+ _simple = "@" + _fqn.split(".")[-1]
172
+ _bean_ann = next(iter(_match))
173
+ _meta_bean_stereotype[_simple] = _bean_ann.lstrip("@").lower()
174
+
175
+ # Pass 2: collect all bean nodes (direct or via meta-annotation).
176
+ for node in nodes:
177
+ if not isinstance(node, dict):
178
+ continue
179
+ if (node.get("symbol_kind") or node.get("type") or "") == "annotation":
180
+ continue # annotation-type nodes are not beans
159
181
  ann_set = set(node.get("annotations") or [])
160
182
  match = ann_set & _BEAN_ANNOTATIONS
161
183
  if not match:
162
- continue
184
+ meta_match = ann_set & set(_meta_bean_stereotype)
185
+ if not meta_match:
186
+ continue
187
+ ann = next(iter(meta_match))
188
+ stereotype = _meta_bean_stereotype[ann]
189
+ else:
190
+ ann = next(iter(match))
191
+ stereotype = ann.lstrip("@").lower()
163
192
  fqn = node.get("fqn") or ""
164
193
  if not fqn:
165
194
  continue
166
- ann = next(iter(match))
167
- stereotype = ann.lstrip("@").lower()
168
195
  beans[fqn] = BeanNode(
169
196
  fqn=fqn,
170
197
  stereotype=stereotype,
@@ -293,26 +293,53 @@ def build_tx_index(cir: "CanonicalRepositoryIR") -> TransactionBoundaryIndex:
293
293
  graph = raw_ir.get("graph") or {}
294
294
  nodes = graph.get("nodes") or []
295
295
 
296
+ # Pass 1: build meta-@Transactional map from annotation-type nodes.
297
+ # e.g. @ReadOnlyTransaction (annotated with @Transactional(readOnly=true))
298
+ # maps "@ReadOnlyTransaction" → "readOnly = true"
299
+ _meta_tx_args: dict[str, str] = {}
296
300
  for node in nodes:
297
301
  if not isinstance(node, dict):
298
302
  continue
303
+ if (node.get("symbol_kind") or node.get("type") or "") != "annotation":
304
+ continue
305
+ ann_set = set(node.get("annotations") or [])
306
+ if "@Transactional" not in ann_set:
307
+ continue
308
+ _fqn = node.get("fqn") or node.get("symbol") or ""
309
+ if not _fqn:
310
+ continue
311
+ _simple = "@" + _fqn.split(".")[-1]
312
+ _av = node.get("annotation_values") or {}
313
+ _meta_tx_args[_simple] = _av.get("@Transactional", "")
299
314
 
300
- annotations = node.get("annotations") or []
301
- if "@Transactional" not in annotations:
315
+ # Pass 2: index all @Transactional boundaries (direct or via meta-annotation).
316
+ for node in nodes:
317
+ if not isinstance(node, dict):
302
318
  continue
303
319
 
320
+ symbol_kind = node.get("symbol_kind") or node.get("type") or ""
321
+ if symbol_kind == "annotation":
322
+ continue # annotation-type nodes are not TX boundaries
323
+
324
+ annotations = node.get("annotations") or []
325
+ ann_values = node.get("annotation_values") or {}
326
+
327
+ if "@Transactional" in annotations:
328
+ raw_args = ann_values.get("@Transactional", "")
329
+ else:
330
+ # Check meta-annotations (composed @Transactional)
331
+ meta_key = next((a for a in annotations if a in _meta_tx_args), None)
332
+ if meta_key is None:
333
+ continue
334
+ raw_args = _meta_tx_args[meta_key]
335
+
304
336
  fqn = node.get("fqn") or node.get("symbol") or ""
305
337
  if not fqn:
306
338
  continue
307
339
 
308
- symbol_kind = node.get("symbol_kind") or node.get("type") or ""
309
340
  source_file = node.get("source_file") or node.get("declaring_file") or ""
310
341
  modifiers = node.get("modifiers") or []
311
342
 
312
- # annotation_values is stored per-symbol in the graph node
313
- ann_values = node.get("annotation_values") or {}
314
- raw_args = ann_values.get("@Transactional", "")
315
-
316
343
  # Determine scope: class-level or method-level
317
344
  if symbol_kind in ("class", "interface", "enum"):
318
345
  scope = "class"
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes