sourcecode 1.31.17__tar.gz → 1.31.18__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 (260) hide show
  1. sourcecode-1.31.18/.continue-here.md +134 -0
  2. {sourcecode-1.31.17 → sourcecode-1.31.18}/PKG-INFO +3 -3
  3. {sourcecode-1.31.17 → sourcecode-1.31.18}/README.md +2 -2
  4. {sourcecode-1.31.17 → sourcecode-1.31.18}/pyproject.toml +1 -1
  5. {sourcecode-1.31.17 → sourcecode-1.31.18}/src/sourcecode/__init__.py +1 -1
  6. sourcecode-1.31.18/src/sourcecode/cache.py +470 -0
  7. {sourcecode-1.31.17 → sourcecode-1.31.18}/src/sourcecode/cli.py +17 -13
  8. sourcecode-1.31.18/tests/test_cache.py +500 -0
  9. sourcecode-1.31.17/.continue-here.md +0 -145
  10. sourcecode-1.31.17/.sourcecode-cache/snapshot-88dc388-07db8d0b.json +0 -122
  11. sourcecode-1.31.17/.sourcecode-cache/snapshot-88dc388-178c65fa.json +0 -4012
  12. sourcecode-1.31.17/.sourcecode-cache/snapshot-88dc388-379aba51.json +0 -265
  13. sourcecode-1.31.17/.sourcecode-cache/snapshot-88dc388-530bd9cf.json +0 -406
  14. sourcecode-1.31.17/.sourcecode-cache/snapshot-88dc388-56888a2a.json +0 -244
  15. sourcecode-1.31.17/.sourcecode-cache/snapshot-88dc388-5b602060.json +0 -265
  16. sourcecode-1.31.17/.sourcecode-cache/snapshot-88dc388-5ea1c8f1.json +0 -570
  17. sourcecode-1.31.17/.sourcecode-cache/snapshot-88dc388-6934996c.json +0 -129
  18. sourcecode-1.31.17/.sourcecode-cache/snapshot-88dc388-7b6dd6cc.json +0 -351
  19. sourcecode-1.31.17/.sourcecode-cache/snapshot-88dc388-8cb41bc4.json +0 -390
  20. sourcecode-1.31.17/.sourcecode-cache/snapshot-88dc388-c9ab42a3.json +0 -13523
  21. sourcecode-1.31.17/.sourcecode-cache/snapshot-bc358fa-06d2f793.json +0 -129
  22. sourcecode-1.31.17/.sourcecode-cache/snapshot-bc358fa-0bed428d.json +0 -406
  23. sourcecode-1.31.17/.sourcecode-cache/snapshot-bc358fa-1457dd37.json +0 -129
  24. sourcecode-1.31.17/.sourcecode-cache/snapshot-bc358fa-1da953a8.json +0 -122
  25. sourcecode-1.31.17/.sourcecode-cache/snapshot-bc358fa-218f2312.json +0 -570
  26. sourcecode-1.31.17/.sourcecode-cache/snapshot-bc358fa-2ab2bdd3.json +0 -390
  27. sourcecode-1.31.17/.sourcecode-cache/snapshot-bc358fa-3077fe4e.json +0 -4012
  28. sourcecode-1.31.17/.sourcecode-cache/snapshot-bc358fa-30daccea.json +0 -266
  29. sourcecode-1.31.17/.sourcecode-cache/snapshot-bc358fa-542cb39a.json +0 -244
  30. sourcecode-1.31.17/.sourcecode-cache/snapshot-bc358fa-6d9137f2.json +0 -351
  31. sourcecode-1.31.17/.sourcecode-cache/snapshot-bc358fa-72866fd0.json +0 -570
  32. sourcecode-1.31.17/.sourcecode-cache/snapshot-bc358fa-8afce878.json +0 -122
  33. sourcecode-1.31.17/.sourcecode-cache/snapshot-bc358fa-9d0d48c4.json +0 -244
  34. sourcecode-1.31.17/.sourcecode-cache/snapshot-bc358fa-9d1f2878.json +0 -406
  35. sourcecode-1.31.17/.sourcecode-cache/snapshot-bc358fa-a071a9df.json +0 -266
  36. sourcecode-1.31.17/.sourcecode-cache/snapshot-bc358fa-a1077d85.json +0 -390
  37. sourcecode-1.31.17/.sourcecode-cache/snapshot-bc358fa-a72aa41f.json +0 -351
  38. sourcecode-1.31.17/.sourcecode-cache/snapshot-bc358fa-b9ee8ffa.json +0 -4012
  39. sourcecode-1.31.17/.sourcecode-cache/snapshot-bc358fa-c5fc7db0.json +0 -266
  40. sourcecode-1.31.17/.sourcecode-cache/snapshot-bc358fa-cf823b7a.json +0 -266
  41. sourcecode-1.31.17/.sourcecode-cache/snapshot-bc358fa-d2d141a4.json +0 -13534
  42. sourcecode-1.31.17/.sourcecode-cache/snapshot-bc358fa-fc0985b2.json +0 -13534
  43. {sourcecode-1.31.17 → sourcecode-1.31.18}/.agents/skills/source-command-gsd-join-discord/SKILL.md +0 -0
  44. {sourcecode-1.31.17 → sourcecode-1.31.18}/.agents/skills/source-command-gsd-review-backlog/SKILL.md +0 -0
  45. {sourcecode-1.31.17 → sourcecode-1.31.18}/.agents/skills/source-command-gsd-workstreams/SKILL.md +0 -0
  46. {sourcecode-1.31.17 → sourcecode-1.31.18}/.github/workflows/build-windows.yml +0 -0
  47. {sourcecode-1.31.17 → sourcecode-1.31.18}/.gitignore +0 -0
  48. {sourcecode-1.31.17 → sourcecode-1.31.18}/.ruff.toml +0 -0
  49. {sourcecode-1.31.17 → sourcecode-1.31.18}/.sourcecode-cache/snapshot-3b5997a-fa5c742c.json +0 -0
  50. {sourcecode-1.31.17 → sourcecode-1.31.18}/AUDIT_REAL_REPOS.md +0 -0
  51. {sourcecode-1.31.17 → sourcecode-1.31.18}/CHANGELOG.md +0 -0
  52. {sourcecode-1.31.17 → sourcecode-1.31.18}/CONTRIBUTING.md +0 -0
  53. {sourcecode-1.31.17 → sourcecode-1.31.18}/LICENSE +0 -0
  54. {sourcecode-1.31.17 → sourcecode-1.31.18}/SECURITY.md +0 -0
  55. {sourcecode-1.31.17 → sourcecode-1.31.18}/docs/PRODUCT_TIERS.md +0 -0
  56. {sourcecode-1.31.17 → sourcecode-1.31.18}/docs/privacy.md +0 -0
  57. {sourcecode-1.31.17 → sourcecode-1.31.18}/docs/schema.md +0 -0
  58. {sourcecode-1.31.17 → sourcecode-1.31.18}/raw +0 -0
  59. {sourcecode-1.31.17 → sourcecode-1.31.18}/run_cli.py +0 -0
  60. {sourcecode-1.31.17 → sourcecode-1.31.18}/src/sourcecode/adaptive_scanner.py +0 -0
  61. {sourcecode-1.31.17 → sourcecode-1.31.18}/src/sourcecode/architecture_analyzer.py +0 -0
  62. {sourcecode-1.31.17 → sourcecode-1.31.18}/src/sourcecode/architecture_summary.py +0 -0
  63. {sourcecode-1.31.17 → sourcecode-1.31.18}/src/sourcecode/ast_extractor.py +0 -0
  64. {sourcecode-1.31.17 → sourcecode-1.31.18}/src/sourcecode/canonical_ir.py +0 -0
  65. {sourcecode-1.31.17 → sourcecode-1.31.18}/src/sourcecode/classifier.py +0 -0
  66. {sourcecode-1.31.17 → sourcecode-1.31.18}/src/sourcecode/code_notes_analyzer.py +0 -0
  67. {sourcecode-1.31.17 → sourcecode-1.31.18}/src/sourcecode/confidence_analyzer.py +0 -0
  68. {sourcecode-1.31.17 → sourcecode-1.31.18}/src/sourcecode/context_scorer.py +0 -0
  69. {sourcecode-1.31.17 → sourcecode-1.31.18}/src/sourcecode/context_summarizer.py +0 -0
  70. {sourcecode-1.31.17 → sourcecode-1.31.18}/src/sourcecode/contract_model.py +0 -0
  71. {sourcecode-1.31.17 → sourcecode-1.31.18}/src/sourcecode/contract_pipeline.py +0 -0
  72. {sourcecode-1.31.17 → sourcecode-1.31.18}/src/sourcecode/coverage_parser.py +0 -0
  73. {sourcecode-1.31.17 → sourcecode-1.31.18}/src/sourcecode/dependency_analyzer.py +0 -0
  74. {sourcecode-1.31.17 → sourcecode-1.31.18}/src/sourcecode/detectors/__init__.py +0 -0
  75. {sourcecode-1.31.17 → sourcecode-1.31.18}/src/sourcecode/detectors/base.py +0 -0
  76. {sourcecode-1.31.17 → sourcecode-1.31.18}/src/sourcecode/detectors/csproj_parser.py +0 -0
  77. {sourcecode-1.31.17 → sourcecode-1.31.18}/src/sourcecode/detectors/dart.py +0 -0
  78. {sourcecode-1.31.17 → sourcecode-1.31.18}/src/sourcecode/detectors/dotnet.py +0 -0
  79. {sourcecode-1.31.17 → sourcecode-1.31.18}/src/sourcecode/detectors/elixir.py +0 -0
  80. {sourcecode-1.31.17 → sourcecode-1.31.18}/src/sourcecode/detectors/go.py +0 -0
  81. {sourcecode-1.31.17 → sourcecode-1.31.18}/src/sourcecode/detectors/heuristic.py +0 -0
  82. {sourcecode-1.31.17 → sourcecode-1.31.18}/src/sourcecode/detectors/hybrid.py +0 -0
  83. {sourcecode-1.31.17 → sourcecode-1.31.18}/src/sourcecode/detectors/java.py +0 -0
  84. {sourcecode-1.31.17 → sourcecode-1.31.18}/src/sourcecode/detectors/jvm_ext.py +0 -0
  85. {sourcecode-1.31.17 → sourcecode-1.31.18}/src/sourcecode/detectors/nodejs.py +0 -0
  86. {sourcecode-1.31.17 → sourcecode-1.31.18}/src/sourcecode/detectors/parsers.py +0 -0
  87. {sourcecode-1.31.17 → sourcecode-1.31.18}/src/sourcecode/detectors/php.py +0 -0
  88. {sourcecode-1.31.17 → sourcecode-1.31.18}/src/sourcecode/detectors/project.py +0 -0
  89. {sourcecode-1.31.17 → sourcecode-1.31.18}/src/sourcecode/detectors/python.py +0 -0
  90. {sourcecode-1.31.17 → sourcecode-1.31.18}/src/sourcecode/detectors/ruby.py +0 -0
  91. {sourcecode-1.31.17 → sourcecode-1.31.18}/src/sourcecode/detectors/rust.py +0 -0
  92. {sourcecode-1.31.17 → sourcecode-1.31.18}/src/sourcecode/detectors/systems.py +0 -0
  93. {sourcecode-1.31.17 → sourcecode-1.31.18}/src/sourcecode/detectors/terraform.py +0 -0
  94. {sourcecode-1.31.17 → sourcecode-1.31.18}/src/sourcecode/detectors/tooling.py +0 -0
  95. {sourcecode-1.31.17 → sourcecode-1.31.18}/src/sourcecode/doc_analyzer.py +0 -0
  96. {sourcecode-1.31.17 → sourcecode-1.31.18}/src/sourcecode/entrypoint_classifier.py +0 -0
  97. {sourcecode-1.31.17 → sourcecode-1.31.18}/src/sourcecode/env_analyzer.py +0 -0
  98. {sourcecode-1.31.17 → sourcecode-1.31.18}/src/sourcecode/file_classifier.py +0 -0
  99. {sourcecode-1.31.17 → sourcecode-1.31.18}/src/sourcecode/flow_analyzer.py +0 -0
  100. {sourcecode-1.31.17 → sourcecode-1.31.18}/src/sourcecode/git_analyzer.py +0 -0
  101. {sourcecode-1.31.17 → sourcecode-1.31.18}/src/sourcecode/graph_analyzer.py +0 -0
  102. {sourcecode-1.31.17 → sourcecode-1.31.18}/src/sourcecode/mcp/__init__.py +0 -0
  103. {sourcecode-1.31.17 → sourcecode-1.31.18}/src/sourcecode/mcp/onboarding/__init__.py +0 -0
  104. {sourcecode-1.31.17 → sourcecode-1.31.18}/src/sourcecode/mcp/onboarding/applier.py +0 -0
  105. {sourcecode-1.31.17 → sourcecode-1.31.18}/src/sourcecode/mcp/onboarding/backup.py +0 -0
  106. {sourcecode-1.31.17 → sourcecode-1.31.18}/src/sourcecode/mcp/onboarding/detector.py +0 -0
  107. {sourcecode-1.31.17 → sourcecode-1.31.18}/src/sourcecode/mcp/onboarding/planner.py +0 -0
  108. {sourcecode-1.31.17 → sourcecode-1.31.18}/src/sourcecode/mcp/runner.py +0 -0
  109. {sourcecode-1.31.17 → sourcecode-1.31.18}/src/sourcecode/mcp/server.py +0 -0
  110. {sourcecode-1.31.17 → sourcecode-1.31.18}/src/sourcecode/metrics_analyzer.py +0 -0
  111. {sourcecode-1.31.17 → sourcecode-1.31.18}/src/sourcecode/output_budget.py +0 -0
  112. {sourcecode-1.31.17 → sourcecode-1.31.18}/src/sourcecode/path_filters.py +0 -0
  113. {sourcecode-1.31.17 → sourcecode-1.31.18}/src/sourcecode/pr_comment_renderer.py +0 -0
  114. {sourcecode-1.31.17 → sourcecode-1.31.18}/src/sourcecode/prepare_context.py +0 -0
  115. {sourcecode-1.31.17 → sourcecode-1.31.18}/src/sourcecode/progress.py +0 -0
  116. {sourcecode-1.31.17 → sourcecode-1.31.18}/src/sourcecode/ranking_engine.py +0 -0
  117. {sourcecode-1.31.17 → sourcecode-1.31.18}/src/sourcecode/redactor.py +0 -0
  118. {sourcecode-1.31.17 → sourcecode-1.31.18}/src/sourcecode/relevance_scorer.py +0 -0
  119. {sourcecode-1.31.17 → sourcecode-1.31.18}/src/sourcecode/repo_classifier.py +0 -0
  120. {sourcecode-1.31.17 → sourcecode-1.31.18}/src/sourcecode/repository_ir.py +0 -0
  121. {sourcecode-1.31.17 → sourcecode-1.31.18}/src/sourcecode/runtime_classifier.py +0 -0
  122. {sourcecode-1.31.17 → sourcecode-1.31.18}/src/sourcecode/scanner.py +0 -0
  123. {sourcecode-1.31.17 → sourcecode-1.31.18}/src/sourcecode/schema.py +0 -0
  124. {sourcecode-1.31.17 → sourcecode-1.31.18}/src/sourcecode/semantic_analyzer.py +0 -0
  125. {sourcecode-1.31.17 → sourcecode-1.31.18}/src/sourcecode/serializer.py +0 -0
  126. {sourcecode-1.31.17 → sourcecode-1.31.18}/src/sourcecode/summarizer.py +0 -0
  127. {sourcecode-1.31.17 → sourcecode-1.31.18}/src/sourcecode/telemetry/__init__.py +0 -0
  128. {sourcecode-1.31.17 → sourcecode-1.31.18}/src/sourcecode/telemetry/config.py +0 -0
  129. {sourcecode-1.31.17 → sourcecode-1.31.18}/src/sourcecode/telemetry/consent.py +0 -0
  130. {sourcecode-1.31.17 → sourcecode-1.31.18}/src/sourcecode/telemetry/events.py +0 -0
  131. {sourcecode-1.31.17 → sourcecode-1.31.18}/src/sourcecode/telemetry/filters.py +0 -0
  132. {sourcecode-1.31.17 → sourcecode-1.31.18}/src/sourcecode/telemetry/transport.py +0 -0
  133. {sourcecode-1.31.17 → sourcecode-1.31.18}/src/sourcecode/tree_utils.py +0 -0
  134. {sourcecode-1.31.17 → sourcecode-1.31.18}/src/sourcecode/workspace.py +0 -0
  135. {sourcecode-1.31.17 → sourcecode-1.31.18}/tests/__init__.py +0 -0
  136. {sourcecode-1.31.17 → sourcecode-1.31.18}/tests/conftest.py +0 -0
  137. {sourcecode-1.31.17 → sourcecode-1.31.18}/tests/fixtures/coverage.xml +0 -0
  138. {sourcecode-1.31.17 → sourcecode-1.31.18}/tests/fixtures/fastapi_app/pyproject.toml +0 -0
  139. {sourcecode-1.31.17 → sourcecode-1.31.18}/tests/fixtures/fastapi_app/src/main.py +0 -0
  140. {sourcecode-1.31.17 → sourcecode-1.31.18}/tests/fixtures/go_service/cmd/api/main.go +0 -0
  141. {sourcecode-1.31.17 → sourcecode-1.31.18}/tests/fixtures/go_service/go.mod +0 -0
  142. {sourcecode-1.31.17 → sourcecode-1.31.18}/tests/fixtures/jacoco.xml +0 -0
  143. {sourcecode-1.31.17 → sourcecode-1.31.18}/tests/fixtures/latin1_sample.java +0 -0
  144. {sourcecode-1.31.17 → sourcecode-1.31.18}/tests/fixtures/latin1_sample_iso.java +0 -0
  145. {sourcecode-1.31.17 → sourcecode-1.31.18}/tests/fixtures/lcov.info +0 -0
  146. {sourcecode-1.31.17 → sourcecode-1.31.18}/tests/fixtures/nextjs_app/app/page.tsx +0 -0
  147. {sourcecode-1.31.17 → sourcecode-1.31.18}/tests/fixtures/nextjs_app/package.json +0 -0
  148. {sourcecode-1.31.17 → sourcecode-1.31.18}/tests/fixtures/nextjs_app/pnpm-lock.yaml +0 -0
  149. {sourcecode-1.31.17 → sourcecode-1.31.18}/tests/fixtures/pnpm_monorepo/apps/web/app/page.tsx +0 -0
  150. {sourcecode-1.31.17 → sourcecode-1.31.18}/tests/fixtures/pnpm_monorepo/apps/web/package.json +0 -0
  151. {sourcecode-1.31.17 → sourcecode-1.31.18}/tests/fixtures/pnpm_monorepo/packages/api/main.py +0 -0
  152. {sourcecode-1.31.17 → sourcecode-1.31.18}/tests/fixtures/pnpm_monorepo/packages/api/pyproject.toml +0 -0
  153. {sourcecode-1.31.17 → sourcecode-1.31.18}/tests/fixtures/pnpm_monorepo/pnpm-workspace.yaml +0 -0
  154. {sourcecode-1.31.17 → sourcecode-1.31.18}/tests/fixtures/spring_boot_minimal/pom.xml +0 -0
  155. {sourcecode-1.31.17 → sourcecode-1.31.18}/tests/fixtures/spring_boot_minimal/src/main/java/com/example/ddd/ausente/application/service/FindAusenteService.java +0 -0
  156. {sourcecode-1.31.17 → sourcecode-1.31.18}/tests/fixtures/spring_boot_minimal/src/main/java/com/example/ddd/ausente/domain/entities/Ausente.java +0 -0
  157. {sourcecode-1.31.17 → sourcecode-1.31.18}/tests/fixtures/spring_boot_minimal/src/main/java/com/example/ddd/ausente/infrastructure/rest/AusenteRestController.java +0 -0
  158. {sourcecode-1.31.17 → sourcecode-1.31.18}/tests/fixtures/spring_boot_minimal/src/main/java/com/example/ddd/autocoberturas/application/service/FindAutocoberturasService.java +0 -0
  159. {sourcecode-1.31.17 → sourcecode-1.31.18}/tests/fixtures/spring_boot_minimal/src/main/java/com/example/ddd/autocoberturas/domain/entities/Autocoberturas.java +0 -0
  160. {sourcecode-1.31.17 → sourcecode-1.31.18}/tests/fixtures/spring_boot_minimal/src/main/java/com/example/ddd/autocoberturas/infrastructure/rest/AutocoberturasRestController.java +0 -0
  161. {sourcecode-1.31.17 → sourcecode-1.31.18}/tests/fixtures/spring_boot_minimal/src/main/java/com/example/ddd/calendarioTrabajador/application/service/FindCalendarioTrabajadorService.java +0 -0
  162. {sourcecode-1.31.17 → sourcecode-1.31.18}/tests/fixtures/spring_boot_minimal/src/main/java/com/example/ddd/calendarioTrabajador/domain/entities/CalendarioTrabajador.java +0 -0
  163. {sourcecode-1.31.17 → sourcecode-1.31.18}/tests/fixtures/spring_boot_minimal/src/main/java/com/example/ddd/calendarioTrabajador/infrastructure/rest/CalendarioTrabajadorRestController.java +0 -0
  164. {sourcecode-1.31.17 → sourcecode-1.31.18}/tests/fixtures/spring_boot_minimal/src/main/java/com/example/ddd/departamento/application/service/FindDepartamentoService.java +0 -0
  165. {sourcecode-1.31.17 → sourcecode-1.31.18}/tests/fixtures/spring_boot_minimal/src/main/java/com/example/ddd/departamento/domain/entities/Departamento.java +0 -0
  166. {sourcecode-1.31.17 → sourcecode-1.31.18}/tests/fixtures/spring_boot_minimal/src/main/java/com/example/ddd/departamento/infrastructure/rest/DepartamentoRestController.java +0 -0
  167. {sourcecode-1.31.17 → sourcecode-1.31.18}/tests/fixtures/spring_boot_minimal/src/main/java/com/example/ddd/empleado/application/service/FindEmpleadoService.java +0 -0
  168. {sourcecode-1.31.17 → sourcecode-1.31.18}/tests/fixtures/spring_boot_minimal/src/main/java/com/example/ddd/empleado/domain/entities/Empleado.java +0 -0
  169. {sourcecode-1.31.17 → sourcecode-1.31.18}/tests/fixtures/spring_boot_minimal/src/main/java/com/example/ddd/empleado/infrastructure/rest/EmpleadoRestController.java +0 -0
  170. {sourcecode-1.31.17 → sourcecode-1.31.18}/tests/fixtures/spring_boot_minimal/src/main/java/com/example/demo/DemoApplication.java +0 -0
  171. {sourcecode-1.31.17 → sourcecode-1.31.18}/tests/fixtures/spring_boot_minimal/src/main/java/com/example/demo/config/FilterConfig.java +0 -0
  172. {sourcecode-1.31.17 → sourcecode-1.31.18}/tests/fixtures/spring_boot_minimal/src/main/java/com/example/demo/domain/Health.java +0 -0
  173. {sourcecode-1.31.17 → sourcecode-1.31.18}/tests/fixtures/spring_boot_minimal/src/main/java/com/example/demo/mapper/HealthMapper.java +0 -0
  174. {sourcecode-1.31.17 → sourcecode-1.31.18}/tests/fixtures/spring_boot_minimal/src/main/java/com/example/demo/repository/HealthRepository.java +0 -0
  175. {sourcecode-1.31.17 → sourcecode-1.31.18}/tests/fixtures/spring_boot_minimal/src/main/java/com/example/demo/service/HealthService.java +0 -0
  176. {sourcecode-1.31.17 → sourcecode-1.31.18}/tests/fixtures/spring_boot_minimal/src/main/java/com/example/demo/web/HealthRestController.java +0 -0
  177. {sourcecode-1.31.17 → sourcecode-1.31.18}/tests/fixtures/spring_boot_minimal/src/main/java/com/example/demo/web/NominaRestController.java +0 -0
  178. {sourcecode-1.31.17 → sourcecode-1.31.18}/tests/fixtures/spring_boot_minimal/src/main/resources/application-dev.yml +0 -0
  179. {sourcecode-1.31.17 → sourcecode-1.31.18}/tests/fixtures/spring_boot_minimal/src/main/resources/application.yml +0 -0
  180. {sourcecode-1.31.17 → sourcecode-1.31.18}/tests/fixtures/spring_boot_minimal/src/main/resources/mapper/HealthMapper.xml +0 -0
  181. {sourcecode-1.31.17 → sourcecode-1.31.18}/tests/test_architecture_analyzer.py +0 -0
  182. {sourcecode-1.31.17 → sourcecode-1.31.18}/tests/test_architecture_summary.py +0 -0
  183. {sourcecode-1.31.17 → sourcecode-1.31.18}/tests/test_ast_extractor.py +0 -0
  184. {sourcecode-1.31.17 → sourcecode-1.31.18}/tests/test_audit_fixes.py +0 -0
  185. {sourcecode-1.31.17 → sourcecode-1.31.18}/tests/test_audit_sas_v2.py +0 -0
  186. {sourcecode-1.31.17 → sourcecode-1.31.18}/tests/test_block1_reliability.py +0 -0
  187. {sourcecode-1.31.17 → sourcecode-1.31.18}/tests/test_block2_coverage.py +0 -0
  188. {sourcecode-1.31.17 → sourcecode-1.31.18}/tests/test_block5_quality.py +0 -0
  189. {sourcecode-1.31.17 → sourcecode-1.31.18}/tests/test_broadleaf_fixes.py +0 -0
  190. {sourcecode-1.31.17 → sourcecode-1.31.18}/tests/test_bug_fixes_v1302.py +0 -0
  191. {sourcecode-1.31.17 → sourcecode-1.31.18}/tests/test_bug_fixes_v13115.py +0 -0
  192. {sourcecode-1.31.17 → sourcecode-1.31.18}/tests/test_bug_fixes_v1312.py +0 -0
  193. {sourcecode-1.31.17 → sourcecode-1.31.18}/tests/test_bug_fixes_v1313.py +0 -0
  194. {sourcecode-1.31.17 → sourcecode-1.31.18}/tests/test_bug_fixes_v16.py +0 -0
  195. {sourcecode-1.31.17 → sourcecode-1.31.18}/tests/test_bug_fixes_v2.py +0 -0
  196. {sourcecode-1.31.17 → sourcecode-1.31.18}/tests/test_canonical_ir.py +0 -0
  197. {sourcecode-1.31.17 → sourcecode-1.31.18}/tests/test_classifier.py +0 -0
  198. {sourcecode-1.31.17 → sourcecode-1.31.18}/tests/test_cli.py +0 -0
  199. {sourcecode-1.31.17 → sourcecode-1.31.18}/tests/test_code_notes_analyzer.py +0 -0
  200. {sourcecode-1.31.17 → sourcecode-1.31.18}/tests/test_context_scorer.py +0 -0
  201. {sourcecode-1.31.17 → sourcecode-1.31.18}/tests/test_contract_pipeline.py +0 -0
  202. {sourcecode-1.31.17 → sourcecode-1.31.18}/tests/test_coverage_parser.py +0 -0
  203. {sourcecode-1.31.17 → sourcecode-1.31.18}/tests/test_cross_consistency.py +0 -0
  204. {sourcecode-1.31.17 → sourcecode-1.31.18}/tests/test_dependency_analyzer_node_python.py +0 -0
  205. {sourcecode-1.31.17 → sourcecode-1.31.18}/tests/test_dependency_analyzer_polyglot.py +0 -0
  206. {sourcecode-1.31.17 → sourcecode-1.31.18}/tests/test_dependency_schema.py +0 -0
  207. {sourcecode-1.31.17 → sourcecode-1.31.18}/tests/test_detector_dotnet.py +0 -0
  208. {sourcecode-1.31.17 → sourcecode-1.31.18}/tests/test_detector_go_rust_java.py +0 -0
  209. {sourcecode-1.31.17 → sourcecode-1.31.18}/tests/test_detector_nodejs.py +0 -0
  210. {sourcecode-1.31.17 → sourcecode-1.31.18}/tests/test_detector_php_ruby_dart.py +0 -0
  211. {sourcecode-1.31.17 → sourcecode-1.31.18}/tests/test_detector_python.py +0 -0
  212. {sourcecode-1.31.17 → sourcecode-1.31.18}/tests/test_detector_universal_managed.py +0 -0
  213. {sourcecode-1.31.17 → sourcecode-1.31.18}/tests/test_detector_universal_systems.py +0 -0
  214. {sourcecode-1.31.17 → sourcecode-1.31.18}/tests/test_detectors_base.py +0 -0
  215. {sourcecode-1.31.17 → sourcecode-1.31.18}/tests/test_doc_analyzer_jsdom.py +0 -0
  216. {sourcecode-1.31.17 → sourcecode-1.31.18}/tests/test_doc_analyzer_python.py +0 -0
  217. {sourcecode-1.31.17 → sourcecode-1.31.18}/tests/test_encoding_regression.py +0 -0
  218. {sourcecode-1.31.17 → sourcecode-1.31.18}/tests/test_enterprise_benchmarks.py +0 -0
  219. {sourcecode-1.31.17 → sourcecode-1.31.18}/tests/test_graph_analyzer_polyglot.py +0 -0
  220. {sourcecode-1.31.17 → sourcecode-1.31.18}/tests/test_graph_analyzer_python_node.py +0 -0
  221. {sourcecode-1.31.17 → sourcecode-1.31.18}/tests/test_graph_schema.py +0 -0
  222. {sourcecode-1.31.17 → sourcecode-1.31.18}/tests/test_hybrid_inference.py +0 -0
  223. {sourcecode-1.31.17 → sourcecode-1.31.18}/tests/test_integration.py +0 -0
  224. {sourcecode-1.31.17 → sourcecode-1.31.18}/tests/test_integration_dependencies.py +0 -0
  225. {sourcecode-1.31.17 → sourcecode-1.31.18}/tests/test_integration_detection.py +0 -0
  226. {sourcecode-1.31.17 → sourcecode-1.31.18}/tests/test_integration_docs.py +0 -0
  227. {sourcecode-1.31.17 → sourcecode-1.31.18}/tests/test_integration_graph_modules.py +0 -0
  228. {sourcecode-1.31.17 → sourcecode-1.31.18}/tests/test_integration_lqn.py +0 -0
  229. {sourcecode-1.31.17 → sourcecode-1.31.18}/tests/test_integration_metrics.py +0 -0
  230. {sourcecode-1.31.17 → sourcecode-1.31.18}/tests/test_integration_multistack.py +0 -0
  231. {sourcecode-1.31.17 → sourcecode-1.31.18}/tests/test_integration_semantics.py +0 -0
  232. {sourcecode-1.31.17 → sourcecode-1.31.18}/tests/test_integration_universal.py +0 -0
  233. {sourcecode-1.31.17 → sourcecode-1.31.18}/tests/test_java_spring_integration.py +0 -0
  234. {sourcecode-1.31.17 → sourcecode-1.31.18}/tests/test_mcp_runner.py +0 -0
  235. {sourcecode-1.31.17 → sourcecode-1.31.18}/tests/test_mcp_serve.py +0 -0
  236. {sourcecode-1.31.17 → sourcecode-1.31.18}/tests/test_mcp_tools.py +0 -0
  237. {sourcecode-1.31.17 → sourcecode-1.31.18}/tests/test_metrics_analyzer.py +0 -0
  238. {sourcecode-1.31.17 → sourcecode-1.31.18}/tests/test_output_ux.py +0 -0
  239. {sourcecode-1.31.17 → sourcecode-1.31.18}/tests/test_packaging.py +0 -0
  240. {sourcecode-1.31.17 → sourcecode-1.31.18}/tests/test_phase1_improvements.py +0 -0
  241. {sourcecode-1.31.17 → sourcecode-1.31.18}/tests/test_pipeline_integrity.py +0 -0
  242. {sourcecode-1.31.17 → sourcecode-1.31.18}/tests/test_real_projects.py +0 -0
  243. {sourcecode-1.31.17 → sourcecode-1.31.18}/tests/test_redactor.py +0 -0
  244. {sourcecode-1.31.17 → sourcecode-1.31.18}/tests/test_repository_ir.py +0 -0
  245. {sourcecode-1.31.17 → sourcecode-1.31.18}/tests/test_scanner.py +0 -0
  246. {sourcecode-1.31.17 → sourcecode-1.31.18}/tests/test_schema.py +0 -0
  247. {sourcecode-1.31.17 → sourcecode-1.31.18}/tests/test_schema_normalization.py +0 -0
  248. {sourcecode-1.31.17 → sourcecode-1.31.18}/tests/test_scoring_calibration.py +0 -0
  249. {sourcecode-1.31.17 → sourcecode-1.31.18}/tests/test_semantic_analyzer_node.py +0 -0
  250. {sourcecode-1.31.17 → sourcecode-1.31.18}/tests/test_semantic_analyzer_python.py +0 -0
  251. {sourcecode-1.31.17 → sourcecode-1.31.18}/tests/test_semantic_import_resolution.py +0 -0
  252. {sourcecode-1.31.17 → sourcecode-1.31.18}/tests/test_semantic_schema.py +0 -0
  253. {sourcecode-1.31.17 → sourcecode-1.31.18}/tests/test_signal_hierarchy.py +0 -0
  254. {sourcecode-1.31.17 → sourcecode-1.31.18}/tests/test_summarizer.py +0 -0
  255. {sourcecode-1.31.17 → sourcecode-1.31.18}/tests/test_surface_honesty.py +0 -0
  256. {sourcecode-1.31.17 → sourcecode-1.31.18}/tests/test_task_differentiation.py +0 -0
  257. {sourcecode-1.31.17 → sourcecode-1.31.18}/tests/test_telemetry.py +0 -0
  258. {sourcecode-1.31.17 → sourcecode-1.31.18}/tests/test_v131_improvements.py +0 -0
  259. {sourcecode-1.31.17 → sourcecode-1.31.18}/tests/test_v1_10_regressions.py +0 -0
  260. {sourcecode-1.31.17 → sourcecode-1.31.18}/tests/test_workspace_analyzer.py +0 -0
@@ -0,0 +1,134 @@
1
+ # Continue Here — atlas-cli sesión 21
2
+
3
+ **Paused:** 2026-05-24
4
+ **Repo:** `/Users/user/Documents/workspace/atlas-cli`
5
+ **Branch:** master
6
+ **Version:** sourcecode 1.31.17
7
+
8
+ ---
9
+
10
+ ## Objetivo de esta sesión
11
+
12
+ Continuar corrección de bugs post-auditoría v1.31.16 (adversarial audit contra Keycloak + Broadleaf).
13
+ Sesión 21 atacó los dos P0s. Quedan P1s y P2s.
14
+
15
+ ---
16
+
17
+ ## Trabajo completado esta sesión
18
+
19
+ ### P0-01 — `impact OrderServiceImpl` → 0 callers FIXED ✅
20
+
21
+ **Root cause:** `_build_reverse_adjacency` descartaba edges `implements` cuando `to` era
22
+ nombre corto no-resuelto (`"OrderService"` en vez de FQN). El `reverse_graph["OrderService"]`
23
+ no tenía clave `"implements"` → scan desde reverse side imposible.
24
+
25
+ **Fix:** Escanear `graph.edges` forward para `type=implements` FROM matched classes.
26
+ Resolver `to` (short/FQN) contra claves de `reverse_graph` via suffix match.
27
+ Callers de la interfaz se añaden a `direct_callers`. Output incluye `via_interface_resolution`
28
+ y `via_interface_note`.
29
+
30
+ **Resultado:** `impact OrderServiceImpl` en Broadleaf: 0 callers → **74 callers**, risk LOW → **HIGH**.
31
+
32
+ ### P0-02 — `reverse_graph` unbounded por `--max-nodes`/`--max-edges` FIXED ✅
33
+
34
+ **Root cause:** `apply_ir_size_limits` sólo acotaba `graph.nodes`/`graph.edges`.
35
+ `reverse_graph` emitía 2685 claves (~3MB) aunque se pidieran 200 nodos.
36
+
37
+ **Fix:** Cuando `max_nodes` activo: restringir `reverse_graph` a `kept_fqns` + cap inner
38
+ caller lists a `max(20, max_nodes//4)`. Cuando sólo `max_edges`: cap a `max_edges` claves
39
+ por in-degree. Añade `reverse_graph_note` cuando trimmed.
40
+
41
+ **Resultado:** `--max-nodes 200 --max-edges 500`: 3.85MB → **939KB** (76% reducción).
42
+
43
+ ### Commit esta sesión
44
+
45
+ ```
46
+ e5fba19 fix(ir): resolve Spring DI interface bridging and bound reverse_graph
47
+ ```
48
+
49
+ ---
50
+
51
+ ## Estado archivos sin commitear
52
+
53
+ ```
54
+ ?? AUDIT_REAL_REPOS.md — auditoría 22 secciones (de sesión 20, no tocar)
55
+ ?? docs/PRODUCT_TIERS.md — de sesión anterior (no tocar esta sesión)
56
+ ```
57
+
58
+ Commitear estos docs antes de continuar con fixes:
59
+ ```bash
60
+ git add AUDIT_REAL_REPOS.md docs/PRODUCT_TIERS.md
61
+ git commit -m "docs(audit): adversarial audit v1.31.16 — real benchmarks, P0/P1/P2 findings"
62
+ ```
63
+
64
+ ---
65
+
66
+ ## Bugs pendientes por ROI
67
+
68
+ ### P1 — Altos
69
+
70
+ | # | Bug | Archivo | Tiempo est. |
71
+ |---|-----|---------|-------------|
72
+ | **P1-01** | Risk score inconsistente para 0-caller impls: `OrderServiceImpl`→low vs `OrderDaoImpl`→high (mismo 0 callers, distinta heurística). POST P0-01 fix: verificar si aún aplica o si interface bridging ya lo resuelve. | `repository_ir.py` → `compute_blast_radius` | 30 min |
73
+ | **P1-02** | `project_summary` = primera línea del README (license/marketing). Debe generarse de código: "N-module Spring Boot — M classes, K endpoints, J txn boundaries" | `serializer.py` o `summarizer.py` | 1h |
74
+ | **P1-03** | `fix-bug` devuelve 426 archivos para NPE genérico (14% repo, sin `score` field) | `prepare_context.py` → `fix_bug` | 30 min |
75
+ | **P1-04** | `indirect_callers:0` para KeycloakSession (1992 direct callers) — BFS para en nivel 1 por hub guard | `repository_ir.py` → `compute_blast_radius` hub guard logic | 45 min |
76
+ | **P1-05** | `fix-bug` 23s cold en Keycloak | profiling needed | ~1h |
77
+
78
+ ### P2 — Medios (batch)
79
+
80
+ - `bounded_contexts: ["dto","file"]` Broadleaf — WRONG. Fix: usar Maven module names
81
+ - `role: unknown` para todos `high_coupling_nodes` en modernize — nunca clasifica annotation/interface/entity
82
+ - `no_security_signal: 100%` en ambos repos — filter-based security nunca detectado
83
+ - JAX-RS sub-resource paths no compuestos con parent `@Path`
84
+ - `hotspot_candidates: []` siempre — git churn ignorado
85
+ - `--format`/`--no-cache` ausentes en `impact`, `endpoints`, `fix-bug`, `onboard`, `modernize`, `review-pr`
86
+ - Architecture confidence diferente entre `--compact` y `--agent` mismo repo
87
+
88
+ ---
89
+
90
+ ## Primera acción al retomar
91
+
92
+ ```bash
93
+ cd /Users/user/Documents/workspace/atlas-cli
94
+
95
+ # 1. Commitear docs pendientes
96
+ git add AUDIT_REAL_REPOS.md docs/PRODUCT_TIERS.md
97
+ git commit -m "docs(audit): adversarial audit v1.31.16 — Keycloak + Broadleaf findings"
98
+
99
+ # 2. Verificar si P1-01 aún existe post-fix P0-01:
100
+ sourcecode impact OrderDaoImpl ~/Documents/workspace/BroadleafCommerce 2>&1 | python3 -m json.tool | grep -E 'risk_level|confidence_level|direct_callers|via_interface'
101
+ # Si risk_level sigue siendo inconsistente → fix P1-01
102
+ # Si interface bridging ya lo resuelve → skip a P1-02
103
+
104
+ # 3. Si P1-01 persiste:
105
+ # En compute_blast_radius: cuando direct_callers=0 Y no hay interface bridging Y
106
+ # clase es @Service/@Repository impl → bajar confidence, añadir gap en explanation
107
+ ```
108
+
109
+ ---
110
+
111
+ ## Archivos clave del codebase
112
+
113
+ ```
114
+ src/sourcecode/
115
+ repository_ir.py — impact analysis, blast radius, interface bridging (P0-01/02 fixeados aquí)
116
+ prepare_context.py — fix-bug, onboard, review-pr output (P1-03 aquí)
117
+ serializer.py — compact/agent output, project_summary (P1-02 aquí)
118
+ summarizer.py — ProjectSummarizer (P1-02 posiblemente aquí)
119
+ cli.py — top-level commands, cache logic
120
+ tests/
121
+ test_enterprise_benchmarks.py
122
+ ```
123
+
124
+ ---
125
+
126
+ ## Contexto de versiones
127
+
128
+ - v1.31.16: bugs auditados en sesión 20
129
+ - v1.31.17: versión actual (bumpeada en 0cf28b1 por el usuario)
130
+ - Fixes P0-01/P0-02: commit e5fba19 (sesión 21)
131
+
132
+ ---
133
+
134
+ *Pausado 2026-05-24 — gsd:pause-work (sesión 21)*
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: sourcecode
3
- Version: 1.31.17
3
+ Version: 1.31.18
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.17-blue)
228
+ ![Version](https://img.shields.io/badge/version-1.31.18-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.17
266
+ # sourcecode 1.31.18
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.17-blue)
5
+ ![Version](https://img.shields.io/badge/version-1.31.18-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.17
43
+ # sourcecode 1.31.18
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.17"
7
+ version = "1.31.18"
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.17"
3
+ __version__ = "1.31.18"
@@ -0,0 +1,470 @@
1
+ """
2
+ Snapshot cache manager for sourcecode — v2.
3
+
4
+ Cache layout
5
+ ------------
6
+ ~/.sourcecode/cache/<repo_id>/
7
+ snapshot-<git_sha>-<flags_hash>.json.gz ← versioned envelope
8
+ cas/
9
+ <blob_hash16>.gz ← content-addressed blobs
10
+
11
+ Schema
12
+ ------
13
+ Every snapshot file is a gzip-compressed JSON *envelope*:
14
+
15
+ {
16
+ "sv": "2", // schema version — bump to invalidate all
17
+ "key": "abc1234-aabbccdd", // cache key (git_sha + flags_hash)
18
+ "ts": "2026-05-24T22:00:00Z", // write timestamp (ISO-8601 UTC)
19
+ "fmt": "json", // output format: "json" | "yaml"
20
+ "layers": {"heuristic": "...", ...}, // analyzer fingerprints at write time
21
+ // ── content (one of two forms) ──────────────────────────────────────
22
+ "snap": {...}, // inline fields (small) — JSON mode
23
+ "cas": {"file_paths": "<h16>",…} // large fields deduped into CAS store
24
+ // — OR —
25
+ "raw": "<content string>" // YAML or unparseable JSON stored as-is
26
+ }
27
+
28
+ Content-addressed store (CAS)
29
+ -----------------------------
30
+ Large top-level JSON fields (> _CAS_THRESHOLD bytes) are extracted into the
31
+ ``cas/`` directory as individual gzip-compressed blobs identified by a 16-char
32
+ SHA-256 hash of their uncompressed bytes. Two snapshots that share an
33
+ identical ``file_paths`` array reference the *same* blob — zero duplication.
34
+
35
+ Eviction / GC
36
+ -------------
37
+ After each write, ``_gc()`` keeps snapshots from the last
38
+ ``SOURCECODE_CACHE_KEEP_COMMITS`` distinct git commits (default 5, override via
39
+ env var). A CAS sweep runs concurrently: blobs unreferenced by any surviving
40
+ snapshot are deleted.
41
+
42
+ Backward compatibility
43
+ ----------------------
44
+ v1 files (raw gzip'd content, no envelope) are detected by the absence of an
45
+ ``sv`` key in the decompressed JSON, and served transparently. Legacy files
46
+ in ``<repo>/.sourcecode-cache/`` are also checked as a final fallback.
47
+
48
+ Env vars
49
+ --------
50
+ SOURCECODE_CACHE_DIR Override global cache base (default: ~/.sourcecode/cache)
51
+ SOURCECODE_CACHE_KEEP_COMMITS How many git commits to retain (default: 5; 0 = unlimited)
52
+ """
53
+ from __future__ import annotations
54
+
55
+ import gzip
56
+ import hashlib
57
+ import json
58
+ import os
59
+ import re
60
+ from datetime import datetime, timezone
61
+ from pathlib import Path
62
+ from typing import Any, Optional
63
+
64
+
65
+ # ---------------------------------------------------------------------------
66
+ # Version / constants
67
+ # ---------------------------------------------------------------------------
68
+
69
+ #: Bump this string to invalidate *all* existing cached snapshots.
70
+ SCHEMA_VERSION: str = "2"
71
+
72
+ #: Fields eligible for CAS deduplication (applied to top-level JSON dict keys).
73
+ _CAS_FIELDS: frozenset[str] = frozenset([
74
+ "file_paths",
75
+ "entry_points",
76
+ "docs",
77
+ "dependencies",
78
+ "graph",
79
+ "semantic_calls",
80
+ "semantic_symbols",
81
+ "architecture",
82
+ "metrics",
83
+ "git_history",
84
+ "env_map",
85
+ "code_notes",
86
+ ])
87
+
88
+ #: Serialised size threshold (bytes) above which a field is moved to CAS.
89
+ _CAS_THRESHOLD: int = 4096
90
+
91
+ _DEFAULT_KEEP_COMMITS: int = 5
92
+
93
+ # Matches "snapshot-<hex_commit>-<hex_flags>.json.gz"
94
+ _SNAPSHOT_RE = re.compile(r"^snapshot-([0-9a-f]+)-[0-9a-f]+\.json\.gz$")
95
+
96
+
97
+ # ---------------------------------------------------------------------------
98
+ # Public API — location helpers
99
+ # ---------------------------------------------------------------------------
100
+
101
+ def repo_id(repo_root: Path) -> str:
102
+ """Stable 16-char hex identifier derived from the canonical repo path."""
103
+ return hashlib.sha256(str(repo_root.resolve()).encode()).hexdigest()[:16]
104
+
105
+
106
+ def cache_dir(repo_root: Path) -> Path:
107
+ """
108
+ Return the per-repo cache directory (``~/.sourcecode/cache/<repo_id>/``).
109
+
110
+ Override the base via ``SOURCECODE_CACHE_DIR``.
111
+ """
112
+ env_base = os.environ.get("SOURCECODE_CACHE_DIR", "")
113
+ base: Path = Path(env_base) if env_base else Path.home() / ".sourcecode" / "cache"
114
+ return base / repo_id(repo_root)
115
+
116
+
117
+ # ---------------------------------------------------------------------------
118
+ # Public API — read / write
119
+ # ---------------------------------------------------------------------------
120
+
121
+ def read(repo_root: Path, cache_key: str) -> Optional[str]:
122
+ """
123
+ Return the cached snapshot string for *cache_key*, or ``None`` on miss.
124
+
125
+ Lookup order:
126
+ 1. ``<cache_dir>/snapshot-<cache_key>.json.gz`` — v2 envelope (new)
127
+ 2. ``<repo_root>/.sourcecode-cache/snapshot-<cache_key>.json`` — legacy
128
+ """
129
+ cache_d = cache_dir(repo_root)
130
+
131
+ # ── 1. Global location (.json.gz, v2 envelope or v1 raw) ───────────────
132
+ gz_path = cache_d / f"snapshot-{cache_key}.json.gz"
133
+ if gz_path.exists():
134
+ try:
135
+ result = _parse_envelope(gz_path.read_bytes(), cache_d)
136
+ if result is not None:
137
+ return result
138
+ except Exception:
139
+ pass
140
+ _safe_unlink(gz_path) # corrupted or version mismatch — evict
141
+ return None
142
+
143
+ # ── 2. Legacy location (<repo>/.sourcecode-cache/*.json) ───────────────
144
+ legacy = repo_root / ".sourcecode-cache" / f"snapshot-{cache_key}.json"
145
+ if legacy.exists():
146
+ try:
147
+ return legacy.read_text(encoding="utf-8")
148
+ except Exception:
149
+ return None
150
+
151
+ return None
152
+
153
+
154
+ def write(
155
+ repo_root: Path,
156
+ cache_key: str,
157
+ content: str,
158
+ *,
159
+ fmt: str = "json",
160
+ layers: Optional[dict[str, str]] = None,
161
+ ) -> None:
162
+ """
163
+ Persist *content* as a versioned, optionally CAS-deduped snapshot.
164
+
165
+ Parameters
166
+ ----------
167
+ repo_root : Path
168
+ Root directory of the analysed repository.
169
+ cache_key : str
170
+ ``"{git_sha}-{flags_hash}"`` identifying this analysis.
171
+ content : str
172
+ Final rendered output (JSON or YAML string).
173
+ fmt : str
174
+ ``"json"`` or ``"yaml"`` — determines whether CAS extraction applies.
175
+ layers : dict[str, str], optional
176
+ Analyzer fingerprints (from ``_compute_analyzer_fingerprints()``).
177
+ Stored in the envelope for future layer-aware reuse.
178
+
179
+ Writes are always best-effort: any failure is silently swallowed.
180
+ """
181
+ cache_d = cache_dir(repo_root)
182
+ dest = cache_d / f"snapshot-{cache_key}.json.gz"
183
+ try:
184
+ cache_d.mkdir(parents=True, exist_ok=True)
185
+ payload = _build_envelope(cache_key, content, fmt, layers or {}, cache_d)
186
+ dest.write_bytes(payload)
187
+ except Exception:
188
+ return # non-fatal
189
+
190
+ _gc(cache_d)
191
+
192
+
193
+ # ---------------------------------------------------------------------------
194
+ # Envelope (de)serialisation
195
+ # ---------------------------------------------------------------------------
196
+
197
+ def _now_iso() -> str:
198
+ return datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")
199
+
200
+
201
+ def _build_envelope(
202
+ cache_key: str,
203
+ content: str,
204
+ fmt: str,
205
+ layers: dict[str, str],
206
+ cache_d: Path,
207
+ ) -> bytes:
208
+ """Build a versioned envelope and return gzip-compressed bytes."""
209
+ envelope: dict[str, Any] = {
210
+ "sv": SCHEMA_VERSION,
211
+ "key": cache_key,
212
+ "ts": _now_iso(),
213
+ "fmt": fmt,
214
+ "layers": layers,
215
+ }
216
+
217
+ if fmt == "json":
218
+ # Try to parse and extract large fields into CAS
219
+ try:
220
+ snap_dict = json.loads(content)
221
+ if isinstance(snap_dict, dict):
222
+ inline, cas_refs = _cas_extract(snap_dict, cache_d)
223
+ envelope["snap"] = inline
224
+ if cas_refs:
225
+ envelope["cas"] = cas_refs
226
+ else:
227
+ # JSON array or primitive — store as-is
228
+ envelope["raw"] = content
229
+ except Exception:
230
+ envelope["raw"] = content
231
+ else:
232
+ # YAML or unknown format — store raw string
233
+ envelope["raw"] = content
234
+
235
+ return gzip.compress(
236
+ json.dumps(envelope, ensure_ascii=False).encode("utf-8"),
237
+ compresslevel=6,
238
+ )
239
+
240
+
241
+ def _parse_envelope(data: bytes, cache_d: Path) -> Optional[str]:
242
+ """
243
+ Decompress *data*, parse envelope, resolve CAS refs, return content string.
244
+
245
+ Returns ``None`` on schema version mismatch, CAS miss, or parse failure.
246
+ v1 files (no envelope wrapper) are detected and served transparently.
247
+ """
248
+ try:
249
+ raw_bytes = gzip.decompress(data)
250
+ except Exception:
251
+ return None
252
+
253
+ # ── v1 detection ────────────────────────────────────────────────────────
254
+ # v1 stored the content string directly (gzip'd UTF-8), not an envelope.
255
+ # Heuristic: if decompressed bytes are not a JSON object with an "sv" key,
256
+ # treat as v1 and return the raw bytes as the content string.
257
+ try:
258
+ envelope = json.loads(raw_bytes.decode("utf-8"))
259
+ except Exception:
260
+ # Not JSON at all (e.g. YAML v1) — return as-is
261
+ try:
262
+ return raw_bytes.decode("utf-8")
263
+ except Exception:
264
+ return None
265
+
266
+ if not isinstance(envelope, dict) or envelope.get("sv") != SCHEMA_VERSION:
267
+ # dict without "sv" → v1 JSON snapshot; non-matching sv → old envelope
268
+ # Serve v1 transparently; reject mismatched schema versions as a miss.
269
+ if isinstance(envelope, dict) and "sv" in envelope:
270
+ return None # schema version mismatch
271
+ # No "sv" at all → v1 format, raw content
272
+ return raw_bytes.decode("utf-8")
273
+
274
+ # ── v2 envelope ─────────────────────────────────────────────────────────
275
+ if "raw" in envelope:
276
+ return envelope["raw"]
277
+
278
+ if "snap" in envelope:
279
+ inline: dict[str, Any] = envelope["snap"]
280
+ cas_refs: dict[str, str] = envelope.get("cas", {})
281
+ if cas_refs:
282
+ restored = _cas_restore(inline, cas_refs, cache_d)
283
+ if restored is None:
284
+ return None # CAS miss (blob evicted or corrupted)
285
+ else:
286
+ restored = dict(inline)
287
+ # Re-serialise with the same parameters used by the pipeline.
288
+ # json.loads → json.dumps round-trips correctly: Python 3.7+ preserves
289
+ # dict insertion order and the pipeline uses indent=2, ensure_ascii=False.
290
+ return json.dumps(restored, indent=2, ensure_ascii=False)
291
+
292
+ return None # malformed envelope
293
+
294
+
295
+ # ---------------------------------------------------------------------------
296
+ # CAS store
297
+ # ---------------------------------------------------------------------------
298
+
299
+ def _cas_dir(cache_d: Path) -> Path:
300
+ return cache_d / "cas"
301
+
302
+
303
+ def _cas_path(cache_d: Path, blob_hash: str) -> Path:
304
+ return _cas_dir(cache_d) / f"{blob_hash}.gz"
305
+
306
+
307
+ def _cas_store_blob(cache_d: Path, serialised: str) -> str:
308
+ """
309
+ Store *serialised* (a JSON string) in the CAS. Idempotent.
310
+
311
+ Returns the 16-char SHA-256 hex hash that identifies the blob.
312
+ """
313
+ raw = serialised.encode("utf-8")
314
+ blob_hash = hashlib.sha256(raw).hexdigest()[:16]
315
+ path = _cas_path(cache_d, blob_hash)
316
+ if not path.exists():
317
+ path.parent.mkdir(parents=True, exist_ok=True)
318
+ path.write_bytes(gzip.compress(raw, compresslevel=6))
319
+ return blob_hash
320
+
321
+
322
+ def _cas_load_blob(cache_d: Path, blob_hash: str) -> Optional[str]:
323
+ """Return the stored JSON string for *blob_hash*, or ``None`` if absent."""
324
+ path = _cas_path(cache_d, blob_hash)
325
+ if not path.exists():
326
+ return None
327
+ try:
328
+ return gzip.decompress(path.read_bytes()).decode("utf-8")
329
+ except Exception:
330
+ return None
331
+
332
+
333
+ def _cas_extract(
334
+ snap_dict: dict[str, Any],
335
+ cache_d: Path,
336
+ ) -> tuple[dict[str, Any], dict[str, str]]:
337
+ """
338
+ Walk *snap_dict* top-level fields. Fields that:
339
+ - are in ``_CAS_FIELDS``
340
+ - serialise to more than ``_CAS_THRESHOLD`` bytes
341
+
342
+ … are stored as CAS blobs and replaced with their hash in the returned
343
+ ``cas_refs`` mapping. Other fields remain inline.
344
+ """
345
+ inline: dict[str, Any] = {}
346
+ cas_refs: dict[str, str] = {}
347
+
348
+ for key, value in snap_dict.items():
349
+ if key in _CAS_FIELDS and value is not None:
350
+ serialised = json.dumps(value, ensure_ascii=False)
351
+ if len(serialised.encode("utf-8")) > _CAS_THRESHOLD:
352
+ blob_hash = _cas_store_blob(cache_d, serialised)
353
+ cas_refs[key] = blob_hash
354
+ continue
355
+ inline[key] = value
356
+
357
+ return inline, cas_refs
358
+
359
+
360
+ def _cas_restore(
361
+ inline: dict[str, Any],
362
+ cas_refs: dict[str, str],
363
+ cache_d: Path,
364
+ ) -> Optional[dict[str, Any]]:
365
+ """
366
+ Reconstruct a full snapshot dict by loading CAS blobs for *cas_refs*.
367
+
368
+ Returns ``None`` if any blob is missing (treat as cache miss).
369
+ """
370
+ result: dict[str, Any] = dict(inline)
371
+ for field, blob_hash in cas_refs.items():
372
+ blob_str = _cas_load_blob(cache_d, blob_hash)
373
+ if blob_str is None:
374
+ return None # blob evicted or corrupted → full miss
375
+ try:
376
+ result[field] = json.loads(blob_str)
377
+ except Exception:
378
+ return None
379
+ return result
380
+
381
+
382
+ # ---------------------------------------------------------------------------
383
+ # Eviction / GC
384
+ # ---------------------------------------------------------------------------
385
+
386
+ def _gc(cache_d: Path) -> None:
387
+ """
388
+ Evict old snapshots and sweep orphaned CAS blobs.
389
+
390
+ Keeps snapshots from the last ``SOURCECODE_CACHE_KEEP_COMMITS`` distinct
391
+ git commits (determined by mtime of files in each commit group).
392
+ """
393
+ keep = int(os.environ.get("SOURCECODE_CACHE_KEEP_COMMITS", _DEFAULT_KEEP_COMMITS))
394
+
395
+ try:
396
+ all_snapshots = list(cache_d.glob("snapshot-*.json.gz"))
397
+ if not all_snapshots:
398
+ return
399
+
400
+ # Group snapshot files by commit SHA
401
+ groups: dict[str, list[Path]] = {}
402
+ for f in all_snapshots:
403
+ m = _SNAPSHOT_RE.match(f.name)
404
+ if m:
405
+ groups.setdefault(m.group(1), []).append(f)
406
+
407
+ surviving: list[Path]
408
+
409
+ if keep <= 0 or len(groups) <= keep:
410
+ # No eviction needed — but still sweep CAS
411
+ surviving = all_snapshots
412
+ else:
413
+ def _newest_mtime(commit: str) -> float:
414
+ return max(p.stat().st_mtime for p in groups[commit])
415
+
416
+ sorted_commits = sorted(groups, key=_newest_mtime, reverse=True)
417
+ surviving = []
418
+ for i, commit in enumerate(sorted_commits):
419
+ if i < keep:
420
+ surviving.extend(groups[commit])
421
+ else:
422
+ for f in groups[commit]:
423
+ _safe_unlink(f)
424
+
425
+ _gc_cas(cache_d, surviving)
426
+
427
+ except Exception:
428
+ pass # GC failure is non-fatal
429
+
430
+
431
+ def _gc_cas(cache_d: Path, surviving_snapshots: list[Path]) -> None:
432
+ """
433
+ Delete CAS blobs not referenced by any snapshot in *surviving_snapshots*.
434
+
435
+ Walks each snapshot's ``cas`` dict to collect live hashes; deletes the rest.
436
+ """
437
+ cas_d = _cas_dir(cache_d)
438
+ if not cas_d.exists():
439
+ return
440
+
441
+ try:
442
+ # Collect all hashes referenced by surviving snapshots
443
+ referenced: set[str] = set()
444
+ for snap_path in surviving_snapshots:
445
+ try:
446
+ raw = gzip.decompress(snap_path.read_bytes())
447
+ env = json.loads(raw.decode("utf-8"))
448
+ if isinstance(env, dict) and "cas" in env:
449
+ referenced.update(env["cas"].values())
450
+ except Exception:
451
+ pass # unreadable snapshot — conservatively keep its blobs unknown
452
+
453
+ # Delete blobs not referenced by any surviving snapshot
454
+ for blob in cas_d.glob("*.gz"):
455
+ if blob.stem not in referenced:
456
+ _safe_unlink(blob)
457
+
458
+ except Exception:
459
+ pass # CAS sweep failure is non-fatal
460
+
461
+
462
+ # ---------------------------------------------------------------------------
463
+ # Utilities
464
+ # ---------------------------------------------------------------------------
465
+
466
+ def _safe_unlink(path: Path) -> None:
467
+ try:
468
+ path.unlink(missing_ok=True)
469
+ except Exception:
470
+ pass