sourcecode 1.35.27__tar.gz → 1.35.28__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.27 → sourcecode-1.35.28}/PKG-INFO +5 -3
  2. {sourcecode-1.35.27 → sourcecode-1.35.28}/README.md +4 -2
  3. {sourcecode-1.35.27 → sourcecode-1.35.28}/pyproject.toml +1 -1
  4. {sourcecode-1.35.27 → sourcecode-1.35.28}/src/sourcecode/__init__.py +1 -1
  5. {sourcecode-1.35.27 → sourcecode-1.35.28}/src/sourcecode/cli.py +6 -3
  6. {sourcecode-1.35.27 → sourcecode-1.35.28}/src/sourcecode/explain.py +3 -0
  7. {sourcecode-1.35.27 → sourcecode-1.35.28}/src/sourcecode/rename_refactor.py +58 -6
  8. {sourcecode-1.35.27 → sourcecode-1.35.28}/src/sourcecode/repository_ir.py +59 -7
  9. {sourcecode-1.35.27 → sourcecode-1.35.28}/src/sourcecode/spring_model.py +2 -0
  10. {sourcecode-1.35.27 → sourcecode-1.35.28}/.github/workflows/build-windows.yml +0 -0
  11. {sourcecode-1.35.27 → sourcecode-1.35.28}/.gitignore +0 -0
  12. {sourcecode-1.35.27 → sourcecode-1.35.28}/.ruff.toml +0 -0
  13. {sourcecode-1.35.27 → sourcecode-1.35.28}/CHANGELOG.md +0 -0
  14. {sourcecode-1.35.27 → sourcecode-1.35.28}/CONTRIBUTING.md +0 -0
  15. {sourcecode-1.35.27 → sourcecode-1.35.28}/LICENSE +0 -0
  16. {sourcecode-1.35.27 → sourcecode-1.35.28}/SECURITY.md +0 -0
  17. {sourcecode-1.35.27 → sourcecode-1.35.28}/raw +0 -0
  18. {sourcecode-1.35.27 → sourcecode-1.35.28}/src/sourcecode/adaptive_scanner.py +0 -0
  19. {sourcecode-1.35.27 → sourcecode-1.35.28}/src/sourcecode/architecture_analyzer.py +0 -0
  20. {sourcecode-1.35.27 → sourcecode-1.35.28}/src/sourcecode/architecture_summary.py +0 -0
  21. {sourcecode-1.35.27 → sourcecode-1.35.28}/src/sourcecode/ast_extractor.py +0 -0
  22. {sourcecode-1.35.27 → sourcecode-1.35.28}/src/sourcecode/cache.py +0 -0
  23. {sourcecode-1.35.27 → sourcecode-1.35.28}/src/sourcecode/canonical_ir.py +0 -0
  24. {sourcecode-1.35.27 → sourcecode-1.35.28}/src/sourcecode/cir_graphs.py +0 -0
  25. {sourcecode-1.35.27 → sourcecode-1.35.28}/src/sourcecode/classifier.py +0 -0
  26. {sourcecode-1.35.27 → sourcecode-1.35.28}/src/sourcecode/code_notes_analyzer.py +0 -0
  27. {sourcecode-1.35.27 → sourcecode-1.35.28}/src/sourcecode/confidence_analyzer.py +0 -0
  28. {sourcecode-1.35.27 → sourcecode-1.35.28}/src/sourcecode/context_scorer.py +0 -0
  29. {sourcecode-1.35.27 → sourcecode-1.35.28}/src/sourcecode/context_summarizer.py +0 -0
  30. {sourcecode-1.35.27 → sourcecode-1.35.28}/src/sourcecode/contract_model.py +0 -0
  31. {sourcecode-1.35.27 → sourcecode-1.35.28}/src/sourcecode/contract_pipeline.py +0 -0
  32. {sourcecode-1.35.27 → sourcecode-1.35.28}/src/sourcecode/coverage_parser.py +0 -0
  33. {sourcecode-1.35.27 → sourcecode-1.35.28}/src/sourcecode/dependency_analyzer.py +0 -0
  34. {sourcecode-1.35.27 → sourcecode-1.35.28}/src/sourcecode/detectors/__init__.py +0 -0
  35. {sourcecode-1.35.27 → sourcecode-1.35.28}/src/sourcecode/detectors/base.py +0 -0
  36. {sourcecode-1.35.27 → sourcecode-1.35.28}/src/sourcecode/detectors/csproj_parser.py +0 -0
  37. {sourcecode-1.35.27 → sourcecode-1.35.28}/src/sourcecode/detectors/dart.py +0 -0
  38. {sourcecode-1.35.27 → sourcecode-1.35.28}/src/sourcecode/detectors/dotnet.py +0 -0
  39. {sourcecode-1.35.27 → sourcecode-1.35.28}/src/sourcecode/detectors/elixir.py +0 -0
  40. {sourcecode-1.35.27 → sourcecode-1.35.28}/src/sourcecode/detectors/go.py +0 -0
  41. {sourcecode-1.35.27 → sourcecode-1.35.28}/src/sourcecode/detectors/heuristic.py +0 -0
  42. {sourcecode-1.35.27 → sourcecode-1.35.28}/src/sourcecode/detectors/hybrid.py +0 -0
  43. {sourcecode-1.35.27 → sourcecode-1.35.28}/src/sourcecode/detectors/java.py +0 -0
  44. {sourcecode-1.35.27 → sourcecode-1.35.28}/src/sourcecode/detectors/jvm_ext.py +0 -0
  45. {sourcecode-1.35.27 → sourcecode-1.35.28}/src/sourcecode/detectors/nodejs.py +0 -0
  46. {sourcecode-1.35.27 → sourcecode-1.35.28}/src/sourcecode/detectors/parsers.py +0 -0
  47. {sourcecode-1.35.27 → sourcecode-1.35.28}/src/sourcecode/detectors/php.py +0 -0
  48. {sourcecode-1.35.27 → sourcecode-1.35.28}/src/sourcecode/detectors/project.py +0 -0
  49. {sourcecode-1.35.27 → sourcecode-1.35.28}/src/sourcecode/detectors/python.py +0 -0
  50. {sourcecode-1.35.27 → sourcecode-1.35.28}/src/sourcecode/detectors/ruby.py +0 -0
  51. {sourcecode-1.35.27 → sourcecode-1.35.28}/src/sourcecode/detectors/rust.py +0 -0
  52. {sourcecode-1.35.27 → sourcecode-1.35.28}/src/sourcecode/detectors/systems.py +0 -0
  53. {sourcecode-1.35.27 → sourcecode-1.35.28}/src/sourcecode/detectors/terraform.py +0 -0
  54. {sourcecode-1.35.27 → sourcecode-1.35.28}/src/sourcecode/detectors/tooling.py +0 -0
  55. {sourcecode-1.35.27 → sourcecode-1.35.28}/src/sourcecode/doc_analyzer.py +0 -0
  56. {sourcecode-1.35.27 → sourcecode-1.35.28}/src/sourcecode/entrypoint_classifier.py +0 -0
  57. {sourcecode-1.35.27 → sourcecode-1.35.28}/src/sourcecode/env_analyzer.py +0 -0
  58. {sourcecode-1.35.27 → sourcecode-1.35.28}/src/sourcecode/error_schema.py +0 -0
  59. {sourcecode-1.35.27 → sourcecode-1.35.28}/src/sourcecode/file_chunker.py +0 -0
  60. {sourcecode-1.35.27 → sourcecode-1.35.28}/src/sourcecode/file_classifier.py +0 -0
  61. {sourcecode-1.35.27 → sourcecode-1.35.28}/src/sourcecode/flow_analyzer.py +0 -0
  62. {sourcecode-1.35.27 → sourcecode-1.35.28}/src/sourcecode/fqn_utils.py +0 -0
  63. {sourcecode-1.35.27 → sourcecode-1.35.28}/src/sourcecode/git_analyzer.py +0 -0
  64. {sourcecode-1.35.27 → sourcecode-1.35.28}/src/sourcecode/graph_analyzer.py +0 -0
  65. {sourcecode-1.35.27 → sourcecode-1.35.28}/src/sourcecode/license.py +0 -0
  66. {sourcecode-1.35.27 → sourcecode-1.35.28}/src/sourcecode/mcp/__init__.py +0 -0
  67. {sourcecode-1.35.27 → sourcecode-1.35.28}/src/sourcecode/mcp/onboarding/__init__.py +0 -0
  68. {sourcecode-1.35.27 → sourcecode-1.35.28}/src/sourcecode/mcp/onboarding/applier.py +0 -0
  69. {sourcecode-1.35.27 → sourcecode-1.35.28}/src/sourcecode/mcp/onboarding/backup.py +0 -0
  70. {sourcecode-1.35.27 → sourcecode-1.35.28}/src/sourcecode/mcp/onboarding/detector.py +0 -0
  71. {sourcecode-1.35.27 → sourcecode-1.35.28}/src/sourcecode/mcp/onboarding/planner.py +0 -0
  72. {sourcecode-1.35.27 → sourcecode-1.35.28}/src/sourcecode/mcp/orchestrator.py +0 -0
  73. {sourcecode-1.35.27 → sourcecode-1.35.28}/src/sourcecode/mcp/registry.py +0 -0
  74. {sourcecode-1.35.27 → sourcecode-1.35.28}/src/sourcecode/mcp/runner.py +0 -0
  75. {sourcecode-1.35.27 → sourcecode-1.35.28}/src/sourcecode/mcp/server.py +0 -0
  76. {sourcecode-1.35.27 → sourcecode-1.35.28}/src/sourcecode/mcp_nudge.py +0 -0
  77. {sourcecode-1.35.27 → sourcecode-1.35.28}/src/sourcecode/metrics_analyzer.py +0 -0
  78. {sourcecode-1.35.27 → sourcecode-1.35.28}/src/sourcecode/migrate_check.py +0 -0
  79. {sourcecode-1.35.27 → sourcecode-1.35.28}/src/sourcecode/output_budget.py +0 -0
  80. {sourcecode-1.35.27 → sourcecode-1.35.28}/src/sourcecode/path_filters.py +0 -0
  81. {sourcecode-1.35.27 → sourcecode-1.35.28}/src/sourcecode/pr_comment_renderer.py +0 -0
  82. {sourcecode-1.35.27 → sourcecode-1.35.28}/src/sourcecode/pr_impact.py +0 -0
  83. {sourcecode-1.35.27 → sourcecode-1.35.28}/src/sourcecode/prepare_context.py +0 -0
  84. {sourcecode-1.35.27 → sourcecode-1.35.28}/src/sourcecode/progress.py +0 -0
  85. {sourcecode-1.35.27 → sourcecode-1.35.28}/src/sourcecode/ranking_engine.py +0 -0
  86. {sourcecode-1.35.27 → sourcecode-1.35.28}/src/sourcecode/redactor.py +0 -0
  87. {sourcecode-1.35.27 → sourcecode-1.35.28}/src/sourcecode/relevance_scorer.py +0 -0
  88. {sourcecode-1.35.27 → sourcecode-1.35.28}/src/sourcecode/repo_classifier.py +0 -0
  89. {sourcecode-1.35.27 → sourcecode-1.35.28}/src/sourcecode/ris.py +0 -0
  90. {sourcecode-1.35.27 → sourcecode-1.35.28}/src/sourcecode/runtime_classifier.py +0 -0
  91. {sourcecode-1.35.27 → sourcecode-1.35.28}/src/sourcecode/scanner.py +0 -0
  92. {sourcecode-1.35.27 → sourcecode-1.35.28}/src/sourcecode/schema.py +0 -0
  93. {sourcecode-1.35.27 → sourcecode-1.35.28}/src/sourcecode/semantic_analyzer.py +0 -0
  94. {sourcecode-1.35.27 → sourcecode-1.35.28}/src/sourcecode/serializer.py +0 -0
  95. {sourcecode-1.35.27 → sourcecode-1.35.28}/src/sourcecode/spring_event_topology.py +0 -0
  96. {sourcecode-1.35.27 → sourcecode-1.35.28}/src/sourcecode/spring_findings.py +0 -0
  97. {sourcecode-1.35.27 → sourcecode-1.35.28}/src/sourcecode/spring_impact.py +0 -0
  98. {sourcecode-1.35.27 → sourcecode-1.35.28}/src/sourcecode/spring_security_audit.py +0 -0
  99. {sourcecode-1.35.27 → sourcecode-1.35.28}/src/sourcecode/spring_semantic.py +0 -0
  100. {sourcecode-1.35.27 → sourcecode-1.35.28}/src/sourcecode/spring_tx_analyzer.py +0 -0
  101. {sourcecode-1.35.27 → sourcecode-1.35.28}/src/sourcecode/summarizer.py +0 -0
  102. {sourcecode-1.35.27 → sourcecode-1.35.28}/src/sourcecode/telemetry/__init__.py +0 -0
  103. {sourcecode-1.35.27 → sourcecode-1.35.28}/src/sourcecode/telemetry/config.py +0 -0
  104. {sourcecode-1.35.27 → sourcecode-1.35.28}/src/sourcecode/telemetry/consent.py +0 -0
  105. {sourcecode-1.35.27 → sourcecode-1.35.28}/src/sourcecode/telemetry/events.py +0 -0
  106. {sourcecode-1.35.27 → sourcecode-1.35.28}/src/sourcecode/telemetry/filters.py +0 -0
  107. {sourcecode-1.35.27 → sourcecode-1.35.28}/src/sourcecode/telemetry/transport.py +0 -0
  108. {sourcecode-1.35.27 → sourcecode-1.35.28}/src/sourcecode/tree_utils.py +0 -0
  109. {sourcecode-1.35.27 → sourcecode-1.35.28}/src/sourcecode/workspace.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: sourcecode
3
- Version: 1.35.27
3
+ Version: 1.35.28
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.27-blue)
43
+ ![Version](https://img.shields.io/badge/version-1.35.28-blue)
44
44
  ![Python](https://img.shields.io/badge/python-3.10%2B-green)
45
45
 
46
46
  ---
@@ -114,7 +114,9 @@ pipx install sourcecode
114
114
 
115
115
  ```bash
116
116
  sourcecode version
117
- # sourcecode 1.35.27
117
+ # sourcecode 1.35.28
118
+
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).
118
120
  ```
119
121
 
120
122
  ---
@@ -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.27-blue)
5
+ ![Version](https://img.shields.io/badge/version-1.35.28-blue)
6
6
  ![Python](https://img.shields.io/badge/python-3.10%2B-green)
7
7
 
8
8
  ---
@@ -76,7 +76,9 @@ pipx install sourcecode
76
76
 
77
77
  ```bash
78
78
  sourcecode version
79
- # sourcecode 1.35.27
79
+ # sourcecode 1.35.28
80
+
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).
80
82
  ```
81
83
 
82
84
  ---
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "sourcecode"
7
- version = "1.35.27"
7
+ version = "1.35.28"
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.27"
3
+ __version__ = "1.35.28"
@@ -5397,10 +5397,13 @@ def cold_start_cmd(
5397
5397
  result = _gcs(target)
5398
5398
  if compact:
5399
5399
  # P1-C: cap at ~10K tokens — keep only fields essential for orientation.
5400
- _cs_keys = {"status", "git_head", "stacks", "entry_points",
5401
- "key_dependencies", "project_type", "project_summary",
5402
- "validation", "_meta"}
5400
+ # BUG-6 fix: use actual RIS key names (summary/entrypoints, not stacks/entry_points)
5401
+ _cs_keys = {"status", "git_head", "summary", "entrypoints", "endpoints",
5402
+ "project_type", "validation", "_meta"}
5403
5403
  result = {k: v for k, v in result.items() if k in _cs_keys}
5404
+ # Truncate endpoints to first 30 to stay within ~10K token budget
5405
+ if isinstance(result.get("endpoints"), list):
5406
+ result["endpoints"] = result["endpoints"][:30]
5404
5407
  result["_meta"] = {**(result.get("_meta") or {}), "compact_mode": True,
5405
5408
  "full_available": "sourcecode cold-start (without --compact)"}
5406
5409
  _out = _json.dumps(result, indent=2, ensure_ascii=False)
@@ -28,6 +28,9 @@ _STEREOTYPE_DESC: dict[str, str] = {
28
28
  "component": "Spring @Component — general-purpose bean",
29
29
  "configuration": "Spring @Configuration — bean factory / config",
30
30
  "bean": "Spring @Bean — managed component",
31
+ "entity": "JPA @Entity — persistent domain object mapped to a database table",
32
+ "mappedsuperclass": "JPA @MappedSuperclass — base class sharing persistent state with subclasses",
33
+ "embeddable": "JPA @Embeddable — value object embedded in owning entity table",
31
34
  }
32
35
 
33
36
  _SECURITY_ANNOTATION_PREFIXES = (
@@ -106,7 +106,11 @@ def _collect_java_files(root: Path, *, include_tests: bool = True) -> list[Path]
106
106
  if any(part in _VENDOR_DIRS for part in parts[:-1]):
107
107
  continue
108
108
  if not include_tests:
109
- if "/test/" in rel or "/tests/" in rel or rel.startswith("test/"):
109
+ if (
110
+ "/src/test/" in rel or rel.startswith("src/test/")
111
+ or "/src/tests/" in rel or rel.startswith("src/tests/")
112
+ or rel.startswith("test/") or rel.startswith("tests/")
113
+ ):
110
114
  continue
111
115
  results.append(p)
112
116
  return results
@@ -150,10 +154,42 @@ def _find_class_file(
150
154
 
151
155
  def _apply_rename(source: str, old_name: str, new_name: str) -> str:
152
156
  """Apply word-boundary replacement for class name (PascalCase and camelCase forms)."""
153
- # PascalCase replacement: all type references, declarations, imports
154
157
  result = re.sub(r'\b' + re.escape(old_name) + r'\b', new_name, source)
155
158
 
156
- # camelCase instance names: serviceA → serviceB (only when different from PascalCase)
159
+ old_camel = _to_camel(old_name)
160
+ new_camel = _to_camel(new_name)
161
+ if old_camel != old_name and old_camel in result:
162
+ result = re.sub(r'\b' + re.escape(old_camel) + r'\b', new_camel, result)
163
+
164
+ return result
165
+
166
+
167
+ # Matches a class/interface/enum/record declaration of a given name
168
+ _CLASS_DECL_RE_TMPL = r'\b(?:class|interface|enum|record)\s+{name}\b'
169
+ # Matches a constructor declaration: optional access modifier + ClassName + (
170
+ _CTOR_DECL_RE_TMPL = r'^\s*(?:(?:public|protected|private)\s+)?' + r'{name}\s*\('
171
+
172
+
173
+ def _apply_rename_refs_only(source: str, old_name: str, new_name: str) -> str:
174
+ """Rename old_name→new_name in a non-source file (import/type references only).
175
+
176
+ Skips lines containing a class/interface/enum/record declaration or constructor
177
+ declaration of old_name, so that a class sharing the simple name in another
178
+ package is not corrupted.
179
+ """
180
+ class_decl_re = re.compile(_CLASS_DECL_RE_TMPL.format(name=re.escape(old_name)))
181
+ ctor_decl_re = re.compile(_CTOR_DECL_RE_TMPL.format(name=re.escape(old_name)))
182
+ ref_re = re.compile(r'\b' + re.escape(old_name) + r'\b')
183
+
184
+ lines = source.splitlines(keepends=True)
185
+ result_lines = []
186
+ for line in lines:
187
+ if class_decl_re.search(line) or ctor_decl_re.search(line):
188
+ result_lines.append(line)
189
+ else:
190
+ result_lines.append(ref_re.sub(new_name, line))
191
+ result = ''.join(result_lines)
192
+
157
193
  old_camel = _to_camel(old_name)
158
194
  new_camel = _to_camel(new_name)
159
195
  if old_camel != old_name and old_camel in result:
@@ -245,6 +281,19 @@ def rename_class(
245
281
  result.old_file = str(source_file.relative_to(root)).replace("\\", "/")
246
282
  result.new_file = str(new_file_path.relative_to(root)).replace("\\", "/")
247
283
 
284
+ # BUG-2: check for collision anywhere in the repo, not just same directory
285
+ collision = next(
286
+ (f for f in java_files if f.stem == new_name and f.resolve() != new_file_path.resolve()),
287
+ None,
288
+ )
289
+ if collision is not None:
290
+ collision_rel = str(collision.relative_to(root)).replace("\\", "/")
291
+ result.errors.append(
292
+ f"'{new_name}' already exists at '{collision_rel}' — "
293
+ f"rename would create a duplicate class name. Pass --force to override."
294
+ )
295
+ return result
296
+
248
297
  if new_file_path.exists() and new_file_path != source_file:
249
298
  result.errors.append(
250
299
  f"Target file '{result.new_file}' already exists — aborting to avoid overwrite."
@@ -260,15 +309,18 @@ def rename_class(
260
309
  result.errors.append(f"Could not read '{java_file}': {e}")
261
310
  continue
262
311
 
263
- new_text = _apply_rename(old_text, old_name, new_name)
312
+ is_source = java_file == source_file
313
+ if is_source:
314
+ new_text = _apply_rename(old_text, old_name, new_name)
315
+ else:
316
+ # BUG-4: use refs-only variant to avoid clobbering same-named class in other package
317
+ new_text = _apply_rename_refs_only(old_text, old_name, new_name)
264
318
  if new_text == old_text:
265
319
  continue
266
320
 
267
321
  rel_path = str(java_file.relative_to(root)).replace("\\", "/")
268
322
  diff = _make_diff(old_text, new_text, rel_path)
269
323
 
270
- # Determine intent
271
- is_source = java_file == source_file
272
324
  if is_source:
273
325
  intent = f"Renamed class declaration: {old_name} → {new_name}"
274
326
  else:
@@ -202,8 +202,9 @@ _SECURITY_MARKER_ANNOTATIONS: frozenset[str] = frozenset({
202
202
  # is expected and does NOT mean endpoints are unprotected.
203
203
  _FILTER_SECURITY_ANNOTATIONS: frozenset[str] = frozenset({
204
204
  "@EnableWebSecurity",
205
- "@EnableMethodSecurity",
206
- "@EnableGlobalMethodSecurity",
205
+ # @EnableMethodSecurity / @EnableGlobalMethodSecurity enable per-method annotation
206
+ # security (@PreAuthorize/@Secured), NOT a filter chain — must NOT be treated as
207
+ # filter_based or SEC-001 is suppressed for every unannotated endpoint.
207
208
  })
208
209
 
209
210
  # Programmatic security: method-call patterns that indicate runtime auth enforcement.
@@ -2893,6 +2894,48 @@ def build_repo_ir(
2893
2894
  )
2894
2895
  ir = _assemble(all_symbols, unique_relations, all_changed, spring_summary, route_diffs_arg)
2895
2896
 
2897
+ # BUG-7: XML Spring Security detection for the canonical CIR pipeline.
2898
+ # _assemble only sees Java symbols — XML config is invisible to it.
2899
+ # Scan here (where root is available) and retag route_surface entries so
2900
+ # build_canonical_ir produces correct CanonicalEndpoint.security values.
2901
+ _xml_sec_re = re.compile(
2902
+ r'(?:xmlns(?::[a-z]+)?="http://www\.springframework\.org/schema/security"'
2903
+ r'|<security:http\b'
2904
+ r'|<http\s[^>]*use-expressions'
2905
+ r'|spring-security-[2345]'
2906
+ r'|xmlns:security="http://www\.springframework\.org/schema/security")',
2907
+ re.IGNORECASE,
2908
+ )
2909
+ _xml_sec_detected = False
2910
+ for _xml_glob in (
2911
+ "*security*.xml", "*Security*.xml",
2912
+ "*applicationContext*.xml", "*-context.xml", "*Context.xml",
2913
+ "*spring*.xml", "*Spring*.xml",
2914
+ ):
2915
+ for _xf in root.rglob(_xml_glob):
2916
+ if "target/" in str(_xf).replace("\\", "/"):
2917
+ continue
2918
+ try:
2919
+ _xt = _xf.read_text(encoding="utf-8", errors="replace")
2920
+ except OSError:
2921
+ continue
2922
+ if _xml_sec_re.search(_xt):
2923
+ _xml_sec_detected = True
2924
+ break
2925
+ if _xml_sec_detected:
2926
+ break
2927
+ if _xml_sec_detected:
2928
+ _sec_model = ir.get("security_model", "unknown")
2929
+ if _sec_model == "unknown":
2930
+ ir["security_model"] = "xml_or_filter_chain"
2931
+ elif _sec_model in ("annotation_based", "mixed"):
2932
+ ir["security_model"] = "mixed"
2933
+ # Retag route_surface entries that have no security (would become none_detected in CIR)
2934
+ for _r in ir.get("route_surface") or []:
2935
+ _r_sec = _r.get("security_annotations")
2936
+ if _r_sec is None or (isinstance(_r_sec, dict) and _r_sec.get("policy") == "none_detected"):
2937
+ _r["security_annotations"] = {"policy": "xml_or_filter_chain"}
2938
+
2896
2939
  # L-6: inject analysis_meta — files_read, lines_read, symbols_analyzed, token_estimate
2897
2940
  ir["analysis_meta"] = {
2898
2941
  "files_read": _meta_files_read,
@@ -3358,13 +3401,18 @@ def extract_java_endpoints(root: Path) -> "dict[str, Any]":
3358
3401
  if _xml_security_detected:
3359
3402
  break
3360
3403
 
3361
- if _xml_security_detected and security_model == "unknown":
3362
- security_model = "xml_or_filter_chain"
3363
- # Re-tag per-endpoint none_detected xml_or_filter_chain so the output
3364
- # cannot be misread as "endpoint is unprotected".
3404
+ if _xml_security_detected:
3405
+ # Re-tag per-endpoint none_detected → xml_or_filter_chain regardless of security_model.
3406
+ # BUG-7 fix: previously only ran when model == "unknown", causing false-positive SEC-001
3407
+ # when annotation security (@PreAuthorize) coexisted with XML security config.
3365
3408
  for ep in endpoints:
3366
3409
  if ep.get("security", {}).get("policy") == "none_detected":
3367
3410
  ep["security"] = {"policy": "xml_or_filter_chain"}
3411
+ if security_model == "unknown":
3412
+ security_model = "xml_or_filter_chain"
3413
+ elif security_model in ("annotation_based", "mixed"):
3414
+ security_model = "mixed"
3415
+ # filter_based stays filter_based — XML + filter chain is still filter_based
3368
3416
  # Recompute no_security_signal (now counts only truly unknown endpoints)
3369
3417
  no_security_signal = sum(
3370
3418
  1 for e in endpoints
@@ -3395,7 +3443,11 @@ def find_java_files(root: Path, *, max_files: int = 8000, limitations: list[str]
3395
3443
  continue
3396
3444
  parts = rel.split("/")
3397
3445
  # Skip test dirs
3398
- if "/test/" in rel or "/tests/" in rel or rel.startswith("test/"):
3446
+ if (
3447
+ "/src/test/" in rel or rel.startswith("src/test/")
3448
+ or "/src/tests/" in rel or rel.startswith("src/tests/")
3449
+ or rel.startswith("test/") or rel.startswith("tests/")
3450
+ ):
3399
3451
  continue
3400
3452
  # Skip vendor/generated/build dirs
3401
3453
  if any(part in _VENDOR_DIRS for part in parts[:-1]):
@@ -41,6 +41,8 @@ _CALL_SKIP: frozenset[str] = frozenset({"annotated_with", "mapped_to", "containe
41
41
  _BEAN_ANNOTATIONS: frozenset[str] = frozenset({
42
42
  "@Component", "@Service", "@Repository",
43
43
  "@Controller", "@RestController", "@Configuration", "@Bean",
44
+ # JPA persistence annotations — not Spring beans but need stereotype recognition in explain
45
+ "@Entity", "@MappedSuperclass", "@Embeddable",
44
46
  })
45
47
 
46
48
  _GENERIC_PARAM_RE = re.compile(r"<[A-Z][\w,\s<>?]*>")
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes