sourcecode 1.35.2__tar.gz → 1.35.4__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.2 → sourcecode-1.35.4}/PKG-INFO +99 -6
  2. {sourcecode-1.35.2 → sourcecode-1.35.4}/README.md +98 -5
  3. {sourcecode-1.35.2 → sourcecode-1.35.4}/pyproject.toml +1 -1
  4. {sourcecode-1.35.2 → sourcecode-1.35.4}/src/sourcecode/__init__.py +1 -1
  5. {sourcecode-1.35.2 → sourcecode-1.35.4}/src/sourcecode/cir_graphs.py +20 -4
  6. {sourcecode-1.35.2 → sourcecode-1.35.4}/src/sourcecode/cli.py +51 -1
  7. {sourcecode-1.35.2 → sourcecode-1.35.4}/src/sourcecode/repository_ir.py +112 -13
  8. sourcecode-1.35.4/src/sourcecode/spring_event_topology.py +427 -0
  9. {sourcecode-1.35.2 → sourcecode-1.35.4}/src/sourcecode/spring_impact.py +54 -10
  10. {sourcecode-1.35.2 → sourcecode-1.35.4}/src/sourcecode/spring_tx_analyzer.py +34 -2
  11. {sourcecode-1.35.2 → sourcecode-1.35.4}/.github/workflows/build-windows.yml +0 -0
  12. {sourcecode-1.35.2 → sourcecode-1.35.4}/.gitignore +0 -0
  13. {sourcecode-1.35.2 → sourcecode-1.35.4}/.ruff.toml +0 -0
  14. {sourcecode-1.35.2 → sourcecode-1.35.4}/CHANGELOG.md +0 -0
  15. {sourcecode-1.35.2 → sourcecode-1.35.4}/CONTRIBUTING.md +0 -0
  16. {sourcecode-1.35.2 → sourcecode-1.35.4}/LICENSE +0 -0
  17. {sourcecode-1.35.2 → sourcecode-1.35.4}/SECURITY.md +0 -0
  18. {sourcecode-1.35.2 → sourcecode-1.35.4}/raw +0 -0
  19. {sourcecode-1.35.2 → sourcecode-1.35.4}/src/sourcecode/adaptive_scanner.py +0 -0
  20. {sourcecode-1.35.2 → sourcecode-1.35.4}/src/sourcecode/architecture_analyzer.py +0 -0
  21. {sourcecode-1.35.2 → sourcecode-1.35.4}/src/sourcecode/architecture_summary.py +0 -0
  22. {sourcecode-1.35.2 → sourcecode-1.35.4}/src/sourcecode/ast_extractor.py +0 -0
  23. {sourcecode-1.35.2 → sourcecode-1.35.4}/src/sourcecode/cache.py +0 -0
  24. {sourcecode-1.35.2 → sourcecode-1.35.4}/src/sourcecode/canonical_ir.py +0 -0
  25. {sourcecode-1.35.2 → sourcecode-1.35.4}/src/sourcecode/classifier.py +0 -0
  26. {sourcecode-1.35.2 → sourcecode-1.35.4}/src/sourcecode/code_notes_analyzer.py +0 -0
  27. {sourcecode-1.35.2 → sourcecode-1.35.4}/src/sourcecode/confidence_analyzer.py +0 -0
  28. {sourcecode-1.35.2 → sourcecode-1.35.4}/src/sourcecode/context_scorer.py +0 -0
  29. {sourcecode-1.35.2 → sourcecode-1.35.4}/src/sourcecode/context_summarizer.py +0 -0
  30. {sourcecode-1.35.2 → sourcecode-1.35.4}/src/sourcecode/contract_model.py +0 -0
  31. {sourcecode-1.35.2 → sourcecode-1.35.4}/src/sourcecode/contract_pipeline.py +0 -0
  32. {sourcecode-1.35.2 → sourcecode-1.35.4}/src/sourcecode/coverage_parser.py +0 -0
  33. {sourcecode-1.35.2 → sourcecode-1.35.4}/src/sourcecode/dependency_analyzer.py +0 -0
  34. {sourcecode-1.35.2 → sourcecode-1.35.4}/src/sourcecode/detectors/__init__.py +0 -0
  35. {sourcecode-1.35.2 → sourcecode-1.35.4}/src/sourcecode/detectors/base.py +0 -0
  36. {sourcecode-1.35.2 → sourcecode-1.35.4}/src/sourcecode/detectors/csproj_parser.py +0 -0
  37. {sourcecode-1.35.2 → sourcecode-1.35.4}/src/sourcecode/detectors/dart.py +0 -0
  38. {sourcecode-1.35.2 → sourcecode-1.35.4}/src/sourcecode/detectors/dotnet.py +0 -0
  39. {sourcecode-1.35.2 → sourcecode-1.35.4}/src/sourcecode/detectors/elixir.py +0 -0
  40. {sourcecode-1.35.2 → sourcecode-1.35.4}/src/sourcecode/detectors/go.py +0 -0
  41. {sourcecode-1.35.2 → sourcecode-1.35.4}/src/sourcecode/detectors/heuristic.py +0 -0
  42. {sourcecode-1.35.2 → sourcecode-1.35.4}/src/sourcecode/detectors/hybrid.py +0 -0
  43. {sourcecode-1.35.2 → sourcecode-1.35.4}/src/sourcecode/detectors/java.py +0 -0
  44. {sourcecode-1.35.2 → sourcecode-1.35.4}/src/sourcecode/detectors/jvm_ext.py +0 -0
  45. {sourcecode-1.35.2 → sourcecode-1.35.4}/src/sourcecode/detectors/nodejs.py +0 -0
  46. {sourcecode-1.35.2 → sourcecode-1.35.4}/src/sourcecode/detectors/parsers.py +0 -0
  47. {sourcecode-1.35.2 → sourcecode-1.35.4}/src/sourcecode/detectors/php.py +0 -0
  48. {sourcecode-1.35.2 → sourcecode-1.35.4}/src/sourcecode/detectors/project.py +0 -0
  49. {sourcecode-1.35.2 → sourcecode-1.35.4}/src/sourcecode/detectors/python.py +0 -0
  50. {sourcecode-1.35.2 → sourcecode-1.35.4}/src/sourcecode/detectors/ruby.py +0 -0
  51. {sourcecode-1.35.2 → sourcecode-1.35.4}/src/sourcecode/detectors/rust.py +0 -0
  52. {sourcecode-1.35.2 → sourcecode-1.35.4}/src/sourcecode/detectors/systems.py +0 -0
  53. {sourcecode-1.35.2 → sourcecode-1.35.4}/src/sourcecode/detectors/terraform.py +0 -0
  54. {sourcecode-1.35.2 → sourcecode-1.35.4}/src/sourcecode/detectors/tooling.py +0 -0
  55. {sourcecode-1.35.2 → sourcecode-1.35.4}/src/sourcecode/doc_analyzer.py +0 -0
  56. {sourcecode-1.35.2 → sourcecode-1.35.4}/src/sourcecode/entrypoint_classifier.py +0 -0
  57. {sourcecode-1.35.2 → sourcecode-1.35.4}/src/sourcecode/env_analyzer.py +0 -0
  58. {sourcecode-1.35.2 → sourcecode-1.35.4}/src/sourcecode/error_schema.py +0 -0
  59. {sourcecode-1.35.2 → sourcecode-1.35.4}/src/sourcecode/file_classifier.py +0 -0
  60. {sourcecode-1.35.2 → sourcecode-1.35.4}/src/sourcecode/flow_analyzer.py +0 -0
  61. {sourcecode-1.35.2 → sourcecode-1.35.4}/src/sourcecode/git_analyzer.py +0 -0
  62. {sourcecode-1.35.2 → sourcecode-1.35.4}/src/sourcecode/graph_analyzer.py +0 -0
  63. {sourcecode-1.35.2 → sourcecode-1.35.4}/src/sourcecode/license.py +0 -0
  64. {sourcecode-1.35.2 → sourcecode-1.35.4}/src/sourcecode/mcp/__init__.py +0 -0
  65. {sourcecode-1.35.2 → sourcecode-1.35.4}/src/sourcecode/mcp/onboarding/__init__.py +0 -0
  66. {sourcecode-1.35.2 → sourcecode-1.35.4}/src/sourcecode/mcp/onboarding/applier.py +0 -0
  67. {sourcecode-1.35.2 → sourcecode-1.35.4}/src/sourcecode/mcp/onboarding/backup.py +0 -0
  68. {sourcecode-1.35.2 → sourcecode-1.35.4}/src/sourcecode/mcp/onboarding/detector.py +0 -0
  69. {sourcecode-1.35.2 → sourcecode-1.35.4}/src/sourcecode/mcp/onboarding/planner.py +0 -0
  70. {sourcecode-1.35.2 → sourcecode-1.35.4}/src/sourcecode/mcp/orchestrator.py +0 -0
  71. {sourcecode-1.35.2 → sourcecode-1.35.4}/src/sourcecode/mcp/registry.py +0 -0
  72. {sourcecode-1.35.2 → sourcecode-1.35.4}/src/sourcecode/mcp/runner.py +0 -0
  73. {sourcecode-1.35.2 → sourcecode-1.35.4}/src/sourcecode/mcp/server.py +0 -0
  74. {sourcecode-1.35.2 → sourcecode-1.35.4}/src/sourcecode/mcp_nudge.py +0 -0
  75. {sourcecode-1.35.2 → sourcecode-1.35.4}/src/sourcecode/metrics_analyzer.py +0 -0
  76. {sourcecode-1.35.2 → sourcecode-1.35.4}/src/sourcecode/output_budget.py +0 -0
  77. {sourcecode-1.35.2 → sourcecode-1.35.4}/src/sourcecode/path_filters.py +0 -0
  78. {sourcecode-1.35.2 → sourcecode-1.35.4}/src/sourcecode/pr_comment_renderer.py +0 -0
  79. {sourcecode-1.35.2 → sourcecode-1.35.4}/src/sourcecode/prepare_context.py +0 -0
  80. {sourcecode-1.35.2 → sourcecode-1.35.4}/src/sourcecode/progress.py +0 -0
  81. {sourcecode-1.35.2 → sourcecode-1.35.4}/src/sourcecode/ranking_engine.py +0 -0
  82. {sourcecode-1.35.2 → sourcecode-1.35.4}/src/sourcecode/redactor.py +0 -0
  83. {sourcecode-1.35.2 → sourcecode-1.35.4}/src/sourcecode/relevance_scorer.py +0 -0
  84. {sourcecode-1.35.2 → sourcecode-1.35.4}/src/sourcecode/repo_classifier.py +0 -0
  85. {sourcecode-1.35.2 → sourcecode-1.35.4}/src/sourcecode/ris.py +0 -0
  86. {sourcecode-1.35.2 → sourcecode-1.35.4}/src/sourcecode/runtime_classifier.py +0 -0
  87. {sourcecode-1.35.2 → sourcecode-1.35.4}/src/sourcecode/scanner.py +0 -0
  88. {sourcecode-1.35.2 → sourcecode-1.35.4}/src/sourcecode/schema.py +0 -0
  89. {sourcecode-1.35.2 → sourcecode-1.35.4}/src/sourcecode/semantic_analyzer.py +0 -0
  90. {sourcecode-1.35.2 → sourcecode-1.35.4}/src/sourcecode/serializer.py +0 -0
  91. {sourcecode-1.35.2 → sourcecode-1.35.4}/src/sourcecode/spring_findings.py +0 -0
  92. {sourcecode-1.35.2 → sourcecode-1.35.4}/src/sourcecode/spring_model.py +0 -0
  93. {sourcecode-1.35.2 → sourcecode-1.35.4}/src/sourcecode/spring_security_audit.py +0 -0
  94. {sourcecode-1.35.2 → sourcecode-1.35.4}/src/sourcecode/spring_semantic.py +0 -0
  95. {sourcecode-1.35.2 → sourcecode-1.35.4}/src/sourcecode/summarizer.py +0 -0
  96. {sourcecode-1.35.2 → sourcecode-1.35.4}/src/sourcecode/telemetry/__init__.py +0 -0
  97. {sourcecode-1.35.2 → sourcecode-1.35.4}/src/sourcecode/telemetry/config.py +0 -0
  98. {sourcecode-1.35.2 → sourcecode-1.35.4}/src/sourcecode/telemetry/consent.py +0 -0
  99. {sourcecode-1.35.2 → sourcecode-1.35.4}/src/sourcecode/telemetry/events.py +0 -0
  100. {sourcecode-1.35.2 → sourcecode-1.35.4}/src/sourcecode/telemetry/filters.py +0 -0
  101. {sourcecode-1.35.2 → sourcecode-1.35.4}/src/sourcecode/telemetry/transport.py +0 -0
  102. {sourcecode-1.35.2 → sourcecode-1.35.4}/src/sourcecode/tree_utils.py +0 -0
  103. {sourcecode-1.35.2 → sourcecode-1.35.4}/src/sourcecode/workspace.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: sourcecode
