sourcecode 1.19.0__tar.gz → 1.21.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 (189) hide show
  1. {sourcecode-1.19.0 → sourcecode-1.21.0}/PKG-INFO +3 -3
  2. {sourcecode-1.19.0 → sourcecode-1.21.0}/README.md +2 -2
  3. {sourcecode-1.19.0 → sourcecode-1.21.0}/pyproject.toml +1 -1
  4. {sourcecode-1.19.0 → sourcecode-1.21.0}/src/sourcecode/__init__.py +1 -1
  5. {sourcecode-1.19.0 → sourcecode-1.21.0}/src/sourcecode/cli.py +16 -2
  6. {sourcecode-1.19.0 → sourcecode-1.21.0}/src/sourcecode/prepare_context.py +489 -17
  7. {sourcecode-1.19.0 → sourcecode-1.21.0}/src/sourcecode/serializer.py +54 -1
  8. {sourcecode-1.19.0 → sourcecode-1.21.0}/tests/test_signal_hierarchy.py +3 -1
  9. {sourcecode-1.19.0 → sourcecode-1.21.0}/.agents/skills/source-command-gsd-join-discord/SKILL.md +0 -0
  10. {sourcecode-1.19.0 → sourcecode-1.21.0}/.agents/skills/source-command-gsd-review-backlog/SKILL.md +0 -0
  11. {sourcecode-1.19.0 → sourcecode-1.21.0}/.agents/skills/source-command-gsd-workstreams/SKILL.md +0 -0
  12. {sourcecode-1.19.0 → sourcecode-1.21.0}/.continue-here.md +0 -0
  13. {sourcecode-1.19.0 → sourcecode-1.21.0}/.github/workflows/build-windows.yml +0 -0
  14. {sourcecode-1.19.0 → sourcecode-1.21.0}/.gitignore +0 -0
  15. {sourcecode-1.19.0 → sourcecode-1.21.0}/.ruff.toml +0 -0
  16. {sourcecode-1.19.0 → sourcecode-1.21.0}/CONTRIBUTING.md +0 -0
  17. {sourcecode-1.19.0 → sourcecode-1.21.0}/LICENSE +0 -0
  18. {sourcecode-1.19.0 → sourcecode-1.21.0}/SECURITY.md +0 -0
  19. {sourcecode-1.19.0 → sourcecode-1.21.0}/docs/privacy.md +0 -0
  20. {sourcecode-1.19.0 → sourcecode-1.21.0}/docs/schema.md +0 -0
  21. {sourcecode-1.19.0 → sourcecode-1.21.0}/raw +0 -0
  22. {sourcecode-1.19.0 → sourcecode-1.21.0}/run_cli.py +0 -0
  23. {sourcecode-1.19.0 → sourcecode-1.21.0}/src/sourcecode/adaptive_scanner.py +0 -0
  24. {sourcecode-1.19.0 → sourcecode-1.21.0}/src/sourcecode/architecture_analyzer.py +0 -0
  25. {sourcecode-1.19.0 → sourcecode-1.21.0}/src/sourcecode/architecture_summary.py +0 -0
  26. {sourcecode-1.19.0 → sourcecode-1.21.0}/src/sourcecode/ast_extractor.py +0 -0
  27. {sourcecode-1.19.0 → sourcecode-1.21.0}/src/sourcecode/classifier.py +0 -0
  28. {sourcecode-1.19.0 → sourcecode-1.21.0}/src/sourcecode/code_notes_analyzer.py +0 -0
  29. {sourcecode-1.19.0 → sourcecode-1.21.0}/src/sourcecode/confidence_analyzer.py +0 -0
  30. {sourcecode-1.19.0 → sourcecode-1.21.0}/src/sourcecode/context_scorer.py +0 -0
  31. {sourcecode-1.19.0 → sourcecode-1.21.0}/src/sourcecode/context_summarizer.py +0 -0
  32. {sourcecode-1.19.0 → sourcecode-1.21.0}/src/sourcecode/contract_model.py +0 -0
  33. {sourcecode-1.19.0 → sourcecode-1.21.0}/src/sourcecode/contract_pipeline.py +0 -0
  34. {sourcecode-1.19.0 → sourcecode-1.21.0}/src/sourcecode/coverage_parser.py +0 -0
  35. {sourcecode-1.19.0 → sourcecode-1.21.0}/src/sourcecode/dependency_analyzer.py +0 -0
  36. {sourcecode-1.19.0 → sourcecode-1.21.0}/src/sourcecode/detectors/__init__.py +0 -0
  37. {sourcecode-1.19.0 → sourcecode-1.21.0}/src/sourcecode/detectors/base.py +0 -0
  38. {sourcecode-1.19.0 → sourcecode-1.21.0}/src/sourcecode/detectors/csproj_parser.py +0 -0
  39. {sourcecode-1.19.0 → sourcecode-1.21.0}/src/sourcecode/detectors/dart.py +0 -0
  40. {sourcecode-1.19.0 → sourcecode-1.21.0}/src/sourcecode/detectors/dotnet.py +0 -0
  41. {sourcecode-1.19.0 → sourcecode-1.21.0}/src/sourcecode/detectors/elixir.py +0 -0
  42. {sourcecode-1.19.0 → sourcecode-1.21.0}/src/sourcecode/detectors/go.py +0 -0
  43. {sourcecode-1.19.0 → sourcecode-1.21.0}/src/sourcecode/detectors/heuristic.py +0 -0
  44. {sourcecode-1.19.0 → sourcecode-1.21.0}/src/sourcecode/detectors/hybrid.py +0 -0
  45. {sourcecode-1.19.0 → sourcecode-1.21.0}/src/sourcecode/detectors/java.py +0 -0
  46. {sourcecode-1.19.0 → sourcecode-1.21.0}/src/sourcecode/detectors/jvm_ext.py +0 -0
  47. {sourcecode-1.19.0 → sourcecode-1.21.0}/src/sourcecode/detectors/nodejs.py +0 -0
  48. {sourcecode-1.19.0 → sourcecode-1.21.0}/src/sourcecode/detectors/parsers.py +0 -0
  49. {sourcecode-1.19.0 → sourcecode-1.21.0}/src/sourcecode/detectors/php.py +0 -0
  50. {sourcecode-1.19.0 → sourcecode-1.21.0}/src/sourcecode/detectors/project.py +0 -0
  51. {sourcecode-1.19.0 → sourcecode-1.21.0}/src/sourcecode/detectors/python.py +0 -0
  52. {sourcecode-1.19.0 → sourcecode-1.21.0}/src/sourcecode/detectors/ruby.py +0 -0
  53. {sourcecode-1.19.0 → sourcecode-1.21.0}/src/sourcecode/detectors/rust.py +0 -0
  54. {sourcecode-1.19.0 → sourcecode-1.21.0}/src/sourcecode/detectors/systems.py +0 -0
  55. {sourcecode-1.19.0 → sourcecode-1.21.0}/src/sourcecode/detectors/terraform.py +0 -0
  56. {sourcecode-1.19.0 → sourcecode-1.21.0}/src/sourcecode/detectors/tooling.py +0 -0
  57. {sourcecode-1.19.0 → sourcecode-1.21.0}/src/sourcecode/doc_analyzer.py +0 -0
  58. {sourcecode-1.19.0 → sourcecode-1.21.0}/src/sourcecode/entrypoint_classifier.py +0 -0
  59. {sourcecode-1.19.0 → sourcecode-1.21.0}/src/sourcecode/env_analyzer.py +0 -0
  60. {sourcecode-1.19.0 → sourcecode-1.21.0}/src/sourcecode/file_classifier.py +0 -0
  61. {sourcecode-1.19.0 → sourcecode-1.21.0}/src/sourcecode/git_analyzer.py +0 -0
  62. {sourcecode-1.19.0 → sourcecode-1.21.0}/src/sourcecode/graph_analyzer.py +0 -0
  63. {sourcecode-1.19.0 → sourcecode-1.21.0}/src/sourcecode/metrics_analyzer.py +0 -0
  64. {sourcecode-1.19.0 → sourcecode-1.21.0}/src/sourcecode/progress.py +0 -0
  65. {sourcecode-1.19.0 → sourcecode-1.21.0}/src/sourcecode/ranking_engine.py +0 -0
  66. {sourcecode-1.19.0 → sourcecode-1.21.0}/src/sourcecode/redactor.py +0 -0
  67. {sourcecode-1.19.0 → sourcecode-1.21.0}/src/sourcecode/relevance_scorer.py +0 -0
  68. {sourcecode-1.19.0 → sourcecode-1.21.0}/src/sourcecode/repo_classifier.py +0 -0
  69. {sourcecode-1.19.0 → sourcecode-1.21.0}/src/sourcecode/runtime_classifier.py +0 -0
  70. {sourcecode-1.19.0 → sourcecode-1.21.0}/src/sourcecode/scanner.py +0 -0
  71. {sourcecode-1.19.0 → sourcecode-1.21.0}/src/sourcecode/schema.py +0 -0
  72. {sourcecode-1.19.0 → sourcecode-1.21.0}/src/sourcecode/semantic_analyzer.py +0 -0
  73. {sourcecode-1.19.0 → sourcecode-1.21.0}/src/sourcecode/summarizer.py +0 -0
  74. {sourcecode-1.19.0 → sourcecode-1.21.0}/src/sourcecode/telemetry/__init__.py +0 -0
  75. {sourcecode-1.19.0 → sourcecode-1.21.0}/src/sourcecode/telemetry/config.py +0 -0
  76. {sourcecode-1.19.0 → sourcecode-1.21.0}/src/sourcecode/telemetry/consent.py +0 -0
  77. {sourcecode-1.19.0 → sourcecode-1.21.0}/src/sourcecode/telemetry/events.py +0 -0
  78. {sourcecode-1.19.0 → sourcecode-1.21.0}/src/sourcecode/telemetry/filters.py +0 -0
  79. {sourcecode-1.19.0 → sourcecode-1.21.0}/src/sourcecode/telemetry/transport.py +0 -0
  80. {sourcecode-1.19.0 → sourcecode-1.21.0}/src/sourcecode/tree_utils.py +0 -0
  81. {sourcecode-1.19.0 → sourcecode-1.21.0}/src/sourcecode/workspace.py +0 -0
  82. {sourcecode-1.19.0 → sourcecode-1.21.0}/tests/__init__.py +0 -0
  83. {sourcecode-1.19.0 → sourcecode-1.21.0}/tests/conftest.py +0 -0
  84. {sourcecode-1.19.0 → sourcecode-1.21.0}/tests/fixtures/coverage.xml +0 -0
  85. {sourcecode-1.19.0 → sourcecode-1.21.0}/tests/fixtures/fastapi_app/pyproject.toml +0 -0
  86. {sourcecode-1.19.0 → sourcecode-1.21.0}/tests/fixtures/fastapi_app/src/main.py +0 -0
  87. {sourcecode-1.19.0 → sourcecode-1.21.0}/tests/fixtures/go_service/cmd/api/main.go +0 -0
  88. {sourcecode-1.19.0 → sourcecode-1.21.0}/tests/fixtures/go_service/go.mod +0 -0
  89. {sourcecode-1.19.0 → sourcecode-1.21.0}/tests/fixtures/jacoco.xml +0 -0
  90. {sourcecode-1.19.0 → sourcecode-1.21.0}/tests/fixtures/latin1_sample.java +0 -0
  91. {sourcecode-1.19.0 → sourcecode-1.21.0}/tests/fixtures/latin1_sample_iso.java +0 -0
  92. {sourcecode-1.19.0 → sourcecode-1.21.0}/tests/fixtures/lcov.info +0 -0
  93. {sourcecode-1.19.0 → sourcecode-1.21.0}/tests/fixtures/nextjs_app/app/page.tsx +0 -0
  94. {sourcecode-1.19.0 → sourcecode-1.21.0}/tests/fixtures/nextjs_app/package.json +0 -0
  95. {sourcecode-1.19.0 → sourcecode-1.21.0}/tests/fixtures/nextjs_app/pnpm-lock.yaml +0 -0
  96. {sourcecode-1.19.0 → sourcecode-1.21.0}/tests/fixtures/pnpm_monorepo/apps/web/app/page.tsx +0 -0
  97. {sourcecode-1.19.0 → sourcecode-1.21.0}/tests/fixtures/pnpm_monorepo/apps/web/package.json +0 -0
  98. {sourcecode-1.19.0 → sourcecode-1.21.0}/tests/fixtures/pnpm_monorepo/packages/api/main.py +0 -0
  99. {sourcecode-1.19.0 → sourcecode-1.21.0}/tests/fixtures/pnpm_monorepo/packages/api/pyproject.toml +0 -0
  100. {sourcecode-1.19.0 → sourcecode-1.21.0}/tests/fixtures/pnpm_monorepo/pnpm-workspace.yaml +0 -0
  101. {sourcecode-1.19.0 → sourcecode-1.21.0}/tests/fixtures/spring_boot_minimal/pom.xml +0 -0
  102. {sourcecode-1.19.0 → sourcecode-1.21.0}/tests/fixtures/spring_boot_minimal/src/main/java/com/example/ddd/ausente/application/service/FindAusenteService.java +0 -0
  103. {sourcecode-1.19.0 → sourcecode-1.21.0}/tests/fixtures/spring_boot_minimal/src/main/java/com/example/ddd/ausente/domain/entities/Ausente.java +0 -0
  104. {sourcecode-1.19.0 → sourcecode-1.21.0}/tests/fixtures/spring_boot_minimal/src/main/java/com/example/ddd/ausente/infrastructure/rest/AusenteRestController.java +0 -0
  105. {sourcecode-1.19.0 → sourcecode-1.21.0}/tests/fixtures/spring_boot_minimal/src/main/java/com/example/ddd/autocoberturas/application/service/FindAutocoberturasService.java +0 -0
  106. {sourcecode-1.19.0 → sourcecode-1.21.0}/tests/fixtures/spring_boot_minimal/src/main/java/com/example/ddd/autocoberturas/domain/entities/Autocoberturas.java +0 -0
  107. {sourcecode-1.19.0 → sourcecode-1.21.0}/tests/fixtures/spring_boot_minimal/src/main/java/com/example/ddd/autocoberturas/infrastructure/rest/AutocoberturasRestController.java +0 -0
  108. {sourcecode-1.19.0 → sourcecode-1.21.0}/tests/fixtures/spring_boot_minimal/src/main/java/com/example/ddd/calendarioTrabajador/application/service/FindCalendarioTrabajadorService.java +0 -0
  109. {sourcecode-1.19.0 → sourcecode-1.21.0}/tests/fixtures/spring_boot_minimal/src/main/java/com/example/ddd/calendarioTrabajador/domain/entities/CalendarioTrabajador.java +0 -0
  110. {sourcecode-1.19.0 → sourcecode-1.21.0}/tests/fixtures/spring_boot_minimal/src/main/java/com/example/ddd/calendarioTrabajador/infrastructure/rest/CalendarioTrabajadorRestController.java +0 -0
  111. {sourcecode-1.19.0 → sourcecode-1.21.0}/tests/fixtures/spring_boot_minimal/src/main/java/com/example/ddd/departamento/application/service/FindDepartamentoService.java +0 -0
  112. {sourcecode-1.19.0 → sourcecode-1.21.0}/tests/fixtures/spring_boot_minimal/src/main/java/com/example/ddd/departamento/domain/entities/Departamento.java +0 -0
  113. {sourcecode-1.19.0 → sourcecode-1.21.0}/tests/fixtures/spring_boot_minimal/src/main/java/com/example/ddd/departamento/infrastructure/rest/DepartamentoRestController.java +0 -0
  114. {sourcecode-1.19.0 → sourcecode-1.21.0}/tests/fixtures/spring_boot_minimal/src/main/java/com/example/ddd/empleado/application/service/FindEmpleadoService.java +0 -0
  115. {sourcecode-1.19.0 → sourcecode-1.21.0}/tests/fixtures/spring_boot_minimal/src/main/java/com/example/ddd/empleado/domain/entities/Empleado.java +0 -0
  116. {sourcecode-1.19.0 → sourcecode-1.21.0}/tests/fixtures/spring_boot_minimal/src/main/java/com/example/ddd/empleado/infrastructure/rest/EmpleadoRestController.java +0 -0
  117. {sourcecode-1.19.0 → sourcecode-1.21.0}/tests/fixtures/spring_boot_minimal/src/main/java/com/example/demo/DemoApplication.java +0 -0
  118. {sourcecode-1.19.0 → sourcecode-1.21.0}/tests/fixtures/spring_boot_minimal/src/main/java/com/example/demo/config/FilterConfig.java +0 -0
  119. {sourcecode-1.19.0 → sourcecode-1.21.0}/tests/fixtures/spring_boot_minimal/src/main/java/com/example/demo/domain/Health.java +0 -0
  120. {sourcecode-1.19.0 → sourcecode-1.21.0}/tests/fixtures/spring_boot_minimal/src/main/java/com/example/demo/mapper/HealthMapper.java +0 -0
  121. {sourcecode-1.19.0 → sourcecode-1.21.0}/tests/fixtures/spring_boot_minimal/src/main/java/com/example/demo/repository/HealthRepository.java +0 -0
  122. {sourcecode-1.19.0 → sourcecode-1.21.0}/tests/fixtures/spring_boot_minimal/src/main/java/com/example/demo/service/HealthService.java +0 -0
  123. {sourcecode-1.19.0 → sourcecode-1.21.0}/tests/fixtures/spring_boot_minimal/src/main/java/com/example/demo/web/HealthRestController.java +0 -0
  124. {sourcecode-1.19.0 → sourcecode-1.21.0}/tests/fixtures/spring_boot_minimal/src/main/java/com/example/demo/web/NominaRestController.java +0 -0
  125. {sourcecode-1.19.0 → sourcecode-1.21.0}/tests/fixtures/spring_boot_minimal/src/main/resources/application-dev.yml +0 -0
  126. {sourcecode-1.19.0 → sourcecode-1.21.0}/tests/fixtures/spring_boot_minimal/src/main/resources/application.yml +0 -0
  127. {sourcecode-1.19.0 → sourcecode-1.21.0}/tests/fixtures/spring_boot_minimal/src/main/resources/mapper/HealthMapper.xml +0 -0
  128. {sourcecode-1.19.0 → sourcecode-1.21.0}/tests/test_architecture_analyzer.py +0 -0
  129. {sourcecode-1.19.0 → sourcecode-1.21.0}/tests/test_architecture_summary.py +0 -0
  130. {sourcecode-1.19.0 → sourcecode-1.21.0}/tests/test_ast_extractor.py +0 -0
  131. {sourcecode-1.19.0 → sourcecode-1.21.0}/tests/test_block1_reliability.py +0 -0
  132. {sourcecode-1.19.0 → sourcecode-1.21.0}/tests/test_block2_coverage.py +0 -0
  133. {sourcecode-1.19.0 → sourcecode-1.21.0}/tests/test_block5_quality.py +0 -0
  134. {sourcecode-1.19.0 → sourcecode-1.21.0}/tests/test_bug_fixes_v16.py +0 -0
  135. {sourcecode-1.19.0 → sourcecode-1.21.0}/tests/test_classifier.py +0 -0
  136. {sourcecode-1.19.0 → sourcecode-1.21.0}/tests/test_cli.py +0 -0
  137. {sourcecode-1.19.0 → sourcecode-1.21.0}/tests/test_code_notes_analyzer.py +0 -0
  138. {sourcecode-1.19.0 → sourcecode-1.21.0}/tests/test_context_scorer.py +0 -0
  139. {sourcecode-1.19.0 → sourcecode-1.21.0}/tests/test_contract_pipeline.py +0 -0
  140. {sourcecode-1.19.0 → sourcecode-1.21.0}/tests/test_coverage_parser.py +0 -0
  141. {sourcecode-1.19.0 → sourcecode-1.21.0}/tests/test_cross_consistency.py +0 -0
  142. {sourcecode-1.19.0 → sourcecode-1.21.0}/tests/test_dependency_analyzer_node_python.py +0 -0
  143. {sourcecode-1.19.0 → sourcecode-1.21.0}/tests/test_dependency_analyzer_polyglot.py +0 -0
  144. {sourcecode-1.19.0 → sourcecode-1.21.0}/tests/test_dependency_schema.py +0 -0
  145. {sourcecode-1.19.0 → sourcecode-1.21.0}/tests/test_detector_dotnet.py +0 -0
  146. {sourcecode-1.19.0 → sourcecode-1.21.0}/tests/test_detector_go_rust_java.py +0 -0
  147. {sourcecode-1.19.0 → sourcecode-1.21.0}/tests/test_detector_nodejs.py +0 -0
  148. {sourcecode-1.19.0 → sourcecode-1.21.0}/tests/test_detector_php_ruby_dart.py +0 -0
  149. {sourcecode-1.19.0 → sourcecode-1.21.0}/tests/test_detector_python.py +0 -0
  150. {sourcecode-1.19.0 → sourcecode-1.21.0}/tests/test_detector_universal_managed.py +0 -0
  151. {sourcecode-1.19.0 → sourcecode-1.21.0}/tests/test_detector_universal_systems.py +0 -0
  152. {sourcecode-1.19.0 → sourcecode-1.21.0}/tests/test_detectors_base.py +0 -0
  153. {sourcecode-1.19.0 → sourcecode-1.21.0}/tests/test_doc_analyzer_jsdom.py +0 -0
  154. {sourcecode-1.19.0 → sourcecode-1.21.0}/tests/test_doc_analyzer_python.py +0 -0
  155. {sourcecode-1.19.0 → sourcecode-1.21.0}/tests/test_encoding_regression.py +0 -0
  156. {sourcecode-1.19.0 → sourcecode-1.21.0}/tests/test_graph_analyzer_polyglot.py +0 -0
  157. {sourcecode-1.19.0 → sourcecode-1.21.0}/tests/test_graph_analyzer_python_node.py +0 -0
  158. {sourcecode-1.19.0 → sourcecode-1.21.0}/tests/test_graph_schema.py +0 -0
  159. {sourcecode-1.19.0 → sourcecode-1.21.0}/tests/test_hybrid_inference.py +0 -0
  160. {sourcecode-1.19.0 → sourcecode-1.21.0}/tests/test_integration.py +0 -0
  161. {sourcecode-1.19.0 → sourcecode-1.21.0}/tests/test_integration_dependencies.py +0 -0
  162. {sourcecode-1.19.0 → sourcecode-1.21.0}/tests/test_integration_detection.py +0 -0
  163. {sourcecode-1.19.0 → sourcecode-1.21.0}/tests/test_integration_docs.py +0 -0
  164. {sourcecode-1.19.0 → sourcecode-1.21.0}/tests/test_integration_graph_modules.py +0 -0
  165. {sourcecode-1.19.0 → sourcecode-1.21.0}/tests/test_integration_lqn.py +0 -0
  166. {sourcecode-1.19.0 → sourcecode-1.21.0}/tests/test_integration_metrics.py +0 -0
  167. {sourcecode-1.19.0 → sourcecode-1.21.0}/tests/test_integration_multistack.py +0 -0
  168. {sourcecode-1.19.0 → sourcecode-1.21.0}/tests/test_integration_semantics.py +0 -0
  169. {sourcecode-1.19.0 → sourcecode-1.21.0}/tests/test_integration_universal.py +0 -0
  170. {sourcecode-1.19.0 → sourcecode-1.21.0}/tests/test_java_spring_integration.py +0 -0
  171. {sourcecode-1.19.0 → sourcecode-1.21.0}/tests/test_metrics_analyzer.py +0 -0
  172. {sourcecode-1.19.0 → sourcecode-1.21.0}/tests/test_packaging.py +0 -0
  173. {sourcecode-1.19.0 → sourcecode-1.21.0}/tests/test_phase1_improvements.py +0 -0
  174. {sourcecode-1.19.0 → sourcecode-1.21.0}/tests/test_pipeline_integrity.py +0 -0
  175. {sourcecode-1.19.0 → sourcecode-1.21.0}/tests/test_real_projects.py +0 -0
  176. {sourcecode-1.19.0 → sourcecode-1.21.0}/tests/test_redactor.py +0 -0
  177. {sourcecode-1.19.0 → sourcecode-1.21.0}/tests/test_scanner.py +0 -0
  178. {sourcecode-1.19.0 → sourcecode-1.21.0}/tests/test_schema.py +0 -0
  179. {sourcecode-1.19.0 → sourcecode-1.21.0}/tests/test_schema_normalization.py +0 -0
  180. {sourcecode-1.19.0 → sourcecode-1.21.0}/tests/test_semantic_analyzer_node.py +0 -0
  181. {sourcecode-1.19.0 → sourcecode-1.21.0}/tests/test_semantic_analyzer_python.py +0 -0
  182. {sourcecode-1.19.0 → sourcecode-1.21.0}/tests/test_semantic_import_resolution.py +0 -0
  183. {sourcecode-1.19.0 → sourcecode-1.21.0}/tests/test_semantic_schema.py +0 -0
  184. {sourcecode-1.19.0 → sourcecode-1.21.0}/tests/test_summarizer.py +0 -0
  185. {sourcecode-1.19.0 → sourcecode-1.21.0}/tests/test_surface_honesty.py +0 -0
  186. {sourcecode-1.19.0 → sourcecode-1.21.0}/tests/test_task_differentiation.py +0 -0
  187. {sourcecode-1.19.0 → sourcecode-1.21.0}/tests/test_telemetry.py +0 -0
  188. {sourcecode-1.19.0 → sourcecode-1.21.0}/tests/test_v1_10_regressions.py +0 -0
  189. {sourcecode-1.19.0 → sourcecode-1.21.0}/tests/test_workspace_analyzer.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: sourcecode
3
- Version: 1.19.0
3
+ Version: 1.21.0
4
4
  Summary: Deterministic codebase context for AI coding agents
5
5
  License: Apache License
6
6
  Version 2.0, January 2004
@@ -221,7 +221,7 @@ Description-Content-Type: text/markdown
221
221
 
222
222
  **Compressed AI-ready context for Java/Spring enterprise codebases.**
223
223
 
224
- ![Version](https://img.shields.io/badge/version-1.19.0-blue)
224
+ ![Version](https://img.shields.io/badge/version-1.21.0-blue)
225
225
  ![Python](https://img.shields.io/badge/python-3.10%2B-green)
226
226
 
227
227
  ---
@@ -255,7 +255,7 @@ pipx install sourcecode
255
255
 
256
256
  ```bash
257
257
  sourcecode version
258
- # sourcecode 1.19.0
258
+ # sourcecode 1.21.0
259
259
  ```
260
260
 
261
261
  ---
@@ -2,7 +2,7 @@
2
2
 
3
3
  **Compressed AI-ready context for Java/Spring enterprise codebases.**
4
4
 
5
- ![Version](https://img.shields.io/badge/version-1.19.0-blue)
5
+ ![Version](https://img.shields.io/badge/version-1.21.0-blue)
6
6
  ![Python](https://img.shields.io/badge/python-3.10%2B-green)
7
7
 
8
8
  ---
@@ -36,7 +36,7 @@ pipx install sourcecode
36
36
 
37
37
  ```bash
38
38
  sourcecode version
39
- # sourcecode 1.19.0
39
+ # sourcecode 1.21.0
40
40
  ```
41
41
 
42
42
  ---
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "sourcecode"
7
- version = "1.19.0"
7
+ version = "1.21.0"
8
8
  description = "Deterministic codebase context for AI coding agents"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.9"
@@ -1,3 +1,3 @@
1
1
  """sourcecode — Deterministic codebase context maps for AI coding agents."""
2
2
 
3
- __version__ = "1.19.0"
3
+ __version__ = "1.21.0"
@@ -1727,9 +1727,9 @@ def prepare_context_cmd(
1727
1727
  "changed_files": False, "affected_entry_points": False,
1728
1728
  },
1729
1729
  "delta": {
1730
- "project_summary": True, "architecture_summary": False,
1730
+ "project_summary": False, "architecture_summary": False,
1731
1731
  "relevant_files": True, "key_dependencies": False,
1732
- "gaps": False, "confidence": True,
1732
+ "gaps": True, "confidence": True,
1733
1733
  "suspected_areas": False, "improvement_opportunities": False,
1734
1734
  "test_gaps": False, "code_notes_summary": False,
1735
1735
  "changed_files": True, "affected_entry_points": True,
@@ -1771,12 +1771,26 @@ def prepare_context_cmd(
1771
1771
  out["changed_files"] = output.changed_files
1772
1772
  if _task_include("affected_entry_points") and output.affected_entry_points:
1773
1773
  out["affected_entry_points"] = output.affected_entry_points
1774
+ # Delta-specific impact fields
1775
+ if task == "delta":
1776
+ if output.since:
1777
+ out["since"] = output.since
1778
+ if output.impact_summary:
1779
+ out["impact_summary"] = output.impact_summary
1780
+ if output.affected_modules:
1781
+ out["affected_modules"] = output.affected_modules
1782
+ if output.risk_areas:
1783
+ out["risk_areas"] = output.risk_areas
1784
+ if output.why_these_files:
1785
+ out["reasoning"] = output.why_these_files
1774
1786
  if output.limitations:
1775
1787
  out["limitations"] = output.limitations
1776
1788
  if output.symptom:
1777
1789
  out["symptom"] = output.symptom
1778
1790
  if output.related_notes:
1779
1791
  out["related_notes"] = output.related_notes
1792
+ if output.symptom_note:
1793
+ out["symptom_note"] = output.symptom_note
1780
1794
  if llm_prompt:
1781
1795
  out["llm_prompt"] = builder.render_prompt(output)
1782
1796
 
@@ -323,6 +323,12 @@ class TaskOutput:
323
323
  affected_entry_points: list[str] = field(default_factory=list) # delta task only
324
324
  symptom: Optional[str] = None # fix-bug only
325
325
  related_notes: list[dict] = field(default_factory=list) # fix-bug + symptom only
326
+ symptom_note: Optional[str] = None # fix-bug: cross-layer synonym note
327
+ # delta-specific impact fields
328
+ impact_summary: Optional[str] = None
329
+ affected_modules: list[str] = field(default_factory=list)
330
+ risk_areas: list[dict] = field(default_factory=list)
331
+ since: Optional[str] = None
326
332
 
327
333
 
328
334
  # ─────────────────────────────────────────────────────────────────────────────
@@ -382,6 +388,22 @@ _ALL_EXTENSIONS: frozenset[str] = _SOURCE_EXTENSIONS | frozenset({
382
388
  ".md", ".toml", ".yaml", ".yml", ".json", ".xml",
383
389
  })
384
390
 
391
+ # Maps frontend symptom keywords → backend terms likely to contain the root cause.
392
+ # Used to boost service/interceptor files when the symptom is UI-only.
393
+ _FRONTEND_SYMPTOM_MAP: dict[str, list[str]] = {
394
+ "spinner": ["loading", "setloading", "finalize", "httpinterceptor", "interceptor", "service"],
395
+ "loading": ["loading", "setloading", "finalize", "httpinterceptor", "interceptor", "service"],
396
+ "login": ["authcontroller", "securityconfig", "filterconfig", "jwtfilter", "auth", "authentication"],
397
+ "logout": ["authcontroller", "securityconfig", "jwtfilter", "auth", "session"],
398
+ "dropdown": ["getmapping", "findall", "obtenertodos", "listall", "findby"],
399
+ "modal": ["controller", "getmapping", "findby", "search"],
400
+ "popup": ["controller", "getmapping", "findby", "search"],
401
+ "table": ["paginated", "findby", "search", "getmapping", "listall"],
402
+ "grid": ["paginated", "findby", "search", "getmapping"],
403
+ "button": ["postmapping", "putmapping", "deletemapping", "controller", "service"],
404
+ "form": ["postmapping", "putmapping", "controller", "service", "dto"],
405
+ }
406
+
385
407
 
386
408
  class TaskContextBuilder:
387
409
  def __init__(self, root: Path) -> None:
@@ -623,18 +645,42 @@ class TaskContextBuilder:
623
645
  test_set = {p for p in all_paths if self._is_test(p)}
624
646
  source_set = {p for p in all_paths if not self._is_test(p) and self._is_source(p)}
625
647
 
626
- relevant_files = self._rank_files(
627
- task_name, spec, all_paths, entry_set, test_set,
628
- monorepo_packages=sm.monorepo_packages if sm.monorepo_packages else None,
629
- git_hotspots=git_hotspots,
630
- uncommitted_files=uncommitted_files,
631
- code_notes=cn_notes_for_ranking if cn_notes_for_ranking else None,
632
- delta_files=_delta_files,
633
- )
648
+ # Delta uses a dedicated impact-analysis path — never the generic ranker.
649
+ _delta_impact_summary: Optional[str] = None
650
+ _delta_affected_modules: list[str] = []
651
+ _delta_risk_areas: list[dict] = []
652
+ _delta_why: dict[str, str] = {}
653
+ _delta_analysis_gaps: list[str] = []
654
+
655
+ if task_name == "delta":
656
+ _delta_changed_list: list[str] = sorted(_delta_files) if _delta_files else []
657
+ (
658
+ relevant_files,
659
+ _delta_impact_summary,
660
+ _delta_affected_modules,
661
+ _delta_risk_areas,
662
+ _delta_why,
663
+ _delta_analysis_gaps,
664
+ ) = self._build_delta_impact(
665
+ changed_files=_delta_changed_list,
666
+ all_paths=all_paths,
667
+ entry_points=entry_points,
668
+ since=since,
669
+ )
670
+ else:
671
+ relevant_files = self._rank_files(
672
+ task_name, spec, all_paths, entry_set, test_set,
673
+ monorepo_packages=sm.monorepo_packages if sm.monorepo_packages else None,
674
+ git_hotspots=git_hotspots,
675
+ uncommitted_files=uncommitted_files,
676
+ code_notes=cn_notes_for_ranking if cn_notes_for_ranking else None,
677
+ delta_files=None,
678
+ )
634
679
 
635
680
  # ── 6b. Symptom keyword boost + related notes (fix-bug + --symptom) ──
636
681
  symptom_keywords: list[str] = []
637
682
  related_notes: list[dict] = []
683
+ symptom_note: Optional[str] = None
638
684
  if task_name == "fix-bug" and symptom:
639
685
  import re as _re
640
686
  _camel_expanded = _re.sub(r'([a-z])([A-Z])', r'\1 \2', symptom)
@@ -708,6 +754,41 @@ class TaskContextBuilder:
708
754
  ))
709
755
  relevant_files = sorted(_content_boosted, key=lambda rf: -rf.score)
710
756
 
757
+ # Cross-layer synonym boost: frontend keywords → backend equivalents
758
+ _synonym_note: Optional[str] = None
759
+ _frontend_kws = [kw for kw in symptom_keywords if kw in _FRONTEND_SYMPTOM_MAP]
760
+ if _frontend_kws:
761
+ _backend_terms: list[str] = []
762
+ for _fkw in _frontend_kws:
763
+ _backend_terms.extend(_FRONTEND_SYMPTOM_MAP[_fkw])
764
+ _backend_terms_set = list(dict.fromkeys(_backend_terms)) # dedup, preserve order
765
+ _synonym_boosted: list[RelevantFile] = []
766
+ for _rf in relevant_files:
767
+ _extra_syn = 0.0
768
+ if Path(_rf.path).suffix.lower() in _src_exts:
769
+ try:
770
+ _lines_syn = (self.root / _rf.path).read_text(
771
+ encoding="utf-8", errors="replace"
772
+ ).splitlines()[:300]
773
+ _body_syn = "\n".join(_lines_syn).lower()
774
+ _hits_syn = sum(_body_syn.count(t) for t in _backend_terms_set)
775
+ _extra_syn = min(0.20, _hits_syn * 0.02)
776
+ except OSError:
777
+ pass
778
+ _synonym_boosted.append(RelevantFile(
779
+ path=_rf.path,
780
+ role=_rf.role,
781
+ score=round(min(_rf.score + _extra_syn, 1.0), 2),
782
+ reason=_rf.reason + (f", synonym-match backend (+{_extra_syn:.2f})" if _extra_syn > 0 else ""),
783
+ why=_rf.why,
784
+ ))
785
+ relevant_files = sorted(_synonym_boosted, key=lambda rf: -rf.score)
786
+ _synonym_note = (
787
+ f"Frontend concept detected ({', '.join(_frontend_kws)}). "
788
+ "Boosted backend service-layer and interceptor files as likely root cause."
789
+ )
790
+ symptom_note = _synonym_note
791
+
711
792
  # ── 7. Test gaps (generate-tests only) ────────────────────────────
712
793
  test_gaps: list[str] = []
713
794
  if task_name == "generate-tests":
@@ -752,22 +833,30 @@ class TaskContextBuilder:
752
833
 
753
834
  conf_summary, analysis_gaps = ConfidenceAnalyzer().analyze(sm_for_conf)
754
835
  confidence = conf_summary.overall
755
- gaps = [g.reason for g in analysis_gaps]
756
- if _mybatis_warning:
757
- gaps.append(_mybatis_warning["reason"])
836
+ if task_name == "delta":
837
+ # Use delta-specific gaps; ConfidenceAnalyzer gaps are about full-repo
838
+ # detection quality and are not meaningful for an incremental diff.
839
+ gaps = _delta_analysis_gaps
840
+ if _mybatis_warning:
841
+ gaps.append(_mybatis_warning["reason"])
842
+ else:
843
+ gaps = [g.reason for g in analysis_gaps]
844
+ if _mybatis_warning:
845
+ gaps.append(_mybatis_warning["reason"])
758
846
 
759
847
  # ── 9. why_these_files ────────────────────────────────────────────────
760
- why_these_files: dict[str, str] = {
761
- rf.path: rf.reason for rf in relevant_files
762
- }
848
+ if task_name == "delta":
849
+ why_these_files = _delta_why
850
+ else:
851
+ why_these_files = {rf.path: rf.reason for rf in relevant_files}
763
852
 
764
- # ── 10. Delta: git changed files (reuse pre-computed set from step 5c) ──
853
+ # ── 10. Delta: git changed files + entry points ───────────────────────
765
854
  changed_files: list[str] = []
766
855
  affected_entry_points: list[str] = []
767
856
  if task_name == "delta":
768
857
  changed_files = sorted(_delta_files) if _delta_files else self._get_git_changed_files(since=since)
769
- ep_set = {ep.path for ep in entry_points}
770
- affected_entry_points = [f for f in changed_files if f in ep_set]
858
+ _ep_set = {ep.path for ep in entry_points}
859
+ affected_entry_points = [f for f in changed_files if f in _ep_set]
771
860
 
772
861
  return TaskOutput(
773
862
  task=task_name,
@@ -788,6 +877,11 @@ class TaskContextBuilder:
788
877
  affected_entry_points=affected_entry_points,
789
878
  symptom=symptom if task_name == "fix-bug" and symptom else None,
790
879
  related_notes=related_notes,
880
+ symptom_note=symptom_note,
881
+ impact_summary=_delta_impact_summary,
882
+ affected_modules=_delta_affected_modules,
883
+ risk_areas=_delta_risk_areas,
884
+ since=since if task_name == "delta" else None,
791
885
  )
792
886
 
793
887
  def render_prompt(self, output: TaskOutput) -> str:
@@ -1079,6 +1173,384 @@ class TaskContextBuilder:
1079
1173
  def _is_source(self, path: str) -> bool:
1080
1174
  return Path(path).suffix.lower() in _SOURCE_EXTENSIONS
1081
1175
 
1176
+ # ── Delta impact analysis ─────────────────────────────────────────────────
1177
+
1178
+ @staticmethod
1179
+ def _classify_changed_file(path: str) -> dict[str, Any]:
1180
+ """Classify a changed file by artifact type, risk areas, and impact level.
1181
+
1182
+ Returns dict: artifact_type, risk_areas, impact_level, is_noise, module.
1183
+ Pure path/name heuristics — no file reads, fully deterministic.
1184
+ """
1185
+ norm = path.replace("\\", "/")
1186
+ name = Path(path).name
1187
+ stem = Path(path).stem
1188
+ suffix = Path(path).suffix.lower()
1189
+ norm_lower = norm.lower()
1190
+ stem_lower = stem.lower()
1191
+ name_lower = name.lower()
1192
+
1193
+ _CODE_EXTS = frozenset({
1194
+ ".py", ".js", ".ts", ".tsx", ".jsx", ".java", ".kt", ".go",
1195
+ ".rs", ".rb", ".php", ".cs", ".dart", ".mjs", ".cjs", ".scala",
1196
+ })
1197
+ _CONFIG_EXTS = frozenset({
1198
+ ".yml", ".yaml", ".json", ".xml", ".toml", ".properties",
1199
+ ".env", ".cfg", ".ini", ".conf",
1200
+ })
1201
+
1202
+ # IDE/hidden-tool directories → noise, skip impact analysis
1203
+ _IDE_DIR_NAMES = frozenset({
1204
+ ".idea", ".vscode", ".eclipse", ".fleet", ".git", ".github",
1205
+ ".circleci", ".travis", ".teamcity", ".gradle", ".mvn",
1206
+ })
1207
+ path_dir_parts = norm_lower.split("/")[:-1] # all components except filename
1208
+ if any(part in _IDE_DIR_NAMES for part in path_dir_parts):
1209
+ return {
1210
+ "artifact_type": "ide_noise",
1211
+ "risk_areas": [],
1212
+ "impact_level": "noise",
1213
+ "is_noise": True,
1214
+ "module": "",
1215
+ }
1216
+
1217
+ module = _extract_ddd_domain(path)
1218
+
1219
+ # Tests (before other checks to avoid misclassifying TestFoo as service etc.)
1220
+ _is_test = (
1221
+ (stem_lower.startswith("test") and len(stem_lower) > 4)
1222
+ or (stem_lower.endswith("test") and len(stem_lower) > 4)
1223
+ or stem_lower.endswith("tests")
1224
+ or stem_lower.endswith("spec")
1225
+ or any(t in f"/{norm_lower}/" for t in (
1226
+ "/test/", "/tests/", "/spec/", "/specs/", "/__tests__/", "/it/",
1227
+ ))
1228
+ )
1229
+ if _is_test:
1230
+ return {"artifact_type": "test", "risk_areas": ["tests"], "impact_level": "low", "is_noise": False, "module": module}
1231
+
1232
+ # Security surface
1233
+ _SECURITY_KW = ("security", "auth", "jwt", "token", "permission", "role",
1234
+ "credential", "encrypt", "decrypt", "oauth", "saml", "ldap",
1235
+ "password", "secret")
1236
+ if suffix in _CODE_EXTS and any(kw in stem_lower for kw in _SECURITY_KW):
1237
+ impact = "critical" if any(kw in stem_lower for kw in ("security", "auth", "jwt")) else "high"
1238
+ return {"artifact_type": "security", "risk_areas": ["security"], "impact_level": impact, "is_noise": False, "module": module}
1239
+
1240
+ # API / controller layer
1241
+ _API_KW = ("controller", "restcontroller", "resource", "handler",
1242
+ "router", "route", "endpoint", "servlet")
1243
+ if suffix in _CODE_EXTS and any(kw in stem_lower for kw in _API_KW):
1244
+ return {"artifact_type": "api_endpoint", "risk_areas": ["api"], "impact_level": "high", "is_noise": False, "module": module}
1245
+
1246
+ # Business logic / services
1247
+ if suffix in _CODE_EXTS and "service" in stem_lower:
1248
+ return {"artifact_type": "business_logic", "risk_areas": ["transactions", "business_logic"], "impact_level": "high", "is_noise": False, "module": module}
1249
+
1250
+ # Data access
1251
+ _DAO_KW = ("repository", "repositoryimpl", "dao", "daoimpl", "store")
1252
+ if suffix in _CODE_EXTS and any(kw in stem_lower for kw in _DAO_KW):
1253
+ return {"artifact_type": "data_access", "risk_areas": ["persistence"], "impact_level": "high", "is_noise": False, "module": module}
1254
+ if "mapper" in stem_lower:
1255
+ atype = "mybatis_mapper" if suffix == ".xml" else "data_access"
1256
+ return {"artifact_type": atype, "risk_areas": ["persistence"], "impact_level": "high", "is_noise": False, "module": module}
1257
+
1258
+ # Spring / app config files (by canonical name)
1259
+ if name_lower in ("application.yml", "application.yaml", "application.properties",
1260
+ "bootstrap.yml", "bootstrap.yaml", "bootstrap.properties"):
1261
+ return {"artifact_type": "spring_config", "risk_areas": ["config"], "impact_level": "high", "is_noise": False, "module": module}
1262
+ if name_lower.startswith("application-") and suffix in (".yml", ".yaml", ".properties"):
1263
+ return {"artifact_type": "spring_profile", "risk_areas": ["config"], "impact_level": "medium", "is_noise": False, "module": module}
1264
+ if name_lower in ("pom.xml", "build.gradle", "build.gradle.kts",
1265
+ "settings.gradle", "settings.gradle.kts"):
1266
+ return {"artifact_type": "build_manifest", "risk_areas": ["config", "dependencies"], "impact_level": "medium", "is_noise": False, "module": module}
1267
+
1268
+ # Configuration classes / files
1269
+ _CONFIG_STEM_KW = ("config", "configuration", "properties", "settings")
1270
+ if suffix in _CODE_EXTS and any(kw in stem_lower for kw in _CONFIG_STEM_KW):
1271
+ return {"artifact_type": "configuration", "risk_areas": ["config"], "impact_level": "medium", "is_noise": False, "module": module}
1272
+
1273
+ # DB migrations / SQL
1274
+ if suffix == ".sql" or any(kw in norm_lower for kw in ("migration", "flyway", "liquibase", "changelog")):
1275
+ return {"artifact_type": "db_migration", "risk_areas": ["persistence"], "impact_level": "high", "is_noise": False, "module": module}
1276
+
1277
+ # Domain models / entities
1278
+ _ENTITY_KW = ("entity", "model", "domain", "aggregate", "valueobject")
1279
+ if suffix in _CODE_EXTS and any(kw in stem_lower for kw in _ENTITY_KW):
1280
+ return {"artifact_type": "domain_model", "risk_areas": ["persistence"], "impact_level": "medium", "is_noise": False, "module": module}
1281
+
1282
+ # DTOs / request-response objects
1283
+ _DTO_KW = ("dto", "request", "response", "payload", "command", "query", "event")
1284
+ if suffix in _CODE_EXTS and any(kw in stem_lower for kw in _DTO_KW):
1285
+ return {"artifact_type": "dto", "risk_areas": [], "impact_level": "low", "is_noise": False, "module": module}
1286
+
1287
+ # Generic source code
1288
+ if suffix in _CODE_EXTS:
1289
+ return {"artifact_type": "unknown_source", "risk_areas": [], "impact_level": "medium", "is_noise": False, "module": module}
1290
+
1291
+ # Config / data files
1292
+ if suffix in _CONFIG_EXTS:
1293
+ return {"artifact_type": "unknown_config", "risk_areas": ["config"], "impact_level": "low", "is_noise": False, "module": module}
1294
+
1295
+ # Docs
1296
+ if suffix in (".md", ".rst", ".txt", ".adoc"):
1297
+ return {"artifact_type": "documentation", "risk_areas": [], "impact_level": "low", "is_noise": False, "module": module}
1298
+
1299
+ return {"artifact_type": "binary_or_unknown", "risk_areas": [], "impact_level": "noise", "is_noise": True, "module": module}
1300
+
1301
+ def _build_delta_impact(
1302
+ self,
1303
+ changed_files: list[str],
1304
+ all_paths: list[str],
1305
+ entry_points: list,
1306
+ since: Optional[str],
1307
+ ) -> tuple[list[RelevantFile], str, list[str], list[dict[str, Any]], dict[str, str], list[str]]:
1308
+ """Build incremental impact analysis for changed files.
1309
+
1310
+ Returns:
1311
+ (relevant_files, impact_summary, affected_modules, risk_areas,
1312
+ why_these_files, analysis_gaps)
1313
+
1314
+ Changed files are always included in relevant_files (never dropped by score).
1315
+ Related files from the same module/directory are appended with lower scores.
1316
+ """
1317
+ _IMPACT_SCORE: dict[str, float] = {
1318
+ "critical": 1.00,
1319
+ "high": 0.85,
1320
+ "medium": 0.65,
1321
+ "low": 0.40,
1322
+ "noise": 0.10,
1323
+ }
1324
+ _SEV_ORDER = ["noise", "low", "medium", "high", "critical"]
1325
+
1326
+ if not changed_files:
1327
+ return (
1328
+ [],
1329
+ "No changes detected — verify the git ref passed to --since",
1330
+ [],
1331
+ [],
1332
+ {},
1333
+ ["No changed files found. Check that --since ref exists and the diff is non-empty."],
1334
+ )
1335
+
1336
+ ep_paths = {ep.path for ep in entry_points}
1337
+
1338
+ # ── Step 1: classify every changed file ───────────────────────────────
1339
+ classifications: dict[str, dict[str, Any]] = {
1340
+ f: self._classify_changed_file(f) for f in changed_files
1341
+ }
1342
+
1343
+ # ── Step 2: build relevant_files from the changed set ─────────────────
1344
+ relevant: list[RelevantFile] = []
1345
+ why: dict[str, str] = {}
1346
+ affected_modules_set: set[str] = set()
1347
+ changed_dirs: set[str] = set()
1348
+ risk_acc: dict[str, dict[str, Any]] = {} # area → {files, severity}
1349
+ ref_label = since or "HEAD~1"
1350
+
1351
+ for path, cls in classifications.items():
1352
+ score = _IMPACT_SCORE.get(cls["impact_level"], 0.50)
1353
+ module = cls["module"]
1354
+
1355
+ if module:
1356
+ affected_modules_set.add(module)
1357
+ if not cls["is_noise"]:
1358
+ parent = str(Path(path).parent).replace("\\", "/")
1359
+ if parent and parent != ".":
1360
+ changed_dirs.add(parent)
1361
+
1362
+ for area in cls["risk_areas"]:
1363
+ if area not in risk_acc:
1364
+ risk_acc[area] = {"files": [], "severity": "noise"}
1365
+ risk_acc[area]["files"].append(path)
1366
+ cur_idx = _SEV_ORDER.index(risk_acc[area]["severity"])
1367
+ new_idx = _SEV_ORDER.index(cls["impact_level"])
1368
+ if new_idx > cur_idx:
1369
+ risk_acc[area]["severity"] = cls["impact_level"]
1370
+
1371
+ artifact_display = cls["artifact_type"].replace("_", " ")
1372
+ reason_parts = [f"changed since {ref_label}", f"artifact: {cls['artifact_type']}"]
1373
+ if cls["risk_areas"]:
1374
+ reason_parts.append(f"risk: {', '.join(cls['risk_areas'])}")
1375
+ reason = ", ".join(reason_parts)
1376
+
1377
+ why_parts = [f"Changed {artifact_display}"]
1378
+ if module:
1379
+ why_parts.append(f"in module '{module}'")
1380
+ if cls["risk_areas"]:
1381
+ why_parts.append(f"Risk: {', '.join(cls['risk_areas'])}")
1382
+ why_str = ". ".join(why_parts) + "."
1383
+
1384
+ role = "entrypoint" if path in ep_paths else ("source" if not cls["is_noise"] else "noise")
1385
+ relevant.append(RelevantFile(path=path, role=role, score=round(score, 2), reason=reason, why=why_str))
1386
+ why[path] = why_str
1387
+
1388
+ relevant.sort(key=lambda f: (-f.score, f.path))
1389
+
1390
+ # ── Step 3: expand to related files (same module or same directory) ───
1391
+ existing_paths = {rf.path for rf in relevant}
1392
+
1393
+ _HIGH_IMPACT_STEMS = frozenset({
1394
+ "controller", "restcontroller", "resource", "handler",
1395
+ "service", "serviceimpl", "servicefacade",
1396
+ "repository", "repositoryimpl", "dao", "daoimpl",
1397
+ "mapper", "security", "securityconfig",
1398
+ "config", "configuration", "filter", "authcontroller",
1399
+ })
1400
+
1401
+ related: list[tuple[float, str, RelevantFile]] = []
1402
+ for path in all_paths:
1403
+ if path in existing_paths:
1404
+ continue
1405
+ suffix = Path(path).suffix.lower()
1406
+ if suffix not in _ALL_EXTENSIONS:
1407
+ continue
1408
+ stem_lower = Path(path).stem.lower()
1409
+ if not any(s in stem_lower for s in _HIGH_IMPACT_STEMS):
1410
+ continue
1411
+
1412
+ parent = str(Path(path).parent).replace("\\", "/")
1413
+ path_module = _extract_ddd_domain(path)
1414
+
1415
+ in_same_module = bool(path_module and path_module in affected_modules_set)
1416
+ in_same_dir = parent in changed_dirs
1417
+
1418
+ if not (in_same_module or in_same_dir):
1419
+ continue
1420
+
1421
+ rel_cls = self._classify_changed_file(path)
1422
+ if rel_cls["is_noise"]:
1423
+ continue
1424
+
1425
+ rel_score = _IMPACT_SCORE.get(rel_cls["impact_level"], 0.50) * 0.50
1426
+ ctx_type = "module" if in_same_module else "directory"
1427
+ ctx_val = path_module if in_same_module else parent
1428
+
1429
+ triggers = [
1430
+ Path(f).name for f in changed_files
1431
+ if (
1432
+ (_extract_ddd_domain(f) == path_module if in_same_module
1433
+ else str(Path(f).parent).replace("\\", "/") == parent)
1434
+ )
1435
+ ]
1436
+ reason = f"related: same {ctx_type} '{ctx_val}', artifact: {rel_cls['artifact_type']}"
1437
+ why_str = (
1438
+ f"In changed {ctx_type} '{ctx_val}'. "
1439
+ f"May be affected by: {', '.join(triggers[:3])}"
1440
+ )
1441
+ role = "entrypoint" if path in ep_paths else "source"
1442
+ related.append((rel_score, path, RelevantFile(
1443
+ path=path, role=role, score=round(rel_score, 2), reason=reason, why=why_str
1444
+ )))
1445
+ why[path] = why_str
1446
+
1447
+ related.sort(key=lambda x: (-x[0], x[1]))
1448
+ relevant.extend(rf for _, _, rf in related[:10])
1449
+
1450
+ # ── Step 4: impact summary ─────────────────────────────────────────────
1451
+ type_counts: dict[str, int] = {}
1452
+ all_risk_areas: set[str] = set()
1453
+ noise_count = 0
1454
+ for cls in classifications.values():
1455
+ t = cls["artifact_type"]
1456
+ type_counts[t] = type_counts.get(t, 0) + 1
1457
+ all_risk_areas.update(cls["risk_areas"])
1458
+ if cls["is_noise"]:
1459
+ noise_count += 1
1460
+ meaningful = len(changed_files) - noise_count
1461
+
1462
+ _SUMMARY_LABELS: dict[str, str] = {
1463
+ "security": "security file(s)",
1464
+ "api_endpoint": "API endpoint(s)",
1465
+ "business_logic": "service(s)",
1466
+ "data_access": "data access file(s)",
1467
+ "mybatis_mapper": "MyBatis mapper(s)",
1468
+ "spring_config": "Spring config file(s)",
1469
+ "spring_profile": "Spring profile config(s)",
1470
+ "configuration": "configuration file(s)",
1471
+ "build_manifest": "build manifest(s)",
1472
+ "db_migration": "database migration(s)",
1473
+ "domain_model": "domain model(s)",
1474
+ "dto": "DTO(s)",
1475
+ "test": "test file(s)",
1476
+ "unknown_source": "source file(s)",
1477
+ "unknown_config": "config file(s)",
1478
+ "documentation": "documentation file(s)",
1479
+ }
1480
+
1481
+ if meaningful == 0:
1482
+ impact_summary = (
1483
+ f"{noise_count} IDE/tooling file(s) changed"
1484
+ " — no semantic impact on application logic"
1485
+ )
1486
+ else:
1487
+ _sev_rank = {"critical": 4, "high": 3, "medium": 2, "low": 1, "noise": 0}
1488
+ parts = []
1489
+ for atype, count in sorted(
1490
+ type_counts.items(),
1491
+ key=lambda kv: -_sev_rank.get(
1492
+ classifications[next(
1493
+ (f for f in changed_files if classifications[f]["artifact_type"] == kv[0]),
1494
+ changed_files[0],
1495
+ )]["impact_level"], 0,
1496
+ ),
1497
+ ):
1498
+ if atype in ("ide_noise", "binary_or_unknown"):
1499
+ continue
1500
+ label = _SUMMARY_LABELS.get(atype, f"source file(s) ({atype})")
1501
+ parts.append(f"{count} {label}")
1502
+ impact_summary = "; ".join(parts) if parts else f"{meaningful} source file(s) changed"
1503
+ if all_risk_areas:
1504
+ impact_summary += f" — risk areas: {', '.join(sorted(all_risk_areas))}"
1505
+ if noise_count > 0:
1506
+ impact_summary += f" ({noise_count} IDE/tooling file(s) excluded)"
1507
+
1508
+ # ── Step 5: risk_areas output list ─────────────────────────────────────
1509
+ risk_areas_out: list[dict[str, Any]] = sorted(
1510
+ [
1511
+ {
1512
+ "area": area,
1513
+ "severity": info["severity"],
1514
+ "affected_files": sorted(info["files"])[:5],
1515
+ }
1516
+ for area, info in risk_acc.items()
1517
+ ],
1518
+ key=lambda x: (-_SEV_ORDER.index(x["severity"]), x["area"]),
1519
+ )
1520
+
1521
+ # ── Step 6: analysis gaps ──────────────────────────────────────────────
1522
+ analysis_gaps: list[str] = [
1523
+ "Related file expansion uses module/package and directory heuristics — import graph not traced",
1524
+ ]
1525
+ if noise_count > 0 and meaningful > 0:
1526
+ analysis_gaps.append(
1527
+ f"{noise_count} IDE/tooling file(s) in diff excluded from impact analysis"
1528
+ )
1529
+ elif noise_count > 0 and meaningful == 0:
1530
+ analysis_gaps.append(
1531
+ "All changed files are IDE/tooling — no actionable semantic impact detected"
1532
+ )
1533
+ unknown_sources = [f for f, cls in classifications.items() if cls["artifact_type"] == "unknown_source"]
1534
+ if unknown_sources:
1535
+ analysis_gaps.append(
1536
+ f"{len(unknown_sources)} source file(s) could not be classified by artifact type: "
1537
+ + ", ".join(Path(f).name for f in unknown_sources[:3])
1538
+ )
1539
+ if not affected_modules_set and any(not cls["is_noise"] for cls in classifications.values()):
1540
+ analysis_gaps.append(
1541
+ "DDD module/package structure not detected in changed paths"
1542
+ " — related file expansion uses directory proximity only"
1543
+ )
1544
+
1545
+ return (
1546
+ relevant,
1547
+ impact_summary,
1548
+ sorted(affected_modules_set),
1549
+ risk_areas_out,
1550
+ why,
1551
+ analysis_gaps,
1552
+ )
1553
+
1082
1554
  def _get_git_changed_files(self, since: Optional[str] = None) -> list[str]:
1083
1555
  """Get files changed since a git ref (default: HEAD~1) relative to self.root.
1084
1556