sourcecode 1.31.18__tar.gz → 1.31.21__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 (227) hide show
  1. sourcecode-1.31.21/.continue-here.md +87 -0
  2. {sourcecode-1.31.18 → sourcecode-1.31.21}/PKG-INFO +3 -3
  3. {sourcecode-1.31.18 → sourcecode-1.31.21}/README.md +2 -2
  4. {sourcecode-1.31.18 → sourcecode-1.31.21}/pyproject.toml +1 -1
  5. {sourcecode-1.31.18 → sourcecode-1.31.21}/src/sourcecode/__init__.py +1 -1
  6. {sourcecode-1.31.18 → sourcecode-1.31.21}/src/sourcecode/cache.py +193 -9
  7. {sourcecode-1.31.18 → sourcecode-1.31.21}/src/sourcecode/cli.py +155 -34
  8. {sourcecode-1.31.18 → sourcecode-1.31.21}/src/sourcecode/contract_pipeline.py +21 -1
  9. {sourcecode-1.31.18 → sourcecode-1.31.21}/src/sourcecode/mcp/server.py +26 -4
  10. {sourcecode-1.31.18 → sourcecode-1.31.21}/src/sourcecode/serializer.py +113 -0
  11. {sourcecode-1.31.18 → sourcecode-1.31.21}/tests/test_bug_fixes_v16.py +82 -0
  12. {sourcecode-1.31.18 → sourcecode-1.31.21}/tests/test_cache.py +168 -0
  13. {sourcecode-1.31.18 → sourcecode-1.31.21}/tests/test_mcp_tools.py +47 -15
  14. sourcecode-1.31.18/.continue-here.md +0 -134
  15. {sourcecode-1.31.18 → sourcecode-1.31.21}/.agents/skills/source-command-gsd-join-discord/SKILL.md +0 -0
  16. {sourcecode-1.31.18 → sourcecode-1.31.21}/.agents/skills/source-command-gsd-review-backlog/SKILL.md +0 -0
  17. {sourcecode-1.31.18 → sourcecode-1.31.21}/.agents/skills/source-command-gsd-workstreams/SKILL.md +0 -0
  18. {sourcecode-1.31.18 → sourcecode-1.31.21}/.github/workflows/build-windows.yml +0 -0
  19. {sourcecode-1.31.18 → sourcecode-1.31.21}/.gitignore +0 -0
  20. {sourcecode-1.31.18 → sourcecode-1.31.21}/.ruff.toml +0 -0
  21. {sourcecode-1.31.18 → sourcecode-1.31.21}/.sourcecode-cache/snapshot-3b5997a-fa5c742c.json +0 -0
  22. {sourcecode-1.31.18 → sourcecode-1.31.21}/AUDIT_REAL_REPOS.md +0 -0
  23. {sourcecode-1.31.18 → sourcecode-1.31.21}/CHANGELOG.md +0 -0
  24. {sourcecode-1.31.18 → sourcecode-1.31.21}/CONTRIBUTING.md +0 -0
  25. {sourcecode-1.31.18 → sourcecode-1.31.21}/LICENSE +0 -0
  26. {sourcecode-1.31.18 → sourcecode-1.31.21}/SECURITY.md +0 -0
  27. {sourcecode-1.31.18 → sourcecode-1.31.21}/docs/PRODUCT_TIERS.md +0 -0
  28. {sourcecode-1.31.18 → sourcecode-1.31.21}/docs/privacy.md +0 -0
  29. {sourcecode-1.31.18 → sourcecode-1.31.21}/docs/schema.md +0 -0
  30. {sourcecode-1.31.18 → sourcecode-1.31.21}/raw +0 -0
  31. {sourcecode-1.31.18 → sourcecode-1.31.21}/run_cli.py +0 -0
  32. {sourcecode-1.31.18 → sourcecode-1.31.21}/src/sourcecode/adaptive_scanner.py +0 -0
  33. {sourcecode-1.31.18 → sourcecode-1.31.21}/src/sourcecode/architecture_analyzer.py +0 -0
  34. {sourcecode-1.31.18 → sourcecode-1.31.21}/src/sourcecode/architecture_summary.py +0 -0
  35. {sourcecode-1.31.18 → sourcecode-1.31.21}/src/sourcecode/ast_extractor.py +0 -0
  36. {sourcecode-1.31.18 → sourcecode-1.31.21}/src/sourcecode/canonical_ir.py +0 -0
  37. {sourcecode-1.31.18 → sourcecode-1.31.21}/src/sourcecode/classifier.py +0 -0
  38. {sourcecode-1.31.18 → sourcecode-1.31.21}/src/sourcecode/code_notes_analyzer.py +0 -0
  39. {sourcecode-1.31.18 → sourcecode-1.31.21}/src/sourcecode/confidence_analyzer.py +0 -0
  40. {sourcecode-1.31.18 → sourcecode-1.31.21}/src/sourcecode/context_scorer.py +0 -0
  41. {sourcecode-1.31.18 → sourcecode-1.31.21}/src/sourcecode/context_summarizer.py +0 -0
  42. {sourcecode-1.31.18 → sourcecode-1.31.21}/src/sourcecode/contract_model.py +0 -0
  43. {sourcecode-1.31.18 → sourcecode-1.31.21}/src/sourcecode/coverage_parser.py +0 -0
  44. {sourcecode-1.31.18 → sourcecode-1.31.21}/src/sourcecode/dependency_analyzer.py +0 -0
  45. {sourcecode-1.31.18 → sourcecode-1.31.21}/src/sourcecode/detectors/__init__.py +0 -0
  46. {sourcecode-1.31.18 → sourcecode-1.31.21}/src/sourcecode/detectors/base.py +0 -0
  47. {sourcecode-1.31.18 → sourcecode-1.31.21}/src/sourcecode/detectors/csproj_parser.py +0 -0
  48. {sourcecode-1.31.18 → sourcecode-1.31.21}/src/sourcecode/detectors/dart.py +0 -0
  49. {sourcecode-1.31.18 → sourcecode-1.31.21}/src/sourcecode/detectors/dotnet.py +0 -0
  50. {sourcecode-1.31.18 → sourcecode-1.31.21}/src/sourcecode/detectors/elixir.py +0 -0
  51. {sourcecode-1.31.18 → sourcecode-1.31.21}/src/sourcecode/detectors/go.py +0 -0
  52. {sourcecode-1.31.18 → sourcecode-1.31.21}/src/sourcecode/detectors/heuristic.py +0 -0
  53. {sourcecode-1.31.18 → sourcecode-1.31.21}/src/sourcecode/detectors/hybrid.py +0 -0
  54. {sourcecode-1.31.18 → sourcecode-1.31.21}/src/sourcecode/detectors/java.py +0 -0
  55. {sourcecode-1.31.18 → sourcecode-1.31.21}/src/sourcecode/detectors/jvm_ext.py +0 -0
  56. {sourcecode-1.31.18 → sourcecode-1.31.21}/src/sourcecode/detectors/nodejs.py +0 -0
  57. {sourcecode-1.31.18 → sourcecode-1.31.21}/src/sourcecode/detectors/parsers.py +0 -0
  58. {sourcecode-1.31.18 → sourcecode-1.31.21}/src/sourcecode/detectors/php.py +0 -0
  59. {sourcecode-1.31.18 → sourcecode-1.31.21}/src/sourcecode/detectors/project.py +0 -0
  60. {sourcecode-1.31.18 → sourcecode-1.31.21}/src/sourcecode/detectors/python.py +0 -0
  61. {sourcecode-1.31.18 → sourcecode-1.31.21}/src/sourcecode/detectors/ruby.py +0 -0
  62. {sourcecode-1.31.18 → sourcecode-1.31.21}/src/sourcecode/detectors/rust.py +0 -0
  63. {sourcecode-1.31.18 → sourcecode-1.31.21}/src/sourcecode/detectors/systems.py +0 -0
  64. {sourcecode-1.31.18 → sourcecode-1.31.21}/src/sourcecode/detectors/terraform.py +0 -0
  65. {sourcecode-1.31.18 → sourcecode-1.31.21}/src/sourcecode/detectors/tooling.py +0 -0
  66. {sourcecode-1.31.18 → sourcecode-1.31.21}/src/sourcecode/doc_analyzer.py +0 -0
  67. {sourcecode-1.31.18 → sourcecode-1.31.21}/src/sourcecode/entrypoint_classifier.py +0 -0
  68. {sourcecode-1.31.18 → sourcecode-1.31.21}/src/sourcecode/env_analyzer.py +0 -0
  69. {sourcecode-1.31.18 → sourcecode-1.31.21}/src/sourcecode/file_classifier.py +0 -0
  70. {sourcecode-1.31.18 → sourcecode-1.31.21}/src/sourcecode/flow_analyzer.py +0 -0
  71. {sourcecode-1.31.18 → sourcecode-1.31.21}/src/sourcecode/git_analyzer.py +0 -0
  72. {sourcecode-1.31.18 → sourcecode-1.31.21}/src/sourcecode/graph_analyzer.py +0 -0
  73. {sourcecode-1.31.18 → sourcecode-1.31.21}/src/sourcecode/mcp/__init__.py +0 -0
  74. {sourcecode-1.31.18 → sourcecode-1.31.21}/src/sourcecode/mcp/onboarding/__init__.py +0 -0
  75. {sourcecode-1.31.18 → sourcecode-1.31.21}/src/sourcecode/mcp/onboarding/applier.py +0 -0
  76. {sourcecode-1.31.18 → sourcecode-1.31.21}/src/sourcecode/mcp/onboarding/backup.py +0 -0
  77. {sourcecode-1.31.18 → sourcecode-1.31.21}/src/sourcecode/mcp/onboarding/detector.py +0 -0
  78. {sourcecode-1.31.18 → sourcecode-1.31.21}/src/sourcecode/mcp/onboarding/planner.py +0 -0
  79. {sourcecode-1.31.18 → sourcecode-1.31.21}/src/sourcecode/mcp/runner.py +0 -0
  80. {sourcecode-1.31.18 → sourcecode-1.31.21}/src/sourcecode/metrics_analyzer.py +0 -0
  81. {sourcecode-1.31.18 → sourcecode-1.31.21}/src/sourcecode/output_budget.py +0 -0
  82. {sourcecode-1.31.18 → sourcecode-1.31.21}/src/sourcecode/path_filters.py +0 -0
  83. {sourcecode-1.31.18 → sourcecode-1.31.21}/src/sourcecode/pr_comment_renderer.py +0 -0
  84. {sourcecode-1.31.18 → sourcecode-1.31.21}/src/sourcecode/prepare_context.py +0 -0
  85. {sourcecode-1.31.18 → sourcecode-1.31.21}/src/sourcecode/progress.py +0 -0
  86. {sourcecode-1.31.18 → sourcecode-1.31.21}/src/sourcecode/ranking_engine.py +0 -0
  87. {sourcecode-1.31.18 → sourcecode-1.31.21}/src/sourcecode/redactor.py +0 -0
  88. {sourcecode-1.31.18 → sourcecode-1.31.21}/src/sourcecode/relevance_scorer.py +0 -0
  89. {sourcecode-1.31.18 → sourcecode-1.31.21}/src/sourcecode/repo_classifier.py +0 -0
  90. {sourcecode-1.31.18 → sourcecode-1.31.21}/src/sourcecode/repository_ir.py +0 -0
  91. {sourcecode-1.31.18 → sourcecode-1.31.21}/src/sourcecode/runtime_classifier.py +0 -0
  92. {sourcecode-1.31.18 → sourcecode-1.31.21}/src/sourcecode/scanner.py +0 -0
  93. {sourcecode-1.31.18 → sourcecode-1.31.21}/src/sourcecode/schema.py +0 -0
  94. {sourcecode-1.31.18 → sourcecode-1.31.21}/src/sourcecode/semantic_analyzer.py +0 -0
  95. {sourcecode-1.31.18 → sourcecode-1.31.21}/src/sourcecode/summarizer.py +0 -0
  96. {sourcecode-1.31.18 → sourcecode-1.31.21}/src/sourcecode/telemetry/__init__.py +0 -0
  97. {sourcecode-1.31.18 → sourcecode-1.31.21}/src/sourcecode/telemetry/config.py +0 -0
  98. {sourcecode-1.31.18 → sourcecode-1.31.21}/src/sourcecode/telemetry/consent.py +0 -0
  99. {sourcecode-1.31.18 → sourcecode-1.31.21}/src/sourcecode/telemetry/events.py +0 -0
  100. {sourcecode-1.31.18 → sourcecode-1.31.21}/src/sourcecode/telemetry/filters.py +0 -0
  101. {sourcecode-1.31.18 → sourcecode-1.31.21}/src/sourcecode/telemetry/transport.py +0 -0
  102. {sourcecode-1.31.18 → sourcecode-1.31.21}/src/sourcecode/tree_utils.py +0 -0
  103. {sourcecode-1.31.18 → sourcecode-1.31.21}/src/sourcecode/workspace.py +0 -0
  104. {sourcecode-1.31.18 → sourcecode-1.31.21}/tests/__init__.py +0 -0
  105. {sourcecode-1.31.18 → sourcecode-1.31.21}/tests/conftest.py +0 -0
  106. {sourcecode-1.31.18 → sourcecode-1.31.21}/tests/fixtures/coverage.xml +0 -0
  107. {sourcecode-1.31.18 → sourcecode-1.31.21}/tests/fixtures/fastapi_app/pyproject.toml +0 -0
  108. {sourcecode-1.31.18 → sourcecode-1.31.21}/tests/fixtures/fastapi_app/src/main.py +0 -0
  109. {sourcecode-1.31.18 → sourcecode-1.31.21}/tests/fixtures/go_service/cmd/api/main.go +0 -0
  110. {sourcecode-1.31.18 → sourcecode-1.31.21}/tests/fixtures/go_service/go.mod +0 -0
  111. {sourcecode-1.31.18 → sourcecode-1.31.21}/tests/fixtures/jacoco.xml +0 -0
  112. {sourcecode-1.31.18 → sourcecode-1.31.21}/tests/fixtures/latin1_sample.java +0 -0
  113. {sourcecode-1.31.18 → sourcecode-1.31.21}/tests/fixtures/latin1_sample_iso.java +0 -0
  114. {sourcecode-1.31.18 → sourcecode-1.31.21}/tests/fixtures/lcov.info +0 -0
  115. {sourcecode-1.31.18 → sourcecode-1.31.21}/tests/fixtures/nextjs_app/app/page.tsx +0 -0
  116. {sourcecode-1.31.18 → sourcecode-1.31.21}/tests/fixtures/nextjs_app/package.json +0 -0
  117. {sourcecode-1.31.18 → sourcecode-1.31.21}/tests/fixtures/nextjs_app/pnpm-lock.yaml +0 -0
  118. {sourcecode-1.31.18 → sourcecode-1.31.21}/tests/fixtures/pnpm_monorepo/apps/web/app/page.tsx +0 -0
  119. {sourcecode-1.31.18 → sourcecode-1.31.21}/tests/fixtures/pnpm_monorepo/apps/web/package.json +0 -0
  120. {sourcecode-1.31.18 → sourcecode-1.31.21}/tests/fixtures/pnpm_monorepo/packages/api/main.py +0 -0
  121. {sourcecode-1.31.18 → sourcecode-1.31.21}/tests/fixtures/pnpm_monorepo/packages/api/pyproject.toml +0 -0
  122. {sourcecode-1.31.18 → sourcecode-1.31.21}/tests/fixtures/pnpm_monorepo/pnpm-workspace.yaml +0 -0
  123. {sourcecode-1.31.18 → sourcecode-1.31.21}/tests/fixtures/spring_boot_minimal/pom.xml +0 -0
  124. {sourcecode-1.31.18 → sourcecode-1.31.21}/tests/fixtures/spring_boot_minimal/src/main/java/com/example/ddd/ausente/application/service/FindAusenteService.java +0 -0
  125. {sourcecode-1.31.18 → sourcecode-1.31.21}/tests/fixtures/spring_boot_minimal/src/main/java/com/example/ddd/ausente/domain/entities/Ausente.java +0 -0
  126. {sourcecode-1.31.18 → sourcecode-1.31.21}/tests/fixtures/spring_boot_minimal/src/main/java/com/example/ddd/ausente/infrastructure/rest/AusenteRestController.java +0 -0
  127. {sourcecode-1.31.18 → sourcecode-1.31.21}/tests/fixtures/spring_boot_minimal/src/main/java/com/example/ddd/autocoberturas/application/service/FindAutocoberturasService.java +0 -0
  128. {sourcecode-1.31.18 → sourcecode-1.31.21}/tests/fixtures/spring_boot_minimal/src/main/java/com/example/ddd/autocoberturas/domain/entities/Autocoberturas.java +0 -0
  129. {sourcecode-1.31.18 → sourcecode-1.31.21}/tests/fixtures/spring_boot_minimal/src/main/java/com/example/ddd/autocoberturas/infrastructure/rest/AutocoberturasRestController.java +0 -0
  130. {sourcecode-1.31.18 → sourcecode-1.31.21}/tests/fixtures/spring_boot_minimal/src/main/java/com/example/ddd/calendarioTrabajador/application/service/FindCalendarioTrabajadorService.java +0 -0
  131. {sourcecode-1.31.18 → sourcecode-1.31.21}/tests/fixtures/spring_boot_minimal/src/main/java/com/example/ddd/calendarioTrabajador/domain/entities/CalendarioTrabajador.java +0 -0
  132. {sourcecode-1.31.18 → sourcecode-1.31.21}/tests/fixtures/spring_boot_minimal/src/main/java/com/example/ddd/calendarioTrabajador/infrastructure/rest/CalendarioTrabajadorRestController.java +0 -0
  133. {sourcecode-1.31.18 → sourcecode-1.31.21}/tests/fixtures/spring_boot_minimal/src/main/java/com/example/ddd/departamento/application/service/FindDepartamentoService.java +0 -0
  134. {sourcecode-1.31.18 → sourcecode-1.31.21}/tests/fixtures/spring_boot_minimal/src/main/java/com/example/ddd/departamento/domain/entities/Departamento.java +0 -0
  135. {sourcecode-1.31.18 → sourcecode-1.31.21}/tests/fixtures/spring_boot_minimal/src/main/java/com/example/ddd/departamento/infrastructure/rest/DepartamentoRestController.java +0 -0
  136. {sourcecode-1.31.18 → sourcecode-1.31.21}/tests/fixtures/spring_boot_minimal/src/main/java/com/example/ddd/empleado/application/service/FindEmpleadoService.java +0 -0
  137. {sourcecode-1.31.18 → sourcecode-1.31.21}/tests/fixtures/spring_boot_minimal/src/main/java/com/example/ddd/empleado/domain/entities/Empleado.java +0 -0
  138. {sourcecode-1.31.18 → sourcecode-1.31.21}/tests/fixtures/spring_boot_minimal/src/main/java/com/example/ddd/empleado/infrastructure/rest/EmpleadoRestController.java +0 -0
  139. {sourcecode-1.31.18 → sourcecode-1.31.21}/tests/fixtures/spring_boot_minimal/src/main/java/com/example/demo/DemoApplication.java +0 -0
  140. {sourcecode-1.31.18 → sourcecode-1.31.21}/tests/fixtures/spring_boot_minimal/src/main/java/com/example/demo/config/FilterConfig.java +0 -0
  141. {sourcecode-1.31.18 → sourcecode-1.31.21}/tests/fixtures/spring_boot_minimal/src/main/java/com/example/demo/domain/Health.java +0 -0
  142. {sourcecode-1.31.18 → sourcecode-1.31.21}/tests/fixtures/spring_boot_minimal/src/main/java/com/example/demo/mapper/HealthMapper.java +0 -0
  143. {sourcecode-1.31.18 → sourcecode-1.31.21}/tests/fixtures/spring_boot_minimal/src/main/java/com/example/demo/repository/HealthRepository.java +0 -0
  144. {sourcecode-1.31.18 → sourcecode-1.31.21}/tests/fixtures/spring_boot_minimal/src/main/java/com/example/demo/service/HealthService.java +0 -0
  145. {sourcecode-1.31.18 → sourcecode-1.31.21}/tests/fixtures/spring_boot_minimal/src/main/java/com/example/demo/web/HealthRestController.java +0 -0
  146. {sourcecode-1.31.18 → sourcecode-1.31.21}/tests/fixtures/spring_boot_minimal/src/main/java/com/example/demo/web/NominaRestController.java +0 -0
  147. {sourcecode-1.31.18 → sourcecode-1.31.21}/tests/fixtures/spring_boot_minimal/src/main/resources/application-dev.yml +0 -0
  148. {sourcecode-1.31.18 → sourcecode-1.31.21}/tests/fixtures/spring_boot_minimal/src/main/resources/application.yml +0 -0
  149. {sourcecode-1.31.18 → sourcecode-1.31.21}/tests/fixtures/spring_boot_minimal/src/main/resources/mapper/HealthMapper.xml +0 -0
  150. {sourcecode-1.31.18 → sourcecode-1.31.21}/tests/test_architecture_analyzer.py +0 -0
  151. {sourcecode-1.31.18 → sourcecode-1.31.21}/tests/test_architecture_summary.py +0 -0
  152. {sourcecode-1.31.18 → sourcecode-1.31.21}/tests/test_ast_extractor.py +0 -0
  153. {sourcecode-1.31.18 → sourcecode-1.31.21}/tests/test_audit_fixes.py +0 -0
  154. {sourcecode-1.31.18 → sourcecode-1.31.21}/tests/test_audit_sas_v2.py +0 -0
  155. {sourcecode-1.31.18 → sourcecode-1.31.21}/tests/test_block1_reliability.py +0 -0
  156. {sourcecode-1.31.18 → sourcecode-1.31.21}/tests/test_block2_coverage.py +0 -0
  157. {sourcecode-1.31.18 → sourcecode-1.31.21}/tests/test_block5_quality.py +0 -0
  158. {sourcecode-1.31.18 → sourcecode-1.31.21}/tests/test_broadleaf_fixes.py +0 -0
  159. {sourcecode-1.31.18 → sourcecode-1.31.21}/tests/test_bug_fixes_v1302.py +0 -0
  160. {sourcecode-1.31.18 → sourcecode-1.31.21}/tests/test_bug_fixes_v13115.py +0 -0
  161. {sourcecode-1.31.18 → sourcecode-1.31.21}/tests/test_bug_fixes_v1312.py +0 -0
  162. {sourcecode-1.31.18 → sourcecode-1.31.21}/tests/test_bug_fixes_v1313.py +0 -0
  163. {sourcecode-1.31.18 → sourcecode-1.31.21}/tests/test_bug_fixes_v2.py +0 -0
  164. {sourcecode-1.31.18 → sourcecode-1.31.21}/tests/test_canonical_ir.py +0 -0
  165. {sourcecode-1.31.18 → sourcecode-1.31.21}/tests/test_classifier.py +0 -0
  166. {sourcecode-1.31.18 → sourcecode-1.31.21}/tests/test_cli.py +0 -0
  167. {sourcecode-1.31.18 → sourcecode-1.31.21}/tests/test_code_notes_analyzer.py +0 -0
  168. {sourcecode-1.31.18 → sourcecode-1.31.21}/tests/test_context_scorer.py +0 -0
  169. {sourcecode-1.31.18 → sourcecode-1.31.21}/tests/test_contract_pipeline.py +0 -0
  170. {sourcecode-1.31.18 → sourcecode-1.31.21}/tests/test_coverage_parser.py +0 -0
  171. {sourcecode-1.31.18 → sourcecode-1.31.21}/tests/test_cross_consistency.py +0 -0
  172. {sourcecode-1.31.18 → sourcecode-1.31.21}/tests/test_dependency_analyzer_node_python.py +0 -0
  173. {sourcecode-1.31.18 → sourcecode-1.31.21}/tests/test_dependency_analyzer_polyglot.py +0 -0
  174. {sourcecode-1.31.18 → sourcecode-1.31.21}/tests/test_dependency_schema.py +0 -0
  175. {sourcecode-1.31.18 → sourcecode-1.31.21}/tests/test_detector_dotnet.py +0 -0
  176. {sourcecode-1.31.18 → sourcecode-1.31.21}/tests/test_detector_go_rust_java.py +0 -0
  177. {sourcecode-1.31.18 → sourcecode-1.31.21}/tests/test_detector_nodejs.py +0 -0
  178. {sourcecode-1.31.18 → sourcecode-1.31.21}/tests/test_detector_php_ruby_dart.py +0 -0
  179. {sourcecode-1.31.18 → sourcecode-1.31.21}/tests/test_detector_python.py +0 -0
  180. {sourcecode-1.31.18 → sourcecode-1.31.21}/tests/test_detector_universal_managed.py +0 -0
  181. {sourcecode-1.31.18 → sourcecode-1.31.21}/tests/test_detector_universal_systems.py +0 -0
  182. {sourcecode-1.31.18 → sourcecode-1.31.21}/tests/test_detectors_base.py +0 -0
  183. {sourcecode-1.31.18 → sourcecode-1.31.21}/tests/test_doc_analyzer_jsdom.py +0 -0
  184. {sourcecode-1.31.18 → sourcecode-1.31.21}/tests/test_doc_analyzer_python.py +0 -0
  185. {sourcecode-1.31.18 → sourcecode-1.31.21}/tests/test_encoding_regression.py +0 -0
  186. {sourcecode-1.31.18 → sourcecode-1.31.21}/tests/test_enterprise_benchmarks.py +0 -0
  187. {sourcecode-1.31.18 → sourcecode-1.31.21}/tests/test_graph_analyzer_polyglot.py +0 -0
  188. {sourcecode-1.31.18 → sourcecode-1.31.21}/tests/test_graph_analyzer_python_node.py +0 -0
  189. {sourcecode-1.31.18 → sourcecode-1.31.21}/tests/test_graph_schema.py +0 -0
  190. {sourcecode-1.31.18 → sourcecode-1.31.21}/tests/test_hybrid_inference.py +0 -0
  191. {sourcecode-1.31.18 → sourcecode-1.31.21}/tests/test_integration.py +0 -0
  192. {sourcecode-1.31.18 → sourcecode-1.31.21}/tests/test_integration_dependencies.py +0 -0
  193. {sourcecode-1.31.18 → sourcecode-1.31.21}/tests/test_integration_detection.py +0 -0
  194. {sourcecode-1.31.18 → sourcecode-1.31.21}/tests/test_integration_docs.py +0 -0
  195. {sourcecode-1.31.18 → sourcecode-1.31.21}/tests/test_integration_graph_modules.py +0 -0
  196. {sourcecode-1.31.18 → sourcecode-1.31.21}/tests/test_integration_lqn.py +0 -0
  197. {sourcecode-1.31.18 → sourcecode-1.31.21}/tests/test_integration_metrics.py +0 -0
  198. {sourcecode-1.31.18 → sourcecode-1.31.21}/tests/test_integration_multistack.py +0 -0
  199. {sourcecode-1.31.18 → sourcecode-1.31.21}/tests/test_integration_semantics.py +0 -0
  200. {sourcecode-1.31.18 → sourcecode-1.31.21}/tests/test_integration_universal.py +0 -0
  201. {sourcecode-1.31.18 → sourcecode-1.31.21}/tests/test_java_spring_integration.py +0 -0
  202. {sourcecode-1.31.18 → sourcecode-1.31.21}/tests/test_mcp_runner.py +0 -0
  203. {sourcecode-1.31.18 → sourcecode-1.31.21}/tests/test_mcp_serve.py +0 -0
  204. {sourcecode-1.31.18 → sourcecode-1.31.21}/tests/test_metrics_analyzer.py +0 -0
  205. {sourcecode-1.31.18 → sourcecode-1.31.21}/tests/test_output_ux.py +0 -0
  206. {sourcecode-1.31.18 → sourcecode-1.31.21}/tests/test_packaging.py +0 -0
  207. {sourcecode-1.31.18 → sourcecode-1.31.21}/tests/test_phase1_improvements.py +0 -0
  208. {sourcecode-1.31.18 → sourcecode-1.31.21}/tests/test_pipeline_integrity.py +0 -0
  209. {sourcecode-1.31.18 → sourcecode-1.31.21}/tests/test_real_projects.py +0 -0
  210. {sourcecode-1.31.18 → sourcecode-1.31.21}/tests/test_redactor.py +0 -0
  211. {sourcecode-1.31.18 → sourcecode-1.31.21}/tests/test_repository_ir.py +0 -0
  212. {sourcecode-1.31.18 → sourcecode-1.31.21}/tests/test_scanner.py +0 -0
  213. {sourcecode-1.31.18 → sourcecode-1.31.21}/tests/test_schema.py +0 -0
  214. {sourcecode-1.31.18 → sourcecode-1.31.21}/tests/test_schema_normalization.py +0 -0
  215. {sourcecode-1.31.18 → sourcecode-1.31.21}/tests/test_scoring_calibration.py +0 -0
  216. {sourcecode-1.31.18 → sourcecode-1.31.21}/tests/test_semantic_analyzer_node.py +0 -0
  217. {sourcecode-1.31.18 → sourcecode-1.31.21}/tests/test_semantic_analyzer_python.py +0 -0
  218. {sourcecode-1.31.18 → sourcecode-1.31.21}/tests/test_semantic_import_resolution.py +0 -0
  219. {sourcecode-1.31.18 → sourcecode-1.31.21}/tests/test_semantic_schema.py +0 -0
  220. {sourcecode-1.31.18 → sourcecode-1.31.21}/tests/test_signal_hierarchy.py +0 -0
  221. {sourcecode-1.31.18 → sourcecode-1.31.21}/tests/test_summarizer.py +0 -0
  222. {sourcecode-1.31.18 → sourcecode-1.31.21}/tests/test_surface_honesty.py +0 -0
  223. {sourcecode-1.31.18 → sourcecode-1.31.21}/tests/test_task_differentiation.py +0 -0
  224. {sourcecode-1.31.18 → sourcecode-1.31.21}/tests/test_telemetry.py +0 -0
  225. {sourcecode-1.31.18 → sourcecode-1.31.21}/tests/test_v131_improvements.py +0 -0
  226. {sourcecode-1.31.18 → sourcecode-1.31.21}/tests/test_v1_10_regressions.py +0 -0
  227. {sourcecode-1.31.18 → sourcecode-1.31.21}/tests/test_workspace_analyzer.py +0 -0
@@ -0,0 +1,87 @@
1
+ # Handoff — sesión 22
2
+ **Date:** 2026-05-25T09:10:25Z
3
+ **Branch:** master
4
+ **Last commit:** `f1e80b6 feat(cache): introduce two-layer cache (L1 core + L2 view)`
5
+ **Suite:** 1510 passed, 3 skipped — all green
6
+
7
+ ---
8
+
9
+ ## What was done this session
10
+
11
+ ### Credibility audit (`AUDIT_REAL_REPOS.md`)
12
+ Full adversarial audit of `sourcecode` against Keycloak (7885 Java files) and BroadleafCommerce (2985 Java files). Findings logged in `AUDIT_REAL_REPOS.md` at repo root. Key bugs fixed in prior sessions (P0-01 Spring DI bridging, P0-02 repo-ir boundedness).
13
+
14
+ ### Two-layer cache architecture (this session)
15
+
16
+ **Problem:** cache key was `(commit, ALL_flags)` → N flag combos × M commits = N×M full snapshots. Same commit with `--compact` vs `--agent` ran the full analysis pipeline twice.
17
+
18
+ **Solution implemented:**
19
+ - **L1 core** `core-<sha>-<analysis_hash>.json.gz` → pre-computed view data, keyed by analysis-affecting flags only
20
+ - **L2 view** `view-<core_hash16>-<view_hash>.json.gz` → rendered string, keyed by view-affecting flags
21
+ - Lookup: L2 hit → L1 hit + view rebuild → full analysis (skip analysis on L1 hit)
22
+
23
+ **Files changed:**
24
+ | File | What |
25
+ |------|------|
26
+ | `src/sourcecode/serializer.py` | Added `core_view(sm)` + `build_view_from_core()` + `CORE_VIEW_VERSION` |
27
+ | `src/sourcecode/cache.py` | Added `read_core/write_core`, `read_view/write_view`, `_gc_views()`, updated `_gc()` |
28
+ | `src/sourcecode/cli.py` | Replaced single-key cache block with 2-layer lookup + write; split flags into core vs view |
29
+ | `tests/test_cache.py` | +12 new tests: TestCoreCache, TestViewCache, TestGCLayered (44 total cache tests) |
30
+
31
+ **Flag split:**
32
+ - Core (analysis): `dep/gm/docs/fm/sem/arch/gc/em/cn/mode/exclude/depth` + version
33
+ - View (presentation): `compact/agent/format/full/no_tree/tree/rank_by/symbol/entrypoints_only/no_redact/graph_detail/docs_depth/max_nodes/graph_edges/max_importers/emit_graph`
34
+
35
+ ---
36
+
37
+ ## Current state
38
+
39
+ Working tree is **clean** — everything committed.
40
+
41
+ GSD state: milestone v1.0, 13 phases all complete (100%). No pending todos in `.planning/STATE.md`.
42
+
43
+ Outstanding work from `AUDIT_REAL_REPOS.md` (NOT yet implemented — only audit findings logged):
44
+
45
+ ### P1 remaining (from audit):
46
+ - **BUG-P1-01** Risk score inconsistency: `OrderServiceImpl` 0 callers → risk_level: low vs `OrderDaoImpl` 0 callers → risk_level: high. Same root cause, different heuristics fire.
47
+ - **BUG-P1-02** `project_summary` copies README blurb instead of generating from code structure
48
+ - **BUG-P1-03** `fix-bug` returns 426 relevant files for generic NPE symptom — no score field, no cap
49
+ - **BUG-P1-04** `indirect_callers: 0` for KeycloakSession (1992 direct callers) — BFS exhausts at level 1
50
+
51
+ ### P2 remaining:
52
+ - BUG-P2-01 `bounded_contexts` detection wrong (uses utility packages, not Maven modules)
53
+ - BUG-P2-02 `role: unknown` for all modernize high_coupling_nodes
54
+ - BUG-P2-03 `no_security_signal` always 100% for filter-based security (JAX-RS/XML)
55
+ - BUG-P2-04 JAX-RS sub-resource paths not composed with parent `@Path`
56
+ - BUG-P2-05 Broadleaf admin paths mixed into REST endpoints
57
+ - BUG-P2-06 Architecture confidence inconsistent between `--compact` and `--agent`
58
+ - BUG-P2-07 `entry_points.controllers.methods: 21` vs endpoints finding 130 (unexplained gap)
59
+ - BUG-P2-08 `--format`/`--no-cache` inconsistently available across subcommands
60
+
61
+ ### Cosmetic:
62
+ - Code notes URLs truncated (`s.webkit.org/...`)
63
+ - `truncated: None` vs `truncated: false` inconsistency
64
+ - `--deep` flag referenced in output but absent from `--help`
65
+ - `--compact --help` claims 1000-3000 tokens; measured 2856-4031
66
+
67
+ ---
68
+
69
+ ## Next session: where to start
70
+
71
+ **Option A** — tackle P1 bugs from audit (highest credibility impact):
72
+ 1. P1-01: risk score consistency when direct_callers=0 (Spring impl detection)
73
+ 2. P1-02: `project_summary` generation from code structure not README
74
+
75
+ **Option B** — write integration tests for 2-layer cache (verify cli.py actually hits L1 on second run with different view flags)
76
+
77
+ **Option C** — version bump + changelog (v1.32.0) with 2-layer cache as headline
78
+
79
+ **Recommended: Option A → P1-01 first** (user-visible correctness, shortest fix).
80
+
81
+ ---
82
+
83
+ ## Resume
84
+
85
+ ```
86
+ cat .continue-here.md
87
+ ```
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: sourcecode
3
- Version: 1.31.18
3
+ Version: 1.31.21
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.18-blue)
228
+ ![Version](https://img.shields.io/badge/version-1.31.21-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.18
266
+ # sourcecode 1.31.21
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.18-blue)
5
+ ![Version](https://img.shields.io/badge/version-1.31.21-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.18
43
+ # sourcecode 1.31.21
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.18"
7
+ version = "1.31.21"
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.18"
3
+ __version__ = "1.31.21"
@@ -69,6 +69,9 @@ from typing import Any, Optional
69
69
  #: Bump this string to invalidate *all* existing cached snapshots.
70
70
  SCHEMA_VERSION: str = "2"
71
71
 
72
+ #: Bump to invalidate all L1 core caches (independent of snapshot version).
73
+ CORE_SCHEMA_VERSION: str = "1"
74
+
72
75
  #: Fields eligible for CAS deduplication (applied to top-level JSON dict keys).
73
76
  _CAS_FIELDS: frozenset[str] = frozenset([
74
77
  "file_paths",
@@ -93,11 +96,18 @@ _DEFAULT_KEEP_COMMITS: int = 5
93
96
  # Matches "snapshot-<hex_commit>-<hex_flags>.json.gz"
94
97
  _SNAPSHOT_RE = re.compile(r"^snapshot-([0-9a-f]+)-[0-9a-f]+\.json\.gz$")
95
98
 
99
+ # Matches "core-<hex_commit>-<hex_analysis>.json.gz"
100
+ _CORE_RE = re.compile(r"^core-([0-9a-f]+)-[0-9a-f]+\.json\.gz$")
101
+
102
+ # Matches "view-<hex_core_hash16>-<hex_view_flags>.json.gz"
103
+ _VIEW_RE = re.compile(r"^view-([0-9a-f]{16})-[0-9a-f]+\.json\.gz$")
104
+
96
105
 
97
106
  # ---------------------------------------------------------------------------
98
107
  # Public API — location helpers
99
108
  # ---------------------------------------------------------------------------
100
109
 
110
+
101
111
  def repo_id(repo_root: Path) -> str:
102
112
  """Stable 16-char hex identifier derived from the canonical repo path."""
103
113
  return hashlib.sha256(str(repo_root.resolve()).encode()).hexdigest()[:16]
@@ -190,6 +200,138 @@ def write(
190
200
  _gc(cache_d)
191
201
 
192
202
 
203
+ # ---------------------------------------------------------------------------
204
+ # Layer 1 — Core Analysis cache
205
+ # ---------------------------------------------------------------------------
206
+
207
+ def read_core(repo_root: Path, core_key: str) -> Optional[tuple[dict[str, Any], str]]:
208
+ """Read core analysis artifacts from L1 cache.
209
+
210
+ Returns ``(core_dict, core_hash)`` on hit, or ``None`` on miss.
211
+ ``core_hash`` is the 16-char SHA-256 of the stored core JSON, used as
212
+ the L2 view-key prefix so that different views of the same core share
213
+ a common ancestry without a full re-analysis.
214
+ """
215
+ cache_d = cache_dir(repo_root)
216
+ gz_path = cache_d / f"core-{core_key}.json.gz"
217
+ if not gz_path.exists():
218
+ return None
219
+ try:
220
+ raw_bytes = gzip.decompress(gz_path.read_bytes())
221
+ envelope = json.loads(raw_bytes.decode("utf-8"))
222
+ except Exception:
223
+ _safe_unlink(gz_path)
224
+ return None
225
+
226
+ if not isinstance(envelope, dict):
227
+ _safe_unlink(gz_path)
228
+ return None
229
+ if envelope.get("csv") != CORE_SCHEMA_VERSION:
230
+ _safe_unlink(gz_path) # schema mismatch — evict
231
+ return None
232
+
233
+ core_data = envelope.get("data")
234
+ core_hash = envelope.get("hash", "")
235
+ if not isinstance(core_data, dict) or not core_hash:
236
+ _safe_unlink(gz_path)
237
+ return None
238
+
239
+ return core_data, core_hash
240
+
241
+
242
+ def write_core(repo_root: Path, core_key: str, core_data: dict[str, Any]) -> str:
243
+ """Persist core analysis dict to L1 cache.
244
+
245
+ Returns the 16-char SHA-256 hash of the core JSON (the L2 key prefix).
246
+ Writes are always best-effort; failures are silently swallowed.
247
+
248
+ File layout::
249
+
250
+ ~/.sourcecode/cache/<repo_id>/core-<core_key>.json.gz
251
+
252
+ Envelope schema::
253
+
254
+ { "csv": "1", // CORE_SCHEMA_VERSION
255
+ "key": "...", // core_key passed in
256
+ "hash": "<h16>", // SHA-256[:16] of core JSON — used as L2 prefix
257
+ "ts": "...", // ISO-8601 UTC write time
258
+ "data": {...} } // core_view(sm) dict
259
+ """
260
+ core_json = json.dumps(core_data, ensure_ascii=False)
261
+ core_hash = hashlib.sha256(core_json.encode()).hexdigest()[:16]
262
+
263
+ cache_d = cache_dir(repo_root)
264
+ dest = cache_d / f"core-{core_key}.json.gz"
265
+ try:
266
+ cache_d.mkdir(parents=True, exist_ok=True)
267
+ envelope: dict[str, Any] = {
268
+ "csv": CORE_SCHEMA_VERSION,
269
+ "key": core_key,
270
+ "hash": core_hash,
271
+ "ts": _now_iso(),
272
+ "data": core_data,
273
+ }
274
+ payload = gzip.compress(
275
+ json.dumps(envelope, ensure_ascii=False).encode("utf-8"),
276
+ compresslevel=6,
277
+ )
278
+ dest.write_bytes(payload)
279
+ except Exception:
280
+ pass
281
+
282
+ return core_hash
283
+
284
+
285
+ # ---------------------------------------------------------------------------
286
+ # Layer 2 — Derived View cache
287
+ # ---------------------------------------------------------------------------
288
+
289
+ def read_view(repo_root: Path, view_key: str) -> Optional[str]:
290
+ """Read a rendered view string from L2 cache.
291
+
292
+ Views are stored as ``view-{view_key}.json.gz`` using the same
293
+ envelope+CAS format as snapshot files. Returns the content string
294
+ (JSON or YAML) or ``None`` on miss.
295
+ """
296
+ cache_d = cache_dir(repo_root)
297
+ gz_path = cache_d / f"view-{view_key}.json.gz"
298
+ if not gz_path.exists():
299
+ return None
300
+ try:
301
+ result = _parse_envelope(gz_path.read_bytes(), cache_d)
302
+ if result is not None:
303
+ return result
304
+ except Exception:
305
+ pass
306
+ _safe_unlink(gz_path)
307
+ return None
308
+
309
+
310
+ def write_view(
311
+ repo_root: Path,
312
+ view_key: str,
313
+ content: str,
314
+ *,
315
+ fmt: str = "json",
316
+ layers: Optional[dict[str, str]] = None,
317
+ ) -> None:
318
+ """Persist a rendered view string to L2 cache as ``view-{view_key}.json.gz``.
319
+
320
+ Reuses the envelope+CAS infrastructure so large fields (file_paths,
321
+ graph, docs …) are automatically deduplicated with other snapshots/views.
322
+ Writes are always best-effort; GC is **not** triggered here — callers
323
+ that want eviction should invoke ``_gc(cache_dir(repo_root))`` explicitly.
324
+ """
325
+ cache_d = cache_dir(repo_root)
326
+ dest = cache_d / f"view-{view_key}.json.gz"
327
+ try:
328
+ cache_d.mkdir(parents=True, exist_ok=True)
329
+ payload = _build_envelope(view_key, content, fmt, layers or {}, cache_d)
330
+ dest.write_bytes(payload)
331
+ except Exception:
332
+ pass
333
+
334
+
193
335
  # ---------------------------------------------------------------------------
194
336
  # Envelope (de)serialisation
195
337
  # ---------------------------------------------------------------------------
@@ -384,31 +526,39 @@ def _cas_restore(
384
526
  # ---------------------------------------------------------------------------
385
527
 
386
528
  def _gc(cache_d: Path) -> None:
387
- """
388
- Evict old snapshots and sweep orphaned CAS blobs.
529
+ """Evict old snapshots/cores/views and sweep orphaned CAS blobs.
389
530
 
390
- Keeps snapshots from the last ``SOURCECODE_CACHE_KEEP_COMMITS`` distinct
391
- git commits (determined by mtime of files in each commit group).
531
+ Keeps snapshots and cores from the last ``SOURCECODE_CACHE_KEEP_COMMITS``
532
+ distinct git commits (determined by newest mtime within each commit group).
533
+ Views are then pruned: a view survives only when its core-hash prefix
534
+ matches a core file in the surviving set.
392
535
  """
393
536
  keep = int(os.environ.get("SOURCECODE_CACHE_KEEP_COMMITS", _DEFAULT_KEEP_COMMITS))
394
537
 
395
538
  try:
396
539
  all_snapshots = list(cache_d.glob("snapshot-*.json.gz"))
397
- if not all_snapshots:
540
+ all_cores = list(cache_d.glob("core-*.json.gz"))
541
+ all_views = list(cache_d.glob("view-*.json.gz"))
542
+
543
+ if not all_snapshots and not all_cores and not all_views:
398
544
  return
399
545
 
400
- # Group snapshot files by commit SHA
546
+ # Group snapshot + core files by commit SHA
401
547
  groups: dict[str, list[Path]] = {}
402
548
  for f in all_snapshots:
403
549
  m = _SNAPSHOT_RE.match(f.name)
404
550
  if m:
405
551
  groups.setdefault(m.group(1), []).append(f)
552
+ for f in all_cores:
553
+ m = _CORE_RE.match(f.name)
554
+ if m:
555
+ groups.setdefault(m.group(1), []).append(f)
406
556
 
407
557
  surviving: list[Path]
408
558
 
409
559
  if keep <= 0 or len(groups) <= keep:
410
- # No eviction needed — but still sweep CAS
411
- surviving = all_snapshots
560
+ # No eviction needed — but still sweep views + CAS
561
+ surviving = all_snapshots + all_cores
412
562
  else:
413
563
  def _newest_mtime(commit: str) -> float:
414
564
  return max(p.stat().st_mtime for p in groups[commit])
@@ -422,12 +572,46 @@ def _gc(cache_d: Path) -> None:
422
572
  for f in groups[commit]:
423
573
  _safe_unlink(f)
424
574
 
425
- _gc_cas(cache_d, surviving)
575
+ # Prune view files whose core hash is no longer in the surviving set
576
+ _gc_views(cache_d, surviving, all_views)
577
+
578
+ # Sweep orphaned CAS blobs (surviving snapshots + view files may ref them)
579
+ surviving_with_views = surviving + [v for v in all_views if v.exists()]
580
+ _gc_cas(cache_d, surviving_with_views)
426
581
 
427
582
  except Exception:
428
583
  pass # GC failure is non-fatal
429
584
 
430
585
 
586
+ def _gc_views(cache_d: Path, surviving: list[Path], all_views: list[Path]) -> None:
587
+ """Delete view files not traceable to a surviving core.
588
+
589
+ Collects the ``hash`` field from every surviving core envelope, then
590
+ deletes view files whose filename core-hash prefix is absent from that
591
+ set. View files with unrecognisable names are left untouched.
592
+ """
593
+ if not all_views:
594
+ return
595
+
596
+ # Collect live core hashes from surviving core-*.json.gz files
597
+ live_hashes: set[str] = set()
598
+ for path in surviving:
599
+ if not path.name.startswith("core-"):
600
+ continue
601
+ try:
602
+ env = json.loads(gzip.decompress(path.read_bytes()).decode("utf-8"))
603
+ h = env.get("hash", "")
604
+ if h:
605
+ live_hashes.add(h)
606
+ except Exception:
607
+ pass # unreadable core — conservatively keep its views unknown
608
+
609
+ for vp in all_views:
610
+ m = _VIEW_RE.match(vp.name)
611
+ if m and m.group(1) not in live_hashes:
612
+ _safe_unlink(vp)
613
+
614
+
431
615
  def _gc_cas(cache_d: Path, surviving_snapshots: list[Path]) -> None:
432
616
  """
433
617
  Delete CAS blobs not referenced by any snapshot in *surviving_snapshots*.
@@ -876,16 +876,30 @@ def main(
876
876
  architecture = True # agents need full architectural signal (M4)
877
877
  graph_modules = True # IC-003: import graph needed for architecture confidence
878
878
 
879
- # ── GAP-9: Cache check — serve from global cache when git SHA unchanged ──
880
- # Cache is stored in ~/.sourcecode/cache/<repo_id>/ (outside the repo).
881
- # Snapshots are gzip-compressed (.json.gz) — ~85 % smaller than plain JSON.
882
- # Eviction keeps the last SOURCECODE_CACHE_KEEP_COMMITS commits (default 5).
879
+ # ── Two-layer cache ────────────────────────────────────────────────────────
880
+ # L1 (core): (repo, commit, analysis_flags) pre-computed view data dict
881
+ # key = core-<git_sha>-<analysis_hash>.json.gz
882
+ # L2 (view): (core_hash, view_flags) → final rendered string
883
+ # key = view-<core_hash16>-<view_hash>.json.gz
884
+ #
885
+ # Lookup order: L2 exact hit → L1 hit + view rebuild → full analysis
886
+ # Write order: full analysis → write L1 core → write L2 view
887
+ #
888
+ # Flags split:
889
+ # core (analysis) — affect WHAT is analysed; same core for any view
890
+ # view — affect HOW it's presented; same view for any format variant
883
891
  import hashlib as _hashlib
884
892
  import subprocess as _sub
885
893
  from sourcecode import cache as _cache_mod
894
+
886
895
  _cache_hit_content: Optional[str] = None
887
896
  _git_sha = ""
888
- _cache_key = ""
897
+ _core_key = ""
898
+ _view_key = ""
899
+ _core_hash = ""
900
+ _core_flags_str = ""
901
+ _view_flags_str = ""
902
+
889
903
  if not no_cache:
890
904
  try:
891
905
  _sha_r = _sub.run(
@@ -896,37 +910,112 @@ def main(
896
910
  # Only cache when target IS the git repo root (not a subdir of one),
897
911
  # to avoid polluting sub-project directories used in tests.
898
912
  if _git_sha and (target / ".git").exists():
899
- # Include every output-affecting flag so different flag combos never collide
900
- # Include version so cache is invalidated on sourcecode upgrades
901
913
  from sourcecode import __version__ as _sc_version
902
- # FIX-P0-1: cache key must include ALL analysis-affecting flags.
903
- # Previously missing: exclude, depth, rank_by, symbol, entrypoints_only,
904
- # no_redact, graph_detail, docs_depth, max_nodes, graph_edges,
905
- # max_importers, emit_graph.
906
- # Use effective_depth (not raw depth) so Java auto-adjustment is captured.
907
914
  _excl_key = (
908
915
  ",".join(sorted(e.strip() for e in exclude.split(",") if e.strip()))
909
916
  if exclude else ""
910
917
  )
911
- _flags_str = (
918
+
919
+ # ── Core (analysis) flags: affect which analyzers run + scan config ──
920
+ # Use effective_depth (not raw depth) so Java auto-adjustment is captured.
921
+ _core_flags_str = (
912
922
  f"v={_sc_version},"
913
- f"c={compact},ag={agent},fmt={format},full={full},"
914
- f"co={changed_only},dep={dependencies},gm={graph_modules},"
923
+ f"dep={dependencies},gm={graph_modules},"
915
924
  f"docs={docs},fm={full_metrics},sem={semantics},"
916
925
  f"arch={architecture},gc={git_context},em={env_map},"
917
- f"cn={code_notes},tree={tree},mode={mode},"
918
- f"ex={_excl_key},depth={effective_depth},"
926
+ f"cn={code_notes},mode={mode},"
927
+ f"ex={_excl_key},depth={effective_depth}"
928
+ )
929
+ _core_h = _hashlib.md5(_core_flags_str.encode()).hexdigest()[:8]
930
+ _core_key = f"{_git_sha}-{_core_h}"
931
+
932
+ # ── View flags: output presentation only (no re-analysis needed) ──
933
+ _view_flags_str = (
934
+ f"c={compact},ag={agent},fmt={format},full={full},"
935
+ f"co={changed_only},tree={tree},nt={no_tree},"
919
936
  f"rb={rank_by},sym={symbol},ep={entrypoints_only},"
920
937
  f"nr={no_redact},gd={graph_detail},dd={docs_depth},"
921
938
  f"mn={max_nodes},ge={graph_edges},mi={max_importers},"
922
939
  f"eg={emit_graph}"
923
940
  )
924
- _flags_h = _hashlib.md5(_flags_str.encode()).hexdigest()[:8]
925
- _cache_key = f"{_git_sha}-{_flags_h}"
926
- _cache_hit_content = _cache_mod.read(target, _cache_key)
941
+ _view_h = _hashlib.md5(_view_flags_str.encode()).hexdigest()[:8]
942
+
943
+ # ── Lookup ──────────────────────────────────────────────────────
944
+ # Step 1: try L1 to obtain the core_hash needed for L2 key
945
+ _l1_result = _cache_mod.read_core(target, _core_key)
946
+ if _l1_result is not None:
947
+ _core_dict_l1, _core_hash = _l1_result
948
+ _view_key = f"{_core_hash}-{_view_h}"
949
+
950
+ # Step 2: try L2 (exact view match)
951
+ _cache_hit_content = _cache_mod.read_view(target, _view_key)
952
+
953
+ # Step 3: L1 hit but L2 miss → rebuild view from core dict
954
+ if _cache_hit_content is None:
955
+ try:
956
+ from sourcecode.serializer import build_view_from_core as _bvfc
957
+ _rebuilt = _bvfc(
958
+ _core_dict_l1,
959
+ compact=compact,
960
+ agent=agent,
961
+ full=full,
962
+ no_tree=no_tree,
963
+ tree=tree,
964
+ )
965
+ if _rebuilt is not None:
966
+ # Apply redaction
967
+ if not no_redact:
968
+ from sourcecode.redactor import redact_dict as _red_l1
969
+ _rebuilt = _red_l1(_rebuilt)
970
+ # Apply output budget
971
+ if agent:
972
+ from sourcecode.output_budget import (
973
+ trim_to_budget as _trim_l1,
974
+ BUDGET_AGENT,
975
+ )
976
+ _rebuilt = _trim_l1(_rebuilt, BUDGET_AGENT, label="agent")
977
+ elif compact:
978
+ from sourcecode.output_budget import (
979
+ trim_to_budget as _trim_l1c,
980
+ BUDGET_COMPACT,
981
+ )
982
+ _rebuilt = _trim_l1c(_rebuilt, BUDGET_COMPACT, label="compact")
983
+ # Serialize
984
+ if format == "yaml":
985
+ from io import StringIO as _SIO_L1
986
+ from ruamel.yaml import YAML as _YAML_L1
987
+ _yl1 = _YAML_L1()
988
+ _yl1.default_flow_style = False
989
+ _yl1.representer.add_representer(
990
+ type(None),
991
+ lambda d, v: d.represent_scalar(
992
+ "tag:yaml.org,2002:null", "null"
993
+ ),
994
+ )
995
+ _sl1 = _SIO_L1()
996
+ _yl1.dump(_rebuilt, _sl1)
997
+ _cache_hit_content = _sl1.getvalue()
998
+ else:
999
+ import json as _json_l1
1000
+ _cache_hit_content = _json_l1.dumps(
1001
+ _rebuilt, indent=2, ensure_ascii=False
1002
+ )
1003
+ # Cache rebuilt view in L2
1004
+ if _cache_hit_content:
1005
+ _cache_mod.write_view(
1006
+ target,
1007
+ _view_key,
1008
+ _cache_hit_content,
1009
+ fmt=format,
1010
+ )
1011
+ except Exception:
1012
+ _cache_hit_content = None # rebuild failed → full analysis
1013
+
927
1014
  except Exception:
928
1015
  _git_sha = ""
929
- _cache_key = ""
1016
+ _core_key = ""
1017
+ _view_key = ""
1018
+ _core_hash = ""
930
1019
 
931
1020
  if _cache_hit_content is not None:
932
1021
  from sourcecode.serializer import write_output
@@ -1687,8 +1776,21 @@ def main(
1687
1776
  if changed_only and _allowed_changed_files:
1688
1777
  # GAP-5: preserve full entry_points for architecture context even in
1689
1778
  # --changed-only mode. Only filter file_paths and code_notes.
1779
+ # ALWAYS-INCLUDE: security-const files must stay in file_paths even when
1780
+ # not in the git diff — they resolve Java constant references used in
1781
+ # @M3FiltroSeguridad annotations (read-only anchors, not diff output).
1782
+ def _is_always_include_ref(p: str) -> bool:
1783
+ name = p.rsplit("/", 1)[-1].rsplit("\\", 1)[-1]
1784
+ if name.endswith("Const.java") or name.endswith("Constants.java"):
1785
+ return True
1786
+ parts = p.replace("\\", "/").lower().split("/")
1787
+ return any(seg in ("security", "seguridad", "constantes") for seg in parts)
1788
+
1690
1789
  sm = _replace(sm,
1691
- file_paths=[p for p in sm.file_paths if p in _allowed_changed_files],
1790
+ file_paths=[
1791
+ p for p in sm.file_paths
1792
+ if p in _allowed_changed_files or _is_always_include_ref(p)
1793
+ ],
1692
1794
  code_notes=[n for n in sm.code_notes if n.path in _allowed_changed_files],
1693
1795
  )
1694
1796
  data = compact_view(sm, no_tree=no_tree, full=full)
@@ -1760,18 +1862,37 @@ def main(
1760
1862
  _progress.finish()
1761
1863
  write_output(content, output=output)
1762
1864
 
1763
- # GAP-9: Persist to cache for future identical runs (git SHA unchanged)
1764
- # Writes versioned envelope to ~/.sourcecode/cache/<repo_id>/<key>.json.gz.
1765
- # Large JSON fields are extracted into shared CAS blobs (deduplication).
1766
- # GC runs inline after each write (keep last N commits + CAS sweep).
1767
- if not no_cache and _cache_key and not _pipeline_error:
1768
- _cache_mod.write(
1769
- target,
1770
- _cache_key,
1771
- content,
1772
- fmt=format,
1773
- layers=_compute_analyzer_fingerprints(),
1774
- )
1865
+ # Persist to two-layer cache (git SHA unchanged re-use on next run).
1866
+ #
1867
+ # L1 (core): stores pre-computed compact+agent+standard views at max
1868
+ # fidelity so any subsequent view can be derived without re-analysis.
1869
+ # L2 (view): stores the exact rendered string for this flag combination.
1870
+ #
1871
+ # GC runs after L2 write to evict old commits and orphaned blobs/views.
1872
+ if not no_cache and _core_key and not _pipeline_error:
1873
+ try:
1874
+ from sourcecode.serializer import core_view as _core_view_fn
1875
+ _core_dict_write = _core_view_fn(sm)
1876
+ _written_core_hash = _cache_mod.write_core(target, _core_key, _core_dict_write)
1877
+
1878
+ # Compute view key using the just-written core hash
1879
+ if _written_core_hash:
1880
+ if not _view_key:
1881
+ # _view_key not set (L1 was also a miss); compute it now
1882
+ _wvh = _hashlib.md5(_view_flags_str.encode()).hexdigest()[:8]
1883
+ _view_key = f"{_written_core_hash}-{_wvh}"
1884
+ _cache_mod.write_view(
1885
+ target,
1886
+ _view_key,
1887
+ content,
1888
+ fmt=format,
1889
+ layers=_compute_analyzer_fingerprints(),
1890
+ )
1891
+ # Trigger GC (evict old commits + orphaned views + CAS blobs)
1892
+ from sourcecode.cache import cache_dir as _cdir, _gc as _run_gc
1893
+ _run_gc(_cdir(target))
1894
+ except Exception:
1895
+ pass # non-fatal: cache write failure
1775
1896
 
1776
1897
  if _pipeline_error:
1777
1898
  raise typer.Exit(code=2)