3
- Version: 1.35.2
3
+ Version: 1.35.4
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.2-blue)
42
+ ![Version](https://img.shields.io/badge/version-1.35.4-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.4
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.2-blue)
5
+ ![Version](https://img.shields.io/badge/version-1.35.4-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.4
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.2"
7
+ version = "1.35.4"
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.2"
3
+ __version__ = "1.35.4"
@@ -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
@@ -3917,6 +3917,11 @@ def impact_chain_cmd(
3917
3917
  False, "--copy", "-c",
3918
3918
  help="Copy output to clipboard after a successful run.",
3919
3919
  ),
3920
+ query_type: str = typer.Option(
3921
+ "impact", "--type", "-t",
3922
+ help="Query type: impact (default) or events.",
3923
+ show_default=True,
3924
+ ),
3920
3925
  ) -> None:
3921
3926
  """Spring impact-chain: systemic blast radius of a symbol with TX/SEC enrichment.
3922
3927
 
@@ -3930,6 +3935,14 @@ def impact_chain_cmd(
3930
3935
  - impact_findings — TX/SEC audit findings touching the call chain
3931
3936
  - risk_level — critical | high | medium | low
3932
3937
 
3938
+ \b
3939
+ With --type events, returns event topology:
3940
+ - publishers — FQNs that publish the event class
3941
+ - consumers — listeners with TX phase metadata
3942
+ - event_graph — publisher → event → consumer edges (BFS ≤ 2)
3943
+ - transaction_context — AFTER_COMMIT consumers, BEFORE_COMMIT risks
3944
+ - risk_level — high | medium | low
3945
+
3933
3946
  \b
3934
3947
  Consumes SpringSemanticModel — zero duplicate CIR traversals.
3935
3948
  JAVA/SPRING ONLY.
@@ -3948,6 +3961,19 @@ def impact_chain_cmd(
3948
3961
  from sourcecode.spring_impact import run_impact_chain
3949
3962
  from sourcecode.spring_findings import SpringAuditResult
3950
3963
 
3964
+ _VALID_TYPES = ("impact", "events")
3965
+ if query_type not in _VALID_TYPES:
3966
+ _emit_error_json(
3967
+ INVALID_INPUT_CODE,
3968
+ f"Invalid --type '{query_type}'. Valid values: {', '.join(_VALID_TYPES)}",
3969
+ flag="--type",
3970
+ value=query_type,
3971
+ valid_values=list(_VALID_TYPES),
3972
+ hint="Use --type impact (default) or --type events.",
3973
+ expected="impact | events",
3974
+ )
3975
+ raise typer.Exit(code=1)
3976
+
3951
3977
  target = path.resolve()
3952
3978
  if not target.exists() or not target.is_dir():
3953
3979
  _emit_error_json(
@@ -3970,7 +3996,7 @@ def impact_chain_cmd(
3970
3996
 
3971
3997
  file_list = find_java_files(target)
3972
3998
  if not file_list:
3973
- data = {
3999
+ data: dict = {
3974
4000
  "schema_version": "1.0",
3975
4001
  "symbol": symbol,
3976
4002
  "resolution": "not_found",
@@ -3991,6 +4017,30 @@ def impact_chain_cmd(
3991
4017
 
3992
4018
  cir = build_canonical_ir(file_list, target)
3993
4019
  _model = SpringSemanticModel.build(cir)
4020
+
4021
+ if query_type == "events":
4022
+ from sourcecode.spring_event_topology import run_event_topology
4023
+ evt_result = run_event_topology(cir, symbol, model=_model)
4024
+ data = evt_result.to_dict()
4025
+ output = _serialize_dict(data, format)
4026
+ if output_path is not None:
4027
+ output_path.write_text(output, encoding="utf-8")
4028
+ typer.echo(
4029
+ f"Event topology written to {output_path} "
4030
+ f"(risk: {evt_result.risk_level}, "
4031
+ f"{evt_result.metadata.get('publisher_count', 0)} publishers, "
4032
+ f"{evt_result.metadata.get('consumer_count', 0)} consumers)",
4033
+ err=True,
4034
+ )
4035
+ else:
4036
+ sys.stdout.buffer.write(output.encode("utf-8"))
4037
+ sys.stdout.buffer.write(b"\n")
4038
+ sys.stdout.buffer.flush()
4039
+ if copy:
4040
+ if _copy_to_clipboard(output):
4041
+ typer.echo("✓ copied to clipboard", err=True)
4042
+ return
4043
+
3994
4044
  result = run_impact_chain(cir, symbol, depth=depth, root=target, model=_model)
3995
4045
 
3996
4046
  data = result.to_dict()
@@ -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
  )
@@ -239,7 +242,7 @@ _LOMBOK_CTOR_ANNOTATIONS: frozenset[str] = frozenset({
239
242
  })
240
243
 
241
244
  # Transaction annotations whose args must be captured for semantic analysis.
242
- _TX_ANNOTATIONS: frozenset[str] = frozenset({"@Transactional"})
245
+ _TX_ANNOTATIONS: frozenset[str] = frozenset({"@Transactional", "@TransactionalEventListener"})
243
246
 
244
247
  # Combined set used in _extract_symbols annotation-value capture.
245
248
  _CAPTURE_ANN_ARGS: frozenset[str] = (
@@ -307,7 +310,9 @@ _SPRING_OTHER: frozenset[str] = frozenset({
307
310
  "@PutMapping", "@DeleteMapping", "@PatchMapping", "@Autowired",
308
311
  "@Inject", "@Value", "@Qualifier", "@EnableWebSecurity",
309
312
  "@SpringBootApplication", "@EnableAutoConfiguration",
310
- "@EventListener", "@Async", "@Scheduled", "@Cacheable", "@CacheEvict",
313
+ "@EventListener", "@TransactionalEventListener",
314
+ "@KafkaListener", "@RabbitListener",
315
+ "@Async", "@Scheduled", "@Cacheable", "@CacheEvict",
311
316
  # CDI / Jakarta EE
312
317
  "@ApplicationScoped", "@RequestScoped", "@SessionScoped", "@Dependent",
313
318
  "@Named", "@Produces", "@Consumes",
@@ -321,6 +326,30 @@ _PUBLISH_EVENT_RE = re.compile(r'\.publishEvent\s*\(\s*new\s+(\w+)\s*[(\{]')
321
326
  # Keycloak SPI event fire pattern: XxxEvent.fire(session, ...)
322
327
  _FIRE_EVENT_RE = re.compile(r'\b(\w+Event)\.fire\s*\(')
323
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
+
324
353
  # Edge types used for subsystem grouping — semantic hierarchy only, not imports
325
354
  _SUBSYSTEM_STRUCTURAL_EDGES: frozenset[str] = frozenset({
326
355
  "extends", "implements", "injects", "contained_in",
@@ -544,9 +573,37 @@ def _extract_symbols(source: str, rel_path: str) -> tuple[str, list[SymbolRecord
544
573
  pending_ann_values: dict[str, str] = {}
545
574
  in_block_comment = False
546
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
+
547
604
  # P1 fix: normalize multiline annotations (e.g. @RequestMapping(\n value="..."\n))
548
605
  # into single lines so the per-line regex can capture annotation args correctly.
549
- _normalized_lines = _normalize_multiline_annotations(source.splitlines())
606
+ _normalized_lines = _normalize_multiline_annotations(_joined)
550
607
 
551
608
  for line in _normalized_lines:
552
609
  stripped = line.strip()
@@ -1101,24 +1158,34 @@ def _build_relations(
1101
1158
  ))
1102
1159
 
1103
1160
  # Event flow edges — listens_to_event and publishes_event.
1104
- # Spring: method with @EventListener → resolved event parameter type(s).
1161
+ # Spring: method with @EventListener or @TransactionalEventListener → resolved event type(s).
1162
+ _LISTENER_ANNOTATIONS: frozenset[str] = frozenset({
1163
+ "@EventListener", "@TransactionalEventListener",
1164
+ })
1105
1165
  for sym in symbols:
1106
- if sym.type == "method" and "@EventListener" in sym.annotations:
1166
+ if sym.type == "method" and (sym.annotations and
1167
+ any(a in _LISTENER_ANNOTATIONS for a in sym.annotations)):
1168
+ ann = next(a for a in sym.annotations if a in _LISTENER_ANNOTATIONS)
1107
1169
  for imp_fqn in sym.imports_used:
1108
1170
  edges.append(RelationEdge(
1109
1171
  from_symbol=sym.symbol,
1110
1172
  to_symbol=imp_fqn,
1111
1173
  type="listens_to_event",
1112
1174
  confidence="high",
1113
- evidence={"type": "annotation", "value": "@EventListener"},
1175
+ evidence={"type": "annotation", "value": ann},
1114
1176
  ))
1115
1177
 
1116
1178
  _class_syms = [s for s in symbols if s.type in ("class", "interface") and "#" not in s.symbol]
1117
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
+
1118
1184
  # Spring: class that calls publishEvent(new XxxEvent(...)) → event type FQN.
1119
- for m in _PUBLISH_EVENT_RE.finditer(source):
1185
+ for m in _PUBLISH_EVENT_RE.finditer(_source_no_comments):
1120
1186
  event_simple = m.group(1)
1121
- 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
1122
1189
  for cls_sym in _class_syms:
1123
1190
  edges.append(RelationEdge(
1124
1191
  from_symbol=cls_sym.symbol,
@@ -1129,9 +1196,9 @@ def _build_relations(
1129
1196
  ))
1130
1197
 
1131
1198
  # Keycloak SPI: XxxEvent.fire(...) static dispatch → publishes_event.
1132
- for m in _FIRE_EVENT_RE.finditer(source):
1199
+ for m in _FIRE_EVENT_RE.finditer(_source_no_comments):
1133
1200
  event_simple = m.group(1)
1134
- 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
1135
1202
  for cls_sym in _class_syms:
1136
1203
  edges.append(RelationEdge(
1137
1204
  from_symbol=cls_sym.symbol,
@@ -1154,6 +1221,34 @@ def _build_relations(
1154
1221
  evidence={"type": "signature", "value": f"implements {_ELP_IFACE}"},
1155
1222
  ))
1156
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
+
1157
1252
  seen: set[tuple[str, str, str]] = set()
1158
1253
  unique: list[RelationEdge] = []
1159
1254
  for e in edges:
@@ -2314,8 +2409,12 @@ def _assemble(
2314
2409
  for sym in _class_syms_asm
2315
2410
  )
2316
2411
  )
2412
+ # Only real annotation-based policies count (not "programmatic" fallback).
2413
+ # Programmatic security does not mean every unannotated endpoint is unsecured.
2317
2414
  _has_ann_sec_asm = any(
2318
- 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
2319
2418
  if isinstance(r, dict)
2320
2419
  )
2321
2420
  if _filter_based_asm and _has_ann_sec_asm:
@@ -3165,7 +3264,7 @@ def extract_java_endpoints(root: Path) -> "dict[str, Any]":
3165
3264
  )
3166
3265
  )
3167
3266
  _has_annotation_security = any(
3168
- e.get("security", {}).get("policy") != "none_detected"
3267
+ e.get("security", {}).get("policy") not in (None, "none_detected", "programmatic")
3169
3268
  for e in endpoints
3170
3269
  )
3171
3270
  if _filter_based and _has_annotation_security: