sourcecode 2.5.12__tar.gz → 2.5.15__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 (160) hide show
  1. {sourcecode-2.5.12 → sourcecode-2.5.15}/CHANGELOG.md +134 -0
  2. {sourcecode-2.5.12 → sourcecode-2.5.15}/PKG-INFO +1 -1
  3. {sourcecode-2.5.12 → sourcecode-2.5.15}/pyproject.toml +1 -1
  4. {sourcecode-2.5.12 → sourcecode-2.5.15}/src/sourcecode/__init__.py +1 -1
  5. {sourcecode-2.5.12 → sourcecode-2.5.15}/src/sourcecode/canonical_ir.py +8 -0
  6. {sourcecode-2.5.12 → sourcecode-2.5.15}/src/sourcecode/classifier.py +80 -52
  7. {sourcecode-2.5.12 → sourcecode-2.5.15}/src/sourcecode/cli.py +7 -0
  8. {sourcecode-2.5.12 → sourcecode-2.5.15}/src/sourcecode/detectors/java.py +35 -1
  9. {sourcecode-2.5.12 → sourcecode-2.5.15}/src/sourcecode/endpoint_metrics.py +4 -1
  10. {sourcecode-2.5.12 → sourcecode-2.5.15}/src/sourcecode/path_filters.py +79 -0
  11. {sourcecode-2.5.12 → sourcecode-2.5.15}/src/sourcecode/repository_ir.py +59 -2
  12. {sourcecode-2.5.12 → sourcecode-2.5.15}/src/sourcecode/serializer.py +35 -2
  13. {sourcecode-2.5.12 → sourcecode-2.5.15}/src/sourcecode/summarizer.py +7 -17
  14. {sourcecode-2.5.12 → sourcecode-2.5.15}/.github/workflows/build-windows.yml +0 -0
  15. {sourcecode-2.5.12 → sourcecode-2.5.15}/.gitignore +0 -0
  16. {sourcecode-2.5.12 → sourcecode-2.5.15}/.ruff.toml +0 -0
  17. {sourcecode-2.5.12 → sourcecode-2.5.15}/CONTRIBUTING.md +0 -0
  18. {sourcecode-2.5.12 → sourcecode-2.5.15}/LICENSE +0 -0
  19. {sourcecode-2.5.12 → sourcecode-2.5.15}/README.md +0 -0
  20. {sourcecode-2.5.12 → sourcecode-2.5.15}/SECURITY.md +0 -0
  21. {sourcecode-2.5.12 → sourcecode-2.5.15}/raw +0 -0
  22. {sourcecode-2.5.12 → sourcecode-2.5.15}/scripts/compare_integration_engines.py +0 -0
  23. {sourcecode-2.5.12 → sourcecode-2.5.15}/scripts/customer_smoke_test.sh +0 -0
  24. {sourcecode-2.5.12 → sourcecode-2.5.15}/scripts/generate_jdk_exports.py +0 -0
  25. {sourcecode-2.5.12 → sourcecode-2.5.15}/src/sourcecode/adaptive_scanner.py +0 -0
  26. {sourcecode-2.5.12 → sourcecode-2.5.15}/src/sourcecode/archetype.py +0 -0
  27. {sourcecode-2.5.12 → sourcecode-2.5.15}/src/sourcecode/architecture_analyzer.py +0 -0
  28. {sourcecode-2.5.12 → sourcecode-2.5.15}/src/sourcecode/architecture_summary.py +0 -0
  29. {sourcecode-2.5.12 → sourcecode-2.5.15}/src/sourcecode/ast_extractor.py +0 -0
  30. {sourcecode-2.5.12 → sourcecode-2.5.15}/src/sourcecode/cache.py +0 -0
  31. {sourcecode-2.5.12 → sourcecode-2.5.15}/src/sourcecode/call_surface.py +0 -0
  32. {sourcecode-2.5.12 → sourcecode-2.5.15}/src/sourcecode/caller_metrics.py +0 -0
  33. {sourcecode-2.5.12 → sourcecode-2.5.15}/src/sourcecode/cir_graphs.py +0 -0
  34. {sourcecode-2.5.12 → sourcecode-2.5.15}/src/sourcecode/code_notes_analyzer.py +0 -0
  35. {sourcecode-2.5.12 → sourcecode-2.5.15}/src/sourcecode/confidence_analyzer.py +0 -0
  36. {sourcecode-2.5.12 → sourcecode-2.5.15}/src/sourcecode/context_cache.py +0 -0
  37. {sourcecode-2.5.12 → sourcecode-2.5.15}/src/sourcecode/context_graph.py +0 -0
  38. {sourcecode-2.5.12 → sourcecode-2.5.15}/src/sourcecode/context_scorer.py +0 -0
  39. {sourcecode-2.5.12 → sourcecode-2.5.15}/src/sourcecode/context_summarizer.py +0 -0
  40. {sourcecode-2.5.12 → sourcecode-2.5.15}/src/sourcecode/contract_model.py +0 -0
  41. {sourcecode-2.5.12 → sourcecode-2.5.15}/src/sourcecode/contract_pipeline.py +0 -0
  42. {sourcecode-2.5.12 → sourcecode-2.5.15}/src/sourcecode/coverage_parser.py +0 -0
  43. {sourcecode-2.5.12 → sourcecode-2.5.15}/src/sourcecode/dependency_analyzer.py +0 -0
  44. {sourcecode-2.5.12 → sourcecode-2.5.15}/src/sourcecode/detectors/__init__.py +0 -0
  45. {sourcecode-2.5.12 → sourcecode-2.5.15}/src/sourcecode/detectors/base.py +0 -0
  46. {sourcecode-2.5.12 → sourcecode-2.5.15}/src/sourcecode/detectors/csproj_parser.py +0 -0
  47. {sourcecode-2.5.12 → sourcecode-2.5.15}/src/sourcecode/detectors/dart.py +0 -0
  48. {sourcecode-2.5.12 → sourcecode-2.5.15}/src/sourcecode/detectors/dotnet.py +0 -0
  49. {sourcecode-2.5.12 → sourcecode-2.5.15}/src/sourcecode/detectors/elixir.py +0 -0
  50. {sourcecode-2.5.12 → sourcecode-2.5.15}/src/sourcecode/detectors/go.py +0 -0
  51. {sourcecode-2.5.12 → sourcecode-2.5.15}/src/sourcecode/detectors/heuristic.py +0 -0
  52. {sourcecode-2.5.12 → sourcecode-2.5.15}/src/sourcecode/detectors/hybrid.py +0 -0
  53. {sourcecode-2.5.12 → sourcecode-2.5.15}/src/sourcecode/detectors/jvm_ext.py +0 -0
  54. {sourcecode-2.5.12 → sourcecode-2.5.15}/src/sourcecode/detectors/nodejs.py +0 -0
  55. {sourcecode-2.5.12 → sourcecode-2.5.15}/src/sourcecode/detectors/parsers.py +0 -0
  56. {sourcecode-2.5.12 → sourcecode-2.5.15}/src/sourcecode/detectors/php.py +0 -0
  57. {sourcecode-2.5.12 → sourcecode-2.5.15}/src/sourcecode/detectors/project.py +0 -0
  58. {sourcecode-2.5.12 → sourcecode-2.5.15}/src/sourcecode/detectors/python.py +0 -0
  59. {sourcecode-2.5.12 → sourcecode-2.5.15}/src/sourcecode/detectors/ruby.py +0 -0
  60. {sourcecode-2.5.12 → sourcecode-2.5.15}/src/sourcecode/detectors/rust.py +0 -0
  61. {sourcecode-2.5.12 → sourcecode-2.5.15}/src/sourcecode/detectors/systems.py +0 -0
  62. {sourcecode-2.5.12 → sourcecode-2.5.15}/src/sourcecode/detectors/terraform.py +0 -0
  63. {sourcecode-2.5.12 → sourcecode-2.5.15}/src/sourcecode/detectors/tooling.py +0 -0
  64. {sourcecode-2.5.12 → sourcecode-2.5.15}/src/sourcecode/doc_analyzer.py +0 -0
  65. {sourcecode-2.5.12 → sourcecode-2.5.15}/src/sourcecode/dynamic_argument_surface.py +0 -0
  66. {sourcecode-2.5.12 → sourcecode-2.5.15}/src/sourcecode/endpoint_literals.py +0 -0
  67. {sourcecode-2.5.12 → sourcecode-2.5.15}/src/sourcecode/entrypoint_classifier.py +0 -0
  68. {sourcecode-2.5.12 → sourcecode-2.5.15}/src/sourcecode/env_analyzer.py +0 -0
  69. {sourcecode-2.5.12 → sourcecode-2.5.15}/src/sourcecode/error_schema.py +0 -0
  70. {sourcecode-2.5.12 → sourcecode-2.5.15}/src/sourcecode/evidence_provider.py +0 -0
  71. {sourcecode-2.5.12 → sourcecode-2.5.15}/src/sourcecode/explain.py +0 -0
  72. {sourcecode-2.5.12 → sourcecode-2.5.15}/src/sourcecode/file_chunker.py +0 -0
  73. {sourcecode-2.5.12 → sourcecode-2.5.15}/src/sourcecode/file_classifier.py +0 -0
  74. {sourcecode-2.5.12 → sourcecode-2.5.15}/src/sourcecode/format_contract.py +0 -0
  75. {sourcecode-2.5.12 → sourcecode-2.5.15}/src/sourcecode/fqn_utils.py +0 -0
  76. {sourcecode-2.5.12 → sourcecode-2.5.15}/src/sourcecode/git_analyzer.py +0 -0
  77. {sourcecode-2.5.12 → sourcecode-2.5.15}/src/sourcecode/graph_analyzer.py +0 -0
  78. {sourcecode-2.5.12 → sourcecode-2.5.15}/src/sourcecode/graph_evidence.py +0 -0
  79. {sourcecode-2.5.12 → sourcecode-2.5.15}/src/sourcecode/hibernate_strat.py +0 -0
  80. {sourcecode-2.5.12 → sourcecode-2.5.15}/src/sourcecode/jdk_exports.py +0 -0
  81. {sourcecode-2.5.12 → sourcecode-2.5.15}/src/sourcecode/license.py +0 -0
  82. {sourcecode-2.5.12 → sourcecode-2.5.15}/src/sourcecode/mcp/__init__.py +0 -0
  83. {sourcecode-2.5.12 → sourcecode-2.5.15}/src/sourcecode/mcp/onboarding/__init__.py +0 -0
  84. {sourcecode-2.5.12 → sourcecode-2.5.15}/src/sourcecode/mcp/onboarding/applier.py +0 -0
  85. {sourcecode-2.5.12 → sourcecode-2.5.15}/src/sourcecode/mcp/onboarding/backup.py +0 -0
  86. {sourcecode-2.5.12 → sourcecode-2.5.15}/src/sourcecode/mcp/onboarding/detector.py +0 -0
  87. {sourcecode-2.5.12 → sourcecode-2.5.15}/src/sourcecode/mcp/onboarding/planner.py +0 -0
  88. {sourcecode-2.5.12 → sourcecode-2.5.15}/src/sourcecode/mcp/orchestrator.py +0 -0
  89. {sourcecode-2.5.12 → sourcecode-2.5.15}/src/sourcecode/mcp/registry.py +0 -0
  90. {sourcecode-2.5.12 → sourcecode-2.5.15}/src/sourcecode/mcp/runner.py +0 -0
  91. {sourcecode-2.5.12 → sourcecode-2.5.15}/src/sourcecode/mcp/server.py +0 -0
  92. {sourcecode-2.5.12 → sourcecode-2.5.15}/src/sourcecode/mcp_nudge.py +0 -0
  93. {sourcecode-2.5.12 → sourcecode-2.5.15}/src/sourcecode/metrics_analyzer.py +0 -0
  94. {sourcecode-2.5.12 → sourcecode-2.5.15}/src/sourcecode/migrate_check.py +0 -0
  95. {sourcecode-2.5.12 → sourcecode-2.5.15}/src/sourcecode/openapi_surface.py +0 -0
  96. {sourcecode-2.5.12 → sourcecode-2.5.15}/src/sourcecode/output_budget.py +0 -0
  97. {sourcecode-2.5.12 → sourcecode-2.5.15}/src/sourcecode/pr_comment_renderer.py +0 -0
  98. {sourcecode-2.5.12 → sourcecode-2.5.15}/src/sourcecode/pr_impact.py +0 -0
  99. {sourcecode-2.5.12 → sourcecode-2.5.15}/src/sourcecode/prepare_context.py +0 -0
  100. {sourcecode-2.5.12 → sourcecode-2.5.15}/src/sourcecode/progress.py +0 -0
  101. {sourcecode-2.5.12 → sourcecode-2.5.15}/src/sourcecode/ranking_engine.py +0 -0
  102. {sourcecode-2.5.12 → sourcecode-2.5.15}/src/sourcecode/reconciliation.py +0 -0
  103. {sourcecode-2.5.12 → sourcecode-2.5.15}/src/sourcecode/redactor.py +0 -0
  104. {sourcecode-2.5.12 → sourcecode-2.5.15}/src/sourcecode/relevance_scorer.py +0 -0
  105. {sourcecode-2.5.12 → sourcecode-2.5.15}/src/sourcecode/rename_refactor.py +0 -0
  106. {sourcecode-2.5.12 → sourcecode-2.5.15}/src/sourcecode/repo_classifier.py +0 -0
  107. {sourcecode-2.5.12 → sourcecode-2.5.15}/src/sourcecode/retrieval/__init__.py +0 -0
  108. {sourcecode-2.5.12 → sourcecode-2.5.15}/src/sourcecode/retrieval/context.py +0 -0
  109. {sourcecode-2.5.12 → sourcecode-2.5.15}/src/sourcecode/retrieval/errors.py +0 -0
  110. {sourcecode-2.5.12 → sourcecode-2.5.15}/src/sourcecode/retrieval/executor.py +0 -0
  111. {sourcecode-2.5.12 → sourcecode-2.5.15}/src/sourcecode/retrieval/planner.py +0 -0
  112. {sourcecode-2.5.12 → sourcecode-2.5.15}/src/sourcecode/retrieval/query.py +0 -0
  113. {sourcecode-2.5.12 → sourcecode-2.5.15}/src/sourcecode/retrieval/request.py +0 -0
  114. {sourcecode-2.5.12 → sourcecode-2.5.15}/src/sourcecode/retrieval/resolution.py +0 -0
  115. {sourcecode-2.5.12 → sourcecode-2.5.15}/src/sourcecode/retrieval/result.py +0 -0
  116. {sourcecode-2.5.12 → sourcecode-2.5.15}/src/sourcecode/retrieval/retriever.py +0 -0
  117. {sourcecode-2.5.12 → sourcecode-2.5.15}/src/sourcecode/retrieval/runtime.py +0 -0
  118. {sourcecode-2.5.12 → sourcecode-2.5.15}/src/sourcecode/retrieval/steps.py +0 -0
  119. {sourcecode-2.5.12 → sourcecode-2.5.15}/src/sourcecode/retrieval/steps_endpoint.py +0 -0
  120. {sourcecode-2.5.12 → sourcecode-2.5.15}/src/sourcecode/retrieval/steps_graph.py +0 -0
  121. {sourcecode-2.5.12 → sourcecode-2.5.15}/src/sourcecode/retrieval/steps_impact.py +0 -0
  122. {sourcecode-2.5.12 → sourcecode-2.5.15}/src/sourcecode/retrieval/steps_intf.py +0 -0
  123. {sourcecode-2.5.12 → sourcecode-2.5.15}/src/sourcecode/retrieval/steps_struct.py +0 -0
  124. {sourcecode-2.5.12 → sourcecode-2.5.15}/src/sourcecode/retrieval/steps_txsec.py +0 -0
  125. {sourcecode-2.5.12 → sourcecode-2.5.15}/src/sourcecode/ris.py +0 -0
  126. {sourcecode-2.5.12 → sourcecode-2.5.15}/src/sourcecode/runtime_classifier.py +0 -0
  127. {sourcecode-2.5.12 → sourcecode-2.5.15}/src/sourcecode/scanner.py +0 -0
  128. {sourcecode-2.5.12 → sourcecode-2.5.15}/src/sourcecode/schema.py +0 -0
  129. {sourcecode-2.5.12 → sourcecode-2.5.15}/src/sourcecode/security_config.py +0 -0
  130. {sourcecode-2.5.12 → sourcecode-2.5.15}/src/sourcecode/security_posture.py +0 -0
  131. {sourcecode-2.5.12 → sourcecode-2.5.15}/src/sourcecode/semantic_analyzer.py +0 -0
  132. {sourcecode-2.5.12 → sourcecode-2.5.15}/src/sourcecode/semantic_impact_engine.py +0 -0
  133. {sourcecode-2.5.12 → sourcecode-2.5.15}/src/sourcecode/semantic_integration_engine.py +0 -0
  134. {sourcecode-2.5.12 → sourcecode-2.5.15}/src/sourcecode/semantic_services.py +0 -0
  135. {sourcecode-2.5.12 → sourcecode-2.5.15}/src/sourcecode/spring_event_topology.py +0 -0
  136. {sourcecode-2.5.12 → sourcecode-2.5.15}/src/sourcecode/spring_findings.py +0 -0
  137. {sourcecode-2.5.12 → sourcecode-2.5.15}/src/sourcecode/spring_impact.py +0 -0
  138. {sourcecode-2.5.12 → sourcecode-2.5.15}/src/sourcecode/spring_model.py +0 -0
  139. {sourcecode-2.5.12 → sourcecode-2.5.15}/src/sourcecode/spring_security_audit.py +0 -0
  140. {sourcecode-2.5.12 → sourcecode-2.5.15}/src/sourcecode/spring_semantic.py +0 -0
  141. {sourcecode-2.5.12 → sourcecode-2.5.15}/src/sourcecode/spring_tx_analyzer.py +0 -0
  142. {sourcecode-2.5.12 → sourcecode-2.5.15}/src/sourcecode/telemetry/__init__.py +0 -0
  143. {sourcecode-2.5.12 → sourcecode-2.5.15}/src/sourcecode/telemetry/config.py +0 -0
  144. {sourcecode-2.5.12 → sourcecode-2.5.15}/src/sourcecode/telemetry/consent.py +0 -0
  145. {sourcecode-2.5.12 → sourcecode-2.5.15}/src/sourcecode/telemetry/events.py +0 -0
  146. {sourcecode-2.5.12 → sourcecode-2.5.15}/src/sourcecode/telemetry/filters.py +0 -0
  147. {sourcecode-2.5.12 → sourcecode-2.5.15}/src/sourcecode/telemetry/transport.py +0 -0
  148. {sourcecode-2.5.12 → sourcecode-2.5.15}/src/sourcecode/tree_utils.py +0 -0
  149. {sourcecode-2.5.12 → sourcecode-2.5.15}/src/sourcecode/type_usage_surface.py +0 -0
  150. {sourcecode-2.5.12 → sourcecode-2.5.15}/src/sourcecode/validation_inference.py +0 -0
  151. {sourcecode-2.5.12 → sourcecode-2.5.15}/src/sourcecode/validation_surface.py +0 -0
  152. {sourcecode-2.5.12 → sourcecode-2.5.15}/src/sourcecode/version_check.py +0 -0
  153. {sourcecode-2.5.12 → sourcecode-2.5.15}/src/sourcecode/workspace.py +0 -0
  154. {sourcecode-2.5.12 → sourcecode-2.5.15}/supabase/functions/README.md +0 -0
  155. {sourcecode-2.5.12 → sourcecode-2.5.15}/supabase/functions/get-license/index.ts +0 -0
  156. {sourcecode-2.5.12 → sourcecode-2.5.15}/supabase/functions/lemonsqueezy-webhook/index.ts +0 -0
  157. {sourcecode-2.5.12 → sourcecode-2.5.15}/supabase/functions/telemetry/index.ts +0 -0
  158. {sourcecode-2.5.12 → sourcecode-2.5.15}/supabase/sql/license_event_ordering.sql +0 -0
  159. {sourcecode-2.5.12 → sourcecode-2.5.15}/supabase/sql/licensing_schema.sql +0 -0
  160. {sourcecode-2.5.12 → sourcecode-2.5.15}/supabase/sql/telemetry_events.sql +0 -0
@@ -1,5 +1,139 @@
1
1
  # Changelog
2
2
 
3
+ ## [2.5.15] — 2026-07-19
4
+
5
+ **Bootstrap detection recovers the real entry point on multi-`main` repos, without
6
+ noise.** Post-F1 field validation (neo4j) found the compact `bootstrap` was name-gated
7
+ to `*Application.java` / `*Main.java`: neo4j's real server entry `Neo4jCommunity.java`
8
+ (a `main()` in a differently-named class) was invisible, so a client CLI `Main.java` was
9
+ reported as the sole bootstrap and the repo's 31 `main()` methods were hidden.
10
+
11
+ ### Fixed
12
+
13
+ - **`main()`-bearing classes are recovered regardless of filename, comment-aware, and
14
+ the bootstrap list is ordered/capped/counted** (`detectors/java.py`, `serializer.py`,
15
+ `path_filters.py`): the annotation scan (which already reads each candidate file) now
16
+ also emits an `application` entry when a file declares a real `main(String[])` or a
17
+ bootstrap annotation — after stripping comments, so a `main()` / `@SpringBootApplication`
18
+ shown in a **javadoc example** is never mistaken for an entry (fixed Broadleaf
19
+ `*AutoConfiguration` false positives). `_bootstrap_structured` then: excludes entries
20
+ classified `auxiliary`/`development`; excludes mains in **example/demo modules**
21
+ (new structural `is_example_module_path`, e.g. `eureka-examples/`) and **test-fixture
22
+ modules** (`is_test_fixture_module_path`, e.g. `eureka-test-utils/`) — filtered here,
23
+ not in the detector, because workspace sub-scans prefix the module name onto the path
24
+ only after detection; orders deployment-annotated entries first; caps the list at 10;
25
+ and adds a `bootstrap_note` reporting the true count when more than one entry exists.
26
+ Measured (fleet): neo4j surfaces `Neo4jCommunity.java` with note "11 application entry
27
+ points detected; showing 10"; Broadleaf false positives 7 → 0; eureka's example/
28
+ test-util mains dropped (5 → 1); petclinic / spaghetti / openmrs unchanged (single
29
+ bootstrap). Full-output A/B confirms the change is confined to the entry-point surface —
30
+ no drift in stacks, architecture, or confidence. Residual real tool/CLI mains in library
31
+ packages (e.g. jobrunr's `DatabaseCreator`) are reported honestly with the count, as they
32
+ cannot be distinguished from deployment mains without name heuristics. Suite: 3521 passed.
33
+
34
+ ## [2.5.14] — 2026-07-18
35
+
36
+ **One authority decides the primary stack; every surface transports its choice.**
37
+ Post-F1 field validation found a polyglot repo (Confluent `examples`: 1 C# file vs 86
38
+ Java files) labelled `.NET` as primary. Root cause was not one coin flip but several:
39
+ `primary` was chosen by `max()` over `(priority, confidence, manifest)` — which **ties**
40
+ for two manifest-detected API stacks, so **list order** decided — and then re-derived
41
+ independently in more than one place (the classifier marked `stacks[].primary`; the
42
+ summarizer re-selected for the `project_summary` prose; the serializer / architecture
43
+ summary invented a `stacks[0]` fallback when the flag was absent). These agreed only by
44
+ sharing the same arbitrary order, so they could — and did — disagree once the input
45
+ changed.
46
+
47
+ ### Fixed
48
+
49
+ - **Single primary-stack authority with an evidence-weighted tiebreaker**
50
+ (`classifier.py`, `cli.py`, `summarizer.py`): a new `select_primary_stack(stacks,
51
+ project_type, file_paths)` is the ONE decider. It keeps the existing
52
+ `(priority, confidence, manifest)` ordering and adds **source-file volume as the last
53
+ tuple element** — a pure tiebreaker that decides only when the prior three tie, so a
54
+ 1-file .NET example never outranks 86 Java files, and stacks that already differ are
55
+ never reordered. It marks exactly one instance `primary=True` (all others `False`) over
56
+ the final assembled root+workspace set. `TypeClassifier.enrich` now delegates to it (the
57
+ old `_select_primary_stack` name-returning helper is removed), and the main compact path
58
+ re-runs it after workspace stacks are appended. The summarizer's independent
59
+ `_select_summary_primary_stack` is deleted — the prose now reads the same `stack.primary`
60
+ flag as `architecture_summary`, `stacks[].primary` and `confidence_analyzer`. The
61
+ extension table shared by confidence enrichment and the tiebreaker was hoisted to one
62
+ constant (`_STACK_EXTENSIONS`) so the two never diverge. Fleet A/B (12 repos): only
63
+ `examples` changes (primary `dotnet`→`java`; architecture `C#/.NET`→`Java … JAX-RS`;
64
+ summary `Stack: Dotnet`→`Stack: Java … 80 Java classes`; the previously-emitted false
65
+ "polyglot / two programs" anomaly correctly disappears because Java genuinely attests the
66
+ API surface). All 11 other repos are byte-identical. Suite: 3508 passed, 5 skipped.
67
+
68
+ ## [2.5.13] — 2026-07-18
69
+
70
+ **"Production endpoint" is now a single definition across every user-facing surface.**
71
+ Post-F1 field validation (neo4j, keycloak) found that a JAX-RS / Spring resource shipping
72
+ under `src/main` of a test-utility module — a dummy web service, an Arquillian test-harness
73
+ resource — was reported as production API surface, and that different commands disagreed on
74
+ what counted: `endpoints` / `validation` derived their view one way while `impact`,
75
+ `impact-chain`, `pr-impact`, `review-pr` and `spring-audit` derived theirs from the raw
76
+ `route_surface`, so the same repo had two definitions of "exposed endpoint" depending on the
77
+ command. This release unifies them: every user-facing surface now consumes the same
78
+ production-endpoint view (test scaffolding excluded), while `route_surface` remains the RAW
79
+ structural evidence by design — full and unfiltered — consumed only by `repo-ir`. The
80
+ under-modelling that motivated this (test fixtures counted as HTTP exposure, inflating
81
+ endpoint counts, blast radius, risk scores and security analysis) was the one defect the
82
+ post-F1 review classified as blocking.
83
+
84
+ ### Fixed
85
+
86
+ - **Endpoints from test-fixture / test-utility modules are excluded from the production
87
+ surface** (`repository_ir.py`, `path_filters.py`): a new structural, name-agnostic
88
+ helper `is_test_fixture_module_path` flags a module directory segment that carries a
89
+ delimited `test` token *together* with an infrastructure word (`util(s)`, `fixture(s)`,
90
+ `support`, `framework`, `kit`, `harness`, `tools`, `helper(s)`, `mock(s)`, `stub(s)`,
91
+ `dummy`) — e.g. `server-test-utils`, `foo-testkit`, `bar-test-support` — or a known
92
+ test-module segment (`testsuite`, `integration-arquillian`, `test-framework`). Requiring
93
+ both tokens in the *same* segment keeps legitimate `utils` / `support` / `tools` packages
94
+ from ever matching. `extract_java_endpoints` traces each endpoint back to its declaring
95
+ module and, when it is a test fixture, drops it from `endpoints` / `total` /
96
+ `no_security_signal` and the security-model heuristics. The exclusion is **transparent,
97
+ not silent**: excluded entries are preserved in a `test_util_endpoints` bucket with a
98
+ `scope: "test_util"` tag, a `test_util_excluded` count, and an explanatory `notes` entry.
99
+ Measured: neo4j 18 → 14 production (4 dummy `DummyThirdPartyWebService` endpoints moved to
100
+ the bucket); keycloak 775 → 673 production (102 Arquillian/test-framework harness
101
+ endpoints excluded). Control repos unchanged (spaghetti-api 7, petclinic 17, eureka 18,
102
+ Broadleaf 225). `is_test_or_fixture_path` and the migrate-check blocking count it feeds are
103
+ deliberately untouched — the new helper is narrower and wired only into endpoint
104
+ extraction.
105
+
106
+ - **One production-endpoint definition across every CIR consumer** (`repository_ir.py`,
107
+ `canonical_ir.py`, `endpoint_metrics.py`): the fix above only reached the `endpoints` /
108
+ `validation` surface; `impact-chain`, `pr-impact`, `review-pr` and `spring-audit` derive
109
+ their HTTP surface from the canonical `cir.endpoints`, which is built from the RAW
110
+ `route_surface` and did **not** inherit the test-fixture exclusion — so those commands
111
+ still counted test scaffolding as production exposure (e.g. keycloak's 102 Arquillian
112
+ endpoints inflating blast radius / risk score / SEC analysis). `build_repo_ir` now tags
113
+ test-fixture routes with `scope="test_util"` on `route_surface`, and `ir_dict_to_canonical`
114
+ excludes tagged routes when building `cir.endpoints` — the same place the pre-existing
115
+ FQN dynamic-admin filter already reconciles the CIR surface with the `endpoints` command.
116
+ `route_surface` itself is left **raw and complete** (the tag is additive) so `repo-ir`
117
+ keeps full evidence; the divergence is documented in `ENDPOINT_SURFACE_RECONCILIATION`.
118
+ No heuristic, scoring, risk or reconciliation logic was changed — the four consumers change
119
+ only because they now receive a coherent `cir.endpoints`. Measured: keycloak
120
+ `cir.endpoints` / `spring-audit.endpoints_analyzed` 775 → 673 (route_surface stays 775);
121
+ neo4j 18 → 14 (route_surface stays 18); Broadleaf/petclinic/spaghetti/eureka unchanged
122
+ (0 test_util routes). Zero test-fixture controllers leak into `cir.endpoints` on any repo.
123
+
124
+ - **The legacy `impact` command now shares the same production-endpoint definition**
125
+ (`repository_ir.py`): `impact` reads `route_surface` directly via `compute_blast_radius`
126
+ rather than through `cir.endpoints`, so it was the last user surface still counting test
127
+ fixtures as HTTP exposure. `compute_blast_radius` now skips `scope=="test_util"` routes when
128
+ building `endpoints_affected`. The change is a single guard at the consumption point: the
129
+ blast-radius algorithm, caller selection, risk formula, security surface and the FQN-admin
130
+ handling are all unchanged — the endpoint list simply no longer includes test scaffolding.
131
+ Validated by A/B (stash) diff: `impact TestingOIDCEndpointsApplicationResource` on keycloak
132
+ went from 28 endpoints_affected (all test) / risk 87 to 0 / risk 3 with `direct_callers`
133
+ unchanged (1 → 1); `impact CatalogServiceImpl` on Broadleaf is **byte-identical** before and
134
+ after (54 endpoints, 5 FQN-shaped paths preserved, 30 callers). "Production endpoint" is now
135
+ a single definition across every user-facing command.
136
+
3
137
  ## [2.5.12] — 2026-07-18
4
138
 
5
139
  **F-1 (the Evidence Reconciliation Layer) is closed and its rule core frozen.** This release
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: sourcecode
3
- Version: 2.5.12
3
+ Version: 2.5.15
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.5.12"
7
+ version = "2.5.15"
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.12"
7
+ __version__ = "2.5.15"
@@ -398,9 +398,17 @@ def ir_dict_to_canonical(
398
398
  # real REST surface. The CIR endpoint list feeds `spring-audit` (endpoints_analyzed),
399
399
  # `impact` and `validation`; it must apply the SAME filter so its count reconciles
400
400
  # with `endpoints` instead of being silently inflated by those pseudo-routes.
401
+ # Endpoints declared in a test-fixture / test-utility module are tagged
402
+ # scope="test_util" on route_surface by build_repo_ir. They are excluded from the
403
+ # canonical endpoint list for the same reason as the FQN filter above: the CIR
404
+ # surface feeds impact-chain, pr-impact, review-pr and spring-audit, and test
405
+ # scaffolding is not production HTTP exposure. route_surface itself keeps them
406
+ # (raw evidence); only this canonical projection drops them.
401
407
  _seen_ids: set[str] = set()
402
408
  raw_endpoints: list[CanonicalEndpoint] = []
403
409
  for r in route_surface:
410
+ if r.get("scope") == "test_util":
411
+ continue
404
412
  if _FQN_PATH_RE.search(r.get("path", "") or ""):
405
413
  continue
406
414
  ep = _route_to_canonical_endpoint(r)
@@ -37,6 +37,26 @@ _WEB_FRAMEWORKS = {"Next.js", "React", "Vue", "Svelte", "Vite", "Flutter", "Phoe
37
37
  _CLI_FRAMEWORKS = {"Typer", "Cobra", "Clap"}
38
38
  _API_STACKS = {"python", "go", "java", "php", "ruby", "dotnet", "kotlin", "scala"}
39
39
 
40
+ # Source-file extensions per stack — single table shared by the confidence
41
+ # enrichment (_count_extension_hits) and the primary-stack evidence tiebreaker
42
+ # (select_primary_stack). Kept in one place so the two never diverge.
43
+ _STACK_EXTENSIONS: dict[str, tuple[str, ...]] = {
44
+ "python": (".py",),
45
+ "nodejs": (".js", ".ts", ".tsx"),
46
+ "go": (".go",),
47
+ "rust": (".rs",),
48
+ "java": (".java",),
49
+ "php": (".php",),
50
+ "ruby": (".rb",),
51
+ "dart": (".dart",),
52
+ "dotnet": (".cs", ".fs"),
53
+ "elixir": (".ex", ".exs"),
54
+ "kotlin": (".kt", ".kts"),
55
+ "scala": (".scala",),
56
+ "terraform": (".tf",),
57
+ "cpp": (".c", ".cc", ".cpp", ".cxx", ".hpp", ".h"),
58
+ }
59
+
40
60
  # Center-of-gravity gate for the "api" decision (neo4j field test).
41
61
  # The historical rule was pure PRESENCE: any API framework on the classpath →
42
62
  # project_type="api". That mislabels a large engine/platform that merely exposes a
@@ -63,6 +83,59 @@ _API_SURFACE_ENTRY_KINDS = frozenset({
63
83
  ConfidenceLevel = Literal["high", "medium", "low"]
64
84
 
65
85
 
86
+ def _primary_ext_hits(stack_name: str, file_paths: Sequence[str]) -> int:
87
+ exts = _STACK_EXTENSIONS.get(stack_name, ())
88
+ if not exts:
89
+ return 0
90
+ return sum(1 for p in file_paths if p.endswith(exts))
91
+
92
+
93
+ def select_primary_stack(
94
+ stacks: Sequence[StackDetection],
95
+ project_type: str | None,
96
+ file_paths: Sequence[str] | None = None,
97
+ ) -> list[StackDetection]:
98
+ """Single authority for which stack is primary.
99
+
100
+ Marks exactly ONE stack instance ``primary=True`` (all others ``False``) on the
101
+ FINAL assembled stack set (root + workspace stacks). Selection order:
102
+ ``(priority, confidence, manifest, source-file volume)``. The volume component is
103
+ a pure tiebreaker — it decides only when the prior three tie (e.g. a polyglot repo
104
+ where a Java and a .NET stack are both manifest-detected API stacks), so a 1-file
105
+ .NET example never outranks 86 Java files. Being last, it never reorders stacks
106
+ that already differ on the earlier components.
107
+
108
+ This is the ONLY place a primary decision is made. Every consumer must read
109
+ ``stack.primary`` — none may re-select or invent a ``stacks[0]`` fallback.
110
+ """
111
+ stack_list = list(stacks)
112
+ if not stack_list:
113
+ return stack_list
114
+ paths = list(file_paths or [])
115
+
116
+ def sort_key(stack: StackDetection) -> tuple[int, int, int, int]:
117
+ score = {"low": 0, "medium": 1, "high": 2}.get(stack.confidence, 0)
118
+ manifest_weight = 1 if stack.manifests else 0
119
+ priority = 0
120
+ # Backend server-side stacks with manifest evidence outrank frontend stacks
121
+ # in fullstack projects (e.g. Java+Spring wins over nodejs+Angular).
122
+ if (project_type in {"fullstack", "api"}
123
+ and stack.stack in _API_STACKS
124
+ and stack.manifests):
125
+ priority = 4
126
+ elif (project_type in {"webapp", "fullstack"} and stack.stack in {"nodejs", "elixir"}
127
+ or project_type == "api" and stack.stack in _API_STACKS
128
+ or project_type == "cli" and any(
129
+ framework.name in _CLI_FRAMEWORKS for framework in stack.frameworks
130
+ )
131
+ or project_type == "cli" and stack.stack in {"cpp", "dotnet"}):
132
+ priority = 3
133
+ return (priority, score, manifest_weight, _primary_ext_hits(stack.stack, paths))
134
+
135
+ winner = max(stack_list, key=sort_key)
136
+ return [replace(s, primary=(s is winner)) for s in stack_list]
137
+
138
+
66
139
  class TypeClassifier:
67
140
  """Clasifica project_type y enriquece stacks con confianza/primary."""
68
141
 
@@ -74,15 +147,12 @@ class TypeClassifier:
74
147
  ) -> tuple[list[StackDetection], str | None]:
75
148
  enriched = [self._enrich_stack(file_tree, stack, entry_points) for stack in stacks]
76
149
  project_type = self._classify_project_type(file_tree, enriched, entry_points)
77
- primary_stack = self._select_primary_stack(enriched, project_type)
78
-
79
- final_stacks: list[StackDetection] = []
80
- primary_assigned = False
81
- for stack in enriched:
82
- is_primary = stack.stack == primary_stack and not primary_assigned
83
- if is_primary:
84
- primary_assigned = True
85
- final_stacks.append(replace(stack, primary=is_primary))
150
+ # Delegate to the single primary-stack authority. enrich only ever sees the
151
+ # ROOT stacks; the authority runs again at model finalization over the full
152
+ # root+workspace set (that call is the one consumers read).
153
+ final_stacks = select_primary_stack(
154
+ enriched, project_type, flatten_file_tree(file_tree)
155
+ )
86
156
  return final_stacks, project_type
87
157
 
88
158
  def _enrich_stack(
@@ -371,50 +441,8 @@ class TypeClassifier:
371
441
  has_api = True
372
442
  return has_web and has_api
373
443
 
374
- def _select_primary_stack(
375
- self, stacks: Sequence[StackDetection], project_type: str | None
376
- ) -> str | None:
377
- if not stacks:
378
- return None
379
-
380
- def sort_key(stack: StackDetection) -> tuple[int, int, int]:
381
- score = {"low": 0, "medium": 1, "high": 2}.get(stack.confidence, 0)
382
- manifest_weight = 1 if stack.manifests else 0
383
- priority = 0
384
- # Backend server-side stacks with manifest evidence outrank frontend stacks
385
- # in fullstack projects (e.g. Java+Spring wins over nodejs+Angular).
386
- if (project_type in {"fullstack", "api"}
387
- and stack.stack in _API_STACKS
388
- and stack.manifests):
389
- priority = 4
390
- elif (project_type in {"webapp", "fullstack"} and stack.stack in {"nodejs", "elixir"}
391
- or project_type == "api" and stack.stack in _API_STACKS
392
- or project_type == "cli" and any(
393
- framework.name in _CLI_FRAMEWORKS for framework in stack.frameworks
394
- )
395
- or project_type == "cli" and stack.stack in {"cpp", "dotnet"}):
396
- priority = 3
397
- return (priority, score, manifest_weight)
398
-
399
- return max(stacks, key=sort_key).stack
400
-
401
444
  def _count_extension_hits(self, file_tree: dict[str, Any], stack: str) -> int:
402
- extensions = {
403
- "python": (".py",),
404
- "nodejs": (".js", ".ts", ".tsx"),
405
- "go": (".go",),
406
- "rust": (".rs",),
407
- "java": (".java",),
408
- "php": (".php",),
409
- "ruby": (".rb",),
410
- "dart": (".dart",),
411
- "dotnet": (".cs", ".fs"),
412
- "elixir": (".ex", ".exs"),
413
- "kotlin": (".kt", ".kts"),
414
- "scala": (".scala",),
415
- "terraform": (".tf",),
416
- "cpp": (".c", ".cc", ".cpp", ".cxx", ".hpp", ".h"),
417
- }.get(stack, ())
445
+ extensions = _STACK_EXTENSIONS.get(stack, ())
418
446
  return sum(1 for path in flatten_file_tree(file_tree) if path.endswith(extensions))
419
447
 
420
448
  def _score_to_confidence(self, score: int) -> ConfidenceLevel:
@@ -1965,6 +1965,13 @@ def main(
1965
1965
  analyzer_fingerprints=_fingerprints,
1966
1966
  traversal_topology=_topology.as_dict(),
1967
1967
  )
1968
+ # Single primary-stack authority over the FINAL assembled set (root + workspace
1969
+ # stacks). enrich marked the root stacks; workspace stacks were appended after,
1970
+ # so re-run the one authority here — idempotent — so every consumer of sm.stacks
1971
+ # reads a coherent primary flag.
1972
+ from sourcecode.classifier import select_primary_stack as _select_primary_stack
1973
+ from sourcecode.tree_utils import flatten_file_tree as _flatten_ft
1974
+ stacks = _select_primary_stack(stacks, project_type, _flatten_ft(file_tree))
1968
1975
  sm = SourceMap(
1969
1976
  metadata=metadata,
1970
1977
  file_tree=file_tree,
@@ -67,6 +67,16 @@ _BOOTSTRAP_ANNOTATION_RE = re.compile(
67
67
  r'EnableAutoConfiguration|SpringBootConfiguration)\b'
68
68
  r'|\bextends\s+SpringBootServletInitializer\b'
69
69
  )
70
+ # Comment strippers — a main()/bootstrap annotation inside a javadoc example or a
71
+ # comment is NOT an entry point (e.g. Broadleaf's *AutoConfiguration classes whose
72
+ # javadoc shows `public static void main` and mentions @SpringBootApplication).
73
+ # Detection of bootstrap entries must run on de-commented source.
74
+ _BLOCK_COMMENT_RE = re.compile(r'/\*.*?\*/', re.DOTALL)
75
+ _LINE_COMMENT_RE = re.compile(r'//[^\n]*')
76
+
77
+
78
+ def _strip_java_comments(source: str) -> str:
79
+ return _LINE_COMMENT_RE.sub(' ', _BLOCK_COMMENT_RE.sub(' ', source))
70
80
 
71
81
  # --- Keycloak / Quarkus SPI ---
72
82
  # Matches "implements EventListenerProvider" or "implements Foo, EventListenerProvider, Bar"
@@ -533,12 +543,21 @@ class JavaDetector(AbstractDetector):
533
543
  _other_files = [p for p in _non_test if "Controller" not in p]
534
544
  scan_candidates = _ctrl_files + _other_files[:max(0, _MAX_JAVA_ENTRY_SCAN - len(_ctrl_files))]
535
545
 
546
+ # Paths already emitted as application entries by the name-gated step-1 scan,
547
+ # so the annotation scan (which now also recovers main()-bearing files) does
548
+ # not list the same bootstrap twice.
549
+ _app_paths = {ep.path for ep in entry_points if ep.kind == "application"}
536
550
  annotation_eps: list[EntryPoint] = []
537
551
  for rel_path in scan_candidates:
538
552
  if len(annotation_eps) >= _MAX_ANNOTATION_ENTRY_POINTS:
539
553
  break
540
554
  abs_path = context.root / rel_path
541
- annotation_eps.extend(self._scan_java_file_for_entry_points(abs_path, rel_path))
555
+ for _ep in self._scan_java_file_for_entry_points(abs_path, rel_path):
556
+ if _ep.kind == "application":
557
+ if _ep.path in _app_paths:
558
+ continue
559
+ _app_paths.add(_ep.path)
560
+ annotation_eps.append(_ep)
542
561
  entry_points.extend(annotation_eps)
543
562
 
544
563
  # 3. web.xml servlet/filter declarations
@@ -611,6 +630,7 @@ class JavaDetector(AbstractDetector):
611
630
  content = abs_path.read_text(encoding="utf-8", errors="replace")
612
631
  except OSError:
613
632
  return None
633
+ content = _strip_java_comments(content)
614
634
  if _BOOTSTRAP_ANNOTATION_RE.search(content):
615
635
  return "bootstrap_annotation"
616
636
  if _MAIN_METHOD_RE.search(content):
@@ -678,6 +698,20 @@ class JavaDetector(AbstractDetector):
678
698
  and "ControllerAdvice" not in content
679
699
  and not any(ann in content for ann in _CUSTOM_SECURITY_ANNOTATIONS)
680
700
  and not has_jax_rs and not has_cdi and not has_spi):
701
+ # Not a controller/security/DI file — but it may still be an application
702
+ # entry point. The name-gated step-1 scan only reads *Application.java /
703
+ # *Main.java, so a real main(String[]) in a differently-named class (e.g.
704
+ # neo4j's Neo4jCommunity.java server bootstrap) would otherwise be invisible,
705
+ # leaving a client CLI named Main.java as the sole reported bootstrap. Reuse
706
+ # the content already read here; strip comments first so a main()/annotation
707
+ # in a javadoc example is never mistaken for a real entry point.
708
+ _decommented = _strip_java_comments(content)
709
+ if _BOOTSTRAP_ANNOTATION_RE.search(_decommented):
710
+ return [EntryPoint(path=rel_path, stack="java", kind="application",
711
+ source="bootstrap_annotation")]
712
+ if _MAIN_METHOD_RE.search(_decommented):
713
+ return [EntryPoint(path=rel_path, stack="java", kind="application",
714
+ source="main_method")]
681
715
  return []
682
716
 
683
717
  if _REST_CONTROLLER_RE.search(content):
@@ -30,7 +30,10 @@ ENDPOINT_SURFACE_RECONCILIATION: str = (
30
30
  "handler after inheritance projection and JAX-RS sub-resource-locator composition. "
31
31
  "It is NOT the canonical API surface — it still contains framework dynamic-admin "
32
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 "
33
+ "`endpoints` command excludes, and endpoints from test-fixture / test-utility "
34
+ "modules (tagged scope=\"test_util\"), which the canonical CIR surface excludes so "
35
+ "impact-chain / pr-impact / spring-audit do not report test scaffolding as "
36
+ "production HTTP exposure. Endpoint counts differ across commands BY DESIGN "
34
37
  "and are not contradictory; expected ordering for one repo is "
35
38
  "repo-ir.route_surface (raw, includes FQN-shaped framework routes) >= "
36
39
  "endpoints.total (canonical REST/API surface, FQN-shaped routes filtered out) >= "
@@ -120,6 +120,85 @@ def is_test_or_fixture_path(path: str) -> bool:
120
120
  return False
121
121
 
122
122
 
123
+ # Infrastructure words that, combined with a delimited "test" token in the same
124
+ # directory segment, identify a module whose code is test scaffolding even though
125
+ # it lives under src/main (e.g. "server-test-utils", "foo-testkit"). Kept narrow
126
+ # on purpose: the "test" token must be present in the SAME segment, so packages
127
+ # named "utils"/"support"/"tools" alone never match.
128
+ _TEST_INFRA_TOKENS = frozenset({
129
+ "util", "utils", "fixture", "fixtures", "support",
130
+ "framework", "kit", "harness", "tools", "tool",
131
+ "helper", "helpers", "mock", "mocks", "stub", "stubs", "dummy",
132
+ })
133
+
134
+ # Glued (non-hyphenated) test-fixture module suffixes.
135
+ _TEST_INFRA_SUFFIXES = (
136
+ "testutils", "testutil", "testkit", "testfixtures", "testfixture",
137
+ "testsupport", "testframework", "testharness", "testtools",
138
+ "testhelpers", "testmocks", "teststubs",
139
+ )
140
+
141
+
142
+ def is_test_fixture_module_path(path: str) -> bool:
143
+ """Return True when *path* belongs to a test-fixture / test-utility MODULE.
144
+
145
+ Structural, name-agnostic: a directory segment must contain a hyphen/underscore
146
+ -delimited ``test`` token together with an infrastructure word (util(s),
147
+ fixture(s), support, framework, kit, harness, tools, helper(s), mock(s),
148
+ stub(s), dummy) — e.g. ``server-test-utils``, ``foo-testkit``,
149
+ ``bar-test-support`` — or match a known test-module segment outright.
150
+
151
+ This is deliberately NARROWER than :func:`is_test_or_fixture_path`: it targets
152
+ modules that ship HTTP resources, JAX-RS ``@Path`` fixtures, or dummy
153
+ controllers under ``src/main`` so they are not misreported as production API
154
+ surface. Requiring both tokens in one segment keeps legitimate ``utils`` /
155
+ ``support`` / ``tools`` packages from ever matching.
156
+ """
157
+ norm = path.replace("\\", "/").lower()
158
+ parts = norm.split("/")
159
+ for part in parts[:-1]: # skip the filename
160
+ bare = part.rstrip("/")
161
+ if bare in _TEST_MODULE_SEGMENTS:
162
+ return True
163
+ if bare.endswith(_TEST_INFRA_SUFFIXES):
164
+ return True
165
+ toks = bare.replace("_", "-").split("-")
166
+ if "test" in toks and any(t in _TEST_INFRA_TOKENS for t in toks):
167
+ return True
168
+ return False
169
+
170
+
171
+ _EXAMPLE_MODULE_TOKENS = frozenset({
172
+ "example", "examples", "demo", "demos", "sample", "samples", "showcase",
173
+ })
174
+
175
+
176
+ def is_example_module_path(path: str) -> bool:
177
+ """Return True when *path* is inside an example/demo MODULE directory.
178
+
179
+ Structural: a directory segment above the source root carries a hyphen/underscore
180
+ -delimited example/demo token (e.g. ``eureka-examples/src/main/java/...``). Package
181
+ components below the source root (``io/confluent/examples/...``) are NOT matched —
182
+ only a module directory is a functional role; a package of the same name is not.
183
+ """
184
+ norm = path.replace("\\", "/").lower()
185
+ # Prepend "/" so a relative path ("src/main/...") matches the same boundary as an
186
+ # absolute one; without this the head-truncation silently failed and package
187
+ # components below the source root (e.g. ".../samples/petclinic/...") false-matched.
188
+ probe = "/" + norm
189
+ head = norm
190
+ for root in ("/src/main/", "/src/test/", "/src/"):
191
+ idx = probe.find(root)
192
+ if idx >= 0:
193
+ head = probe[:idx]
194
+ break
195
+ for seg in head.split("/"):
196
+ toks = seg.replace("_", "-").split("-")
197
+ if any(t in _EXAMPLE_MODULE_TOKENS for t in toks):
198
+ return True
199
+ return False
200
+
201
+
123
202
  def is_vendor_path(path: str) -> bool:
124
203
  """Return True when *path* is inside a vendored / third-party directory.
125
204
 
@@ -25,7 +25,10 @@ from typing import Any, Iterable, Optional
25
25
  from sourcecode.caller_metrics import CALLER_METRIC_RECONCILIATION
26
26
  from sourcecode.endpoint_metrics import ENDPOINT_SURFACE_RECONCILIATION
27
27
  from sourcecode.fqn_utils import normalize_owner_fqn as _normalize_owner_fqn
28
- from sourcecode.path_filters import is_test_path as _is_test_path
28
+ from sourcecode.path_filters import (
29
+ is_test_path as _is_test_path,
30
+ is_test_fixture_module_path as _is_test_fixture_module_path,
31
+ )
29
32
  from sourcecode.security_config import (
30
33
  CustomSecuritySpec,
31
34
  capture_markers as _capture_markers,
@@ -4176,6 +4179,23 @@ def _assemble(
4176
4179
  _route_surface = _build_route_surface(
4177
4180
  sorted_syms, route_diffs, extends_map=_extends_map, custom_security=custom_security
4178
4181
  )
4182
+ # Tag routes that originate in a test-fixture / test-utility module (dummy web
4183
+ # services, JAX-RS @Path fixtures shipped under src/main of a *-test-utils /
4184
+ # testsuite / test-framework module). route_surface stays RAW evidence — nothing
4185
+ # is dropped here; the tag lets the CIR endpoint projection (ir_dict_to_canonical)
4186
+ # exclude them so every CIR consumer (impact-chain, pr-impact, review-pr,
4187
+ # spring-audit) shares one production-endpoint definition with the `endpoints`
4188
+ # command, while repo-ir can still see the full surface. Mirrors the scope tag
4189
+ # applied in extract_java_endpoints.
4190
+ _rs_fqn_file = {
4191
+ s.symbol: (s.source_file or s.declaring_file)
4192
+ for s in sorted_syms
4193
+ if s.type in ("class", "interface")
4194
+ }
4195
+ for _r in _route_surface:
4196
+ _rf = _rs_fqn_file.get(_r.get("effective_class", ""), "")
4197
+ if _rf and _is_test_fixture_module_path(_rf):
4198
+ _r["scope"] = "test_util"
4179
4199
  _analysis_gaps = _compute_analysis_gaps(
4180
4200
  sorted_syms, spring_summary, _route_surface, sorted_rels,
4181
4201
  unparsed_type_files=unparsed_type_files,
@@ -5584,7 +5604,7 @@ def extract_java_endpoints(root: Path) -> "dict[str, Any]":
5584
5604
  """
5585
5605
  import re as _re
5586
5606
  from typing import Any as _Any
5587
- from sourcecode.path_filters import is_test_path
5607
+ from sourcecode.path_filters import is_test_path, is_test_fixture_module_path
5588
5608
 
5589
5609
  _EXTENDS_FROM_SIG = _re.compile(r'\bextends\s+(\w+)')
5590
5610
 
@@ -5753,6 +5773,16 @@ def extract_java_endpoints(root: Path) -> "dict[str, Any]":
5753
5773
  endpoint_warnings = _recovery["warnings"]
5754
5774
  _openapi_spec_path = _recovery["spec_path"]
5755
5775
 
5776
+ # Map each declared class/interface FQN to its source file so an endpoint can be
5777
+ # traced back to the MODULE that ships it. Used to flag endpoints that originate
5778
+ # in a test-fixture / test-utility module (dummy web services, JAX-RS @Path
5779
+ # fixtures) living under src/main — these are not production API surface.
5780
+ _fqn_to_file: dict[str, str] = {
5781
+ s.symbol: (s.source_file or s.declaring_file)
5782
+ for s in all_symbols
5783
+ if s.type in ("class", "interface")
5784
+ }
5785
+
5756
5786
  endpoints: list[dict] = []
5757
5787
  for route in routes:
5758
5788
  handler = (
@@ -5769,6 +5799,9 @@ def extract_java_endpoints(root: Path) -> "dict[str, Any]":
5769
5799
  "handler": handler,
5770
5800
  "return_type": route.get("return_type", "void"),
5771
5801
  }
5802
+ _src = _fqn_to_file.get(route.get("effective_class", ""), "")
5803
+ if _src and is_test_fixture_module_path(_src):
5804
+ entry["scope"] = "test_util"
5772
5805
  # Use security_annotations already extracted by _build_route_surface
5773
5806
  # via the canonical _route_security_from_sym extractor.
5774
5807
  security_info = route.get("security_annotations")
@@ -5791,6 +5824,14 @@ def extract_java_endpoints(root: Path) -> "dict[str, Any]":
5791
5824
  )
5792
5825
  endpoints = [e for e in endpoints if not _FQN_PATH_RE.search(e.get("path", ""))]
5793
5826
 
5827
+ # Partition test-fixture/test-utility endpoints out of the production surface.
5828
+ # They ship under src/main of a *-test-utils / *-testkit / test-framework module
5829
+ # (dummy web services, JAX-RS @Path fixtures) and must not be reported as, or
5830
+ # counted with, the real API surface — but they are preserved in their own bucket
5831
+ # so nothing is silently dropped and the exclusion is auditable.
5832
+ _test_util_endpoints = [e for e in endpoints if e.get("scope") == "test_util"]
5833
+ endpoints = [e for e in endpoints if e.get("scope") != "test_util"]
5834
+
5794
5835
  # "no_security_signal" = no recognized security annotation at method OR class level.
5795
5836
  # Note: repos may use framework-level security (e.g. Keycloak itself) with no
5796
5837
  # per-endpoint annotations — this count reflects annotation-based coverage only.
@@ -5919,6 +5960,16 @@ def extract_java_endpoints(root: Path) -> "dict[str, Any]":
5919
5960
  # Keep legacy field name for backward compat, now means same as no_security_signal
5920
5961
  "undocumented": no_security_signal,
5921
5962
  }
5963
+ # Surface test-fixture endpoints excluded from the production surface above.
5964
+ # Reported separately (not hidden) so the exclusion is transparent and auditable.
5965
+ if _test_util_endpoints:
5966
+ result["test_util_endpoints"] = _test_util_endpoints
5967
+ result["test_util_excluded"] = len(_test_util_endpoints)
5968
+ result.setdefault("notes", []).append(
5969
+ f"{len(_test_util_endpoints)} endpoint(s) from test-fixture/test-utility "
5970
+ f"module(s) under src/main were excluded from the production surface "
5971
+ f"(see test_util_endpoints)."
5972
+ )
5922
5973
  # Surface incomplete-endpoint warnings (interface-defined controllers) only when
5923
5974
  # present, to keep output backward-compatible for the common case.
5924
5975
  if endpoint_warnings:
@@ -6366,6 +6417,12 @@ def compute_blast_radius(
6366
6417
 
6367
6418
  endpoints_affected: list[dict] = []
6368
6419
  for ep in route_surface:
6420
+ # Test-fixture routes (tagged by build_repo_ir) are not production HTTP
6421
+ # exposure — skip them so the legacy `impact` command shares the single
6422
+ # production-endpoint definition used by the CIR surfaces. Nothing else about
6423
+ # blast-radius selection changes; FQN-admin routes are untouched.
6424
+ if ep.get("scope") == "test_util":
6425
+ continue
6369
6426
  ep_class = ep.get("effective_class") or ep.get("controller") or ep.get("class") or ""
6370
6427
  ep_symbol = ep.get("symbol") or ""
6371
6428
  ep_handler = (