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.
- sourcecode-1.31.18/.continue-here.md +134 -0
- {sourcecode-1.31.17 → sourcecode-1.31.18}/PKG-INFO +3 -3
- {sourcecode-1.31.17 → sourcecode-1.31.18}/README.md +2 -2
- {sourcecode-1.31.17 → sourcecode-1.31.18}/pyproject.toml +1 -1
- {sourcecode-1.31.17 → sourcecode-1.31.18}/src/sourcecode/__init__.py +1 -1
- sourcecode-1.31.18/src/sourcecode/cache.py +470 -0
- {sourcecode-1.31.17 → sourcecode-1.31.18}/src/sourcecode/cli.py +17 -13
- sourcecode-1.31.18/tests/test_cache.py +500 -0
- sourcecode-1.31.17/.continue-here.md +0 -145
- sourcecode-1.31.17/.sourcecode-cache/snapshot-88dc388-07db8d0b.json +0 -122
- sourcecode-1.31.17/.sourcecode-cache/snapshot-88dc388-178c65fa.json +0 -4012
- sourcecode-1.31.17/.sourcecode-cache/snapshot-88dc388-379aba51.json +0 -265
- sourcecode-1.31.17/.sourcecode-cache/snapshot-88dc388-530bd9cf.json +0 -406
- sourcecode-1.31.17/.sourcecode-cache/snapshot-88dc388-56888a2a.json +0 -244
- sourcecode-1.31.17/.sourcecode-cache/snapshot-88dc388-5b602060.json +0 -265
- sourcecode-1.31.17/.sourcecode-cache/snapshot-88dc388-5ea1c8f1.json +0 -570
- sourcecode-1.31.17/.sourcecode-cache/snapshot-88dc388-6934996c.json +0 -129
- sourcecode-1.31.17/.sourcecode-cache/snapshot-88dc388-7b6dd6cc.json +0 -351
- sourcecode-1.31.17/.sourcecode-cache/snapshot-88dc388-8cb41bc4.json +0 -390
- sourcecode-1.31.17/.sourcecode-cache/snapshot-88dc388-c9ab42a3.json +0 -13523
- sourcecode-1.31.17/.sourcecode-cache/snapshot-bc358fa-06d2f793.json +0 -129
- sourcecode-1.31.17/.sourcecode-cache/snapshot-bc358fa-0bed428d.json +0 -406
- sourcecode-1.31.17/.sourcecode-cache/snapshot-bc358fa-1457dd37.json +0 -129
- sourcecode-1.31.17/.sourcecode-cache/snapshot-bc358fa-1da953a8.json +0 -122
- sourcecode-1.31.17/.sourcecode-cache/snapshot-bc358fa-218f2312.json +0 -570
- sourcecode-1.31.17/.sourcecode-cache/snapshot-bc358fa-2ab2bdd3.json +0 -390
- sourcecode-1.31.17/.sourcecode-cache/snapshot-bc358fa-3077fe4e.json +0 -4012
- sourcecode-1.31.17/.sourcecode-cache/snapshot-bc358fa-30daccea.json +0 -266
- sourcecode-1.31.17/.sourcecode-cache/snapshot-bc358fa-542cb39a.json +0 -244
- sourcecode-1.31.17/.sourcecode-cache/snapshot-bc358fa-6d9137f2.json +0 -351
- sourcecode-1.31.17/.sourcecode-cache/snapshot-bc358fa-72866fd0.json +0 -570
- sourcecode-1.31.17/.sourcecode-cache/snapshot-bc358fa-8afce878.json +0 -122
- sourcecode-1.31.17/.sourcecode-cache/snapshot-bc358fa-9d0d48c4.json +0 -244
- sourcecode-1.31.17/.sourcecode-cache/snapshot-bc358fa-9d1f2878.json +0 -406
- sourcecode-1.31.17/.sourcecode-cache/snapshot-bc358fa-a071a9df.json +0 -266
- sourcecode-1.31.17/.sourcecode-cache/snapshot-bc358fa-a1077d85.json +0 -390
- sourcecode-1.31.17/.sourcecode-cache/snapshot-bc358fa-a72aa41f.json +0 -351
- sourcecode-1.31.17/.sourcecode-cache/snapshot-bc358fa-b9ee8ffa.json +0 -4012
- sourcecode-1.31.17/.sourcecode-cache/snapshot-bc358fa-c5fc7db0.json +0 -266
- sourcecode-1.31.17/.sourcecode-cache/snapshot-bc358fa-cf823b7a.json +0 -266
- sourcecode-1.31.17/.sourcecode-cache/snapshot-bc358fa-d2d141a4.json +0 -13534
- sourcecode-1.31.17/.sourcecode-cache/snapshot-bc358fa-fc0985b2.json +0 -13534
- {sourcecode-1.31.17 → sourcecode-1.31.18}/.agents/skills/source-command-gsd-join-discord/SKILL.md +0 -0
- {sourcecode-1.31.17 → sourcecode-1.31.18}/.agents/skills/source-command-gsd-review-backlog/SKILL.md +0 -0
- {sourcecode-1.31.17 → sourcecode-1.31.18}/.agents/skills/source-command-gsd-workstreams/SKILL.md +0 -0
- {sourcecode-1.31.17 → sourcecode-1.31.18}/.github/workflows/build-windows.yml +0 -0
- {sourcecode-1.31.17 → sourcecode-1.31.18}/.gitignore +0 -0
- {sourcecode-1.31.17 → sourcecode-1.31.18}/.ruff.toml +0 -0
- {sourcecode-1.31.17 → sourcecode-1.31.18}/.sourcecode-cache/snapshot-3b5997a-fa5c742c.json +0 -0
- {sourcecode-1.31.17 → sourcecode-1.31.18}/AUDIT_REAL_REPOS.md +0 -0
- {sourcecode-1.31.17 → sourcecode-1.31.18}/CHANGELOG.md +0 -0
- {sourcecode-1.31.17 → sourcecode-1.31.18}/CONTRIBUTING.md +0 -0
- {sourcecode-1.31.17 → sourcecode-1.31.18}/LICENSE +0 -0
- {sourcecode-1.31.17 → sourcecode-1.31.18}/SECURITY.md +0 -0
- {sourcecode-1.31.17 → sourcecode-1.31.18}/docs/PRODUCT_TIERS.md +0 -0
- {sourcecode-1.31.17 → sourcecode-1.31.18}/docs/privacy.md +0 -0
- {sourcecode-1.31.17 → sourcecode-1.31.18}/docs/schema.md +0 -0
- {sourcecode-1.31.17 → sourcecode-1.31.18}/raw +0 -0
- {sourcecode-1.31.17 → sourcecode-1.31.18}/run_cli.py +0 -0
- {sourcecode-1.31.17 → sourcecode-1.31.18}/src/sourcecode/adaptive_scanner.py +0 -0
- {sourcecode-1.31.17 → sourcecode-1.31.18}/src/sourcecode/architecture_analyzer.py +0 -0
- {sourcecode-1.31.17 → sourcecode-1.31.18}/src/sourcecode/architecture_summary.py +0 -0
- {sourcecode-1.31.17 → sourcecode-1.31.18}/src/sourcecode/ast_extractor.py +0 -0
- {sourcecode-1.31.17 → sourcecode-1.31.18}/src/sourcecode/canonical_ir.py +0 -0
- {sourcecode-1.31.17 → sourcecode-1.31.18}/src/sourcecode/classifier.py +0 -0
- {sourcecode-1.31.17 → sourcecode-1.31.18}/src/sourcecode/code_notes_analyzer.py +0 -0
- {sourcecode-1.31.17 → sourcecode-1.31.18}/src/sourcecode/confidence_analyzer.py +0 -0
- {sourcecode-1.31.17 → sourcecode-1.31.18}/src/sourcecode/context_scorer.py +0 -0
- {sourcecode-1.31.17 → sourcecode-1.31.18}/src/sourcecode/context_summarizer.py +0 -0
- {sourcecode-1.31.17 → sourcecode-1.31.18}/src/sourcecode/contract_model.py +0 -0
- {sourcecode-1.31.17 → sourcecode-1.31.18}/src/sourcecode/contract_pipeline.py +0 -0
- {sourcecode-1.31.17 → sourcecode-1.31.18}/src/sourcecode/coverage_parser.py +0 -0
- {sourcecode-1.31.17 → sourcecode-1.31.18}/src/sourcecode/dependency_analyzer.py +0 -0
- {sourcecode-1.31.17 → sourcecode-1.31.18}/src/sourcecode/detectors/__init__.py +0 -0
- {sourcecode-1.31.17 → sourcecode-1.31.18}/src/sourcecode/detectors/base.py +0 -0
- {sourcecode-1.31.17 → sourcecode-1.31.18}/src/sourcecode/detectors/csproj_parser.py +0 -0
- {sourcecode-1.31.17 → sourcecode-1.31.18}/src/sourcecode/detectors/dart.py +0 -0
- {sourcecode-1.31.17 → sourcecode-1.31.18}/src/sourcecode/detectors/dotnet.py +0 -0
- {sourcecode-1.31.17 → sourcecode-1.31.18}/src/sourcecode/detectors/elixir.py +0 -0
- {sourcecode-1.31.17 → sourcecode-1.31.18}/src/sourcecode/detectors/go.py +0 -0
- {sourcecode-1.31.17 → sourcecode-1.31.18}/src/sourcecode/detectors/heuristic.py +0 -0
- {sourcecode-1.31.17 → sourcecode-1.31.18}/src/sourcecode/detectors/hybrid.py +0 -0
- {sourcecode-1.31.17 → sourcecode-1.31.18}/src/sourcecode/detectors/java.py +0 -0
- {sourcecode-1.31.17 → sourcecode-1.31.18}/src/sourcecode/detectors/jvm_ext.py +0 -0
- {sourcecode-1.31.17 → sourcecode-1.31.18}/src/sourcecode/detectors/nodejs.py +0 -0
- {sourcecode-1.31.17 → sourcecode-1.31.18}/src/sourcecode/detectors/parsers.py +0 -0
- {sourcecode-1.31.17 → sourcecode-1.31.18}/src/sourcecode/detectors/php.py +0 -0
- {sourcecode-1.31.17 → sourcecode-1.31.18}/src/sourcecode/detectors/project.py +0 -0
- {sourcecode-1.31.17 → sourcecode-1.31.18}/src/sourcecode/detectors/python.py +0 -0
- {sourcecode-1.31.17 → sourcecode-1.31.18}/src/sourcecode/detectors/ruby.py +0 -0
- {sourcecode-1.31.17 → sourcecode-1.31.18}/src/sourcecode/detectors/rust.py +0 -0
- {sourcecode-1.31.17 → sourcecode-1.31.18}/src/sourcecode/detectors/systems.py +0 -0
- {sourcecode-1.31.17 → sourcecode-1.31.18}/src/sourcecode/detectors/terraform.py +0 -0
- {sourcecode-1.31.17 → sourcecode-1.31.18}/src/sourcecode/detectors/tooling.py +0 -0
- {sourcecode-1.31.17 → sourcecode-1.31.18}/src/sourcecode/doc_analyzer.py +0 -0
- {sourcecode-1.31.17 → sourcecode-1.31.18}/src/sourcecode/entrypoint_classifier.py +0 -0
- {sourcecode-1.31.17 → sourcecode-1.31.18}/src/sourcecode/env_analyzer.py +0 -0
- {sourcecode-1.31.17 → sourcecode-1.31.18}/src/sourcecode/file_classifier.py +0 -0
- {sourcecode-1.31.17 → sourcecode-1.31.18}/src/sourcecode/flow_analyzer.py +0 -0
- {sourcecode-1.31.17 → sourcecode-1.31.18}/src/sourcecode/git_analyzer.py +0 -0
- {sourcecode-1.31.17 → sourcecode-1.31.18}/src/sourcecode/graph_analyzer.py +0 -0
- {sourcecode-1.31.17 → sourcecode-1.31.18}/src/sourcecode/mcp/__init__.py +0 -0
- {sourcecode-1.31.17 → sourcecode-1.31.18}/src/sourcecode/mcp/onboarding/__init__.py +0 -0
- {sourcecode-1.31.17 → sourcecode-1.31.18}/src/sourcecode/mcp/onboarding/applier.py +0 -0
- {sourcecode-1.31.17 → sourcecode-1.31.18}/src/sourcecode/mcp/onboarding/backup.py +0 -0
- {sourcecode-1.31.17 → sourcecode-1.31.18}/src/sourcecode/mcp/onboarding/detector.py +0 -0
- {sourcecode-1.31.17 → sourcecode-1.31.18}/src/sourcecode/mcp/onboarding/planner.py +0 -0
- {sourcecode-1.31.17 → sourcecode-1.31.18}/src/sourcecode/mcp/runner.py +0 -0
- {sourcecode-1.31.17 → sourcecode-1.31.18}/src/sourcecode/mcp/server.py +0 -0
- {sourcecode-1.31.17 → sourcecode-1.31.18}/src/sourcecode/metrics_analyzer.py +0 -0
- {sourcecode-1.31.17 → sourcecode-1.31.18}/src/sourcecode/output_budget.py +0 -0
- {sourcecode-1.31.17 → sourcecode-1.31.18}/src/sourcecode/path_filters.py +0 -0
- {sourcecode-1.31.17 → sourcecode-1.31.18}/src/sourcecode/pr_comment_renderer.py +0 -0
- {sourcecode-1.31.17 → sourcecode-1.31.18}/src/sourcecode/prepare_context.py +0 -0
- {sourcecode-1.31.17 → sourcecode-1.31.18}/src/sourcecode/progress.py +0 -0
- {sourcecode-1.31.17 → sourcecode-1.31.18}/src/sourcecode/ranking_engine.py +0 -0
- {sourcecode-1.31.17 → sourcecode-1.31.18}/src/sourcecode/redactor.py +0 -0
- {sourcecode-1.31.17 → sourcecode-1.31.18}/src/sourcecode/relevance_scorer.py +0 -0
- {sourcecode-1.31.17 → sourcecode-1.31.18}/src/sourcecode/repo_classifier.py +0 -0
- {sourcecode-1.31.17 → sourcecode-1.31.18}/src/sourcecode/repository_ir.py +0 -0
- {sourcecode-1.31.17 → sourcecode-1.31.18}/src/sourcecode/runtime_classifier.py +0 -0
- {sourcecode-1.31.17 → sourcecode-1.31.18}/src/sourcecode/scanner.py +0 -0
- {sourcecode-1.31.17 → sourcecode-1.31.18}/src/sourcecode/schema.py +0 -0
- {sourcecode-1.31.17 → sourcecode-1.31.18}/src/sourcecode/semantic_analyzer.py +0 -0
- {sourcecode-1.31.17 → sourcecode-1.31.18}/src/sourcecode/serializer.py +0 -0
- {sourcecode-1.31.17 → sourcecode-1.31.18}/src/sourcecode/summarizer.py +0 -0
- {sourcecode-1.31.17 → sourcecode-1.31.18}/src/sourcecode/telemetry/__init__.py +0 -0
- {sourcecode-1.31.17 → sourcecode-1.31.18}/src/sourcecode/telemetry/config.py +0 -0
- {sourcecode-1.31.17 → sourcecode-1.31.18}/src/sourcecode/telemetry/consent.py +0 -0
- {sourcecode-1.31.17 → sourcecode-1.31.18}/src/sourcecode/telemetry/events.py +0 -0
- {sourcecode-1.31.17 → sourcecode-1.31.18}/src/sourcecode/telemetry/filters.py +0 -0
- {sourcecode-1.31.17 → sourcecode-1.31.18}/src/sourcecode/telemetry/transport.py +0 -0
- {sourcecode-1.31.17 → sourcecode-1.31.18}/src/sourcecode/tree_utils.py +0 -0
- {sourcecode-1.31.17 → sourcecode-1.31.18}/src/sourcecode/workspace.py +0 -0
- {sourcecode-1.31.17 → sourcecode-1.31.18}/tests/__init__.py +0 -0
- {sourcecode-1.31.17 → sourcecode-1.31.18}/tests/conftest.py +0 -0
- {sourcecode-1.31.17 → sourcecode-1.31.18}/tests/fixtures/coverage.xml +0 -0
- {sourcecode-1.31.17 → sourcecode-1.31.18}/tests/fixtures/fastapi_app/pyproject.toml +0 -0
- {sourcecode-1.31.17 → sourcecode-1.31.18}/tests/fixtures/fastapi_app/src/main.py +0 -0
- {sourcecode-1.31.17 → sourcecode-1.31.18}/tests/fixtures/go_service/cmd/api/main.go +0 -0
- {sourcecode-1.31.17 → sourcecode-1.31.18}/tests/fixtures/go_service/go.mod +0 -0
- {sourcecode-1.31.17 → sourcecode-1.31.18}/tests/fixtures/jacoco.xml +0 -0
- {sourcecode-1.31.17 → sourcecode-1.31.18}/tests/fixtures/latin1_sample.java +0 -0
- {sourcecode-1.31.17 → sourcecode-1.31.18}/tests/fixtures/latin1_sample_iso.java +0 -0
- {sourcecode-1.31.17 → sourcecode-1.31.18}/tests/fixtures/lcov.info +0 -0
- {sourcecode-1.31.17 → sourcecode-1.31.18}/tests/fixtures/nextjs_app/app/page.tsx +0 -0
- {sourcecode-1.31.17 → sourcecode-1.31.18}/tests/fixtures/nextjs_app/package.json +0 -0
- {sourcecode-1.31.17 → sourcecode-1.31.18}/tests/fixtures/nextjs_app/pnpm-lock.yaml +0 -0
- {sourcecode-1.31.17 → sourcecode-1.31.18}/tests/fixtures/pnpm_monorepo/apps/web/app/page.tsx +0 -0
- {sourcecode-1.31.17 → sourcecode-1.31.18}/tests/fixtures/pnpm_monorepo/apps/web/package.json +0 -0
- {sourcecode-1.31.17 → sourcecode-1.31.18}/tests/fixtures/pnpm_monorepo/packages/api/main.py +0 -0
- {sourcecode-1.31.17 → sourcecode-1.31.18}/tests/fixtures/pnpm_monorepo/packages/api/pyproject.toml +0 -0
- {sourcecode-1.31.17 → sourcecode-1.31.18}/tests/fixtures/pnpm_monorepo/pnpm-workspace.yaml +0 -0
- {sourcecode-1.31.17 → sourcecode-1.31.18}/tests/fixtures/spring_boot_minimal/pom.xml +0 -0
- {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
- {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
- {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
- {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
- {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
- {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
- {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
- {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
- {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
- {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
- {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
- {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
- {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
- {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
- {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
- {sourcecode-1.31.17 → sourcecode-1.31.18}/tests/fixtures/spring_boot_minimal/src/main/java/com/example/demo/DemoApplication.java +0 -0
- {sourcecode-1.31.17 → sourcecode-1.31.18}/tests/fixtures/spring_boot_minimal/src/main/java/com/example/demo/config/FilterConfig.java +0 -0
- {sourcecode-1.31.17 → sourcecode-1.31.18}/tests/fixtures/spring_boot_minimal/src/main/java/com/example/demo/domain/Health.java +0 -0
- {sourcecode-1.31.17 → sourcecode-1.31.18}/tests/fixtures/spring_boot_minimal/src/main/java/com/example/demo/mapper/HealthMapper.java +0 -0
- {sourcecode-1.31.17 → sourcecode-1.31.18}/tests/fixtures/spring_boot_minimal/src/main/java/com/example/demo/repository/HealthRepository.java +0 -0
- {sourcecode-1.31.17 → sourcecode-1.31.18}/tests/fixtures/spring_boot_minimal/src/main/java/com/example/demo/service/HealthService.java +0 -0
- {sourcecode-1.31.17 → sourcecode-1.31.18}/tests/fixtures/spring_boot_minimal/src/main/java/com/example/demo/web/HealthRestController.java +0 -0
- {sourcecode-1.31.17 → sourcecode-1.31.18}/tests/fixtures/spring_boot_minimal/src/main/java/com/example/demo/web/NominaRestController.java +0 -0
- {sourcecode-1.31.17 → sourcecode-1.31.18}/tests/fixtures/spring_boot_minimal/src/main/resources/application-dev.yml +0 -0
- {sourcecode-1.31.17 → sourcecode-1.31.18}/tests/fixtures/spring_boot_minimal/src/main/resources/application.yml +0 -0
- {sourcecode-1.31.17 → sourcecode-1.31.18}/tests/fixtures/spring_boot_minimal/src/main/resources/mapper/HealthMapper.xml +0 -0
- {sourcecode-1.31.17 → sourcecode-1.31.18}/tests/test_architecture_analyzer.py +0 -0
- {sourcecode-1.31.17 → sourcecode-1.31.18}/tests/test_architecture_summary.py +0 -0
- {sourcecode-1.31.17 → sourcecode-1.31.18}/tests/test_ast_extractor.py +0 -0
- {sourcecode-1.31.17 → sourcecode-1.31.18}/tests/test_audit_fixes.py +0 -0
- {sourcecode-1.31.17 → sourcecode-1.31.18}/tests/test_audit_sas_v2.py +0 -0
- {sourcecode-1.31.17 → sourcecode-1.31.18}/tests/test_block1_reliability.py +0 -0
- {sourcecode-1.31.17 → sourcecode-1.31.18}/tests/test_block2_coverage.py +0 -0
- {sourcecode-1.31.17 → sourcecode-1.31.18}/tests/test_block5_quality.py +0 -0
- {sourcecode-1.31.17 → sourcecode-1.31.18}/tests/test_broadleaf_fixes.py +0 -0
- {sourcecode-1.31.17 → sourcecode-1.31.18}/tests/test_bug_fixes_v1302.py +0 -0
- {sourcecode-1.31.17 → sourcecode-1.31.18}/tests/test_bug_fixes_v13115.py +0 -0
- {sourcecode-1.31.17 → sourcecode-1.31.18}/tests/test_bug_fixes_v1312.py +0 -0
- {sourcecode-1.31.17 → sourcecode-1.31.18}/tests/test_bug_fixes_v1313.py +0 -0
- {sourcecode-1.31.17 → sourcecode-1.31.18}/tests/test_bug_fixes_v16.py +0 -0
- {sourcecode-1.31.17 → sourcecode-1.31.18}/tests/test_bug_fixes_v2.py +0 -0
- {sourcecode-1.31.17 → sourcecode-1.31.18}/tests/test_canonical_ir.py +0 -0
- {sourcecode-1.31.17 → sourcecode-1.31.18}/tests/test_classifier.py +0 -0
- {sourcecode-1.31.17 → sourcecode-1.31.18}/tests/test_cli.py +0 -0
- {sourcecode-1.31.17 → sourcecode-1.31.18}/tests/test_code_notes_analyzer.py +0 -0
- {sourcecode-1.31.17 → sourcecode-1.31.18}/tests/test_context_scorer.py +0 -0
- {sourcecode-1.31.17 → sourcecode-1.31.18}/tests/test_contract_pipeline.py +0 -0
- {sourcecode-1.31.17 → sourcecode-1.31.18}/tests/test_coverage_parser.py +0 -0
- {sourcecode-1.31.17 → sourcecode-1.31.18}/tests/test_cross_consistency.py +0 -0
- {sourcecode-1.31.17 → sourcecode-1.31.18}/tests/test_dependency_analyzer_node_python.py +0 -0
- {sourcecode-1.31.17 → sourcecode-1.31.18}/tests/test_dependency_analyzer_polyglot.py +0 -0
- {sourcecode-1.31.17 → sourcecode-1.31.18}/tests/test_dependency_schema.py +0 -0
- {sourcecode-1.31.17 → sourcecode-1.31.18}/tests/test_detector_dotnet.py +0 -0
- {sourcecode-1.31.17 → sourcecode-1.31.18}/tests/test_detector_go_rust_java.py +0 -0
- {sourcecode-1.31.17 → sourcecode-1.31.18}/tests/test_detector_nodejs.py +0 -0
- {sourcecode-1.31.17 → sourcecode-1.31.18}/tests/test_detector_php_ruby_dart.py +0 -0
- {sourcecode-1.31.17 → sourcecode-1.31.18}/tests/test_detector_python.py +0 -0
- {sourcecode-1.31.17 → sourcecode-1.31.18}/tests/test_detector_universal_managed.py +0 -0
- {sourcecode-1.31.17 → sourcecode-1.31.18}/tests/test_detector_universal_systems.py +0 -0
- {sourcecode-1.31.17 → sourcecode-1.31.18}/tests/test_detectors_base.py +0 -0
- {sourcecode-1.31.17 → sourcecode-1.31.18}/tests/test_doc_analyzer_jsdom.py +0 -0
- {sourcecode-1.31.17 → sourcecode-1.31.18}/tests/test_doc_analyzer_python.py +0 -0
- {sourcecode-1.31.17 → sourcecode-1.31.18}/tests/test_encoding_regression.py +0 -0
- {sourcecode-1.31.17 → sourcecode-1.31.18}/tests/test_enterprise_benchmarks.py +0 -0
- {sourcecode-1.31.17 → sourcecode-1.31.18}/tests/test_graph_analyzer_polyglot.py +0 -0
- {sourcecode-1.31.17 → sourcecode-1.31.18}/tests/test_graph_analyzer_python_node.py +0 -0
- {sourcecode-1.31.17 → sourcecode-1.31.18}/tests/test_graph_schema.py +0 -0
- {sourcecode-1.31.17 → sourcecode-1.31.18}/tests/test_hybrid_inference.py +0 -0
- {sourcecode-1.31.17 → sourcecode-1.31.18}/tests/test_integration.py +0 -0
- {sourcecode-1.31.17 → sourcecode-1.31.18}/tests/test_integration_dependencies.py +0 -0
- {sourcecode-1.31.17 → sourcecode-1.31.18}/tests/test_integration_detection.py +0 -0
- {sourcecode-1.31.17 → sourcecode-1.31.18}/tests/test_integration_docs.py +0 -0
- {sourcecode-1.31.17 → sourcecode-1.31.18}/tests/test_integration_graph_modules.py +0 -0
- {sourcecode-1.31.17 → sourcecode-1.31.18}/tests/test_integration_lqn.py +0 -0
- {sourcecode-1.31.17 → sourcecode-1.31.18}/tests/test_integration_metrics.py +0 -0
- {sourcecode-1.31.17 → sourcecode-1.31.18}/tests/test_integration_multistack.py +0 -0
- {sourcecode-1.31.17 → sourcecode-1.31.18}/tests/test_integration_semantics.py +0 -0
- {sourcecode-1.31.17 → sourcecode-1.31.18}/tests/test_integration_universal.py +0 -0
- {sourcecode-1.31.17 → sourcecode-1.31.18}/tests/test_java_spring_integration.py +0 -0
- {sourcecode-1.31.17 → sourcecode-1.31.18}/tests/test_mcp_runner.py +0 -0
- {sourcecode-1.31.17 → sourcecode-1.31.18}/tests/test_mcp_serve.py +0 -0
- {sourcecode-1.31.17 → sourcecode-1.31.18}/tests/test_mcp_tools.py +0 -0
- {sourcecode-1.31.17 → sourcecode-1.31.18}/tests/test_metrics_analyzer.py +0 -0
- {sourcecode-1.31.17 → sourcecode-1.31.18}/tests/test_output_ux.py +0 -0
- {sourcecode-1.31.17 → sourcecode-1.31.18}/tests/test_packaging.py +0 -0
- {sourcecode-1.31.17 → sourcecode-1.31.18}/tests/test_phase1_improvements.py +0 -0
- {sourcecode-1.31.17 → sourcecode-1.31.18}/tests/test_pipeline_integrity.py +0 -0
- {sourcecode-1.31.17 → sourcecode-1.31.18}/tests/test_real_projects.py +0 -0
- {sourcecode-1.31.17 → sourcecode-1.31.18}/tests/test_redactor.py +0 -0
- {sourcecode-1.31.17 → sourcecode-1.31.18}/tests/test_repository_ir.py +0 -0
- {sourcecode-1.31.17 → sourcecode-1.31.18}/tests/test_scanner.py +0 -0
- {sourcecode-1.31.17 → sourcecode-1.31.18}/tests/test_schema.py +0 -0
- {sourcecode-1.31.17 → sourcecode-1.31.18}/tests/test_schema_normalization.py +0 -0
- {sourcecode-1.31.17 → sourcecode-1.31.18}/tests/test_scoring_calibration.py +0 -0
- {sourcecode-1.31.17 → sourcecode-1.31.18}/tests/test_semantic_analyzer_node.py +0 -0
- {sourcecode-1.31.17 → sourcecode-1.31.18}/tests/test_semantic_analyzer_python.py +0 -0
- {sourcecode-1.31.17 → sourcecode-1.31.18}/tests/test_semantic_import_resolution.py +0 -0
- {sourcecode-1.31.17 → sourcecode-1.31.18}/tests/test_semantic_schema.py +0 -0
- {sourcecode-1.31.17 → sourcecode-1.31.18}/tests/test_signal_hierarchy.py +0 -0
- {sourcecode-1.31.17 → sourcecode-1.31.18}/tests/test_summarizer.py +0 -0
- {sourcecode-1.31.17 → sourcecode-1.31.18}/tests/test_surface_honesty.py +0 -0
- {sourcecode-1.31.17 → sourcecode-1.31.18}/tests/test_task_differentiation.py +0 -0
- {sourcecode-1.31.17 → sourcecode-1.31.18}/tests/test_telemetry.py +0 -0
- {sourcecode-1.31.17 → sourcecode-1.31.18}/tests/test_v131_improvements.py +0 -0
- {sourcecode-1.31.17 → sourcecode-1.31.18}/tests/test_v1_10_regressions.py +0 -0
- {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.
|
|
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
|
-

|
|
229
229
|

|
|
230
230
|
|
|
231
231
|
---
|
|
@@ -263,7 +263,7 @@ pipx install sourcecode
|
|
|
263
263
|
|
|
264
264
|
```bash
|
|
265
265
|
sourcecode version
|
|
266
|
-
# sourcecode 1.31.
|
|
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
|
-

|
|
6
6
|

|
|
7
7
|
|
|
8
8
|
---
|
|
@@ -40,7 +40,7 @@ pipx install sourcecode
|
|
|
40
40
|
|
|
41
41
|
```bash
|
|
42
42
|
sourcecode version
|
|
43
|
-
# sourcecode 1.31.
|
|
43
|
+
# sourcecode 1.31.18
|
|
44
44
|
```
|
|
45
45
|
|
|
46
46
|
---
|
|
@@ -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
|