sourcecode 4.5.3__tar.gz → 4.6.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 (215) hide show
  1. {sourcecode-4.5.3 → sourcecode-4.6.0}/CHANGELOG.md +127 -0
  2. {sourcecode-4.5.3 → sourcecode-4.6.0}/PKG-INFO +3 -3
  3. {sourcecode-4.5.3 → sourcecode-4.6.0}/README.md +2 -2
  4. {sourcecode-4.5.3 → sourcecode-4.6.0}/pyproject.toml +1 -1
  5. {sourcecode-4.5.3 → sourcecode-4.6.0}/src/sourcecode/__init__.py +1 -1
  6. {sourcecode-4.5.3 → sourcecode-4.6.0}/src/sourcecode/archetype.py +44 -0
  7. {sourcecode-4.5.3 → sourcecode-4.6.0}/src/sourcecode/baseline_autocapture.py +13 -1
  8. {sourcecode-4.5.3 → sourcecode-4.6.0}/src/sourcecode/cache.py +27 -1
  9. {sourcecode-4.5.3 → sourcecode-4.6.0}/src/sourcecode/cli.py +3 -1
  10. {sourcecode-4.5.3 → sourcecode-4.6.0}/src/sourcecode/explain.py +133 -40
  11. {sourcecode-4.5.3 → sourcecode-4.6.0}/src/sourcecode/path_filters.py +50 -40
  12. {sourcecode-4.5.3 → sourcecode-4.6.0}/src/sourcecode/posture.py +65 -5
  13. {sourcecode-4.5.3 → sourcecode-4.6.0}/src/sourcecode/repository_ir.py +20 -13
  14. {sourcecode-4.5.3 → sourcecode-4.6.0}/src/sourcecode/risk.py +119 -31
  15. {sourcecode-4.5.3 → sourcecode-4.6.0}/src/sourcecode/spring_impact.py +66 -6
  16. {sourcecode-4.5.3 → sourcecode-4.6.0}/src/sourcecode/test_sources.py +47 -4
  17. {sourcecode-4.5.3 → sourcecode-4.6.0}/.github/workflows/build-windows.yml +0 -0
  18. {sourcecode-4.5.3 → sourcecode-4.6.0}/.gitignore +0 -0
  19. {sourcecode-4.5.3 → sourcecode-4.6.0}/.ruff.toml +0 -0
  20. {sourcecode-4.5.3 → sourcecode-4.6.0}/CLAUDE.md +0 -0
  21. {sourcecode-4.5.3 → sourcecode-4.6.0}/CONTRIBUTING.md +0 -0
  22. {sourcecode-4.5.3 → sourcecode-4.6.0}/LICENSE +0 -0
  23. {sourcecode-4.5.3 → sourcecode-4.6.0}/SECURITY.md +0 -0
  24. {sourcecode-4.5.3 → sourcecode-4.6.0}/raw +0 -0
  25. {sourcecode-4.5.3 → sourcecode-4.6.0}/scripts/compare_integration_engines.py +0 -0
  26. {sourcecode-4.5.3 → sourcecode-4.6.0}/scripts/customer_smoke_test.sh +0 -0
  27. {sourcecode-4.5.3 → sourcecode-4.6.0}/scripts/generate_jdk_exports.py +0 -0
  28. {sourcecode-4.5.3 → sourcecode-4.6.0}/scripts/perf_harness.py +0 -0
  29. {sourcecode-4.5.3 → sourcecode-4.6.0}/src/sourcecode/adaptive_scanner.py +0 -0
  30. {sourcecode-4.5.3 → sourcecode-4.6.0}/src/sourcecode/architectural_baseline.py +0 -0
  31. {sourcecode-4.5.3 → sourcecode-4.6.0}/src/sourcecode/architectural_delta.py +0 -0
  32. {sourcecode-4.5.3 → sourcecode-4.6.0}/src/sourcecode/architecture_analyzer.py +0 -0
  33. {sourcecode-4.5.3 → sourcecode-4.6.0}/src/sourcecode/architecture_summary.py +0 -0
  34. {sourcecode-4.5.3 → sourcecode-4.6.0}/src/sourcecode/ast_extractor.py +0 -0
  35. {sourcecode-4.5.3 → sourcecode-4.6.0}/src/sourcecode/cache_model.py +0 -0
  36. {sourcecode-4.5.3 → sourcecode-4.6.0}/src/sourcecode/call_surface.py +0 -0
  37. {sourcecode-4.5.3 → sourcecode-4.6.0}/src/sourcecode/caller_metrics.py +0 -0
  38. {sourcecode-4.5.3 → sourcecode-4.6.0}/src/sourcecode/caller_reach.py +0 -0
  39. {sourcecode-4.5.3 → sourcecode-4.6.0}/src/sourcecode/canonical_ir.py +0 -0
  40. {sourcecode-4.5.3 → sourcecode-4.6.0}/src/sourcecode/chain_rules.py +0 -0
  41. {sourcecode-4.5.3 → sourcecode-4.6.0}/src/sourcecode/change_plan.py +0 -0
  42. {sourcecode-4.5.3 → sourcecode-4.6.0}/src/sourcecode/cir_graphs.py +0 -0
  43. {sourcecode-4.5.3 → sourcecode-4.6.0}/src/sourcecode/classifier.py +0 -0
  44. {sourcecode-4.5.3 → sourcecode-4.6.0}/src/sourcecode/client_calls.py +0 -0
  45. {sourcecode-4.5.3 → sourcecode-4.6.0}/src/sourcecode/code_notes_analyzer.py +0 -0
  46. {sourcecode-4.5.3 → sourcecode-4.6.0}/src/sourcecode/compare.py +0 -0
  47. {sourcecode-4.5.3 → sourcecode-4.6.0}/src/sourcecode/confidence_analyzer.py +0 -0
  48. {sourcecode-4.5.3 → sourcecode-4.6.0}/src/sourcecode/constraint_diff.py +0 -0
  49. {sourcecode-4.5.3 → sourcecode-4.6.0}/src/sourcecode/context_cache.py +0 -0
  50. {sourcecode-4.5.3 → sourcecode-4.6.0}/src/sourcecode/context_graph.py +0 -0
  51. {sourcecode-4.5.3 → sourcecode-4.6.0}/src/sourcecode/context_scorer.py +0 -0
  52. {sourcecode-4.5.3 → sourcecode-4.6.0}/src/sourcecode/context_summarizer.py +0 -0
  53. {sourcecode-4.5.3 → sourcecode-4.6.0}/src/sourcecode/contract_diff.py +0 -0
  54. {sourcecode-4.5.3 → sourcecode-4.6.0}/src/sourcecode/contract_init.py +0 -0
  55. {sourcecode-4.5.3 → sourcecode-4.6.0}/src/sourcecode/contract_model.py +0 -0
  56. {sourcecode-4.5.3 → sourcecode-4.6.0}/src/sourcecode/contract_pipeline.py +0 -0
  57. {sourcecode-4.5.3 → sourcecode-4.6.0}/src/sourcecode/coverage_parser.py +0 -0
  58. {sourcecode-4.5.3 → sourcecode-4.6.0}/src/sourcecode/data_exposure.py +0 -0
  59. {sourcecode-4.5.3 → sourcecode-4.6.0}/src/sourcecode/data_labels.py +0 -0
  60. {sourcecode-4.5.3 → sourcecode-4.6.0}/src/sourcecode/defect_identity.py +0 -0
  61. {sourcecode-4.5.3 → sourcecode-4.6.0}/src/sourcecode/degradation.py +0 -0
  62. {sourcecode-4.5.3 → sourcecode-4.6.0}/src/sourcecode/dependency_analyzer.py +0 -0
  63. {sourcecode-4.5.3 → sourcecode-4.6.0}/src/sourcecode/deployment_prefix.py +0 -0
  64. {sourcecode-4.5.3 → sourcecode-4.6.0}/src/sourcecode/detectors/__init__.py +0 -0
  65. {sourcecode-4.5.3 → sourcecode-4.6.0}/src/sourcecode/detectors/base.py +0 -0
  66. {sourcecode-4.5.3 → sourcecode-4.6.0}/src/sourcecode/detectors/csproj_parser.py +0 -0
  67. {sourcecode-4.5.3 → sourcecode-4.6.0}/src/sourcecode/detectors/dart.py +0 -0
  68. {sourcecode-4.5.3 → sourcecode-4.6.0}/src/sourcecode/detectors/dotnet.py +0 -0
  69. {sourcecode-4.5.3 → sourcecode-4.6.0}/src/sourcecode/detectors/elixir.py +0 -0
  70. {sourcecode-4.5.3 → sourcecode-4.6.0}/src/sourcecode/detectors/go.py +0 -0
  71. {sourcecode-4.5.3 → sourcecode-4.6.0}/src/sourcecode/detectors/heuristic.py +0 -0
  72. {sourcecode-4.5.3 → sourcecode-4.6.0}/src/sourcecode/detectors/hybrid.py +0 -0
  73. {sourcecode-4.5.3 → sourcecode-4.6.0}/src/sourcecode/detectors/java.py +0 -0
  74. {sourcecode-4.5.3 → sourcecode-4.6.0}/src/sourcecode/detectors/jvm_ext.py +0 -0
  75. {sourcecode-4.5.3 → sourcecode-4.6.0}/src/sourcecode/detectors/nodejs.py +0 -0
  76. {sourcecode-4.5.3 → sourcecode-4.6.0}/src/sourcecode/detectors/parsers.py +0 -0
  77. {sourcecode-4.5.3 → sourcecode-4.6.0}/src/sourcecode/detectors/php.py +0 -0
  78. {sourcecode-4.5.3 → sourcecode-4.6.0}/src/sourcecode/detectors/project.py +0 -0
  79. {sourcecode-4.5.3 → sourcecode-4.6.0}/src/sourcecode/detectors/python.py +0 -0
  80. {sourcecode-4.5.3 → sourcecode-4.6.0}/src/sourcecode/detectors/ruby.py +0 -0
  81. {sourcecode-4.5.3 → sourcecode-4.6.0}/src/sourcecode/detectors/rust.py +0 -0
  82. {sourcecode-4.5.3 → sourcecode-4.6.0}/src/sourcecode/detectors/systems.py +0 -0
  83. {sourcecode-4.5.3 → sourcecode-4.6.0}/src/sourcecode/detectors/terraform.py +0 -0
  84. {sourcecode-4.5.3 → sourcecode-4.6.0}/src/sourcecode/detectors/tooling.py +0 -0
  85. {sourcecode-4.5.3 → sourcecode-4.6.0}/src/sourcecode/doc_analyzer.py +0 -0
  86. {sourcecode-4.5.3 → sourcecode-4.6.0}/src/sourcecode/dynamic_argument_surface.py +0 -0
  87. {sourcecode-4.5.3 → sourcecode-4.6.0}/src/sourcecode/endpoint_literals.py +0 -0
  88. {sourcecode-4.5.3 → sourcecode-4.6.0}/src/sourcecode/endpoint_metrics.py +0 -0
  89. {sourcecode-4.5.3 → sourcecode-4.6.0}/src/sourcecode/entrypoint_classifier.py +0 -0
  90. {sourcecode-4.5.3 → sourcecode-4.6.0}/src/sourcecode/env_analyzer.py +0 -0
  91. {sourcecode-4.5.3 → sourcecode-4.6.0}/src/sourcecode/envelope.py +0 -0
  92. {sourcecode-4.5.3 → sourcecode-4.6.0}/src/sourcecode/environment_resolution.py +0 -0
  93. {sourcecode-4.5.3 → sourcecode-4.6.0}/src/sourcecode/error_schema.py +0 -0
  94. {sourcecode-4.5.3 → sourcecode-4.6.0}/src/sourcecode/evidence_provider.py +0 -0
  95. {sourcecode-4.5.3 → sourcecode-4.6.0}/src/sourcecode/facts/__init__.py +0 -0
  96. {sourcecode-4.5.3 → sourcecode-4.6.0}/src/sourcecode/facts/registry.json +0 -0
  97. {sourcecode-4.5.3 → sourcecode-4.6.0}/src/sourcecode/file_chunker.py +0 -0
  98. {sourcecode-4.5.3 → sourcecode-4.6.0}/src/sourcecode/file_classifier.py +0 -0
  99. {sourcecode-4.5.3 → sourcecode-4.6.0}/src/sourcecode/filter_surface.py +0 -0
  100. {sourcecode-4.5.3 → sourcecode-4.6.0}/src/sourcecode/format_contract.py +0 -0
  101. {sourcecode-4.5.3 → sourcecode-4.6.0}/src/sourcecode/fqn_utils.py +0 -0
  102. {sourcecode-4.5.3 → sourcecode-4.6.0}/src/sourcecode/git_analyzer.py +0 -0
  103. {sourcecode-4.5.3 → sourcecode-4.6.0}/src/sourcecode/git_checkout.py +0 -0
  104. {sourcecode-4.5.3 → sourcecode-4.6.0}/src/sourcecode/graph_analyzer.py +0 -0
  105. {sourcecode-4.5.3 → sourcecode-4.6.0}/src/sourcecode/graph_evidence.py +0 -0
  106. {sourcecode-4.5.3 → sourcecode-4.6.0}/src/sourcecode/hibernate_strat.py +0 -0
  107. {sourcecode-4.5.3 → sourcecode-4.6.0}/src/sourcecode/identity_fallback.py +0 -0
  108. {sourcecode-4.5.3 → sourcecode-4.6.0}/src/sourcecode/integration_coordinates.py +0 -0
  109. {sourcecode-4.5.3 → sourcecode-4.6.0}/src/sourcecode/jdk_exports.py +0 -0
  110. {sourcecode-4.5.3 → sourcecode-4.6.0}/src/sourcecode/license.py +0 -0
  111. {sourcecode-4.5.3 → sourcecode-4.6.0}/src/sourcecode/mcp/__init__.py +0 -0
  112. {sourcecode-4.5.3 → sourcecode-4.6.0}/src/sourcecode/mcp/onboarding/__init__.py +0 -0
  113. {sourcecode-4.5.3 → sourcecode-4.6.0}/src/sourcecode/mcp/onboarding/applier.py +0 -0
  114. {sourcecode-4.5.3 → sourcecode-4.6.0}/src/sourcecode/mcp/onboarding/backup.py +0 -0
  115. {sourcecode-4.5.3 → sourcecode-4.6.0}/src/sourcecode/mcp/onboarding/detector.py +0 -0
  116. {sourcecode-4.5.3 → sourcecode-4.6.0}/src/sourcecode/mcp/onboarding/planner.py +0 -0
  117. {sourcecode-4.5.3 → sourcecode-4.6.0}/src/sourcecode/mcp/orchestrator.py +0 -0
  118. {sourcecode-4.5.3 → sourcecode-4.6.0}/src/sourcecode/mcp/registry.py +0 -0
  119. {sourcecode-4.5.3 → sourcecode-4.6.0}/src/sourcecode/mcp/runner.py +0 -0
  120. {sourcecode-4.5.3 → sourcecode-4.6.0}/src/sourcecode/mcp/server.py +0 -0
  121. {sourcecode-4.5.3 → sourcecode-4.6.0}/src/sourcecode/mcp_nudge.py +0 -0
  122. {sourcecode-4.5.3 → sourcecode-4.6.0}/src/sourcecode/metrics_analyzer.py +0 -0
  123. {sourcecode-4.5.3 → sourcecode-4.6.0}/src/sourcecode/migrate_check.py +0 -0
  124. {sourcecode-4.5.3 → sourcecode-4.6.0}/src/sourcecode/migration_blast.py +0 -0
  125. {sourcecode-4.5.3 → sourcecode-4.6.0}/src/sourcecode/non_coverage.py +0 -0
  126. {sourcecode-4.5.3 → sourcecode-4.6.0}/src/sourcecode/openapi_surface.py +0 -0
  127. {sourcecode-4.5.3 → sourcecode-4.6.0}/src/sourcecode/openrewrite_recipe.py +0 -0
  128. {sourcecode-4.5.3 → sourcecode-4.6.0}/src/sourcecode/output_budget.py +0 -0
  129. {sourcecode-4.5.3 → sourcecode-4.6.0}/src/sourcecode/parse_cache.py +0 -0
  130. {sourcecode-4.5.3 → sourcecode-4.6.0}/src/sourcecode/path_admission.py +0 -0
  131. {sourcecode-4.5.3 → sourcecode-4.6.0}/src/sourcecode/perf.py +0 -0
  132. {sourcecode-4.5.3 → sourcecode-4.6.0}/src/sourcecode/pipe_contract.py +0 -0
  133. {sourcecode-4.5.3 → sourcecode-4.6.0}/src/sourcecode/pr_comment_renderer.py +0 -0
  134. {sourcecode-4.5.3 → sourcecode-4.6.0}/src/sourcecode/pr_impact.py +0 -0
  135. {sourcecode-4.5.3 → sourcecode-4.6.0}/src/sourcecode/prepare_context.py +0 -0
  136. {sourcecode-4.5.3 → sourcecode-4.6.0}/src/sourcecode/progress.py +0 -0
  137. {sourcecode-4.5.3 → sourcecode-4.6.0}/src/sourcecode/ranking_engine.py +0 -0
  138. {sourcecode-4.5.3 → sourcecode-4.6.0}/src/sourcecode/readiness_timeline.py +0 -0
  139. {sourcecode-4.5.3 → sourcecode-4.6.0}/src/sourcecode/reconciliation.py +0 -0
  140. {sourcecode-4.5.3 → sourcecode-4.6.0}/src/sourcecode/redactor.py +0 -0
  141. {sourcecode-4.5.3 → sourcecode-4.6.0}/src/sourcecode/reference_facts.py +0 -0
  142. {sourcecode-4.5.3 → sourcecode-4.6.0}/src/sourcecode/release_info.py +0 -0
  143. {sourcecode-4.5.3 → sourcecode-4.6.0}/src/sourcecode/relevance_scorer.py +0 -0
  144. {sourcecode-4.5.3 → sourcecode-4.6.0}/src/sourcecode/remedies.py +0 -0
  145. {sourcecode-4.5.3 → sourcecode-4.6.0}/src/sourcecode/rename_refactor.py +0 -0
  146. {sourcecode-4.5.3 → sourcecode-4.6.0}/src/sourcecode/repo_classifier.py +0 -0
  147. {sourcecode-4.5.3 → sourcecode-4.6.0}/src/sourcecode/retrieval/__init__.py +0 -0
  148. {sourcecode-4.5.3 → sourcecode-4.6.0}/src/sourcecode/retrieval/context.py +0 -0
  149. {sourcecode-4.5.3 → sourcecode-4.6.0}/src/sourcecode/retrieval/errors.py +0 -0
  150. {sourcecode-4.5.3 → sourcecode-4.6.0}/src/sourcecode/retrieval/executor.py +0 -0
  151. {sourcecode-4.5.3 → sourcecode-4.6.0}/src/sourcecode/retrieval/planner.py +0 -0
  152. {sourcecode-4.5.3 → sourcecode-4.6.0}/src/sourcecode/retrieval/query.py +0 -0
  153. {sourcecode-4.5.3 → sourcecode-4.6.0}/src/sourcecode/retrieval/request.py +0 -0
  154. {sourcecode-4.5.3 → sourcecode-4.6.0}/src/sourcecode/retrieval/resolution.py +0 -0
  155. {sourcecode-4.5.3 → sourcecode-4.6.0}/src/sourcecode/retrieval/result.py +0 -0
  156. {sourcecode-4.5.3 → sourcecode-4.6.0}/src/sourcecode/retrieval/retriever.py +0 -0
  157. {sourcecode-4.5.3 → sourcecode-4.6.0}/src/sourcecode/retrieval/runtime.py +0 -0
  158. {sourcecode-4.5.3 → sourcecode-4.6.0}/src/sourcecode/retrieval/steps.py +0 -0
  159. {sourcecode-4.5.3 → sourcecode-4.6.0}/src/sourcecode/retrieval/steps_endpoint.py +0 -0
  160. {sourcecode-4.5.3 → sourcecode-4.6.0}/src/sourcecode/retrieval/steps_graph.py +0 -0
  161. {sourcecode-4.5.3 → sourcecode-4.6.0}/src/sourcecode/retrieval/steps_impact.py +0 -0
  162. {sourcecode-4.5.3 → sourcecode-4.6.0}/src/sourcecode/retrieval/steps_intf.py +0 -0
  163. {sourcecode-4.5.3 → sourcecode-4.6.0}/src/sourcecode/retrieval/steps_struct.py +0 -0
  164. {sourcecode-4.5.3 → sourcecode-4.6.0}/src/sourcecode/retrieval/steps_txsec.py +0 -0
  165. {sourcecode-4.5.3 → sourcecode-4.6.0}/src/sourcecode/ris.py +0 -0
  166. {sourcecode-4.5.3 → sourcecode-4.6.0}/src/sourcecode/rule_catalog.py +0 -0
  167. {sourcecode-4.5.3 → sourcecode-4.6.0}/src/sourcecode/runtime_classifier.py +0 -0
  168. {sourcecode-4.5.3 → sourcecode-4.6.0}/src/sourcecode/sarif.py +0 -0
  169. {sourcecode-4.5.3 → sourcecode-4.6.0}/src/sourcecode/scanner.py +0 -0
  170. {sourcecode-4.5.3 → sourcecode-4.6.0}/src/sourcecode/schema.py +0 -0
  171. {sourcecode-4.5.3 → sourcecode-4.6.0}/src/sourcecode/schemas/envelope-v1.schema.json +0 -0
  172. {sourcecode-4.5.3 → sourcecode-4.6.0}/src/sourcecode/security_config.py +0 -0
  173. {sourcecode-4.5.3 → sourcecode-4.6.0}/src/sourcecode/security_config_scan.py +0 -0
  174. {sourcecode-4.5.3 → sourcecode-4.6.0}/src/sourcecode/security_posture.py +0 -0
  175. {sourcecode-4.5.3 → sourcecode-4.6.0}/src/sourcecode/semantic_analyzer.py +0 -0
  176. {sourcecode-4.5.3 → sourcecode-4.6.0}/src/sourcecode/semantic_impact_engine.py +0 -0
  177. {sourcecode-4.5.3 → sourcecode-4.6.0}/src/sourcecode/semantic_integration_engine.py +0 -0
  178. {sourcecode-4.5.3 → sourcecode-4.6.0}/src/sourcecode/semantic_services.py +0 -0
  179. {sourcecode-4.5.3 → sourcecode-4.6.0}/src/sourcecode/serializer.py +0 -0
  180. {sourcecode-4.5.3 → sourcecode-4.6.0}/src/sourcecode/servlet_surface.py +0 -0
  181. {sourcecode-4.5.3 → sourcecode-4.6.0}/src/sourcecode/source_text.py +0 -0
  182. {sourcecode-4.5.3 → sourcecode-4.6.0}/src/sourcecode/spring_event_topology.py +0 -0
  183. {sourcecode-4.5.3 → sourcecode-4.6.0}/src/sourcecode/spring_findings.py +0 -0
  184. {sourcecode-4.5.3 → sourcecode-4.6.0}/src/sourcecode/spring_model.py +0 -0
  185. {sourcecode-4.5.3 → sourcecode-4.6.0}/src/sourcecode/spring_profiles.py +0 -0
  186. {sourcecode-4.5.3 → sourcecode-4.6.0}/src/sourcecode/spring_properties.py +0 -0
  187. {sourcecode-4.5.3 → sourcecode-4.6.0}/src/sourcecode/spring_security_audit.py +0 -0
  188. {sourcecode-4.5.3 → sourcecode-4.6.0}/src/sourcecode/spring_semantic.py +0 -0
  189. {sourcecode-4.5.3 → sourcecode-4.6.0}/src/sourcecode/spring_tx_analyzer.py +0 -0
  190. {sourcecode-4.5.3 → sourcecode-4.6.0}/src/sourcecode/summarizer.py +0 -0
  191. {sourcecode-4.5.3 → sourcecode-4.6.0}/src/sourcecode/target_admission.py +0 -0
  192. {sourcecode-4.5.3 → sourcecode-4.6.0}/src/sourcecode/telemetry/__init__.py +0 -0
  193. {sourcecode-4.5.3 → sourcecode-4.6.0}/src/sourcecode/telemetry/config.py +0 -0
  194. {sourcecode-4.5.3 → sourcecode-4.6.0}/src/sourcecode/telemetry/consent.py +0 -0
  195. {sourcecode-4.5.3 → sourcecode-4.6.0}/src/sourcecode/telemetry/events.py +0 -0
  196. {sourcecode-4.5.3 → sourcecode-4.6.0}/src/sourcecode/telemetry/filters.py +0 -0
  197. {sourcecode-4.5.3 → sourcecode-4.6.0}/src/sourcecode/telemetry/transport.py +0 -0
  198. {sourcecode-4.5.3 → sourcecode-4.6.0}/src/sourcecode/test_gap_ranking.py +0 -0
  199. {sourcecode-4.5.3 → sourcecode-4.6.0}/src/sourcecode/token_estimate.py +0 -0
  200. {sourcecode-4.5.3 → sourcecode-4.6.0}/src/sourcecode/tree_utils.py +0 -0
  201. {sourcecode-4.5.3 → sourcecode-4.6.0}/src/sourcecode/type_usage_surface.py +0 -0
  202. {sourcecode-4.5.3 → sourcecode-4.6.0}/src/sourcecode/validation_inference.py +0 -0
  203. {sourcecode-4.5.3 → sourcecode-4.6.0}/src/sourcecode/validation_surface.py +0 -0
  204. {sourcecode-4.5.3 → sourcecode-4.6.0}/src/sourcecode/verify_edit.py +0 -0
  205. {sourcecode-4.5.3 → sourcecode-4.6.0}/src/sourcecode/verify_repo.py +0 -0
  206. {sourcecode-4.5.3 → sourcecode-4.6.0}/src/sourcecode/verify_rules.py +0 -0
  207. {sourcecode-4.5.3 → sourcecode-4.6.0}/src/sourcecode/version_check.py +0 -0
  208. {sourcecode-4.5.3 → sourcecode-4.6.0}/src/sourcecode/workspace.py +0 -0
  209. {sourcecode-4.5.3 → sourcecode-4.6.0}/supabase/functions/README.md +0 -0
  210. {sourcecode-4.5.3 → sourcecode-4.6.0}/supabase/functions/get-license/index.ts +0 -0
  211. {sourcecode-4.5.3 → sourcecode-4.6.0}/supabase/functions/lemonsqueezy-webhook/index.ts +0 -0
  212. {sourcecode-4.5.3 → sourcecode-4.6.0}/supabase/functions/telemetry/index.ts +0 -0
  213. {sourcecode-4.5.3 → sourcecode-4.6.0}/supabase/sql/license_event_ordering.sql +0 -0
  214. {sourcecode-4.5.3 → sourcecode-4.6.0}/supabase/sql/licensing_schema.sql +0 -0
  215. {sourcecode-4.5.3 → sourcecode-4.6.0}/supabase/sql/telemetry_events.sql +0 -0
@@ -2,6 +2,133 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [4.6.0] — 2026-08-04
6
+
7
+ Field evaluation #11 (7,5/10), by the evaluator who measured 4.5.2 twelve hours
8
+ earlier, on the build that closed their previous list. **Eight complaints, eight
9
+ reproductions, eight fixes** — the first evaluation where every complaint
10
+ survives verification and none re-derives a closed row.
11
+
12
+ The headline is not in the list below and is worth stating: the four false
13
+ positives measured on 4.5.2 are **gone**, verified line by line in the field, and
14
+ the run reports **0 false positives across 19 findings** — precision 9/10,
15
+ epistemic honesty 10/10 (*"no he visto nada mejor en ninguna herramienta de
16
+ análisis, comercial o no"*). E-2 is closed in the field, not only in the battery.
17
+
18
+ Minor, not patch: `risk` publishes a sixth reachability value and three new
19
+ fields, `posture` publishes two, `impact-chain` publishes two, and the test/
20
+ production split moves published populations. **No field was removed and no exit
21
+ code moved.**
22
+
23
+ ### Fixed
24
+
25
+ - **`impact-chain` spent 2 300× longer answering than building the model it
26
+ answers from** (C3-42, **high**). The field published `query_time_ms: 1 187 063`
27
+ beside `model_build_time_ms: 516` and abandoned two more invocations past 25
28
+ minutes — the axis this evaluation scored **3/10**, with the verdict that
29
+ decides it: *"un gate de CI que tarda 45 min no es un gate"*. The seed
30
+ expansions (CH-001a/b) answered *"every symbol of this class"* by scanning the
31
+ whole symbol table once per subtype, with the seed set rebuilt for every symbol
32
+ tested. Both now read one class→symbols index memoized on the CIR, which
33
+ matters twice: `RiskComposer.reach` calls that path **once per defect**, and
34
+ that is where the field's ~45-minute `risk` comes from. Measured A/B on
35
+ keycloak (5 525 files, 59 410 symbols), identical results in all four queries:
36
+ `Provider` **8.15 s → 2.60 s**. Second half, and the reason the rest is flat:
37
+ `query_time_ms` was never the walk — it includes a repository-wide TX +
38
+ security audit, now published as `audit_time_ms` with a `query_time_basis`
39
+ stating that the walk is the difference between the two and that
40
+ `model_build_time_ms` is not comparable with either.
41
+ - **`risk` did not read the other authority that answered its own access axis**
42
+ (C1-26, **high**). It asked `posture` only when `--profile` was given, so the
43
+ default invocation weighed an endpoint at `unknown` (×1.4) that `posture`, in
44
+ the same process and over the same CIR, decided was reachable without
45
+ authentication (×2.0). The resolution is now read always; with no set named it
46
+ is the one Spring performs with none given — `default` alone, which CL-8
47
+ records as the permissive case, not a neutral one — and every row publishes
48
+ `profile_set`, `profile_set_basis` (*"a default, never a guarantee: the process
49
+ environment outranks every file"*) and `profile_decisions`. It cannot
50
+ manufacture a verdict: `no_rule_matched` stays unknown coverage, the
51
+ combination is per endpoint, and on a fixture where both chains are
52
+ profile-gated the answer stays `unknown`. Measured on the field's shape: **8.06
53
+ → 11.52**, exactly the ratio of the two access weights, every other factor
54
+ unmoved.
55
+ - **`risk` degraded a repository-wide control defect with a sentence that is
56
+ false in effect** (C1-25, configuration half; second field witness). *"No HTTP
57
+ endpoint in this repository reaches it"* — about the file that decides whether
58
+ any endpoint is reachable at all. Reach for such a defect is **declared, not
59
+ walked**: `posture` already knew which configuration file decided which
60
+ request and now publishes it as `chains.decides_endpoints`, `risk` reads it as
61
+ the reachability value `request_chain`, and the two units stay apart —
62
+ `endpoints_reached` remains the walk, `decides_requests` is the scope.
63
+ Evidence-bound both ways: only a file declaring readable chain rules qualifies,
64
+ and an unresolved servlet path clears the attribution entirely. Fixture: **low
65
+ 2.8 → medium 4.48**.
66
+ - **`posture --resolve-environments` truncated the open-endpoint list without
67
+ declaring the cap** (C2-25). 50 entries under a count of 2 635, with no
68
+ `total`, no `omitted`, no `direction: under_reports` — in the module that
69
+ defines the cap helper and applies it to four other lists. *"Rompe la garantía
70
+ que el resto del tool cumple escrupulosamente, y es precisamente en la lista
71
+ que un auditor necesita completa."* The helper grew the one parameter it was
72
+ missing (the limit that actually bit) and every other slice in the module was
73
+ checked.
74
+ - **`explain` answered a configuration class with every section empty**,
75
+ `found: true` and no warning (C3-41, **high**) — on the command the same
76
+ evaluation calls the product's best asset, for the class an auditor opens
77
+ first. One model error with three faces: a factory member is a method
78
+ (`symbol_kind: "bean"` was excluded by kind, so **every** `@Configuration` in
79
+ every repository published no methods); a configuration class wires through its
80
+ members' signatures and bodies, not through an injection graph (both are atoms
81
+ already published, read only when the class really declares factory members);
82
+ and its constraints are in a DSL, so the section now reads `chain_rules` — the
83
+ authority `posture` uses on that same file — with `file:line` per rule. Fourth,
84
+ and the reason the report was silent: every section was wrapped in a bare
85
+ `except: []`, so a crash and a measured emptiness were one output. A failed
86
+ section now says which one failed.
87
+ - **`cache freshness` called a snapshot STALE over a file no analyser reads**
88
+ (C1-27) — with `RIS HEAD == current HEAD` and `Delta: 0` beside it. C3-40
89
+ (4.5.3) replaced a false *fresh* with a false *stale* by counting any porcelain
90
+ line. One authority now decides which paths qualify
91
+ (`path_filters.analysis_reads`) and both consumers ask it: the freshness
92
+ boolean and the **tree signature**, which is what actually decides the verdict
93
+ and is also the cache key — so an agent writing its own settings file was
94
+ discarding every warm answer. The excused set is state nothing opens and is
95
+ **not** every dot directory: `.github` is read as tooling evidence and stays a
96
+ change. The default is inclusive, because a missed change is a false *fresh*.
97
+ - **The test/production split still had two authorities** (C1-28).
98
+ `path_filters.is_test_path` was never retired after C1-2 made `test_sources`
99
+ the authority in 3.2.1, and the two disagreed on five path shapes in both
100
+ directions. Field symptom: *"`--compact` counts 1 test file that is actually in
101
+ `src/main`"* — verdict right, numerator wrong. The second module is now a
102
+ passthrough, after absorbing everything it knew that the authority did not; and
103
+ a declared **main** source root now outranks a naming convention, the same rule
104
+ the directory half always applied, in the other direction. Fleet effect,
105
+ measured cold: keycloak 5 525 → 5 486 Java files with endpoints **676 → 678**,
106
+ openmrs-core 866 → 861, Broadleaf / alfresco / petclinic unchanged.
107
+ - **`archetype` never read the endpoint surface for the dimension the endpoint
108
+ surface decides** (C3-43). A 3 574-endpoint CRUD REST monolith was labelled
109
+ `engine` at medium confidence on a fan-in Gini, while the legacy classifier it
110
+ supersedes answered `api` and was right. The density `_primary_interface`
111
+ already reads is now positive evidence for `application` and **named negative
112
+ evidence** against `engine` — the shape the library de-bias uses — scaled by
113
+ the measured density, and emitted only when the surface was measured.
114
+ Measured: Broadleaf and keycloak `engine/high → application/low`, petclinic
115
+ `engine/low → application/medium`, jobrunr `engine/high → engine/low`,
116
+ openmrs-core unchanged (1 endpoint, no density, no discount — the negative
117
+ control).
118
+
119
+ ### Notes
120
+
121
+ Two rows from this evaluation are **not** fixed here, on purpose: **P-9** is a
122
+ pricing decision (a fifth pass, and the fourth to argue itself off the seat axis
123
+ — the ceiling is conditioned on this battery's performance work and on a tier
124
+ promotion, not on any missing capability), and **CL-13** is the narrow rule
125
+ catalogue (7 SEC + 6 TX), queued as **F-F** with the *test surface in
126
+ production* rule first, because C1-28 just gave it its admission surface for
127
+ free. Full record in `.planning/FIELD-EVAL-11-2026-08-04.md`; every row is in
128
+ `docs/DEFECT-LEDGER.md`.
129
+
130
+ Suite: 6 468 passed, 6 skipped.
131
+
5
132
  ## [4.5.3] — 2026-08-04
6
133
 
7
134
  Patch: the fixable half of field evaluation #10 (7,5/10, CI reliability **4/10**), in one
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: sourcecode
3
- Version: 4.5.3
3
+ Version: 4.6.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
@@ -42,7 +42,7 @@ Description-Content-Type: text/markdown
42
42
 
43
43
  **Context · Impact · Migration · Architecture · Review — everything from one structural model.**
44
44
 
45
- ![Version](https://img.shields.io/badge/version-4.5.3-blue)
45
+ ![Version](https://img.shields.io/badge/version-4.6.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`**
@@ -97,7 +97,7 @@ brew tap haroundominique/sourcecode && brew install sourcecode
97
97
  # pip / pipx
98
98
  pipx install sourcecode # or: pip install sourcecode
99
99
 
100
- ask version # ask 4.5.3 — and, on a build that has aged,
100
+ ask version # ask 4.6.0 — and, on a build that has aged,
101
101
  # how many releases have probably shipped since
102
102
  ```
103
103
 
@@ -4,7 +4,7 @@
4
4
 
5
5
  **Context · Impact · Migration · Architecture · Review — everything from one structural model.**
6
6
 
7
- ![Version](https://img.shields.io/badge/version-4.5.3-blue)
7
+ ![Version](https://img.shields.io/badge/version-4.6.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`**
@@ -59,7 +59,7 @@ brew tap haroundominique/sourcecode && brew install sourcecode
59
59
  # pip / pipx
60
60
  pipx install sourcecode # or: pip install sourcecode
61
61
 
62
- ask version # ask 4.5.3 — and, on a build that has aged,
62
+ ask version # ask 4.6.0 — and, on a build that has aged,
63
63
  # how many releases have probably shipped since
64
64
  ```
65
65
 
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "sourcecode"
7
- version = "4.5.3"
7
+ version = "4.6.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__ = "4.5.3"
7
+ __version__ = "4.6.0"
@@ -86,6 +86,14 @@ _MIN_CONFIDENT_SCORE = 0.5
86
86
  # discriminating. Tuned so a clear library (no app/server/servlet entry) is not confidently
87
87
  # labeled an engine, while runnable systems (which never trigger the gate) are untouched.
88
88
  _LIBRARY_ENGINE_DISCOUNT = 0.55
89
+ # The same shape for the opposite mistake (C3-43): a repository whose primary
90
+ # surface is HTTP owns storage/kernel packages and hub topology *because* it
91
+ # serves requests with them, so that evidence is not discriminating for "engine"
92
+ # either. Scaled by the measured density, so a repository with a marginal surface
93
+ # is barely discounted and one that is nothing but endpoints is discounted fully.
94
+ # Below the library figure on purpose: an application CAN be built around an
95
+ # engine, whereas a non-runnable artifact cannot be an engine product at all.
96
+ _HTTP_ENGINE_DISCOUNT = 0.45
89
97
  # Entry kinds that make a repo a runnable system (an app/server/servlet/daemon) rather
90
98
  # than a consumable library. Used to gate the library de-bias (P0).
91
99
  _RUNNABLE_ENTRY_KINDS = frozenset({
@@ -409,6 +417,42 @@ class ArchetypeClassifier:
409
417
  add("application", "runtime_entry_plus_services",
410
418
  f"bootstrap/server entry + service mass ≈ {cm['service']:.0%}",
411
419
  2.0, 1.0, max(cm["service"], 0.2))
420
+ # C3-43. The strongest MEASURED discriminator this build has for what a
421
+ # repository is — its HTTP surface — was read by `_primary_interface` and
422
+ # ignored here, so a 3 574-endpoint CRUD REST monolith was labelled
423
+ # `engine` on package-name masses and a fan-in Gini, while the legacy
424
+ # classifier it supersedes answered `api` and was right. Same ramp as the
425
+ # interface dimension, from the same features: one authority for the
426
+ # figure, two dimensions reading it.
427
+ if f.endpoint_share is None:
428
+ # Unmeasured is not zero: contribute nothing, and say so.
429
+ add("application", "http_surface_density",
430
+ "endpoint surface not measured — this dimension is scored without it",
431
+ 2.5, 0.0, 0.0)
432
+ else:
433
+ density = f.endpoint_share * 100
434
+ http_strength = (
435
+ max(0.0, min(1.0, (density - 0.5) / 1.5)) if f.endpoint_total else 0.0
436
+ )
437
+ add("application", "http_surface_density",
438
+ f"{f.endpoint_total} endpoints over {f.total_files} files "
439
+ f"({density:.2f}/100 files) — the repository's primary surface is HTTP",
440
+ 2.5, http_strength, 1.0)
441
+ # And the same fact as NAMED negative evidence for `engine`, the way
442
+ # the library de-bias below does it: a codebase whose product is a
443
+ # request surface owns storage and kernel packages *because* it is an
444
+ # application, so that mass is not, alone, evidence of an engine.
445
+ engine_positive_http = sum(max(0.0, e.contribution) for e in cands["engine"])
446
+ if http_strength > 0 and engine_positive_http > 0:
447
+ cands["engine"].append(Evidence(
448
+ "http_surface_discount",
449
+ f"{f.endpoint_total} endpoints over {f.total_files} files: the "
450
+ "primary surface of this repository is HTTP, and the storage/"
451
+ "kernel mass and hub topology an application needs to serve it "
452
+ "are not, alone, evidence of an engine product",
453
+ 1.0, 1.0, 1.0,
454
+ -engine_positive_http * _HTTP_ENGINE_DISCOUNT * http_strength,
455
+ ))
412
456
  # platform: many modules + multiple concept clusters + distribution
413
457
  multi = 1.0 if f.module_count >= 8 else f.module_count / 8
414
458
  add("platform", "many_modules_multi_concern",
@@ -210,7 +210,19 @@ def worktree_dirty(
210
210
  return None
211
211
  if out.returncode != 0:
212
212
  return None
213
- lines = [ln for ln in out.stdout.splitlines() if ln.strip()]
213
+ # C1-27. "Dirty" means the tree *the analysis reads* differs from HEAD. A
214
+ # change under an editor or agent state directory is not one — no analyser
215
+ # opens those files — and counting it reported STALE on a snapshot that
216
+ # described the tree exactly, with `Delta: 0` beside it. One authority answers
217
+ # which paths qualify (`path_filters.analysis_reads`) and its default is
218
+ # inclusive: anything not named there still counts, because a missed change
219
+ # is a false *fresh* and a spurious one only costs a rebuild.
220
+ from sourcecode.path_filters import analysis_reads
221
+
222
+ lines = [
223
+ ln for ln in out.stdout.splitlines()
224
+ if ln.strip() and analysis_reads(_porcelain_path(ln))
225
+ ]
214
226
  if ignore is None:
215
227
  return bool(lines)
216
228
  candidates = [ignore] if isinstance(ignore, (str, Path)) else list(ignore)
@@ -213,6 +213,14 @@ def _untracked_tree_fingerprint(target: Path) -> str:
213
213
  return f"ff{h.hexdigest()[:14]}"
214
214
 
215
215
 
216
+ def _porcelain_status_path(line: str) -> str:
217
+ """The path a `git status --porcelain` line refers to (destination on renames)."""
218
+ body = line[3:] if len(line) > 3 else ""
219
+ if " -> " in body:
220
+ body = body.split(" -> ", 1)[1]
221
+ return body.strip().strip('"')
222
+
223
+
216
224
  def worktree_signature(repo_root: Path, scope: Optional[Path] = None) -> str:
217
225
  """Hex fingerprint of the **exact tree state an analysis would read**.
218
226
 
@@ -252,12 +260,30 @@ def worktree_signature(repo_root: Path, scope: Optional[Path] = None) -> str:
252
260
  porcelain = st.stdout if st.returncode == 0 else ""
253
261
  except Exception:
254
262
  porcelain = ""
263
+ # C1-27. The signature describes *the tree an analysis would read*, so a line
264
+ # about a file no analyser opens does not belong in it — an editor settings
265
+ # file was invalidating every cached answer and publishing STALE beside
266
+ # `Delta: 0`. One authority decides which paths qualify, and its default is
267
+ # inclusive: anything not named there still invalidates.
268
+ from sourcecode.path_filters import analysis_reads
269
+
270
+ porcelain = "\n".join(
271
+ ln for ln in porcelain.splitlines()
272
+ if ln.strip() and analysis_reads(_porcelain_status_path(ln))
273
+ )
255
274
  if not porcelain.strip():
256
275
  return head # clean — HEAD fully describes the tree
257
276
 
258
277
  try:
278
+ from sourcecode.path_filters import _TOOL_STATE_DIRS
279
+
280
+ # The same exclusion, expressed to git for the half that is not a list of
281
+ # paths. A tracked editor-state file must not enter the signature through
282
+ # the diff after being kept out of the status.
283
+ _diff_spec = list(pathspec) or ["--", "."]
284
+ _diff_spec += [f":(exclude,glob)**/{d}/**" for d in sorted(_TOOL_STATE_DIRS)]
259
285
  df = subprocess.run(
260
- ["git", "-C", str(root), "diff", "HEAD", *pathspec],
286
+ ["git", "-C", str(root), "diff", "HEAD", *_diff_spec],
261
287
  capture_output=True, text=True, timeout=20,
262
288
  )
263
289
  diff_txt = df.stdout if df.returncode == 0 else ""
@@ -8853,7 +8853,9 @@ def explain_cmd(
8853
8853
  except Exception:
8854
8854
  cir = ContextGraph.build(file_list, target).cir # fallback: never break explain
8855
8855
  model = SpringSemanticModel.build(cir)
8856
- explanation = explain_class(class_name, cir, model).capped(limit)
8856
+ # `root` is what lets the security section read the request-chain rules a
8857
+ # configuration class declares in a DSL rather than in annotations (C3-41).
8858
+ explanation = explain_class(class_name, cir, model, root=target).capped(limit)
8857
8859
  finally:
8858
8860
  _prog.finish()
8859
8861
  if _cc_look is not None:
@@ -15,6 +15,7 @@ are transitional and tracked for a later phase.
15
15
  from __future__ import annotations
16
16
 
17
17
  from dataclasses import dataclass, field
18
+ from pathlib import Path
18
19
  from typing import TYPE_CHECKING, Optional
19
20
 
20
21
  from sourcecode.caller_metrics import CALLER_METRIC_RECONCILIATION
@@ -397,7 +398,15 @@ def _structural_purpose(
397
398
 
398
399
 
399
400
  def _build_public_methods(class_fqn: str, raw_nodes: list[dict]) -> list[str]:
400
- """Return public method names for class_fqn from raw_ir nodes."""
401
+ """Return public method names for class_fqn from raw_ir nodes.
402
+
403
+ C3-41: a factory member (`symbol_kind: "bean"`) is a method, and it is the
404
+ *whole* API of a configuration class. Excluding the kind reported
405
+ `public_methods: []` for every `@Configuration` in every repository. Its
406
+ Java visibility is not the test either — a factory member is called by the
407
+ container, not by a caller in this repository, so a package-private one is
408
+ still the class's published surface.
409
+ """
401
410
  prefix = class_fqn + "#"
402
411
  methods: list[str] = []
403
412
  for node in raw_nodes:
@@ -405,6 +414,11 @@ def _build_public_methods(class_fqn: str, raw_nodes: list[dict]) -> list[str]:
405
414
  if not fqn.startswith(prefix):
406
415
  continue
407
416
  kind = node.get("symbol_kind") or node.get("type") or ""
417
+ if kind == "bean":
418
+ name = fqn[len(prefix):]
419
+ if name and not name.startswith("<"):
420
+ methods.append(name)
421
+ continue
408
422
  if kind not in ("method", "endpoint", "constructor", ""):
409
423
  continue
410
424
  modifiers: list[str] = node.get("modifiers") or []
@@ -465,10 +479,45 @@ def _build_callers(
465
479
  return [_simple(f) if counts[_simple(f)] == 1 else f for f in fqns]
466
480
 
467
481
 
468
- def _build_deps(class_fqn: str, graph: "ContextGraph") -> list[str]:
469
- """DI injected dependencies, simple names — via the ContextGraph public API."""
470
- deps = graph.injected_dependencies_of(class_fqn)
471
- return sorted({_simple(d) for d in deps})
482
+ def _build_deps(
483
+ class_fqn: str, graph: "ContextGraph", raw_nodes: "Optional[list[dict]]" = None
484
+ ) -> list[str]:
485
+ """What this class depends on, simple names — via the ContextGraph public API.
486
+
487
+ Injected dependencies for a class that is wired; **plus what a factory
488
+ declares**, for a class that does the wiring (C3-41). A configuration class
489
+ injects nothing: it names its collaborators in the signatures of its factory
490
+ members and builds them in their bodies, so reading only the injection graph
491
+ reported `outgoing_deps: []` for a class with eight collaborators per method.
492
+
493
+ Evidence, not naming: the extra sources are read only when the class actually
494
+ declares factory members, and both are atoms the IR already publishes — the
495
+ class-scope type surface and the instantiation facts.
496
+ """
497
+ deps = set(graph.injected_dependencies_of(class_fqn))
498
+ if _factory_members(class_fqn, raw_nodes or []):
499
+ deps.update(
500
+ ref.type for ref in graph.class_type_references_in(class_fqn) if ref.type
501
+ )
502
+ for member in _factory_members(class_fqn, raw_nodes or []):
503
+ deps.update(inst.type for inst in graph.instantiations_in(member) if inst.type)
504
+ return sorted({_simple(d) for d in deps if d})
505
+
506
+
507
+ def _factory_members(class_fqn: str, raw_nodes: list[dict]) -> list[str]:
508
+ """Members of this class that declare a bean rather than behaviour.
509
+
510
+ The IR already classifies them (`symbol_kind: "bean"`); this is the one place
511
+ that reads it, so both the method list and the dependency list agree about
512
+ what a configuration class contains.
513
+ """
514
+ prefix = class_fqn + "#"
515
+ return [
516
+ str(node.get("fqn"))
517
+ for node in raw_nodes
518
+ if str(node.get("fqn") or "").startswith(prefix)
519
+ and (node.get("symbol_kind") or "") == "bean"
520
+ ]
472
521
 
473
522
 
474
523
  def _build_events_published(class_fqn: str, model: "SpringSemanticModel") -> list[str]:
@@ -566,6 +615,44 @@ def _build_security(
566
615
  return result
567
616
 
568
617
 
618
+ def _build_chain_rules(class_fqn: str, cir: "CanonicalRepositoryIR", root) -> list[str]:
619
+ """Access rules this class declares for the request chain.
620
+
621
+ C3-41. A class that configures the chain writes its constraints in a DSL, not
622
+ in annotations, so the annotation reader returned `[]` for the one class whose
623
+ entire job is deciding access — while `posture`, in the same build, read its
624
+ rules from that very file. One authority answers it (`chain_rules`), so the
625
+ two commands cannot describe the same file differently.
626
+ """
627
+ if root is None:
628
+ return []
629
+ from sourcecode.chain_rules import rules_from_source
630
+
631
+ rel = _source_file_of(class_fqn, cir)
632
+ if not rel:
633
+ return []
634
+ try:
635
+ source = (Path(root) / rel).read_text(encoding="utf-8", errors="replace")
636
+ except OSError:
637
+ return []
638
+ out: list[str] = []
639
+ for rule in rules_from_source(source, rel):
640
+ patterns = ", ".join(rule.patterns) if rule.patterns else "**"
641
+ out.append(
642
+ f"request chain: {patterns} → {rule.decision} "
643
+ f"({rel}:{rule.line})"
644
+ )
645
+ return out
646
+
647
+
648
+ def _source_file_of(class_fqn: str, cir: "CanonicalRepositoryIR") -> str:
649
+ """The repo-relative file declaring this class, from the IR's own node."""
650
+ for node in _get_raw_nodes(cir):
651
+ if node.get("fqn") == class_fqn:
652
+ return str(node.get("file") or node.get("source_file") or "")
653
+ return ""
654
+
655
+
569
656
  def _build_endpoints(class_fqn: str, model: "SpringSemanticModel") -> list[str]:
570
657
  """REST endpoints declared on this controller class."""
571
658
  endpoints = model.endpoint_index.endpoints_for(class_fqn)
@@ -585,13 +672,27 @@ def explain_class(
585
672
  class_name: str,
586
673
  cir: "CanonicalRepositoryIR",
587
674
  model: "SpringSemanticModel",
675
+ root: "Optional[Path]" = None,
588
676
  ) -> ClassExplanation:
589
677
  """Build a ClassExplanation for class_name from existing CIR + model.
590
678
 
591
- Never raises wraps all derivation in try/except.
679
+ Never raises. C3-41: a section that *failed* now says so. Every section was
680
+ wrapped in a bare `except: []`, which made a crash and a measured emptiness
681
+ the same output — and an empty section reads as *"there are none"*.
592
682
  """
593
683
  warnings: list[str] = []
594
684
 
685
+ def _section_of(name: str, build, default):
686
+ """Run one section builder; a failure becomes a warning, not a silence."""
687
+ try:
688
+ return build()
689
+ except Exception as exc: # pragma: no cover — defensive by contract
690
+ warnings.append(
691
+ f"{name} could not be derived ({type(exc).__name__}): this section "
692
+ f"is empty because the derivation failed, not because there is none"
693
+ )
694
+ return default
695
+
595
696
  try:
596
697
  class_fqn, all_matches = _resolve_fqn(class_name, cir)
597
698
  except Exception:
@@ -630,40 +731,32 @@ def explain_class(
630
731
  except Exception:
631
732
  purpose = f"{stereotype} class"
632
733
 
633
- try:
634
- public_methods = _build_public_methods(class_fqn, raw_nodes)
635
- except Exception:
636
- public_methods = []
637
-
638
- try:
639
- incoming_callers = _build_callers(class_fqn, cir, graph)
640
- except Exception:
641
- incoming_callers = []
642
-
643
- try:
644
- outgoing_deps = _build_deps(class_fqn, graph)
645
- except Exception:
646
- outgoing_deps = []
647
-
648
- try:
649
- events_published = _build_events_published(class_fqn, model)
650
- except Exception:
651
- events_published = []
652
-
653
- try:
654
- events_consumed = _build_events_consumed(class_fqn, model)
655
- except Exception:
656
- events_consumed = []
657
-
658
- try:
659
- transactions = _build_transactions(class_fqn, model)
660
- except Exception:
661
- transactions = []
662
-
663
- try:
664
- security_constraints = _build_security(class_fqn, raw_nodes, cir)
665
- except Exception:
666
- security_constraints = []
734
+ public_methods = _section_of(
735
+ "public_methods", lambda: _build_public_methods(class_fqn, raw_nodes), []
736
+ )
737
+ incoming_callers = _section_of(
738
+ "incoming_callers", lambda: _build_callers(class_fqn, cir, graph), []
739
+ )
740
+ outgoing_deps = _section_of(
741
+ "outgoing_deps", lambda: _build_deps(class_fqn, graph, raw_nodes), []
742
+ )
743
+ events_published = _section_of(
744
+ "events_published", lambda: _build_events_published(class_fqn, model), []
745
+ )
746
+ events_consumed = _section_of(
747
+ "events_consumed", lambda: _build_events_consumed(class_fqn, model), []
748
+ )
749
+ transactions = _section_of(
750
+ "transactions", lambda: _build_transactions(class_fqn, model), []
751
+ )
752
+ security_constraints = _section_of(
753
+ "security_constraints", lambda: _build_security(class_fqn, raw_nodes, cir), []
754
+ )
755
+ # A class that configures the request chain declares its constraints in a
756
+ # DSL. Same section, one authority (`chain_rules`) — never a second parser.
757
+ security_constraints = security_constraints + _section_of(
758
+ "security_constraints", lambda: _build_chain_rules(class_fqn, cir, root), []
759
+ )
667
760
 
668
761
  try:
669
762
  rest_endpoints = _build_endpoints(class_fqn, model)