sourcecode 1.31.30__tar.gz → 1.31.31__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 (233) hide show
  1. {sourcecode-1.31.30 → sourcecode-1.31.31}/PKG-INFO +3 -3
  2. {sourcecode-1.31.30 → sourcecode-1.31.31}/README.md +2 -2
  3. {sourcecode-1.31.30 → sourcecode-1.31.31}/pyproject.toml +1 -1
  4. {sourcecode-1.31.30 → sourcecode-1.31.31}/src/sourcecode/__init__.py +1 -1
  5. {sourcecode-1.31.30 → sourcecode-1.31.31}/src/sourcecode/cli.py +124 -3
  6. sourcecode-1.31.31/src/sourcecode/license.py +166 -0
  7. {sourcecode-1.31.30 → sourcecode-1.31.31}/src/sourcecode/mcp/server.py +63 -0
  8. {sourcecode-1.31.30 → sourcecode-1.31.31}/src/sourcecode/repository_ir.py +38 -2
  9. sourcecode-1.31.31/tests/test_bug_fixes_v13130.py +449 -0
  10. {sourcecode-1.31.30 → sourcecode-1.31.31}/tests/test_enterprise_benchmarks.py +5 -4
  11. {sourcecode-1.31.30 → sourcecode-1.31.31}/tests/test_mcp_tools.py +10 -0
  12. {sourcecode-1.31.30 → sourcecode-1.31.31}/.agents/skills/source-command-gsd-join-discord/SKILL.md +0 -0
  13. {sourcecode-1.31.30 → sourcecode-1.31.31}/.agents/skills/source-command-gsd-review-backlog/SKILL.md +0 -0
  14. {sourcecode-1.31.30 → sourcecode-1.31.31}/.agents/skills/source-command-gsd-workstreams/SKILL.md +0 -0
  15. {sourcecode-1.31.30 → sourcecode-1.31.31}/.continue-here.md +0 -0
  16. {sourcecode-1.31.30 → sourcecode-1.31.31}/.github/workflows/build-windows.yml +0 -0
  17. {sourcecode-1.31.30 → sourcecode-1.31.31}/.gitignore +0 -0
  18. {sourcecode-1.31.30 → sourcecode-1.31.31}/.ruff.toml +0 -0
  19. {sourcecode-1.31.30 → sourcecode-1.31.31}/.sourcecode-cache/snapshot-3b5997a-fa5c742c.json +0 -0
  20. {sourcecode-1.31.30 → sourcecode-1.31.31}/AUDIT_REAL_REPOS.md +0 -0
  21. {sourcecode-1.31.30 → sourcecode-1.31.31}/AUDIT_v1.31.23.md +0 -0
  22. {sourcecode-1.31.30 → sourcecode-1.31.31}/CHANGELOG.md +0 -0
  23. {sourcecode-1.31.30 → sourcecode-1.31.31}/CONTRIBUTING.md +0 -0
  24. {sourcecode-1.31.30 → sourcecode-1.31.31}/LICENSE +0 -0
  25. {sourcecode-1.31.30 → sourcecode-1.31.31}/SECURITY.md +0 -0
  26. {sourcecode-1.31.30 → sourcecode-1.31.31}/docs/PRODUCT_TIERS.md +0 -0
  27. {sourcecode-1.31.30 → sourcecode-1.31.31}/docs/privacy.md +0 -0
  28. {sourcecode-1.31.30 → sourcecode-1.31.31}/docs/schema.md +0 -0
  29. {sourcecode-1.31.30 → sourcecode-1.31.31}/raw +0 -0
  30. {sourcecode-1.31.30 → sourcecode-1.31.31}/run_cli.py +0 -0
  31. {sourcecode-1.31.30 → sourcecode-1.31.31}/src/sourcecode/adaptive_scanner.py +0 -0
  32. {sourcecode-1.31.30 → sourcecode-1.31.31}/src/sourcecode/architecture_analyzer.py +0 -0
  33. {sourcecode-1.31.30 → sourcecode-1.31.31}/src/sourcecode/architecture_summary.py +0 -0
  34. {sourcecode-1.31.30 → sourcecode-1.31.31}/src/sourcecode/ast_extractor.py +0 -0
  35. {sourcecode-1.31.30 → sourcecode-1.31.31}/src/sourcecode/cache.py +0 -0
  36. {sourcecode-1.31.30 → sourcecode-1.31.31}/src/sourcecode/canonical_ir.py +0 -0
  37. {sourcecode-1.31.30 → sourcecode-1.31.31}/src/sourcecode/classifier.py +0 -0
  38. {sourcecode-1.31.30 → sourcecode-1.31.31}/src/sourcecode/code_notes_analyzer.py +0 -0
  39. {sourcecode-1.31.30 → sourcecode-1.31.31}/src/sourcecode/confidence_analyzer.py +0 -0
  40. {sourcecode-1.31.30 → sourcecode-1.31.31}/src/sourcecode/context_scorer.py +0 -0
  41. {sourcecode-1.31.30 → sourcecode-1.31.31}/src/sourcecode/context_summarizer.py +0 -0
  42. {sourcecode-1.31.30 → sourcecode-1.31.31}/src/sourcecode/contract_model.py +0 -0
  43. {sourcecode-1.31.30 → sourcecode-1.31.31}/src/sourcecode/contract_pipeline.py +0 -0
  44. {sourcecode-1.31.30 → sourcecode-1.31.31}/src/sourcecode/coverage_parser.py +0 -0
  45. {sourcecode-1.31.30 → sourcecode-1.31.31}/src/sourcecode/dependency_analyzer.py +0 -0
  46. {sourcecode-1.31.30 → sourcecode-1.31.31}/src/sourcecode/detectors/__init__.py +0 -0
  47. {sourcecode-1.31.30 → sourcecode-1.31.31}/src/sourcecode/detectors/base.py +0 -0
  48. {sourcecode-1.31.30 → sourcecode-1.31.31}/src/sourcecode/detectors/csproj_parser.py +0 -0
  49. {sourcecode-1.31.30 → sourcecode-1.31.31}/src/sourcecode/detectors/dart.py +0 -0
  50. {sourcecode-1.31.30 → sourcecode-1.31.31}/src/sourcecode/detectors/dotnet.py +0 -0
  51. {sourcecode-1.31.30 → sourcecode-1.31.31}/src/sourcecode/detectors/elixir.py +0 -0
  52. {sourcecode-1.31.30 → sourcecode-1.31.31}/src/sourcecode/detectors/go.py +0 -0
  53. {sourcecode-1.31.30 → sourcecode-1.31.31}/src/sourcecode/detectors/heuristic.py +0 -0
  54. {sourcecode-1.31.30 → sourcecode-1.31.31}/src/sourcecode/detectors/hybrid.py +0 -0
  55. {sourcecode-1.31.30 → sourcecode-1.31.31}/src/sourcecode/detectors/java.py +0 -0
  56. {sourcecode-1.31.30 → sourcecode-1.31.31}/src/sourcecode/detectors/jvm_ext.py +0 -0
  57. {sourcecode-1.31.30 → sourcecode-1.31.31}/src/sourcecode/detectors/nodejs.py +0 -0
  58. {sourcecode-1.31.30 → sourcecode-1.31.31}/src/sourcecode/detectors/parsers.py +0 -0
  59. {sourcecode-1.31.30 → sourcecode-1.31.31}/src/sourcecode/detectors/php.py +0 -0
  60. {sourcecode-1.31.30 → sourcecode-1.31.31}/src/sourcecode/detectors/project.py +0 -0
  61. {sourcecode-1.31.30 → sourcecode-1.31.31}/src/sourcecode/detectors/python.py +0 -0
  62. {sourcecode-1.31.30 → sourcecode-1.31.31}/src/sourcecode/detectors/ruby.py +0 -0
  63. {sourcecode-1.31.30 → sourcecode-1.31.31}/src/sourcecode/detectors/rust.py +0 -0
  64. {sourcecode-1.31.30 → sourcecode-1.31.31}/src/sourcecode/detectors/systems.py +0 -0
  65. {sourcecode-1.31.30 → sourcecode-1.31.31}/src/sourcecode/detectors/terraform.py +0 -0
  66. {sourcecode-1.31.30 → sourcecode-1.31.31}/src/sourcecode/detectors/tooling.py +0 -0
  67. {sourcecode-1.31.30 → sourcecode-1.31.31}/src/sourcecode/doc_analyzer.py +0 -0
  68. {sourcecode-1.31.30 → sourcecode-1.31.31}/src/sourcecode/entrypoint_classifier.py +0 -0
  69. {sourcecode-1.31.30 → sourcecode-1.31.31}/src/sourcecode/env_analyzer.py +0 -0
  70. {sourcecode-1.31.30 → sourcecode-1.31.31}/src/sourcecode/file_classifier.py +0 -0
  71. {sourcecode-1.31.30 → sourcecode-1.31.31}/src/sourcecode/flow_analyzer.py +0 -0
  72. {sourcecode-1.31.30 → sourcecode-1.31.31}/src/sourcecode/git_analyzer.py +0 -0
  73. {sourcecode-1.31.30 → sourcecode-1.31.31}/src/sourcecode/graph_analyzer.py +0 -0
  74. {sourcecode-1.31.30 → sourcecode-1.31.31}/src/sourcecode/mcp/__init__.py +0 -0
  75. {sourcecode-1.31.30 → sourcecode-1.31.31}/src/sourcecode/mcp/onboarding/__init__.py +0 -0
  76. {sourcecode-1.31.30 → sourcecode-1.31.31}/src/sourcecode/mcp/onboarding/applier.py +0 -0
  77. {sourcecode-1.31.30 → sourcecode-1.31.31}/src/sourcecode/mcp/onboarding/backup.py +0 -0
  78. {sourcecode-1.31.30 → sourcecode-1.31.31}/src/sourcecode/mcp/onboarding/detector.py +0 -0
  79. {sourcecode-1.31.30 → sourcecode-1.31.31}/src/sourcecode/mcp/onboarding/planner.py +0 -0
  80. {sourcecode-1.31.30 → sourcecode-1.31.31}/src/sourcecode/mcp/runner.py +0 -0
  81. {sourcecode-1.31.30 → sourcecode-1.31.31}/src/sourcecode/mcp_nudge.py +0 -0
  82. {sourcecode-1.31.30 → sourcecode-1.31.31}/src/sourcecode/metrics_analyzer.py +0 -0
  83. {sourcecode-1.31.30 → sourcecode-1.31.31}/src/sourcecode/output_budget.py +0 -0
  84. {sourcecode-1.31.30 → sourcecode-1.31.31}/src/sourcecode/path_filters.py +0 -0
  85. {sourcecode-1.31.30 → sourcecode-1.31.31}/src/sourcecode/pr_comment_renderer.py +0 -0
  86. {sourcecode-1.31.30 → sourcecode-1.31.31}/src/sourcecode/prepare_context.py +0 -0
  87. {sourcecode-1.31.30 → sourcecode-1.31.31}/src/sourcecode/progress.py +0 -0
  88. {sourcecode-1.31.30 → sourcecode-1.31.31}/src/sourcecode/ranking_engine.py +0 -0
  89. {sourcecode-1.31.30 → sourcecode-1.31.31}/src/sourcecode/redactor.py +0 -0
  90. {sourcecode-1.31.30 → sourcecode-1.31.31}/src/sourcecode/relevance_scorer.py +0 -0
  91. {sourcecode-1.31.30 → sourcecode-1.31.31}/src/sourcecode/repo_classifier.py +0 -0
  92. {sourcecode-1.31.30 → sourcecode-1.31.31}/src/sourcecode/runtime_classifier.py +0 -0
  93. {sourcecode-1.31.30 → sourcecode-1.31.31}/src/sourcecode/scanner.py +0 -0
  94. {sourcecode-1.31.30 → sourcecode-1.31.31}/src/sourcecode/schema.py +0 -0
  95. {sourcecode-1.31.30 → sourcecode-1.31.31}/src/sourcecode/semantic_analyzer.py +0 -0
  96. {sourcecode-1.31.30 → sourcecode-1.31.31}/src/sourcecode/serializer.py +0 -0
  97. {sourcecode-1.31.30 → sourcecode-1.31.31}/src/sourcecode/summarizer.py +0 -0
  98. {sourcecode-1.31.30 → sourcecode-1.31.31}/src/sourcecode/telemetry/__init__.py +0 -0
  99. {sourcecode-1.31.30 → sourcecode-1.31.31}/src/sourcecode/telemetry/config.py +0 -0
  100. {sourcecode-1.31.30 → sourcecode-1.31.31}/src/sourcecode/telemetry/consent.py +0 -0
  101. {sourcecode-1.31.30 → sourcecode-1.31.31}/src/sourcecode/telemetry/events.py +0 -0
  102. {sourcecode-1.31.30 → sourcecode-1.31.31}/src/sourcecode/telemetry/filters.py +0 -0
  103. {sourcecode-1.31.30 → sourcecode-1.31.31}/src/sourcecode/telemetry/transport.py +0 -0
  104. {sourcecode-1.31.30 → sourcecode-1.31.31}/src/sourcecode/tree_utils.py +0 -0
  105. {sourcecode-1.31.30 → sourcecode-1.31.31}/src/sourcecode/workspace.py +0 -0
  106. {sourcecode-1.31.30 → sourcecode-1.31.31}/tests/__init__.py +0 -0
  107. {sourcecode-1.31.30 → sourcecode-1.31.31}/tests/conftest.py +0 -0
  108. {sourcecode-1.31.30 → sourcecode-1.31.31}/tests/fixtures/coverage.xml +0 -0
  109. {sourcecode-1.31.30 → sourcecode-1.31.31}/tests/fixtures/fastapi_app/pyproject.toml +0 -0
  110. {sourcecode-1.31.30 → sourcecode-1.31.31}/tests/fixtures/fastapi_app/src/main.py +0 -0
  111. {sourcecode-1.31.30 → sourcecode-1.31.31}/tests/fixtures/go_service/cmd/api/main.go +0 -0
  112. {sourcecode-1.31.30 → sourcecode-1.31.31}/tests/fixtures/go_service/go.mod +0 -0
  113. {sourcecode-1.31.30 → sourcecode-1.31.31}/tests/fixtures/jacoco.xml +0 -0
  114. {sourcecode-1.31.30 → sourcecode-1.31.31}/tests/fixtures/latin1_sample.java +0 -0
  115. {sourcecode-1.31.30 → sourcecode-1.31.31}/tests/fixtures/latin1_sample_iso.java +0 -0
  116. {sourcecode-1.31.30 → sourcecode-1.31.31}/tests/fixtures/lcov.info +0 -0
  117. {sourcecode-1.31.30 → sourcecode-1.31.31}/tests/fixtures/nextjs_app/app/page.tsx +0 -0
  118. {sourcecode-1.31.30 → sourcecode-1.31.31}/tests/fixtures/nextjs_app/package.json +0 -0
  119. {sourcecode-1.31.30 → sourcecode-1.31.31}/tests/fixtures/nextjs_app/pnpm-lock.yaml +0 -0
  120. {sourcecode-1.31.30 → sourcecode-1.31.31}/tests/fixtures/pnpm_monorepo/apps/web/app/page.tsx +0 -0
  121. {sourcecode-1.31.30 → sourcecode-1.31.31}/tests/fixtures/pnpm_monorepo/apps/web/package.json +0 -0
  122. {sourcecode-1.31.30 → sourcecode-1.31.31}/tests/fixtures/pnpm_monorepo/packages/api/main.py +0 -0
  123. {sourcecode-1.31.30 → sourcecode-1.31.31}/tests/fixtures/pnpm_monorepo/packages/api/pyproject.toml +0 -0
  124. {sourcecode-1.31.30 → sourcecode-1.31.31}/tests/fixtures/pnpm_monorepo/pnpm-workspace.yaml +0 -0
  125. {sourcecode-1.31.30 → sourcecode-1.31.31}/tests/fixtures/spring_boot_minimal/pom.xml +0 -0
  126. {sourcecode-1.31.30 → sourcecode-1.31.31}/tests/fixtures/spring_boot_minimal/src/main/java/com/example/ddd/ausente/application/service/FindAusenteService.java +0 -0
  127. {sourcecode-1.31.30 → sourcecode-1.31.31}/tests/fixtures/spring_boot_minimal/src/main/java/com/example/ddd/ausente/domain/entities/Ausente.java +0 -0
  128. {sourcecode-1.31.30 → sourcecode-1.31.31}/tests/fixtures/spring_boot_minimal/src/main/java/com/example/ddd/ausente/infrastructure/rest/AusenteRestController.java +0 -0
  129. {sourcecode-1.31.30 → sourcecode-1.31.31}/tests/fixtures/spring_boot_minimal/src/main/java/com/example/ddd/autocoberturas/application/service/FindAutocoberturasService.java +0 -0
  130. {sourcecode-1.31.30 → sourcecode-1.31.31}/tests/fixtures/spring_boot_minimal/src/main/java/com/example/ddd/autocoberturas/domain/entities/Autocoberturas.java +0 -0
  131. {sourcecode-1.31.30 → sourcecode-1.31.31}/tests/fixtures/spring_boot_minimal/src/main/java/com/example/ddd/autocoberturas/infrastructure/rest/AutocoberturasRestController.java +0 -0
  132. {sourcecode-1.31.30 → sourcecode-1.31.31}/tests/fixtures/spring_boot_minimal/src/main/java/com/example/ddd/calendarioTrabajador/application/service/FindCalendarioTrabajadorService.java +0 -0
  133. {sourcecode-1.31.30 → sourcecode-1.31.31}/tests/fixtures/spring_boot_minimal/src/main/java/com/example/ddd/calendarioTrabajador/domain/entities/CalendarioTrabajador.java +0 -0
  134. {sourcecode-1.31.30 → sourcecode-1.31.31}/tests/fixtures/spring_boot_minimal/src/main/java/com/example/ddd/calendarioTrabajador/infrastructure/rest/CalendarioTrabajadorRestController.java +0 -0
  135. {sourcecode-1.31.30 → sourcecode-1.31.31}/tests/fixtures/spring_boot_minimal/src/main/java/com/example/ddd/departamento/application/service/FindDepartamentoService.java +0 -0
  136. {sourcecode-1.31.30 → sourcecode-1.31.31}/tests/fixtures/spring_boot_minimal/src/main/java/com/example/ddd/departamento/domain/entities/Departamento.java +0 -0
  137. {sourcecode-1.31.30 → sourcecode-1.31.31}/tests/fixtures/spring_boot_minimal/src/main/java/com/example/ddd/departamento/infrastructure/rest/DepartamentoRestController.java +0 -0
  138. {sourcecode-1.31.30 → sourcecode-1.31.31}/tests/fixtures/spring_boot_minimal/src/main/java/com/example/ddd/empleado/application/service/FindEmpleadoService.java +0 -0
  139. {sourcecode-1.31.30 → sourcecode-1.31.31}/tests/fixtures/spring_boot_minimal/src/main/java/com/example/ddd/empleado/domain/entities/Empleado.java +0 -0
  140. {sourcecode-1.31.30 → sourcecode-1.31.31}/tests/fixtures/spring_boot_minimal/src/main/java/com/example/ddd/empleado/infrastructure/rest/EmpleadoRestController.java +0 -0
  141. {sourcecode-1.31.30 → sourcecode-1.31.31}/tests/fixtures/spring_boot_minimal/src/main/java/com/example/demo/DemoApplication.java +0 -0
  142. {sourcecode-1.31.30 → sourcecode-1.31.31}/tests/fixtures/spring_boot_minimal/src/main/java/com/example/demo/config/FilterConfig.java +0 -0
  143. {sourcecode-1.31.30 → sourcecode-1.31.31}/tests/fixtures/spring_boot_minimal/src/main/java/com/example/demo/domain/Health.java +0 -0
  144. {sourcecode-1.31.30 → sourcecode-1.31.31}/tests/fixtures/spring_boot_minimal/src/main/java/com/example/demo/mapper/HealthMapper.java +0 -0
  145. {sourcecode-1.31.30 → sourcecode-1.31.31}/tests/fixtures/spring_boot_minimal/src/main/java/com/example/demo/repository/HealthRepository.java +0 -0
  146. {sourcecode-1.31.30 → sourcecode-1.31.31}/tests/fixtures/spring_boot_minimal/src/main/java/com/example/demo/service/HealthService.java +0 -0
  147. {sourcecode-1.31.30 → sourcecode-1.31.31}/tests/fixtures/spring_boot_minimal/src/main/java/com/example/demo/web/HealthRestController.java +0 -0
  148. {sourcecode-1.31.30 → sourcecode-1.31.31}/tests/fixtures/spring_boot_minimal/src/main/java/com/example/demo/web/NominaRestController.java +0 -0
  149. {sourcecode-1.31.30 → sourcecode-1.31.31}/tests/fixtures/spring_boot_minimal/src/main/resources/application-dev.yml +0 -0
  150. {sourcecode-1.31.30 → sourcecode-1.31.31}/tests/fixtures/spring_boot_minimal/src/main/resources/application.yml +0 -0
  151. {sourcecode-1.31.30 → sourcecode-1.31.31}/tests/fixtures/spring_boot_minimal/src/main/resources/mapper/HealthMapper.xml +0 -0
  152. {sourcecode-1.31.30 → sourcecode-1.31.31}/tests/test_architecture_analyzer.py +0 -0
  153. {sourcecode-1.31.30 → sourcecode-1.31.31}/tests/test_architecture_summary.py +0 -0
  154. {sourcecode-1.31.30 → sourcecode-1.31.31}/tests/test_ast_extractor.py +0 -0
  155. {sourcecode-1.31.30 → sourcecode-1.31.31}/tests/test_audit_fixes.py +0 -0
  156. {sourcecode-1.31.30 → sourcecode-1.31.31}/tests/test_audit_sas_v2.py +0 -0
  157. {sourcecode-1.31.30 → sourcecode-1.31.31}/tests/test_block1_reliability.py +0 -0
  158. {sourcecode-1.31.30 → sourcecode-1.31.31}/tests/test_block2_coverage.py +0 -0
  159. {sourcecode-1.31.30 → sourcecode-1.31.31}/tests/test_block5_quality.py +0 -0
  160. {sourcecode-1.31.30 → sourcecode-1.31.31}/tests/test_broadleaf_fixes.py +0 -0
  161. {sourcecode-1.31.30 → sourcecode-1.31.31}/tests/test_bug_fixes_v1302.py +0 -0
  162. {sourcecode-1.31.30 → sourcecode-1.31.31}/tests/test_bug_fixes_v13115.py +0 -0
  163. {sourcecode-1.31.30 → sourcecode-1.31.31}/tests/test_bug_fixes_v1312.py +0 -0
  164. {sourcecode-1.31.30 → sourcecode-1.31.31}/tests/test_bug_fixes_v13122.py +0 -0
  165. {sourcecode-1.31.30 → sourcecode-1.31.31}/tests/test_bug_fixes_v1313.py +0 -0
  166. {sourcecode-1.31.30 → sourcecode-1.31.31}/tests/test_bug_fixes_v1321.py +0 -0
  167. {sourcecode-1.31.30 → sourcecode-1.31.31}/tests/test_bug_fixes_v16.py +0 -0
  168. {sourcecode-1.31.30 → sourcecode-1.31.31}/tests/test_bug_fixes_v2.py +0 -0
  169. {sourcecode-1.31.30 → sourcecode-1.31.31}/tests/test_cache.py +0 -0
  170. {sourcecode-1.31.30 → sourcecode-1.31.31}/tests/test_canonical_ir.py +0 -0
  171. {sourcecode-1.31.30 → sourcecode-1.31.31}/tests/test_classifier.py +0 -0
  172. {sourcecode-1.31.30 → sourcecode-1.31.31}/tests/test_cli.py +0 -0
  173. {sourcecode-1.31.30 → sourcecode-1.31.31}/tests/test_code_notes_analyzer.py +0 -0
  174. {sourcecode-1.31.30 → sourcecode-1.31.31}/tests/test_context_scorer.py +0 -0
  175. {sourcecode-1.31.30 → sourcecode-1.31.31}/tests/test_contract_pipeline.py +0 -0
  176. {sourcecode-1.31.30 → sourcecode-1.31.31}/tests/test_coverage_parser.py +0 -0
  177. {sourcecode-1.31.30 → sourcecode-1.31.31}/tests/test_cross_consistency.py +0 -0
  178. {sourcecode-1.31.30 → sourcecode-1.31.31}/tests/test_dependency_analyzer_node_python.py +0 -0
  179. {sourcecode-1.31.30 → sourcecode-1.31.31}/tests/test_dependency_analyzer_polyglot.py +0 -0
  180. {sourcecode-1.31.30 → sourcecode-1.31.31}/tests/test_dependency_schema.py +0 -0
  181. {sourcecode-1.31.30 → sourcecode-1.31.31}/tests/test_detector_dotnet.py +0 -0
  182. {sourcecode-1.31.30 → sourcecode-1.31.31}/tests/test_detector_go_rust_java.py +0 -0
  183. {sourcecode-1.31.30 → sourcecode-1.31.31}/tests/test_detector_nodejs.py +0 -0
  184. {sourcecode-1.31.30 → sourcecode-1.31.31}/tests/test_detector_php_ruby_dart.py +0 -0
  185. {sourcecode-1.31.30 → sourcecode-1.31.31}/tests/test_detector_python.py +0 -0
  186. {sourcecode-1.31.30 → sourcecode-1.31.31}/tests/test_detector_universal_managed.py +0 -0
  187. {sourcecode-1.31.30 → sourcecode-1.31.31}/tests/test_detector_universal_systems.py +0 -0
  188. {sourcecode-1.31.30 → sourcecode-1.31.31}/tests/test_detectors_base.py +0 -0
  189. {sourcecode-1.31.30 → sourcecode-1.31.31}/tests/test_doc_analyzer_jsdom.py +0 -0
  190. {sourcecode-1.31.30 → sourcecode-1.31.31}/tests/test_doc_analyzer_python.py +0 -0
  191. {sourcecode-1.31.30 → sourcecode-1.31.31}/tests/test_encoding_regression.py +0 -0
  192. {sourcecode-1.31.30 → sourcecode-1.31.31}/tests/test_graph_analyzer_polyglot.py +0 -0
  193. {sourcecode-1.31.30 → sourcecode-1.31.31}/tests/test_graph_analyzer_python_node.py +0 -0
  194. {sourcecode-1.31.30 → sourcecode-1.31.31}/tests/test_graph_schema.py +0 -0
  195. {sourcecode-1.31.30 → sourcecode-1.31.31}/tests/test_hybrid_inference.py +0 -0
  196. {sourcecode-1.31.30 → sourcecode-1.31.31}/tests/test_integration.py +0 -0
  197. {sourcecode-1.31.30 → sourcecode-1.31.31}/tests/test_integration_dependencies.py +0 -0
  198. {sourcecode-1.31.30 → sourcecode-1.31.31}/tests/test_integration_detection.py +0 -0
  199. {sourcecode-1.31.30 → sourcecode-1.31.31}/tests/test_integration_docs.py +0 -0
  200. {sourcecode-1.31.30 → sourcecode-1.31.31}/tests/test_integration_graph_modules.py +0 -0
  201. {sourcecode-1.31.30 → sourcecode-1.31.31}/tests/test_integration_lqn.py +0 -0
  202. {sourcecode-1.31.30 → sourcecode-1.31.31}/tests/test_integration_metrics.py +0 -0
  203. {sourcecode-1.31.30 → sourcecode-1.31.31}/tests/test_integration_multistack.py +0 -0
  204. {sourcecode-1.31.30 → sourcecode-1.31.31}/tests/test_integration_semantics.py +0 -0
  205. {sourcecode-1.31.30 → sourcecode-1.31.31}/tests/test_integration_universal.py +0 -0
  206. {sourcecode-1.31.30 → sourcecode-1.31.31}/tests/test_java_spring_integration.py +0 -0
  207. {sourcecode-1.31.30 → sourcecode-1.31.31}/tests/test_mcp_nudge.py +0 -0
  208. {sourcecode-1.31.30 → sourcecode-1.31.31}/tests/test_mcp_runner.py +0 -0
  209. {sourcecode-1.31.30 → sourcecode-1.31.31}/tests/test_mcp_serve.py +0 -0
  210. {sourcecode-1.31.30 → sourcecode-1.31.31}/tests/test_metrics_analyzer.py +0 -0
  211. {sourcecode-1.31.30 → sourcecode-1.31.31}/tests/test_output_ux.py +0 -0
  212. {sourcecode-1.31.30 → sourcecode-1.31.31}/tests/test_packaging.py +0 -0
  213. {sourcecode-1.31.30 → sourcecode-1.31.31}/tests/test_phase1_improvements.py +0 -0
  214. {sourcecode-1.31.30 → sourcecode-1.31.31}/tests/test_pipeline_integrity.py +0 -0
  215. {sourcecode-1.31.30 → sourcecode-1.31.31}/tests/test_real_projects.py +0 -0
  216. {sourcecode-1.31.30 → sourcecode-1.31.31}/tests/test_redactor.py +0 -0
  217. {sourcecode-1.31.30 → sourcecode-1.31.31}/tests/test_repository_ir.py +0 -0
  218. {sourcecode-1.31.30 → sourcecode-1.31.31}/tests/test_scanner.py +0 -0
  219. {sourcecode-1.31.30 → sourcecode-1.31.31}/tests/test_schema.py +0 -0
  220. {sourcecode-1.31.30 → sourcecode-1.31.31}/tests/test_schema_normalization.py +0 -0
  221. {sourcecode-1.31.30 → sourcecode-1.31.31}/tests/test_scoring_calibration.py +0 -0
  222. {sourcecode-1.31.30 → sourcecode-1.31.31}/tests/test_semantic_analyzer_node.py +0 -0
  223. {sourcecode-1.31.30 → sourcecode-1.31.31}/tests/test_semantic_analyzer_python.py +0 -0
  224. {sourcecode-1.31.30 → sourcecode-1.31.31}/tests/test_semantic_import_resolution.py +0 -0
  225. {sourcecode-1.31.30 → sourcecode-1.31.31}/tests/test_semantic_schema.py +0 -0
  226. {sourcecode-1.31.30 → sourcecode-1.31.31}/tests/test_signal_hierarchy.py +0 -0
  227. {sourcecode-1.31.30 → sourcecode-1.31.31}/tests/test_summarizer.py +0 -0
  228. {sourcecode-1.31.30 → sourcecode-1.31.31}/tests/test_surface_honesty.py +0 -0
  229. {sourcecode-1.31.30 → sourcecode-1.31.31}/tests/test_task_differentiation.py +0 -0
  230. {sourcecode-1.31.30 → sourcecode-1.31.31}/tests/test_telemetry.py +0 -0
  231. {sourcecode-1.31.30 → sourcecode-1.31.31}/tests/test_v131_improvements.py +0 -0
  232. {sourcecode-1.31.30 → sourcecode-1.31.31}/tests/test_v1_10_regressions.py +0 -0
  233. {sourcecode-1.31.30 → sourcecode-1.31.31}/tests/test_workspace_analyzer.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: sourcecode
3
- Version: 1.31.30
3
+ Version: 1.31.31
4
4
  Summary: Deterministic codebase context for AI coding agents
5
5
  License: Apache License
6
6
  Version 2.0, January 2004
@@ -225,7 +225,7 @@ Description-Content-Type: text/markdown
225
225
 
226
226
  **AI-ready change intelligence for Java/Spring enterprise monoliths.**
227
227
 
228
- ![Version](https://img.shields.io/badge/version-1.31.30-blue)
228
+ ![Version](https://img.shields.io/badge/version-1.31.31-blue)
229
229
  ![Python](https://img.shields.io/badge/python-3.10%2B-green)
230
230
 
231
231
  ---
@@ -263,7 +263,7 @@ pipx install sourcecode
263
263
 
264
264
  ```bash
265
265
  sourcecode version
266
- # sourcecode 1.31.30
266
+ # sourcecode 1.31.31
267
267
  ```
268
268
 
269
269
  ---
@@ -2,7 +2,7 @@
2
2
 
3
3
  **AI-ready change intelligence for Java/Spring enterprise monoliths.**
4
4
 
5
- ![Version](https://img.shields.io/badge/version-1.31.30-blue)
5
+ ![Version](https://img.shields.io/badge/version-1.31.31-blue)
6
6
  ![Python](https://img.shields.io/badge/python-3.10%2B-green)
7
7
 
8
8
  ---
@@ -40,7 +40,7 @@ pipx install sourcecode
40
40
 
41
41
  ```bash
42
42
  sourcecode version
43
- # sourcecode 1.31.30
43
+ # sourcecode 1.31.31
44
44
  ```
45
45
 
46
46
  ---
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "sourcecode"
7
- version = "1.31.30"
7
+ version = "1.31.31"
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.31.30"
3
+ __version__ = "1.31.31"
@@ -171,6 +171,8 @@ _SUBCOMMANDS: frozenset[str] = frozenset(
171
171
  "repo-ir", "mcp", "endpoints", "impact",
172
172
  # Enterprise workflow commands
173
173
  "onboard", "modernize", "fix-bug", "review-pr",
174
+ # License
175
+ "activate",
174
176
  }
175
177
  )
176
178
 
@@ -253,6 +255,35 @@ def _emit_error_json(error: str, message: str, **context: object) -> None:
253
255
  _sys.stderr.flush()
254
256
 
255
257
 
258
+ # H-06: Intercept Click-level UsageError (unknown options, bad args) and emit JSON.
259
+ # Click's default show() writes "Error: No such option: --foo" as plain text.
260
+ # Automation consumers need JSON on stderr regardless of how the error originated.
261
+ try:
262
+ import click.exceptions as _click_exc
263
+
264
+ def _json_click_usage_error_show(self: Any, file: Any = None) -> None: # type: ignore[override]
265
+ import json as _je
266
+ import sys as _jse
267
+ _code_map = {
268
+ "NoSuchOption": "invalid_option",
269
+ "BadOptionUsage": "invalid_option",
270
+ "BadParameter": "bad_parameter",
271
+ "MissingParameter": "missing_required",
272
+ "BadArgumentUsage": "bad_argument",
273
+ }
274
+ code = _code_map.get(type(self).__name__, "invalid_option")
275
+ payload: dict[str, object] = {"error": code, "message": self.format_message()}
276
+ _opt = getattr(self, "option_name", None) or getattr(self, "param_hint", None)
277
+ if _opt:
278
+ payload["flag"] = str(_opt).strip("'\"")
279
+ _jse.stderr.write(_je.dumps(payload, ensure_ascii=False) + "\n")
280
+ _jse.stderr.flush()
281
+
282
+ _click_exc.UsageError.show = _json_click_usage_error_show # type: ignore[method-assign]
283
+ except Exception:
284
+ pass # click unavailable — plain-text fallback
285
+
286
+
256
287
  def _copy_to_clipboard(content: str) -> bool:
257
288
  """Copy text to system clipboard. Returns True on success, False otherwise (never raises)."""
258
289
  import subprocess
@@ -2165,6 +2196,12 @@ def prepare_context_cmd(
2165
2196
  )
2166
2197
  raise typer.Exit(code=1)
2167
2198
 
2199
+ # Pro gate: generate-tests and delta require an active Pro license.
2200
+ _PRO_TASKS: frozenset[str] = frozenset({"generate-tests", "delta"})
2201
+ if task in _PRO_TASKS:
2202
+ from sourcecode.license import require_pro as _require_pro
2203
+ _require_pro(task)
2204
+
2168
2205
  # Validate --format: only "json" and "github-comment" are valid for prepare-context.
2169
2206
  # "yaml" is intentionally NOT supported here (use main command for yaml output).
2170
2207
  # Invalid values must error loudly — silently falling through to JSON is a lie.
@@ -2222,7 +2259,49 @@ def prepare_context_cmd(
2222
2259
  _sys.stderr.flush()
2223
2260
  _t0 = _time.perf_counter()
2224
2261
  try:
2225
- output = builder.build(task, since=since, symptom=symptom, fast=fast, include_config=include_config, all_gaps=all_gaps)
2262
+ # H-02: apply timeout for generate-tests large repos can stall indefinitely.
2263
+ # Mirrors SOURCECODE_TESTS_TIMEOUT_MS used by the MCP generate_tests_context tool.
2264
+ if task == "generate-tests" and not fast:
2265
+ import concurrent.futures as _cf
2266
+ import os as _os_gt
2267
+ _timeout_ms = int(_os_gt.environ.get("SOURCECODE_TESTS_TIMEOUT_MS", "30000"))
2268
+ _timeout_s = _timeout_ms / 1000.0
2269
+ _ex = _cf.ThreadPoolExecutor(max_workers=1)
2270
+ _fut = _ex.submit(
2271
+ builder.build, task,
2272
+ since=since, symptom=symptom, fast=fast,
2273
+ include_config=include_config, all_gaps=all_gaps,
2274
+ )
2275
+ _done_set, _nd_set = _cf.wait([_fut], timeout=_timeout_s)
2276
+ _ex.shutdown(wait=False)
2277
+ if _nd_set:
2278
+ import sys as _sys_gt
2279
+ if _sys_gt.stderr.isatty():
2280
+ _sys_gt.stderr.write(
2281
+ f"[generate-tests] timeout after {_timeout_ms}ms — returning partial result\n"
2282
+ )
2283
+ _sys_gt.stderr.flush()
2284
+ from sourcecode.prepare_context import TaskOutput as _TO
2285
+ output = _TO(
2286
+ task=task,
2287
+ goal=TASKS[task].goal,
2288
+ project_summary=None,
2289
+ architecture_summary=None,
2290
+ relevant_files=[],
2291
+ suspected_areas=[],
2292
+ improvement_opportunities=[],
2293
+ test_gaps=[],
2294
+ key_dependencies=[],
2295
+ code_notes_summary=None,
2296
+ limitations=[f"generate-tests timed out after {_timeout_ms}ms"],
2297
+ truncated=True,
2298
+ truncated_reason=f"timeout_{_timeout_ms}ms",
2299
+ confidence="low",
2300
+ )
2301
+ else:
2302
+ output = _fut.result()
2303
+ else:
2304
+ output = builder.build(task, since=since, symptom=symptom, fast=fast, include_config=include_config, all_gaps=all_gaps)
2226
2305
  finally:
2227
2306
  _progress.finish()
2228
2307
  _t_total = (_time.perf_counter() - _t0) * 1000
@@ -2532,6 +2611,19 @@ def prepare_context_cmd(
2532
2611
  if llm_prompt:
2533
2612
  out["llm_prompt"] = builder.render_prompt(output)
2534
2613
 
2614
+ # H-01: fast-mode analysis transparency — consumer must not confuse "not analyzed"
2615
+ # with "analyzed and found nothing". Fields that were never computed are absent or null,
2616
+ # not zero. analysis_mode and skipped_analyzers make the omission explicit.
2617
+ if fast:
2618
+ out["analysis_mode"] = "fast"
2619
+ _skipped: list[str] = ["deep_content_scan"]
2620
+ _spec = TASKS.get(task)
2621
+ if _spec and _spec.enable_code_notes:
2622
+ _skipped.append("code_notes")
2623
+ if task == "generate-tests":
2624
+ _skipped.append("test_gap_discovery")
2625
+ out["skipped_analyzers"] = _skipped
2626
+
2535
2627
  # P0-1: Apply output budget per task — safety net for large repos.
2536
2628
  from sourcecode.output_budget import (
2537
2629
  trim_to_budget as _pc_trim,
@@ -2864,6 +2956,9 @@ def impact_cmd(
2864
2956
  sourcecode impact org.keycloak.services.DefaultKeycloakSession /path/to/keycloak
2865
2957
  sourcecode impact UserService --depth 6 --output impact.json
2866
2958
  """
2959
+ from sourcecode.license import require_pro as _require_pro
2960
+ _require_pro("impact")
2961
+
2867
2962
  import json as _json
2868
2963
  import sys as _sys
2869
2964
 
@@ -2924,9 +3019,11 @@ def impact_cmd(
2924
3019
  if _copy_to_clipboard(output):
2925
3020
  typer.echo("✓ copied to clipboard", err=True)
2926
3021
 
2927
- # Non-zero exit when target not found
3022
+ # H-03: resolution=not_found is a valid structured answer, not an infra failure.
3023
+ # Exit 0 so pipelines can parse the JSON without treating it as an error.
3024
+ # Exit 1 is reserved for path-not-found, I/O failures, and real infra errors.
2928
3025
  if result.get("resolution") == "not_found":
2929
- raise typer.Exit(code=1)
3026
+ raise typer.Exit(code=0)
2930
3027
 
2931
3028
  from sourcecode.mcp_nudge import nudge_mcp_if_needed as _nudge
2932
3029
  _nudge()
@@ -3246,6 +3343,9 @@ def modernize_cmd(
3246
3343
  sourcecode onboard . — Architecture overview first
3247
3344
  sourcecode impact <target> — Verify impact before touching a hotspot
3248
3345
  """
3346
+ from sourcecode.license import require_pro as _require_pro
3347
+ _require_pro("modernize")
3348
+
3249
3349
  import json as _json
3250
3350
  import sys as _sys
3251
3351
  from sourcecode.repository_ir import build_repo_ir, find_java_files, apply_ir_size_limits
@@ -3418,6 +3518,24 @@ def modernize_cmd(
3418
3518
 
3419
3519
  # ── version ───────────────────────────────────────────────────────────────────
3420
3520
 
3521
+ @app.command("activate")
3522
+ def activate_cmd(
3523
+ license_key: str = typer.Argument(..., help="Your Pro license key"),
3524
+ ) -> None:
3525
+ """Activate a Pro license key.
3526
+
3527
+ \b
3528
+ Validates the key against the license server and writes
3529
+ ~/.sourcecode/license.json.
3530
+
3531
+ \b
3532
+ Examples:
3533
+ sourcecode activate SC-XXXX-XXXX-XXXX
3534
+ """
3535
+ from sourcecode.license import activate_license as _activate
3536
+ _activate(license_key)
3537
+
3538
+
3421
3539
  @app.command("version")
3422
3540
  def version_cmd() -> None:
3423
3541
  """Show version and exit."""
@@ -3471,6 +3589,9 @@ def mcp_serve() -> None:
3471
3589
  }
3472
3590
  }
3473
3591
  """
3592
+ from sourcecode.license import require_pro as _require_pro
3593
+ _require_pro("mcp serve")
3594
+
3474
3595
  import logging
3475
3596
  import sys as _sys
3476
3597
 
@@ -0,0 +1,166 @@
1
+ """License activation and enforcement for the sourcecode CLI.
2
+
3
+ Flow:
4
+ 1. Module imported → _init() loads ~/.sourcecode/license.json (if present)
5
+ 2. is_pro set globally (True when plan == "pro")
6
+ 3. Pro commands call require_pro(feature_name) at entry — exits 1 if not Pro
7
+ 4. `sourcecode activate <key>` calls activate_license(key) — validates via
8
+ Supabase REST, writes ~/.sourcecode/license.json, exits 0 on success
9
+
10
+ Supabase credentials:
11
+ SOURCECODE_SUPABASE_URL — project REST endpoint
12
+ SOURCECODE_SUPABASE_ANON_KEY — public anon key (not a secret)
13
+ """
14
+ from __future__ import annotations
15
+
16
+ import json
17
+ import os
18
+ import sys
19
+ from datetime import datetime, timezone
20
+ from pathlib import Path
21
+ from typing import Optional
22
+
23
+ # ---------------------------------------------------------------------------
24
+ # Supabase endpoint config — override via env vars
25
+ # ---------------------------------------------------------------------------
26
+ _SUPABASE_URL: str = os.environ.get(
27
+ "SOURCECODE_SUPABASE_URL",
28
+ "https://YOUR_PROJECT.supabase.co",
29
+ )
30
+ _SUPABASE_ANON_KEY: str = os.environ.get(
31
+ "SOURCECODE_SUPABASE_ANON_KEY",
32
+ "",
33
+ )
34
+
35
+ _LICENSE_DIR: Path = Path.home() / ".sourcecode"
36
+ _LICENSE_FILE: Path = _LICENSE_DIR / "license.json"
37
+
38
+ # ---------------------------------------------------------------------------
39
+ # Global license state — loaded once at import time
40
+ # ---------------------------------------------------------------------------
41
+ _license_data: Optional[dict] = None
42
+ is_pro: bool = False
43
+
44
+
45
+ def _load_license_file() -> Optional[dict]:
46
+ """Read ~/.sourcecode/license.json. Returns parsed dict or None."""
47
+ try:
48
+ if _LICENSE_FILE.exists():
49
+ raw = _LICENSE_FILE.read_text(encoding="utf-8")
50
+ return json.loads(raw)
51
+ except Exception:
52
+ pass
53
+ return None
54
+
55
+
56
+ def _init() -> None:
57
+ global _license_data, is_pro
58
+ _license_data = _load_license_file()
59
+ is_pro = (
60
+ _license_data is not None
61
+ and _license_data.get("plan") == "pro"
62
+ )
63
+
64
+
65
+ _init()
66
+
67
+
68
+ # ---------------------------------------------------------------------------
69
+ # Enforcement
70
+ # ---------------------------------------------------------------------------
71
+
72
+ def require_pro(feature_name: str) -> None:
73
+ """Exit with structured JSON error when not Pro.
74
+
75
+ Call at the very top of every Pro-gated command, before any work.
76
+
77
+ Example:
78
+ from sourcecode.license import require_pro
79
+ require_pro("impact")
80
+ """
81
+ if not is_pro:
82
+ payload = {
83
+ "error": "pro_required",
84
+ "feature": feature_name,
85
+ "message": "Run sourcecode activate <license_key>",
86
+ }
87
+ sys.stdout.write(json.dumps(payload, ensure_ascii=False) + "\n")
88
+ sys.stdout.flush()
89
+ sys.exit(1)
90
+
91
+
92
+ # ---------------------------------------------------------------------------
93
+ # Activation
94
+ # ---------------------------------------------------------------------------
95
+
96
+ def activate_license(license_key: str) -> None:
97
+ """Validate license_key via Supabase, write ~/.sourcecode/license.json.
98
+
99
+ Outputs JSON to stdout; exits 0 on success, 1 on any failure.
100
+ Never raises — all error paths emit JSON and call sys.exit(1).
101
+ """
102
+ import urllib.error
103
+ import urllib.request
104
+
105
+ # Bail early when Supabase isn't configured yet
106
+ if not _SUPABASE_ANON_KEY or _SUPABASE_URL == "https://YOUR_PROJECT.supabase.co":
107
+ _fail("configuration_error", "SOURCECODE_SUPABASE_URL / SOURCECODE_SUPABASE_ANON_KEY not configured.")
108
+
109
+ url = (
110
+ f"{_SUPABASE_URL}/rest/v1/users"
111
+ f"?license_key=eq.{license_key}"
112
+ f"&select=license_key,plan,email"
113
+ )
114
+ req = urllib.request.Request(url)
115
+ req.add_header("apikey", _SUPABASE_ANON_KEY)
116
+ req.add_header("Authorization", f"Bearer {_SUPABASE_ANON_KEY}")
117
+ req.add_header("Accept", "application/json")
118
+
119
+ try:
120
+ with urllib.request.urlopen(req, timeout=10) as resp:
121
+ body = resp.read().decode("utf-8")
122
+ except urllib.error.HTTPError as exc:
123
+ _fail("network_error", f"Supabase returned HTTP {exc.code}")
124
+ except Exception as exc:
125
+ _fail("network_error", str(exc))
126
+
127
+ try:
128
+ rows = json.loads(body)
129
+ except Exception:
130
+ _fail("network_error", "Invalid JSON response from Supabase")
131
+
132
+ if not rows:
133
+ _fail("invalid_license", "License key not found")
134
+
135
+ user = rows[0]
136
+ if user.get("plan") != "pro":
137
+ _fail("not_pro", "This license is not Pro")
138
+
139
+ # Write license file
140
+ _LICENSE_DIR.mkdir(parents=True, exist_ok=True)
141
+ data = {
142
+ "license_key": license_key,
143
+ "plan": "pro",
144
+ "email": user.get("email", ""),
145
+ "activated_at": datetime.now(timezone.utc).isoformat(),
146
+ }
147
+ _LICENSE_FILE.write_text(
148
+ json.dumps(data, indent=2, ensure_ascii=False),
149
+ encoding="utf-8",
150
+ )
151
+
152
+ result = {"status": "activated", "plan": "pro"}
153
+ sys.stdout.write(json.dumps(result, ensure_ascii=False) + "\n")
154
+ sys.stdout.flush()
155
+
156
+
157
+ # ---------------------------------------------------------------------------
158
+ # Internal helper
159
+ # ---------------------------------------------------------------------------
160
+
161
+ def _fail(error: str, message: str) -> None:
162
+ """Emit JSON error to stdout and exit 1. Never returns."""
163
+ payload = {"error": error, "message": message}
164
+ sys.stdout.write(json.dumps(payload, ensure_ascii=False) + "\n")
165
+ sys.stdout.flush()
166
+ sys.exit(1)
@@ -88,6 +88,27 @@ def _normalize_repo_path(path: str) -> str:
88
88
  return path
89
89
 
90
90
 
91
+ def _check_repo_path(path: str) -> "CallToolResult | None":
92
+ """H-05: Validate repo_path exists and is a directory before executing.
93
+
94
+ Returns a structured CallToolResult(isError=True) when the path is invalid,
95
+ or None when the path is valid. Must be called after _normalize_repo_path().
96
+ Early validation prevents the MCP server from hanging when the CLI exits
97
+ non-zero with an empty stdout (error went to stderr, not captured by runner).
98
+ """
99
+ if not os.path.exists(path):
100
+ return _err(
101
+ f"directory_not_found: '{path}' does not exist.",
102
+ "DIRECTORY_NOT_FOUND",
103
+ )
104
+ if not os.path.isdir(path):
105
+ return _err(
106
+ f"not_a_directory: '{path}' is not a directory.",
107
+ "NOT_A_DIRECTORY",
108
+ )
109
+ return None
110
+
111
+
91
112
  @mcp.tool()
92
113
  def get_compact_context(repo_path: str = ".", git_context: bool = False) -> dict:
93
114
  """Compact human/LLM summary of a repository (~1000-3000 tokens). USE THIS FIRST.
@@ -108,6 +129,9 @@ def get_compact_context(repo_path: str = ".", git_context: bool = False) -> dict
108
129
  if not isinstance(git_context, bool):
109
130
  return _err("git_context must be boolean", "INVALID_ARGUMENT")
110
131
  repo_path = _normalize_repo_path(repo_path)
132
+ _path_err = _check_repo_path(repo_path)
133
+ if _path_err is not None:
134
+ return _path_err
111
135
  args = [repo_path, "--compact"]
112
136
  if git_context:
113
137
  args.append("--git-context")
@@ -139,6 +163,9 @@ def get_agent_context(repo_path: str = ".", git_context: bool = False) -> dict:
139
163
  if not isinstance(git_context, bool):
140
164
  return _err("git_context must be boolean", "INVALID_ARGUMENT")
141
165
  repo_path = _normalize_repo_path(repo_path)
166
+ _path_err = _check_repo_path(repo_path)
167
+ if _path_err is not None:
168
+ return _path_err
142
169
  args = [repo_path, "--agent"]
143
170
  if git_context:
144
171
  args.append("--git-context")
@@ -174,6 +201,9 @@ def get_endpoints(repo_path: str = ".") -> dict:
174
201
  if not isinstance(repo_path, str):
175
202
  return _err("repo_path must be a string", "INVALID_ARGUMENT")
176
203
  repo_path = _normalize_repo_path(repo_path)
204
+ _path_err = _check_repo_path(repo_path)
205
+ if _path_err is not None:
206
+ return _path_err
177
207
  return _execute(["endpoints", repo_path])
178
208
  except Exception as exc:
179
209
  return _err(
@@ -197,6 +227,9 @@ def get_module_context(repo_path: str = ".", module: str = "") -> dict:
197
227
  if not isinstance(module, str) or not module.strip():
198
228
  return _err("module must be a non-empty string", "INVALID_ARGUMENT")
199
229
  repo_path = _normalize_repo_path(repo_path)
230
+ _path_err = _check_repo_path(repo_path)
231
+ if _path_err is not None:
232
+ return _path_err
200
233
  module_path = repo_path.rstrip("/") + "/" + module.strip("/")
201
234
  return _execute([module_path, "--compact"])
202
235
  except Exception as exc:
@@ -221,6 +254,9 @@ def get_delta(repo_path: str = ".", since: str = "HEAD~1") -> dict:
221
254
  if not isinstance(since, str) or not since.strip():
222
255
  return _err("since must be a non-empty git ref", "INVALID_ARGUMENT")
223
256
  repo_path = _normalize_repo_path(repo_path)
257
+ _path_err = _check_repo_path(repo_path)
258
+ if _path_err is not None:
259
+ return _path_err
224
260
  return _execute(["prepare-context", "delta", repo_path, "--since", since])
225
261
  except Exception as exc:
226
262
  return _err(
@@ -248,6 +284,9 @@ def get_ir_summary(repo_path: str = ".") -> dict:
248
284
  if not isinstance(repo_path, str):
249
285
  return _err("repo_path must be a string", "INVALID_ARGUMENT")
250
286
  repo_path = _normalize_repo_path(repo_path)
287
+ _path_err = _check_repo_path(repo_path)
288
+ if _path_err is not None:
289
+ return _path_err
251
290
  return _execute(["repo-ir", repo_path, "--summary-only"])
252
291
  except Exception as exc:
253
292
  return _err(
@@ -271,6 +310,9 @@ def fix_bug_context(repo_path: str = ".", symptom: str = "") -> dict:
271
310
  if not isinstance(repo_path, str):
272
311
  return _err("repo_path must be a string", "INVALID_ARGUMENT")
273
312
  repo_path = _normalize_repo_path(repo_path)
313
+ _path_err = _check_repo_path(repo_path)
314
+ if _path_err is not None:
315
+ return _path_err
274
316
  args = ["prepare-context", "fix-bug", repo_path]
275
317
  if symptom and isinstance(symptom, str) and symptom.strip():
276
318
  args.extend(["--symptom", symptom.strip()])
@@ -297,6 +339,9 @@ def review_pr_context(repo_path: str = ".", since: str = "") -> dict:
297
339
  if not isinstance(repo_path, str):
298
340
  return _err("repo_path must be a string", "INVALID_ARGUMENT")
299
341
  repo_path = _normalize_repo_path(repo_path)
342
+ _path_err = _check_repo_path(repo_path)
343
+ if _path_err is not None:
344
+ return _path_err
300
345
  args = ["prepare-context", "review-pr", repo_path]
301
346
  if since and isinstance(since, str) and since.strip():
302
347
  args.extend(["--since", since.strip()])
@@ -320,6 +365,9 @@ def onboard_context(repo_path: str = ".") -> dict:
320
365
  if not isinstance(repo_path, str):
321
366
  return _err("repo_path must be a string", "INVALID_ARGUMENT")
322
367
  repo_path = _normalize_repo_path(repo_path)
368
+ _path_err = _check_repo_path(repo_path)
369
+ if _path_err is not None:
370
+ return _path_err
323
371
  return _execute(["prepare-context", "onboard", repo_path])
324
372
  except Exception as exc:
325
373
  return _err(
@@ -341,6 +389,9 @@ def explain_context(repo_path: str = ".") -> dict:
341
389
  if not isinstance(repo_path, str):
342
390
  return _err("repo_path must be a string", "INVALID_ARGUMENT")
343
391
  repo_path = _normalize_repo_path(repo_path)
392
+ _path_err = _check_repo_path(repo_path)
393
+ if _path_err is not None:
394
+ return _path_err
344
395
  return _execute(["prepare-context", "explain", repo_path])
345
396
  except Exception as exc:
346
397
  return _err(
@@ -362,6 +413,9 @@ def refactor_context(repo_path: str = ".") -> dict:
362
413
  if not isinstance(repo_path, str):
363
414
  return _err("repo_path must be a string", "INVALID_ARGUMENT")
364
415
  repo_path = _normalize_repo_path(repo_path)
416
+ _path_err = _check_repo_path(repo_path)
417
+ if _path_err is not None:
418
+ return _path_err
365
419
  return _execute(["prepare-context", "refactor", repo_path])
366
420
  except Exception as exc:
367
421
  return _err(
@@ -388,6 +442,9 @@ def generate_tests_context(repo_path: str = ".", include_all: bool = False) -> d
388
442
  if not isinstance(include_all, bool):
389
443
  return _err("include_all must be boolean", "INVALID_ARGUMENT")
390
444
  repo_path = _normalize_repo_path(repo_path)
445
+ _path_err = _check_repo_path(repo_path)
446
+ if _path_err is not None:
447
+ return _path_err
391
448
  args = ["prepare-context", "generate-tests", repo_path]
392
449
  if include_all:
393
450
  args.append("--all")
@@ -444,6 +501,9 @@ def get_impact_context(repo_path: str = ".", target: str = "", depth: int = 4) -
444
501
  if not isinstance(depth, int) or depth < 1 or depth > 8:
445
502
  return _err("depth must be an integer between 1 and 8", "INVALID_ARGUMENT")
446
503
  repo_path = _normalize_repo_path(repo_path)
504
+ _path_err = _check_repo_path(repo_path)
505
+ if _path_err is not None:
506
+ return _path_err
447
507
  args = ["impact", target.strip(), repo_path, "--depth", str(depth)]
448
508
  return _execute(args)
449
509
  except Exception as exc:
@@ -475,6 +535,9 @@ def modernize_context(repo_path: str = ".", format: str = "json") -> dict:
475
535
  if not isinstance(format, str) or format not in ("json", "yaml"):
476
536
  return _err("format must be 'json' or 'yaml'", "INVALID_ARGUMENT")
477
537
  repo_path = _normalize_repo_path(repo_path)
538
+ _path_err = _check_repo_path(repo_path)
539
+ if _path_err is not None:
540
+ return _path_err
478
541
  return _execute(["modernize", repo_path])
479
542
  except Exception as exc:
480
543
  return _err(
@@ -1524,6 +1524,31 @@ def _get_git_old_content(git_root: Path, rel_path: str, since: str) -> Optional[
1524
1524
  return None
1525
1525
 
1526
1526
 
1527
+ def _get_git_changed_files(root: "Path", since: str) -> "Optional[frozenset[str]]":
1528
+ """H-04: Return set of paths changed between `since` and HEAD, or None on failure.
1529
+
1530
+ One `git diff --name-only` call replaces O(n) `git show` calls — only files
1531
+ in the returned set need old-content fetched for symbol diff computation.
1532
+ Returns None when git is unavailable or the ref cannot be resolved; the
1533
+ caller must fall back to the original per-file fetch in that case.
1534
+ """
1535
+ try:
1536
+ result = subprocess.run(
1537
+ ["git", "diff", "--name-only", since, "HEAD"],
1538
+ cwd=str(root),
1539
+ capture_output=True,
1540
+ text=True,
1541
+ encoding="utf-8",
1542
+ errors="replace",
1543
+ timeout=10,
1544
+ )
1545
+ if result.returncode == 0:
1546
+ return frozenset(p.strip() for p in result.stdout.splitlines() if p.strip())
1547
+ except (subprocess.TimeoutExpired, OSError, FileNotFoundError):
1548
+ pass
1549
+ return None
1550
+
1551
+
1527
1552
  # ---------------------------------------------------------------------------
1528
1553
  # Phase 5 — Evidence Engine
1529
1554
  # ---------------------------------------------------------------------------
@@ -2511,6 +2536,12 @@ def build_repo_ir(
2511
2536
  all_changed: list[ChangedSymbol] = []
2512
2537
  all_route_diffs: list[dict] = []
2513
2538
 
2539
+ # H-04: prefetch changed-file list once; avoids O(n) `git show` calls.
2540
+ # _since_changed=None means git unavailable → fall back to per-file fetch.
2541
+ _since_changed: "Optional[frozenset[str]]" = None
2542
+ if since:
2543
+ _since_changed = _get_git_changed_files(root, since)
2544
+
2514
2545
  for rel_path in sorted(file_paths):
2515
2546
  abs_path = root / rel_path
2516
2547
  try:
@@ -2520,7 +2551,11 @@ def build_repo_ir(
2520
2551
 
2521
2552
  old_source: Optional[str] = None
2522
2553
  if since:
2523
- old_source = _get_git_old_content(root, rel_path, since)
2554
+ # Only fetch old content for files known to have changed.
2555
+ # Unchanged files have no diff entries — skip git show entirely.
2556
+ _file_changed = _since_changed is None or rel_path in _since_changed
2557
+ if _file_changed:
2558
+ old_source = _get_git_old_content(root, rel_path, since)
2524
2559
 
2525
2560
  package, symbols, raw_imports = _extract_symbols(source, rel_path)
2526
2561
  relations = _build_relations(symbols, raw_imports, source, package, rel_path)
@@ -2529,7 +2564,8 @@ def build_repo_ir(
2529
2564
  _, old_symbols, _ = _extract_symbols(old_source, rel_path)
2530
2565
  all_changed.extend(_diff_symbols(old_symbols, symbols))
2531
2566
  all_route_diffs.extend(_diff_routes(old_symbols, symbols))
2532
- elif since:
2567
+ elif since and (_since_changed is None or rel_path in _since_changed):
2568
+ # File is new in since..HEAD (not in old ref) — treat as added.
2533
2569
  for sym in symbols:
2534
2570
  all_changed.append(ChangedSymbol(
2535
2571
  symbol=sym.symbol,