sourcecode 1.35.3__tar.gz → 1.35.5__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 (103) hide show
  1. {sourcecode-1.35.3 → sourcecode-1.35.5}/PKG-INFO +99 -6
  2. {sourcecode-1.35.3 → sourcecode-1.35.5}/README.md +98 -5
  3. {sourcecode-1.35.3 → sourcecode-1.35.5}/pyproject.toml +1 -1
  4. {sourcecode-1.35.3 → sourcecode-1.35.5}/src/sourcecode/__init__.py +1 -1
  5. {sourcecode-1.35.3 → sourcecode-1.35.5}/src/sourcecode/cir_graphs.py +20 -4
  6. {sourcecode-1.35.3 → sourcecode-1.35.5}/src/sourcecode/repository_ir.py +100 -8
  7. {sourcecode-1.35.3 → sourcecode-1.35.5}/src/sourcecode/spring_impact.py +54 -10
  8. {sourcecode-1.35.3 → sourcecode-1.35.5}/src/sourcecode/spring_tx_analyzer.py +34 -2
  9. {sourcecode-1.35.3 → sourcecode-1.35.5}/.github/workflows/build-windows.yml +0 -0
  10. {sourcecode-1.35.3 → sourcecode-1.35.5}/.gitignore +0 -0
  11. {sourcecode-1.35.3 → sourcecode-1.35.5}/.ruff.toml +0 -0
  12. {sourcecode-1.35.3 → sourcecode-1.35.5}/CHANGELOG.md +0 -0
  13. {sourcecode-1.35.3 → sourcecode-1.35.5}/CONTRIBUTING.md +0 -0
  14. {sourcecode-1.35.3 → sourcecode-1.35.5}/LICENSE +0 -0
  15. {sourcecode-1.35.3 → sourcecode-1.35.5}/SECURITY.md +0 -0
  16. {sourcecode-1.35.3 → sourcecode-1.35.5}/raw +0 -0
  17. {sourcecode-1.35.3 → sourcecode-1.35.5}/src/sourcecode/adaptive_scanner.py +0 -0
  18. {sourcecode-1.35.3 → sourcecode-1.35.5}/src/sourcecode/architecture_analyzer.py +0 -0
  19. {sourcecode-1.35.3 → sourcecode-1.35.5}/src/sourcecode/architecture_summary.py +0 -0
  20. {sourcecode-1.35.3 → sourcecode-1.35.5}/src/sourcecode/ast_extractor.py +0 -0
  21. {sourcecode-1.35.3 → sourcecode-1.35.5}/src/sourcecode/cache.py +0 -0
  22. {sourcecode-1.35.3 → sourcecode-1.35.5}/src/sourcecode/canonical_ir.py +0 -0
  23. {sourcecode-1.35.3 → sourcecode-1.35.5}/src/sourcecode/classifier.py +0 -0
  24. {sourcecode-1.35.3 → sourcecode-1.35.5}/src/sourcecode/cli.py +0 -0
  25. {sourcecode-1.35.3 → sourcecode-1.35.5}/src/sourcecode/code_notes_analyzer.py +0 -0
  26. {sourcecode-1.35.3 → sourcecode-1.35.5}/src/sourcecode/confidence_analyzer.py +0 -0
  27. {sourcecode-1.35.3 → sourcecode-1.35.5}/src/sourcecode/context_scorer.py +0 -0
  28. {sourcecode-1.35.3 → sourcecode-1.35.5}/src/sourcecode/context_summarizer.py +0 -0
  29. {sourcecode-1.35.3 → sourcecode-1.35.5}/src/sourcecode/contract_model.py +0 -0
  30. {sourcecode-1.35.3 → sourcecode-1.35.5}/src/sourcecode/contract_pipeline.py +0 -0
  31. {sourcecode-1.35.3 → sourcecode-1.35.5}/src/sourcecode/coverage_parser.py +0 -0
  32. {sourcecode-1.35.3 → sourcecode-1.35.5}/src/sourcecode/dependency_analyzer.py +0 -0
  33. {sourcecode-1.35.3 → sourcecode-1.35.5}/src/sourcecode/detectors/__init__.py +0 -0
  34. {sourcecode-1.35.3 → sourcecode-1.35.5}/src/sourcecode/detectors/base.py +0 -0
  35. {sourcecode-1.35.3 → sourcecode-1.35.5}/src/sourcecode/detectors/csproj_parser.py +0 -0
  36. {sourcecode-1.35.3 → sourcecode-1.35.5}/src/sourcecode/detectors/dart.py +0 -0
  37. {sourcecode-1.35.3 → sourcecode-1.35.5}/src/sourcecode/detectors/dotnet.py +0 -0
  38. {sourcecode-1.35.3 → sourcecode-1.35.5}/src/sourcecode/detectors/elixir.py +0 -0
  39. {sourcecode-1.35.3 → sourcecode-1.35.5}/src/sourcecode/detectors/go.py +0 -0
  40. {sourcecode-1.35.3 → sourcecode-1.35.5}/src/sourcecode/detectors/heuristic.py +0 -0
  41. {sourcecode-1.35.3 → sourcecode-1.35.5}/src/sourcecode/detectors/hybrid.py +0 -0
  42. {sourcecode-1.35.3 → sourcecode-1.35.5}/src/sourcecode/detectors/java.py +0 -0
  43. {sourcecode-1.35.3 → sourcecode-1.35.5}/src/sourcecode/detectors/jvm_ext.py +0 -0
  44. {sourcecode-1.35.3 → sourcecode-1.35.5}/src/sourcecode/detectors/nodejs.py +0 -0
  45. {sourcecode-1.35.3 → sourcecode-1.35.5}/src/sourcecode/detectors/parsers.py +0 -0
  46. {sourcecode-1.35.3 → sourcecode-1.35.5}/src/sourcecode/detectors/php.py +0 -0
  47. {sourcecode-1.35.3 → sourcecode-1.35.5}/src/sourcecode/detectors/project.py +0 -0
  48. {sourcecode-1.35.3 → sourcecode-1.35.5}/src/sourcecode/detectors/python.py +0 -0
  49. {sourcecode-1.35.3 → sourcecode-1.35.5}/src/sourcecode/detectors/ruby.py +0 -0
  50. {sourcecode-1.35.3 → sourcecode-1.35.5}/src/sourcecode/detectors/rust.py +0 -0
  51. {sourcecode-1.35.3 → sourcecode-1.35.5}/src/sourcecode/detectors/systems.py +0 -0
  52. {sourcecode-1.35.3 → sourcecode-1.35.5}/src/sourcecode/detectors/terraform.py +0 -0
  53. {sourcecode-1.35.3 → sourcecode-1.35.5}/src/sourcecode/detectors/tooling.py +0 -0
  54. {sourcecode-1.35.3 → sourcecode-1.35.5}/src/sourcecode/doc_analyzer.py +0 -0
  55. {sourcecode-1.35.3 → sourcecode-1.35.5}/src/sourcecode/entrypoint_classifier.py +0 -0
  56. {sourcecode-1.35.3 → sourcecode-1.35.5}/src/sourcecode/env_analyzer.py +0 -0
  57. {sourcecode-1.35.3 → sourcecode-1.35.5}/src/sourcecode/error_schema.py +0 -0
  58. {sourcecode-1.35.3 → sourcecode-1.35.5}/src/sourcecode/file_classifier.py +0 -0
  59. {sourcecode-1.35.3 → sourcecode-1.35.5}/src/sourcecode/flow_analyzer.py +0 -0
  60. {sourcecode-1.35.3 → sourcecode-1.35.5}/src/sourcecode/git_analyzer.py +0 -0
  61. {sourcecode-1.35.3 → sourcecode-1.35.5}/src/sourcecode/graph_analyzer.py +0 -0
  62. {sourcecode-1.35.3 → sourcecode-1.35.5}/src/sourcecode/license.py +0 -0
  63. {sourcecode-1.35.3 → sourcecode-1.35.5}/src/sourcecode/mcp/__init__.py +0 -0
  64. {sourcecode-1.35.3 → sourcecode-1.35.5}/src/sourcecode/mcp/onboarding/__init__.py +0 -0
  65. {sourcecode-1.35.3 → sourcecode-1.35.5}/src/sourcecode/mcp/onboarding/applier.py +0 -0
  66. {sourcecode-1.35.3 → sourcecode-1.35.5}/src/sourcecode/mcp/onboarding/backup.py +0 -0
  67. {sourcecode-1.35.3 → sourcecode-1.35.5}/src/sourcecode/mcp/onboarding/detector.py +0 -0
  68. {sourcecode-1.35.3 → sourcecode-1.35.5}/src/sourcecode/mcp/onboarding/planner.py +0 -0
  69. {sourcecode-1.35.3 → sourcecode-1.35.5}/src/sourcecode/mcp/orchestrator.py +0 -0
  70. {sourcecode-1.35.3 → sourcecode-1.35.5}/src/sourcecode/mcp/registry.py +0 -0
  71. {sourcecode-1.35.3 → sourcecode-1.35.5}/src/sourcecode/mcp/runner.py +0 -0
  72. {sourcecode-1.35.3 → sourcecode-1.35.5}/src/sourcecode/mcp/server.py +0 -0
  73. {sourcecode-1.35.3 → sourcecode-1.35.5}/src/sourcecode/mcp_nudge.py +0 -0
  74. {sourcecode-1.35.3 → sourcecode-1.35.5}/src/sourcecode/metrics_analyzer.py +0 -0
  75. {sourcecode-1.35.3 → sourcecode-1.35.5}/src/sourcecode/output_budget.py +0 -0
  76. {sourcecode-1.35.3 → sourcecode-1.35.5}/src/sourcecode/path_filters.py +0 -0
  77. {sourcecode-1.35.3 → sourcecode-1.35.5}/src/sourcecode/pr_comment_renderer.py +0 -0
  78. {sourcecode-1.35.3 → sourcecode-1.35.5}/src/sourcecode/prepare_context.py +0 -0
  79. {sourcecode-1.35.3 → sourcecode-1.35.5}/src/sourcecode/progress.py +0 -0
  80. {sourcecode-1.35.3 → sourcecode-1.35.5}/src/sourcecode/ranking_engine.py +0 -0
  81. {sourcecode-1.35.3 → sourcecode-1.35.5}/src/sourcecode/redactor.py +0 -0
  82. {sourcecode-1.35.3 → sourcecode-1.35.5}/src/sourcecode/relevance_scorer.py +0 -0
  83. {sourcecode-1.35.3 → sourcecode-1.35.5}/src/sourcecode/repo_classifier.py +0 -0
  84. {sourcecode-1.35.3 → sourcecode-1.35.5}/src/sourcecode/ris.py +0 -0
  85. {sourcecode-1.35.3 → sourcecode-1.35.5}/src/sourcecode/runtime_classifier.py +0 -0
  86. {sourcecode-1.35.3 → sourcecode-1.35.5}/src/sourcecode/scanner.py +0 -0
  87. {sourcecode-1.35.3 → sourcecode-1.35.5}/src/sourcecode/schema.py +0 -0
  88. {sourcecode-1.35.3 → sourcecode-1.35.5}/src/sourcecode/semantic_analyzer.py +0 -0
  89. {sourcecode-1.35.3 → sourcecode-1.35.5}/src/sourcecode/serializer.py +0 -0
  90. {sourcecode-1.35.3 → sourcecode-1.35.5}/src/sourcecode/spring_event_topology.py +0 -0
  91. {sourcecode-1.35.3 → sourcecode-1.35.5}/src/sourcecode/spring_findings.py +0 -0
  92. {sourcecode-1.35.3 → sourcecode-1.35.5}/src/sourcecode/spring_model.py +0 -0
  93. {sourcecode-1.35.3 → sourcecode-1.35.5}/src/sourcecode/spring_security_audit.py +0 -0
  94. {sourcecode-1.35.3 → sourcecode-1.35.5}/src/sourcecode/spring_semantic.py +0 -0
  95. {sourcecode-1.35.3 → sourcecode-1.35.5}/src/sourcecode/summarizer.py +0 -0
  96. {sourcecode-1.35.3 → sourcecode-1.35.5}/src/sourcecode/telemetry/__init__.py +0 -0
  97. {sourcecode-1.35.3 → sourcecode-1.35.5}/src/sourcecode/telemetry/config.py +0 -0
  98. {sourcecode-1.35.3 → sourcecode-1.35.5}/src/sourcecode/telemetry/consent.py +0 -0
  99. {sourcecode-1.35.3 → sourcecode-1.35.5}/src/sourcecode/telemetry/events.py +0 -0
  100. {sourcecode-1.35.3 → sourcecode-1.35.5}/src/sourcecode/telemetry/filters.py +0 -0
  101. {sourcecode-1.35.3 → sourcecode-1.35.5}/src/sourcecode/telemetry/transport.py +0 -0
  102. {sourcecode-1.35.3 → sourcecode-1.35.5}/src/sourcecode/tree_utils.py +0 -0
  103. {sourcecode-1.35.3 → sourcecode-1.35.5}/src/sourcecode/workspace.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: sourcecode
3
- Version: 1.35.3
3
+ Version: 1.35.5
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
@@ -39,7 +39,7 @@ Description-Content-Type: text/markdown
39
39
 
40
40
  **Persistent structural context and ultra-fast repeated analysis for AI coding agents.**
41
41
 
42
- ![Version](https://img.shields.io/badge/version-1.35.3-blue)
42
+ ![Version](https://img.shields.io/badge/version-1.35.5-blue)
43
43
  ![Python](https://img.shields.io/badge/python-3.10%2B-green)
44
44
 
45
45
  ---
@@ -113,7 +113,7 @@ pipx install sourcecode
113
113
 
114
114
  ```bash
115
115
  sourcecode version
116
- # sourcecode 1.33.4
116
+ # sourcecode 1.35.5
117
117
  ```
118
118
 
119
119
  ---
@@ -133,6 +133,15 @@ sourcecode --agent
133
133
  # Blast radius: what breaks if this class changes?
134
134
  sourcecode impact OrderService /path/to/repo
135
135
 
136
+ # Spring semantic audit: TX anomalies + security surface (free)
137
+ sourcecode spring-audit /path/to/repo
138
+
139
+ # Impact chain: systemic blast radius with TX/SEC enrichment (free)
140
+ sourcecode impact-chain OrderService /path/to/repo
141
+
142
+ # Event topology: publisher → event → consumer graph (free)
143
+ sourcecode impact-chain OrderPlacedEvent /path/to/repo --type events
144
+
136
145
  # REST endpoint surface
137
146
  sourcecode endpoints /path/to/repo
138
147
 
@@ -187,15 +196,21 @@ sourcecode /repo --agent # ~4,500–5,500 tokens — more detail
187
196
  sourcecode onboard /repo # task-structured: entry points, key files, gaps
188
197
  ```
189
198
 
190
- ### Before every change — blast radius check
199
+ ### Before every change — blast radius + TX/SEC check
191
200
 
192
201
  ```bash
193
202
  # Always target the INTERFACE in Spring projects, not the implementation:
194
203
  sourcecode impact OrderService /repo # ✓ 30 callers, 11 endpoints
195
204
  sourcecode impact OrderServiceImpl /repo # ✗ 0 callers (Spring DI blindness)
196
205
 
197
- # Large hub interfaces depth=1 is faster and still the most actionable signal:
198
- sourcecode impact KeycloakSession /repo --depth 1
206
+ # Impact chain: blast radius enriched with TX boundary and security surfaces
207
+ sourcecode impact-chain OrderService /repo
208
+
209
+ # Event topology: who publishes/consumes this event, and in what TX phase?
210
+ sourcecode impact-chain OrderPlacedEvent /repo --type events
211
+
212
+ # Spring audit: catch TX anomalies before they hit production
213
+ sourcecode spring-audit /repo --scope tx
199
214
  ```
200
215
 
201
216
  ### Continuous agent loop — delta context
@@ -262,6 +277,9 @@ Specifically:
262
277
  - Endpoint recall for JAX-RS subresource locator pattern is ~65%
263
278
  - `impact` on implementation classes (e.g. `OrderServiceImpl`) returns 0 callers in Spring Boot — callers inject the interface via `@Autowired`. Always target the interface. When `direct_callers: []` with `confidence_level: high` for a `@Service` class, re-query the interface.
264
279
  - `no_security_signal` on endpoints means no method-level annotations found — does **not** mean the endpoint is unsecured. Projects using Spring Security filter chains show 100% `no_security_signal` even when fully secured.
280
+ - `spring-audit` and `impact-chain` are **Java/Spring only** — non-Java repos return `spring_detected: false`
281
+ - Event topology via `--type events` does not resolve Kafka/RabbitMQ/Redis message routes — only Spring ApplicationEvent and `@EventListener` chains
282
+ - Self-invocation TX bypass (calling `@Transactional` method from the same class without going through the proxy) is not detected
265
283
 
266
284
  ---
267
285
 
@@ -312,6 +330,81 @@ sourcecode endpoints /path/to/repo --output endpoints.json
312
330
 
313
331
  Extracts all Spring MVC (`@GetMapping`, `@PostMapping`, `@RequestMapping`, etc.) and JAX-RS (`@GET`, `@POST`, `@Path`) endpoint methods. Returns HTTP method, path, controller class, and handler method.
314
332
 
333
+ ### `spring-audit` — Spring semantic audit [free]
334
+
335
+ ```bash
336
+ sourcecode spring-audit /path/to/repo
337
+ sourcecode spring-audit /path/to/repo --scope tx # TX anomalies only
338
+ sourcecode spring-audit /path/to/repo --scope security # security surface only
339
+ sourcecode spring-audit /path/to/repo --min-severity high
340
+ ```
341
+
342
+ Detects structural Spring anomalies that survive code review and tests, but cause production failures:
343
+
344
+ | Pattern | Description |
345
+ |---------|-------------|
346
+ | `TX-001` | `@Transactional` on private/final method — CGLIB proxy bypass, TX silently ignored |
347
+ | `TX-002` | `REQUIRES_NEW` nested inside `REQUIRED` call chain — unexpected transaction nesting |
348
+ | `TX-003` | `readOnly=true` boundary propagating to write operation |
349
+ | `TX-004` | `NOT_SUPPORTED`/`NEVER` called within active TX chain |
350
+ | `TX-005` | Exception swallowing inside `@Transactional` — silent TX rollback suppression |
351
+ | `SEC-001` | Unsecured endpoint in annotation-based security model |
352
+ | `SEC-002` | CVE-2025-41248: `@PreAuthorize` on inherited method from generic supertype |
353
+ | `SEC-003` | `@Transactional` on `@Controller`/`@RestController` — TX in wrong layer |
354
+
355
+ Returns structured findings with `severity`, `confidence`, `symbol`, `source_file`, `evidence`, `explanation`, and `fix_hint`. JAVA/SPRING ONLY.
356
+
357
+ ### `impact-chain` — systemic blast radius with TX/SEC enrichment [free]
358
+
359
+ ```bash
360
+ sourcecode impact-chain OrderService /path/to/repo
361
+ sourcecode impact-chain com.example.OrderService#placeOrder /path/to/repo
362
+ sourcecode impact-chain PaymentService . --depth 6
363
+ ```
364
+
365
+ Unlike `impact` (which traces the caller graph), `impact-chain` builds on the SpringSemanticModel to enrich every step of the blast cone with transaction and security context:
366
+
367
+ | Field | Description |
368
+ |-------|-------------|
369
+ | `direct_callers` | Symbols that directly call the target |
370
+ | `indirect_callers` | Transitive callers (BFS up to `--depth` hops, default: 4) |
371
+ | `endpoints_affected` | HTTP endpoints reachable through the call chain |
372
+ | `transaction_boundary` | `@Transactional` semantics on the target: propagation, isolation, readOnly |
373
+ | `security_surfaces` | Per-endpoint security policy + SEC finding IDs |
374
+ | `impact_findings` | TX-001..005 and SEC-001..003 findings that touch the call chain |
375
+ | `risk_level` | `critical` \| `high` \| `medium` \| `low` |
376
+
377
+ **Event topology** — query the publisher/consumer graph for a Spring event class:
378
+
379
+ ```bash
380
+ sourcecode impact-chain OrderPlacedEvent /path/to/repo --type events
381
+ ```
382
+
383
+ | Field | Description |
384
+ |-------|-------------|
385
+ | `publishers` | FQNs that publish this event class |
386
+ | `consumers` | Listeners with TX phase metadata (`AFTER_COMMIT`, `BEFORE_COMMIT`, etc.) |
387
+ | `event_graph` | Publisher → event → consumer edges (BFS ≤ 2 hops) |
388
+ | `transaction_context` | `AFTER_COMMIT` consumers, `BEFORE_COMMIT` risks |
389
+ | `risk_level` | Derived from TX phase and consumer count |
390
+
391
+ **Limitations of event topology:**
392
+ - Resolves Spring `ApplicationEvent` / `@EventListener` chains only
393
+ - Does not trace Kafka, RabbitMQ, Redis, or other message brokers
394
+ - Does not detect self-invocation proxy bypass
395
+ - Conditional beans (`@ConditionalOnProperty`) are not evaluated at analysis time
396
+
397
+ ### `cold-start` — RIS bootstrap context
398
+
399
+ ```bash
400
+ sourcecode cold-start /path/to/repo
401
+ sourcecode cold-start /path/to/repo --compact # ~10K token subset
402
+ ```
403
+
404
+ Returns the Repository Intelligence Snapshot (RIS) instantly — zero re-analysis. The RIS is built by a prior warm cache pass and includes stacks, entry points, endpoint surface, and Spring semantic signals. Status field: `cold_start_ready` | `cold_start_stale` | `no_ris`.
405
+
406
+ Use `--compact` to get a ~10K token subset safe for direct LLM injection. Full snapshot can exceed 100K tokens on medium repos — use `--output FILE` for local search tooling.
407
+
315
408
  ### `repo-ir` — symbol-level IR
316
409
 
317
410
  ```bash
@@ -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.3-blue)
5
+ ![Version](https://img.shields.io/badge/version-1.35.5-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.33.4
79
+ # sourcecode 1.35.5
80
80
  ```
81
81
 
82
82
  ---
@@ -96,6 +96,15 @@ sourcecode --agent
96
96
  # Blast radius: what breaks if this class changes?
97
97
  sourcecode impact OrderService /path/to/repo
98
98
 
99
+ # Spring semantic audit: TX anomalies + security surface (free)
100
+ sourcecode spring-audit /path/to/repo
101
+
102
+ # Impact chain: systemic blast radius with TX/SEC enrichment (free)
103
+ sourcecode impact-chain OrderService /path/to/repo
104
+
105
+ # Event topology: publisher → event → consumer graph (free)
106
+ sourcecode impact-chain OrderPlacedEvent /path/to/repo --type events
107
+
99
108
  # REST endpoint surface
100
109
  sourcecode endpoints /path/to/repo
101
110
 
@@ -150,15 +159,21 @@ sourcecode /repo --agent # ~4,500–5,500 tokens — more detail
150
159
  sourcecode onboard /repo # task-structured: entry points, key files, gaps
151
160
  ```
152
161
 
153
- ### Before every change — blast radius check
162
+ ### Before every change — blast radius + TX/SEC check
154
163
 
155
164
  ```bash
156
165
  # Always target the INTERFACE in Spring projects, not the implementation:
157
166
  sourcecode impact OrderService /repo # ✓ 30 callers, 11 endpoints
158
167
  sourcecode impact OrderServiceImpl /repo # ✗ 0 callers (Spring DI blindness)
159
168
 
160
- # Large hub interfaces depth=1 is faster and still the most actionable signal:
161
- sourcecode impact KeycloakSession /repo --depth 1
169
+ # Impact chain: blast radius enriched with TX boundary and security surfaces
170
+ sourcecode impact-chain OrderService /repo
171
+
172
+ # Event topology: who publishes/consumes this event, and in what TX phase?
173
+ sourcecode impact-chain OrderPlacedEvent /repo --type events
174
+
175
+ # Spring audit: catch TX anomalies before they hit production
176
+ sourcecode spring-audit /repo --scope tx
162
177
  ```
163
178
 
164
179
  ### Continuous agent loop — delta context
@@ -225,6 +240,9 @@ Specifically:
225
240
  - Endpoint recall for JAX-RS subresource locator pattern is ~65%
226
241
  - `impact` on implementation classes (e.g. `OrderServiceImpl`) returns 0 callers in Spring Boot — callers inject the interface via `@Autowired`. Always target the interface. When `direct_callers: []` with `confidence_level: high` for a `@Service` class, re-query the interface.
227
242
  - `no_security_signal` on endpoints means no method-level annotations found — does **not** mean the endpoint is unsecured. Projects using Spring Security filter chains show 100% `no_security_signal` even when fully secured.
243
+ - `spring-audit` and `impact-chain` are **Java/Spring only** — non-Java repos return `spring_detected: false`
244
+ - Event topology via `--type events` does not resolve Kafka/RabbitMQ/Redis message routes — only Spring ApplicationEvent and `@EventListener` chains
245
+ - Self-invocation TX bypass (calling `@Transactional` method from the same class without going through the proxy) is not detected
228
246
 
229
247
  ---
230
248
 
@@ -275,6 +293,81 @@ sourcecode endpoints /path/to/repo --output endpoints.json
275
293
 
276
294
  Extracts all Spring MVC (`@GetMapping`, `@PostMapping`, `@RequestMapping`, etc.) and JAX-RS (`@GET`, `@POST`, `@Path`) endpoint methods. Returns HTTP method, path, controller class, and handler method.
277
295
 
296
+ ### `spring-audit` — Spring semantic audit [free]
297
+
298
+ ```bash
299
+ sourcecode spring-audit /path/to/repo
300
+ sourcecode spring-audit /path/to/repo --scope tx # TX anomalies only
301
+ sourcecode spring-audit /path/to/repo --scope security # security surface only
302
+ sourcecode spring-audit /path/to/repo --min-severity high
303
+ ```
304
+
305
+ Detects structural Spring anomalies that survive code review and tests, but cause production failures:
306
+
307
+ | Pattern | Description |
308
+ |---------|-------------|
309
+ | `TX-001` | `@Transactional` on private/final method — CGLIB proxy bypass, TX silently ignored |
310
+ | `TX-002` | `REQUIRES_NEW` nested inside `REQUIRED` call chain — unexpected transaction nesting |
311
+ | `TX-003` | `readOnly=true` boundary propagating to write operation |
312
+ | `TX-004` | `NOT_SUPPORTED`/`NEVER` called within active TX chain |
313
+ | `TX-005` | Exception swallowing inside `@Transactional` — silent TX rollback suppression |
314
+ | `SEC-001` | Unsecured endpoint in annotation-based security model |
315
+ | `SEC-002` | CVE-2025-41248: `@PreAuthorize` on inherited method from generic supertype |
316
+ | `SEC-003` | `@Transactional` on `@Controller`/`@RestController` — TX in wrong layer |
317
+
318
+ Returns structured findings with `severity`, `confidence`, `symbol`, `source_file`, `evidence`, `explanation`, and `fix_hint`. JAVA/SPRING ONLY.
319
+
320
+ ### `impact-chain` — systemic blast radius with TX/SEC enrichment [free]
321
+
322
+ ```bash
323
+ sourcecode impact-chain OrderService /path/to/repo
324
+ sourcecode impact-chain com.example.OrderService#placeOrder /path/to/repo
325
+ sourcecode impact-chain PaymentService . --depth 6
326
+ ```
327
+
328
+ Unlike `impact` (which traces the caller graph), `impact-chain` builds on the SpringSemanticModel to enrich every step of the blast cone with transaction and security context:
329
+
330
+ | Field | Description |
331
+ |-------|-------------|
332
+ | `direct_callers` | Symbols that directly call the target |
333
+ | `indirect_callers` | Transitive callers (BFS up to `--depth` hops, default: 4) |
334
+ | `endpoints_affected` | HTTP endpoints reachable through the call chain |
335
+ | `transaction_boundary` | `@Transactional` semantics on the target: propagation, isolation, readOnly |
336
+ | `security_surfaces` | Per-endpoint security policy + SEC finding IDs |
337
+ | `impact_findings` | TX-001..005 and SEC-001..003 findings that touch the call chain |
338
+ | `risk_level` | `critical` \| `high` \| `medium` \| `low` |
339
+
340
+ **Event topology** — query the publisher/consumer graph for a Spring event class:
341
+
342
+ ```bash
343
+ sourcecode impact-chain OrderPlacedEvent /path/to/repo --type events
344
+ ```
345
+
346
+ | Field | Description |
347
+ |-------|-------------|
348
+ | `publishers` | FQNs that publish this event class |
349
+ | `consumers` | Listeners with TX phase metadata (`AFTER_COMMIT`, `BEFORE_COMMIT`, etc.) |
350
+ | `event_graph` | Publisher → event → consumer edges (BFS ≤ 2 hops) |
351
+ | `transaction_context` | `AFTER_COMMIT` consumers, `BEFORE_COMMIT` risks |
352
+ | `risk_level` | Derived from TX phase and consumer count |
353
+
354
+ **Limitations of event topology:**
355
+ - Resolves Spring `ApplicationEvent` / `@EventListener` chains only
356
+ - Does not trace Kafka, RabbitMQ, Redis, or other message brokers
357
+ - Does not detect self-invocation proxy bypass
358
+ - Conditional beans (`@ConditionalOnProperty`) are not evaluated at analysis time
359
+
360
+ ### `cold-start` — RIS bootstrap context
361
+
362
+ ```bash
363
+ sourcecode cold-start /path/to/repo
364
+ sourcecode cold-start /path/to/repo --compact # ~10K token subset
365
+ ```
366
+
367
+ Returns the Repository Intelligence Snapshot (RIS) instantly — zero re-analysis. The RIS is built by a prior warm cache pass and includes stacks, entry points, endpoint surface, and Spring semantic signals. Status field: `cold_start_ready` | `cold_start_stale` | `no_ris`.
368
+
369
+ Use `--compact` to get a ~10K token subset safe for direct LLM injection. Full snapshot can exceed 100K tokens on medium repos — use `--output FILE` for local search tooling.
370
+
278
371
  ### `repo-ir` — symbol-level IR
279
372
 
280
373
  ```bash
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "sourcecode"
7
- version = "1.35.3"
7
+ version = "1.35.5"
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.3"
3
+ __version__ = "1.35.5"
@@ -68,11 +68,23 @@ class ImplementationGraph:
68
68
  dependencies: cir.dependencies — list of edge dicts with 'from'/'to'/'type'
69
69
  known_symbols: set(cir.symbols) — only in-repo FQNs
70
70
 
71
- Excludes implements edges where the interface (to_fqn) is NOT in known_symbols
72
- (e.g. java.io.Serializable, org.springframework.* framework interfaces).
71
+ The Java parser stores 'implements' edges with the simple class name in the 'to'
72
+ field (e.g. 'OrderService') rather than the FQN. We resolve these via a
73
+ precomputed simple-name → FQN map built from known_symbols. Only unambiguous
74
+ resolutions are accepted; external framework interfaces and ambiguous names are
75
+ excluded.
76
+
73
77
  Includes edges where the implementing class (from_fqn) is NOT in known_symbols
74
78
  only when the interface IS known — this handles partial-parse edge cases.
75
79
  """
80
+ # Pre-build simple-name → [FQN] lookup for class-level symbols only (no '#').
81
+ # Used to resolve unqualified interface names (BUG-IC-001).
82
+ _simple_to_fqn: dict[str, list[str]] = {}
83
+ for sym in known_symbols:
84
+ if "#" not in sym and "." in sym:
85
+ simple = sym.rsplit(".", 1)[1]
86
+ _simple_to_fqn.setdefault(simple, []).append(sym)
87
+
76
88
  impl_of: dict[str, list[str]] = {}
77
89
  ifaces_of: dict[str, list[str]] = {}
78
90
 
@@ -83,9 +95,13 @@ class ImplementationGraph:
83
95
  to_fqn = (edge.get("to") or "").strip()
84
96
  if not from_fqn or not to_fqn:
85
97
  continue
86
- # Only track when the interface is an in-repo symbol
98
+ # Resolve to_fqn: prefer exact known-symbol match, then try simple-name lookup.
99
+ # Rejects external interfaces (java.*, org.springframework.*) and ambiguous names.
87
100
  if to_fqn not in known_symbols:
88
- continue
101
+ candidates = _simple_to_fqn.get(to_fqn, [])
102
+ if len(candidates) != 1:
103
+ continue
104
+ to_fqn = candidates[0]
89
105
  # Ignore malformed FQNs (e.g. generic type fragments like "Long>")
90
106
  if ">" in to_fqn or "<" in to_fqn:
91
107
  continue
@@ -202,10 +202,13 @@ _FILTER_SECURITY_ANNOTATIONS: frozenset[str] = frozenset({
202
202
  })
203
203
 
204
204
  # Programmatic security: method-call patterns that indicate runtime auth enforcement.
205
+ # Requires method-call or field-access context — bare class name mentions (imports,
206
+ # type declarations) must NOT match or IAM/auth-domain repos generate false positives.
205
207
  _PROGRAMMATIC_SECURITY_RE = re.compile(
206
208
  r"\b(?:hasRole|hasAuthority|isAuthenticated|requirePermission|checkPermission"
207
209
  r"|assertAuthorized|authenticate)\s*\("
208
- r"|(?:Authentication|SecurityContext|Principal|AuthorizationManager|AccessDecisionManager)\b"
210
+ r"|SecurityContextHolder\."
211
+ r"|\.(?:getAuthentication|getSecurityContext|getPrincipal|isAuthorized|checkAccess)\s*\("
209
212
  r"|throw\s+new\s+(?:AccessDeniedException|UnauthorizedException|ForbiddenException|AuthenticationException)\b",
210
213
  re.MULTILINE,
211
214
  )
@@ -323,6 +326,30 @@ _PUBLISH_EVENT_RE = re.compile(r'\.publishEvent\s*\(\s*new\s+(\w+)\s*[(\{]')
323
326
  # Keycloak SPI event fire pattern: XxxEvent.fire(session, ...)
324
327
  _FIRE_EVENT_RE = re.compile(r'\b(\w+Event)\.fire\s*\(')
325
328
 
329
+ # Class-level consumer detection from class signature (not annotations).
330
+ # Pattern 1: implements [Prefix]ApplicationListener<EventType>
331
+ # Matches both the standard Spring interface (ApplicationListener<E>) and
332
+ # framework-specific subinterfaces (BroadleafApplicationListener<E>,
333
+ # SmartApplicationListener<E>, etc.). Uses \w* prefix instead of \b so
334
+ # that "Broadleaf" prefix does not break the word boundary. (BUG-EVT-001)
335
+ _APP_LISTENER_RE = re.compile(r'\w*ApplicationListener\s*<\s*(\w+)\s*>')
336
+ # Pattern 2: extends AbstractXxxEventListener<EventType> — abstract base class pattern
337
+ # (Broadleaf's AbstractBroadleafApplicationEventListener and similar).
338
+ # Matches any parent class name that contains "EventListener".
339
+ _ABSTRACT_LISTENER_RE = re.compile(r'\bextends\s+\w+EventListener\w*\s*<\s*(\w+)\s*>')
340
+
341
+ # Block comment stripper — removes /* ... */ (including Javadoc) to prevent
342
+ # _PUBLISH_EVENT_RE / _FIRE_EVENT_RE from matching example code in comments.
343
+ _BLOCK_COMMENT_RE = re.compile(r'/\*.*?\*/', re.DOTALL)
344
+ _LINE_COMMENT_RE = re.compile(r'//[^\n]*')
345
+
346
+
347
+ def _strip_java_comments(source: str) -> str:
348
+ """Remove // line comments and /* */ block comments from Java source."""
349
+ source = _BLOCK_COMMENT_RE.sub(' ', source)
350
+ source = _LINE_COMMENT_RE.sub(' ', source)
351
+ return source
352
+
326
353
  # Edge types used for subsystem grouping — semantic hierarchy only, not imports
327
354
  _SUBSYSTEM_STRUCTURAL_EDGES: frozenset[str] = frozenset({
328
355
  "extends", "implements", "injects", "contained_in",
@@ -546,9 +573,37 @@ def _extract_symbols(source: str, rel_path: str) -> tuple[str, list[SymbolRecord
546
573
  pending_ann_values: dict[str, str] = {}
547
574
  in_block_comment = False
548
575
 
576
+ # BUG-PARSER-001: normalize multi-line class declarations where the opening brace
577
+ # appears on a continuation line (e.g. "implements A,\n B, C {").
578
+ # _CLASS_DECL_RE requires '{' on the same line as 'class' — joining the continuation
579
+ # here makes the regex work without changing the per-line brace-depth counter.
580
+ _raw_lines = source.splitlines()
581
+ _joined: list[str] = []
582
+ _i = 0
583
+ _CLASS_KW_RE = re.compile(r'\b(?:class|interface|enum)\s+[A-Z]')
584
+ while _i < len(_raw_lines):
585
+ _line = _raw_lines[_i]
586
+ _stripped = _line.strip()
587
+ if (_CLASS_KW_RE.search(_stripped) and '{' not in _stripped
588
+ and not _stripped.startswith('//')
589
+ and not _stripped.startswith('*')):
590
+ # Continuation: join until we hit a line containing '{'
591
+ _buf = _line
592
+ _i += 1
593
+ while _i < len(_raw_lines):
594
+ _cont = _raw_lines[_i]
595
+ _buf = _buf.rstrip() + ' ' + _cont.strip()
596
+ _i += 1
597
+ if '{' in _cont:
598
+ break
599
+ _joined.append(_buf)
600
+ else:
601
+ _joined.append(_line)
602
+ _i += 1
603
+
549
604
  # P1 fix: normalize multiline annotations (e.g. @RequestMapping(\n value="..."\n))
550
605
  # into single lines so the per-line regex can capture annotation args correctly.
551
- _normalized_lines = _normalize_multiline_annotations(source.splitlines())
606
+ _normalized_lines = _normalize_multiline_annotations(_joined)
552
607
 
553
608
  for line in _normalized_lines:
554
609
  stripped = line.strip()
@@ -1122,10 +1177,15 @@ def _build_relations(
1122
1177
 
1123
1178
  _class_syms = [s for s in symbols if s.type in ("class", "interface") and "#" not in s.symbol]
1124
1179
 
1180
+ # Strip comments before event scanning to prevent Javadoc examples from
1181
+ # generating false publisher edges (BUG-003).
1182
+ _source_no_comments = _strip_java_comments(source)
1183
+
1125
1184
  # Spring: class that calls publishEvent(new XxxEvent(...)) → event type FQN.
1126
- for m in _PUBLISH_EVENT_RE.finditer(source):
1185
+ for m in _PUBLISH_EVENT_RE.finditer(_source_no_comments):
1127
1186
  event_simple = m.group(1)
1128
- event_fqn = import_map.get(event_simple, event_simple)
1187
+ # BUG-004: try import_map first, then same-package map, then keep simple name.
1188
+ event_fqn = import_map.get(event_simple) or _same_pkg.get(event_simple) or event_simple
1129
1189
  for cls_sym in _class_syms:
1130
1190
  edges.append(RelationEdge(
1131
1191
  from_symbol=cls_sym.symbol,
@@ -1136,9 +1196,9 @@ def _build_relations(
1136
1196
  ))
1137
1197
 
1138
1198
  # Keycloak SPI: XxxEvent.fire(...) static dispatch → publishes_event.
1139
- for m in _FIRE_EVENT_RE.finditer(source):
1199
+ for m in _FIRE_EVENT_RE.finditer(_source_no_comments):
1140
1200
  event_simple = m.group(1)
1141
- event_fqn = import_map.get(event_simple, event_simple)
1201
+ event_fqn = import_map.get(event_simple) or _same_pkg.get(event_simple) or event_simple
1142
1202
  for cls_sym in _class_syms:
1143
1203
  edges.append(RelationEdge(
1144
1204
  from_symbol=cls_sym.symbol,
@@ -1161,6 +1221,34 @@ def _build_relations(
1161
1221
  evidence={"type": "signature", "value": f"implements {_ELP_IFACE}"},
1162
1222
  ))
1163
1223
 
1224
+ # Class-level consumer detection via class signature (EVT-003 / EVT-004).
1225
+ # Pattern A: class Foo implements ApplicationListener<XxxEvent>
1226
+ # → standard Spring interface, event type = generic param.
1227
+ # Pattern B: class Foo extends AbstractXxxEventListener<XxxEvent>
1228
+ # → abstract base class pattern (Broadleaf and similar frameworks),
1229
+ # event type = generic param of the parent class.
1230
+ for sym in _class_syms:
1231
+ sig = sym.signature or ""
1232
+ for pattern, ev_label in (
1233
+ (_APP_LISTENER_RE, "implements ApplicationListener"),
1234
+ (_ABSTRACT_LISTENER_RE, "extends *EventListener"),
1235
+ ):
1236
+ m = pattern.search(sig)
1237
+ if m:
1238
+ event_simple = m.group(1)
1239
+ event_fqn = (
1240
+ import_map.get(event_simple)
1241
+ or _same_pkg.get(event_simple)
1242
+ or event_simple
1243
+ )
1244
+ edges.append(RelationEdge(
1245
+ from_symbol=sym.symbol,
1246
+ to_symbol=event_fqn,
1247
+ type="listens_to_event",
1248
+ confidence="high",
1249
+ evidence={"type": "signature", "value": f"{ev_label}<{event_simple}>"},
1250
+ ))
1251
+
1164
1252
  seen: set[tuple[str, str, str]] = set()
1165
1253
  unique: list[RelationEdge] = []
1166
1254
  for e in edges:
@@ -2321,8 +2409,12 @@ def _assemble(
2321
2409
  for sym in _class_syms_asm
2322
2410
  )
2323
2411
  )
2412
+ # Only real annotation-based policies count (not "programmatic" fallback).
2413
+ # Programmatic security does not mean every unannotated endpoint is unsecured.
2324
2414
  _has_ann_sec_asm = any(
2325
- r.get("security_annotations") for r in _route_surface
2415
+ isinstance(r.get("security_annotations"), dict)
2416
+ and r["security_annotations"].get("policy") not in (None, "programmatic", "none_detected")
2417
+ for r in _route_surface
2326
2418
  if isinstance(r, dict)
2327
2419
  )
2328
2420
  if _filter_based_asm and _has_ann_sec_asm:
@@ -3172,7 +3264,7 @@ def extract_java_endpoints(root: Path) -> "dict[str, Any]":
3172
3264
  )
3173
3265
  )
3174
3266
  _has_annotation_security = any(
3175
- e.get("security", {}).get("policy") != "none_detected"
3267
+ e.get("security", {}).get("policy") not in (None, "none_detected", "programmatic")
3176
3268
  for e in endpoints
3177
3269
  )
3178
3270
  if _filter_based and _has_annotation_security:
@@ -289,9 +289,16 @@ def _bfs_callers(
289
289
  _add_caller(caller, depth)
290
290
  # CH-002: injects edge to a field/constructor node → also traverse
291
291
  # the containing class, bypassing the skipped contained_in edge.
292
- if etype == "injects" and "#" in caller:
293
- class_fqn = caller.rsplit("#", 1)[0]
294
- _add_caller(class_fqn, depth)
292
+ # Two formats emitted by the CIR parser:
293
+ # Constructor injection: pkg.Class#<init> (hash separator)
294
+ # Field injection: pkg.Class.field (dot, lowercase last segment)
295
+ if etype == "injects":
296
+ if "#" in caller:
297
+ _add_caller(caller.rsplit("#", 1)[0], depth)
298
+ elif "." in caller:
299
+ last_seg = caller.rsplit(".", 1)[1]
300
+ if last_seg and last_seg[0].islower():
301
+ _add_caller(caller.rsplit(".", 1)[0], depth)
295
302
 
296
303
  return direct, indirect, was_truncated
297
304
 
@@ -316,10 +323,16 @@ def _collect_endpoints(
316
323
  """
317
324
  all_fqns = set(seed_fqns) | set(all_callers)
318
325
 
319
- # Class-level seeds: controller class nodes (no '#') that are controllers.
320
- # These arise when the user queries an entire class, not a specific method.
326
+ # Class-level controller FQNs (no '#') that appear anywhere in the chain.
327
+ # Two cases produce a class-level controller node in the chain:
328
+ # 1. Seed is the controller class itself (user queried the whole controller).
329
+ # 2. Caller is the controller class node — happens when a service/repository
330
+ # is injected into a controller: the BFS reverse edge lands on the class
331
+ # node (e.g. "OwnerController" with no '#'), not a specific method.
332
+ # All endpoints of that controller are affected because any change to the
333
+ # injected dependency impacts every handler that uses it.
321
334
  class_level_controllers: set[str] = {
322
- fqn for fqn in seed_fqns
335
+ fqn for fqn in all_fqns
323
336
  if "#" not in fqn and fqn in model.endpoint_index.controller_fqns
324
337
  }
325
338
 
@@ -327,7 +340,7 @@ def _collect_endpoints(
327
340
  seen_ep_ids: set[str] = set()
328
341
 
329
342
  # Collect candidate controllers: those whose handler_symbol is in the chain
330
- # OR whose class node was a seed (class-level query).
343
+ # OR whose class node appears in the chain (class-level).
331
344
  candidate_controllers: set[str] = set(class_level_controllers)
332
345
  for fqn in all_fqns:
333
346
  cls = _class_of(fqn)
@@ -339,8 +352,8 @@ def _collect_endpoints(
339
352
  handler = getattr(ep, "handler_symbol", "") or ""
340
353
  ep_id = getattr(ep, "id", "") or ""
341
354
 
342
- # Include if: handler is directly in call chain OR controller is a
343
- # class-level seed (whole-class query, all its endpoints in scope).
355
+ # Include if: handler method is in call chain OR the controller's class
356
+ # node appears at class-level in the chain (seed or DI-injected class).
344
357
  if handler not in all_fqns and controller not in class_level_controllers:
345
358
  continue
346
359
 
@@ -561,6 +574,32 @@ class ImpactOrchestrator:
561
574
  f"added {n_syms} symbol(s) from {n_classes} implementation(s)."
562
575
  )
563
576
 
577
+ # CH-001b: expand impl seeds to include their interfaces for BFS (BUG-IC-002).
578
+ # Callers typically inject the interface type, so reverse-graph edges live on
579
+ # the interface node, not on the implementation node. Without this expansion,
580
+ # querying 'OrderServiceImpl' finds 0 callers even though 36 classes inject it.
581
+ if impl_graph is not None:
582
+ current_seed_classes = {_class_of(s) for s in seed_fqns}
583
+ iface_seeds: list[str] = []
584
+ iface_classes_added: set[str] = set()
585
+ for seed_class in sorted(current_seed_classes):
586
+ ifaces = impl_graph.interfaces_of(seed_class)
587
+ for iface_class in ifaces:
588
+ if iface_class in iface_classes_added or iface_class in current_seed_classes:
589
+ continue
590
+ iface_classes_added.add(iface_class)
591
+ for sym in cir.symbols:
592
+ if _class_of(sym) == iface_class and sym not in set(seed_fqns):
593
+ iface_seeds.append(sym)
594
+ if iface_seeds:
595
+ seed_fqns = list(dict.fromkeys(seed_fqns + iface_seeds))
596
+ n_classes = len(iface_classes_added)
597
+ n_syms = len(iface_seeds)
598
+ warnings.append(
599
+ f"Implementation-to-interface expansion (CH-001b): "
600
+ f"added {n_syms} symbol(s) from {n_classes} interface(s) for caller BFS."
601
+ )
602
+
564
603
  # ── 2. BFS through reverse graph ─────────────────────────────────
565
604
  direct_callers, indirect_callers, truncated = _bfs_callers(
566
605
  seed_fqns, cir.reverse_graph, depth
@@ -580,8 +619,13 @@ class ImpactOrchestrator:
580
619
  try:
581
620
  boundary = model.tx_index.effective_boundary(resolved_symbol)
582
621
  if boundary is None and "#" not in resolved_symbol:
583
- # Class-level symbol — try class_level directly
622
+ # Class-level symbol — try class_level directly, then fall back
623
+ # to first method-level boundary if class has only method-level TX.
584
624
  boundary = model.tx_index.class_level.get(resolved_symbol)
625
+ if boundary is None:
626
+ method_boundaries = model.tx_index.by_class.get(resolved_symbol, [])
627
+ if method_boundaries:
628
+ boundary = method_boundaries[0]
585
629
  if boundary is not None:
586
630
  tx_boundary = boundary.to_dict()
587
631
  except Exception:
@@ -63,6 +63,30 @@ _CATCH_SWALLOW_RE = re.compile(
63
63
  _RETHROW_IN_CATCH_RE = re.compile(r'\bthrow\b')
64
64
 
65
65
 
66
+ def _extract_method_body(source: str, method_name: str) -> str:
67
+ """Extract the first method body matching method_name using brace counting.
68
+
69
+ Returns the text from '{' to the matching '}', or empty string if not found.
70
+ Needed to scope TX-005 regex to the specific method instead of the whole file.
71
+ """
72
+ pattern = re.compile(r'\b' + re.escape(method_name) + r'\s*\(')
73
+ for m in pattern.finditer(source):
74
+ brace_pos = source.find('{', m.end())
75
+ if brace_pos < 0:
76
+ continue
77
+ depth = 1
78
+ i = brace_pos + 1
79
+ while i < len(source) and depth > 0:
80
+ c = source[i]
81
+ if c == '{':
82
+ depth += 1
83
+ elif c == '}':
84
+ depth -= 1
85
+ i += 1
86
+ return source[brace_pos:i]
87
+ return ""
88
+
89
+
66
90
  # ---------------------------------------------------------------------------
67
91
  # Pattern protocol
68
92
  # ---------------------------------------------------------------------------
@@ -531,8 +555,16 @@ class _TX005ExceptionSwallowing:
531
555
  return findings
532
556
 
533
557
  def _has_swallowed_exception(self, source: str, symbol: str) -> bool:
534
- """Return True if source contains a catch-log-no-rethrow pattern."""
535
- for match in _CATCH_SWALLOW_RE.finditer(source):
558
+ """Return True if the specific method body has a catch-log-no-rethrow pattern.
559
+
560
+ Scopes the search to the method body only (not the whole file) to avoid
561
+ false positives when other methods in the same file have swallowed exceptions.
562
+ """
563
+ method_name = symbol.split("#")[-1] if "#" in symbol else symbol.rsplit(".", 1)[-1]
564
+ body = _extract_method_body(source, method_name)
565
+ if not body:
566
+ return False
567
+ for match in _CATCH_SWALLOW_RE.finditer(body):
536
568
  block = match.group(0)
537
569
  if not _RETHROW_IN_CATCH_RE.search(block):
538
570
  return True
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes