sourcecode 2.1.0__tar.gz → 2.2.0__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 (138) hide show
  1. {sourcecode-2.1.0 → sourcecode-2.2.0}/CHANGELOG.md +75 -0
  2. {sourcecode-2.1.0 → sourcecode-2.2.0}/PKG-INFO +4 -4
  3. {sourcecode-2.1.0 → sourcecode-2.2.0}/README.md +3 -3
  4. {sourcecode-2.1.0 → sourcecode-2.2.0}/pyproject.toml +1 -1
  5. {sourcecode-2.1.0 → sourcecode-2.2.0}/src/sourcecode/__init__.py +1 -1
  6. sourcecode-2.2.0/src/sourcecode/caller_metrics.py +42 -0
  7. {sourcecode-2.1.0 → sourcecode-2.2.0}/src/sourcecode/cli.py +118 -29
  8. {sourcecode-2.1.0 → sourcecode-2.2.0}/src/sourcecode/detectors/java.py +29 -1
  9. sourcecode-2.2.0/src/sourcecode/endpoint_metrics.py +42 -0
  10. {sourcecode-2.1.0 → sourcecode-2.2.0}/src/sourcecode/explain.py +2 -0
  11. {sourcecode-2.1.0 → sourcecode-2.2.0}/src/sourcecode/path_filters.py +2 -1
  12. {sourcecode-2.1.0 → sourcecode-2.2.0}/src/sourcecode/repository_ir.py +50 -14
  13. {sourcecode-2.1.0 → sourcecode-2.2.0}/src/sourcecode/semantic_integration_engine.py +18 -2
  14. {sourcecode-2.1.0 → sourcecode-2.2.0}/src/sourcecode/serializer.py +23 -3
  15. {sourcecode-2.1.0 → sourcecode-2.2.0}/src/sourcecode/spring_semantic.py +18 -5
  16. {sourcecode-2.1.0 → sourcecode-2.2.0}/src/sourcecode/spring_tx_analyzer.py +21 -6
  17. {sourcecode-2.1.0 → sourcecode-2.2.0}/.github/workflows/build-windows.yml +0 -0
  18. {sourcecode-2.1.0 → sourcecode-2.2.0}/.gitignore +0 -0
  19. {sourcecode-2.1.0 → sourcecode-2.2.0}/.ruff.toml +0 -0
  20. {sourcecode-2.1.0 → sourcecode-2.2.0}/CONTRIBUTING.md +0 -0
  21. {sourcecode-2.1.0 → sourcecode-2.2.0}/LICENSE +0 -0
  22. {sourcecode-2.1.0 → sourcecode-2.2.0}/SECURITY.md +0 -0
  23. {sourcecode-2.1.0 → sourcecode-2.2.0}/raw +0 -0
  24. {sourcecode-2.1.0 → sourcecode-2.2.0}/scripts/compare_integration_engines.py +0 -0
  25. {sourcecode-2.1.0 → sourcecode-2.2.0}/scripts/customer_smoke_test.sh +0 -0
  26. {sourcecode-2.1.0 → sourcecode-2.2.0}/scripts/generate_jdk_exports.py +0 -0
  27. {sourcecode-2.1.0 → sourcecode-2.2.0}/src/sourcecode/adaptive_scanner.py +0 -0
  28. {sourcecode-2.1.0 → sourcecode-2.2.0}/src/sourcecode/architecture_analyzer.py +0 -0
  29. {sourcecode-2.1.0 → sourcecode-2.2.0}/src/sourcecode/architecture_summary.py +0 -0
  30. {sourcecode-2.1.0 → sourcecode-2.2.0}/src/sourcecode/ast_extractor.py +0 -0
  31. {sourcecode-2.1.0 → sourcecode-2.2.0}/src/sourcecode/cache.py +0 -0
  32. {sourcecode-2.1.0 → sourcecode-2.2.0}/src/sourcecode/call_surface.py +0 -0
  33. {sourcecode-2.1.0 → sourcecode-2.2.0}/src/sourcecode/canonical_ir.py +0 -0
  34. {sourcecode-2.1.0 → sourcecode-2.2.0}/src/sourcecode/cir_graphs.py +0 -0
  35. {sourcecode-2.1.0 → sourcecode-2.2.0}/src/sourcecode/classifier.py +0 -0
  36. {sourcecode-2.1.0 → sourcecode-2.2.0}/src/sourcecode/code_notes_analyzer.py +0 -0
  37. {sourcecode-2.1.0 → sourcecode-2.2.0}/src/sourcecode/confidence_analyzer.py +0 -0
  38. {sourcecode-2.1.0 → sourcecode-2.2.0}/src/sourcecode/context_graph.py +0 -0
  39. {sourcecode-2.1.0 → sourcecode-2.2.0}/src/sourcecode/context_scorer.py +0 -0
  40. {sourcecode-2.1.0 → sourcecode-2.2.0}/src/sourcecode/context_summarizer.py +0 -0
  41. {sourcecode-2.1.0 → sourcecode-2.2.0}/src/sourcecode/contract_model.py +0 -0
  42. {sourcecode-2.1.0 → sourcecode-2.2.0}/src/sourcecode/contract_pipeline.py +0 -0
  43. {sourcecode-2.1.0 → sourcecode-2.2.0}/src/sourcecode/coverage_parser.py +0 -0
  44. {sourcecode-2.1.0 → sourcecode-2.2.0}/src/sourcecode/dependency_analyzer.py +0 -0
  45. {sourcecode-2.1.0 → sourcecode-2.2.0}/src/sourcecode/detectors/__init__.py +0 -0
  46. {sourcecode-2.1.0 → sourcecode-2.2.0}/src/sourcecode/detectors/base.py +0 -0
  47. {sourcecode-2.1.0 → sourcecode-2.2.0}/src/sourcecode/detectors/csproj_parser.py +0 -0
  48. {sourcecode-2.1.0 → sourcecode-2.2.0}/src/sourcecode/detectors/dart.py +0 -0
  49. {sourcecode-2.1.0 → sourcecode-2.2.0}/src/sourcecode/detectors/dotnet.py +0 -0
  50. {sourcecode-2.1.0 → sourcecode-2.2.0}/src/sourcecode/detectors/elixir.py +0 -0
  51. {sourcecode-2.1.0 → sourcecode-2.2.0}/src/sourcecode/detectors/go.py +0 -0
  52. {sourcecode-2.1.0 → sourcecode-2.2.0}/src/sourcecode/detectors/heuristic.py +0 -0
  53. {sourcecode-2.1.0 → sourcecode-2.2.0}/src/sourcecode/detectors/hybrid.py +0 -0
  54. {sourcecode-2.1.0 → sourcecode-2.2.0}/src/sourcecode/detectors/jvm_ext.py +0 -0
  55. {sourcecode-2.1.0 → sourcecode-2.2.0}/src/sourcecode/detectors/nodejs.py +0 -0
  56. {sourcecode-2.1.0 → sourcecode-2.2.0}/src/sourcecode/detectors/parsers.py +0 -0
  57. {sourcecode-2.1.0 → sourcecode-2.2.0}/src/sourcecode/detectors/php.py +0 -0
  58. {sourcecode-2.1.0 → sourcecode-2.2.0}/src/sourcecode/detectors/project.py +0 -0
  59. {sourcecode-2.1.0 → sourcecode-2.2.0}/src/sourcecode/detectors/python.py +0 -0
  60. {sourcecode-2.1.0 → sourcecode-2.2.0}/src/sourcecode/detectors/ruby.py +0 -0
  61. {sourcecode-2.1.0 → sourcecode-2.2.0}/src/sourcecode/detectors/rust.py +0 -0
  62. {sourcecode-2.1.0 → sourcecode-2.2.0}/src/sourcecode/detectors/systems.py +0 -0
  63. {sourcecode-2.1.0 → sourcecode-2.2.0}/src/sourcecode/detectors/terraform.py +0 -0
  64. {sourcecode-2.1.0 → sourcecode-2.2.0}/src/sourcecode/detectors/tooling.py +0 -0
  65. {sourcecode-2.1.0 → sourcecode-2.2.0}/src/sourcecode/doc_analyzer.py +0 -0
  66. {sourcecode-2.1.0 → sourcecode-2.2.0}/src/sourcecode/dynamic_argument_surface.py +0 -0
  67. {sourcecode-2.1.0 → sourcecode-2.2.0}/src/sourcecode/endpoint_literals.py +0 -0
  68. {sourcecode-2.1.0 → sourcecode-2.2.0}/src/sourcecode/entrypoint_classifier.py +0 -0
  69. {sourcecode-2.1.0 → sourcecode-2.2.0}/src/sourcecode/env_analyzer.py +0 -0
  70. {sourcecode-2.1.0 → sourcecode-2.2.0}/src/sourcecode/error_schema.py +0 -0
  71. {sourcecode-2.1.0 → sourcecode-2.2.0}/src/sourcecode/evidence_provider.py +0 -0
  72. {sourcecode-2.1.0 → sourcecode-2.2.0}/src/sourcecode/file_chunker.py +0 -0
  73. {sourcecode-2.1.0 → sourcecode-2.2.0}/src/sourcecode/file_classifier.py +0 -0
  74. {sourcecode-2.1.0 → sourcecode-2.2.0}/src/sourcecode/format_contract.py +0 -0
  75. {sourcecode-2.1.0 → sourcecode-2.2.0}/src/sourcecode/fqn_utils.py +0 -0
  76. {sourcecode-2.1.0 → sourcecode-2.2.0}/src/sourcecode/git_analyzer.py +0 -0
  77. {sourcecode-2.1.0 → sourcecode-2.2.0}/src/sourcecode/graph_analyzer.py +0 -0
  78. {sourcecode-2.1.0 → sourcecode-2.2.0}/src/sourcecode/hibernate_strat.py +0 -0
  79. {sourcecode-2.1.0 → sourcecode-2.2.0}/src/sourcecode/jdk_exports.py +0 -0
  80. {sourcecode-2.1.0 → sourcecode-2.2.0}/src/sourcecode/license.py +0 -0
  81. {sourcecode-2.1.0 → sourcecode-2.2.0}/src/sourcecode/mcp/__init__.py +0 -0
  82. {sourcecode-2.1.0 → sourcecode-2.2.0}/src/sourcecode/mcp/onboarding/__init__.py +0 -0
  83. {sourcecode-2.1.0 → sourcecode-2.2.0}/src/sourcecode/mcp/onboarding/applier.py +0 -0
  84. {sourcecode-2.1.0 → sourcecode-2.2.0}/src/sourcecode/mcp/onboarding/backup.py +0 -0
  85. {sourcecode-2.1.0 → sourcecode-2.2.0}/src/sourcecode/mcp/onboarding/detector.py +0 -0
  86. {sourcecode-2.1.0 → sourcecode-2.2.0}/src/sourcecode/mcp/onboarding/planner.py +0 -0
  87. {sourcecode-2.1.0 → sourcecode-2.2.0}/src/sourcecode/mcp/orchestrator.py +0 -0
  88. {sourcecode-2.1.0 → sourcecode-2.2.0}/src/sourcecode/mcp/registry.py +0 -0
  89. {sourcecode-2.1.0 → sourcecode-2.2.0}/src/sourcecode/mcp/runner.py +0 -0
  90. {sourcecode-2.1.0 → sourcecode-2.2.0}/src/sourcecode/mcp/server.py +0 -0
  91. {sourcecode-2.1.0 → sourcecode-2.2.0}/src/sourcecode/mcp_nudge.py +0 -0
  92. {sourcecode-2.1.0 → sourcecode-2.2.0}/src/sourcecode/metrics_analyzer.py +0 -0
  93. {sourcecode-2.1.0 → sourcecode-2.2.0}/src/sourcecode/migrate_check.py +0 -0
  94. {sourcecode-2.1.0 → sourcecode-2.2.0}/src/sourcecode/openapi_surface.py +0 -0
  95. {sourcecode-2.1.0 → sourcecode-2.2.0}/src/sourcecode/output_budget.py +0 -0
  96. {sourcecode-2.1.0 → sourcecode-2.2.0}/src/sourcecode/pr_comment_renderer.py +0 -0
  97. {sourcecode-2.1.0 → sourcecode-2.2.0}/src/sourcecode/pr_impact.py +0 -0
  98. {sourcecode-2.1.0 → sourcecode-2.2.0}/src/sourcecode/prepare_context.py +0 -0
  99. {sourcecode-2.1.0 → sourcecode-2.2.0}/src/sourcecode/progress.py +0 -0
  100. {sourcecode-2.1.0 → sourcecode-2.2.0}/src/sourcecode/ranking_engine.py +0 -0
  101. {sourcecode-2.1.0 → sourcecode-2.2.0}/src/sourcecode/redactor.py +0 -0
  102. {sourcecode-2.1.0 → sourcecode-2.2.0}/src/sourcecode/relevance_scorer.py +0 -0
  103. {sourcecode-2.1.0 → sourcecode-2.2.0}/src/sourcecode/rename_refactor.py +0 -0
  104. {sourcecode-2.1.0 → sourcecode-2.2.0}/src/sourcecode/repo_classifier.py +0 -0
  105. {sourcecode-2.1.0 → sourcecode-2.2.0}/src/sourcecode/ris.py +0 -0
  106. {sourcecode-2.1.0 → sourcecode-2.2.0}/src/sourcecode/runtime_classifier.py +0 -0
  107. {sourcecode-2.1.0 → sourcecode-2.2.0}/src/sourcecode/scanner.py +0 -0
  108. {sourcecode-2.1.0 → sourcecode-2.2.0}/src/sourcecode/schema.py +0 -0
  109. {sourcecode-2.1.0 → sourcecode-2.2.0}/src/sourcecode/security_config.py +0 -0
  110. {sourcecode-2.1.0 → sourcecode-2.2.0}/src/sourcecode/security_posture.py +0 -0
  111. {sourcecode-2.1.0 → sourcecode-2.2.0}/src/sourcecode/semantic_analyzer.py +0 -0
  112. {sourcecode-2.1.0 → sourcecode-2.2.0}/src/sourcecode/semantic_impact_engine.py +0 -0
  113. {sourcecode-2.1.0 → sourcecode-2.2.0}/src/sourcecode/semantic_services.py +0 -0
  114. {sourcecode-2.1.0 → sourcecode-2.2.0}/src/sourcecode/spring_event_topology.py +0 -0
  115. {sourcecode-2.1.0 → sourcecode-2.2.0}/src/sourcecode/spring_findings.py +0 -0
  116. {sourcecode-2.1.0 → sourcecode-2.2.0}/src/sourcecode/spring_impact.py +0 -0
  117. {sourcecode-2.1.0 → sourcecode-2.2.0}/src/sourcecode/spring_model.py +0 -0
  118. {sourcecode-2.1.0 → sourcecode-2.2.0}/src/sourcecode/spring_security_audit.py +0 -0
  119. {sourcecode-2.1.0 → sourcecode-2.2.0}/src/sourcecode/summarizer.py +0 -0
  120. {sourcecode-2.1.0 → sourcecode-2.2.0}/src/sourcecode/telemetry/__init__.py +0 -0
  121. {sourcecode-2.1.0 → sourcecode-2.2.0}/src/sourcecode/telemetry/config.py +0 -0
  122. {sourcecode-2.1.0 → sourcecode-2.2.0}/src/sourcecode/telemetry/consent.py +0 -0
  123. {sourcecode-2.1.0 → sourcecode-2.2.0}/src/sourcecode/telemetry/events.py +0 -0
  124. {sourcecode-2.1.0 → sourcecode-2.2.0}/src/sourcecode/telemetry/filters.py +0 -0
  125. {sourcecode-2.1.0 → sourcecode-2.2.0}/src/sourcecode/telemetry/transport.py +0 -0
  126. {sourcecode-2.1.0 → sourcecode-2.2.0}/src/sourcecode/tree_utils.py +0 -0
  127. {sourcecode-2.1.0 → sourcecode-2.2.0}/src/sourcecode/type_usage_surface.py +0 -0
  128. {sourcecode-2.1.0 → sourcecode-2.2.0}/src/sourcecode/validation_inference.py +0 -0
  129. {sourcecode-2.1.0 → sourcecode-2.2.0}/src/sourcecode/validation_surface.py +0 -0
  130. {sourcecode-2.1.0 → sourcecode-2.2.0}/src/sourcecode/version_check.py +0 -0
  131. {sourcecode-2.1.0 → sourcecode-2.2.0}/src/sourcecode/workspace.py +0 -0
  132. {sourcecode-2.1.0 → sourcecode-2.2.0}/supabase/functions/README.md +0 -0
  133. {sourcecode-2.1.0 → sourcecode-2.2.0}/supabase/functions/get-license/index.ts +0 -0
  134. {sourcecode-2.1.0 → sourcecode-2.2.0}/supabase/functions/lemonsqueezy-webhook/index.ts +0 -0
  135. {sourcecode-2.1.0 → sourcecode-2.2.0}/supabase/functions/telemetry/index.ts +0 -0
  136. {sourcecode-2.1.0 → sourcecode-2.2.0}/supabase/sql/license_event_ordering.sql +0 -0
  137. {sourcecode-2.1.0 → sourcecode-2.2.0}/supabase/sql/licensing_schema.sql +0 -0
  138. {sourcecode-2.1.0 → sourcecode-2.2.0}/supabase/sql/telemetry_events.sql +0 -0
@@ -1,5 +1,80 @@
1
1
  # Changelog
2
2
 
3
+ ## [2.2.0] — 2026-07-12
4
+
5
+ **Field-validation hardening — narrative/reporting-layer correctness across the
6
+ audit corpus.** A refute-first pass over ten real repositories fixed a batch of
7
+ mislabeling and miscount bugs where the headline framing contradicted the
8
+ structured output the engine had already produced. No Semantic IR change — every
9
+ fix is in the reporting/narrative layer and reuses facts already extracted.
10
+
11
+ ### Fixed
12
+
13
+ - **`endpoints` — modeled JAX-RS no longer double-reported as un-modeled surface
14
+ (A2).** `_build_route_surface` fully models JAX-RS, but `extract_java_endpoints`
15
+ also counted every `@Path` file into `non_spring_rest_surface{modeled:false}` and
16
+ warned "count covers Spring-MVC-annotated endpoints only" — contradicting the
17
+ JAX-RS routes just emitted (18 on eureka, 689 on keycloak). Modeled routes now
18
+ carry framework provenance (`jax_rs`/`spring_mvc`); when JAX-RS routes were
19
+ modeled, `jax_rs` is dropped from the un-modeled surface. Genuinely un-modeled
20
+ surfaces (WebScripts/Servlets, or JAX-RS that produced no route) still surface and
21
+ still warn.
22
+ - **`spring-audit` — overloaded `@Transactional` methods counted separately (A1).**
23
+ `TransactionBoundaryIndex.by_symbol` is keyed by signature-less FQN, so overloaded
24
+ methods (`findAll()` / `findAll(Pageable)`) collapsed to one entry and `stats()`
25
+ undercounted. A new `all_declared` list (one entry per site) drives the count
26
+ (petclinic 1→2, matching source); FQN-keyed lookups are unchanged.
27
+ - **`spring-audit` — per-site TX overloads no longer hidden by `by_symbol` collapse
28
+ (A1-RESIDUAL).** TX-001 (proxy-bypass) and TX-005 (exception-swallow) are per-site
29
+ checks but iterated the collapsed FQN map; a non-risky overload could overwrite a
30
+ risky one sharing an FQN, silently dropping the finding. Both now iterate
31
+ `all_declared` with a dedupe guard. Corpus TX counts unchanged (the residual was
32
+ latent).
33
+ - **`compact` — Spring Boot version surfaced from pom property (A3).** `--compact`
34
+ deployment omitted the Boot version when it lived in a `<spring.boot.version>`
35
+ property rather than the starter-parent, while `migrate-check` resolved it. The
36
+ serializer now falls back to the property-resolving detector (single source of
37
+ truth), gated on Boot presence so non-Boot repos gain no phantom version
38
+ (Broadleaf 3.5.14, consistent).
39
+ - **`endpoints` — `Map.put("/", literal)` no longer treated as a router-DSL route
40
+ (B1).** The router-DSL detector matched an HTTP-verb name + `"/"` first arg; a
41
+ plain map write (`swapChars.put("/", "slash")`) was reported as a `PUT /`
42
+ endpoint. A negative lookahead now rejects a literal second argument — a handler
43
+ is never a bare literal (openmrs 3→2, jobrunr 12/12 kept).
44
+ - **`endpoints` — test-suite module controllers excluded from entry points (B2).**
45
+ A scaffolding `@Controller` in openmrs' `test-suite` module (sources under
46
+ `src/main`) surfaced as a production entry point. Entry-point collection now uses
47
+ the broader `is_test_or_fixture_path`, and `test-suite`/`test-suites` are
48
+ recognized test-module segments (openmrs 19→18; petclinic/eureka/keycloak
49
+ unchanged).
50
+ - **`stack` — JAX-RS API surfaced in the headline stack from source annotations
51
+ (C1).** Eureka reported `project_type=library` with an empty stack despite 18
52
+ JAX-RS routes, because the manifest scan has no Jersey/Guice/jsr311 vocabulary and
53
+ (for gradle) reads only the root build file. When the `.java` scan found JAX-RS
54
+ entry points and no Jakarta EE framework was inferred, the stack now surfaces
55
+ `Jakarta EE` from the `@Path`/`@GET` evidence (eureka → `Java rest api using
56
+ Jakarta EE`). Manifest-only jsr311 still infers nothing (honesty preserved).
57
+
58
+ ### Changed
59
+
60
+ - **`modernize` — cyclic tangles distinguished from one-way deps in
61
+ `cross_module_tangles` (TANGLE-1).** A directional dependency is normal layering,
62
+ not a tangle. Each entry now carries `coupling_type` (`cyclic` when mutual, else
63
+ `directional`); the note and recommendation state only cyclic entries are real
64
+ tangles. The key is retained (mcp/USER_GUIDE/tests reference it); cyclic entries
65
+ still sort first.
66
+ - **CLI — consistent progress spinner across all long-running commands (UX-1).** The
67
+ TTY-gated progress spinner ran on only 5 code paths; the slowest commands
68
+ (`spring-audit`, `validation`, `migrate-check`, `endpoints`, `export`,
69
+ `impact-chain`, `pr-impact`, `explain`) ran silent. All 9 now show the same
70
+ stderr-only, TTY/CI-gated indicator (never pollutes stdout JSON, zero cost in CI).
71
+
72
+ ### Notes
73
+
74
+ - No breaking changes. All fixes are reporting/narrative-layer; the Semantic IR and
75
+ default JSON schemas are unchanged (`cross_module_tangles` gains an additive
76
+ `coupling_type` field). 3193 tests passing.
77
+
3
78
  ## [2.1.0] — 2026-07-10
4
79
 
5
80
  **Robustness & Security Inference — vendor-agnostic security & validation posture.**
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: sourcecode
3
- Version: 2.1.0
3
+ Version: 2.2.0
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
@@ -38,11 +38,11 @@ Description-Content-Type: text/markdown
38
38
 
39
39
  # ASK Engine
40
40
 
41
- > **Persistent structural intelligence for AI coding agents.**
41
+ > **ASK — Actionable Software Knowledge.** Persistent structural intelligence for AI coding agents.
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.1.0-blue)
45
+ ![Version](https://img.shields.io/badge/version-2.2.0-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.1.0
87
+ ask version # ask 2.2.0
88
88
  ```
89
89
 
90
90
  > **Package vs. command.** The install package is named `sourcecode` this release
@@ -1,10 +1,10 @@
1
1
  # ASK Engine
2
2
 
3
- > **Persistent structural intelligence for AI coding agents.**
3
+ > **ASK — Actionable Software Knowledge.** Persistent structural intelligence for AI coding agents.
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.1.0-blue)
7
+ ![Version](https://img.shields.io/badge/version-2.2.0-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.1.0
49
+ ask version # ask 2.2.0
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.1.0"
7
+ version = "2.2.0"
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.1.0"
7
+ __version__ = "2.2.0"
@@ -0,0 +1,42 @@
1
+ """Single source of truth for the caller / fan-in metric reconciliation note.
2
+
3
+ Three commands report a "how many things reference this class" number, each
4
+ derived from a different graph projection. Read side by side they can look
5
+ contradictory (e.g. 215 vs 103 for the same class) — they are not. They count
6
+ different populations:
7
+
8
+ modernize.in_degree >= explain.incoming_callers ~= impact.stats.direct_caller_count
9
+
10
+ - ``modernize.in_degree`` counts graph *edges* (all edge types, symbol level, not
11
+ deduplicated per class) — a blast-radius ranking weight, always the largest, and
12
+ not a count of classes.
13
+ - ``explain.incoming_callers`` and ``impact.stats.direct_caller_count`` both count
14
+ *distinct dependent classes*. They normally match and are always the same order
15
+ of magnitude, but can differ by a small margin because each includes a slightly
16
+ different edge set (import-only references, DI-interface resolution). The
17
+ truncated ``impact.direct_callers`` array is a display sample, not the count.
18
+
19
+ Empirically verified across repositories (Broadleaf ExtensionManager: 215 / 103 /
20
+ 103; petclinic entities & repositories: equal; occasional margin-of-one divergence
21
+ on classes with import-only references). The strict-equality claim was deliberately
22
+ NOT made — the note asserts only the robust ordering and the "same distinct-class
23
+ metric" relationship.
24
+
25
+ Every command that emits one of these numbers references this same string so the
26
+ explanation can never drift or contradict itself between commands.
27
+ """
28
+
29
+ CALLER_METRIC_RECONCILIATION: str = (
30
+ "Fan-in / caller counts differ across commands BY DESIGN and are not "
31
+ "contradictory. For the same class the expected relationship is: "
32
+ "modernize.in_degree >= explain.incoming_callers ~= impact.stats.direct_caller_count. "
33
+ "modernize.in_degree = raw count of ALL incoming graph edges (imports, injects, "
34
+ "extends/implements, references, annotations), counted at symbol level and NOT "
35
+ "deduplicated per class — a blast-radius ranking weight, not a count of classes; "
36
+ "it is always the largest of the three. explain.incoming_callers and "
37
+ "impact.stats.direct_caller_count both count DISTINCT dependent classes "
38
+ "(deduplicated to class level); they normally match but can differ by a small "
39
+ "margin because each includes a slightly different edge set (e.g. import-only "
40
+ "references or DI-interface resolution). The top-level impact.direct_callers array "
41
+ "may be truncated for output size — use stats.direct_caller_count for the true total."
42
+ )
@@ -15,6 +15,7 @@ from sourcecode import __version__
15
15
  from sourcecode.error_schema import INVALID_INPUT_CODE, build_error_envelope
16
16
  from sourcecode.entrypoint_classifier import is_production_entry_point, normalize_entry_point
17
17
  from sourcecode.progress import Progress
18
+ from sourcecode.caller_metrics import CALLER_METRIC_RECONCILIATION
18
19
  from sourcecode.repository_ir import extract_java_endpoints as _extract_java_endpoints
19
20
 
20
21
 
@@ -700,7 +701,8 @@ _WELCOME_CMDS = (
700
701
 
701
702
  def _print_welcome_plain(tier: str) -> None:
702
703
  """Plain-text welcome — fallback when rich is unavailable."""
703
- lines = ["", f" ASK Engine {__version__} · {tier} · CLI: ask", "",
704
+ lines = ["", f" ASK Engine {__version__} · {tier} · CLI: ask",
705
+ " Actionable Software Knowledge Engine", "",
704
706
  " AI coding-agent context, instant.", "", " Get started:"]
705
707
  for cmd, desc in _WELCOME_CMDS:
706
708
  lines.append(f" {cmd.ljust(34)}{desc}")
@@ -747,8 +749,9 @@ def _print_welcome() -> None:
747
749
  t.append(" ")
748
750
  t.append(tier, style=tier_style)
749
751
  t.append("\n")
750
- # "ENGINE" label beneath the wordmark.
751
- t.append(_WELCOME_ENGINE + "\n", style="bold dim cyan")
752
+ # "ENGINE" label beneath the wordmark, with the acronym expansion.
753
+ t.append(_WELCOME_ENGINE, style="bold dim cyan")
754
+ t.append(" Actionable Software Knowledge\n", style="dim")
752
755
 
753
756
  t.append("\nAI coding-agent context, instant.\n\n", style="white")
754
757
 
@@ -3911,7 +3914,12 @@ def endpoints_cmd(
3911
3914
  )
3912
3915
  raise typer.Exit(code=1)
3913
3916
 
3914
- data = _extract_java_endpoints(target)
3917
+ _prog = Progress()
3918
+ _prog.start("scanning endpoints")
3919
+ try:
3920
+ data = _extract_java_endpoints(target)
3921
+ finally:
3922
+ _prog.finish()
3915
3923
 
3916
3924
  # Update RIS api_surface section (non-fatal side-effect).
3917
3925
  try:
@@ -4358,6 +4366,9 @@ def export_cmd(
4358
4366
  )
4359
4367
  raise typer.Exit(1)
4360
4368
 
4369
+ _prog = Progress()
4370
+ _prog.start("scanning repository")
4371
+
4361
4372
  file_list = [
4362
4373
  f for f in find_java_files(root)
4363
4374
  if "/test/" not in f and "/tests/" not in f
@@ -4384,6 +4395,7 @@ def export_cmd(
4384
4395
  _integrations,
4385
4396
  endpoint_meta=_ep_data,
4386
4397
  )
4398
+ _prog.finish()
4387
4399
  output = _serialize_dict(data, format)
4388
4400
  _emit_command_output(output, output_path, copy,
4389
4401
  success_msg=f"C4 architecture export written to {output_path}")
@@ -4413,6 +4425,7 @@ def export_cmd(
4413
4425
  ContextGraph.build(file_list, root)
4414
4426
  ).as_report(len(file_list))
4415
4427
 
4428
+ _prog.finish()
4416
4429
  output = _serialize_dict(data, format)
4417
4430
  _emit_command_output(output, output_path, copy,
4418
4431
  success_msg=f"Export written to {output_path}")
@@ -4495,6 +4508,8 @@ def validation_cmd(
4495
4508
 
4496
4509
  from sourcecode.context_graph import ContextGraph
4497
4510
  from sourcecode.validation_surface import build_validation_surface
4511
+ _prog = Progress()
4512
+ _prog.start("mapping validation surface")
4498
4513
  # Structural facts come from the ContextGraph — the single access layer.
4499
4514
  _graph = ContextGraph.build_from_root(target)
4500
4515
  data = build_validation_surface(target, graph=_graph)
@@ -4527,6 +4542,7 @@ def validation_cmd(
4527
4542
  if _note:
4528
4543
  data["note"] = _note
4529
4544
 
4545
+ _prog.finish()
4530
4546
  output = _serialize_dict(data, format)
4531
4547
  _summary = data.get("summary", {})
4532
4548
  # Human-facing heads-up when the result is empty purely because no OpenAPI
@@ -4750,6 +4766,8 @@ def spring_audit_cmd(
4750
4766
  success_msg=f"Spring audit written to {output_path}")
4751
4767
  return
4752
4768
 
4769
+ _prog = Progress()
4770
+ _prog.start(f"auditing {len(file_list)} Java files")
4753
4771
  cir = ContextGraph.build(file_list, target).cir
4754
4772
  _model = SpringSemanticModel.build(cir)
4755
4773
 
@@ -4802,6 +4820,7 @@ def spring_audit_cmd(
4802
4820
  except Exception:
4803
4821
  pass
4804
4822
 
4823
+ _prog.finish()
4805
4824
  if format == "github-comment":
4806
4825
  output = _render_spring_audit_github_comment(combined, min_severity)
4807
4826
  else:
@@ -4920,7 +4939,12 @@ def migrate_check_cmd(
4920
4939
 
4921
4940
  _file_limitations: list[str] = []
4922
4941
  file_list = find_java_files(target, limitations=_file_limitations)
4923
- report = run_migrate_check(file_list, target, min_severity=min_severity)
4942
+ _prog = Progress()
4943
+ _prog.start(f"checking migration ({len(file_list)} files)")
4944
+ try:
4945
+ report = run_migrate_check(file_list, target, min_severity=min_severity)
4946
+ finally:
4947
+ _prog.finish()
4924
4948
  if _file_limitations:
4925
4949
  report.limitations.extend(_file_limitations)
4926
4950
 
@@ -5079,6 +5103,8 @@ def impact_chain_cmd(
5079
5103
  success_msg=f"Impact chain written to {output_path}")
5080
5104
  return
5081
5105
 
5106
+ _prog = Progress()
5107
+ _prog.start(f"analyzing impact ({len(file_list)} files)")
5082
5108
  cir = ContextGraph.build(file_list, target).cir
5083
5109
  _model = SpringSemanticModel.build(cir)
5084
5110
 
@@ -5086,6 +5112,7 @@ def impact_chain_cmd(
5086
5112
  from sourcecode.spring_event_topology import run_event_topology
5087
5113
  evt_result = run_event_topology(cir, symbol, model=_model)
5088
5114
  data = evt_result.to_dict()
5115
+ _prog.finish()
5089
5116
  output = _serialize_dict(data, format)
5090
5117
  _emit_command_output(
5091
5118
  output, output_path, copy,
@@ -5101,6 +5128,7 @@ def impact_chain_cmd(
5101
5128
  result = run_impact_chain(cir, symbol, depth=depth, root=target, model=_model)
5102
5129
 
5103
5130
  data = result.to_dict()
5131
+ _prog.finish()
5104
5132
  output = _serialize_dict(data, format)
5105
5133
  _emit_command_output(
5106
5134
  output, output_path, copy,
@@ -5229,9 +5257,14 @@ def pr_impact_cmd(
5229
5257
  success_msg=f"PR impact report written to {output_path}")
5230
5258
  return
5231
5259
 
5232
- cir = ContextGraph.build(file_list, target).cir
5233
- model = SpringSemanticModel.build(cir)
5234
- report = run_pr_impact(cir, changed_files, root=target, model=model)
5260
+ _prog = Progress()
5261
+ _prog.start(f"analyzing PR impact ({len(file_list)} files)")
5262
+ try:
5263
+ cir = ContextGraph.build(file_list, target).cir
5264
+ model = SpringSemanticModel.build(cir)
5265
+ report = run_pr_impact(cir, changed_files, root=target, model=model)
5266
+ finally:
5267
+ _prog.finish()
5235
5268
 
5236
5269
  output = _serialize_dict(report.to_dict(), "json") if format == "json" else report.render_text()
5237
5270
  _emit_command_output(
@@ -5348,9 +5381,14 @@ def explain_cmd(
5348
5381
  )
5349
5382
  raise typer.Exit(code=1)
5350
5383
 
5351
- cir = ContextGraph.build(file_list, target).cir
5352
- model = SpringSemanticModel.build(cir)
5353
- explanation = explain_class(class_name, cir, model)
5384
+ _prog = Progress()
5385
+ _prog.start(f"explaining {class_name} ({len(file_list)} files)")
5386
+ try:
5387
+ cir = ContextGraph.build(file_list, target).cir
5388
+ model = SpringSemanticModel.build(cir)
5389
+ explanation = explain_class(class_name, cir, model)
5390
+ finally:
5391
+ _prog.finish()
5354
5392
 
5355
5393
  if format == "json":
5356
5394
  output = _json.dumps(explanation.to_dict(), indent=2, ensure_ascii=False)
@@ -5663,7 +5701,56 @@ def _partition_static_unreferenced(nodes: list[dict], root: Path) -> tuple[list[
5663
5701
  if files_scanned >= _CONFIG_SCAN_MAX_FILES or not unresolved_simple:
5664
5702
  break
5665
5703
 
5666
- unreferenced = [n for n in nodes if n["fqn"] not in dispatched_fqns]
5704
+ # 3. Nested-type qualified-reference scan (SC-2 residual). The static call-graph
5705
+ # does not emit an edge for a nested-type member access in ordinary method-body
5706
+ # code — e.g. `PropertyType.AdminGroupPresentation.NAME` credits the enclosing
5707
+ # type, not the nested holder — so a referenced nested class can look
5708
+ # zero-degree. When a candidate is a nested type (its second-to-last FQN segment
5709
+ # is an UpperCamel enclosing type) and its qualified `Outer.Nested` form appears
5710
+ # in some OTHER Java source, it is statically referenced: neither dead nor
5711
+ # framework-dispatched. Structural, name-agnostic (pattern derived from the
5712
+ # candidate's own FQN).
5713
+ import re as _re_nested
5714
+ statically_referenced: set[str] = set()
5715
+ nested_patterns: dict[str, "tuple"] = {}
5716
+ for n in nodes:
5717
+ if n["fqn"] in dispatched_fqns:
5718
+ continue
5719
+ parts = (n.get("fqn") or "").split(".")
5720
+ if len(parts) >= 2 and parts[-2][:1].isupper():
5721
+ outer, nested = parts[-2], parts[-1]
5722
+ nested_patterns[n["fqn"]] = (
5723
+ _re_nested.compile(r"\b" + _re_nested.escape(outer) + r"\." + _re_nested.escape(nested) + r"\b"),
5724
+ n.get("source_file") or "",
5725
+ )
5726
+ if nested_patterns:
5727
+ files_scanned = 0
5728
+ for dirpath, dirnames, filenames in os.walk(root):
5729
+ dirnames[:] = [d for d in dirnames
5730
+ if d not in {".git", "build", "out", "target", "node_modules", ".gradle"}]
5731
+ for fname in filenames:
5732
+ if not fname.endswith(".java"):
5733
+ continue
5734
+ if files_scanned >= _CONFIG_SCAN_MAX_FILES or len(statically_referenced) == len(nested_patterns):
5735
+ break
5736
+ fpath = os.path.join(dirpath, fname)
5737
+ rel = os.path.relpath(fpath, root)
5738
+ try:
5739
+ with open(fpath, "r", encoding="utf-8", errors="replace") as fh:
5740
+ text = fh.read(_CONFIG_SCAN_MAX_BYTES)
5741
+ except OSError:
5742
+ continue
5743
+ files_scanned += 1
5744
+ for fqn, (pat, own) in nested_patterns.items():
5745
+ if fqn in statically_referenced or rel == own:
5746
+ continue
5747
+ if pat.search(text):
5748
+ statically_referenced.add(fqn)
5749
+ if files_scanned >= _CONFIG_SCAN_MAX_FILES or len(statically_referenced) == len(nested_patterns):
5750
+ break
5751
+
5752
+ unreferenced = [n for n in nodes
5753
+ if n["fqn"] not in dispatched_fqns and n["fqn"] not in statically_referenced]
5667
5754
  dispatched = [n for n in nodes if n["fqn"] in dispatched_fqns]
5668
5755
  return unreferenced, dispatched
5669
5756
 
@@ -5870,8 +5957,13 @@ def modernize_cmd(
5870
5957
  "edge_count": _cnt,
5871
5958
  "reverse_edge_count": _reverse,
5872
5959
  # A mutual (cyclic) dependency is the actual "tangle" — both modules
5873
- # reference each other, so neither can be extracted independently.
5960
+ # reference each other, so neither can be extracted independently. A
5961
+ # one-way (directional) dependency is normal layering, NOT a tangle:
5962
+ # TANGLE-1 (petclinic field test) surfaced owner→model / vet→model
5963
+ # (reverse_edge_count:0) being labelled "tangles" when they are plain
5964
+ # base-model usage. coupling_type states which it is, honestly.
5874
5965
  "mutual": _reverse > 0,
5966
+ "coupling_type": "cyclic" if _reverse > 0 else "directional",
5875
5967
  "example": _pair_example.get((_a, _b), ""),
5876
5968
  })
5877
5969
  # Surface mutual tangles first (highest decomposition risk), then by strength.
@@ -5947,17 +6039,10 @@ def modernize_cmd(
5947
6039
  {"fqn": n["fqn"], "in_degree": n.get("in_degree", 0), "role": n.get("role", "other")}
5948
6040
  for n in coupling_nodes
5949
6041
  ],
5950
- # BUG #6 (Broadleaf field test): make the in_degree metric self-describing
5951
- # so it is not confused with `explain`'s caller count. They differ by design.
5952
- "high_coupling_nodes_note": (
5953
- "in_degree = raw count of incoming graph edges across ALL edge types "
5954
- "(imports, injects, extends/implements, references, annotations), "
5955
- "counted at symbol level. This is deliberately larger than and NOT "
5956
- "equal to `ask explain`'s caller count, which reports DISTINCT "
5957
- "dependent classes (DI dependents + reverse-call-graph callers, "
5958
- "deduplicated to class level). Use in_degree for blast-radius ranking, "
5959
- "explain's caller list for the concrete dependents to inspect."
5960
- ),
6042
+ # SC-6 (Broadleaf field test): make the in_degree metric self-describing
6043
+ # so it is not confused with `explain`'s / `impact`'s caller counts. All
6044
+ # three share one reconciliation note so they can never contradict.
6045
+ "high_coupling_nodes_note": CALLER_METRIC_RECONCILIATION,
5961
6046
  "statically_unreferenced": [
5962
6047
  {"fqn": n["fqn"], "type": n.get("type", ""), "role": n.get("role", "other")}
5963
6048
  for n in dead_zones
@@ -5978,10 +6063,12 @@ def modernize_cmd(
5978
6063
  "cross_module_tangles_note": (
5979
6064
  "Directed inter-subsystem coupling measured from structural graph "
5980
6065
  "edges (imports/injects/extends/implements/references/calls). "
5981
- "edge_count = edges from_package→to_package; mutual=true marks a "
5982
- "bidirectional (cyclic) dependency — the actual tangle that blocks "
5983
- "independent module extraction. Empty means no cross-subsystem "
5984
- "structural coupling was detected."
6066
+ "edge_count = edges from_package→to_package. coupling_type=cyclic "
6067
+ "(mutual=true) is a bidirectional dependency — the actual tangle "
6068
+ "that blocks independent module extraction; coupling_type="
6069
+ "directional (reverse_edge_count=0) is a normal one-way layered "
6070
+ "dependency, listed for coupling strength, NOT a tangle. Empty "
6071
+ "means no cross-subsystem structural coupling was detected."
5985
6072
  ),
5986
6073
  # BUG-05 fix: don't recommend "Start with hotspot_candidates" when the list is empty.
5987
6074
  "recommendation": (
@@ -5992,7 +6079,9 @@ def modernize_cmd(
5992
6079
  )
5993
6080
  + "statically_unreferenced lists classes with no Java callers — review "
5994
6081
  + "for framework dispatch (XML/reflection/SPI) before removing. "
5995
- + "Cross-module tangles indicate coupling worth decomposing."
6082
+ + "In cross_module_tangles, coupling_type=cyclic entries are the "
6083
+ + "real tangles to decompose first; directional entries are normal "
6084
+ + "layering ranked by coupling strength."
5996
6085
  ),
5997
6086
  }
5998
6087
 
@@ -176,6 +176,28 @@ class JavaDetector(AbstractDetector):
176
176
  frameworks = [f for f in frameworks if f.name != "MyBatis"]
177
177
 
178
178
  entry_points = self._collect_entry_points(context)
179
+
180
+ # C1 (Eureka field test): a repo whose REST surface is JAX-RS / Jakarta
181
+ # REST — declared on the source via @Path/@GET, not through a web framework
182
+ # coordinate in the build manifest — must still surface that in the headline
183
+ # stack. Eureka wires JAX-RS through Jersey 1.x (com.sun.jersey) + jsr311-api
184
+ # + Guice, none of which the manifest-coordinate scan models, and its child
185
+ # build.gradle files are not scanned at all — yet it ships 18 @Path resource
186
+ # classes. Without this the headline read "Java library / package" while the
187
+ # endpoints command listed a full JAX-RS API. When the annotation scan found
188
+ # JAX-RS entry points and no Jakarta-EE framework was inferred from the
189
+ # manifest, surface it so the stack (and project_type) reflect the REST API
190
+ # the endpoints already prove. Structural (VAI): reasons from the JAX-RS
191
+ # entry points found in source, not from a vendor dependency-name allowlist.
192
+ # detected_via carries no path form, so it is treated as repo-wide evidence
193
+ # (never localized to an adapter submodule).
194
+ if not any(f.name == "Jakarta EE" for f in frameworks) and any(
195
+ ep.kind in ("jax_rs_controller", "jax_rs_provider") for ep in entry_points
196
+ ):
197
+ frameworks.append(FrameworkDetection(
198
+ name="Jakarta EE", source="annotation", detected_via=["jax_rs_endpoints"],
199
+ ))
200
+
179
201
  transactional_classes = self._collect_transactional_classes(context, all_paths)
180
202
  stack = StackDetection(
181
203
  stack="java",
@@ -459,7 +481,13 @@ class JavaDetector(AbstractDetector):
459
481
  # 1. @SpringBootApplication entry: Application.java / Main.java by name
460
482
  # Exclude test trees: test helpers like AdminApplication.java in
461
483
  # integration/src/test/java/ must not be treated as production entrypoints.
462
- from sourcecode.path_filters import is_test_path as _is_test_path
484
+ # B2 (openmrs field test): exclude not just src/test/ trees but whole
485
+ # test-harness MODULES whose sources sit under src/main — e.g. openmrs'
486
+ # `test-suite/module/omod/src/main/java/.../TestModuleController.java`, a
487
+ # scaffolding @Controller (`GET /module/testmodule/hello`) that a narrow
488
+ # src/test/ check misses, so it surfaced as a production entry point. The
489
+ # broader is_test_or_fixture_path catches test-suite / *-test / *-it modules.
490
+ from sourcecode.path_filters import is_test_or_fixture_path as _is_test_path
463
491
  # BUG #3 (Alfresco field test): a `*Application.java` / `*Main.java` NAME does
464
492
  # not make a file a bootstrap entry point. Alfresco has XSD-generated JAXB
465
493
  # `Application.java` model classes (no main(), no bootstrap annotation) in a
@@ -0,0 +1,42 @@
1
+ """Single source of truth for the endpoint / route-surface reconciliation note.
2
+
3
+ Three commands report an endpoint count, each from a different point in the
4
+ pipeline. Read side by side they look contradictory (e.g. 245 vs 225 vs 220 for
5
+ one repo) — they are not. Expected ordering for a single repo:
6
+
7
+ repo-ir.route_surface >= endpoints.total >= spring-audit.endpoints_analyzed
8
+
9
+ - ``repo-ir.route_surface`` is the RAW structural surface (every @RequestMapping /
10
+ @Path handler after inheritance projection and JAX-RS sub-resource-locator
11
+ composition). It still contains framework dynamic-admin routes whose path is a
12
+ Java FQN (e.g. ``/org.broadleafcommerce.core.search.domain.FieldImpl``), which
13
+ the ``endpoints`` command filters out — so it is NOT the canonical API surface.
14
+ - ``endpoints.total`` is the canonical REST/API surface (FQN-shaped framework
15
+ routes removed). This is the number to cite for "how many endpoints".
16
+ - ``spring-audit.endpoints_analyzed`` is that canonical surface further deduplicated
17
+ by (method, path, controller, handler) for security analysis.
18
+
19
+ The exact arithmetic between the three is intentionally NOT asserted (the pipelines
20
+ differ in dedup/expansion, so route_surface minus FQN-paths does not equal
21
+ endpoints.total). Only the robust ordering and the population of each are stated.
22
+
23
+ ``spring-audit`` already documents its own count inline; this note gives repo-ir's
24
+ raw ``route_surface`` the same self-description so no reader mistakes it for the
25
+ canonical endpoint total.
26
+ """
27
+
28
+ ENDPOINT_SURFACE_RECONCILIATION: str = (
29
+ "route_surface is the RAW structural route surface: every @RequestMapping/@Path "
30
+ "handler after inheritance projection and JAX-RS sub-resource-locator composition. "
31
+ "It is NOT the canonical API surface — it still contains framework dynamic-admin "
32
+ "routes whose path is a Java FQN (e.g. /org.broadleafcommerce...), which the "
33
+ "`endpoints` command excludes. Endpoint counts differ across commands BY DESIGN "
34
+ "and are not contradictory; expected ordering for one repo is "
35
+ "repo-ir.route_surface (raw, includes FQN-shaped framework routes) >= "
36
+ "endpoints.total (canonical REST/API surface, FQN-shaped routes filtered out) >= "
37
+ "spring-audit.metadata.endpoints_analyzed (canonical surface further deduplicated "
38
+ "by (method, path, controller, handler) for security analysis). Cite "
39
+ "endpoints.total for the real API surface; the pipelines differ in dedup and "
40
+ "prefix expansion, so the three counts are related by this ordering, not by exact "
41
+ "arithmetic."
42
+ )
@@ -17,6 +17,7 @@ from __future__ import annotations
17
17
  from dataclasses import dataclass, field
18
18
  from typing import TYPE_CHECKING, Optional
19
19
 
20
+ from sourcecode.caller_metrics import CALLER_METRIC_RECONCILIATION
20
21
  from sourcecode.fqn_utils import normalize_owner_fqn
21
22
 
22
23
  if TYPE_CHECKING:
@@ -121,6 +122,7 @@ class ClassExplanation:
121
122
  "purpose": self.purpose,
122
123
  "public_methods": self.public_methods,
123
124
  "incoming_callers": self.incoming_callers,
125
+ "incoming_callers_note": CALLER_METRIC_RECONCILIATION,
124
126
  "outgoing_deps": self.outgoing_deps,
125
127
  "events_published": self.events_published,
126
128
  "events_consumed": self.events_consumed,
@@ -16,7 +16,8 @@ _TEST_SEGMENTS = frozenset({
16
16
  # code lives under src/main (e.g. a Maven module that ships a test framework).
17
17
  # A finding under one of these modules is test infrastructure, not the product.
18
18
  _TEST_MODULE_SEGMENTS = frozenset({
19
- "testsuite", "test-framework", "testframework",
19
+ "testsuite", "test-suite", "test-suites",
20
+ "test-framework", "testframework",
20
21
  "integration-arquillian", "arquillian",
21
22
  "test-utils", "test-util", "testutils",
22
23
  "test-support", "testsupport",