sourcecode 1.27.0__tar.gz → 1.29.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 (191) hide show
  1. sourcecode-1.29.0/.continue-here.md +108 -0
  2. {sourcecode-1.27.0 → sourcecode-1.29.0}/PKG-INFO +3 -3
  3. {sourcecode-1.27.0 → sourcecode-1.29.0}/README.md +2 -2
  4. {sourcecode-1.27.0 → sourcecode-1.29.0}/pyproject.toml +1 -1
  5. {sourcecode-1.27.0 → sourcecode-1.29.0}/src/sourcecode/__init__.py +1 -1
  6. {sourcecode-1.27.0 → sourcecode-1.29.0}/src/sourcecode/cli.py +58 -1
  7. {sourcecode-1.27.0 → sourcecode-1.29.0}/src/sourcecode/prepare_context.py +175 -12
  8. sourcecode-1.29.0/tests/test_encoding_regression.py +138 -0
  9. {sourcecode-1.27.0 → sourcecode-1.29.0}/tests/test_v1_10_regressions.py +39 -13
  10. sourcecode-1.27.0/.continue-here.md +0 -173
  11. sourcecode-1.27.0/tests/test_encoding_regression.py +0 -62
  12. {sourcecode-1.27.0 → sourcecode-1.29.0}/.agents/skills/source-command-gsd-join-discord/SKILL.md +0 -0
  13. {sourcecode-1.27.0 → sourcecode-1.29.0}/.agents/skills/source-command-gsd-review-backlog/SKILL.md +0 -0
  14. {sourcecode-1.27.0 → sourcecode-1.29.0}/.agents/skills/source-command-gsd-workstreams/SKILL.md +0 -0
  15. {sourcecode-1.27.0 → sourcecode-1.29.0}/.github/workflows/build-windows.yml +0 -0
  16. {sourcecode-1.27.0 → sourcecode-1.29.0}/.gitignore +0 -0
  17. {sourcecode-1.27.0 → sourcecode-1.29.0}/.ruff.toml +0 -0
  18. {sourcecode-1.27.0 → sourcecode-1.29.0}/CONTRIBUTING.md +0 -0
  19. {sourcecode-1.27.0 → sourcecode-1.29.0}/LICENSE +0 -0
  20. {sourcecode-1.27.0 → sourcecode-1.29.0}/SECURITY.md +0 -0
  21. {sourcecode-1.27.0 → sourcecode-1.29.0}/docs/privacy.md +0 -0
  22. {sourcecode-1.27.0 → sourcecode-1.29.0}/docs/schema.md +0 -0
  23. {sourcecode-1.27.0 → sourcecode-1.29.0}/raw +0 -0
  24. {sourcecode-1.27.0 → sourcecode-1.29.0}/run_cli.py +0 -0
  25. {sourcecode-1.27.0 → sourcecode-1.29.0}/src/sourcecode/adaptive_scanner.py +0 -0
  26. {sourcecode-1.27.0 → sourcecode-1.29.0}/src/sourcecode/architecture_analyzer.py +0 -0
  27. {sourcecode-1.27.0 → sourcecode-1.29.0}/src/sourcecode/architecture_summary.py +0 -0
  28. {sourcecode-1.27.0 → sourcecode-1.29.0}/src/sourcecode/ast_extractor.py +0 -0
  29. {sourcecode-1.27.0 → sourcecode-1.29.0}/src/sourcecode/classifier.py +0 -0
  30. {sourcecode-1.27.0 → sourcecode-1.29.0}/src/sourcecode/code_notes_analyzer.py +0 -0
  31. {sourcecode-1.27.0 → sourcecode-1.29.0}/src/sourcecode/confidence_analyzer.py +0 -0
  32. {sourcecode-1.27.0 → sourcecode-1.29.0}/src/sourcecode/context_scorer.py +0 -0
  33. {sourcecode-1.27.0 → sourcecode-1.29.0}/src/sourcecode/context_summarizer.py +0 -0
  34. {sourcecode-1.27.0 → sourcecode-1.29.0}/src/sourcecode/contract_model.py +0 -0
  35. {sourcecode-1.27.0 → sourcecode-1.29.0}/src/sourcecode/contract_pipeline.py +0 -0
  36. {sourcecode-1.27.0 → sourcecode-1.29.0}/src/sourcecode/coverage_parser.py +0 -0
  37. {sourcecode-1.27.0 → sourcecode-1.29.0}/src/sourcecode/dependency_analyzer.py +0 -0
  38. {sourcecode-1.27.0 → sourcecode-1.29.0}/src/sourcecode/detectors/__init__.py +0 -0
  39. {sourcecode-1.27.0 → sourcecode-1.29.0}/src/sourcecode/detectors/base.py +0 -0
  40. {sourcecode-1.27.0 → sourcecode-1.29.0}/src/sourcecode/detectors/csproj_parser.py +0 -0
  41. {sourcecode-1.27.0 → sourcecode-1.29.0}/src/sourcecode/detectors/dart.py +0 -0
  42. {sourcecode-1.27.0 → sourcecode-1.29.0}/src/sourcecode/detectors/dotnet.py +0 -0
  43. {sourcecode-1.27.0 → sourcecode-1.29.0}/src/sourcecode/detectors/elixir.py +0 -0
  44. {sourcecode-1.27.0 → sourcecode-1.29.0}/src/sourcecode/detectors/go.py +0 -0
  45. {sourcecode-1.27.0 → sourcecode-1.29.0}/src/sourcecode/detectors/heuristic.py +0 -0
  46. {sourcecode-1.27.0 → sourcecode-1.29.0}/src/sourcecode/detectors/hybrid.py +0 -0
  47. {sourcecode-1.27.0 → sourcecode-1.29.0}/src/sourcecode/detectors/java.py +0 -0
  48. {sourcecode-1.27.0 → sourcecode-1.29.0}/src/sourcecode/detectors/jvm_ext.py +0 -0
  49. {sourcecode-1.27.0 → sourcecode-1.29.0}/src/sourcecode/detectors/nodejs.py +0 -0
  50. {sourcecode-1.27.0 → sourcecode-1.29.0}/src/sourcecode/detectors/parsers.py +0 -0
  51. {sourcecode-1.27.0 → sourcecode-1.29.0}/src/sourcecode/detectors/php.py +0 -0
  52. {sourcecode-1.27.0 → sourcecode-1.29.0}/src/sourcecode/detectors/project.py +0 -0
  53. {sourcecode-1.27.0 → sourcecode-1.29.0}/src/sourcecode/detectors/python.py +0 -0
  54. {sourcecode-1.27.0 → sourcecode-1.29.0}/src/sourcecode/detectors/ruby.py +0 -0
  55. {sourcecode-1.27.0 → sourcecode-1.29.0}/src/sourcecode/detectors/rust.py +0 -0
  56. {sourcecode-1.27.0 → sourcecode-1.29.0}/src/sourcecode/detectors/systems.py +0 -0
  57. {sourcecode-1.27.0 → sourcecode-1.29.0}/src/sourcecode/detectors/terraform.py +0 -0
  58. {sourcecode-1.27.0 → sourcecode-1.29.0}/src/sourcecode/detectors/tooling.py +0 -0
  59. {sourcecode-1.27.0 → sourcecode-1.29.0}/src/sourcecode/doc_analyzer.py +0 -0
  60. {sourcecode-1.27.0 → sourcecode-1.29.0}/src/sourcecode/entrypoint_classifier.py +0 -0
  61. {sourcecode-1.27.0 → sourcecode-1.29.0}/src/sourcecode/env_analyzer.py +0 -0
  62. {sourcecode-1.27.0 → sourcecode-1.29.0}/src/sourcecode/file_classifier.py +0 -0
  63. {sourcecode-1.27.0 → sourcecode-1.29.0}/src/sourcecode/git_analyzer.py +0 -0
  64. {sourcecode-1.27.0 → sourcecode-1.29.0}/src/sourcecode/graph_analyzer.py +0 -0
  65. {sourcecode-1.27.0 → sourcecode-1.29.0}/src/sourcecode/metrics_analyzer.py +0 -0
  66. {sourcecode-1.27.0 → sourcecode-1.29.0}/src/sourcecode/progress.py +0 -0
  67. {sourcecode-1.27.0 → sourcecode-1.29.0}/src/sourcecode/ranking_engine.py +0 -0
  68. {sourcecode-1.27.0 → sourcecode-1.29.0}/src/sourcecode/redactor.py +0 -0
  69. {sourcecode-1.27.0 → sourcecode-1.29.0}/src/sourcecode/relevance_scorer.py +0 -0
  70. {sourcecode-1.27.0 → sourcecode-1.29.0}/src/sourcecode/repo_classifier.py +0 -0
  71. {sourcecode-1.27.0 → sourcecode-1.29.0}/src/sourcecode/runtime_classifier.py +0 -0
  72. {sourcecode-1.27.0 → sourcecode-1.29.0}/src/sourcecode/scanner.py +0 -0
  73. {sourcecode-1.27.0 → sourcecode-1.29.0}/src/sourcecode/schema.py +0 -0
  74. {sourcecode-1.27.0 → sourcecode-1.29.0}/src/sourcecode/semantic_analyzer.py +0 -0
  75. {sourcecode-1.27.0 → sourcecode-1.29.0}/src/sourcecode/serializer.py +0 -0
  76. {sourcecode-1.27.0 → sourcecode-1.29.0}/src/sourcecode/summarizer.py +0 -0
  77. {sourcecode-1.27.0 → sourcecode-1.29.0}/src/sourcecode/telemetry/__init__.py +0 -0
  78. {sourcecode-1.27.0 → sourcecode-1.29.0}/src/sourcecode/telemetry/config.py +0 -0
  79. {sourcecode-1.27.0 → sourcecode-1.29.0}/src/sourcecode/telemetry/consent.py +0 -0
  80. {sourcecode-1.27.0 → sourcecode-1.29.0}/src/sourcecode/telemetry/events.py +0 -0
  81. {sourcecode-1.27.0 → sourcecode-1.29.0}/src/sourcecode/telemetry/filters.py +0 -0
  82. {sourcecode-1.27.0 → sourcecode-1.29.0}/src/sourcecode/telemetry/transport.py +0 -0
  83. {sourcecode-1.27.0 → sourcecode-1.29.0}/src/sourcecode/tree_utils.py +0 -0
  84. {sourcecode-1.27.0 → sourcecode-1.29.0}/src/sourcecode/workspace.py +0 -0
  85. {sourcecode-1.27.0 → sourcecode-1.29.0}/tests/__init__.py +0 -0
  86. {sourcecode-1.27.0 → sourcecode-1.29.0}/tests/conftest.py +0 -0
  87. {sourcecode-1.27.0 → sourcecode-1.29.0}/tests/fixtures/coverage.xml +0 -0
  88. {sourcecode-1.27.0 → sourcecode-1.29.0}/tests/fixtures/fastapi_app/pyproject.toml +0 -0
  89. {sourcecode-1.27.0 → sourcecode-1.29.0}/tests/fixtures/fastapi_app/src/main.py +0 -0
  90. {sourcecode-1.27.0 → sourcecode-1.29.0}/tests/fixtures/go_service/cmd/api/main.go +0 -0
  91. {sourcecode-1.27.0 → sourcecode-1.29.0}/tests/fixtures/go_service/go.mod +0 -0
  92. {sourcecode-1.27.0 → sourcecode-1.29.0}/tests/fixtures/jacoco.xml +0 -0
  93. {sourcecode-1.27.0 → sourcecode-1.29.0}/tests/fixtures/latin1_sample.java +0 -0
  94. {sourcecode-1.27.0 → sourcecode-1.29.0}/tests/fixtures/latin1_sample_iso.java +0 -0
  95. {sourcecode-1.27.0 → sourcecode-1.29.0}/tests/fixtures/lcov.info +0 -0
  96. {sourcecode-1.27.0 → sourcecode-1.29.0}/tests/fixtures/nextjs_app/app/page.tsx +0 -0
  97. {sourcecode-1.27.0 → sourcecode-1.29.0}/tests/fixtures/nextjs_app/package.json +0 -0
  98. {sourcecode-1.27.0 → sourcecode-1.29.0}/tests/fixtures/nextjs_app/pnpm-lock.yaml +0 -0
  99. {sourcecode-1.27.0 → sourcecode-1.29.0}/tests/fixtures/pnpm_monorepo/apps/web/app/page.tsx +0 -0
  100. {sourcecode-1.27.0 → sourcecode-1.29.0}/tests/fixtures/pnpm_monorepo/apps/web/package.json +0 -0
  101. {sourcecode-1.27.0 → sourcecode-1.29.0}/tests/fixtures/pnpm_monorepo/packages/api/main.py +0 -0
  102. {sourcecode-1.27.0 → sourcecode-1.29.0}/tests/fixtures/pnpm_monorepo/packages/api/pyproject.toml +0 -0
  103. {sourcecode-1.27.0 → sourcecode-1.29.0}/tests/fixtures/pnpm_monorepo/pnpm-workspace.yaml +0 -0
  104. {sourcecode-1.27.0 → sourcecode-1.29.0}/tests/fixtures/spring_boot_minimal/pom.xml +0 -0
  105. {sourcecode-1.27.0 → sourcecode-1.29.0}/tests/fixtures/spring_boot_minimal/src/main/java/com/example/ddd/ausente/application/service/FindAusenteService.java +0 -0
  106. {sourcecode-1.27.0 → sourcecode-1.29.0}/tests/fixtures/spring_boot_minimal/src/main/java/com/example/ddd/ausente/domain/entities/Ausente.java +0 -0
  107. {sourcecode-1.27.0 → sourcecode-1.29.0}/tests/fixtures/spring_boot_minimal/src/main/java/com/example/ddd/ausente/infrastructure/rest/AusenteRestController.java +0 -0
  108. {sourcecode-1.27.0 → sourcecode-1.29.0}/tests/fixtures/spring_boot_minimal/src/main/java/com/example/ddd/autocoberturas/application/service/FindAutocoberturasService.java +0 -0
  109. {sourcecode-1.27.0 → sourcecode-1.29.0}/tests/fixtures/spring_boot_minimal/src/main/java/com/example/ddd/autocoberturas/domain/entities/Autocoberturas.java +0 -0
  110. {sourcecode-1.27.0 → sourcecode-1.29.0}/tests/fixtures/spring_boot_minimal/src/main/java/com/example/ddd/autocoberturas/infrastructure/rest/AutocoberturasRestController.java +0 -0
  111. {sourcecode-1.27.0 → sourcecode-1.29.0}/tests/fixtures/spring_boot_minimal/src/main/java/com/example/ddd/calendarioTrabajador/application/service/FindCalendarioTrabajadorService.java +0 -0
  112. {sourcecode-1.27.0 → sourcecode-1.29.0}/tests/fixtures/spring_boot_minimal/src/main/java/com/example/ddd/calendarioTrabajador/domain/entities/CalendarioTrabajador.java +0 -0
  113. {sourcecode-1.27.0 → sourcecode-1.29.0}/tests/fixtures/spring_boot_minimal/src/main/java/com/example/ddd/calendarioTrabajador/infrastructure/rest/CalendarioTrabajadorRestController.java +0 -0
  114. {sourcecode-1.27.0 → sourcecode-1.29.0}/tests/fixtures/spring_boot_minimal/src/main/java/com/example/ddd/departamento/application/service/FindDepartamentoService.java +0 -0
  115. {sourcecode-1.27.0 → sourcecode-1.29.0}/tests/fixtures/spring_boot_minimal/src/main/java/com/example/ddd/departamento/domain/entities/Departamento.java +0 -0
  116. {sourcecode-1.27.0 → sourcecode-1.29.0}/tests/fixtures/spring_boot_minimal/src/main/java/com/example/ddd/departamento/infrastructure/rest/DepartamentoRestController.java +0 -0
  117. {sourcecode-1.27.0 → sourcecode-1.29.0}/tests/fixtures/spring_boot_minimal/src/main/java/com/example/ddd/empleado/application/service/FindEmpleadoService.java +0 -0
  118. {sourcecode-1.27.0 → sourcecode-1.29.0}/tests/fixtures/spring_boot_minimal/src/main/java/com/example/ddd/empleado/domain/entities/Empleado.java +0 -0
  119. {sourcecode-1.27.0 → sourcecode-1.29.0}/tests/fixtures/spring_boot_minimal/src/main/java/com/example/ddd/empleado/infrastructure/rest/EmpleadoRestController.java +0 -0
  120. {sourcecode-1.27.0 → sourcecode-1.29.0}/tests/fixtures/spring_boot_minimal/src/main/java/com/example/demo/DemoApplication.java +0 -0
  121. {sourcecode-1.27.0 → sourcecode-1.29.0}/tests/fixtures/spring_boot_minimal/src/main/java/com/example/demo/config/FilterConfig.java +0 -0
  122. {sourcecode-1.27.0 → sourcecode-1.29.0}/tests/fixtures/spring_boot_minimal/src/main/java/com/example/demo/domain/Health.java +0 -0
  123. {sourcecode-1.27.0 → sourcecode-1.29.0}/tests/fixtures/spring_boot_minimal/src/main/java/com/example/demo/mapper/HealthMapper.java +0 -0
  124. {sourcecode-1.27.0 → sourcecode-1.29.0}/tests/fixtures/spring_boot_minimal/src/main/java/com/example/demo/repository/HealthRepository.java +0 -0
  125. {sourcecode-1.27.0 → sourcecode-1.29.0}/tests/fixtures/spring_boot_minimal/src/main/java/com/example/demo/service/HealthService.java +0 -0
  126. {sourcecode-1.27.0 → sourcecode-1.29.0}/tests/fixtures/spring_boot_minimal/src/main/java/com/example/demo/web/HealthRestController.java +0 -0
  127. {sourcecode-1.27.0 → sourcecode-1.29.0}/tests/fixtures/spring_boot_minimal/src/main/java/com/example/demo/web/NominaRestController.java +0 -0
  128. {sourcecode-1.27.0 → sourcecode-1.29.0}/tests/fixtures/spring_boot_minimal/src/main/resources/application-dev.yml +0 -0
  129. {sourcecode-1.27.0 → sourcecode-1.29.0}/tests/fixtures/spring_boot_minimal/src/main/resources/application.yml +0 -0
  130. {sourcecode-1.27.0 → sourcecode-1.29.0}/tests/fixtures/spring_boot_minimal/src/main/resources/mapper/HealthMapper.xml +0 -0
  131. {sourcecode-1.27.0 → sourcecode-1.29.0}/tests/test_architecture_analyzer.py +0 -0
  132. {sourcecode-1.27.0 → sourcecode-1.29.0}/tests/test_architecture_summary.py +0 -0
  133. {sourcecode-1.27.0 → sourcecode-1.29.0}/tests/test_ast_extractor.py +0 -0
  134. {sourcecode-1.27.0 → sourcecode-1.29.0}/tests/test_block1_reliability.py +0 -0
  135. {sourcecode-1.27.0 → sourcecode-1.29.0}/tests/test_block2_coverage.py +0 -0
  136. {sourcecode-1.27.0 → sourcecode-1.29.0}/tests/test_block5_quality.py +0 -0
  137. {sourcecode-1.27.0 → sourcecode-1.29.0}/tests/test_bug_fixes_v16.py +0 -0
  138. {sourcecode-1.27.0 → sourcecode-1.29.0}/tests/test_classifier.py +0 -0
  139. {sourcecode-1.27.0 → sourcecode-1.29.0}/tests/test_cli.py +0 -0
  140. {sourcecode-1.27.0 → sourcecode-1.29.0}/tests/test_code_notes_analyzer.py +0 -0
  141. {sourcecode-1.27.0 → sourcecode-1.29.0}/tests/test_context_scorer.py +0 -0
  142. {sourcecode-1.27.0 → sourcecode-1.29.0}/tests/test_contract_pipeline.py +0 -0
  143. {sourcecode-1.27.0 → sourcecode-1.29.0}/tests/test_coverage_parser.py +0 -0
  144. {sourcecode-1.27.0 → sourcecode-1.29.0}/tests/test_cross_consistency.py +0 -0
  145. {sourcecode-1.27.0 → sourcecode-1.29.0}/tests/test_dependency_analyzer_node_python.py +0 -0
  146. {sourcecode-1.27.0 → sourcecode-1.29.0}/tests/test_dependency_analyzer_polyglot.py +0 -0
  147. {sourcecode-1.27.0 → sourcecode-1.29.0}/tests/test_dependency_schema.py +0 -0
  148. {sourcecode-1.27.0 → sourcecode-1.29.0}/tests/test_detector_dotnet.py +0 -0
  149. {sourcecode-1.27.0 → sourcecode-1.29.0}/tests/test_detector_go_rust_java.py +0 -0
  150. {sourcecode-1.27.0 → sourcecode-1.29.0}/tests/test_detector_nodejs.py +0 -0
  151. {sourcecode-1.27.0 → sourcecode-1.29.0}/tests/test_detector_php_ruby_dart.py +0 -0
  152. {sourcecode-1.27.0 → sourcecode-1.29.0}/tests/test_detector_python.py +0 -0
  153. {sourcecode-1.27.0 → sourcecode-1.29.0}/tests/test_detector_universal_managed.py +0 -0
  154. {sourcecode-1.27.0 → sourcecode-1.29.0}/tests/test_detector_universal_systems.py +0 -0
  155. {sourcecode-1.27.0 → sourcecode-1.29.0}/tests/test_detectors_base.py +0 -0
  156. {sourcecode-1.27.0 → sourcecode-1.29.0}/tests/test_doc_analyzer_jsdom.py +0 -0
  157. {sourcecode-1.27.0 → sourcecode-1.29.0}/tests/test_doc_analyzer_python.py +0 -0
  158. {sourcecode-1.27.0 → sourcecode-1.29.0}/tests/test_graph_analyzer_polyglot.py +0 -0
  159. {sourcecode-1.27.0 → sourcecode-1.29.0}/tests/test_graph_analyzer_python_node.py +0 -0
  160. {sourcecode-1.27.0 → sourcecode-1.29.0}/tests/test_graph_schema.py +0 -0
  161. {sourcecode-1.27.0 → sourcecode-1.29.0}/tests/test_hybrid_inference.py +0 -0
  162. {sourcecode-1.27.0 → sourcecode-1.29.0}/tests/test_integration.py +0 -0
  163. {sourcecode-1.27.0 → sourcecode-1.29.0}/tests/test_integration_dependencies.py +0 -0
  164. {sourcecode-1.27.0 → sourcecode-1.29.0}/tests/test_integration_detection.py +0 -0
  165. {sourcecode-1.27.0 → sourcecode-1.29.0}/tests/test_integration_docs.py +0 -0
  166. {sourcecode-1.27.0 → sourcecode-1.29.0}/tests/test_integration_graph_modules.py +0 -0
  167. {sourcecode-1.27.0 → sourcecode-1.29.0}/tests/test_integration_lqn.py +0 -0
  168. {sourcecode-1.27.0 → sourcecode-1.29.0}/tests/test_integration_metrics.py +0 -0
  169. {sourcecode-1.27.0 → sourcecode-1.29.0}/tests/test_integration_multistack.py +0 -0
  170. {sourcecode-1.27.0 → sourcecode-1.29.0}/tests/test_integration_semantics.py +0 -0
  171. {sourcecode-1.27.0 → sourcecode-1.29.0}/tests/test_integration_universal.py +0 -0
  172. {sourcecode-1.27.0 → sourcecode-1.29.0}/tests/test_java_spring_integration.py +0 -0
  173. {sourcecode-1.27.0 → sourcecode-1.29.0}/tests/test_metrics_analyzer.py +0 -0
  174. {sourcecode-1.27.0 → sourcecode-1.29.0}/tests/test_packaging.py +0 -0
  175. {sourcecode-1.27.0 → sourcecode-1.29.0}/tests/test_phase1_improvements.py +0 -0
  176. {sourcecode-1.27.0 → sourcecode-1.29.0}/tests/test_pipeline_integrity.py +0 -0
  177. {sourcecode-1.27.0 → sourcecode-1.29.0}/tests/test_real_projects.py +0 -0
  178. {sourcecode-1.27.0 → sourcecode-1.29.0}/tests/test_redactor.py +0 -0
  179. {sourcecode-1.27.0 → sourcecode-1.29.0}/tests/test_scanner.py +0 -0
  180. {sourcecode-1.27.0 → sourcecode-1.29.0}/tests/test_schema.py +0 -0
  181. {sourcecode-1.27.0 → sourcecode-1.29.0}/tests/test_schema_normalization.py +0 -0
  182. {sourcecode-1.27.0 → sourcecode-1.29.0}/tests/test_semantic_analyzer_node.py +0 -0
  183. {sourcecode-1.27.0 → sourcecode-1.29.0}/tests/test_semantic_analyzer_python.py +0 -0
  184. {sourcecode-1.27.0 → sourcecode-1.29.0}/tests/test_semantic_import_resolution.py +0 -0
  185. {sourcecode-1.27.0 → sourcecode-1.29.0}/tests/test_semantic_schema.py +0 -0
  186. {sourcecode-1.27.0 → sourcecode-1.29.0}/tests/test_signal_hierarchy.py +0 -0
  187. {sourcecode-1.27.0 → sourcecode-1.29.0}/tests/test_summarizer.py +0 -0
  188. {sourcecode-1.27.0 → sourcecode-1.29.0}/tests/test_surface_honesty.py +0 -0
  189. {sourcecode-1.27.0 → sourcecode-1.29.0}/tests/test_task_differentiation.py +0 -0
  190. {sourcecode-1.27.0 → sourcecode-1.29.0}/tests/test_telemetry.py +0 -0
  191. {sourcecode-1.27.0 → sourcecode-1.29.0}/tests/test_workspace_analyzer.py +0 -0
@@ -0,0 +1,108 @@
1
+ # Continue Here — atlas-cli --agent token reduction
2
+
3
+ **Paused:** 2026-05-16 (sesión 4)
4
+ **Repo:** `/Users/user/Downloads/atlas-cli`
5
+ **Branch:** master
6
+ **Commit:** `f1cb001 corrigiendo bug --agent que duplicaba tokens sin justificación` (committed)
7
+
8
+ ---
9
+
10
+ ## Estado actual
11
+
12
+ Sesión 4: refactorización de `agent_view()` en `serializer.py` completada. **Todo committed.** Working tree limpio.
13
+
14
+ ---
15
+
16
+ ## Qué se hizo esta sesión
17
+
18
+ ### Problema resuelto
19
+
20
+ `--agent` producía ~11.337 tokens en SAS vs ~5.885 de `--compact`, con contenido casi idéntico. Objetivo: reducir tokens significativamente manteniendo solo lo que diferencia `--agent`.
21
+
22
+ ### Cambios en `agent_view()` (`src/sourcecode/serializer.py`)
23
+
24
+ | Cambio | Impacto |
25
+ |--------|---------|
26
+ | Elimina `production_dependencies` / `dev_tools` / `test_utilities` / `build_tooling` (dep_groups asdict completo) | Mayor fuente de bloat: -3.000-5.000 tokens en repos grandes |
27
+ | `key_dependencies`: de asdict+50 items → `name+version+role+risk_flags` solo, cap 20 | -500-1.500 tokens |
28
+ | Elimina `confidence_summary.hard_signals` / `soft_signals` / `ignored_signals` | -500-1.000 tokens |
29
+ | Elimina bloque `agent_mode` (contenido estático sin señal) | -80 tokens siempre |
30
+ | Elimina `signals.env_vars.keys[]` (duplica compact `env_map`) | -300-500 tokens |
31
+ | `signals.code_notes.top` cap 10→5, strip a `kind+path+line+text` | -200 tokens |
32
+ | Entry points (no-bootstrap): strip a `path+kind+confidence` (como compact) | reducción moderada |
33
+ | Elimina `development_entry_points` (noise para agentes de producción) | pequeña |
34
+ | `architecture.layers` capado a 5 | pequeña |
35
+
36
+ **Reducción estimada para SAS:** −40–60% tokens vs `--agent` anterior.
37
+
38
+ ### Campos que permanecen únicos de `--agent`
39
+
40
+ | Campo | Por qué justifica tokens extra vs `--compact` |
41
+ |-------|----------------------------------------------|
42
+ | `file_relevance` (top-20 scored) | Compact no tiene equivalente. Agente sabe qué leer sin explorar. |
43
+ | `architecture.layers` | Compact solo tiene `architecture_summary` string. |
44
+ | `suspicious_dependencies` | Deps declarados sin import observado. Compact no lo tiene. |
45
+ | `confidence_reasons` | Explicación accionable de secciones low-confidence. Compact no lo tiene. |
46
+
47
+ ### Tests actualizados
48
+
49
+ - `tests/test_block5_quality.py` — 5 tests de `agent_mode` reemplazados por tests de las nuevas garantías (no `agent_mode`, no `hard_signals`, no `env_vars.keys`, no dep_groups)
50
+ - `tests/test_pipeline_integrity.py` — `test_agent_splits_development_and_auxiliary_eps` → verifica que dev/aux EPs NO aparecen
51
+
52
+ ---
53
+
54
+ ## Tests al pausar
55
+
56
+ Suite completa (sin fallos pre-existentes): **todo verde**
57
+
58
+ Fallos pre-existentes (no relacionados, ya presentes antes):
59
+ - `test_block2_coverage.py::test_java_marked_unsupported` — DocRecord bug en doc_analyzer.py
60
+ - `test_dependency_analyzer_node_python.py::test_python_requirements_without_lockfile` — versión typer mismatch
61
+
62
+ ---
63
+
64
+ ## Pendiente / próximos pasos opcionales
65
+
66
+ 1. **Smoke test en saint-server** para medir tokens reales antes/después:
67
+ ```bash
68
+ sourcecode saint-server --agent --output /tmp/agent_new.json
69
+ python3 -c "import json,sys; d=json.load(open('/tmp/agent_new.json')); print('agent tokens (est):', len(json.dumps(d))//4)"
70
+ ```
71
+
72
+ 2. **Verificar que `suspicious_dependencies` es útil en repos reales** — en repos Python/JS (no JVM, donde el index de imports no funciona). En JVM siempre vacío por diseño.
73
+
74
+ 3. **Ajustar caps según medición real**: si `file_relevance` (20 files) sigue siendo el mayor contribuyente de tokens, considerar reducir a 15. Medir con `--full` vs default.
75
+
76
+ 4. **Smoke tests delta pendientes** de sesión anterior — ver sección "Smoke tests sesión 3" en historial de commits.
77
+
78
+ ---
79
+
80
+ ## Para retomar
81
+
82
+ ```bash
83
+ cd /Users/user/Downloads/atlas-cli
84
+ git log --oneline -3
85
+ # debe mostrar: f1cb001 corrigiendo bug --agent que duplicaba tokens sin justificación
86
+
87
+ python3 -m pytest tests/ \
88
+ --ignore=tests/test_block2_coverage.py \
89
+ --deselect=tests/test_dependency_analyzer_node_python.py::test_python_requirements_without_lockfile_keeps_declared_versions \
90
+ -q
91
+ # Expected: todo verde
92
+
93
+ # Verificación rápida agent_view
94
+ python3 -c "
95
+ from sourcecode.serializer import agent_view
96
+ from sourcecode.schema import SourceMap, StackInfo, SourceMapMetadata
97
+ sm = SourceMap(stacks=[StackInfo(stack='python', primary=True)], project_type='python', project_summary='test')
98
+ r = agent_view(sm)
99
+ assert 'agent_mode' not in r
100
+ assert 'production_dependencies' not in r
101
+ assert 'hard_signals' not in r.get('confidence_summary', {})
102
+ print('OK — agent_mode/dep_groups/hard_signals all absent')
103
+ "
104
+ ```
105
+
106
+ ---
107
+
108
+ *Pausado 2026-05-16 sesión 4 — gsd:pause-work*
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: sourcecode
3
- Version: 1.27.0
3
+ Version: 1.29.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.27.0-blue)
224
+ ![Version](https://img.shields.io/badge/version-1.29.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.27.0
258
+ # sourcecode 1.29.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.27.0-blue)
5
+ ![Version](https://img.shields.io/badge/version-1.29.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.27.0
39
+ # sourcecode 1.29.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.27.0"
7
+ version = "1.29.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.27.0"
3
+ __version__ = "1.29.0"
@@ -1633,7 +1633,7 @@ def prepare_context_cmd(
1633
1633
  refactor Structural issues, improvement opportunities
1634
1634
  generate-tests Untested source files, test gap analysis
1635
1635
  onboard Full project context for new agents/developers
1636
- review-pr Changed files + architectural impact
1636
+ review-pr PR diff: changed files, security/transactional impact, test gaps (requires git diff or --since)
1637
1637
  delta Incremental context: git-changed files only
1638
1638
 
1639
1639
  \b
@@ -1642,6 +1642,8 @@ def prepare_context_cmd(
1642
1642
  sourcecode prepare-context explain /path/to/repo
1643
1643
  sourcecode prepare-context fix-bug
1644
1644
  sourcecode prepare-context delta --since main
1645
+ sourcecode prepare-context review-pr --since origin/main
1646
+ sourcecode prepare-context review-pr . --since main --output review.json
1645
1647
  sourcecode prepare-context onboard --llm-prompt
1646
1648
  sourcecode prepare-context --task-help
1647
1649
  """
@@ -1734,6 +1736,14 @@ def prepare_context_cmd(
1734
1736
  "test_gaps": False, "code_notes_summary": False,
1735
1737
  "changed_files": True, "affected_entry_points": True,
1736
1738
  },
1739
+ "review-pr": {
1740
+ "project_summary": False, "architecture_summary": False,
1741
+ "relevant_files": True, "key_dependencies": False,
1742
+ "gaps": True, "confidence": False,
1743
+ "suspected_areas": False, "improvement_opportunities": False,
1744
+ "test_gaps": False, "code_notes_summary": False,
1745
+ "changed_files": True, "affected_entry_points": True,
1746
+ },
1737
1747
  }
1738
1748
  _content_filter = _TASK_CONTENT_MAP.get(task, {})
1739
1749
 
@@ -1813,6 +1823,53 @@ def prepare_context_cmd(
1813
1823
  out["dependency_graph_summary"] = output.dependency_graph_summary
1814
1824
  if output.impact_score_per_file:
1815
1825
  out["impact_score_per_file"] = output.impact_score_per_file
1826
+ # review-pr specific fields
1827
+ if task == "review-pr":
1828
+ if output.error_code:
1829
+ _err_out: dict[str, Any] = {
1830
+ "task": output.task,
1831
+ "ci_decision": output.ci_decision or "error",
1832
+ "error": output.error_code,
1833
+ "message": output.error_message,
1834
+ }
1835
+ if output.since:
1836
+ _err_out["since"] = output.since
1837
+ if output.error_hints:
1838
+ _err_out["hint"] = output.error_hints
1839
+ _err_json = json.dumps(_err_out, indent=2, ensure_ascii=False)
1840
+ if output_path is not None:
1841
+ output_path.write_text(_err_json, encoding="utf-8")
1842
+ else:
1843
+ import sys as _sys
1844
+ _sys.stdout.buffer.write(_err_json.encode("utf-8"))
1845
+ _sys.stdout.buffer.write(b"\n")
1846
+ _sys.stdout.buffer.flush()
1847
+ raise typer.Exit(code=1)
1848
+ out["review_type"] = "pull_request"
1849
+ if output.ci_decision:
1850
+ out["ci_decision"] = output.ci_decision
1851
+ if output.base_ref:
1852
+ out["base_ref"] = output.base_ref
1853
+ if output.since:
1854
+ out["since"] = output.since
1855
+ if output.affected_modules:
1856
+ out["affected_modules"] = output.affected_modules
1857
+ if output.security_impact:
1858
+ out["security_impact"] = output.security_impact
1859
+ if output.transactional_impact:
1860
+ out["transactional_impact"] = output.transactional_impact
1861
+ if output.configuration_impact:
1862
+ out["configuration_impact"] = output.configuration_impact
1863
+ if output.test_coverage_risk:
1864
+ out["test_coverage_risk"] = output.test_coverage_risk
1865
+ if output.review_hotspots:
1866
+ out["review_hotspots"] = output.review_hotspots
1867
+ if output.suggested_review_order:
1868
+ out["suggested_review_order"] = output.suggested_review_order
1869
+ if output.impact_summary:
1870
+ out["impact_summary"] = output.impact_summary
1871
+ if output.why_these_files:
1872
+ out["reasoning"] = output.why_these_files
1816
1873
  if output.limitations:
1817
1874
  out["limitations"] = output.limitations
1818
1875
  if output.symptom:
@@ -338,11 +338,19 @@ class TaskOutput:
338
338
  error_message: Optional[str] = None
339
339
  error_hints: list[str] = field(default_factory=list)
340
340
  # CI decision state machine — machine-decidable signal
341
- ci_decision: Optional[str] = None # "no_changes" | "analysis_success" | "git_ref_error"
341
+ ci_decision: Optional[str] = None # "no_changes" | "analysis_success" | "git_ref_error" | "no_git_repo"
342
342
  # git baseline resolution metadata
343
343
  resolved_since_ref: Optional[str] = None # actual ref/hash used for the diff
344
344
  resolution_path: Optional[str] = None # "exact_local_ref"|"remote_tracking_ref"|"symbolic_ref"|"head_minus_1_fallback"|"uncommitted_changes"|"unresolvable"
345
345
  diff_validation_status: Optional[str] = None # "valid_non_empty"|"valid_empty"|"invalid_ref"
346
+ # review-pr specific impact sections
347
+ base_ref: Optional[str] = None
348
+ security_impact: dict = field(default_factory=dict)
349
+ transactional_impact: dict = field(default_factory=dict)
350
+ configuration_impact: dict = field(default_factory=dict)
351
+ test_coverage_risk: dict = field(default_factory=dict)
352
+ review_hotspots: list[str] = field(default_factory=list)
353
+ suggested_review_order: list[str] = field(default_factory=list)
346
354
 
347
355
 
348
356
  # ─────────────────────────────────────────────────────────────────────────────
@@ -659,6 +667,58 @@ class TaskContextBuilder:
659
667
  elif _delta_raw:
660
668
  _delta_files = set(_delta_raw)
661
669
 
670
+ # ── 5d. review-pr: git-first gate ──────────────────────────────────────
671
+ if task_name == "review-pr":
672
+ if not self._is_git_repo():
673
+ return TaskOutput(
674
+ task="review-pr", goal=spec.goal,
675
+ project_summary=None, architecture_summary=None,
676
+ relevant_files=[], suspected_areas=[],
677
+ improvement_opportunities=[], test_gaps=[],
678
+ key_dependencies=[], code_notes_summary=None,
679
+ limitations=[], confidence="low",
680
+ error_code="no_git_repo",
681
+ error_message="review-pr requires a git repository.",
682
+ ci_decision="no_git_repo",
683
+ )
684
+ _pr_raw = self._get_git_changed_files(since=since)
685
+ if _pr_raw is None:
686
+ _avail_pr, _sug_pr = self._get_available_refs(since or "")
687
+ _pr_hints: list[str] = []
688
+ if _sug_pr:
689
+ _pr_hints.append(f"Did you mean '{_sug_pr}'?")
690
+ if _avail_pr:
691
+ _pr_hints.append(f"Available refs: {', '.join(_avail_pr[:8])}")
692
+ return TaskOutput(
693
+ task="review-pr", goal=spec.goal,
694
+ project_summary=None, architecture_summary=None,
695
+ relevant_files=[], suspected_areas=[],
696
+ improvement_opportunities=[], test_gaps=[],
697
+ key_dependencies=[], code_notes_summary=None,
698
+ limitations=[], confidence="low",
699
+ since=since,
700
+ error_code="git_ref_not_found",
701
+ error_message=f"Base ref '{since}' not found in this repository.",
702
+ error_hints=_pr_hints,
703
+ gaps=[f"Cannot compute PR diff: git ref '{since}' not found."] + _pr_hints,
704
+ ci_decision="git_ref_error",
705
+ )
706
+ if not _pr_raw:
707
+ _no_diff_hint = "review-pr requires changed files or --since <ref>."
708
+ return TaskOutput(
709
+ task="review-pr", goal=spec.goal,
710
+ project_summary=None, architecture_summary=None,
711
+ relevant_files=[], suspected_areas=[],
712
+ improvement_opportunities=[], test_gaps=[],
713
+ key_dependencies=[], code_notes_summary=None,
714
+ limitations=[], confidence="low",
715
+ error_code="no_diff",
716
+ error_message=f"No PR diff detected. {_no_diff_hint}",
717
+ gaps=[f"No PR diff detected. {_no_diff_hint}"],
718
+ ci_decision="no_changes",
719
+ )
720
+ _delta_files = set(_pr_raw)
721
+
662
722
  # ── 5c. review-pr suspected_areas (needs git uncommitted_files) ──────
663
723
  if task_name == "review-pr" and spec.enable_code_notes:
664
724
  pr_areas: dict[str, int] = {}
@@ -698,7 +758,7 @@ class TaskContextBuilder:
698
758
  _delta_dep_graph_summary: dict = {}
699
759
  _delta_impact_score_per_file: dict = {}
700
760
 
701
- if task_name == "delta":
761
+ if task_name in ("delta", "review-pr"):
702
762
  _delta_changed_list: list[str] = sorted(_delta_files) if _delta_files else []
703
763
  (
704
764
  relevant_files,
@@ -727,7 +787,88 @@ class TaskContextBuilder:
727
787
  delta_files=None,
728
788
  )
729
789
 
730
- # ── 6b. Symptom keyword boost + related notes (fix-bug + --symptom) ──
790
+ # ── 6b. review-pr: derive PR-specific impact sections from delta analysis ──
791
+ _pr_security_impact: dict = {}
792
+ _pr_transactional_impact: dict = {}
793
+ _pr_configuration_impact: dict = {}
794
+ _pr_test_coverage_risk: dict = {}
795
+ _pr_review_hotspots: list[str] = []
796
+ _pr_suggested_review_order: list[str] = []
797
+ _pr_base_ref: Optional[str] = None
798
+
799
+ if task_name == "review-pr":
800
+ _pr_base_ref = since or "HEAD"
801
+ _sys_risk_areas = _delta_system_impact.get("risk_areas", [])
802
+
803
+ _security_files = [
804
+ f for ra in _sys_risk_areas if ra["area"] == "security"
805
+ for f in ra["affected_files"]
806
+ ]
807
+ _transaction_files = [
808
+ f for ra in _sys_risk_areas if ra["area"] in ("transactions", "business_logic")
809
+ for f in ra["affected_files"]
810
+ ]
811
+ _config_files = [
812
+ f for ra in _sys_risk_areas if ra["area"] in ("api", "config")
813
+ for f in ra["affected_files"]
814
+ if any(kw in f.lower() for kw in ("config", "properties", "yml", "yaml", "xml", "spring"))
815
+ ]
816
+
817
+ if _security_files:
818
+ _pr_security_impact = {
819
+ "affected_resources": _security_files,
820
+ "risk_level": "high",
821
+ }
822
+ if _transaction_files:
823
+ _pr_transactional_impact = {
824
+ "affected_transactions": _transaction_files,
825
+ "risk": "possible transaction boundary change",
826
+ }
827
+ if _config_files:
828
+ _pr_configuration_impact = {"changed_configs": _config_files}
829
+
830
+ # Test coverage risk scoped to changed source files only
831
+ _changed_src = [
832
+ f for f in sorted(_delta_files or set())
833
+ if not self._is_test(f) and self._is_source(f)
834
+ ]
835
+ _test_stems = {Path(p).stem for p in test_set}
836
+ _untested_changed = [f for f in _changed_src if Path(f).stem not in _test_stems]
837
+ _test_risk_level = (
838
+ "high" if len(_untested_changed) > 3
839
+ else "medium" if _untested_changed
840
+ else "low"
841
+ )
842
+ _pr_test_coverage_risk = {
843
+ "changed_files_without_tests": _untested_changed[:10],
844
+ "risk_level": _test_risk_level,
845
+ }
846
+
847
+ # Review hotspots: top changed files ranked by impact score
848
+ _pr_review_hotspots = sorted(
849
+ _delta_files or set(),
850
+ key=lambda f: _delta_impact_score_per_file.get(f, 0.0),
851
+ reverse=True,
852
+ )[:8]
853
+
854
+ # Suggested review order: security first, then api → service → persistence → config
855
+ _ORDER_TYPES = ["security", "controller", "service", "repository", "mapper",
856
+ "spring_config", "config", "domain_model", "dto"]
857
+ _seen_order: set[str] = set()
858
+ for _otype in _ORDER_TYPES:
859
+ for _ra in _delta_risk_areas:
860
+ for _f in _ra.get("affected_files", []):
861
+ if _f not in _seen_order:
862
+ _cls = self._classify_changed_file(_f)
863
+ if _cls["artifact_type"] == _otype:
864
+ _pr_suggested_review_order.append(_f)
865
+ _seen_order.add(_f)
866
+ for _f in _pr_review_hotspots:
867
+ if _f not in _seen_order:
868
+ _pr_suggested_review_order.append(_f)
869
+ _seen_order.add(_f)
870
+
871
+ # ── 6c. Symptom keyword boost + related notes (fix-bug + --symptom) ──
731
872
  symptom_keywords: list[str] = []
732
873
  related_notes: list[dict] = []
733
874
  symptom_note: Optional[str] = None
@@ -883,7 +1024,7 @@ class TaskContextBuilder:
883
1024
 
884
1025
  conf_summary, analysis_gaps = ConfidenceAnalyzer().analyze(sm_for_conf)
885
1026
  confidence = conf_summary.overall
886
- if task_name == "delta":
1027
+ if task_name in ("delta", "review-pr"):
887
1028
  # Use delta-specific gaps; ConfidenceAnalyzer gaps are about full-repo
888
1029
  # detection quality and are not meaningful for an incremental diff.
889
1030
  gaps = _delta_analysis_gaps
@@ -895,16 +1036,16 @@ class TaskContextBuilder:
895
1036
  gaps.append(_mybatis_warning["reason"])
896
1037
 
897
1038
  # ── 9. why_these_files ────────────────────────────────────────────────
898
- if task_name == "delta":
1039
+ if task_name in ("delta", "review-pr"):
899
1040
  why_these_files = _delta_why
900
1041
  else:
901
1042
  why_these_files = {rf.path: rf.reason for rf in relevant_files}
902
1043
 
903
- # ── 10. Delta: git changed files + entry points ───────────────────────
1044
+ # ── 10. Delta / review-pr: git changed files + entry points ──────────
904
1045
  changed_files: list[str] = []
905
1046
  affected_entry_points: list[str] = []
906
- if task_name == "delta":
907
- changed_files = sorted(_delta_files) if _delta_files else self._get_git_changed_files(since=since)
1047
+ if task_name in ("delta", "review-pr"):
1048
+ changed_files = sorted(_delta_files) if _delta_files else (self._get_git_changed_files(since=since) or [])
908
1049
  _ep_set = {ep.path for ep in entry_points}
909
1050
  # include framework-detected entry points AND files classified as
910
1051
  # entrypoint/controller/security by artifact taxonomy
@@ -939,16 +1080,24 @@ class TaskContextBuilder:
939
1080
  impact_summary=_delta_impact_summary,
940
1081
  affected_modules=_delta_affected_modules,
941
1082
  risk_areas=_delta_risk_areas,
942
- since=since if task_name == "delta" else None,
1083
+ since=since if task_name in ("delta", "review-pr") else None,
943
1084
  system_impact=_delta_system_impact,
944
1085
  change_type=_delta_change_type,
945
1086
  dependency_graph_summary=_delta_dep_graph_summary,
946
1087
  impact_score_per_file=_delta_impact_score_per_file,
947
1088
  ci_decision=(
948
1089
  "no_changes" if task_name == "delta" and not changed_files
949
- else "analysis_success" if task_name == "delta"
1090
+ else "analysis_success" if task_name in ("delta", "review-pr")
950
1091
  else None
951
1092
  ),
1093
+ # review-pr specific
1094
+ base_ref=_pr_base_ref,
1095
+ security_impact=_pr_security_impact,
1096
+ transactional_impact=_pr_transactional_impact,
1097
+ configuration_impact=_pr_configuration_impact,
1098
+ test_coverage_risk=_pr_test_coverage_risk,
1099
+ review_hotspots=_pr_review_hotspots,
1100
+ suggested_review_order=_pr_suggested_review_order,
952
1101
  )
953
1102
 
954
1103
  def render_prompt(self, output: TaskOutput) -> str:
@@ -1240,6 +1389,19 @@ class TaskContextBuilder:
1240
1389
  def _is_source(self, path: str) -> bool:
1241
1390
  return Path(path).suffix.lower() in _SOURCE_EXTENSIONS
1242
1391
 
1392
+ def _is_git_repo(self) -> bool:
1393
+ import subprocess
1394
+ try:
1395
+ r = subprocess.run(
1396
+ ["git", "rev-parse", "--git-dir"],
1397
+ cwd=str(self.root),
1398
+ capture_output=True, text=True,
1399
+ encoding="utf-8", errors="replace", timeout=5,
1400
+ )
1401
+ return r.returncode == 0
1402
+ except (subprocess.TimeoutExpired, FileNotFoundError):
1403
+ return False
1404
+
1243
1405
  # ── Delta impact analysis ─────────────────────────────────────────────────
1244
1406
 
1245
1407
  @staticmethod
@@ -2309,10 +2471,11 @@ class TaskContextBuilder:
2309
2471
  r = subprocess.run(
2310
2472
  ["git", "branch", "-a", "--format=%(refname:short)"],
2311
2473
  cwd=str(self.root),
2312
- capture_output=True, text=True, timeout=5,
2474
+ capture_output=True, text=True,
2475
+ encoding="utf-8", errors="replace", timeout=5,
2313
2476
  )
2314
2477
  if r.returncode == 0:
2315
- all_refs = [b.strip() for b in r.stdout.splitlines() if b.strip()]
2478
+ all_refs = [b.strip() for b in (r.stdout or "").splitlines() if b.strip()]
2316
2479
  branches = [b for b in all_refs if "HEAD" not in b][:10]
2317
2480
  ref_lower = invalid_ref.lower()
2318
2481
  if ref_lower == "master" and any(b.rstrip("/").endswith("main") for b in all_refs):
@@ -0,0 +1,138 @@
1
+ """Regression tests for C3 — UTF-8 double-encoding of accented characters.
2
+
3
+ Fixture files:
4
+ - tests/fixtures/latin1_sample_iso.java — Latin-1 encoded (bytes 0xe1, 0xf3, etc.)
5
+ - tests/fixtures/latin1_sample.java — UTF-8 encoded (multibyte sequences)
6
+
7
+ Both should decode cleanly to the correct Unicode characters.
8
+ The old code read Latin-1 files with errors='replace', producing double-encoded garbage.
9
+ """
10
+ from __future__ import annotations
11
+
12
+ from pathlib import Path
13
+
14
+ import pytest
15
+
16
+ from sourcecode.tree_utils import safe_read_text
17
+
18
+ FIXTURES = Path(__file__).parent / "fixtures"
19
+ LATIN1_FILE = FIXTURES / "latin1_sample_iso.java"
20
+ UTF8_FILE = FIXTURES / "latin1_sample.java"
21
+
22
+
23
+ class TestSafeReadText:
24
+ """safe_read_text must decode both UTF-8 and Latin-1 files correctly."""
25
+
26
+ def test_utf8_file_reads_correctly(self):
27
+ content = safe_read_text(UTF8_FILE)
28
+ # These characters must appear verbatim — not as double-encoded sequences
29
+ assert "días" in content, f"Expected 'días', got: {content[:200]!r}"
30
+ assert "ñ" in content or "función" in content or "ñoño" in content
31
+ assert "Ã" not in content, "Double-encoded: U+00C3 found (Ã). Encoding was mangled."
32
+
33
+ def test_latin1_file_reads_correctly(self):
34
+ content = safe_read_text(LATIN1_FILE)
35
+ # Latin-1 0xe1 = 'á' — must decode to the correct character
36
+ assert "á" in content, f"Expected 'á', got: {content!r}"
37
+ assert "ó" in content, f"Expected 'ó', got: {content!r}"
38
+ # No replacement chars
39
+ assert "�" not in content, "Replacement character found — file was not decoded correctly."
40
+ # No double-encoding: 'á' in double-encoded UTF-8-as-Latin-1 would appear as 'á'
41
+ assert "Ã" not in content, "Double-encoded: U+00C3 found (Ã). Latin-1 file was not decoded correctly."
42
+
43
+ def test_nonexistent_file_raises_oserror(self):
44
+ with pytest.raises(OSError):
45
+ safe_read_text(FIXTURES / "does_not_exist.java")
46
+
47
+
48
+ class TestCodeNotesEncoding:
49
+ """CodeNotesAnalyzer must extract notes from Latin-1 files without encoding corruption."""
50
+
51
+ def test_latin1_todo_extracted_cleanly(self):
52
+ from sourcecode.code_notes_analyzer import CodeNotesAnalyzer
53
+
54
+ # analyze() scans FIXTURES dir — Latin-1 file is there
55
+ analyzer = CodeNotesAnalyzer()
56
+ notes, _, _ = analyzer.analyze(FIXTURES)
57
+ # Should have extracted TODO without replacement chars
58
+ todo_texts = [n.text for n in notes if "latin1_sample_iso" in n.path]
59
+ for text in todo_texts:
60
+ assert "â" not in text, f"Double-encoded byte found in TODO text: {text!r}"
61
+ assert "Ã" not in text, f"Double-encoded char in TODO text: {text!r}"
62
+ assert "�" not in text, f"Replacement char in TODO text: {text!r}"
63
+
64
+
65
+ class TestGetAvailableRefsWindowsEncoding:
66
+ """Regression: _get_available_refs crashed on Windows due to cp1252 encoding and None stdout.
67
+
68
+ Bug: subprocess.run(text=True) without encoding= uses the OS default (cp1252 on Windows).
69
+ Git output containing byte 0x8d raises UnicodeDecodeError (not caught). If stdout is None,
70
+ r.stdout.splitlines() raises AttributeError. Both cause unhandled tracebacks.
71
+ Fix: encoding="utf-8", errors="replace", and (r.stdout or "").splitlines().
72
+ """
73
+
74
+ def _make_builder(self) -> "TaskContextBuilder":
75
+ from sourcecode.prepare_context import TaskContextBuilder
76
+ return TaskContextBuilder(FIXTURES)
77
+
78
+ def test_case_a_stdout_none_does_not_crash(self, monkeypatch):
79
+ """stdout=None must not raise AttributeError on splitlines()."""
80
+ import subprocess
81
+ import types
82
+
83
+ fake = types.SimpleNamespace(returncode=0, stdout=None, stderr="")
84
+ monkeypatch.setattr(subprocess, "run", lambda *a, **kw: fake)
85
+
86
+ builder = self._make_builder()
87
+ refs, suggested = builder._get_available_refs("main")
88
+ assert refs == []
89
+ assert suggested is None
90
+
91
+ def test_case_b_non_utf8_bytes_no_unicode_error(self, monkeypatch):
92
+ """Bytes invalid in cp1252 (e.g. 0x8d) must not raise UnicodeDecodeError.
93
+
94
+ With encoding='utf-8' + errors='replace', replacement chars appear instead.
95
+ """
96
+ import subprocess
97
+ import types
98
+
99
+ # Simulate git returning a branch name that survived errors="replace" decoding
100
+ # (the replacement char � stands in for the bad byte)
101
+ replaced = "develop\nfeature/caf�\n"
102
+ fake = types.SimpleNamespace(returncode=0, stdout=replaced, stderr="")
103
+ monkeypatch.setattr(subprocess, "run", lambda *a, **kw: fake)
104
+
105
+ builder = self._make_builder()
106
+ refs, _ = builder._get_available_refs("main")
107
+ assert "develop" in refs
108
+
109
+ def test_case_c_nonexistent_ref_returns_available_refs(self, monkeypatch):
110
+ """Non-existent --since ref must yield a list of available branches, not a crash."""
111
+ import subprocess
112
+ import types
113
+
114
+ fake = types.SimpleNamespace(
115
+ returncode=0,
116
+ stdout="develop\norigin/develop\nrelease/1.2\n",
117
+ stderr="",
118
+ )
119
+ monkeypatch.setattr(subprocess, "run", lambda *a, **kw: fake)
120
+
121
+ builder = self._make_builder()
122
+ refs, suggested = builder._get_available_refs("main")
123
+ assert "develop" in refs
124
+ # invalid_ref="main", all_refs contains no "master" → no suggestion
125
+ assert suggested is None
126
+
127
+ def test_case_d_nonzero_returncode_returns_empty(self, monkeypatch):
128
+ """returncode != 0 must not parse stdout — returns empty list silently."""
129
+ import subprocess
130
+ import types
131
+
132
+ fake = types.SimpleNamespace(returncode=128, stdout="fatal: not a git repo\n", stderr="")
133
+ monkeypatch.setattr(subprocess, "run", lambda *a, **kw: fake)
134
+
135
+ builder = self._make_builder()
136
+ refs, suggested = builder._get_available_refs("main")
137
+ assert refs == []
138
+ assert suggested is None