sourcecode 1.4.0__tar.gz → 1.6.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (178) hide show
  1. sourcecode-1.6.0/.continue-here.md +105 -0
  2. {sourcecode-1.4.0 → sourcecode-1.6.0}/PKG-INFO +1 -1
  3. {sourcecode-1.4.0 → sourcecode-1.6.0}/pyproject.toml +1 -1
  4. {sourcecode-1.4.0 → sourcecode-1.6.0}/src/sourcecode/__init__.py +1 -1
  5. {sourcecode-1.4.0 → sourcecode-1.6.0}/src/sourcecode/architecture_analyzer.py +12 -1
  6. {sourcecode-1.4.0 → sourcecode-1.6.0}/src/sourcecode/ast_extractor.py +55 -0
  7. {sourcecode-1.4.0 → sourcecode-1.6.0}/src/sourcecode/cli.py +0 -6
  8. {sourcecode-1.4.0 → sourcecode-1.6.0}/src/sourcecode/contract_pipeline.py +22 -3
  9. {sourcecode-1.4.0 → sourcecode-1.6.0}/src/sourcecode/dependency_analyzer.py +12 -0
  10. {sourcecode-1.4.0 → sourcecode-1.6.0}/src/sourcecode/detectors/java.py +47 -5
  11. {sourcecode-1.4.0 → sourcecode-1.6.0}/src/sourcecode/serializer.py +31 -1
  12. {sourcecode-1.4.0 → sourcecode-1.6.0}/tests/test_contract_pipeline.py +4 -4
  13. {sourcecode-1.4.0 → sourcecode-1.6.0}/.agents/skills/source-command-gsd-join-discord/SKILL.md +0 -0
  14. {sourcecode-1.4.0 → sourcecode-1.6.0}/.agents/skills/source-command-gsd-review-backlog/SKILL.md +0 -0
  15. {sourcecode-1.4.0 → sourcecode-1.6.0}/.agents/skills/source-command-gsd-workstreams/SKILL.md +0 -0
  16. {sourcecode-1.4.0 → sourcecode-1.6.0}/.github/workflows/build-windows.yml +0 -0
  17. {sourcecode-1.4.0 → sourcecode-1.6.0}/.gitignore +0 -0
  18. {sourcecode-1.4.0 → sourcecode-1.6.0}/.ruff.toml +0 -0
  19. {sourcecode-1.4.0 → sourcecode-1.6.0}/CONTRIBUTING.md +0 -0
  20. {sourcecode-1.4.0 → sourcecode-1.6.0}/LICENSE +0 -0
  21. {sourcecode-1.4.0 → sourcecode-1.6.0}/README.md +0 -0
  22. {sourcecode-1.4.0 → sourcecode-1.6.0}/SECURITY.md +0 -0
  23. {sourcecode-1.4.0 → sourcecode-1.6.0}/docs/privacy.md +0 -0
  24. {sourcecode-1.4.0 → sourcecode-1.6.0}/docs/schema.md +0 -0
  25. {sourcecode-1.4.0 → sourcecode-1.6.0}/raw +0 -0
  26. {sourcecode-1.4.0 → sourcecode-1.6.0}/run_cli.py +0 -0
  27. {sourcecode-1.4.0 → sourcecode-1.6.0}/src/sourcecode/adaptive_scanner.py +0 -0
  28. {sourcecode-1.4.0 → sourcecode-1.6.0}/src/sourcecode/architecture_summary.py +0 -0
  29. {sourcecode-1.4.0 → sourcecode-1.6.0}/src/sourcecode/classifier.py +0 -0
  30. {sourcecode-1.4.0 → sourcecode-1.6.0}/src/sourcecode/code_notes_analyzer.py +0 -0
  31. {sourcecode-1.4.0 → sourcecode-1.6.0}/src/sourcecode/confidence_analyzer.py +0 -0
  32. {sourcecode-1.4.0 → sourcecode-1.6.0}/src/sourcecode/context_scorer.py +0 -0
  33. {sourcecode-1.4.0 → sourcecode-1.6.0}/src/sourcecode/context_summarizer.py +0 -0
  34. {sourcecode-1.4.0 → sourcecode-1.6.0}/src/sourcecode/contract_model.py +0 -0
  35. {sourcecode-1.4.0 → sourcecode-1.6.0}/src/sourcecode/coverage_parser.py +0 -0
  36. {sourcecode-1.4.0 → sourcecode-1.6.0}/src/sourcecode/detectors/__init__.py +0 -0
  37. {sourcecode-1.4.0 → sourcecode-1.6.0}/src/sourcecode/detectors/base.py +0 -0
  38. {sourcecode-1.4.0 → sourcecode-1.6.0}/src/sourcecode/detectors/csproj_parser.py +0 -0
  39. {sourcecode-1.4.0 → sourcecode-1.6.0}/src/sourcecode/detectors/dart.py +0 -0
  40. {sourcecode-1.4.0 → sourcecode-1.6.0}/src/sourcecode/detectors/dotnet.py +0 -0
  41. {sourcecode-1.4.0 → sourcecode-1.6.0}/src/sourcecode/detectors/elixir.py +0 -0
  42. {sourcecode-1.4.0 → sourcecode-1.6.0}/src/sourcecode/detectors/go.py +0 -0
  43. {sourcecode-1.4.0 → sourcecode-1.6.0}/src/sourcecode/detectors/heuristic.py +0 -0
  44. {sourcecode-1.4.0 → sourcecode-1.6.0}/src/sourcecode/detectors/hybrid.py +0 -0
  45. {sourcecode-1.4.0 → sourcecode-1.6.0}/src/sourcecode/detectors/jvm_ext.py +0 -0
  46. {sourcecode-1.4.0 → sourcecode-1.6.0}/src/sourcecode/detectors/nodejs.py +0 -0
  47. {sourcecode-1.4.0 → sourcecode-1.6.0}/src/sourcecode/detectors/parsers.py +0 -0
  48. {sourcecode-1.4.0 → sourcecode-1.6.0}/src/sourcecode/detectors/php.py +0 -0
  49. {sourcecode-1.4.0 → sourcecode-1.6.0}/src/sourcecode/detectors/project.py +0 -0
  50. {sourcecode-1.4.0 → sourcecode-1.6.0}/src/sourcecode/detectors/python.py +0 -0
  51. {sourcecode-1.4.0 → sourcecode-1.6.0}/src/sourcecode/detectors/ruby.py +0 -0
  52. {sourcecode-1.4.0 → sourcecode-1.6.0}/src/sourcecode/detectors/rust.py +0 -0
  53. {sourcecode-1.4.0 → sourcecode-1.6.0}/src/sourcecode/detectors/systems.py +0 -0
  54. {sourcecode-1.4.0 → sourcecode-1.6.0}/src/sourcecode/detectors/terraform.py +0 -0
  55. {sourcecode-1.4.0 → sourcecode-1.6.0}/src/sourcecode/detectors/tooling.py +0 -0
  56. {sourcecode-1.4.0 → sourcecode-1.6.0}/src/sourcecode/doc_analyzer.py +0 -0
  57. {sourcecode-1.4.0 → sourcecode-1.6.0}/src/sourcecode/entrypoint_classifier.py +0 -0
  58. {sourcecode-1.4.0 → sourcecode-1.6.0}/src/sourcecode/env_analyzer.py +0 -0
  59. {sourcecode-1.4.0 → sourcecode-1.6.0}/src/sourcecode/file_classifier.py +0 -0
  60. {sourcecode-1.4.0 → sourcecode-1.6.0}/src/sourcecode/git_analyzer.py +0 -0
  61. {sourcecode-1.4.0 → sourcecode-1.6.0}/src/sourcecode/graph_analyzer.py +0 -0
  62. {sourcecode-1.4.0 → sourcecode-1.6.0}/src/sourcecode/metrics_analyzer.py +0 -0
  63. {sourcecode-1.4.0 → sourcecode-1.6.0}/src/sourcecode/prepare_context.py +0 -0
  64. {sourcecode-1.4.0 → sourcecode-1.6.0}/src/sourcecode/ranking_engine.py +0 -0
  65. {sourcecode-1.4.0 → sourcecode-1.6.0}/src/sourcecode/redactor.py +0 -0
  66. {sourcecode-1.4.0 → sourcecode-1.6.0}/src/sourcecode/relevance_scorer.py +0 -0
  67. {sourcecode-1.4.0 → sourcecode-1.6.0}/src/sourcecode/repo_classifier.py +0 -0
  68. {sourcecode-1.4.0 → sourcecode-1.6.0}/src/sourcecode/runtime_classifier.py +0 -0
  69. {sourcecode-1.4.0 → sourcecode-1.6.0}/src/sourcecode/scanner.py +0 -0
  70. {sourcecode-1.4.0 → sourcecode-1.6.0}/src/sourcecode/schema.py +0 -0
  71. {sourcecode-1.4.0 → sourcecode-1.6.0}/src/sourcecode/semantic_analyzer.py +0 -0
  72. {sourcecode-1.4.0 → sourcecode-1.6.0}/src/sourcecode/summarizer.py +0 -0
  73. {sourcecode-1.4.0 → sourcecode-1.6.0}/src/sourcecode/telemetry/__init__.py +0 -0
  74. {sourcecode-1.4.0 → sourcecode-1.6.0}/src/sourcecode/telemetry/config.py +0 -0
  75. {sourcecode-1.4.0 → sourcecode-1.6.0}/src/sourcecode/telemetry/consent.py +0 -0
  76. {sourcecode-1.4.0 → sourcecode-1.6.0}/src/sourcecode/telemetry/events.py +0 -0
  77. {sourcecode-1.4.0 → sourcecode-1.6.0}/src/sourcecode/telemetry/filters.py +0 -0
  78. {sourcecode-1.4.0 → sourcecode-1.6.0}/src/sourcecode/telemetry/transport.py +0 -0
  79. {sourcecode-1.4.0 → sourcecode-1.6.0}/src/sourcecode/tree_utils.py +0 -0
  80. {sourcecode-1.4.0 → sourcecode-1.6.0}/src/sourcecode/workspace.py +0 -0
  81. {sourcecode-1.4.0 → sourcecode-1.6.0}/tests/__init__.py +0 -0
  82. {sourcecode-1.4.0 → sourcecode-1.6.0}/tests/conftest.py +0 -0
  83. {sourcecode-1.4.0 → sourcecode-1.6.0}/tests/fixtures/coverage.xml +0 -0
  84. {sourcecode-1.4.0 → sourcecode-1.6.0}/tests/fixtures/fastapi_app/pyproject.toml +0 -0
  85. {sourcecode-1.4.0 → sourcecode-1.6.0}/tests/fixtures/fastapi_app/src/main.py +0 -0
  86. {sourcecode-1.4.0 → sourcecode-1.6.0}/tests/fixtures/go_service/cmd/api/main.go +0 -0
  87. {sourcecode-1.4.0 → sourcecode-1.6.0}/tests/fixtures/go_service/go.mod +0 -0
  88. {sourcecode-1.4.0 → sourcecode-1.6.0}/tests/fixtures/jacoco.xml +0 -0
  89. {sourcecode-1.4.0 → sourcecode-1.6.0}/tests/fixtures/lcov.info +0 -0
  90. {sourcecode-1.4.0 → sourcecode-1.6.0}/tests/fixtures/nextjs_app/app/page.tsx +0 -0
  91. {sourcecode-1.4.0 → sourcecode-1.6.0}/tests/fixtures/nextjs_app/package.json +0 -0
  92. {sourcecode-1.4.0 → sourcecode-1.6.0}/tests/fixtures/nextjs_app/pnpm-lock.yaml +0 -0
  93. {sourcecode-1.4.0 → sourcecode-1.6.0}/tests/fixtures/pnpm_monorepo/apps/web/app/page.tsx +0 -0
  94. {sourcecode-1.4.0 → sourcecode-1.6.0}/tests/fixtures/pnpm_monorepo/apps/web/package.json +0 -0
  95. {sourcecode-1.4.0 → sourcecode-1.6.0}/tests/fixtures/pnpm_monorepo/packages/api/main.py +0 -0
  96. {sourcecode-1.4.0 → sourcecode-1.6.0}/tests/fixtures/pnpm_monorepo/packages/api/pyproject.toml +0 -0
  97. {sourcecode-1.4.0 → sourcecode-1.6.0}/tests/fixtures/pnpm_monorepo/pnpm-workspace.yaml +0 -0
  98. {sourcecode-1.4.0 → sourcecode-1.6.0}/tests/fixtures/spring_boot_minimal/pom.xml +0 -0
  99. {sourcecode-1.4.0 → sourcecode-1.6.0}/tests/fixtures/spring_boot_minimal/src/main/java/com/example/ddd/ausente/application/service/FindAusenteService.java +0 -0
  100. {sourcecode-1.4.0 → sourcecode-1.6.0}/tests/fixtures/spring_boot_minimal/src/main/java/com/example/ddd/ausente/domain/entities/Ausente.java +0 -0
  101. {sourcecode-1.4.0 → sourcecode-1.6.0}/tests/fixtures/spring_boot_minimal/src/main/java/com/example/ddd/ausente/infrastructure/rest/AusenteRestController.java +0 -0
  102. {sourcecode-1.4.0 → sourcecode-1.6.0}/tests/fixtures/spring_boot_minimal/src/main/java/com/example/ddd/autocoberturas/application/service/FindAutocoberturasService.java +0 -0
  103. {sourcecode-1.4.0 → sourcecode-1.6.0}/tests/fixtures/spring_boot_minimal/src/main/java/com/example/ddd/autocoberturas/domain/entities/Autocoberturas.java +0 -0
  104. {sourcecode-1.4.0 → sourcecode-1.6.0}/tests/fixtures/spring_boot_minimal/src/main/java/com/example/ddd/autocoberturas/infrastructure/rest/AutocoberturasRestController.java +0 -0
  105. {sourcecode-1.4.0 → sourcecode-1.6.0}/tests/fixtures/spring_boot_minimal/src/main/java/com/example/ddd/calendarioTrabajador/application/service/FindCalendarioTrabajadorService.java +0 -0
  106. {sourcecode-1.4.0 → sourcecode-1.6.0}/tests/fixtures/spring_boot_minimal/src/main/java/com/example/ddd/calendarioTrabajador/domain/entities/CalendarioTrabajador.java +0 -0
  107. {sourcecode-1.4.0 → sourcecode-1.6.0}/tests/fixtures/spring_boot_minimal/src/main/java/com/example/ddd/calendarioTrabajador/infrastructure/rest/CalendarioTrabajadorRestController.java +0 -0
  108. {sourcecode-1.4.0 → sourcecode-1.6.0}/tests/fixtures/spring_boot_minimal/src/main/java/com/example/ddd/departamento/application/service/FindDepartamentoService.java +0 -0
  109. {sourcecode-1.4.0 → sourcecode-1.6.0}/tests/fixtures/spring_boot_minimal/src/main/java/com/example/ddd/departamento/domain/entities/Departamento.java +0 -0
  110. {sourcecode-1.4.0 → sourcecode-1.6.0}/tests/fixtures/spring_boot_minimal/src/main/java/com/example/ddd/departamento/infrastructure/rest/DepartamentoRestController.java +0 -0
  111. {sourcecode-1.4.0 → sourcecode-1.6.0}/tests/fixtures/spring_boot_minimal/src/main/java/com/example/ddd/empleado/application/service/FindEmpleadoService.java +0 -0
  112. {sourcecode-1.4.0 → sourcecode-1.6.0}/tests/fixtures/spring_boot_minimal/src/main/java/com/example/ddd/empleado/domain/entities/Empleado.java +0 -0
  113. {sourcecode-1.4.0 → sourcecode-1.6.0}/tests/fixtures/spring_boot_minimal/src/main/java/com/example/ddd/empleado/infrastructure/rest/EmpleadoRestController.java +0 -0
  114. {sourcecode-1.4.0 → sourcecode-1.6.0}/tests/fixtures/spring_boot_minimal/src/main/java/com/example/demo/DemoApplication.java +0 -0
  115. {sourcecode-1.4.0 → sourcecode-1.6.0}/tests/fixtures/spring_boot_minimal/src/main/java/com/example/demo/domain/Health.java +0 -0
  116. {sourcecode-1.4.0 → sourcecode-1.6.0}/tests/fixtures/spring_boot_minimal/src/main/java/com/example/demo/mapper/HealthMapper.java +0 -0
  117. {sourcecode-1.4.0 → sourcecode-1.6.0}/tests/fixtures/spring_boot_minimal/src/main/java/com/example/demo/repository/HealthRepository.java +0 -0
  118. {sourcecode-1.4.0 → sourcecode-1.6.0}/tests/fixtures/spring_boot_minimal/src/main/java/com/example/demo/service/HealthService.java +0 -0
  119. {sourcecode-1.4.0 → sourcecode-1.6.0}/tests/fixtures/spring_boot_minimal/src/main/java/com/example/demo/web/HealthRestController.java +0 -0
  120. {sourcecode-1.4.0 → sourcecode-1.6.0}/tests/fixtures/spring_boot_minimal/src/main/resources/application-dev.yml +0 -0
  121. {sourcecode-1.4.0 → sourcecode-1.6.0}/tests/fixtures/spring_boot_minimal/src/main/resources/application.yml +0 -0
  122. {sourcecode-1.4.0 → sourcecode-1.6.0}/tests/test_architecture_analyzer.py +0 -0
  123. {sourcecode-1.4.0 → sourcecode-1.6.0}/tests/test_architecture_summary.py +0 -0
  124. {sourcecode-1.4.0 → sourcecode-1.6.0}/tests/test_ast_extractor.py +0 -0
  125. {sourcecode-1.4.0 → sourcecode-1.6.0}/tests/test_block1_reliability.py +0 -0
  126. {sourcecode-1.4.0 → sourcecode-1.6.0}/tests/test_block2_coverage.py +0 -0
  127. {sourcecode-1.4.0 → sourcecode-1.6.0}/tests/test_block5_quality.py +0 -0
  128. {sourcecode-1.4.0 → sourcecode-1.6.0}/tests/test_classifier.py +0 -0
  129. {sourcecode-1.4.0 → sourcecode-1.6.0}/tests/test_cli.py +0 -0
  130. {sourcecode-1.4.0 → sourcecode-1.6.0}/tests/test_code_notes_analyzer.py +0 -0
  131. {sourcecode-1.4.0 → sourcecode-1.6.0}/tests/test_context_scorer.py +0 -0
  132. {sourcecode-1.4.0 → sourcecode-1.6.0}/tests/test_coverage_parser.py +0 -0
  133. {sourcecode-1.4.0 → sourcecode-1.6.0}/tests/test_cross_consistency.py +0 -0
  134. {sourcecode-1.4.0 → sourcecode-1.6.0}/tests/test_dependency_analyzer_node_python.py +0 -0
  135. {sourcecode-1.4.0 → sourcecode-1.6.0}/tests/test_dependency_analyzer_polyglot.py +0 -0
  136. {sourcecode-1.4.0 → sourcecode-1.6.0}/tests/test_dependency_schema.py +0 -0
  137. {sourcecode-1.4.0 → sourcecode-1.6.0}/tests/test_detector_dotnet.py +0 -0
  138. {sourcecode-1.4.0 → sourcecode-1.6.0}/tests/test_detector_go_rust_java.py +0 -0
  139. {sourcecode-1.4.0 → sourcecode-1.6.0}/tests/test_detector_nodejs.py +0 -0
  140. {sourcecode-1.4.0 → sourcecode-1.6.0}/tests/test_detector_php_ruby_dart.py +0 -0
  141. {sourcecode-1.4.0 → sourcecode-1.6.0}/tests/test_detector_python.py +0 -0
  142. {sourcecode-1.4.0 → sourcecode-1.6.0}/tests/test_detector_universal_managed.py +0 -0
  143. {sourcecode-1.4.0 → sourcecode-1.6.0}/tests/test_detector_universal_systems.py +0 -0
  144. {sourcecode-1.4.0 → sourcecode-1.6.0}/tests/test_detectors_base.py +0 -0
  145. {sourcecode-1.4.0 → sourcecode-1.6.0}/tests/test_doc_analyzer_jsdom.py +0 -0
  146. {sourcecode-1.4.0 → sourcecode-1.6.0}/tests/test_doc_analyzer_python.py +0 -0
  147. {sourcecode-1.4.0 → sourcecode-1.6.0}/tests/test_graph_analyzer_polyglot.py +0 -0
  148. {sourcecode-1.4.0 → sourcecode-1.6.0}/tests/test_graph_analyzer_python_node.py +0 -0
  149. {sourcecode-1.4.0 → sourcecode-1.6.0}/tests/test_graph_schema.py +0 -0
  150. {sourcecode-1.4.0 → sourcecode-1.6.0}/tests/test_hybrid_inference.py +0 -0
  151. {sourcecode-1.4.0 → sourcecode-1.6.0}/tests/test_integration.py +0 -0
  152. {sourcecode-1.4.0 → sourcecode-1.6.0}/tests/test_integration_dependencies.py +0 -0
  153. {sourcecode-1.4.0 → sourcecode-1.6.0}/tests/test_integration_detection.py +0 -0
  154. {sourcecode-1.4.0 → sourcecode-1.6.0}/tests/test_integration_docs.py +0 -0
  155. {sourcecode-1.4.0 → sourcecode-1.6.0}/tests/test_integration_graph_modules.py +0 -0
  156. {sourcecode-1.4.0 → sourcecode-1.6.0}/tests/test_integration_lqn.py +0 -0
  157. {sourcecode-1.4.0 → sourcecode-1.6.0}/tests/test_integration_metrics.py +0 -0
  158. {sourcecode-1.4.0 → sourcecode-1.6.0}/tests/test_integration_multistack.py +0 -0
  159. {sourcecode-1.4.0 → sourcecode-1.6.0}/tests/test_integration_semantics.py +0 -0
  160. {sourcecode-1.4.0 → sourcecode-1.6.0}/tests/test_integration_universal.py +0 -0
  161. {sourcecode-1.4.0 → sourcecode-1.6.0}/tests/test_java_spring_integration.py +0 -0
  162. {sourcecode-1.4.0 → sourcecode-1.6.0}/tests/test_metrics_analyzer.py +0 -0
  163. {sourcecode-1.4.0 → sourcecode-1.6.0}/tests/test_packaging.py +0 -0
  164. {sourcecode-1.4.0 → sourcecode-1.6.0}/tests/test_phase1_improvements.py +0 -0
  165. {sourcecode-1.4.0 → sourcecode-1.6.0}/tests/test_pipeline_integrity.py +0 -0
  166. {sourcecode-1.4.0 → sourcecode-1.6.0}/tests/test_real_projects.py +0 -0
  167. {sourcecode-1.4.0 → sourcecode-1.6.0}/tests/test_redactor.py +0 -0
  168. {sourcecode-1.4.0 → sourcecode-1.6.0}/tests/test_scanner.py +0 -0
  169. {sourcecode-1.4.0 → sourcecode-1.6.0}/tests/test_schema.py +0 -0
  170. {sourcecode-1.4.0 → sourcecode-1.6.0}/tests/test_schema_normalization.py +0 -0
  171. {sourcecode-1.4.0 → sourcecode-1.6.0}/tests/test_semantic_analyzer_node.py +0 -0
  172. {sourcecode-1.4.0 → sourcecode-1.6.0}/tests/test_semantic_analyzer_python.py +0 -0
  173. {sourcecode-1.4.0 → sourcecode-1.6.0}/tests/test_semantic_import_resolution.py +0 -0
  174. {sourcecode-1.4.0 → sourcecode-1.6.0}/tests/test_semantic_schema.py +0 -0
  175. {sourcecode-1.4.0 → sourcecode-1.6.0}/tests/test_signal_hierarchy.py +0 -0
  176. {sourcecode-1.4.0 → sourcecode-1.6.0}/tests/test_summarizer.py +0 -0
  177. {sourcecode-1.4.0 → sourcecode-1.6.0}/tests/test_telemetry.py +0 -0
  178. {sourcecode-1.4.0 → sourcecode-1.6.0}/tests/test_workspace_analyzer.py +0 -0
@@ -0,0 +1,105 @@
1
+ # Continue Here — sourcecode Java/Spring Enterprise Bug Fixes v1.5.x
2
+ **Created:** 2026-05-08
3
+ **Session context:** Ad-hoc benchmark-driven bug fixes (not a GSD phase execution)
4
+
5
+ ---
6
+
7
+ ## What Was Done This Session
8
+
9
+ Applied 5 fixes targeting regressions in v1.5.0 benchmark report on saint-server (3681 .java, 460 *RestController*, 52 *Mapper.xml, Spring Boot 2.7.18 DDD+legacy layout).
10
+
11
+ ### Root Causes Found and Fixed
12
+
13
+ | ID | File(s) | Root Cause | Fix |
14
+ |----|---------|-----------|-----|
15
+ | N1 | `detectors/java.py` | Scanner `depth >= max_depth=10` cuts `ddd/*/infraestructure/rest/` files silently. Path `src/main/java/com/m3/saint/ddd/domain/infraestructure/rest/` = 10 parts → excluded from file_tree → not in entry_points. | New `_augment_deep_java_controllers()`: direct `os.walk` of `src/main/java/` for `*Controller*.java` files not in file_tree |
16
+ | G3 | `contract_pipeline.py` | Mapper.xml score=0.3 (no `.xml` suffix boost) < Java score=0.4. 3000+ files compete for 2500-file cap — all 52 Mapper.xml cut. | `_is_priority()`: Mapper.xml gets same priority as entry_points in cap sort |
17
+ | T11 | `contract_pipeline.py` | Centrality sort key ignored `is_entrypoint`. REST controllers have low fan_in → ranked below JPA entities. | Added `-c.is_entrypoint` as primary key in centrality sort |
18
+ | P3-B | `cli.py` | Auto-switch to centrality when DDD architecture detected — actively hurt REST controller visibility. | Removed 5-line P3-B block |
19
+ | N2 | `serializer.py` | `hotspots` removed from top-level output in v1.5.0, only in `semantic_summary`. Breaking for consumers using `d["hotspots"]`. | Restore `result["hotspots"]` at top level when `semantic_summary.hotspots` exists |
20
+
21
+ ### Modified Files (uncommitted — user requested no commits)
22
+
23
+ ```
24
+ src/sourcecode/cli.py # P3-B removed (DDD auto-centrality switch)
25
+ src/sourcecode/contract_pipeline.py # G3: _is_priority() + T11: centrality sort key
26
+ src/sourcecode/detectors/java.py # N1: _augment_deep_java_controllers() method
27
+ src/sourcecode/serializer.py # N2: result["hotspots"] top-level restored
28
+ ```
29
+
30
+ ### Test Status
31
+ - 651 passed, 3 skipped (subset excluding pre-existing failures) — zero regressions
32
+
33
+ ---
34
+
35
+ ## What Remains
36
+
37
+ ### Not Fixed (deferred)
38
+
39
+ | ID | Description |
40
+ |----|-------------|
41
+ | M1 | `prepare-context` from monorepo root detects Angular instead of saint-server Java. Requires monorepo subproject routing in `prepare_context.py` → `TaskContextBuilder` / `RepoClassifier`. |
42
+ | M2 | `@M3FiltroSeguridad` custom security annotation not aggregated into security map |
43
+ | M3 | Spring profiles detected but not linked to `application-{profile}.yml` files |
44
+
45
+ ### Verification Commands (run against saint-server after commit)
46
+
47
+ ```bash
48
+ # N1: DDD controllers now captured (need --depth 12+ for tree, augment works at any depth)
49
+ sourcecode saint-server --depth 12 --entrypoints-only --format json | python -c "
50
+ import sys, json
51
+ d = json.load(sys.stdin)
52
+ rest = [x for x in d.get('contracts', []) if 'RestController' in x.get('path','')]
53
+ ddd = [x for x in rest if '/ddd/' in x['path'] or '/infraestructure/' in x['path']]
54
+ print(f'REST total: {len(rest)} | DDD: {len(ddd)}')"
55
+
56
+ # G3: MyBatis XML in contracts
57
+ sourcecode saint-server --depth 10 --format json | python -c "
58
+ import sys, json
59
+ d = json.load(sys.stdin)
60
+ xml = [x for x in d.get('contracts', []) if x.get('language') == 'mybatis-xml']
61
+ print(f'MyBatis contracts: {len(xml)}')"
62
+
63
+ # N2: hotspots at top level restored
64
+ sourcecode saint-server --semantics --format json | python -c "
65
+ import sys, json
66
+ d = json.load(sys.stdin)
67
+ print('top-level hotspots:', len(d.get('hotspots', [])))
68
+ print('semantic_summary.hotspots:', len(d.get('semantic_summary',{}).get('hotspots',[])))"
69
+
70
+ # T11: centrality now surfaces REST controllers
71
+ sourcecode saint-server --rank-by centrality --depth 10 --format json | python -c "
72
+ import sys, json
73
+ d = json.load(sys.stdin)
74
+ rest = [x for x in d.get('contracts', []) if 'RestController' in x.get('path','')]
75
+ print(f'REST in centrality: {len(rest)}')"
76
+ ```
77
+
78
+ **Note on N1 depth:** `_augment_deep_java_controllers` bypasses the scanner depth limit and always walks `src/main/java/` directly. DDD controllers will be detected as entry_points regardless of `--depth`. However, for these same files to appear in the general file_paths (not just entry_points), the scanner itself needs `--depth >= 11`.
79
+
80
+ ---
81
+
82
+ ## Previous Session Fixes (already committed)
83
+
84
+ C1/C2/C3/G1/G2/G3 from v1.4.0→v1.5.0:
85
+ - `--mode standard` key `file_contracts` → `contracts`
86
+ - Controller-first scan, cap 500→1000
87
+ - DDD bounded contexts from package structure
88
+ - Maven BOM limitation warning
89
+ - MyBatis XML extractor (code existed but files were cut by cap — fixed in this session)
90
+
91
+ ---
92
+
93
+ ## GSD Project State (unchanged)
94
+
95
+ All 13 phases complete (see `.planning/STATE.md`). Both sessions were ad-hoc bug work from benchmark reports, not planned phase executions.
96
+
97
+ ---
98
+
99
+ ## Resume Instructions
100
+
101
+ 1. `git diff --stat` — verify 4 uncommitted files
102
+ 2. `python -m pytest tests/ --ignore=tests/test_block2_coverage.py --ignore=tests/test_dependency_analyzer_node_python.py --ignore=tests/test_pipeline_integrity.py -q`
103
+ 3. Commit the fixes
104
+ 4. Run verification commands above against saint-server
105
+ 5. If tackling M1, entry point is `prepare_context.py:TaskContextBuilder.build()` + `repo_classifier.py`
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: sourcecode
3
- Version: 1.4.0
3
+ Version: 1.6.0
4
4
  Summary: Deterministic codebase context for AI coding agents
5
5
  License: Apache License
6
6
  Version 2.0, January 2004
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "sourcecode"
7
- version = "1.4.0"
7
+ version = "1.6.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.4.0"
3
+ __version__ = "1.6.0"
@@ -182,8 +182,19 @@ class ArchitectureAnalyzer:
182
182
  ddd_result = self._detect_ddd(sm.file_paths)
183
183
  if ddd_result is not None:
184
184
  ddd_pattern, ddd_layers, ddd_contexts, ddd_layer_names = ddd_result
185
- domains_for_ddd = self._cluster_domains(filtered) if len(filtered) >= 2 else []
186
185
  module_files = self._build_ddd_module_files(sm.file_paths, ddd_contexts)
186
+ # Use DDD bounded context names as domains so --architecture shows each
187
+ # context as a distinct domain instead of collapsing all files under
188
+ # the Maven path segment (e.g. "java").
189
+ domains_for_ddd = [
190
+ ArchitectureDomain(
191
+ name=n,
192
+ files=module_files.get(n, []),
193
+ role="DDD bounded context",
194
+ confidence="high",
195
+ )
196
+ for n in ddd_contexts
197
+ ]
187
198
  bc_list = [
188
199
  BoundedContext(name=n, modules=module_files.get(n, []), confidence="high")
189
200
  for n in ddd_contexts
@@ -1172,6 +1172,51 @@ def _detect_role(path: str, contract: FileContract) -> str:
1172
1172
  return "util"
1173
1173
 
1174
1174
 
1175
+ # ---------------------------------------------------------------------------
1176
+ # MyBatis XML mapper extractor
1177
+ # ---------------------------------------------------------------------------
1178
+
1179
+ def _extract_mybatis_xml(rel_path: str, source: str) -> FileContract:
1180
+ """Extract namespace and SQL operations from a MyBatis *Mapper.xml file."""
1181
+ import re as _re
1182
+ from xml.etree import ElementTree
1183
+
1184
+ _NS_STRIP = _re.compile(r"\{[^}]+\}")
1185
+ _SQL_OPS = frozenset({"select", "insert", "update", "delete"})
1186
+
1187
+ exports: list[ExportRecord] = []
1188
+ namespace: str | None = None
1189
+
1190
+ try:
1191
+ root_elem = ElementTree.fromstring(source.encode("utf-8"))
1192
+ namespace = root_elem.get("namespace") or None
1193
+ for elem in root_elem:
1194
+ tag = _NS_STRIP.sub("", elem.tag).lower()
1195
+ if tag in _SQL_OPS:
1196
+ op_id = (elem.get("id") or "").strip()
1197
+ if op_id:
1198
+ # type_ref carries select/insert/update/delete for the serializer
1199
+ exports.append(ExportRecord(kind="query", name=op_id, type_ref=tag))
1200
+ except Exception:
1201
+ return FileContract(
1202
+ path=rel_path,
1203
+ language="mybatis-xml",
1204
+ role="mybatis-mapper",
1205
+ extraction_method="heuristic",
1206
+ limitations=["xml_parse_error: failed to parse mapper XML"],
1207
+ )
1208
+
1209
+ deps = [f"namespace:{namespace}"] if namespace else []
1210
+ return FileContract(
1211
+ path=rel_path,
1212
+ language="mybatis-xml",
1213
+ role="mybatis-mapper",
1214
+ exports=exports,
1215
+ dependencies=deps,
1216
+ extraction_method="heuristic",
1217
+ )
1218
+
1219
+
1175
1220
  # ---------------------------------------------------------------------------
1176
1221
  # AstExtractor public class
1177
1222
  # ---------------------------------------------------------------------------
@@ -1191,6 +1236,16 @@ class AstExtractor:
1191
1236
  return self._ts_ok
1192
1237
 
1193
1238
  def extract(self, path: Path, root: Optional[Path] = None) -> Optional[FileContract]:
1239
+ # MyBatis mapper XML — handled before the language map lookup so .xml
1240
+ # files are only processed when they match the mapper naming convention.
1241
+ if path.suffix.lower() == ".xml" and path.name.endswith("Mapper.xml"):
1242
+ try:
1243
+ source = path.read_text(encoding="utf-8", errors="replace")
1244
+ except OSError:
1245
+ return None
1246
+ rel_path = str(path.relative_to(root)).replace("\\", "/") if root else path.name
1247
+ return _extract_mybatis_xml(rel_path, source)
1248
+
1194
1249
  ext = path.suffix.lower()
1195
1250
  language = _LANGUAGE_MAP.get(ext)
1196
1251
  if language is None:
@@ -1399,12 +1399,6 @@ def main(
1399
1399
  ))
1400
1400
  sm = _replace(sm, pipeline_trace=_trace.build_trace())
1401
1401
 
1402
- # P3-B: Auto-switch to centrality ranking when DDD layout detected
1403
- if (rank_by == "relevance"
1404
- and sm.architecture is not None
1405
- and sm.architecture.pattern == "ddd"):
1406
- rank_by = "centrality"
1407
-
1408
1402
  # Contract pipeline — runs for mode=contract|standard|deep|hybrid (skip for raw)
1409
1403
  _is_contract_mode = mode in ("contract", "standard")
1410
1404
  _pipeline_error = False
@@ -219,9 +219,18 @@ class ContractPipeline:
219
219
  fname = Path(pn).name
220
220
  return any(fname.startswith(pat) or f".{pat.strip('.')}" in fname for pat in _TEST_PATTERNS)
221
221
 
222
+ def _is_extractable(p: str) -> bool:
223
+ suf = Path(p).suffix.lower()
224
+ if suf in _SRC_EXTENSIONS:
225
+ return True
226
+ # MyBatis mapper XML files — only *Mapper.xml, not all XML
227
+ if suf == ".xml" and p.endswith("Mapper.xml"):
228
+ return True
229
+ return False
230
+
222
231
  src_paths = [
223
232
  p for p in file_paths
224
- if Path(p).suffix.lower() in _SRC_EXTENSIONS
233
+ if _is_extractable(p)
225
234
  and not scorer.is_noise(p)
226
235
  and (symbol is not None or changed_only or not _is_test(p))
227
236
  ]
@@ -231,10 +240,18 @@ class ContractPipeline:
231
240
 
232
241
  # Apply max_files cap — bypass when symbol search to ensure defining files are found.
233
242
  # A symbol query over a large repo needs all files; result set is small after filtering.
243
+ # MyBatis Mapper.xml contracts rank below Java files on path score alone (.xml has no
244
+ # suffix boost). Give them the same priority slot as entry_points so they survive the cap.
245
+ def _is_priority(p: str) -> bool:
246
+ if p in entry_paths:
247
+ return True
248
+ name = p.rsplit("/", 1)[-1]
249
+ return name.lower().endswith("mapper.xml")
250
+
234
251
  if symbol is None and len(src_paths) > self.max_files:
235
252
  src_paths = sorted(
236
253
  src_paths,
237
- key=lambda p: (p in entry_paths, scorer.score(p)),
254
+ key=lambda p: (_is_priority(p), scorer.score(p)),
238
255
  reverse=True,
239
256
  )[:self.max_files]
240
257
 
@@ -353,7 +370,9 @@ class ContractPipeline:
353
370
 
354
371
  def _rank(self, contracts: list[FileContract], rank_by: RankStrategy) -> list[FileContract]:
355
372
  if rank_by == "centrality":
356
- return sorted(contracts, key=lambda c: (-(c.fan_in + c.fan_out), c.path))
373
+ # Entrypoints (REST controllers, main classes) surface first even in centrality mode:
374
+ # they have low fan_in (not imported) but are the primary API surface.
375
+ return sorted(contracts, key=lambda c: (-c.is_entrypoint, -(c.fan_in + c.fan_out), c.path))
357
376
  if rank_by == "git-churn":
358
377
  return sorted(contracts, key=lambda c: (-c.is_changed, -c.relevance_score, c.path))
359
378
  # Default: relevance — path breaks ties deterministically
@@ -1191,6 +1191,18 @@ class DependencyAnalyzer:
1191
1191
  limitations: list[str] = []
1192
1192
  if not records:
1193
1193
  limitations.append("java: pom.xml sin dependencias parseables (puede usar BOM o propiedades)")
1194
+
1195
+ # Warn when Spring Boot BOM manages transitive deps — they can't be resolved statically.
1196
+ parent_artifact_local = (
1197
+ root_elem.findtext(f"{ns}parent/{ns}artifactId") or ""
1198
+ ).strip() if parent_elem is not None else ""
1199
+ if parent_artifact_local == "spring-boot-starter-parent" and parent_version:
1200
+ limitations.append(
1201
+ f"spring_boot_bom_detected: transitive deps managed by Spring Boot BOM "
1202
+ f"v{parent_version}, not resolved statically. "
1203
+ "Run 'mvn dependency:tree' for the full transitive tree."
1204
+ )
1205
+
1194
1206
  return records, limitations
1195
1207
 
1196
1208
  def _analyze_gradle(self, root: Path) -> tuple[list[DependencyRecord], list[str]]:
@@ -18,7 +18,7 @@ _NS_TAG_RE = re.compile(r"\{[^}]+\}")
18
18
 
19
19
  _MAX_FILE_SIZE = 256 * 1024 # 256 KB
20
20
  _MAX_JAVA_ENTRY_SCAN = 1000
21
- _MAX_ANNOTATION_ENTRY_POINTS = 500
21
+ _MAX_ANNOTATION_ENTRY_POINTS = 1000
22
22
 
23
23
  _REST_CONTROLLER_RE = re.compile(r'@RestController\b')
24
24
  _MVC_CONTROLLER_RE = re.compile(r'@Controller\b')
@@ -225,6 +225,12 @@ class JavaDetector(AbstractDetector):
225
225
  all_paths = flatten_file_tree(context.file_tree)
226
226
  all_java = [p for p in all_paths if p.endswith(".java")]
227
227
 
228
+ # Augment with a direct scan of standard Java source roots for Controller-named
229
+ # files that the depth-limited file_tree scanner may have missed.
230
+ # DDD layouts place REST controllers at depth 10+ (e.g.
231
+ # src/main/java/com/org/app/ddd/domain/infraestructure/rest/XxxRestController.java).
232
+ self._augment_deep_java_controllers(context, all_java)
233
+
228
234
  # 1. @SpringBootApplication entry: Application.java / Main.java by name
229
235
  app_candidates = [
230
236
  p for p in all_java
@@ -236,10 +242,12 @@ class JavaDetector(AbstractDetector):
236
242
  ]
237
243
 
238
244
  # 2. Annotation-based scan: @RestController, @WebFilter, FilterRegistrationBean
239
- scan_candidates = [
240
- p for p in all_java
241
- if "/test/" not in p and "/tests/" not in p
242
- ][:_MAX_JAVA_ENTRY_SCAN]
245
+ # Prioritize Controller-named files so all REST controllers are detected
246
+ # even in large codebases where total files > _MAX_JAVA_ENTRY_SCAN.
247
+ _non_test = [p for p in all_java if "/test/" not in p and "/tests/" not in p]
248
+ _ctrl_files = [p for p in _non_test if "Controller" in p]
249
+ _other_files = [p for p in _non_test if "Controller" not in p]
250
+ scan_candidates = _ctrl_files + _other_files[:max(0, _MAX_JAVA_ENTRY_SCAN - len(_ctrl_files))]
243
251
 
244
252
  annotation_eps: list[EntryPoint] = []
245
253
  for rel_path in scan_candidates:
@@ -268,6 +276,40 @@ class JavaDetector(AbstractDetector):
268
276
  unique_eps.append(ep)
269
277
  return unique_eps
270
278
 
279
+ def _augment_deep_java_controllers(self, context: DetectionContext, all_java: list[str]) -> None:
280
+ """Scan standard Java source roots for *Controller*.java files not in all_java.
281
+
282
+ The depth-limited file_tree scanner misses files at depth >= max_depth.
283
+ DDD layouts place REST controllers deep (e.g. depth 10+), so we supplement
284
+ with a direct filesystem walk scoped to the standard Maven/Gradle source root.
285
+ """
286
+ import os as _os
287
+ existing = set(all_java)
288
+ # Standard Java source root candidates (Maven first, then Gradle/other)
289
+ _SRC_ROOTS = ("src/main/java", "src/main/kotlin", "src/java", "src")
290
+ for src_root_name in _SRC_ROOTS:
291
+ src_root = context.root / src_root_name
292
+ if not src_root.is_dir():
293
+ continue
294
+ try:
295
+ for dirpath, _dirs, filenames in _os.walk(str(src_root)):
296
+ for fname in filenames:
297
+ if "Controller" not in fname or not fname.endswith(".java"):
298
+ continue
299
+ full = Path(dirpath) / fname
300
+ if full.is_symlink():
301
+ continue
302
+ try:
303
+ rel = str(full.relative_to(context.root)).replace("\\", "/")
304
+ if rel not in existing:
305
+ all_java.append(rel)
306
+ existing.add(rel)
307
+ except ValueError:
308
+ pass
309
+ except OSError:
310
+ pass
311
+ return # use only first matching source root
312
+
271
313
  def _scan_java_file_for_entry_points(self, abs_path: Path, rel_path: str) -> list[EntryPoint]:
272
314
  try:
273
315
  if abs_path.stat().st_size > _MAX_FILE_SIZE:
@@ -1110,6 +1110,10 @@ def standard_view(sm: SourceMap, *, include_tree: bool = False) -> dict[str, Any
1110
1110
 
1111
1111
  if sm.semantic_summary is not None and sm.semantic_summary.requested:
1112
1112
  result["semantic_summary"] = asdict(sm.semantic_summary)
1113
+ # Backward compat: also emit hotspots at top level (moved to semantic_summary in v1.5.0).
1114
+ # Consumers reading d["hotspots"] directly still work.
1115
+ if sm.semantic_summary.hotspots:
1116
+ result["hotspots"] = sm.semantic_summary.hotspots[:10]
1113
1117
  # Defensive filter: never emit objects with null required fields.
1114
1118
  # A null entry in these arrays is worse than a shorter array — it causes
1115
1119
  # agents to misinterpret the analysis as valid when it is not.
@@ -1439,10 +1443,31 @@ def _serialize_contract_java(c: Any) -> dict[str, Any]:
1439
1443
  return item
1440
1444
 
1441
1445
 
1446
+ def _serialize_contract_mybatis_xml(c: Any) -> dict[str, Any]:
1447
+ """Serialize a MyBatis *Mapper.xml contract."""
1448
+ item: dict[str, Any] = {"path": c.path, "language": "mybatis-xml"}
1449
+ # Extract namespace stored as "namespace:<fqn>" in dependencies
1450
+ for dep in (c.dependencies or []):
1451
+ if dep.startswith("namespace:"):
1452
+ item["namespace"] = dep[len("namespace:"):]
1453
+ break
1454
+ exports_out: list[dict] = []
1455
+ for e in c.exports:
1456
+ entry: dict = {"kind": e.kind, "name": e.name}
1457
+ if getattr(e, "type_ref", None):
1458
+ entry["type"] = e.type_ref
1459
+ exports_out.append(entry)
1460
+ if exports_out:
1461
+ item["exports"] = exports_out
1462
+ return item
1463
+
1464
+
1442
1465
  def _serialize_contract_minimal(c: Any) -> dict[str, Any]:
1443
1466
  """Serialize one FileContract to minimal format."""
1444
1467
  if getattr(c, "language", None) == "java":
1445
1468
  return _serialize_contract_java(c)
1469
+ if getattr(c, "language", None) == "mybatis-xml":
1470
+ return _serialize_contract_mybatis_xml(c)
1446
1471
  item: dict[str, Any] = {"path": c.path, "role": c.role}
1447
1472
 
1448
1473
  if c.is_changed:
@@ -1560,6 +1585,11 @@ def _contract_view_standard(
1560
1585
  if contracts:
1561
1586
  serialized: list[dict[str, Any]] = []
1562
1587
  for c in contracts:
1588
+ if getattr(c, "language", None) == "mybatis-xml":
1589
+ item = _serialize_contract_mybatis_xml(c)
1590
+ item["relevance_score"] = round(c.relevance_score, 3)
1591
+ serialized.append(item)
1592
+ continue
1563
1593
  item: dict[str, Any] = {
1564
1594
  "path": c.path,
1565
1595
  "language": c.language,
@@ -1609,7 +1639,7 @@ def _contract_view_standard(
1609
1639
  item["ranking_reasons"] = non_trivial
1610
1640
  item["method"] = c.extraction_method
1611
1641
  serialized.append(item)
1612
- result["file_contracts"] = serialized
1642
+ result["contracts"] = serialized
1613
1643
 
1614
1644
  # Optional analysis sections (deep mode or when analyzers ran)
1615
1645
  if include_optional:
@@ -188,7 +188,7 @@ def test_cli_raw_mode_preserves_standard_output(tmp_path: Path) -> None:
188
188
  data = json.loads(result.output)
189
189
  assert "metadata" in data
190
190
  assert data["metadata"]["schema_version"] == "1.0"
191
- assert "file_contracts" not in data
191
+ assert "contracts" not in data
192
192
 
193
193
 
194
194
  def test_cli_max_symbols_flag(tmp_path: Path) -> None:
@@ -198,11 +198,11 @@ def test_cli_max_symbols_flag(tmp_path: Path) -> None:
198
198
  f"def func{i}a(): pass\ndef func{i}b(): pass\n"
199
199
  )
200
200
 
201
- # Use --mode standard to get file_contracts with full per-symbol detail
201
+ # Use --mode standard to get contracts with full per-symbol detail
202
202
  result = runner.invoke(app, ["--mode", "standard", "--max-symbols", "5", str(tmp_path)])
203
203
  assert result.exit_code == 0, result.output
204
204
  data = json.loads(result.output)
205
- contracts = data.get("file_contracts", [])
205
+ contracts = data.get("contracts", [])
206
206
  total = sum(
207
207
  len(c.get("exports", [])) + len(c.get("functions", [])) + len(c.get("types", []))
208
208
  for c in contracts
@@ -250,7 +250,7 @@ def test_cli_standard_mode_includes_detail_fields(tmp_path: Path) -> None:
250
250
  assert "schema_version" in data
251
251
  assert "stacks" in data
252
252
  assert "entry_points" in data
253
- assert "file_contracts" in data
253
+ assert "contracts" in data
254
254
 
255
255
 
256
256
  def test_cli_invalid_mode_exits_nonzero(tmp_path: Path) -> None:
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes