sourcecode 2.6.3__tar.gz → 2.6.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 (172) hide show
  1. {sourcecode-2.6.3 → sourcecode-2.6.5}/CHANGELOG.md +65 -0
  2. {sourcecode-2.6.3 → sourcecode-2.6.5}/PKG-INFO +1 -1
  3. {sourcecode-2.6.3 → sourcecode-2.6.5}/pyproject.toml +1 -1
  4. {sourcecode-2.6.3 → sourcecode-2.6.5}/src/sourcecode/__init__.py +1 -1
  5. {sourcecode-2.6.3 → sourcecode-2.6.5}/src/sourcecode/cli.py +71 -3
  6. {sourcecode-2.6.3 → sourcecode-2.6.5}/src/sourcecode/repository_ir.py +15 -0
  7. {sourcecode-2.6.3 → sourcecode-2.6.5}/src/sourcecode/spring_impact.py +1 -1
  8. {sourcecode-2.6.3 → sourcecode-2.6.5}/src/sourcecode/spring_tx_analyzer.py +141 -4
  9. {sourcecode-2.6.3 → sourcecode-2.6.5}/.github/workflows/build-windows.yml +0 -0
  10. {sourcecode-2.6.3 → sourcecode-2.6.5}/.gitignore +0 -0
  11. {sourcecode-2.6.3 → sourcecode-2.6.5}/.ruff.toml +0 -0
  12. {sourcecode-2.6.3 → sourcecode-2.6.5}/CONTRIBUTING.md +0 -0
  13. {sourcecode-2.6.3 → sourcecode-2.6.5}/LICENSE +0 -0
  14. {sourcecode-2.6.3 → sourcecode-2.6.5}/README.md +0 -0
  15. {sourcecode-2.6.3 → sourcecode-2.6.5}/SECURITY.md +0 -0
  16. {sourcecode-2.6.3 → sourcecode-2.6.5}/raw +0 -0
  17. {sourcecode-2.6.3 → sourcecode-2.6.5}/scripts/compare_integration_engines.py +0 -0
  18. {sourcecode-2.6.3 → sourcecode-2.6.5}/scripts/customer_smoke_test.sh +0 -0
  19. {sourcecode-2.6.3 → sourcecode-2.6.5}/scripts/generate_jdk_exports.py +0 -0
  20. {sourcecode-2.6.3 → sourcecode-2.6.5}/scripts/perf_harness.py +0 -0
  21. {sourcecode-2.6.3 → sourcecode-2.6.5}/src/sourcecode/adaptive_scanner.py +0 -0
  22. {sourcecode-2.6.3 → sourcecode-2.6.5}/src/sourcecode/archetype.py +0 -0
  23. {sourcecode-2.6.3 → sourcecode-2.6.5}/src/sourcecode/architectural_baseline.py +0 -0
  24. {sourcecode-2.6.3 → sourcecode-2.6.5}/src/sourcecode/architectural_delta.py +0 -0
  25. {sourcecode-2.6.3 → sourcecode-2.6.5}/src/sourcecode/architecture_analyzer.py +0 -0
  26. {sourcecode-2.6.3 → sourcecode-2.6.5}/src/sourcecode/architecture_summary.py +0 -0
  27. {sourcecode-2.6.3 → sourcecode-2.6.5}/src/sourcecode/ast_extractor.py +0 -0
  28. {sourcecode-2.6.3 → sourcecode-2.6.5}/src/sourcecode/cache.py +0 -0
  29. {sourcecode-2.6.3 → sourcecode-2.6.5}/src/sourcecode/call_surface.py +0 -0
  30. {sourcecode-2.6.3 → sourcecode-2.6.5}/src/sourcecode/caller_metrics.py +0 -0
  31. {sourcecode-2.6.3 → sourcecode-2.6.5}/src/sourcecode/canonical_ir.py +0 -0
  32. {sourcecode-2.6.3 → sourcecode-2.6.5}/src/sourcecode/change_plan.py +0 -0
  33. {sourcecode-2.6.3 → sourcecode-2.6.5}/src/sourcecode/cir_graphs.py +0 -0
  34. {sourcecode-2.6.3 → sourcecode-2.6.5}/src/sourcecode/classifier.py +0 -0
  35. {sourcecode-2.6.3 → sourcecode-2.6.5}/src/sourcecode/code_notes_analyzer.py +0 -0
  36. {sourcecode-2.6.3 → sourcecode-2.6.5}/src/sourcecode/compare.py +0 -0
  37. {sourcecode-2.6.3 → sourcecode-2.6.5}/src/sourcecode/confidence_analyzer.py +0 -0
  38. {sourcecode-2.6.3 → sourcecode-2.6.5}/src/sourcecode/constraint_diff.py +0 -0
  39. {sourcecode-2.6.3 → sourcecode-2.6.5}/src/sourcecode/context_cache.py +0 -0
  40. {sourcecode-2.6.3 → sourcecode-2.6.5}/src/sourcecode/context_graph.py +0 -0
  41. {sourcecode-2.6.3 → sourcecode-2.6.5}/src/sourcecode/context_scorer.py +0 -0
  42. {sourcecode-2.6.3 → sourcecode-2.6.5}/src/sourcecode/context_summarizer.py +0 -0
  43. {sourcecode-2.6.3 → sourcecode-2.6.5}/src/sourcecode/contract_diff.py +0 -0
  44. {sourcecode-2.6.3 → sourcecode-2.6.5}/src/sourcecode/contract_model.py +0 -0
  45. {sourcecode-2.6.3 → sourcecode-2.6.5}/src/sourcecode/contract_pipeline.py +0 -0
  46. {sourcecode-2.6.3 → sourcecode-2.6.5}/src/sourcecode/coverage_parser.py +0 -0
  47. {sourcecode-2.6.3 → sourcecode-2.6.5}/src/sourcecode/dependency_analyzer.py +0 -0
  48. {sourcecode-2.6.3 → sourcecode-2.6.5}/src/sourcecode/detectors/__init__.py +0 -0
  49. {sourcecode-2.6.3 → sourcecode-2.6.5}/src/sourcecode/detectors/base.py +0 -0
  50. {sourcecode-2.6.3 → sourcecode-2.6.5}/src/sourcecode/detectors/csproj_parser.py +0 -0
  51. {sourcecode-2.6.3 → sourcecode-2.6.5}/src/sourcecode/detectors/dart.py +0 -0
  52. {sourcecode-2.6.3 → sourcecode-2.6.5}/src/sourcecode/detectors/dotnet.py +0 -0
  53. {sourcecode-2.6.3 → sourcecode-2.6.5}/src/sourcecode/detectors/elixir.py +0 -0
  54. {sourcecode-2.6.3 → sourcecode-2.6.5}/src/sourcecode/detectors/go.py +0 -0
  55. {sourcecode-2.6.3 → sourcecode-2.6.5}/src/sourcecode/detectors/heuristic.py +0 -0
  56. {sourcecode-2.6.3 → sourcecode-2.6.5}/src/sourcecode/detectors/hybrid.py +0 -0
  57. {sourcecode-2.6.3 → sourcecode-2.6.5}/src/sourcecode/detectors/java.py +0 -0
  58. {sourcecode-2.6.3 → sourcecode-2.6.5}/src/sourcecode/detectors/jvm_ext.py +0 -0
  59. {sourcecode-2.6.3 → sourcecode-2.6.5}/src/sourcecode/detectors/nodejs.py +0 -0
  60. {sourcecode-2.6.3 → sourcecode-2.6.5}/src/sourcecode/detectors/parsers.py +0 -0
  61. {sourcecode-2.6.3 → sourcecode-2.6.5}/src/sourcecode/detectors/php.py +0 -0
  62. {sourcecode-2.6.3 → sourcecode-2.6.5}/src/sourcecode/detectors/project.py +0 -0
  63. {sourcecode-2.6.3 → sourcecode-2.6.5}/src/sourcecode/detectors/python.py +0 -0
  64. {sourcecode-2.6.3 → sourcecode-2.6.5}/src/sourcecode/detectors/ruby.py +0 -0
  65. {sourcecode-2.6.3 → sourcecode-2.6.5}/src/sourcecode/detectors/rust.py +0 -0
  66. {sourcecode-2.6.3 → sourcecode-2.6.5}/src/sourcecode/detectors/systems.py +0 -0
  67. {sourcecode-2.6.3 → sourcecode-2.6.5}/src/sourcecode/detectors/terraform.py +0 -0
  68. {sourcecode-2.6.3 → sourcecode-2.6.5}/src/sourcecode/detectors/tooling.py +0 -0
  69. {sourcecode-2.6.3 → sourcecode-2.6.5}/src/sourcecode/doc_analyzer.py +0 -0
  70. {sourcecode-2.6.3 → sourcecode-2.6.5}/src/sourcecode/dynamic_argument_surface.py +0 -0
  71. {sourcecode-2.6.3 → sourcecode-2.6.5}/src/sourcecode/endpoint_literals.py +0 -0
  72. {sourcecode-2.6.3 → sourcecode-2.6.5}/src/sourcecode/endpoint_metrics.py +0 -0
  73. {sourcecode-2.6.3 → sourcecode-2.6.5}/src/sourcecode/entrypoint_classifier.py +0 -0
  74. {sourcecode-2.6.3 → sourcecode-2.6.5}/src/sourcecode/env_analyzer.py +0 -0
  75. {sourcecode-2.6.3 → sourcecode-2.6.5}/src/sourcecode/error_schema.py +0 -0
  76. {sourcecode-2.6.3 → sourcecode-2.6.5}/src/sourcecode/evidence_provider.py +0 -0
  77. {sourcecode-2.6.3 → sourcecode-2.6.5}/src/sourcecode/explain.py +0 -0
  78. {sourcecode-2.6.3 → sourcecode-2.6.5}/src/sourcecode/file_chunker.py +0 -0
  79. {sourcecode-2.6.3 → sourcecode-2.6.5}/src/sourcecode/file_classifier.py +0 -0
  80. {sourcecode-2.6.3 → sourcecode-2.6.5}/src/sourcecode/filter_surface.py +0 -0
  81. {sourcecode-2.6.3 → sourcecode-2.6.5}/src/sourcecode/format_contract.py +0 -0
  82. {sourcecode-2.6.3 → sourcecode-2.6.5}/src/sourcecode/fqn_utils.py +0 -0
  83. {sourcecode-2.6.3 → sourcecode-2.6.5}/src/sourcecode/git_analyzer.py +0 -0
  84. {sourcecode-2.6.3 → sourcecode-2.6.5}/src/sourcecode/graph_analyzer.py +0 -0
  85. {sourcecode-2.6.3 → sourcecode-2.6.5}/src/sourcecode/graph_evidence.py +0 -0
  86. {sourcecode-2.6.3 → sourcecode-2.6.5}/src/sourcecode/hibernate_strat.py +0 -0
  87. {sourcecode-2.6.3 → sourcecode-2.6.5}/src/sourcecode/jdk_exports.py +0 -0
  88. {sourcecode-2.6.3 → sourcecode-2.6.5}/src/sourcecode/license.py +0 -0
  89. {sourcecode-2.6.3 → sourcecode-2.6.5}/src/sourcecode/mcp/__init__.py +0 -0
  90. {sourcecode-2.6.3 → sourcecode-2.6.5}/src/sourcecode/mcp/onboarding/__init__.py +0 -0
  91. {sourcecode-2.6.3 → sourcecode-2.6.5}/src/sourcecode/mcp/onboarding/applier.py +0 -0
  92. {sourcecode-2.6.3 → sourcecode-2.6.5}/src/sourcecode/mcp/onboarding/backup.py +0 -0
  93. {sourcecode-2.6.3 → sourcecode-2.6.5}/src/sourcecode/mcp/onboarding/detector.py +0 -0
  94. {sourcecode-2.6.3 → sourcecode-2.6.5}/src/sourcecode/mcp/onboarding/planner.py +0 -0
  95. {sourcecode-2.6.3 → sourcecode-2.6.5}/src/sourcecode/mcp/orchestrator.py +0 -0
  96. {sourcecode-2.6.3 → sourcecode-2.6.5}/src/sourcecode/mcp/registry.py +0 -0
  97. {sourcecode-2.6.3 → sourcecode-2.6.5}/src/sourcecode/mcp/runner.py +0 -0
  98. {sourcecode-2.6.3 → sourcecode-2.6.5}/src/sourcecode/mcp/server.py +0 -0
  99. {sourcecode-2.6.3 → sourcecode-2.6.5}/src/sourcecode/mcp_nudge.py +0 -0
  100. {sourcecode-2.6.3 → sourcecode-2.6.5}/src/sourcecode/metrics_analyzer.py +0 -0
  101. {sourcecode-2.6.3 → sourcecode-2.6.5}/src/sourcecode/migrate_check.py +0 -0
  102. {sourcecode-2.6.3 → sourcecode-2.6.5}/src/sourcecode/openapi_surface.py +0 -0
  103. {sourcecode-2.6.3 → sourcecode-2.6.5}/src/sourcecode/output_budget.py +0 -0
  104. {sourcecode-2.6.3 → sourcecode-2.6.5}/src/sourcecode/parse_cache.py +0 -0
  105. {sourcecode-2.6.3 → sourcecode-2.6.5}/src/sourcecode/path_filters.py +0 -0
  106. {sourcecode-2.6.3 → sourcecode-2.6.5}/src/sourcecode/perf.py +0 -0
  107. {sourcecode-2.6.3 → sourcecode-2.6.5}/src/sourcecode/pr_comment_renderer.py +0 -0
  108. {sourcecode-2.6.3 → sourcecode-2.6.5}/src/sourcecode/pr_impact.py +0 -0
  109. {sourcecode-2.6.3 → sourcecode-2.6.5}/src/sourcecode/prepare_context.py +0 -0
  110. {sourcecode-2.6.3 → sourcecode-2.6.5}/src/sourcecode/progress.py +0 -0
  111. {sourcecode-2.6.3 → sourcecode-2.6.5}/src/sourcecode/ranking_engine.py +0 -0
  112. {sourcecode-2.6.3 → sourcecode-2.6.5}/src/sourcecode/reconciliation.py +0 -0
  113. {sourcecode-2.6.3 → sourcecode-2.6.5}/src/sourcecode/redactor.py +0 -0
  114. {sourcecode-2.6.3 → sourcecode-2.6.5}/src/sourcecode/relevance_scorer.py +0 -0
  115. {sourcecode-2.6.3 → sourcecode-2.6.5}/src/sourcecode/rename_refactor.py +0 -0
  116. {sourcecode-2.6.3 → sourcecode-2.6.5}/src/sourcecode/repo_classifier.py +0 -0
  117. {sourcecode-2.6.3 → sourcecode-2.6.5}/src/sourcecode/retrieval/__init__.py +0 -0
  118. {sourcecode-2.6.3 → sourcecode-2.6.5}/src/sourcecode/retrieval/context.py +0 -0
  119. {sourcecode-2.6.3 → sourcecode-2.6.5}/src/sourcecode/retrieval/errors.py +0 -0
  120. {sourcecode-2.6.3 → sourcecode-2.6.5}/src/sourcecode/retrieval/executor.py +0 -0
  121. {sourcecode-2.6.3 → sourcecode-2.6.5}/src/sourcecode/retrieval/planner.py +0 -0
  122. {sourcecode-2.6.3 → sourcecode-2.6.5}/src/sourcecode/retrieval/query.py +0 -0
  123. {sourcecode-2.6.3 → sourcecode-2.6.5}/src/sourcecode/retrieval/request.py +0 -0
  124. {sourcecode-2.6.3 → sourcecode-2.6.5}/src/sourcecode/retrieval/resolution.py +0 -0
  125. {sourcecode-2.6.3 → sourcecode-2.6.5}/src/sourcecode/retrieval/result.py +0 -0
  126. {sourcecode-2.6.3 → sourcecode-2.6.5}/src/sourcecode/retrieval/retriever.py +0 -0
  127. {sourcecode-2.6.3 → sourcecode-2.6.5}/src/sourcecode/retrieval/runtime.py +0 -0
  128. {sourcecode-2.6.3 → sourcecode-2.6.5}/src/sourcecode/retrieval/steps.py +0 -0
  129. {sourcecode-2.6.3 → sourcecode-2.6.5}/src/sourcecode/retrieval/steps_endpoint.py +0 -0
  130. {sourcecode-2.6.3 → sourcecode-2.6.5}/src/sourcecode/retrieval/steps_graph.py +0 -0
  131. {sourcecode-2.6.3 → sourcecode-2.6.5}/src/sourcecode/retrieval/steps_impact.py +0 -0
  132. {sourcecode-2.6.3 → sourcecode-2.6.5}/src/sourcecode/retrieval/steps_intf.py +0 -0
  133. {sourcecode-2.6.3 → sourcecode-2.6.5}/src/sourcecode/retrieval/steps_struct.py +0 -0
  134. {sourcecode-2.6.3 → sourcecode-2.6.5}/src/sourcecode/retrieval/steps_txsec.py +0 -0
  135. {sourcecode-2.6.3 → sourcecode-2.6.5}/src/sourcecode/ris.py +0 -0
  136. {sourcecode-2.6.3 → sourcecode-2.6.5}/src/sourcecode/runtime_classifier.py +0 -0
  137. {sourcecode-2.6.3 → sourcecode-2.6.5}/src/sourcecode/scanner.py +0 -0
  138. {sourcecode-2.6.3 → sourcecode-2.6.5}/src/sourcecode/schema.py +0 -0
  139. {sourcecode-2.6.3 → sourcecode-2.6.5}/src/sourcecode/security_config.py +0 -0
  140. {sourcecode-2.6.3 → sourcecode-2.6.5}/src/sourcecode/security_posture.py +0 -0
  141. {sourcecode-2.6.3 → sourcecode-2.6.5}/src/sourcecode/semantic_analyzer.py +0 -0
  142. {sourcecode-2.6.3 → sourcecode-2.6.5}/src/sourcecode/semantic_impact_engine.py +0 -0
  143. {sourcecode-2.6.3 → sourcecode-2.6.5}/src/sourcecode/semantic_integration_engine.py +0 -0
  144. {sourcecode-2.6.3 → sourcecode-2.6.5}/src/sourcecode/semantic_services.py +0 -0
  145. {sourcecode-2.6.3 → sourcecode-2.6.5}/src/sourcecode/serializer.py +0 -0
  146. {sourcecode-2.6.3 → sourcecode-2.6.5}/src/sourcecode/spring_event_topology.py +0 -0
  147. {sourcecode-2.6.3 → sourcecode-2.6.5}/src/sourcecode/spring_findings.py +0 -0
  148. {sourcecode-2.6.3 → sourcecode-2.6.5}/src/sourcecode/spring_model.py +0 -0
  149. {sourcecode-2.6.3 → sourcecode-2.6.5}/src/sourcecode/spring_security_audit.py +0 -0
  150. {sourcecode-2.6.3 → sourcecode-2.6.5}/src/sourcecode/spring_semantic.py +0 -0
  151. {sourcecode-2.6.3 → sourcecode-2.6.5}/src/sourcecode/summarizer.py +0 -0
  152. {sourcecode-2.6.3 → sourcecode-2.6.5}/src/sourcecode/telemetry/__init__.py +0 -0
  153. {sourcecode-2.6.3 → sourcecode-2.6.5}/src/sourcecode/telemetry/config.py +0 -0
  154. {sourcecode-2.6.3 → sourcecode-2.6.5}/src/sourcecode/telemetry/consent.py +0 -0
  155. {sourcecode-2.6.3 → sourcecode-2.6.5}/src/sourcecode/telemetry/events.py +0 -0
  156. {sourcecode-2.6.3 → sourcecode-2.6.5}/src/sourcecode/telemetry/filters.py +0 -0
  157. {sourcecode-2.6.3 → sourcecode-2.6.5}/src/sourcecode/telemetry/transport.py +0 -0
  158. {sourcecode-2.6.3 → sourcecode-2.6.5}/src/sourcecode/tree_utils.py +0 -0
  159. {sourcecode-2.6.3 → sourcecode-2.6.5}/src/sourcecode/type_usage_surface.py +0 -0
  160. {sourcecode-2.6.3 → sourcecode-2.6.5}/src/sourcecode/validation_inference.py +0 -0
  161. {sourcecode-2.6.3 → sourcecode-2.6.5}/src/sourcecode/validation_surface.py +0 -0
  162. {sourcecode-2.6.3 → sourcecode-2.6.5}/src/sourcecode/verify_edit.py +0 -0
  163. {sourcecode-2.6.3 → sourcecode-2.6.5}/src/sourcecode/verify_rules.py +0 -0
  164. {sourcecode-2.6.3 → sourcecode-2.6.5}/src/sourcecode/version_check.py +0 -0
  165. {sourcecode-2.6.3 → sourcecode-2.6.5}/src/sourcecode/workspace.py +0 -0
  166. {sourcecode-2.6.3 → sourcecode-2.6.5}/supabase/functions/README.md +0 -0
  167. {sourcecode-2.6.3 → sourcecode-2.6.5}/supabase/functions/get-license/index.ts +0 -0
  168. {sourcecode-2.6.3 → sourcecode-2.6.5}/supabase/functions/lemonsqueezy-webhook/index.ts +0 -0
  169. {sourcecode-2.6.3 → sourcecode-2.6.5}/supabase/functions/telemetry/index.ts +0 -0
  170. {sourcecode-2.6.3 → sourcecode-2.6.5}/supabase/sql/license_event_ordering.sql +0 -0
  171. {sourcecode-2.6.3 → sourcecode-2.6.5}/supabase/sql/licensing_schema.sql +0 -0
  172. {sourcecode-2.6.3 → sourcecode-2.6.5}/supabase/sql/telemetry_events.sql +0 -0
@@ -1,5 +1,70 @@
1
1
  # Changelog
2
2
 
3
+ ## [2.6.5] — 2026-07-24
4
+
5
+ **Mistyped-subcommand ergonomics: `ask` no longer swallows a typo'd command as a path
6
+ and then blames the wrong token.** CLI-only; no analysis behaviour changes.
7
+
8
+ ### Fixed
9
+
10
+ - **A mistyped command now names the token the user got wrong, and suggests the real
11
+ command.** `ask` accepts a bare repository path (`ask ./repo`) as an implicit scan, so
12
+ a misspelled subcommand used to be swallowed as a path and surface a misleading error
13
+ about the *next* token — `ask notacommand /tmp` complained `No such command '/tmp'` (the
14
+ path), and `ask spring-audi` complained the directory did not exist. Now:
15
+ - a first positional followed by another non-subcommand positional
16
+ (`ask notacommand /tmp`) is rejected as an unknown command (exit `2`), naming the
17
+ first token and — when it is close to a real command — suggesting it;
18
+ - a lone token that does not resolve to a directory but closely matches a command
19
+ (`ask spring-audi`) adds a *did you mean `ask spring-audit`* hint to the path error.
20
+
21
+ The implicit-scan contract is unchanged: a single positional is still treated as a
22
+ repository path with existence validated downstream, and the documented
23
+ `ask <subcommand> <path>` order (and the `ask <path> <subcommand>` rejection) are
24
+ untouched.
25
+
26
+ ## [2.6.4] — 2026-07-24
27
+
28
+ **TX-006: self-invocation proxy-bypass detection — a new Spring transaction rule that
29
+ also participates automatically in the `verify-edit` write-loop gate.** Plus a root-cause
30
+ false-positive fix in body-fact extraction and one internal-id output leak removed. No
31
+ change to any existing command's contract. Suite: **3742 passed, 5 skipped, 1 deselected**
32
+ (the deselected chunker test is a pre-existing external-fixture flake).
33
+
34
+ ### Added
35
+
36
+ - **TX-006 — self-invocation of a `@Transactional` sibling (Spring proxy bypass).** A
37
+ same-instance call — an unqualified `foo()` or an explicit `this.foo()` to a method of
38
+ the same class — does not pass through the Spring proxy, so the target's method-level
39
+ `@Transactional` is silently ignored at runtime (the Sonar S2229 class of bug). Detection
40
+ is structural and vendor-agnostic, reading the Semantic-IR `invocation` atoms already
41
+ produced by the pipeline: the receiver is `None`/`this` and the call resolves to an
42
+ unambiguous same-class sibling that carries a method-level `@Transactional`. False-positive
43
+ guards are built in — overloaded/ambiguous targets are skipped, class-level-only
44
+ `@Transactional` is not flagged (the proxy already covers the outer entry method), and the
45
+ legitimate self-injection escape hatch (`self.foo()` through an injected same-type field)
46
+ uses a field receiver and never resolves as a sibling. Severity is tiered: `high` only when
47
+ the callee propagation mandates a distinct boundary (`REQUIRES_NEW`/`NESTED`/`NOT_SUPPORTED`/
48
+ `NEVER`/`MANDATORY`); a plain `REQUIRED`/`SUPPORTS` callee or a `readOnly` getter is `medium`.
49
+ Surfaced by `ask spring-audit` and, with no additional wiring, by the `verify-edit`
50
+ `tx_boundary_changed` axis — an edit that *introduces* a self-invocation now breaks the
51
+ write-loop verdict before commit, while a pre-existing one never fails an unrelated change.
52
+
53
+ ### Fixed
54
+
55
+ - **Chained-call receivers no longer read as self-calls (body-fact extraction).** A
56
+ method-chained invocation such as `Context.getBean().method()` left the receiver empty
57
+ because the receiver is an expression, not a bare identifier — causing it to be mistaken
58
+ for an unqualified self-call. A callee immediately preceded by `.` is now marked as a
59
+ non-self expression receiver. This is a general sibling-resolution correctness fix
60
+ (it also prevents a chained sibling-named call from being mis-resolved to a call edge);
61
+ `body_facts` is not a `cir_hash` input, so the fix is hash-neutral. It removed roughly
62
+ half of the raw TX-006 candidates on a real Spring codebase — all of them true bypass
63
+ false-positives, including a service that correctly routes `REQUIRES_NEW` through a
64
+ service-locator proxy.
65
+ - **Internal bug id removed from user-facing output.** The self-referential-exclusion
66
+ warning in `impact` no longer prints the internal `(BUG #2)` tag; the explanation stays.
67
+
3
68
  ## [2.6.3] — 2026-07-24
4
69
 
5
70
  **`ask verify-edit` — the in-loop semantic diff gate lands (the read-context → write-loop
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: sourcecode
3
- Version: 2.6.3
3
+ Version: 2.6.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
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "sourcecode"
7
- version = "2.6.3"
7
+ version = "2.6.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"
@@ -4,4 +4,4 @@ ASK Engine is the product. ``ask`` is the canonical CLI command; ``sourcecode``
4
4
  the legacy compatibility alias and the Python/PyPI package name. See
5
5
  docs/PRODUCT_IDENTITY.md (normative)."""
6
6
 
7
- __version__ = "2.6.3"
7
+ __version__ = "2.6.5"
@@ -2,6 +2,7 @@ from __future__ import annotations
2
2
 
3
3
  import hashlib
4
4
  import json
5
+ import difflib
5
6
  import os
6
7
  import sys
7
8
  import threading
@@ -331,6 +332,54 @@ def _reject_path_before_subcommand(path_token: str, subcommand: str) -> "NoRetur
331
332
  raise SystemExit(2)
332
333
 
333
334
 
335
+ def _reject_unknown_command(token: str) -> "NoReturn":
336
+ """Refuse a first positional that is neither a known subcommand nor a real path.
337
+
338
+ `ask` accepts a bare repository path (`ask ./repo`) as an implicit scan, so a
339
+ mistyped subcommand used to be swallowed as a path and then surfaced a
340
+ misleading error naming the *wrong* token — `ask spring-audi` complained the
341
+ directory 'spring-audi' did not exist, and `ask notacommand /tmp` complained
342
+ 'No such command /tmp' (the path, not the typo). Name the token the user
343
+ actually got wrong, and suggest the closest real commands.
344
+ """
345
+ matches = difflib.get_close_matches(token, sorted(_SUBCOMMANDS), n=3, cutoff=0.6)
346
+ first = f"error: '{token}' is not a known command"
347
+ if matches:
348
+ first += f". Did you mean: {', '.join(matches)}?"
349
+ else:
350
+ first += " and is not an existing directory to scan."
351
+ print(
352
+ first + "\n run 'ask --help' to list commands, or pass an existing "
353
+ "repository path.",
354
+ file=sys.stderr,
355
+ )
356
+ raise SystemExit(2)
357
+
358
+
359
+ def _has_trailing_nonsubcommand_positional(args: list[str], after: int) -> bool:
360
+ """True when a bare positional that is NOT a subcommand appears after index
361
+ ``after`` (options and the values they consume are skipped).
362
+
363
+ The implicit scan (`ask <path>`) owns exactly one positional, and a valid
364
+ invocation with two positionals always starts with a subcommand. So a first
365
+ non-subcommand positional followed by another non-subcommand positional —
366
+ `ask notacommand /tmp` — is malformed: the user meant a command. A trailing
367
+ token that IS a subcommand is left for ``_reject_path_before_subcommand``."""
368
+ skip_next = False
369
+ for arg in args[after + 1:]:
370
+ if skip_next:
371
+ skip_next = False
372
+ continue
373
+ if arg.startswith("-"):
374
+ if arg.split("=")[0] in _OPTIONS_WITH_VALUE and "=" not in arg:
375
+ skip_next = True
376
+ continue
377
+ if arg in _SUBCOMMANDS:
378
+ return False # wrong-order path/subcommand — handled elsewhere
379
+ return True
380
+ return False
381
+
382
+
334
383
  def _preprocess_args(args: list[str]) -> list[str]:
335
384
  """Extract a repository path token from an args list and store it in _detected_path.
336
385
 
@@ -363,7 +412,13 @@ def _preprocess_args(args: list[str]) -> list[str]:
363
412
  return result # known subcommand — leave for Click to dispatch
364
413
  if _path_index >= 0:
365
414
  continue # a later positional is the subcommand's own business
366
- # First genuine positional: treat as repository path
415
+ # First genuine positional. `ask` treats it as the repository path to scan
416
+ # (existence is validated downstream, not here). But if it is followed by
417
+ # another non-subcommand positional, the one-path scan cannot own both —
418
+ # `ask notacommand /tmp` — so the user meant a command: name the token they
419
+ # got wrong instead of swallowing it and erroring on the next one.
420
+ if _has_trailing_nonsubcommand_positional(result, i):
421
+ _reject_unknown_command(arg)
367
422
  _set_detected_path(arg)
368
423
  _path_index = i
369
424
  if _path_index >= 0:
@@ -1278,11 +1333,23 @@ def main(
1278
1333
  _raw_path_input = _get_detected_path()
1279
1334
  target = Path(_raw_path_input).resolve()
1280
1335
  if not target.exists():
1336
+ # A non-existent path that closely matches a command name is almost always a
1337
+ # mistyped subcommand (`ask spring-audi`) swallowed as an implicit-scan path,
1338
+ # not a real directory. Point at the likely command instead of only the path.
1339
+ _cmd_matches = difflib.get_close_matches(
1340
+ _raw_path_input, sorted(_SUBCOMMANDS), n=3, cutoff=0.6
1341
+ )
1342
+ _hint = "Pass an existing repository directory."
1343
+ if _cmd_matches:
1344
+ _hint = (
1345
+ f"If you meant a command, try: {', '.join('ask ' + m for m in _cmd_matches)}. "
1346
+ "Otherwise pass an existing repository directory."
1347
+ )
1281
1348
  _emit_error_json(
1282
1349
  INVALID_INPUT_CODE,
1283
1350
  f"Directory '{_raw_path_input}' does not exist.",
1284
1351
  path=_raw_path_input,
1285
- hint="Pass an existing repository directory.",
1352
+ hint=_hint,
1286
1353
  expected="An existing directory path.",
1287
1354
  )
1288
1355
  raise typer.Exit(code=1)
@@ -5528,7 +5595,7 @@ def spring_audit_cmd(
5528
5595
  help="Accepted for compatibility; this command always reads fresh source (no snapshot cache). No-op.",
5529
5596
  ),
5530
5597
  ) -> None:
5531
- """Spring semantic audit: TX anomalies (TX-001..005) + security surface (SEC-001..003).
5598
+ """Spring semantic audit: TX anomalies (TX-001..006) + security surface (SEC-001..003).
5532
5599
 
5533
5600
  \b
5534
5601
  Detects:
@@ -5537,6 +5604,7 @@ def spring_audit_cmd(
5537
5604
  TX-003 readOnly=true boundary propagating to write operation
5538
5605
  TX-004 NOT_SUPPORTED/NEVER within active TX chain
5539
5606
  TX-005 Exception swallowing inside @Transactional
5607
+ TX-006 Self-invocation of @Transactional sibling (proxy bypass)
5540
5608
  SEC-001 Unsecured endpoint in annotation_based security model
5541
5609
  SEC-002 CVE-2025-41248: @PreAuthorize on inherited method from generic supertype
5542
5610
  SEC-003 @Transactional on @Controller/@RestController (TX in wrong layer)
@@ -2098,6 +2098,21 @@ def _extract_body_facts(
2098
2098
  receiver, callee = m.group(1), m.group(2)
2099
2099
  if callee in _CALL_KEYWORDS:
2100
2100
  continue
2101
+ if receiver is None:
2102
+ # A chained/qualified call — `expr().foo()`, `getBean().foo()`,
2103
+ # `a.b().foo()` — leaves the receiver group empty because the
2104
+ # receiver is not a bare identifier. It must NOT be mistaken for an
2105
+ # unqualified self-call: if the callee is immediately preceded by '.'
2106
+ # the call is qualified (goes through whatever the preceding
2107
+ # expression returns, e.g. a Spring proxy), so mark it as a
2108
+ # non-self expression receiver. (openmrs OrderServiceImpl routes
2109
+ # REQUIRES_NEW through Context.getOrderService()....() precisely to
2110
+ # keep the proxy — that is correct code, not a self-invocation.)
2111
+ j = m.start() - 1
2112
+ while j >= 0 and body[j] in " \t\r\n":
2113
+ j -= 1
2114
+ if j >= 0 and body[j] == ".":
2115
+ receiver = "<expr>"
2101
2116
  resolved: Optional[str] = None
2102
2117
  if receiver in (None, "this") and callee in sib:
2103
2118
  cands = [f for f in sib[callee] if f != caller.symbol]
@@ -1036,7 +1036,7 @@ class ImpactOrchestrator:
1036
1036
  )
1037
1037
  else:
1038
1038
  warnings.append(
1039
- f"Self-referential exclusion (BUG #2): {self_excluded} member(s) of the "
1039
+ f"Self-referential exclusion: {self_excluded} member(s) of the "
1040
1040
  f"analyzed class were dropped from callers — a class's own methods are "
1041
1041
  f"members, not external callers (they do not count toward blast radius)."
1042
1042
  )
@@ -6,9 +6,8 @@ Patterns implemented (Phase 2):
6
6
  TX-003 readOnly=true boundary propagating into write-capable callee
7
7
  TX-004 NOT_SUPPORTED or NEVER invoked from transactional call chain
8
8
  TX-005 Exception swallowing inside @Transactional method (regex)
9
-
10
- Self-invocation via this.method() intentionally excluded from Phase 2:
11
- requires AST-level analysis, regex produces too many false positives.
9
+ TX-006 Self-invocation of a @Transactional sibling — proxy bypass (Semantic-IR
10
+ invocation atoms; unambiguous same-class calls only)
12
11
 
13
12
  All patterns are deterministic and never raise.
14
13
  """
@@ -683,6 +682,142 @@ class _TX005ExceptionSwallowing:
683
682
  return blocks
684
683
 
685
684
 
685
+ # ---------------------------------------------------------------------------
686
+ # TX-006: self-invocation of a @Transactional sibling — proxy bypass
687
+ # ---------------------------------------------------------------------------
688
+
689
+ class _TX006SelfInvocation:
690
+ """Same-instance call to a @Transactional sibling method bypasses the proxy.
691
+
692
+ Spring TX advice is applied by a proxy that wraps the bean. A call that does
693
+ NOT go through that proxy — an unqualified `foo()` or an explicit `this.foo()`
694
+ to a method of the SAME class — reaches the target directly, so its
695
+ method-level @Transactional is silently ignored at runtime (Sonar S2229).
696
+
697
+ Detection is structural and VAI-clean, reading the Semantic-IR invocation
698
+ atoms already produced by build_canonical_ir (cir.body_facts):
699
+
700
+ - the atom's receiver is None (unqualified) or "this" → not via a proxy
701
+ - resolved_fqn is set → unambiguous sibling
702
+ (the IR only resolves a sibling when receiver∈{None,"this"} and exactly one
703
+ same-class method matches — overloaded/ambiguous targets stay None → skipped)
704
+ - the resolved sibling carries a METHOD-LEVEL @Transactional → advice would apply
705
+
706
+ Class-level-only @Transactional is deliberately NOT flagged: the enclosing
707
+ proxy already applies it to the outer entry method, so the self-call runs in
708
+ that same transaction — no advice is lost. The self-injection workaround
709
+ (`self.foo()` where `self` is an injected same-type field) uses a named field
710
+ receiver, never None/"this", so it never resolves as a sibling → no false
711
+ positive on the legitimate escape hatch.
712
+ """
713
+ pattern_id = "TX-006"
714
+ severity = "high" # nominal (Protocol); real severity is tiered per finding
715
+
716
+ # Propagations whose whole purpose is a distinct transaction boundary: a bypass
717
+ # demonstrably breaks them (a new/suspended/forbidden TX that never happens, or a
718
+ # runtime IllegalTransactionStateException). Everything else — a plain REQUIRED/
719
+ # SUPPORTS callee, or a readOnly getter (bypass loses only the read-only
720
+ # optimization, not correctness) — degrades to running in the caller's TX: a real
721
+ # bypass with lower blast radius → medium, matching TX-002/003 discipline.
722
+ _HIGH_IMPACT_PROPAGATION = frozenset({
723
+ "REQUIRES_NEW", "NESTED", "NOT_SUPPORTED", "NEVER", "MANDATORY",
724
+ })
725
+
726
+ def analyze(
727
+ self,
728
+ cir: "CanonicalRepositoryIR",
729
+ tx_index: TransactionBoundaryIndex,
730
+ root: Optional[Path],
731
+ *,
732
+ model: Optional[SpringSemanticModel] = None,
733
+ ) -> list[SpringFinding]:
734
+ findings: list[SpringFinding] = []
735
+ body_facts = getattr(cir, "body_facts", {}) or {}
736
+ _seen_ids: set[str] = set()
737
+
738
+ for caller_fqn, atoms in body_facts.items():
739
+ if not isinstance(atoms, list):
740
+ continue
741
+ for atom in atoms:
742
+ if not isinstance(atom, dict) or atom.get("fact") != "invocation":
743
+ continue
744
+ # Only same-instance calls bypass the proxy: unqualified or `this.`.
745
+ if atom.get("receiver") not in (None, "this"):
746
+ continue
747
+ target = atom.get("resolved_fqn")
748
+ if not target:
749
+ continue # ambiguous/overloaded/unresolved sibling — honest skip
750
+ # The bypass only matters when the sibling declares its OWN
751
+ # method-level @Transactional; class-level advice already covers
752
+ # the outer entry method and is not lost by the self-call.
753
+ tb = tx_index.by_symbol.get(target)
754
+ if tb is None or tb.scope != "method":
755
+ continue
756
+
757
+ _fid = SpringFinding.make_id(self.pattern_id, f"{caller_fqn}→{target}")
758
+ if _fid in _seen_ids:
759
+ continue
760
+ _seen_ids.add(_fid)
761
+
762
+ caller_simple = caller_fqn.rsplit(".", 1)[-1].replace("#", ".")
763
+ callee_simple = target.rsplit(".", 1)[-1].replace("#", ".")
764
+
765
+ # Tier severity by what the bypass demonstrably breaks. A propagation
766
+ # that mandates a distinct boundary (or forbids an active TX) is
767
+ # high; a plain REQUIRED/SUPPORTS/readOnly callee degrades to the
768
+ # caller's TX — still a real bypass, lower blast radius → medium.
769
+ if tb.propagation in self._HIGH_IMPACT_PROPAGATION:
770
+ sev = "high"
771
+ else:
772
+ sev = "medium"
773
+
774
+ findings.append(SpringFinding(
775
+ id=_fid,
776
+ pattern_id=self.pattern_id,
777
+ category="tx",
778
+ severity=sev,
779
+ confidence="high",
780
+ title="Self-invocation of @Transactional method — Spring proxy bypass",
781
+ symbol=target,
782
+ source_file=tb.source_file,
783
+ evidence={
784
+ "caller": caller_fqn,
785
+ "callee": target,
786
+ "receiver": atom.get("receiver") or "(unqualified)",
787
+ "annotation": "@Transactional",
788
+ "callee_propagation": tb.propagation,
789
+ "callee_read_only": tb.read_only,
790
+ "proxy_mechanism": "proxy self-invocation",
791
+ "reason": (
792
+ "same-instance call does not pass through the Spring proxy"
793
+ ),
794
+ },
795
+ explanation=(
796
+ f"{caller_simple} calls sibling {callee_simple} on the same "
797
+ "instance (unqualified / this.). The call does not pass through "
798
+ f"the Spring proxy, so the @Transactional on {callee_simple} "
799
+ f"(propagation={tb.propagation}"
800
+ f"{', readOnly=true' if tb.read_only else ''}) is silently "
801
+ "ignored — it executes in the caller's transaction context "
802
+ "(or none at all), not the one its annotation declares. "
803
+ "No new/independent transaction, propagation, or rollback boundary is applied."
804
+ ),
805
+ fix_hint=(
806
+ "Move the @Transactional method to a separate Spring bean and "
807
+ "call it through the injected dependency, or inject the bean into "
808
+ "itself and call via the injected proxy reference, or use "
809
+ "TransactionTemplate for programmatic transaction control."
810
+ ),
811
+ limitations=[
812
+ "Only unambiguous sibling calls are analyzed — overloaded targets "
813
+ "are reported as unresolved and skipped",
814
+ ],
815
+ related_symbols=[target.split("#")[0]],
816
+ ))
817
+
818
+ return findings
819
+
820
+
686
821
  # ---------------------------------------------------------------------------
687
822
  # Default pattern registry
688
823
  # ---------------------------------------------------------------------------
@@ -693,6 +828,7 @@ _DEFAULT_TX_PATTERNS: list[TxPattern] = [
693
828
  _TX003ReadOnlyWritePropagation(), # type: ignore[list-item]
694
829
  _TX004TxSuspensionRisk(), # type: ignore[list-item]
695
830
  _TX005ExceptionSwallowing(), # type: ignore[list-item]
831
+ _TX006SelfInvocation(), # type: ignore[list-item]
696
832
  ]
697
833
 
698
834
 
@@ -777,7 +913,8 @@ def run_tx_audit(
777
913
  _spring_detected = tx_index.stats()["total"] > 0 or model.bean_graph.has_spring_beans()
778
914
 
779
915
  _tx_limitations = [
780
- "Self-invocation via this.method() not detected requires AST-level analysis",
916
+ "Self-invocation (this.method()) detected for unambiguous same-class calls "
917
+ "(TX-006); overloaded targets are reported as unresolved and skipped",
781
918
  "Dynamic dispatch (interface/polymorphic calls) may produce incomplete call chains",
782
919
  ]
783
920
  for _err in getattr(engine, "_last_analysis_errors", []):
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes