sourcecode 2.5.0__tar.gz → 2.5.2__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 (159) hide show
  1. {sourcecode-2.5.0 → sourcecode-2.5.2}/CHANGELOG.md +66 -0
  2. {sourcecode-2.5.0 → sourcecode-2.5.2}/PKG-INFO +3 -3
  3. {sourcecode-2.5.0 → sourcecode-2.5.2}/README.md +2 -2
  4. {sourcecode-2.5.0 → sourcecode-2.5.2}/pyproject.toml +1 -1
  5. {sourcecode-2.5.0 → sourcecode-2.5.2}/src/sourcecode/__init__.py +1 -1
  6. {sourcecode-2.5.0 → sourcecode-2.5.2}/src/sourcecode/repository_ir.py +137 -6
  7. {sourcecode-2.5.0 → sourcecode-2.5.2}/src/sourcecode/spring_impact.py +52 -10
  8. {sourcecode-2.5.0 → sourcecode-2.5.2}/.github/workflows/build-windows.yml +0 -0
  9. {sourcecode-2.5.0 → sourcecode-2.5.2}/.gitignore +0 -0
  10. {sourcecode-2.5.0 → sourcecode-2.5.2}/.ruff.toml +0 -0
  11. {sourcecode-2.5.0 → sourcecode-2.5.2}/CONTRIBUTING.md +0 -0
  12. {sourcecode-2.5.0 → sourcecode-2.5.2}/LICENSE +0 -0
  13. {sourcecode-2.5.0 → sourcecode-2.5.2}/SECURITY.md +0 -0
  14. {sourcecode-2.5.0 → sourcecode-2.5.2}/raw +0 -0
  15. {sourcecode-2.5.0 → sourcecode-2.5.2}/scripts/compare_integration_engines.py +0 -0
  16. {sourcecode-2.5.0 → sourcecode-2.5.2}/scripts/customer_smoke_test.sh +0 -0
  17. {sourcecode-2.5.0 → sourcecode-2.5.2}/scripts/generate_jdk_exports.py +0 -0
  18. {sourcecode-2.5.0 → sourcecode-2.5.2}/src/sourcecode/adaptive_scanner.py +0 -0
  19. {sourcecode-2.5.0 → sourcecode-2.5.2}/src/sourcecode/archetype.py +0 -0
  20. {sourcecode-2.5.0 → sourcecode-2.5.2}/src/sourcecode/architecture_analyzer.py +0 -0
  21. {sourcecode-2.5.0 → sourcecode-2.5.2}/src/sourcecode/architecture_summary.py +0 -0
  22. {sourcecode-2.5.0 → sourcecode-2.5.2}/src/sourcecode/ast_extractor.py +0 -0
  23. {sourcecode-2.5.0 → sourcecode-2.5.2}/src/sourcecode/cache.py +0 -0
  24. {sourcecode-2.5.0 → sourcecode-2.5.2}/src/sourcecode/call_surface.py +0 -0
  25. {sourcecode-2.5.0 → sourcecode-2.5.2}/src/sourcecode/caller_metrics.py +0 -0
  26. {sourcecode-2.5.0 → sourcecode-2.5.2}/src/sourcecode/canonical_ir.py +0 -0
  27. {sourcecode-2.5.0 → sourcecode-2.5.2}/src/sourcecode/cir_graphs.py +0 -0
  28. {sourcecode-2.5.0 → sourcecode-2.5.2}/src/sourcecode/classifier.py +0 -0
  29. {sourcecode-2.5.0 → sourcecode-2.5.2}/src/sourcecode/cli.py +0 -0
  30. {sourcecode-2.5.0 → sourcecode-2.5.2}/src/sourcecode/code_notes_analyzer.py +0 -0
  31. {sourcecode-2.5.0 → sourcecode-2.5.2}/src/sourcecode/confidence_analyzer.py +0 -0
  32. {sourcecode-2.5.0 → sourcecode-2.5.2}/src/sourcecode/context_cache.py +0 -0
  33. {sourcecode-2.5.0 → sourcecode-2.5.2}/src/sourcecode/context_graph.py +0 -0
  34. {sourcecode-2.5.0 → sourcecode-2.5.2}/src/sourcecode/context_scorer.py +0 -0
  35. {sourcecode-2.5.0 → sourcecode-2.5.2}/src/sourcecode/context_summarizer.py +0 -0
  36. {sourcecode-2.5.0 → sourcecode-2.5.2}/src/sourcecode/contract_model.py +0 -0
  37. {sourcecode-2.5.0 → sourcecode-2.5.2}/src/sourcecode/contract_pipeline.py +0 -0
  38. {sourcecode-2.5.0 → sourcecode-2.5.2}/src/sourcecode/coverage_parser.py +0 -0
  39. {sourcecode-2.5.0 → sourcecode-2.5.2}/src/sourcecode/dependency_analyzer.py +0 -0
  40. {sourcecode-2.5.0 → sourcecode-2.5.2}/src/sourcecode/detectors/__init__.py +0 -0
  41. {sourcecode-2.5.0 → sourcecode-2.5.2}/src/sourcecode/detectors/base.py +0 -0
  42. {sourcecode-2.5.0 → sourcecode-2.5.2}/src/sourcecode/detectors/csproj_parser.py +0 -0
  43. {sourcecode-2.5.0 → sourcecode-2.5.2}/src/sourcecode/detectors/dart.py +0 -0
  44. {sourcecode-2.5.0 → sourcecode-2.5.2}/src/sourcecode/detectors/dotnet.py +0 -0
  45. {sourcecode-2.5.0 → sourcecode-2.5.2}/src/sourcecode/detectors/elixir.py +0 -0
  46. {sourcecode-2.5.0 → sourcecode-2.5.2}/src/sourcecode/detectors/go.py +0 -0
  47. {sourcecode-2.5.0 → sourcecode-2.5.2}/src/sourcecode/detectors/heuristic.py +0 -0
  48. {sourcecode-2.5.0 → sourcecode-2.5.2}/src/sourcecode/detectors/hybrid.py +0 -0
  49. {sourcecode-2.5.0 → sourcecode-2.5.2}/src/sourcecode/detectors/java.py +0 -0
  50. {sourcecode-2.5.0 → sourcecode-2.5.2}/src/sourcecode/detectors/jvm_ext.py +0 -0
  51. {sourcecode-2.5.0 → sourcecode-2.5.2}/src/sourcecode/detectors/nodejs.py +0 -0
  52. {sourcecode-2.5.0 → sourcecode-2.5.2}/src/sourcecode/detectors/parsers.py +0 -0
  53. {sourcecode-2.5.0 → sourcecode-2.5.2}/src/sourcecode/detectors/php.py +0 -0
  54. {sourcecode-2.5.0 → sourcecode-2.5.2}/src/sourcecode/detectors/project.py +0 -0
  55. {sourcecode-2.5.0 → sourcecode-2.5.2}/src/sourcecode/detectors/python.py +0 -0
  56. {sourcecode-2.5.0 → sourcecode-2.5.2}/src/sourcecode/detectors/ruby.py +0 -0
  57. {sourcecode-2.5.0 → sourcecode-2.5.2}/src/sourcecode/detectors/rust.py +0 -0
  58. {sourcecode-2.5.0 → sourcecode-2.5.2}/src/sourcecode/detectors/systems.py +0 -0
  59. {sourcecode-2.5.0 → sourcecode-2.5.2}/src/sourcecode/detectors/terraform.py +0 -0
  60. {sourcecode-2.5.0 → sourcecode-2.5.2}/src/sourcecode/detectors/tooling.py +0 -0
  61. {sourcecode-2.5.0 → sourcecode-2.5.2}/src/sourcecode/doc_analyzer.py +0 -0
  62. {sourcecode-2.5.0 → sourcecode-2.5.2}/src/sourcecode/dynamic_argument_surface.py +0 -0
  63. {sourcecode-2.5.0 → sourcecode-2.5.2}/src/sourcecode/endpoint_literals.py +0 -0
  64. {sourcecode-2.5.0 → sourcecode-2.5.2}/src/sourcecode/endpoint_metrics.py +0 -0
  65. {sourcecode-2.5.0 → sourcecode-2.5.2}/src/sourcecode/entrypoint_classifier.py +0 -0
  66. {sourcecode-2.5.0 → sourcecode-2.5.2}/src/sourcecode/env_analyzer.py +0 -0
  67. {sourcecode-2.5.0 → sourcecode-2.5.2}/src/sourcecode/error_schema.py +0 -0
  68. {sourcecode-2.5.0 → sourcecode-2.5.2}/src/sourcecode/evidence_provider.py +0 -0
  69. {sourcecode-2.5.0 → sourcecode-2.5.2}/src/sourcecode/explain.py +0 -0
  70. {sourcecode-2.5.0 → sourcecode-2.5.2}/src/sourcecode/file_chunker.py +0 -0
  71. {sourcecode-2.5.0 → sourcecode-2.5.2}/src/sourcecode/file_classifier.py +0 -0
  72. {sourcecode-2.5.0 → sourcecode-2.5.2}/src/sourcecode/format_contract.py +0 -0
  73. {sourcecode-2.5.0 → sourcecode-2.5.2}/src/sourcecode/fqn_utils.py +0 -0
  74. {sourcecode-2.5.0 → sourcecode-2.5.2}/src/sourcecode/git_analyzer.py +0 -0
  75. {sourcecode-2.5.0 → sourcecode-2.5.2}/src/sourcecode/graph_analyzer.py +0 -0
  76. {sourcecode-2.5.0 → sourcecode-2.5.2}/src/sourcecode/graph_evidence.py +0 -0
  77. {sourcecode-2.5.0 → sourcecode-2.5.2}/src/sourcecode/hibernate_strat.py +0 -0
  78. {sourcecode-2.5.0 → sourcecode-2.5.2}/src/sourcecode/jdk_exports.py +0 -0
  79. {sourcecode-2.5.0 → sourcecode-2.5.2}/src/sourcecode/license.py +0 -0
  80. {sourcecode-2.5.0 → sourcecode-2.5.2}/src/sourcecode/mcp/__init__.py +0 -0
  81. {sourcecode-2.5.0 → sourcecode-2.5.2}/src/sourcecode/mcp/onboarding/__init__.py +0 -0
  82. {sourcecode-2.5.0 → sourcecode-2.5.2}/src/sourcecode/mcp/onboarding/applier.py +0 -0
  83. {sourcecode-2.5.0 → sourcecode-2.5.2}/src/sourcecode/mcp/onboarding/backup.py +0 -0
  84. {sourcecode-2.5.0 → sourcecode-2.5.2}/src/sourcecode/mcp/onboarding/detector.py +0 -0
  85. {sourcecode-2.5.0 → sourcecode-2.5.2}/src/sourcecode/mcp/onboarding/planner.py +0 -0
  86. {sourcecode-2.5.0 → sourcecode-2.5.2}/src/sourcecode/mcp/orchestrator.py +0 -0
  87. {sourcecode-2.5.0 → sourcecode-2.5.2}/src/sourcecode/mcp/registry.py +0 -0
  88. {sourcecode-2.5.0 → sourcecode-2.5.2}/src/sourcecode/mcp/runner.py +0 -0
  89. {sourcecode-2.5.0 → sourcecode-2.5.2}/src/sourcecode/mcp/server.py +0 -0
  90. {sourcecode-2.5.0 → sourcecode-2.5.2}/src/sourcecode/mcp_nudge.py +0 -0
  91. {sourcecode-2.5.0 → sourcecode-2.5.2}/src/sourcecode/metrics_analyzer.py +0 -0
  92. {sourcecode-2.5.0 → sourcecode-2.5.2}/src/sourcecode/migrate_check.py +0 -0
  93. {sourcecode-2.5.0 → sourcecode-2.5.2}/src/sourcecode/openapi_surface.py +0 -0
  94. {sourcecode-2.5.0 → sourcecode-2.5.2}/src/sourcecode/output_budget.py +0 -0
  95. {sourcecode-2.5.0 → sourcecode-2.5.2}/src/sourcecode/path_filters.py +0 -0
  96. {sourcecode-2.5.0 → sourcecode-2.5.2}/src/sourcecode/pr_comment_renderer.py +0 -0
  97. {sourcecode-2.5.0 → sourcecode-2.5.2}/src/sourcecode/pr_impact.py +0 -0
  98. {sourcecode-2.5.0 → sourcecode-2.5.2}/src/sourcecode/prepare_context.py +0 -0
  99. {sourcecode-2.5.0 → sourcecode-2.5.2}/src/sourcecode/progress.py +0 -0
  100. {sourcecode-2.5.0 → sourcecode-2.5.2}/src/sourcecode/ranking_engine.py +0 -0
  101. {sourcecode-2.5.0 → sourcecode-2.5.2}/src/sourcecode/redactor.py +0 -0
  102. {sourcecode-2.5.0 → sourcecode-2.5.2}/src/sourcecode/relevance_scorer.py +0 -0
  103. {sourcecode-2.5.0 → sourcecode-2.5.2}/src/sourcecode/rename_refactor.py +0 -0
  104. {sourcecode-2.5.0 → sourcecode-2.5.2}/src/sourcecode/repo_classifier.py +0 -0
  105. {sourcecode-2.5.0 → sourcecode-2.5.2}/src/sourcecode/retrieval/__init__.py +0 -0
  106. {sourcecode-2.5.0 → sourcecode-2.5.2}/src/sourcecode/retrieval/context.py +0 -0
  107. {sourcecode-2.5.0 → sourcecode-2.5.2}/src/sourcecode/retrieval/errors.py +0 -0
  108. {sourcecode-2.5.0 → sourcecode-2.5.2}/src/sourcecode/retrieval/executor.py +0 -0
  109. {sourcecode-2.5.0 → sourcecode-2.5.2}/src/sourcecode/retrieval/planner.py +0 -0
  110. {sourcecode-2.5.0 → sourcecode-2.5.2}/src/sourcecode/retrieval/query.py +0 -0
  111. {sourcecode-2.5.0 → sourcecode-2.5.2}/src/sourcecode/retrieval/request.py +0 -0
  112. {sourcecode-2.5.0 → sourcecode-2.5.2}/src/sourcecode/retrieval/resolution.py +0 -0
  113. {sourcecode-2.5.0 → sourcecode-2.5.2}/src/sourcecode/retrieval/result.py +0 -0
  114. {sourcecode-2.5.0 → sourcecode-2.5.2}/src/sourcecode/retrieval/retriever.py +0 -0
  115. {sourcecode-2.5.0 → sourcecode-2.5.2}/src/sourcecode/retrieval/runtime.py +0 -0
  116. {sourcecode-2.5.0 → sourcecode-2.5.2}/src/sourcecode/retrieval/steps.py +0 -0
  117. {sourcecode-2.5.0 → sourcecode-2.5.2}/src/sourcecode/retrieval/steps_endpoint.py +0 -0
  118. {sourcecode-2.5.0 → sourcecode-2.5.2}/src/sourcecode/retrieval/steps_graph.py +0 -0
  119. {sourcecode-2.5.0 → sourcecode-2.5.2}/src/sourcecode/retrieval/steps_impact.py +0 -0
  120. {sourcecode-2.5.0 → sourcecode-2.5.2}/src/sourcecode/retrieval/steps_intf.py +0 -0
  121. {sourcecode-2.5.0 → sourcecode-2.5.2}/src/sourcecode/retrieval/steps_struct.py +0 -0
  122. {sourcecode-2.5.0 → sourcecode-2.5.2}/src/sourcecode/retrieval/steps_txsec.py +0 -0
  123. {sourcecode-2.5.0 → sourcecode-2.5.2}/src/sourcecode/ris.py +0 -0
  124. {sourcecode-2.5.0 → sourcecode-2.5.2}/src/sourcecode/runtime_classifier.py +0 -0
  125. {sourcecode-2.5.0 → sourcecode-2.5.2}/src/sourcecode/scanner.py +0 -0
  126. {sourcecode-2.5.0 → sourcecode-2.5.2}/src/sourcecode/schema.py +0 -0
  127. {sourcecode-2.5.0 → sourcecode-2.5.2}/src/sourcecode/security_config.py +0 -0
  128. {sourcecode-2.5.0 → sourcecode-2.5.2}/src/sourcecode/security_posture.py +0 -0
  129. {sourcecode-2.5.0 → sourcecode-2.5.2}/src/sourcecode/semantic_analyzer.py +0 -0
  130. {sourcecode-2.5.0 → sourcecode-2.5.2}/src/sourcecode/semantic_impact_engine.py +0 -0
  131. {sourcecode-2.5.0 → sourcecode-2.5.2}/src/sourcecode/semantic_integration_engine.py +0 -0
  132. {sourcecode-2.5.0 → sourcecode-2.5.2}/src/sourcecode/semantic_services.py +0 -0
  133. {sourcecode-2.5.0 → sourcecode-2.5.2}/src/sourcecode/serializer.py +0 -0
  134. {sourcecode-2.5.0 → sourcecode-2.5.2}/src/sourcecode/spring_event_topology.py +0 -0
  135. {sourcecode-2.5.0 → sourcecode-2.5.2}/src/sourcecode/spring_findings.py +0 -0
  136. {sourcecode-2.5.0 → sourcecode-2.5.2}/src/sourcecode/spring_model.py +0 -0
  137. {sourcecode-2.5.0 → sourcecode-2.5.2}/src/sourcecode/spring_security_audit.py +0 -0
  138. {sourcecode-2.5.0 → sourcecode-2.5.2}/src/sourcecode/spring_semantic.py +0 -0
  139. {sourcecode-2.5.0 → sourcecode-2.5.2}/src/sourcecode/spring_tx_analyzer.py +0 -0
  140. {sourcecode-2.5.0 → sourcecode-2.5.2}/src/sourcecode/summarizer.py +0 -0
  141. {sourcecode-2.5.0 → sourcecode-2.5.2}/src/sourcecode/telemetry/__init__.py +0 -0
  142. {sourcecode-2.5.0 → sourcecode-2.5.2}/src/sourcecode/telemetry/config.py +0 -0
  143. {sourcecode-2.5.0 → sourcecode-2.5.2}/src/sourcecode/telemetry/consent.py +0 -0
  144. {sourcecode-2.5.0 → sourcecode-2.5.2}/src/sourcecode/telemetry/events.py +0 -0
  145. {sourcecode-2.5.0 → sourcecode-2.5.2}/src/sourcecode/telemetry/filters.py +0 -0
  146. {sourcecode-2.5.0 → sourcecode-2.5.2}/src/sourcecode/telemetry/transport.py +0 -0
  147. {sourcecode-2.5.0 → sourcecode-2.5.2}/src/sourcecode/tree_utils.py +0 -0
  148. {sourcecode-2.5.0 → sourcecode-2.5.2}/src/sourcecode/type_usage_surface.py +0 -0
  149. {sourcecode-2.5.0 → sourcecode-2.5.2}/src/sourcecode/validation_inference.py +0 -0
  150. {sourcecode-2.5.0 → sourcecode-2.5.2}/src/sourcecode/validation_surface.py +0 -0
  151. {sourcecode-2.5.0 → sourcecode-2.5.2}/src/sourcecode/version_check.py +0 -0
  152. {sourcecode-2.5.0 → sourcecode-2.5.2}/src/sourcecode/workspace.py +0 -0
  153. {sourcecode-2.5.0 → sourcecode-2.5.2}/supabase/functions/README.md +0 -0
  154. {sourcecode-2.5.0 → sourcecode-2.5.2}/supabase/functions/get-license/index.ts +0 -0
  155. {sourcecode-2.5.0 → sourcecode-2.5.2}/supabase/functions/lemonsqueezy-webhook/index.ts +0 -0
  156. {sourcecode-2.5.0 → sourcecode-2.5.2}/supabase/functions/telemetry/index.ts +0 -0
  157. {sourcecode-2.5.0 → sourcecode-2.5.2}/supabase/sql/license_event_ordering.sql +0 -0
  158. {sourcecode-2.5.0 → sourcecode-2.5.2}/supabase/sql/licensing_schema.sql +0 -0
  159. {sourcecode-2.5.0 → sourcecode-2.5.2}/supabase/sql/telemetry_events.sql +0 -0
@@ -1,5 +1,71 @@
1
1
  # Changelog
2
2
 
3
+ ## [2.5.2] — 2026-07-14
4
+
5
+ **impact-chain correctness + cold-scan performance.** Two fixes surfaced by an
6
+ adversarial audit across 12 real repositories (alfresco, keycloak, neo4j, jenkins,
7
+ openmrs, ofbiz, jobrunr, eureka, BroadleafCommerce, spring-petclinic, and torture
8
+ fixtures).
9
+
10
+ ### Fixed
11
+
12
+ - **impact-chain lost same-class method callers (P0)** (`spring_impact.py`): a method
13
+ whose only callers were sibling methods in the same class (e.g. a private helper
14
+ `doWork()` invoked by `callWork()`) came back with **0 direct callers** — a false
15
+ "isolated / safe" verdict that defeated the 2.5.1 intra-class caller feature. The
16
+ `calls` edge existed in the reverse graph but the caller BFS discarded it: the
17
+ whole-class self-exclusion (v1.70.0 BUG #2) was applied to *method*-scoped queries,
18
+ where a *different* method on the same class is a legitimate caller, not
19
+ self-reference. Fix threads the existing `_method_scoped` flag into `_bfs_callers`;
20
+ method-scoped queries now exclude only genuine self-recursion, class-scoped queries
21
+ keep the cohesion exclusion. Verified: `TxService#doPrivateWork` → `[callPrivate]`
22
+ (was `[]`); cross-class blast radius unchanged (petclinic 13 endpoints).
23
+
24
+ - **Quadratic cold scan on method-dense classes (P1)** (`repository_ir.py`):
25
+ `_intra_class_call_edges` ran a fresh regex search *per sibling method name* over each
26
+ method body — O(methods × body_len) per class, i.e. quadratic in method count. Large,
27
+ method-dense classes (generated parsers, multi-thousand-line test classes) made cold
28
+ scans pathological. Rewritten to a single pass per body (`_INTRA_CALL_RE`) that
29
+ collects call-site names and intersects them with the sibling index — O(body_len),
30
+ byte-identical edge set. Cold-scan wins: openmrs 43s→19s (2.3×), alfresco 220s→126s
31
+ (1.75×), jenkins 43s→35s. `_build_relations` cumulative time dropped 36.6s→3.9s
32
+ (openmrs profile).
33
+
34
+ ## [2.5.1] — 2026-07-14
35
+
36
+ **impact-chain correctness fix — field-injected callers are no longer lost.** A service
37
+ method reached only through an injected sibling (`someService.doWork(...)`, a lowercase
38
+ field receiver) was reported as an isolated, zero-caller *low-risk* change — a dangerous
39
+ false negative. The static call-graph resolved intra-class and static (`Type.method()`)
40
+ invocations but never an instance call through a typed field, so no `calls` edge reached
41
+ the target and the blast radius came back empty at `confidence=high`.
42
+
43
+ ### Fixed
44
+
45
+ - **Receiver-typed call edges (IC-RTC)** (`repository_ir.py`): a repo-wide post-pass
46
+ emits method-level `calls` edges for instance invocations through a typed field
47
+ receiver (`field.method(...)`), joining facts already extracted — the invocation atom
48
+ (`receiver`/`callee`), the field's declared type, and the target type's method index.
49
+ No new parse. Unresolved, ambiguous, local, or library receivers yield nothing (an
50
+ unresolved fact stays unresolved rather than being guessed). Recovers the injected
51
+ cross-service caller the reverse graph previously missed.
52
+ - **Method-scoped impact expansion** (`spring_impact.py`): a method-level query
53
+ (`Impl#foo`) now expands impl↔interface (CH-001a/b) to the **same-named** member only,
54
+ not the entire interface — so a change to `foo` no longer inherits the callers of a
55
+ sibling method `bar`. Class-level (whole-service) queries stay broad. Now that precise
56
+ `field.method(...)` edges exist, this removes latent caller over-reporting (measured:
57
+ a petclinic-rest method query dropped from 9 whole-controller false callers to the 6
58
+ real handler methods that call it).
59
+ - **Inline-annotation field types** (`repository_ir.py`): `_extract_field_types` skipped
60
+ any line beginning with `@`, dropping single-line declarations such as
61
+ `@Autowired private Foo foo;`. Leading inline annotations are now stripped before the
62
+ field is read (brace counting runs on the stripped residual, so annotation string args
63
+ like `@GetMapping("/a/{id}")` cannot skew class-body depth).
64
+
65
+ No public JSON contract changes; output stays deterministic. Regression coverage added in
66
+ `tests/test_impact_receiver_typed_calls.py` (cross-service recovery, self-call exclusion,
67
+ method-scope precision, `this.field` receivers, no phantom edges).
68
+
3
69
  ## [2.5.0] — 2026-07-13
4
70
 
5
71
  **Semantic Retrieval Engine v1 — a declarative, agent-ready query layer over ASK's
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: sourcecode
3
- Version: 2.5.0
3
+ Version: 2.5.2
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
@@ -42,7 +42,7 @@ Description-Content-Type: text/markdown
42
42
 
43
43
  **Context · Impact · Migration · Architecture · Review — everything from one structural model.**
44
44
 
45
- ![Version](https://img.shields.io/badge/version-2.5.0-blue)
45
+ ![Version](https://img.shields.io/badge/version-2.5.1-blue)
46
46
  ![Python](https://img.shields.io/badge/python-3.9%2B-green)
47
47
 
48
48
  > **ASK Engine** is the product. The CLI command is **`ask`**. The legacy **`sourcecode`**
@@ -84,7 +84,7 @@ brew tap haroundominique/sourcecode && brew install sourcecode
84
84
  # pip / pipx
85
85
  pipx install sourcecode # or: pip install sourcecode
86
86
 
87
- ask version # ask 2.5.0
87
+ ask version # ask 2.5.1
88
88
  ```
89
89
 
90
90
  > **Package vs. command.** The install package is named `sourcecode` this release
@@ -4,7 +4,7 @@
4
4
 
5
5
  **Context · Impact · Migration · Architecture · Review — everything from one structural model.**
6
6
 
7
- ![Version](https://img.shields.io/badge/version-2.5.0-blue)
7
+ ![Version](https://img.shields.io/badge/version-2.5.1-blue)
8
8
  ![Python](https://img.shields.io/badge/python-3.9%2B-green)
9
9
 
10
10
  > **ASK Engine** is the product. The CLI command is **`ask`**. The legacy **`sourcecode`**
@@ -46,7 +46,7 @@ brew tap haroundominique/sourcecode && brew install sourcecode
46
46
  # pip / pipx
47
47
  pipx install sourcecode # or: pip install sourcecode
48
48
 
49
- ask version # ask 2.5.0
49
+ ask version # ask 2.5.1
50
50
  ```
51
51
 
52
52
  > **Package vs. command.** The install package is named `sourcecode` this release
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "sourcecode"
7
- version = "2.5.0"
7
+ version = "2.5.2"
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.5.0"
7
+ __version__ = "2.5.2"
@@ -1489,6 +1489,16 @@ def _method_body(raw_lines: list[str], start_idx: int) -> str:
1489
1489
  return "\n".join(out)
1490
1490
 
1491
1491
 
1492
+ # Intra-class call-site matcher: a bare `name(` (not preceded by a word char or '.')
1493
+ # OR an explicit `this.name(`. Group 1 = bare callee, group 2 = this-qualified callee.
1494
+ # Encodes the SAME two match forms the old per-sibling regex used, so the derived edge
1495
+ # set is byte-identical — but as ONE alternation scanned once per body instead of once
1496
+ # per sibling name (see _intra_class_call_edges perf note).
1497
+ _INTRA_CALL_RE = re.compile(
1498
+ r'(?<![\w.])([A-Za-z_]\w*)\s*\(' r'|\bthis\s*\.\s*([A-Za-z_]\w*)\s*\('
1499
+ )
1500
+
1501
+
1492
1502
  def _intra_class_call_edges(symbols: list[SymbolRecord], source: str) -> list[RelationEdge]:
1493
1503
  """Method-level `calls` edges for intra-class invocations.
1494
1504
 
@@ -1528,13 +1538,20 @@ def _intra_class_call_edges(symbols: list[SymbolRecord], source: str) -> list[Re
1528
1538
  if not body:
1529
1539
  continue
1530
1540
  body = _STRING_LITERAL_RE.sub('', _strip_java_comments(body))
1531
- for name, fqns in sib.items():
1541
+ # PERF: one pass over the body collects every bare / this-qualified callee name,
1542
+ # then intersect with the sibling index. The old code ran a fresh regex search
1543
+ # per sibling name — O(siblings × body_len) per method, i.e. quadratic in method
1544
+ # count, which made large method-dense classes (generated parsers, 4k-line test
1545
+ # classes) pathological (alfresco cold scan ~220s). This is O(body_len). The
1546
+ # edge set is identical: _INTRA_CALL_RE matches the same two call forms.
1547
+ called: set[str] = set()
1548
+ for m in _INTRA_CALL_RE.finditer(body):
1549
+ called.add(m.group(1) or m.group(2))
1550
+ for name in sorted(called): # sorted → deterministic edge order
1532
1551
  if name in _CALL_KEYWORDS:
1533
1552
  continue
1534
- # bare `name(` (not preceded by word char or '.') OR explicit `this.name(`
1535
- pat = (r'(?<![\w.])' + re.escape(name) + r'\s*\('
1536
- + r'|\bthis\s*\.\s*' + re.escape(name) + r'\s*\(')
1537
- if not re.search(pat, body):
1553
+ fqns = sib.get(name)
1554
+ if not fqns:
1538
1555
  continue
1539
1556
  for tgt in fqns:
1540
1557
  if tgt == caller.symbol:
@@ -1967,6 +1984,22 @@ def _extract_class_type_refs(symbols: list[SymbolRecord]) -> dict[str, list[dict
1967
1984
  return out
1968
1985
 
1969
1986
 
1987
+ _LEADING_ANNOT_RE = re.compile(r'^\s*@\w+(?:\s*\([^()]*\))?\s*')
1988
+
1989
+
1990
+ def _strip_leading_annotations(line: str) -> str:
1991
+ """Strip leading inline annotations from a declaration line so the residual
1992
+ declaration is still detected (`@Autowired private Foo f;` -> `private Foo f;`).
1993
+ A pure annotation line (`@Override`, `@GetMapping("/x")`) strips to empty.
1994
+ Nested-paren annotation args (rare on fields) are left intact — the residual
1995
+ just won't match a decl regex, which is safe (a miss, never a wrong entry)."""
1996
+ prev: Optional[str] = None
1997
+ while line != prev:
1998
+ prev = line
1999
+ line = _LEADING_ANNOT_RE.sub('', line, count=1)
2000
+ return line.strip()
2001
+
2002
+
1970
2003
  def _extract_field_types(source: str) -> dict[str, dict[str, str]]:
1971
2004
  """{class_fqn -> {field_name -> declared_type_simple_name}} for EVERY field
1972
2005
  (annotated or plain), the fact that binds an invocation receiver to a typed
@@ -1994,8 +2027,17 @@ def _extract_field_types(source: str) -> dict[str, dict[str, str]]:
1994
2027
  if "*/" not in stripped:
1995
2028
  in_block = True
1996
2029
  continue
1997
- if not stripped or stripped.startswith("//") or stripped.startswith("*") or stripped.startswith("@"):
2030
+ if not stripped or stripped.startswith("//") or stripped.startswith("*"):
1998
2031
  continue
2032
+ if stripped.startswith("@"):
2033
+ # A field/type decl may carry leading inline annotations
2034
+ # (`@Autowired private Foo f;`). Strip them so the residual declaration is
2035
+ # still detected; a pure annotation line strips to empty and is skipped as
2036
+ # before. Brace counting below runs on the stripped residual, so annotation
2037
+ # string args containing braces (`@GetMapping("/a/{id}")`) never skew depth.
2038
+ stripped = _strip_leading_annotations(stripped)
2039
+ if not stripped:
2040
+ continue
1999
2041
  if stripped.startswith("package "):
2000
2042
  package = stripped[8:].rstrip(";").strip()
2001
2043
  continue
@@ -4351,6 +4393,84 @@ def extract_file_ir(
4351
4393
  return _assemble(symbols, relations, changed_symbols, spring_summary, route_diffs)
4352
4394
 
4353
4395
 
4396
+ def _receiver_typed_call_edges(
4397
+ body_facts: dict[str, list[dict]],
4398
+ field_types: dict[str, dict[str, str]],
4399
+ symbols: list[SymbolRecord],
4400
+ ) -> list[RelationEdge]:
4401
+ """Method-level `calls` edges for instance invocations through a typed field
4402
+ receiver (`field.method(...)`).
4403
+
4404
+ The per-file scans in `_build_relations` resolve intra-class (`m()`/`this.m()`)
4405
+ and static (`Type.method()`, uppercase receiver) calls, but a call through a
4406
+ field-injected or plainly-typed dependency goes through a lowercase receiver
4407
+ variable (`nominasCalculoService.aplicarMejora(...)`) and matched none of them.
4408
+ A service reached only via an injected sibling therefore had NO inbound `calls`
4409
+ edge: impact-chain found 0 callers and reported a false-confident "no blast
4410
+ radius / low risk isolated change" — a dangerous false negative.
4411
+
4412
+ Joins facts already extracted (no new parse — Prime Directive):
4413
+ - invocation atom (owner method → {receiver, callee}) [body_facts]
4414
+ - declared type of `receiver` on the owner's class [field_types]
4415
+ - target type resolved to a SINGLE in-repo class FQN [symbols]
4416
+ - callee resolved to that class's method(s) of that name [symbols]
4417
+
4418
+ Emits `owner_method --calls--> target_class#callee` (every overload of the name —
4419
+ arity is not resolved by name alone, matching `_intra_class_call_edges`). Anything
4420
+ unresolved — a local variable (no field entry), a JDK/library type, an ambiguous
4421
+ simple name, or a callee the target class does not declare — yields nothing: an
4422
+ unresolved fact stays unresolved rather than being guessed (INV honesty). Runs as
4423
+ a repo-wide post-pass because the target type + its methods live in other files.
4424
+ """
4425
+ if not body_facts or not field_types:
4426
+ return []
4427
+
4428
+ # type simple name → in-repo class FQN(s); class FQN → {method name → [method FQN]}
4429
+ type_simple_to_fqn: dict[str, list[str]] = {}
4430
+ class_methods: dict[str, dict[str, list[str]]] = {}
4431
+ for s in symbols:
4432
+ if s.type in ("class", "interface", "enum", "record") and "#" not in s.symbol:
4433
+ type_simple_to_fqn.setdefault(s.symbol.rsplit(".", 1)[-1], []).append(s.symbol)
4434
+ elif s.symbol_kind == "method" and "#" in s.symbol:
4435
+ cls = _enclosing_class(s.symbol)
4436
+ name = s.symbol.rsplit("#", 1)[1]
4437
+ class_methods.setdefault(cls, {}).setdefault(name, []).append(s.symbol)
4438
+
4439
+ edges: list[RelationEdge] = []
4440
+ for owner_fqn in sorted(body_facts):
4441
+ owner_cls = _enclosing_class(owner_fqn)
4442
+ fmap = field_types.get(owner_cls)
4443
+ if not fmap:
4444
+ continue
4445
+ for atom in body_facts[owner_fqn]:
4446
+ if atom.get("fact") != "invocation":
4447
+ continue
4448
+ receiver, callee = atom.get("receiver"), atom.get("callee")
4449
+ if not receiver or receiver == "this" or not callee:
4450
+ continue
4451
+ tsimple = fmap.get(receiver)
4452
+ if not tsimple:
4453
+ continue # not a typed field (local / unknown)
4454
+ tfqns = type_simple_to_fqn.get(tsimple)
4455
+ if not tfqns or len(tfqns) != 1:
4456
+ continue # unresolved / ambiguous type → skip (honest)
4457
+ target_cls = tfqns[0]
4458
+ if target_cls == owner_cls:
4459
+ continue # intra-class handled by _intra_class_call_edges
4460
+ target_methods = class_methods.get(target_cls, {}).get(callee)
4461
+ if not target_methods:
4462
+ continue # callee not declared on the target type
4463
+ for mfqn in target_methods:
4464
+ edges.append(RelationEdge(
4465
+ from_symbol=owner_fqn,
4466
+ to_symbol=mfqn,
4467
+ type="calls",
4468
+ confidence="medium",
4469
+ evidence={"type": "method_call", "value": f"{receiver}.{callee}(...)"},
4470
+ ))
4471
+ return edges
4472
+
4473
+
4354
4474
  def build_repo_ir(
4355
4475
  file_paths: list[str],
4356
4476
  root: Path,
@@ -4495,6 +4615,17 @@ def build_repo_ir(
4495
4615
  _annotation_reference_edges(_ann_ref_sources, all_symbols, all_relations, _same_pkg_map)
4496
4616
  )
4497
4617
 
4618
+ # Instance calls through a typed field receiver (`field.method(...)`). The per-file
4619
+ # scans resolve intra-class and static (`Type.method()`) calls but never a call
4620
+ # through a lowercase field-injected dependency, so a service reached only via an
4621
+ # injected sibling had 0 inbound `calls` edges and impact-chain reported a
4622
+ # false-confident "no blast radius". Repo-wide post-pass (target type + methods are
4623
+ # cross-file). Gated on body facts + field types being present (emit_body_facts).
4624
+ if emit_body_facts:
4625
+ all_relations.extend(
4626
+ _receiver_typed_call_edges(_all_body_facts, _all_field_types, all_symbols)
4627
+ )
4628
+
4498
4629
  spring_summary = _build_spring_summary(all_symbols)
4499
4630
 
4500
4631
  # Deduplicate relations
@@ -443,6 +443,7 @@ def _bfs_callers(
443
443
  reverse_graph: dict[str, dict[str, list[str]]],
444
444
  max_depth: int,
445
445
  impl_graph: object | None = None,
446
+ method_scoped: bool = False,
446
447
  ) -> tuple[list[str], list[str], bool]:
447
448
  """BFS from seed FQNs through reverse_graph.
448
449
 
@@ -481,8 +482,22 @@ def _bfs_callers(
481
482
  # into direct_callers and inflating the blast radius ~12×. Exclude any caller
482
483
  # whose owning class is one of the seed classes.
483
484
  seed_classes: set[str] = {normalize_owner_fqn(s) for s in seed_fqns}
485
+ seed_set: set[str] = set(seed_fqns)
484
486
  self_excluded: int = 0
485
487
 
488
+ # P0-IMPACT-01 fix: distinguish self-recursion from same-class sibling calls.
489
+ # For a METHOD-scoped query ("Foo#doWork"), a *different* method on the same class
490
+ # (e.g. Foo#callWork → Foo#doWork) is a legitimate caller — the only true
491
+ # "self-reference" is the seed method invoking itself (recursion). The v1.70.0
492
+ # whole-class exclusion is correct ONLY for CLASS-scoped queries, where a class's
493
+ # own members are members, not external callers. Applying it to method queries
494
+ # silently discarded the intra-class caller edges built by _intra_class_call_edges,
495
+ # producing a false "0 callers / isolated" for private helpers.
496
+ def _is_self_reference(caller: str) -> bool:
497
+ if method_scoped:
498
+ return caller in seed_set # only genuine recursion of a seed method
499
+ return normalize_owner_fqn(caller) in seed_classes
500
+
486
501
  # BUG-004: index class FQN → list of method-level keys in reverse_graph.
487
502
  # Callers of Foo#doWork are stored under reverse_graph["Foo#doWork"], never
488
503
  # under reverse_graph["Foo"]. Without this index, BFS silently terminates
@@ -519,7 +534,7 @@ def _bfs_callers(
519
534
  for etype, fqn_list in _edges_for(seed):
520
535
  if etype not in _SKIP_EDGE_TYPES:
521
536
  unique_direct_callers.update(
522
- c for c in fqn_list if normalize_owner_fqn(c) not in seed_classes
537
+ c for c in fqn_list if not _is_self_reference(c)
523
538
  )
524
539
 
525
540
  effective_depth = 1 if len(unique_direct_callers) > _BFS_CALLER_CAP else max_depth
@@ -534,7 +549,8 @@ def _bfs_callers(
534
549
  if caller in visited:
535
550
  return
536
551
  # BUG #2: a member of a seed class is not an external caller — drop it.
537
- if normalize_owner_fqn(caller) in seed_classes:
552
+ # (Method-scoped queries drop only true self-recursion; see _is_self_reference.)
553
+ if _is_self_reference(caller):
538
554
  visited.add(caller)
539
555
  self_excluded += 1
540
556
  return
@@ -855,6 +871,22 @@ class ImpactOrchestrator:
855
871
  original_seed_classes: set[str] = {_class_of(s) for s in seed_fqns}
856
872
  # Subtype classes surfaced to the caller as the impacted implementation set.
857
873
  subtype_classes_added: set[str] = set()
874
+
875
+ # Method-scoped query (e.g. "Impl#foo"): impl/interface expansion must preserve
876
+ # the queried method's identity. A change to `foo` does not affect callers of a
877
+ # sibling method `bar`, so the expanded SEED symbols are restricted to the same
878
+ # method name(s). Class-level queries (a class node with no '#' in the seed)
879
+ # expand to all members, as before. This bound became observable once precise
880
+ # `field.method(...)` call edges exist: without it a method query inherited the
881
+ # callers of every sibling method on the interface (blast-radius inflation).
882
+ _queried_methods: set[str] = {s.rsplit("#", 1)[1] for s in seed_fqns if "#" in s}
883
+ _method_scoped = bool(_queried_methods) and all("#" in s for s in seed_fqns)
884
+
885
+ def _member_in_scope(sym: str) -> bool:
886
+ if not _method_scoped:
887
+ return True
888
+ return "#" in sym and sym.rsplit("#", 1)[1] in _queried_methods
889
+
858
890
  if impl_graph is not None:
859
891
  seed_classes_ch001 = {_class_of(s) for s in seed_fqns}
860
892
  impl_seeds: list[str] = []
@@ -867,7 +899,8 @@ class ImpactOrchestrator:
867
899
  continue
868
900
  subtype_classes_added.add(impl_class)
869
901
  for sym in cir.symbols:
870
- if _class_of(sym) == impl_class and sym not in set(seed_fqns):
902
+ if (_class_of(sym) == impl_class and sym not in set(seed_fqns)
903
+ and _member_in_scope(sym)):
871
904
  impl_seeds.append(sym)
872
905
  if impl_seeds:
873
906
  seed_fqns = list(dict.fromkeys(seed_fqns + impl_seeds))
@@ -896,7 +929,8 @@ class ImpactOrchestrator:
896
929
  continue
897
930
  iface_classes_added.add(iface_class)
898
931
  for sym in cir.symbols:
899
- if _class_of(sym) == iface_class and sym not in set(seed_fqns):
932
+ if (_class_of(sym) == iface_class and sym not in set(seed_fqns)
933
+ and _member_in_scope(sym)):
900
934
  iface_seeds.append(sym)
901
935
  if iface_seeds:
902
936
  seed_fqns = list(dict.fromkeys(seed_fqns + iface_seeds))
@@ -909,14 +943,22 @@ class ImpactOrchestrator:
909
943
 
910
944
  # ── 2. BFS through reverse graph ─────────────────────────────────
911
945
  direct_callers, indirect_callers, truncated, self_excluded = _bfs_callers(
912
- seed_fqns, cir.reverse_graph, depth, impl_graph=impl_graph
946
+ seed_fqns, cir.reverse_graph, depth, impl_graph=impl_graph,
947
+ method_scoped=_method_scoped,
913
948
  )
914
949
  if self_excluded:
915
- warnings.append(
916
- f"Self-referential exclusion (BUG #2): {self_excluded} member(s) of the "
917
- f"analyzed class were dropped from callers — a class's own methods are "
918
- f"members, not external callers (they do not count toward blast radius)."
919
- )
950
+ if _method_scoped:
951
+ warnings.append(
952
+ f"Self-recursion excluded: {self_excluded} recursive self-call(s) of the "
953
+ f"queried method were dropped from callers (a method is not its own "
954
+ f"caller). Same-class sibling methods ARE counted as callers."
955
+ )
956
+ else:
957
+ warnings.append(
958
+ f"Self-referential exclusion (BUG #2): {self_excluded} member(s) of the "
959
+ f"analyzed class were dropped from callers — a class's own methods are "
960
+ f"members, not external callers (they do not count toward blast radius)."
961
+ )
920
962
  if truncated:
921
963
  warnings.append(
922
964
  "Hub-class guard active: symbol has > 500 direct callers — "
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes