sourcecode 1.31.31__tar.gz → 1.31.32__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 (234) hide show
  1. {sourcecode-1.31.31 → sourcecode-1.31.32}/PKG-INFO +3 -3
  2. {sourcecode-1.31.31 → sourcecode-1.31.32}/README.md +2 -2
  3. {sourcecode-1.31.31 → sourcecode-1.31.32}/pyproject.toml +1 -1
  4. {sourcecode-1.31.31 → sourcecode-1.31.32}/src/sourcecode/__init__.py +1 -1
  5. {sourcecode-1.31.31 → sourcecode-1.31.32}/src/sourcecode/cache.py +1 -0
  6. {sourcecode-1.31.31 → sourcecode-1.31.32}/src/sourcecode/cli.py +43 -13
  7. {sourcecode-1.31.31 → sourcecode-1.31.32}/src/sourcecode/env_analyzer.py +6 -1
  8. {sourcecode-1.31.31 → sourcecode-1.31.32}/src/sourcecode/git_analyzer.py +3 -0
  9. sourcecode-1.31.32/src/sourcecode/license.py +229 -0
  10. {sourcecode-1.31.31 → sourcecode-1.31.32}/src/sourcecode/mcp/onboarding/applier.py +14 -6
  11. {sourcecode-1.31.31 → sourcecode-1.31.32}/src/sourcecode/mcp/runner.py +2 -2
  12. {sourcecode-1.31.31 → sourcecode-1.31.32}/src/sourcecode/mcp/server.py +17 -14
  13. {sourcecode-1.31.31 → sourcecode-1.31.32}/src/sourcecode/mcp_nudge.py +6 -1
  14. {sourcecode-1.31.31 → sourcecode-1.31.32}/src/sourcecode/redactor.py +4 -0
  15. {sourcecode-1.31.31 → sourcecode-1.31.32}/src/sourcecode/repository_ir.py +1 -1
  16. {sourcecode-1.31.31 → sourcecode-1.31.32}/src/sourcecode/serializer.py +1 -1
  17. sourcecode-1.31.31/src/sourcecode/license.py +0 -166
  18. {sourcecode-1.31.31 → sourcecode-1.31.32}/.agents/skills/source-command-gsd-join-discord/SKILL.md +0 -0
  19. {sourcecode-1.31.31 → sourcecode-1.31.32}/.agents/skills/source-command-gsd-review-backlog/SKILL.md +0 -0
  20. {sourcecode-1.31.31 → sourcecode-1.31.32}/.agents/skills/source-command-gsd-workstreams/SKILL.md +0 -0
  21. {sourcecode-1.31.31 → sourcecode-1.31.32}/.continue-here.md +0 -0
  22. {sourcecode-1.31.31 → sourcecode-1.31.32}/.github/workflows/build-windows.yml +0 -0
  23. {sourcecode-1.31.31 → sourcecode-1.31.32}/.gitignore +0 -0
  24. {sourcecode-1.31.31 → sourcecode-1.31.32}/.ruff.toml +0 -0
  25. {sourcecode-1.31.31 → sourcecode-1.31.32}/.sourcecode-cache/snapshot-3b5997a-fa5c742c.json +0 -0
  26. {sourcecode-1.31.31 → sourcecode-1.31.32}/AUDIT_REAL_REPOS.md +0 -0
  27. {sourcecode-1.31.31 → sourcecode-1.31.32}/AUDIT_v1.31.23.md +0 -0
  28. {sourcecode-1.31.31 → sourcecode-1.31.32}/CHANGELOG.md +0 -0
  29. {sourcecode-1.31.31 → sourcecode-1.31.32}/CONTRIBUTING.md +0 -0
  30. {sourcecode-1.31.31 → sourcecode-1.31.32}/LICENSE +0 -0
  31. {sourcecode-1.31.31 → sourcecode-1.31.32}/SECURITY.md +0 -0
  32. {sourcecode-1.31.31 → sourcecode-1.31.32}/docs/PRODUCT_TIERS.md +0 -0
  33. {sourcecode-1.31.31 → sourcecode-1.31.32}/docs/privacy.md +0 -0
  34. {sourcecode-1.31.31 → sourcecode-1.31.32}/docs/schema.md +0 -0
  35. {sourcecode-1.31.31 → sourcecode-1.31.32}/raw +0 -0
  36. {sourcecode-1.31.31 → sourcecode-1.31.32}/run_cli.py +0 -0
  37. {sourcecode-1.31.31 → sourcecode-1.31.32}/src/sourcecode/adaptive_scanner.py +0 -0
  38. {sourcecode-1.31.31 → sourcecode-1.31.32}/src/sourcecode/architecture_analyzer.py +0 -0
  39. {sourcecode-1.31.31 → sourcecode-1.31.32}/src/sourcecode/architecture_summary.py +0 -0
  40. {sourcecode-1.31.31 → sourcecode-1.31.32}/src/sourcecode/ast_extractor.py +0 -0
  41. {sourcecode-1.31.31 → sourcecode-1.31.32}/src/sourcecode/canonical_ir.py +0 -0
  42. {sourcecode-1.31.31 → sourcecode-1.31.32}/src/sourcecode/classifier.py +0 -0
  43. {sourcecode-1.31.31 → sourcecode-1.31.32}/src/sourcecode/code_notes_analyzer.py +0 -0
  44. {sourcecode-1.31.31 → sourcecode-1.31.32}/src/sourcecode/confidence_analyzer.py +0 -0
  45. {sourcecode-1.31.31 → sourcecode-1.31.32}/src/sourcecode/context_scorer.py +0 -0
  46. {sourcecode-1.31.31 → sourcecode-1.31.32}/src/sourcecode/context_summarizer.py +0 -0
  47. {sourcecode-1.31.31 → sourcecode-1.31.32}/src/sourcecode/contract_model.py +0 -0
  48. {sourcecode-1.31.31 → sourcecode-1.31.32}/src/sourcecode/contract_pipeline.py +0 -0
  49. {sourcecode-1.31.31 → sourcecode-1.31.32}/src/sourcecode/coverage_parser.py +0 -0
  50. {sourcecode-1.31.31 → sourcecode-1.31.32}/src/sourcecode/dependency_analyzer.py +0 -0
  51. {sourcecode-1.31.31 → sourcecode-1.31.32}/src/sourcecode/detectors/__init__.py +0 -0
  52. {sourcecode-1.31.31 → sourcecode-1.31.32}/src/sourcecode/detectors/base.py +0 -0
  53. {sourcecode-1.31.31 → sourcecode-1.31.32}/src/sourcecode/detectors/csproj_parser.py +0 -0
  54. {sourcecode-1.31.31 → sourcecode-1.31.32}/src/sourcecode/detectors/dart.py +0 -0
  55. {sourcecode-1.31.31 → sourcecode-1.31.32}/src/sourcecode/detectors/dotnet.py +0 -0
  56. {sourcecode-1.31.31 → sourcecode-1.31.32}/src/sourcecode/detectors/elixir.py +0 -0
  57. {sourcecode-1.31.31 → sourcecode-1.31.32}/src/sourcecode/detectors/go.py +0 -0
  58. {sourcecode-1.31.31 → sourcecode-1.31.32}/src/sourcecode/detectors/heuristic.py +0 -0
  59. {sourcecode-1.31.31 → sourcecode-1.31.32}/src/sourcecode/detectors/hybrid.py +0 -0
  60. {sourcecode-1.31.31 → sourcecode-1.31.32}/src/sourcecode/detectors/java.py +0 -0
  61. {sourcecode-1.31.31 → sourcecode-1.31.32}/src/sourcecode/detectors/jvm_ext.py +0 -0
  62. {sourcecode-1.31.31 → sourcecode-1.31.32}/src/sourcecode/detectors/nodejs.py +0 -0
  63. {sourcecode-1.31.31 → sourcecode-1.31.32}/src/sourcecode/detectors/parsers.py +0 -0
  64. {sourcecode-1.31.31 → sourcecode-1.31.32}/src/sourcecode/detectors/php.py +0 -0
  65. {sourcecode-1.31.31 → sourcecode-1.31.32}/src/sourcecode/detectors/project.py +0 -0
  66. {sourcecode-1.31.31 → sourcecode-1.31.32}/src/sourcecode/detectors/python.py +0 -0
  67. {sourcecode-1.31.31 → sourcecode-1.31.32}/src/sourcecode/detectors/ruby.py +0 -0
  68. {sourcecode-1.31.31 → sourcecode-1.31.32}/src/sourcecode/detectors/rust.py +0 -0
  69. {sourcecode-1.31.31 → sourcecode-1.31.32}/src/sourcecode/detectors/systems.py +0 -0
  70. {sourcecode-1.31.31 → sourcecode-1.31.32}/src/sourcecode/detectors/terraform.py +0 -0
  71. {sourcecode-1.31.31 → sourcecode-1.31.32}/src/sourcecode/detectors/tooling.py +0 -0
  72. {sourcecode-1.31.31 → sourcecode-1.31.32}/src/sourcecode/doc_analyzer.py +0 -0
  73. {sourcecode-1.31.31 → sourcecode-1.31.32}/src/sourcecode/entrypoint_classifier.py +0 -0
  74. {sourcecode-1.31.31 → sourcecode-1.31.32}/src/sourcecode/file_classifier.py +0 -0
  75. {sourcecode-1.31.31 → sourcecode-1.31.32}/src/sourcecode/flow_analyzer.py +0 -0
  76. {sourcecode-1.31.31 → sourcecode-1.31.32}/src/sourcecode/graph_analyzer.py +0 -0
  77. {sourcecode-1.31.31 → sourcecode-1.31.32}/src/sourcecode/mcp/__init__.py +0 -0
  78. {sourcecode-1.31.31 → sourcecode-1.31.32}/src/sourcecode/mcp/onboarding/__init__.py +0 -0
  79. {sourcecode-1.31.31 → sourcecode-1.31.32}/src/sourcecode/mcp/onboarding/backup.py +0 -0
  80. {sourcecode-1.31.31 → sourcecode-1.31.32}/src/sourcecode/mcp/onboarding/detector.py +0 -0
  81. {sourcecode-1.31.31 → sourcecode-1.31.32}/src/sourcecode/mcp/onboarding/planner.py +0 -0
  82. {sourcecode-1.31.31 → sourcecode-1.31.32}/src/sourcecode/metrics_analyzer.py +0 -0
  83. {sourcecode-1.31.31 → sourcecode-1.31.32}/src/sourcecode/output_budget.py +0 -0
  84. {sourcecode-1.31.31 → sourcecode-1.31.32}/src/sourcecode/path_filters.py +0 -0
  85. {sourcecode-1.31.31 → sourcecode-1.31.32}/src/sourcecode/pr_comment_renderer.py +0 -0
  86. {sourcecode-1.31.31 → sourcecode-1.31.32}/src/sourcecode/prepare_context.py +0 -0
  87. {sourcecode-1.31.31 → sourcecode-1.31.32}/src/sourcecode/progress.py +0 -0
  88. {sourcecode-1.31.31 → sourcecode-1.31.32}/src/sourcecode/ranking_engine.py +0 -0
  89. {sourcecode-1.31.31 → sourcecode-1.31.32}/src/sourcecode/relevance_scorer.py +0 -0
  90. {sourcecode-1.31.31 → sourcecode-1.31.32}/src/sourcecode/repo_classifier.py +0 -0
  91. {sourcecode-1.31.31 → sourcecode-1.31.32}/src/sourcecode/runtime_classifier.py +0 -0
  92. {sourcecode-1.31.31 → sourcecode-1.31.32}/src/sourcecode/scanner.py +0 -0
  93. {sourcecode-1.31.31 → sourcecode-1.31.32}/src/sourcecode/schema.py +0 -0
  94. {sourcecode-1.31.31 → sourcecode-1.31.32}/src/sourcecode/semantic_analyzer.py +0 -0
  95. {sourcecode-1.31.31 → sourcecode-1.31.32}/src/sourcecode/summarizer.py +0 -0
  96. {sourcecode-1.31.31 → sourcecode-1.31.32}/src/sourcecode/telemetry/__init__.py +0 -0
  97. {sourcecode-1.31.31 → sourcecode-1.31.32}/src/sourcecode/telemetry/config.py +0 -0
  98. {sourcecode-1.31.31 → sourcecode-1.31.32}/src/sourcecode/telemetry/consent.py +0 -0
  99. {sourcecode-1.31.31 → sourcecode-1.31.32}/src/sourcecode/telemetry/events.py +0 -0
  100. {sourcecode-1.31.31 → sourcecode-1.31.32}/src/sourcecode/telemetry/filters.py +0 -0
  101. {sourcecode-1.31.31 → sourcecode-1.31.32}/src/sourcecode/telemetry/transport.py +0 -0
  102. {sourcecode-1.31.31 → sourcecode-1.31.32}/src/sourcecode/tree_utils.py +0 -0
  103. {sourcecode-1.31.31 → sourcecode-1.31.32}/src/sourcecode/workspace.py +0 -0
  104. {sourcecode-1.31.31 → sourcecode-1.31.32}/tests/__init__.py +0 -0
  105. {sourcecode-1.31.31 → sourcecode-1.31.32}/tests/conftest.py +0 -0
  106. {sourcecode-1.31.31 → sourcecode-1.31.32}/tests/fixtures/coverage.xml +0 -0
  107. {sourcecode-1.31.31 → sourcecode-1.31.32}/tests/fixtures/fastapi_app/pyproject.toml +0 -0
  108. {sourcecode-1.31.31 → sourcecode-1.31.32}/tests/fixtures/fastapi_app/src/main.py +0 -0
  109. {sourcecode-1.31.31 → sourcecode-1.31.32}/tests/fixtures/go_service/cmd/api/main.go +0 -0
  110. {sourcecode-1.31.31 → sourcecode-1.31.32}/tests/fixtures/go_service/go.mod +0 -0
  111. {sourcecode-1.31.31 → sourcecode-1.31.32}/tests/fixtures/jacoco.xml +0 -0
  112. {sourcecode-1.31.31 → sourcecode-1.31.32}/tests/fixtures/latin1_sample.java +0 -0
  113. {sourcecode-1.31.31 → sourcecode-1.31.32}/tests/fixtures/latin1_sample_iso.java +0 -0
  114. {sourcecode-1.31.31 → sourcecode-1.31.32}/tests/fixtures/lcov.info +0 -0
  115. {sourcecode-1.31.31 → sourcecode-1.31.32}/tests/fixtures/nextjs_app/app/page.tsx +0 -0
  116. {sourcecode-1.31.31 → sourcecode-1.31.32}/tests/fixtures/nextjs_app/package.json +0 -0
  117. {sourcecode-1.31.31 → sourcecode-1.31.32}/tests/fixtures/nextjs_app/pnpm-lock.yaml +0 -0
  118. {sourcecode-1.31.31 → sourcecode-1.31.32}/tests/fixtures/pnpm_monorepo/apps/web/app/page.tsx +0 -0
  119. {sourcecode-1.31.31 → sourcecode-1.31.32}/tests/fixtures/pnpm_monorepo/apps/web/package.json +0 -0
  120. {sourcecode-1.31.31 → sourcecode-1.31.32}/tests/fixtures/pnpm_monorepo/packages/api/main.py +0 -0
  121. {sourcecode-1.31.31 → sourcecode-1.31.32}/tests/fixtures/pnpm_monorepo/packages/api/pyproject.toml +0 -0
  122. {sourcecode-1.31.31 → sourcecode-1.31.32}/tests/fixtures/pnpm_monorepo/pnpm-workspace.yaml +0 -0
  123. {sourcecode-1.31.31 → sourcecode-1.31.32}/tests/fixtures/spring_boot_minimal/pom.xml +0 -0
  124. {sourcecode-1.31.31 → sourcecode-1.31.32}/tests/fixtures/spring_boot_minimal/src/main/java/com/example/ddd/ausente/application/service/FindAusenteService.java +0 -0
  125. {sourcecode-1.31.31 → sourcecode-1.31.32}/tests/fixtures/spring_boot_minimal/src/main/java/com/example/ddd/ausente/domain/entities/Ausente.java +0 -0
  126. {sourcecode-1.31.31 → sourcecode-1.31.32}/tests/fixtures/spring_boot_minimal/src/main/java/com/example/ddd/ausente/infrastructure/rest/AusenteRestController.java +0 -0
  127. {sourcecode-1.31.31 → sourcecode-1.31.32}/tests/fixtures/spring_boot_minimal/src/main/java/com/example/ddd/autocoberturas/application/service/FindAutocoberturasService.java +0 -0
  128. {sourcecode-1.31.31 → sourcecode-1.31.32}/tests/fixtures/spring_boot_minimal/src/main/java/com/example/ddd/autocoberturas/domain/entities/Autocoberturas.java +0 -0
  129. {sourcecode-1.31.31 → sourcecode-1.31.32}/tests/fixtures/spring_boot_minimal/src/main/java/com/example/ddd/autocoberturas/infrastructure/rest/AutocoberturasRestController.java +0 -0
  130. {sourcecode-1.31.31 → sourcecode-1.31.32}/tests/fixtures/spring_boot_minimal/src/main/java/com/example/ddd/calendarioTrabajador/application/service/FindCalendarioTrabajadorService.java +0 -0
  131. {sourcecode-1.31.31 → sourcecode-1.31.32}/tests/fixtures/spring_boot_minimal/src/main/java/com/example/ddd/calendarioTrabajador/domain/entities/CalendarioTrabajador.java +0 -0
  132. {sourcecode-1.31.31 → sourcecode-1.31.32}/tests/fixtures/spring_boot_minimal/src/main/java/com/example/ddd/calendarioTrabajador/infrastructure/rest/CalendarioTrabajadorRestController.java +0 -0
  133. {sourcecode-1.31.31 → sourcecode-1.31.32}/tests/fixtures/spring_boot_minimal/src/main/java/com/example/ddd/departamento/application/service/FindDepartamentoService.java +0 -0
  134. {sourcecode-1.31.31 → sourcecode-1.31.32}/tests/fixtures/spring_boot_minimal/src/main/java/com/example/ddd/departamento/domain/entities/Departamento.java +0 -0
  135. {sourcecode-1.31.31 → sourcecode-1.31.32}/tests/fixtures/spring_boot_minimal/src/main/java/com/example/ddd/departamento/infrastructure/rest/DepartamentoRestController.java +0 -0
  136. {sourcecode-1.31.31 → sourcecode-1.31.32}/tests/fixtures/spring_boot_minimal/src/main/java/com/example/ddd/empleado/application/service/FindEmpleadoService.java +0 -0
  137. {sourcecode-1.31.31 → sourcecode-1.31.32}/tests/fixtures/spring_boot_minimal/src/main/java/com/example/ddd/empleado/domain/entities/Empleado.java +0 -0
  138. {sourcecode-1.31.31 → sourcecode-1.31.32}/tests/fixtures/spring_boot_minimal/src/main/java/com/example/ddd/empleado/infrastructure/rest/EmpleadoRestController.java +0 -0
  139. {sourcecode-1.31.31 → sourcecode-1.31.32}/tests/fixtures/spring_boot_minimal/src/main/java/com/example/demo/DemoApplication.java +0 -0
  140. {sourcecode-1.31.31 → sourcecode-1.31.32}/tests/fixtures/spring_boot_minimal/src/main/java/com/example/demo/config/FilterConfig.java +0 -0
  141. {sourcecode-1.31.31 → sourcecode-1.31.32}/tests/fixtures/spring_boot_minimal/src/main/java/com/example/demo/domain/Health.java +0 -0
  142. {sourcecode-1.31.31 → sourcecode-1.31.32}/tests/fixtures/spring_boot_minimal/src/main/java/com/example/demo/mapper/HealthMapper.java +0 -0
  143. {sourcecode-1.31.31 → sourcecode-1.31.32}/tests/fixtures/spring_boot_minimal/src/main/java/com/example/demo/repository/HealthRepository.java +0 -0
  144. {sourcecode-1.31.31 → sourcecode-1.31.32}/tests/fixtures/spring_boot_minimal/src/main/java/com/example/demo/service/HealthService.java +0 -0
  145. {sourcecode-1.31.31 → sourcecode-1.31.32}/tests/fixtures/spring_boot_minimal/src/main/java/com/example/demo/web/HealthRestController.java +0 -0
  146. {sourcecode-1.31.31 → sourcecode-1.31.32}/tests/fixtures/spring_boot_minimal/src/main/java/com/example/demo/web/NominaRestController.java +0 -0
  147. {sourcecode-1.31.31 → sourcecode-1.31.32}/tests/fixtures/spring_boot_minimal/src/main/resources/application-dev.yml +0 -0
  148. {sourcecode-1.31.31 → sourcecode-1.31.32}/tests/fixtures/spring_boot_minimal/src/main/resources/application.yml +0 -0
  149. {sourcecode-1.31.31 → sourcecode-1.31.32}/tests/fixtures/spring_boot_minimal/src/main/resources/mapper/HealthMapper.xml +0 -0
  150. {sourcecode-1.31.31 → sourcecode-1.31.32}/tests/test_architecture_analyzer.py +0 -0
  151. {sourcecode-1.31.31 → sourcecode-1.31.32}/tests/test_architecture_summary.py +0 -0
  152. {sourcecode-1.31.31 → sourcecode-1.31.32}/tests/test_ast_extractor.py +0 -0
  153. {sourcecode-1.31.31 → sourcecode-1.31.32}/tests/test_audit_fixes.py +0 -0
  154. {sourcecode-1.31.31 → sourcecode-1.31.32}/tests/test_audit_sas_v2.py +0 -0
  155. {sourcecode-1.31.31 → sourcecode-1.31.32}/tests/test_block1_reliability.py +0 -0
  156. {sourcecode-1.31.31 → sourcecode-1.31.32}/tests/test_block2_coverage.py +0 -0
  157. {sourcecode-1.31.31 → sourcecode-1.31.32}/tests/test_block5_quality.py +0 -0
  158. {sourcecode-1.31.31 → sourcecode-1.31.32}/tests/test_broadleaf_fixes.py +0 -0
  159. {sourcecode-1.31.31 → sourcecode-1.31.32}/tests/test_bug_fixes_v1302.py +0 -0
  160. {sourcecode-1.31.31 → sourcecode-1.31.32}/tests/test_bug_fixes_v13115.py +0 -0
  161. {sourcecode-1.31.31 → sourcecode-1.31.32}/tests/test_bug_fixes_v1312.py +0 -0
  162. {sourcecode-1.31.31 → sourcecode-1.31.32}/tests/test_bug_fixes_v13122.py +0 -0
  163. {sourcecode-1.31.31 → sourcecode-1.31.32}/tests/test_bug_fixes_v1313.py +0 -0
  164. {sourcecode-1.31.31 → sourcecode-1.31.32}/tests/test_bug_fixes_v13130.py +0 -0
  165. {sourcecode-1.31.31 → sourcecode-1.31.32}/tests/test_bug_fixes_v1321.py +0 -0
  166. {sourcecode-1.31.31 → sourcecode-1.31.32}/tests/test_bug_fixes_v16.py +0 -0
  167. {sourcecode-1.31.31 → sourcecode-1.31.32}/tests/test_bug_fixes_v2.py +0 -0
  168. {sourcecode-1.31.31 → sourcecode-1.31.32}/tests/test_cache.py +0 -0
  169. {sourcecode-1.31.31 → sourcecode-1.31.32}/tests/test_canonical_ir.py +0 -0
  170. {sourcecode-1.31.31 → sourcecode-1.31.32}/tests/test_classifier.py +0 -0
  171. {sourcecode-1.31.31 → sourcecode-1.31.32}/tests/test_cli.py +0 -0
  172. {sourcecode-1.31.31 → sourcecode-1.31.32}/tests/test_code_notes_analyzer.py +0 -0
  173. {sourcecode-1.31.31 → sourcecode-1.31.32}/tests/test_context_scorer.py +0 -0
  174. {sourcecode-1.31.31 → sourcecode-1.31.32}/tests/test_contract_pipeline.py +0 -0
  175. {sourcecode-1.31.31 → sourcecode-1.31.32}/tests/test_coverage_parser.py +0 -0
  176. {sourcecode-1.31.31 → sourcecode-1.31.32}/tests/test_cross_consistency.py +0 -0
  177. {sourcecode-1.31.31 → sourcecode-1.31.32}/tests/test_dependency_analyzer_node_python.py +0 -0
  178. {sourcecode-1.31.31 → sourcecode-1.31.32}/tests/test_dependency_analyzer_polyglot.py +0 -0
  179. {sourcecode-1.31.31 → sourcecode-1.31.32}/tests/test_dependency_schema.py +0 -0
  180. {sourcecode-1.31.31 → sourcecode-1.31.32}/tests/test_detector_dotnet.py +0 -0
  181. {sourcecode-1.31.31 → sourcecode-1.31.32}/tests/test_detector_go_rust_java.py +0 -0
  182. {sourcecode-1.31.31 → sourcecode-1.31.32}/tests/test_detector_nodejs.py +0 -0
  183. {sourcecode-1.31.31 → sourcecode-1.31.32}/tests/test_detector_php_ruby_dart.py +0 -0
  184. {sourcecode-1.31.31 → sourcecode-1.31.32}/tests/test_detector_python.py +0 -0
  185. {sourcecode-1.31.31 → sourcecode-1.31.32}/tests/test_detector_universal_managed.py +0 -0
  186. {sourcecode-1.31.31 → sourcecode-1.31.32}/tests/test_detector_universal_systems.py +0 -0
  187. {sourcecode-1.31.31 → sourcecode-1.31.32}/tests/test_detectors_base.py +0 -0
  188. {sourcecode-1.31.31 → sourcecode-1.31.32}/tests/test_doc_analyzer_jsdom.py +0 -0
  189. {sourcecode-1.31.31 → sourcecode-1.31.32}/tests/test_doc_analyzer_python.py +0 -0
  190. {sourcecode-1.31.31 → sourcecode-1.31.32}/tests/test_encoding_regression.py +0 -0
  191. {sourcecode-1.31.31 → sourcecode-1.31.32}/tests/test_enterprise_benchmarks.py +0 -0
  192. {sourcecode-1.31.31 → sourcecode-1.31.32}/tests/test_graph_analyzer_polyglot.py +0 -0
  193. {sourcecode-1.31.31 → sourcecode-1.31.32}/tests/test_graph_analyzer_python_node.py +0 -0
  194. {sourcecode-1.31.31 → sourcecode-1.31.32}/tests/test_graph_schema.py +0 -0
  195. {sourcecode-1.31.31 → sourcecode-1.31.32}/tests/test_hybrid_inference.py +0 -0
  196. {sourcecode-1.31.31 → sourcecode-1.31.32}/tests/test_integration.py +0 -0
  197. {sourcecode-1.31.31 → sourcecode-1.31.32}/tests/test_integration_dependencies.py +0 -0
  198. {sourcecode-1.31.31 → sourcecode-1.31.32}/tests/test_integration_detection.py +0 -0
  199. {sourcecode-1.31.31 → sourcecode-1.31.32}/tests/test_integration_docs.py +0 -0
  200. {sourcecode-1.31.31 → sourcecode-1.31.32}/tests/test_integration_graph_modules.py +0 -0
  201. {sourcecode-1.31.31 → sourcecode-1.31.32}/tests/test_integration_lqn.py +0 -0
  202. {sourcecode-1.31.31 → sourcecode-1.31.32}/tests/test_integration_metrics.py +0 -0
  203. {sourcecode-1.31.31 → sourcecode-1.31.32}/tests/test_integration_multistack.py +0 -0
  204. {sourcecode-1.31.31 → sourcecode-1.31.32}/tests/test_integration_semantics.py +0 -0
  205. {sourcecode-1.31.31 → sourcecode-1.31.32}/tests/test_integration_universal.py +0 -0
  206. {sourcecode-1.31.31 → sourcecode-1.31.32}/tests/test_java_spring_integration.py +0 -0
  207. {sourcecode-1.31.31 → sourcecode-1.31.32}/tests/test_mcp_nudge.py +0 -0
  208. {sourcecode-1.31.31 → sourcecode-1.31.32}/tests/test_mcp_runner.py +0 -0
  209. {sourcecode-1.31.31 → sourcecode-1.31.32}/tests/test_mcp_serve.py +0 -0
  210. {sourcecode-1.31.31 → sourcecode-1.31.32}/tests/test_mcp_tools.py +0 -0
  211. {sourcecode-1.31.31 → sourcecode-1.31.32}/tests/test_metrics_analyzer.py +0 -0
  212. {sourcecode-1.31.31 → sourcecode-1.31.32}/tests/test_output_ux.py +0 -0
  213. {sourcecode-1.31.31 → sourcecode-1.31.32}/tests/test_packaging.py +0 -0
  214. {sourcecode-1.31.31 → sourcecode-1.31.32}/tests/test_phase1_improvements.py +0 -0
  215. {sourcecode-1.31.31 → sourcecode-1.31.32}/tests/test_pipeline_integrity.py +0 -0
  216. {sourcecode-1.31.31 → sourcecode-1.31.32}/tests/test_real_projects.py +0 -0
  217. {sourcecode-1.31.31 → sourcecode-1.31.32}/tests/test_redactor.py +0 -0
  218. {sourcecode-1.31.31 → sourcecode-1.31.32}/tests/test_repository_ir.py +0 -0
  219. {sourcecode-1.31.31 → sourcecode-1.31.32}/tests/test_scanner.py +0 -0
  220. {sourcecode-1.31.31 → sourcecode-1.31.32}/tests/test_schema.py +0 -0
  221. {sourcecode-1.31.31 → sourcecode-1.31.32}/tests/test_schema_normalization.py +0 -0
  222. {sourcecode-1.31.31 → sourcecode-1.31.32}/tests/test_scoring_calibration.py +0 -0
  223. {sourcecode-1.31.31 → sourcecode-1.31.32}/tests/test_semantic_analyzer_node.py +0 -0
  224. {sourcecode-1.31.31 → sourcecode-1.31.32}/tests/test_semantic_analyzer_python.py +0 -0
  225. {sourcecode-1.31.31 → sourcecode-1.31.32}/tests/test_semantic_import_resolution.py +0 -0
  226. {sourcecode-1.31.31 → sourcecode-1.31.32}/tests/test_semantic_schema.py +0 -0
  227. {sourcecode-1.31.31 → sourcecode-1.31.32}/tests/test_signal_hierarchy.py +0 -0
  228. {sourcecode-1.31.31 → sourcecode-1.31.32}/tests/test_summarizer.py +0 -0
  229. {sourcecode-1.31.31 → sourcecode-1.31.32}/tests/test_surface_honesty.py +0 -0
  230. {sourcecode-1.31.31 → sourcecode-1.31.32}/tests/test_task_differentiation.py +0 -0
  231. {sourcecode-1.31.31 → sourcecode-1.31.32}/tests/test_telemetry.py +0 -0
  232. {sourcecode-1.31.31 → sourcecode-1.31.32}/tests/test_v131_improvements.py +0 -0
  233. {sourcecode-1.31.31 → sourcecode-1.31.32}/tests/test_v1_10_regressions.py +0 -0
  234. {sourcecode-1.31.31 → sourcecode-1.31.32}/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.31
3
+ Version: 1.31.32
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.31-blue)
228
+ ![Version](https://img.shields.io/badge/version-1.31.32-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.31
266
+ # sourcecode 1.31.32
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.31-blue)
5
+ ![Version](https://img.shields.io/badge/version-1.31.32-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.31
43
+ # sourcecode 1.31.32
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.31"
7
+ version = "1.31.32"
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.31"
3
+ __version__ = "1.31.32"
@@ -469,6 +469,7 @@ def _cas_load_blob(cache_d: Path, blob_hash: str) -> Optional[str]:
469
469
  try:
470
470
  return gzip.decompress(path.read_bytes()).decode("utf-8")
471
471
  except Exception:
472
+ _safe_unlink(path) # evict corrupted blob so it doesn't block future reads
472
473
  return None
473
474
 
474
475
 
@@ -2,6 +2,7 @@ from __future__ import annotations
2
2
 
3
3
  import hashlib
4
4
  import json
5
+ import threading
5
6
  import time
6
7
  from pathlib import Path
7
8
  from typing import Any, Optional, cast
@@ -140,7 +141,7 @@ def _check_pipeline_coherence(sm: "SourceMap") -> list[str]: # type: ignore[nam
140
141
  return issues
141
142
 
142
143
  _HELP = """\
143
- Compressed AI-ready context for Java/Spring enterprise codebases.
144
+ Deterministic codebase context for AI coding agents.
144
145
 
145
146
  [bold]Primary usage:[/bold]
146
147
  sourcecode --compact high-signal summary (~600-800 tokens)
@@ -176,9 +177,20 @@ _SUBCOMMANDS: frozenset[str] = frozenset(
176
177
  }
177
178
  )
178
179
 
179
- # Mutable container holding the path extracted by _preprocess_argv().
180
- # Default "." means "current directory" when no path is given.
181
- _detected_path: list[str] = ["."]
180
+ # Thread-local storage for the path extracted by _preprocess_argv().
181
+ # Using threading.local() prevents concurrent MCP tool calls from clobbering
182
+ # each other's target path (the old module-level list was a shared mutable global).
183
+ _tls = threading.local()
184
+
185
+
186
+ def _get_detected_path() -> str:
187
+ """Return the thread-local detected path, defaulting to '.'."""
188
+ return _tls.__dict__.get("detected_path", ".")
189
+
190
+
191
+ def _set_detected_path(value: str) -> None:
192
+ """Set the thread-local detected path."""
193
+ _tls.detected_path = value
182
194
 
183
195
 
184
196
  # Options that take a value token — their next arg must not be treated as a path.
@@ -214,6 +226,7 @@ def _preprocess_args(args: list[str]) -> list[str]:
214
226
  """
215
227
  result = list(args)
216
228
  skip_next = False
229
+ _path_index: int = -1
217
230
  for i, arg in enumerate(result):
218
231
  if skip_next:
219
232
  skip_next = False
@@ -227,9 +240,11 @@ def _preprocess_args(args: list[str]) -> list[str]:
227
240
  if arg in _SUBCOMMANDS:
228
241
  return result # known subcommand — leave for Click to dispatch
229
242
  # First genuine positional: treat as repository path
230
- _detected_path[0] = arg
231
- result.pop(i)
232
- return result
243
+ _set_detected_path(arg)
244
+ _path_index = i
245
+ break
246
+ if _path_index >= 0:
247
+ result.pop(_path_index)
233
248
  return result
234
249
 
235
250
 
@@ -334,7 +349,7 @@ def _get_command_with_preprocessing(typer_instance: Any) -> Any:
334
349
  def _cmd_main(args: Optional[list[str]] = None, **kwargs: Any) -> Any:
335
350
  if args is not None:
336
351
  # CliRunner / programmatic call: preprocess the explicit args list.
337
- _detected_path[0] = "."
352
+ _set_detected_path(".")
338
353
  args = _preprocess_args(list(args))
339
354
  # args=None → Click reads sys.argv; _preprocess_argv() in main_entry handled it.
340
355
  return _orig_cmd_main(args=args, **kwargs)
@@ -363,7 +378,7 @@ app.add_typer(mcp_app, name="mcp")
363
378
  def _maybe_ask_consent() -> None:
364
379
  """Show first-run consent prompt once, on interactive TTYs only."""
365
380
  try:
366
- from sourcecode.telemetry.config import has_been_asked, mark_asked, set_enabled
381
+ from sourcecode.telemetry.config import has_been_asked, set_enabled
367
382
  from sourcecode.telemetry.consent import ask_for_consent
368
383
  if not has_been_asked():
369
384
  enabled = ask_for_consent()
@@ -833,7 +848,7 @@ def main(
833
848
  # Path was extracted from argv by _preprocess_argv() before Click ran.
834
849
  # FIX-P2-8: preserve original user input in error messages (Windows Git Bash
835
850
  # rewrites "/nonexistent" → "C:\Program Files\Git\nonexistent" via Path.resolve()).
836
- _raw_path_input = _detected_path[0]
851
+ _raw_path_input = _get_detected_path()
837
852
  target = Path(_raw_path_input).resolve()
838
853
  if not target.exists():
839
854
  _emit_error_json(
@@ -852,6 +867,7 @@ def main(
852
867
 
853
868
  # Normalize mode aliases
854
869
  _CONTRACT_MODES = frozenset({"contract", "minimal", "standard"})
870
+ _user_mode_explicit = mode not in ("contract",) # track if user passed a non-default value
855
871
  if mode == "minimal":
856
872
  mode = "contract" # minimal is a documented alias for contract
857
873
  elif mode not in _CONTRACT_MODES and mode != "raw":
@@ -872,6 +888,20 @@ def main(
872
888
  or docs or semantics or full_metrics or architecture
873
889
  )
874
890
  if mode in ("contract", "standard") and _legacy_flags_active:
891
+ if _user_mode_explicit:
892
+ _overriding_flags = [
893
+ f for f, v in [
894
+ ("--compact", compact), ("--tree", tree),
895
+ ("--trace-pipeline", trace_pipeline), ("--docs", docs),
896
+ ("--semantics", semantics), ("--full-metrics", full_metrics),
897
+ ("--architecture", architecture),
898
+ ] if v
899
+ ]
900
+ typer.echo(
901
+ f"[warning] --mode {mode} was overridden to raw because legacy flags "
902
+ f"({', '.join(_overriding_flags)}) require raw output mode.",
903
+ err=True,
904
+ )
875
905
  mode = "raw"
876
906
 
877
907
  # Map mode to contract_view depth
@@ -995,7 +1025,7 @@ def main(
995
1025
  f"cn={code_notes},mode={mode},"
996
1026
  f"ex={_excl_key},depth={effective_depth}"
997
1027
  )
998
- _core_h = _hashlib.md5(_core_flags_str.encode()).hexdigest()[:8]
1028
+ _core_h = _hashlib.sha256(_core_flags_str.encode()).hexdigest()[:8]
999
1029
  _core_key = f"{_git_sha}-{_core_h}"
1000
1030
 
1001
1031
  # ── View flags: output presentation only (no re-analysis needed) ──
@@ -1007,7 +1037,7 @@ def main(
1007
1037
  f"mn={max_nodes},ge={graph_edges},mi={max_importers},"
1008
1038
  f"eg={emit_graph}"
1009
1039
  )
1010
- _view_h = _hashlib.md5(_view_flags_str.encode()).hexdigest()[:8]
1040
+ _view_h = _hashlib.sha256(_view_flags_str.encode()).hexdigest()[:8]
1011
1041
 
1012
1042
  # ── Lookup ──────────────────────────────────────────────────────
1013
1043
  # Step 1: try L1 to obtain the core_hash needed for L2 key
@@ -1961,7 +1991,7 @@ def main(
1961
1991
  if _written_core_hash:
1962
1992
  if not _view_key:
1963
1993
  # _view_key not set (L1 was also a miss); compute it now
1964
- _wvh = _hashlib.md5(_view_flags_str.encode()).hexdigest()[:8]
1994
+ _wvh = _hashlib.sha256(_view_flags_str.encode()).hexdigest()[:8]
1965
1995
  _view_key = f"{_written_core_hash}-{_wvh}"
1966
1996
  _cache_mod.write_view(
1967
1997
  target,
@@ -478,8 +478,13 @@ class EnvAnalyzer:
478
478
  example_files_found: list,
479
479
  limitations: list,
480
480
  profiles_scanned: list,
481
+ depth: int = 0,
482
+ max_depth: int = 12,
481
483
  ) -> int:
482
484
  """Walk the directory tree accumulating env var findings. Returns spring_candidates count."""
485
+ if depth >= max_depth:
486
+ return 0
487
+
483
488
  try:
484
489
  entries = sorted(current.iterdir())
485
490
  except PermissionError:
@@ -496,7 +501,7 @@ class EnvAnalyzer:
496
501
  continue
497
502
  total_spring_candidates += self._walk(
498
503
  root, entry, findings, example_entries, example_files_found,
499
- limitations, profiles_scanned,
504
+ limitations, profiles_scanned, depth + 1, max_depth,
500
505
  )
501
506
  elif entry.is_file():
502
507
  rel = entry.relative_to(root).as_posix()
@@ -326,6 +326,9 @@ def _parse_uncommitted(output: str) -> "UncommittedChanges":
326
326
  continue
327
327
  x, y = line[0], line[1]
328
328
  filepath = line[3:].strip()
329
+ # Renamed files: git porcelain v1 produces "old -> new"; keep only the new path.
330
+ if x == "R" and " -> " in filepath:
331
+ filepath = filepath.split(" -> ", 1)[1]
329
332
  if x == "?" and y == "?":
330
333
  untracked.append(filepath)
331
334
  else:
@@ -0,0 +1,229 @@
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
+ Edge Function, writes ~/.sourcecode/license.json, exits 0 on success
9
+ 5. Cached license is re-validated every 24 h (online); network errors keep
10
+ cached state (offline-first). Server-side invalidity clears cache.
11
+
12
+ Supabase credentials (baked in; override via env vars for testing):
13
+ SOURCECODE_SUPABASE_URL — project Edge Function base URL
14
+ SOURCECODE_SUPABASE_ANON_KEY — public anon key (not a secret)
15
+ """
16
+ from __future__ import annotations
17
+
18
+ import json
19
+ import os
20
+ import sys
21
+ from datetime import datetime, timezone
22
+ from pathlib import Path
23
+ from typing import Optional
24
+
25
+ # ---------------------------------------------------------------------------
26
+ # Supabase endpoint config — hardcoded for production; override via env for dev
27
+ # ---------------------------------------------------------------------------
28
+ _SUPABASE_URL: str = os.environ.get(
29
+ "SOURCECODE_SUPABASE_URL",
30
+ "https://qkndlmyekvujjdgthtmz.supabase.co",
31
+ )
32
+ _SUPABASE_ANON_KEY: str = os.environ.get(
33
+ "SOURCECODE_SUPABASE_ANON_KEY",
34
+ "", # Set SOURCECODE_SUPABASE_ANON_KEY to your project anon key
35
+ )
36
+
37
+ _LICENSE_DIR: Path = Path.home() / ".sourcecode"
38
+ _LICENSE_FILE: Path = _LICENSE_DIR / "license.json"
39
+ _CACHE_TTL_SECONDS: int = 86400 # 24 hours
40
+
41
+ # ---------------------------------------------------------------------------
42
+ # Global license state — loaded once at import time
43
+ # ---------------------------------------------------------------------------
44
+ _license_data: Optional[dict] = None
45
+ is_pro: bool = False
46
+
47
+
48
+ def _load_license_file() -> Optional[dict]:
49
+ """Read ~/.sourcecode/license.json. Returns parsed dict or None."""
50
+ try:
51
+ if _LICENSE_FILE.exists():
52
+ raw = _LICENSE_FILE.read_text(encoding="utf-8")
53
+ return json.loads(raw)
54
+ except Exception:
55
+ pass
56
+ return None
57
+
58
+
59
+ def _call_get_license(license_key: str) -> Optional[dict]:
60
+ """POST to /get-license edge function. Returns parsed dict or None on network error."""
61
+ import urllib.error
62
+ import urllib.request
63
+
64
+ if not _SUPABASE_ANON_KEY:
65
+ return None
66
+
67
+ url = f"{_SUPABASE_URL}/functions/v1/get-license"
68
+ body = json.dumps({"license_key": license_key}).encode("utf-8")
69
+ req = urllib.request.Request(url, data=body, method="POST")
70
+ req.add_header("apikey", _SUPABASE_ANON_KEY)
71
+ req.add_header("Authorization", f"Bearer {_SUPABASE_ANON_KEY}")
72
+ req.add_header("Content-Type", "application/json")
73
+ req.add_header("Accept", "application/json")
74
+
75
+ try:
76
+ with urllib.request.urlopen(req, timeout=8) as resp:
77
+ return json.loads(resp.read().decode("utf-8"))
78
+ except urllib.error.HTTPError as exc:
79
+ try:
80
+ return json.loads(exc.read().decode("utf-8", errors="replace"))
81
+ except Exception:
82
+ return {"valid": False, "error": f"HTTP {exc.code}"}
83
+ except Exception:
84
+ return None # Network error — caller decides what to do
85
+
86
+
87
+ def _maybe_revalidate() -> None:
88
+ """Re-validate cached license if stale. Mutates globals; never raises."""
89
+ global _license_data, is_pro
90
+
91
+ if not _license_data:
92
+ return
93
+
94
+ validated_at_str = _license_data.get("validated_at") or _license_data.get("activated_at")
95
+ if validated_at_str:
96
+ try:
97
+ validated_at = datetime.fromisoformat(validated_at_str)
98
+ if validated_at.tzinfo is None:
99
+ validated_at = validated_at.replace(tzinfo=timezone.utc)
100
+ age = (datetime.now(timezone.utc) - validated_at).total_seconds()
101
+ if age < _CACHE_TTL_SECONDS:
102
+ return
103
+ except Exception:
104
+ pass
105
+
106
+ key = _license_data.get("license_key")
107
+ if not key:
108
+ return
109
+
110
+ result = _call_get_license(key)
111
+ if result is None:
112
+ return # Network error — keep cached data (offline-first)
113
+
114
+ if not result.get("valid"):
115
+ _license_data = None
116
+ is_pro = False
117
+ try:
118
+ if _LICENSE_FILE.exists():
119
+ _LICENSE_FILE.unlink()
120
+ except Exception:
121
+ pass
122
+ return
123
+
124
+ _license_data["plan"] = result.get("plan", "pro")
125
+ _license_data["features"] = result.get("features", [])
126
+ _license_data["validated_at"] = datetime.now(timezone.utc).isoformat()
127
+ is_pro = _license_data.get("plan") == "pro"
128
+ try:
129
+ _LICENSE_FILE.write_text(
130
+ json.dumps(_license_data, indent=2, ensure_ascii=False),
131
+ encoding="utf-8",
132
+ )
133
+ except Exception:
134
+ pass
135
+
136
+
137
+ def _init() -> None:
138
+ global _license_data, is_pro
139
+ _license_data = _load_license_file()
140
+ is_pro = (
141
+ _license_data is not None
142
+ and _license_data.get("plan") == "pro"
143
+ )
144
+
145
+
146
+ _init()
147
+
148
+
149
+ # ---------------------------------------------------------------------------
150
+ # Enforcement
151
+ # ---------------------------------------------------------------------------
152
+
153
+ def require_pro(feature_name: str) -> None:
154
+ """Exit with structured JSON error when not Pro.
155
+
156
+ Re-validates stale cached license before gating (once per 24 h, online).
157
+
158
+ Example:
159
+ from sourcecode.license import require_pro
160
+ require_pro("impact")
161
+ """
162
+ if is_pro:
163
+ _maybe_revalidate()
164
+
165
+ if not is_pro:
166
+ payload = {
167
+ "error": "pro_required",
168
+ "feature": feature_name,
169
+ "message": "Run: sourcecode activate <license_key>",
170
+ }
171
+ sys.stdout.write(json.dumps(payload, ensure_ascii=False) + "\n")
172
+ sys.stdout.flush()
173
+ sys.exit(1)
174
+
175
+
176
+ # ---------------------------------------------------------------------------
177
+ # Activation
178
+ # ---------------------------------------------------------------------------
179
+
180
+ def activate_license(license_key: str) -> None:
181
+ """Validate license_key via Edge Function, write ~/.sourcecode/license.json.
182
+
183
+ Outputs JSON to stdout; exits 0 on success, 1 on any failure.
184
+ Never raises — all error paths emit JSON and call sys.exit(1).
185
+ """
186
+ if not _SUPABASE_ANON_KEY:
187
+ _fail("configuration_error", "SOURCECODE_SUPABASE_ANON_KEY not set. Contact support.")
188
+
189
+ result = _call_get_license(license_key)
190
+
191
+ if result is None:
192
+ _fail("network_error", "Could not reach license server. Check your internet connection.")
193
+
194
+ if not result.get("valid"):
195
+ _fail("invalid_license", result.get("error", "License key is not valid or subscription is inactive."))
196
+
197
+ if result.get("plan") != "pro":
198
+ _fail("not_pro", "This license is not a Pro license.")
199
+
200
+ _LICENSE_DIR.mkdir(parents=True, exist_ok=True)
201
+ now = datetime.now(timezone.utc).isoformat()
202
+ data = {
203
+ "license_key": license_key,
204
+ "plan": result["plan"],
205
+ "features": result.get("features", []),
206
+ "email": result.get("email", ""),
207
+ "activated_at": now,
208
+ "validated_at": now,
209
+ }
210
+ _LICENSE_FILE.write_text(
211
+ json.dumps(data, indent=2, ensure_ascii=False),
212
+ encoding="utf-8",
213
+ )
214
+
215
+ output = {"status": "activated", "plan": "pro", "features": data["features"]}
216
+ sys.stdout.write(json.dumps(output, ensure_ascii=False) + "\n")
217
+ sys.stdout.flush()
218
+
219
+
220
+ # ---------------------------------------------------------------------------
221
+ # Internal helper
222
+ # ---------------------------------------------------------------------------
223
+
224
+ def _fail(error: str, message: str) -> None:
225
+ """Emit JSON error to stdout and exit 1. Never returns."""
226
+ payload = {"error": error, "message": message}
227
+ sys.stdout.write(json.dumps(payload, ensure_ascii=False) + "\n")
228
+ sys.stdout.flush()
229
+ sys.exit(1)
@@ -2,6 +2,7 @@
2
2
  from __future__ import annotations
3
3
 
4
4
  import json
5
+ import os
5
6
  from pathlib import Path
6
7
 
7
8
  _MCP_SERVERS_KEY = "mcpServers"
@@ -13,13 +14,14 @@ _ENTRY_VALUE: dict[str, object] = {
13
14
 
14
15
 
15
16
  def read_config(path: Path) -> dict:
16
- """Parse JSON config from path. Returns empty dict if missing or empty."""
17
+ """Parse JSON config from path. Returns empty dict if missing, empty, or unreadable."""
17
18
  if not path.exists():
18
19
  return {}
19
- raw = path.read_text(encoding="utf-8").strip()
20
- if not raw:
20
+ try:
21
+ text = path.read_text(encoding="utf-8")
22
+ return json.loads(text) if text.strip() else {}
23
+ except (OSError, json.JSONDecodeError):
21
24
  return {}
22
- return json.loads(raw) # type: ignore[no-any-return]
23
25
 
24
26
 
25
27
  def is_installed(config: dict) -> bool:
@@ -49,9 +51,15 @@ def remove_entry(config: dict) -> dict:
49
51
 
50
52
 
51
53
  def write_config(path: Path, config: dict) -> None:
52
- """Atomically write config as formatted JSON."""
54
+ """Atomically write config as formatted JSON using a temp file + os.replace."""
53
55
  path.parent.mkdir(parents=True, exist_ok=True)
54
- path.write_text(json.dumps(config, indent=2) + "\n", encoding="utf-8")
56
+ tmp = path.with_suffix(".tmp")
57
+ try:
58
+ tmp.write_text(json.dumps(config, indent=2) + "\n", encoding="utf-8")
59
+ os.replace(tmp, path)
60
+ finally:
61
+ if tmp.exists():
62
+ tmp.unlink(missing_ok=True)
55
63
 
56
64
 
57
65
  def validate(path: Path) -> bool:
@@ -20,9 +20,9 @@ def run_command(args: list[str]) -> Any:
20
20
  Returns parsed JSON dict when output is valid JSON, else the raw string.
21
21
  Raises RuntimeError on non-zero exit or empty output.
22
22
  """
23
- from sourcecode.cli import _detected_path, _preprocess_args, app
23
+ from sourcecode.cli import _set_detected_path, _preprocess_args, app
24
24
 
25
- _detected_path[0] = "."
25
+ _set_detected_path(".")
26
26
  processed = _preprocess_args(list(args))
27
27
  result = _runner.invoke(app, processed)
28
28
 
@@ -454,18 +454,21 @@ def generate_tests_context(repo_path: str = ".", include_all: bool = False) -> d
454
454
  timeout_s = timeout_ms / 1000.0
455
455
 
456
456
  executor = concurrent.futures.ThreadPoolExecutor(max_workers=1)
457
- future = executor.submit(_execute, args)
458
- done, _not_done = concurrent.futures.wait([future], timeout=timeout_s)
459
- if _not_done:
460
- executor.shutdown(wait=False)
461
- return _ok({
462
- "truncated": True,
463
- "truncated_reason": f"timeout_{timeout_ms // 1000}s" if timeout_ms >= 1000 else f"timeout_{timeout_ms}ms",
464
- "files_analyzed": 0,
465
- "results": [],
466
- })
467
- executor.shutdown(wait=False)
468
- return future.result()
457
+ try:
458
+ future = executor.submit(_execute, args)
459
+ done, _not_done = concurrent.futures.wait([future], timeout=timeout_s)
460
+ if _not_done:
461
+ executor.shutdown(wait=False)
462
+ return _ok({
463
+ "truncated": True,
464
+ "truncated_reason": f"timeout_{timeout_ms // 1000}s" if timeout_ms >= 1000 else f"timeout_{timeout_ms}ms",
465
+ "files_analyzed": 0,
466
+ "results": [],
467
+ })
468
+ result = future.result()
469
+ finally:
470
+ executor.shutdown(wait=True)
471
+ return result
469
472
 
470
473
  except Exception as exc:
471
474
  return _err(
@@ -532,8 +535,8 @@ def modernize_context(repo_path: str = ".", format: str = "json") -> dict:
532
535
  try:
533
536
  if not isinstance(repo_path, str):
534
537
  return _err("repo_path must be a string", "INVALID_ARGUMENT")
535
- if not isinstance(format, str) or format not in ("json", "yaml"):
536
- return _err("format must be 'json' or 'yaml'", "INVALID_ARGUMENT")
538
+ if not isinstance(format, str) or format != "json":
539
+ return _err("format must be 'json' yaml is not supported for modernize output", "INVALID_ARGUMENT")
537
540
  repo_path = _normalize_repo_path(repo_path)
538
541
  _path_err = _check_repo_path(repo_path)
539
542
  if _path_err is not None:
@@ -3,7 +3,12 @@
3
3
  Fires when:
4
4
  1. At least one known MCP client (Claude Desktop, Cursor) is installed
5
5
  2. sourcecode is NOT yet registered in that client's config
6
- 3. The nudge hasn't been shown this session (~/.sourcecode/nudge_shown flag)
6
+ 3. The nudge hasn't been shown yet (~/.sourcecode/nudge_shown flag absent)
7
+
8
+ The sentinel flag (~/.sourcecode/nudge_shown) persists globally on the
9
+ filesystem — it is NOT session-scoped. Once written it suppresses all future
10
+ nudges across all terminal sessions and process invocations until it is
11
+ deleted (which `sourcecode mcp init` does on successful installation).
7
12
 
8
13
  Cleared by: a successful `sourcecode mcp init` (deletes the flag so the
9
14
  post-init detection finds is_installed=True and never nudges again).
@@ -23,6 +23,10 @@ _SECRET_PATTERNS: list[re.Pattern[str]] = [
23
23
  re.compile(r"sk-[A-Za-z0-9]{48}"), # OpenAI legacy key
24
24
  re.compile(r"AKIA[0-9A-Z]{16}"), # AWS Access Key ID
25
25
  re.compile(r"Bearer\s+[A-Za-z0-9\-._~+/]+=*"), # Bearer tokens
26
+ re.compile(r"sk-ant-[A-Za-z0-9\-_]{32,}"), # Anthropic API key
27
+ re.compile(r"hf_[A-Za-z0-9]{32,}"), # Hugging Face token
28
+ re.compile(r"sk_live_[A-Za-z0-9]{24,}"), # Stripe live secret key
29
+ re.compile(r"pk_live_[A-Za-z0-9]{24,}"), # Stripe live publishable key
26
30
  ]
27
31
 
28
32
  # Patrones de nombres de fichero que deben excluirse (SEC-02)
@@ -3246,7 +3246,7 @@ def compute_blast_radius(
3246
3246
  if _hub_class_guard and direct_callers:
3247
3247
  _n_direct = len(direct_callers)
3248
3248
  _k = min(_HUB_SAMPLE_SIZE, _n_direct)
3249
- _sample_seeds = random.sample(direct_callers, _k)
3249
+ _sample_seeds = sorted(direct_callers, key=lambda x: str(x))[:_k]
3250
3250
  _sample_visited: set[str] = set(matched_fqns) | set(direct_callers)
3251
3251
  _sample_queue: list[tuple[str, int]] = [(c, 1) for c in _sample_seeds]
3252
3252
  _sample_indirect: list[str] = []
@@ -190,7 +190,7 @@ def _dependency_import_index(root: Path, file_paths: list[str]) -> set[str]:
190
190
  r"require\(['\"]([^'\"]+)['\"]\)|from\s+['\"]([^'\"]+)['\"])",
191
191
  re.MULTILINE,
192
192
  )
193
- for path in file_paths[:2000]:
193
+ for path in file_paths[:500]:
194
194
  if Path(path).suffix.lower() not in {".py", ".js", ".ts", ".tsx", ".jsx", ".mjs", ".cjs"}:
195
195
  continue
196
196
  try: