attocode-code-intel 0.1.0__py3-none-any.whl
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.
- attocode_code_intel-0.1.0.dist-info/METADATA +221 -0
- attocode_code_intel-0.1.0.dist-info/RECORD +345 -0
- attocode_code_intel-0.1.0.dist-info/WHEEL +4 -0
- attocode_code_intel-0.1.0.dist-info/entry_points.txt +2 -0
- attocode_code_intel-0.1.0.dist-info/licenses/LICENSE +21 -0
- attocode_intel/GUIDELINES.md +432 -0
- attocode_intel/__init__.py +52 -0
- attocode_intel/_internal/__init__.py +1 -0
- attocode_intel/_internal/errors/__init__.py +126 -0
- attocode_intel/_internal/integrations/__init__.py +1 -0
- attocode_intel/_internal/integrations/context/__init__.py +1 -0
- attocode_intel/_internal/integrations/context/architecture_drift.py +354 -0
- attocode_intel/_internal/integrations/context/ast_chunker.py +202 -0
- attocode_intel/_internal/integrations/context/ast_service.py +1483 -0
- attocode_intel/_internal/integrations/context/blob_oid.py +423 -0
- attocode_intel/_internal/integrations/context/cache_manifest.py +221 -0
- attocode_intel/_internal/integrations/context/cas.py +466 -0
- attocode_intel/_internal/integrations/context/code_analyzer.py +528 -0
- attocode_intel/_internal/integrations/context/codebase_ast.py +1461 -0
- attocode_intel/_internal/integrations/context/codebase_context.py +1736 -0
- attocode_intel/_internal/integrations/context/cross_mode.py +263 -0
- attocode_intel/_internal/integrations/context/cross_references.py +616 -0
- attocode_intel/_internal/integrations/context/embedding_rotation.py +597 -0
- attocode_intel/_internal/integrations/context/embeddings.py +282 -0
- attocode_intel/_internal/integrations/context/frecency.py +427 -0
- attocode_intel/_internal/integrations/context/fuzzy.py +320 -0
- attocode_intel/_internal/integrations/context/hierarchical_explorer.py +331 -0
- attocode_intel/_internal/integrations/context/hydration.py +90 -0
- attocode_intel/_internal/integrations/context/index_repair.py +87 -0
- attocode_intel/_internal/integrations/context/index_store.py +586 -0
- attocode_intel/_internal/integrations/context/memory_store.py +602 -0
- attocode_intel/_internal/integrations/context/module_resolution.py +52 -0
- attocode_intel/_internal/integrations/context/query_constraints.py +361 -0
- attocode_intel/_internal/integrations/context/query_history.py +417 -0
- attocode_intel/_internal/integrations/context/reranker.py +351 -0
- attocode_intel/_internal/integrations/context/semantic_search.py +2931 -0
- attocode_intel/_internal/integrations/context/syntax_evidence.py +94 -0
- attocode_intel/_internal/integrations/context/systemone_ranker.py +245 -0
- attocode_intel/_internal/integrations/context/temporal_coupling.py +411 -0
- attocode_intel/_internal/integrations/context/trigram_index.py +820 -0
- attocode_intel/_internal/integrations/context/trigram_regex.py +171 -0
- attocode_intel/_internal/integrations/context/ts_parser.py +2199 -0
- attocode_intel/_internal/integrations/context/vector_store.py +795 -0
- attocode_intel/_internal/integrations/feature_flags/__init__.py +357 -0
- attocode_intel/_internal/integrations/lsp/__init__.py +1 -0
- attocode_intel/_internal/integrations/lsp/client.py +1140 -0
- attocode_intel/_internal/integrations/mcp/__init__.py +1 -0
- attocode_intel/_internal/integrations/mcp/client.py +253 -0
- attocode_intel/_internal/integrations/security/__init__.py +1 -0
- attocode_intel/_internal/integrations/security/custom_rules.py +176 -0
- attocode_intel/_internal/integrations/security/dataflow.py +522 -0
- attocode_intel/_internal/integrations/security/dependency_audit.py +303 -0
- attocode_intel/_internal/integrations/security/matcher.py +44 -0
- attocode_intel/_internal/integrations/security/patterns.py +724 -0
- attocode_intel/_internal/integrations/security/scanner.py +351 -0
- attocode_intel/_internal/integrations/utilities/__init__.py +1 -0
- attocode_intel/_internal/integrations/utilities/ignore.py +64 -0
- attocode_intel/_internal/integrations/utilities/token_estimate.py +51 -0
- attocode_intel/_internal/tricks/__init__.py +1 -0
- attocode_intel/_internal/tricks/recursive_context.py +357 -0
- attocode_intel/_internal/types/__init__.py +1 -0
- attocode_intel/_internal/types/events.py +289 -0
- attocode_intel/_internal/types/severity.py +15 -0
- attocode_intel/_shared.py +341 -0
- attocode_intel/analysis_coverage.py +56 -0
- attocode_intel/api/__init__.py +1 -0
- attocode_intel/api/access.py +32 -0
- attocode_intel/api/app.py +247 -0
- attocode_intel/api/auth/__init__.py +128 -0
- attocode_intel/api/auth/api_keys.py +61 -0
- attocode_intel/api/auth/context.py +34 -0
- attocode_intel/api/auth/jwt.py +66 -0
- attocode_intel/api/auth/oauth.py +146 -0
- attocode_intel/api/auth/passwords.py +15 -0
- attocode_intel/api/deps.py +351 -0
- attocode_intel/api/knowledge_bridge.py +13 -0
- attocode_intel/api/mcp_transport.py +51 -0
- attocode_intel/api/middleware.py +411 -0
- attocode_intel/api/models.py +698 -0
- attocode_intel/api/providers/__init__.py +1 -0
- attocode_intel/api/providers/db_provider.py +1106 -0
- attocode_intel/api/providers/local_provider.py +476 -0
- attocode_intel/api/providers/remote_provider.py +740 -0
- attocode_intel/api/repository_source.py +21 -0
- attocode_intel/api/routes/__init__.py +1 -0
- attocode_intel/api/routes/_pin_helper.py +158 -0
- attocode_intel/api/routes/activity.py +148 -0
- attocode_intel/api/routes/adr.py +102 -0
- attocode_intel/api/routes/analysis.py +515 -0
- attocode_intel/api/routes/api_keys.py +170 -0
- attocode_intel/api/routes/auth.py +428 -0
- attocode_intel/api/routes/branches.py +262 -0
- attocode_intel/api/routes/cross_repo_search.py +198 -0
- attocode_intel/api/routes/embeddings.py +386 -0
- attocode_intel/api/routes/files.py +132 -0
- attocode_intel/api/routes/files_v2.py +388 -0
- attocode_intel/api/routes/gc.py +322 -0
- attocode_intel/api/routes/git_v2.py +433 -0
- attocode_intel/api/routes/graph.py +174 -0
- attocode_intel/api/routes/graph_viz.py +160 -0
- attocode_intel/api/routes/health.py +62 -0
- attocode_intel/api/routes/history.py +175 -0
- attocode_intel/api/routes/jobs.py +84 -0
- attocode_intel/api/routes/learning.py +205 -0
- attocode_intel/api/routes/lsp.py +272 -0
- attocode_intel/api/routes/notify.py +623 -0
- attocode_intel/api/routes/operations.py +27 -0
- attocode_intel/api/routes/orgs.py +924 -0
- attocode_intel/api/routes/preferences.py +100 -0
- attocode_intel/api/routes/presence.py +42 -0
- attocode_intel/api/routes/projects.py +116 -0
- attocode_intel/api/routes/repos.py +47 -0
- attocode_intel/api/routes/search.py +180 -0
- attocode_intel/api/routes/snapshots.py +614 -0
- attocode_intel/api/routes/webhooks.py +313 -0
- attocode_intel/api/routes/websocket.py +116 -0
- attocode_intel/api/static/graph.html +239 -0
- attocode_intel/api/utils.py +96 -0
- attocode_intel/artifacts/__init__.py +45 -0
- attocode_intel/artifacts/hashing.py +230 -0
- attocode_intel/artifacts/provenance.py +190 -0
- attocode_intel/artifacts/retrieval_pin.py +159 -0
- attocode_intel/audit.py +59 -0
- attocode_intel/bug_finder.py +206 -0
- attocode_intel/bundle.py +80 -0
- attocode_intel/catalog.py +242 -0
- attocode_intel/change_review.py +75 -0
- attocode_intel/cli.py +3062 -0
- attocode_intel/community.py +63 -0
- attocode_intel/confidence/__init__.py +144 -0
- attocode_intel/confidence/jev.py +234 -0
- attocode_intel/confidence/llm.py +330 -0
- attocode_intel/confidence/redact.py +44 -0
- attocode_intel/confidence/settings.py +74 -0
- attocode_intel/config.py +256 -0
- attocode_intel/crypto.py +31 -0
- attocode_intel/db/__init__.py +7 -0
- attocode_intel/db/base.py +33 -0
- attocode_intel/db/engine.py +61 -0
- attocode_intel/db/models.py +529 -0
- attocode_intel/entrypoint.py +185 -0
- attocode_intel/focused_evidence.py +130 -0
- attocode_intel/freshness.py +154 -0
- attocode_intel/gateway.py +734 -0
- attocode_intel/git/__init__.py +7 -0
- attocode_intel/git/credentials.py +75 -0
- attocode_intel/git/manager.py +493 -0
- attocode_intel/git/models.py +96 -0
- attocode_intel/git/storage.py +98 -0
- attocode_intel/graph_query_parser.py +761 -0
- attocode_intel/helpers.py +712 -0
- attocode_intel/indexing/__init__.py +29 -0
- attocode_intel/indexing/debouncer.py +136 -0
- attocode_intel/indexing/delta_indexer.py +151 -0
- attocode_intel/indexing/full_indexer.py +323 -0
- attocode_intel/indexing/incremental.py +240 -0
- attocode_intel/indexing/parser.py +403 -0
- attocode_intel/indexing/summarizer.py +264 -0
- attocode_intel/installer.py +1896 -0
- attocode_intel/knowledge.py +269 -0
- attocode_intel/local_knowledge.py +77 -0
- attocode_intel/migrations/alembic.ini +36 -0
- attocode_intel/migrations/env.py +71 -0
- attocode_intel/migrations/script.py.mako +27 -0
- attocode_intel/migrations/versions/001_initial_schema.py +219 -0
- attocode_intel/migrations/versions/002_branch_version_and_embeddings.py +61 -0
- attocode_intel/migrations/versions/003_embedding_uniqueness_and_cascade.py +51 -0
- attocode_intel/migrations/versions/004_audit_events.py +38 -0
- attocode_intel/migrations/versions/005_user_preferences.py +30 -0
- attocode_intel/migrations/versions/006_google_oauth.py +27 -0
- attocode_intel/migrations/versions/007_pgvector_embeddings.py +28 -0
- attocode_intel/migrations/versions/008_symbol_references.py +37 -0
- attocode_intel/migrations/versions/009_webhook_secret_encryption.py +25 -0
- attocode_intel/migrations/versions/010_add_commits_table.py +40 -0
- attocode_intel/migrations/versions/011_commit_changed_files.py +25 -0
- attocode_intel/migrations/versions/012_branch_merged_at.py +25 -0
- attocode_intel/migrations/versions/013_revoked_tokens.py +32 -0
- attocode_intel/migrations/versions/014_blame_hunks.py +40 -0
- attocode_intel/migrations/versions/015_commit_file_stats.py +40 -0
- attocode_intel/migrations/versions/016_embedding_provenance_columns.py +179 -0
- attocode_intel/migrations/versions/017_provenance_table.py +102 -0
- attocode_intel/migrations/versions/018_repo_snapshots.py +178 -0
- attocode_intel/migrations/versions/019_symref_caller.py +42 -0
- attocode_intel/migrations/versions/020_shared_knowledge.py +29 -0
- attocode_intel/onboarding.py +258 -0
- attocode_intel/output.py +178 -0
- attocode_intel/precision.py +133 -0
- attocode_intel/presence.py +160 -0
- attocode_intel/probe.py +110 -0
- attocode_intel/project_dir.py +97 -0
- attocode_intel/pubsub.py +113 -0
- attocode_intel/query_ranking.py +242 -0
- attocode_intel/readiness.py +972 -0
- attocode_intel/redis.py +51 -0
- attocode_intel/reference_pages.py +60 -0
- attocode_intel/remote.py +131 -0
- attocode_intel/repo_ranker.py +246 -0
- attocode_intel/request_context.py +82 -0
- attocode_intel/rules/__init__.py +28 -0
- attocode_intel/rules/ci.py +320 -0
- attocode_intel/rules/combinators.py +293 -0
- attocode_intel/rules/enricher.py +127 -0
- attocode_intel/rules/evolution.py +452 -0
- attocode_intel/rules/executor.py +531 -0
- attocode_intel/rules/filters/__init__.py +5 -0
- attocode_intel/rules/filters/pipeline.py +151 -0
- attocode_intel/rules/formatter.py +198 -0
- attocode_intel/rules/hygiene.py +242 -0
- attocode_intel/rules/importers/__init__.py +1 -0
- attocode_intel/rules/importers/semgrep.py +382 -0
- attocode_intel/rules/loader.py +292 -0
- attocode_intel/rules/marketplace.py +375 -0
- attocode_intel/rules/metavar.py +181 -0
- attocode_intel/rules/model.py +317 -0
- attocode_intel/rules/packs/__init__.py +1 -0
- attocode_intel/rules/packs/community/_license_templates/Apache-2.0.txt +176 -0
- attocode_intel/rules/packs/community/_license_templates/MIT.txt +21 -0
- attocode_intel/rules/packs/community/bandit-python/LICENSE +176 -0
- attocode_intel/rules/packs/community/bandit-python/NOTICE +13 -0
- attocode_intel/rules/packs/community/bandit-python/PORTING.md +23 -0
- attocode_intel/rules/packs/community/bandit-python/manifest.yaml +14 -0
- attocode_intel/rules/packs/community/bandit-python/rules/B105-hardcoded-password.yaml +23 -0
- attocode_intel/rules/packs/community/bandit-python/rules/B303-weak-md5.yaml +21 -0
- attocode_intel/rules/packs/community/bandit-python/rules/B304-weak-des.yaml +20 -0
- attocode_intel/rules/packs/community/bandit-python/rules/B307-use-of-eval.yaml +22 -0
- attocode_intel/rules/packs/community/bandit-python/rules/B324-weak-sha1.yaml +21 -0
- attocode_intel/rules/packs/community/bandit-python/rules/B501-no-cert-validation.yaml +23 -0
- attocode_intel/rules/packs/community/bandit-python/rules/B602-shell-true.yaml +22 -0
- attocode_intel/rules/packs/community/bandit-python/rules/B608-sqli.yaml +22 -0
- attocode_intel/rules/packs/community/eslint-typescript/LICENSE +21 -0
- attocode_intel/rules/packs/community/eslint-typescript/NOTICE +13 -0
- attocode_intel/rules/packs/community/eslint-typescript/PORTING.md +23 -0
- attocode_intel/rules/packs/community/eslint-typescript/manifest.yaml +14 -0
- attocode_intel/rules/packs/community/eslint-typescript/rules/eqeqeq.yaml +20 -0
- attocode_intel/rules/packs/community/eslint-typescript/rules/no-eval.yaml +22 -0
- attocode_intel/rules/packs/community/eslint-typescript/rules/no-implied-eval.yaml +20 -0
- attocode_intel/rules/packs/community/eslint-typescript/rules/no-new-func.yaml +21 -0
- attocode_intel/rules/packs/community/eslint-typescript/rules/no-throw-literal.yaml +19 -0
- attocode_intel/rules/packs/community/eslint-typescript/rules/no-var.yaml +20 -0
- attocode_intel/rules/packs/community/eslint-typescript/rules/no-with.yaml +18 -0
- attocode_intel/rules/packs/community/gosec-go/LICENSE +176 -0
- attocode_intel/rules/packs/community/gosec-go/NOTICE +13 -0
- attocode_intel/rules/packs/community/gosec-go/PORTING.md +23 -0
- attocode_intel/rules/packs/community/gosec-go/manifest.yaml +14 -0
- attocode_intel/rules/packs/community/gosec-go/rules/G101-hardcoded-credentials.yaml +22 -0
- attocode_intel/rules/packs/community/gosec-go/rules/G102-bind-all-interfaces.yaml +21 -0
- attocode_intel/rules/packs/community/gosec-go/rules/G103-unsafe-block.yaml +21 -0
- attocode_intel/rules/packs/community/gosec-go/rules/G201-sql-format.yaml +21 -0
- attocode_intel/rules/packs/community/gosec-go/rules/G304-file-inclusion.yaml +23 -0
- attocode_intel/rules/packs/community/gosec-go/rules/G401-weak-cipher.yaml +20 -0
- attocode_intel/rules/packs/community/gosec-go/rules/G402-tls-min-version.yaml +20 -0
- attocode_intel/rules/packs/community/gosec-go/rules/G501-md5-import.yaml +21 -0
- attocode_intel/rules/packs/examples/cpp/manifest.yaml +4 -0
- attocode_intel/rules/packs/examples/cpp/rules/security.yaml +90 -0
- attocode_intel/rules/packs/examples/cpp/rules/structural.yaml +42 -0
- attocode_intel/rules/packs/examples/csharp/manifest.yaml +4 -0
- attocode_intel/rules/packs/examples/csharp/rules/security.yaml +69 -0
- attocode_intel/rules/packs/examples/csharp/rules/structural.yaml +42 -0
- attocode_intel/rules/packs/examples/go/manifest.yaml +4 -0
- attocode_intel/rules/packs/examples/go/rules/performance.yaml +136 -0
- attocode_intel/rules/packs/examples/go/rules/security.yaml +48 -0
- attocode_intel/rules/packs/examples/go/rules/structural.yaml +58 -0
- attocode_intel/rules/packs/examples/go/taint/sanitizers.yaml +24 -0
- attocode_intel/rules/packs/examples/go/taint/sinks.yaml +51 -0
- attocode_intel/rules/packs/examples/go/taint/sources.yaml +26 -0
- attocode_intel/rules/packs/examples/java/manifest.yaml +4 -0
- attocode_intel/rules/packs/examples/java/rules/correctness.yaml +55 -0
- attocode_intel/rules/packs/examples/java/rules/structural.yaml +48 -0
- attocode_intel/rules/packs/examples/java/taint/sinks.yaml +37 -0
- attocode_intel/rules/packs/examples/java/taint/sources.yaml +21 -0
- attocode_intel/rules/packs/examples/kotlin/manifest.yaml +4 -0
- attocode_intel/rules/packs/examples/kotlin/rules/correctness.yaml +50 -0
- attocode_intel/rules/packs/examples/kotlin/rules/structural.yaml +38 -0
- attocode_intel/rules/packs/examples/php/manifest.yaml +4 -0
- attocode_intel/rules/packs/examples/php/rules/security.yaml +66 -0
- attocode_intel/rules/packs/examples/php/rules/structural.yaml +41 -0
- attocode_intel/rules/packs/examples/python/manifest.yaml +4 -0
- attocode_intel/rules/packs/examples/python/rules/performance.yaml +90 -0
- attocode_intel/rules/packs/examples/python/rules/structural.yaml +55 -0
- attocode_intel/rules/packs/examples/ruby/manifest.yaml +4 -0
- attocode_intel/rules/packs/examples/ruby/rules/security.yaml +73 -0
- attocode_intel/rules/packs/examples/ruby/rules/structural.yaml +41 -0
- attocode_intel/rules/packs/examples/rust/manifest.yaml +4 -0
- attocode_intel/rules/packs/examples/rust/rules/correctness.yaml +51 -0
- attocode_intel/rules/packs/examples/rust/rules/structural.yaml +49 -0
- attocode_intel/rules/packs/examples/typescript/manifest.yaml +4 -0
- attocode_intel/rules/packs/examples/typescript/rules/performance.yaml +70 -0
- attocode_intel/rules/packs/examples/typescript/rules/structural.yaml +48 -0
- attocode_intel/rules/packs/pack_loader.py +244 -0
- attocode_intel/rules/plugins/__init__.py +1 -0
- attocode_intel/rules/plugins/plugin_loader.py +132 -0
- attocode_intel/rules/profiling.py +332 -0
- attocode_intel/rules/registry.py +213 -0
- attocode_intel/rules/sarif.py +187 -0
- attocode_intel/rules/synthesis.py +420 -0
- attocode_intel/rules/testing.py +326 -0
- attocode_intel/server.py +734 -0
- attocode_intel/service.py +3416 -0
- attocode_intel/sharing.py +57 -0
- attocode_intel/storage/__init__.py +6 -0
- attocode_intel/storage/branch_overlay.py +352 -0
- attocode_intel/storage/content_store.py +220 -0
- attocode_intel/storage/dependency_store.py +143 -0
- attocode_intel/storage/diff_engine.py +187 -0
- attocode_intel/storage/embedding_store.py +594 -0
- attocode_intel/storage/provenance_store.py +137 -0
- attocode_intel/storage/security_scanner_db.py +164 -0
- attocode_intel/storage/symbol_store.py +160 -0
- attocode_intel/structural_evidence.py +144 -0
- attocode_intel/symbol_inspection.py +141 -0
- attocode_intel/symbol_links.py +167 -0
- attocode_intel/telemetry.py +42 -0
- attocode_intel/test_ranking.py +73 -0
- attocode_intel/testing/__init__.py +43 -0
- attocode_intel/testing/fixtures.py +122 -0
- attocode_intel/testing/helpers.py +75 -0
- attocode_intel/testing/mocks.py +99 -0
- attocode_intel/tools/__init__.py +2 -0
- attocode_intel/tools/adr_tools.py +628 -0
- attocode_intel/tools/analysis_tools.py +382 -0
- attocode_intel/tools/composite_tools.py +695 -0
- attocode_intel/tools/cross_mode_tools.py +173 -0
- attocode_intel/tools/dead_code_tools.py +582 -0
- attocode_intel/tools/distill_tools.py +353 -0
- attocode_intel/tools/frecency_tools.py +220 -0
- attocode_intel/tools/fuzzy_tools.py +255 -0
- attocode_intel/tools/history_tools.py +486 -0
- attocode_intel/tools/learning_tools.py +222 -0
- attocode_intel/tools/lsp_tools.py +218 -0
- attocode_intel/tools/maintenance_tools.py +1662 -0
- attocode_intel/tools/navigation_tools.py +220 -0
- attocode_intel/tools/overlay_tools.py +540 -0
- attocode_intel/tools/pin_store.py +432 -0
- attocode_intel/tools/pin_tools.py +266 -0
- attocode_intel/tools/query_constraints_tools.py +262 -0
- attocode_intel/tools/query_history_tools.py +158 -0
- attocode_intel/tools/readiness_tools.py +44 -0
- attocode_intel/tools/rule_tools.py +992 -0
- attocode_intel/tools/search_tools.py +770 -0
- attocode_intel/tools/snapshot_tools.py +624 -0
- attocode_intel/workers/__init__.py +1 -0
- attocode_intel/workers/job_utils.py +111 -0
- attocode_intel/workers/jobs.py +573 -0
- attocode_intel/workers/run.py +30 -0
- attocode_intel/workers/settings.py +61 -0
|
@@ -0,0 +1,466 @@
|
|
|
1
|
+
"""Content-addressable cache for code-intel derived artifacts.
|
|
2
|
+
|
|
3
|
+
A single shared store (default ``~/.cache/attocode/cas/``) holds both raw
|
|
4
|
+
source blobs and derived artifacts (symbol sets, embeddings, dep graphs,
|
|
5
|
+
stack-graph partials, etc.) keyed by content hash. Cross-project dedup: if
|
|
6
|
+
two projects reference the same source file or the same (file, indexer,
|
|
7
|
+
version, config) tuple, they share the cache entry.
|
|
8
|
+
|
|
9
|
+
Design notes:
|
|
10
|
+
- On-disk layout:
|
|
11
|
+
<cas_root>/
|
|
12
|
+
blobs/<aa>/<bb>/<rest> # raw file contents (opt-in)
|
|
13
|
+
derived/<artifact_type>/<aa>/<bb>/<rest>
|
|
14
|
+
manifest.db # SQLite index: fast exists / gc
|
|
15
|
+
- Keys are hex digests (sha256 or git-style sha1) prefixed with algo
|
|
16
|
+
(``"sha256:..."``, ``"git:..."``). The prefix is stripped before the
|
|
17
|
+
two-level directory sharding.
|
|
18
|
+
- Reference counting is application-level: callers ``incref()`` on write
|
|
19
|
+
and ``decref()`` when a downstream store drops its reference. GC only
|
|
20
|
+
removes entries whose refcount is zero *and* whose last-accessed time
|
|
21
|
+
is older than the min_age threshold (default 7 days), so briefly
|
|
22
|
+
unreferenced entries aren't prematurely evicted.
|
|
23
|
+
- Directly operating on the filesystem keeps the store easy to inspect
|
|
24
|
+
with ``ls`` / ``sha256sum`` — no proprietary format.
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
from __future__ import annotations
|
|
28
|
+
|
|
29
|
+
import logging
|
|
30
|
+
import os
|
|
31
|
+
import sqlite3
|
|
32
|
+
import threading
|
|
33
|
+
import time
|
|
34
|
+
from dataclasses import dataclass, field
|
|
35
|
+
from typing import TYPE_CHECKING, Any
|
|
36
|
+
|
|
37
|
+
from attocode_intel.artifacts import Provenance
|
|
38
|
+
|
|
39
|
+
if TYPE_CHECKING:
|
|
40
|
+
from collections.abc import Iterator
|
|
41
|
+
|
|
42
|
+
logger = logging.getLogger(__name__)
|
|
43
|
+
|
|
44
|
+
CAS_SCHEMA_VERSION = 1
|
|
45
|
+
# Default GC age floor — unreferenced entries younger than this are kept.
|
|
46
|
+
DEFAULT_GC_MIN_AGE_SECONDS = 7 * 24 * 3600 # 7 days
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def _default_cas_root() -> str:
|
|
50
|
+
"""XDG-style default CAS location, overridable via ``ATTOCODE_CAS_DIR``."""
|
|
51
|
+
env = os.environ.get("ATTOCODE_CAS_DIR")
|
|
52
|
+
if env:
|
|
53
|
+
return os.path.expanduser(env)
|
|
54
|
+
xdg_cache = os.environ.get("XDG_CACHE_HOME")
|
|
55
|
+
if xdg_cache:
|
|
56
|
+
return os.path.join(os.path.expanduser(xdg_cache), "attocode", "cas")
|
|
57
|
+
return os.path.expanduser("~/.cache/attocode/cas")
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def _normalize_key(key: str) -> tuple[str, str]:
|
|
61
|
+
"""Return (algo, hex) for a CAS key like ``sha256:abc...`` or ``git:def...``.
|
|
62
|
+
|
|
63
|
+
Raises ``ValueError`` if the key has no algo prefix — we want loud
|
|
64
|
+
failure rather than silently hashing in one format on writes and
|
|
65
|
+
looking it up in another format on reads.
|
|
66
|
+
"""
|
|
67
|
+
if ":" not in key:
|
|
68
|
+
raise ValueError(
|
|
69
|
+
f"CAS key must be prefixed with an algo (e.g. 'sha256:'); got {key!r}"
|
|
70
|
+
)
|
|
71
|
+
algo, _, hexdigest = key.partition(":")
|
|
72
|
+
if not hexdigest:
|
|
73
|
+
raise ValueError(f"CAS key is empty after algo prefix: {key!r}")
|
|
74
|
+
if algo not in {"sha256", "git"}:
|
|
75
|
+
raise ValueError(f"unsupported CAS algo {algo!r}; use 'sha256' or 'git'")
|
|
76
|
+
# Defensive: strip whitespace, lowercase.
|
|
77
|
+
return algo, hexdigest.strip().lower()
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def _shard_path(root: str, subdir: str, key: str) -> str:
|
|
81
|
+
"""Two-level sharding to avoid one giant directory."""
|
|
82
|
+
_, hexdigest = _normalize_key(key)
|
|
83
|
+
return os.path.join(root, subdir, hexdigest[:2], hexdigest[2:4], hexdigest[4:])
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
@dataclass(slots=True)
|
|
87
|
+
class CasEntry:
|
|
88
|
+
"""One row from the CAS manifest, returned by :meth:`ContentAddressedCache.stat`."""
|
|
89
|
+
|
|
90
|
+
key: str
|
|
91
|
+
artifact_type: str
|
|
92
|
+
action_hash: str
|
|
93
|
+
size_bytes: int
|
|
94
|
+
created_at: float
|
|
95
|
+
last_accessed_at: float
|
|
96
|
+
refcount: int
|
|
97
|
+
provenance: Provenance | None
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
@dataclass(slots=True)
|
|
101
|
+
class ContentAddressedCache:
|
|
102
|
+
"""Content-addressable cache for source blobs and derived artifacts.
|
|
103
|
+
|
|
104
|
+
Usage::
|
|
105
|
+
|
|
106
|
+
cas = ContentAddressedCache()
|
|
107
|
+
cas.put(
|
|
108
|
+
key="sha256:abc...",
|
|
109
|
+
data=symbol_set_bytes,
|
|
110
|
+
artifact_type="symbols",
|
|
111
|
+
provenance=Provenance.create(...),
|
|
112
|
+
)
|
|
113
|
+
if cas.exists("sha256:abc...", "symbols"):
|
|
114
|
+
data = cas.get("sha256:abc...", "symbols")
|
|
115
|
+
"""
|
|
116
|
+
|
|
117
|
+
cas_root: str = field(default_factory=_default_cas_root)
|
|
118
|
+
_conn: sqlite3.Connection | None = field(default=None, repr=False)
|
|
119
|
+
_lock: threading.Lock = field(default_factory=threading.Lock, repr=False)
|
|
120
|
+
|
|
121
|
+
def __post_init__(self) -> None:
|
|
122
|
+
os.makedirs(self.cas_root, exist_ok=True)
|
|
123
|
+
os.makedirs(os.path.join(self.cas_root, "blobs"), exist_ok=True)
|
|
124
|
+
os.makedirs(os.path.join(self.cas_root, "derived"), exist_ok=True)
|
|
125
|
+
self._conn = sqlite3.connect(
|
|
126
|
+
os.path.join(self.cas_root, "manifest.db"),
|
|
127
|
+
check_same_thread=False,
|
|
128
|
+
)
|
|
129
|
+
self._conn.execute("PRAGMA journal_mode=WAL")
|
|
130
|
+
self._conn.execute("PRAGMA synchronous=NORMAL")
|
|
131
|
+
self._create_tables()
|
|
132
|
+
|
|
133
|
+
def _create_tables(self) -> None:
|
|
134
|
+
assert self._conn is not None
|
|
135
|
+
self._conn.executescript(
|
|
136
|
+
"""
|
|
137
|
+
CREATE TABLE IF NOT EXISTS cas_meta (
|
|
138
|
+
key TEXT PRIMARY KEY,
|
|
139
|
+
value TEXT NOT NULL
|
|
140
|
+
);
|
|
141
|
+
|
|
142
|
+
CREATE TABLE IF NOT EXISTS cas_entries (
|
|
143
|
+
key TEXT NOT NULL,
|
|
144
|
+
artifact_type TEXT NOT NULL,
|
|
145
|
+
action_hash TEXT NOT NULL DEFAULT '',
|
|
146
|
+
size_bytes INTEGER NOT NULL,
|
|
147
|
+
created_at REAL NOT NULL,
|
|
148
|
+
last_accessed_at REAL NOT NULL,
|
|
149
|
+
refcount INTEGER NOT NULL DEFAULT 0,
|
|
150
|
+
provenance_json TEXT NOT NULL DEFAULT '{}',
|
|
151
|
+
PRIMARY KEY (key, artifact_type)
|
|
152
|
+
);
|
|
153
|
+
|
|
154
|
+
CREATE INDEX IF NOT EXISTS ix_cas_type ON cas_entries(artifact_type);
|
|
155
|
+
CREATE INDEX IF NOT EXISTS ix_cas_atime ON cas_entries(last_accessed_at);
|
|
156
|
+
CREATE INDEX IF NOT EXISTS ix_cas_action ON cas_entries(action_hash);
|
|
157
|
+
"""
|
|
158
|
+
)
|
|
159
|
+
self._conn.execute(
|
|
160
|
+
"INSERT OR REPLACE INTO cas_meta (key, value) VALUES ('schema_version', ?)",
|
|
161
|
+
(str(CAS_SCHEMA_VERSION),),
|
|
162
|
+
)
|
|
163
|
+
self._conn.commit()
|
|
164
|
+
|
|
165
|
+
# ------------------------------------------------------------------
|
|
166
|
+
# Put / Get
|
|
167
|
+
# ------------------------------------------------------------------
|
|
168
|
+
|
|
169
|
+
def _path_for(self, key: str, artifact_type: str) -> str:
|
|
170
|
+
subdir = "blobs" if artifact_type == "blob" else f"derived/{artifact_type}"
|
|
171
|
+
return _shard_path(self.cas_root, subdir, key)
|
|
172
|
+
|
|
173
|
+
def put(
|
|
174
|
+
self,
|
|
175
|
+
key: str,
|
|
176
|
+
data: bytes,
|
|
177
|
+
*,
|
|
178
|
+
artifact_type: str,
|
|
179
|
+
provenance: Provenance | None = None,
|
|
180
|
+
action_hash: str = "",
|
|
181
|
+
) -> str:
|
|
182
|
+
"""Store ``data`` under ``key`` + ``artifact_type``.
|
|
183
|
+
|
|
184
|
+
Returns the on-disk path of the stored file. Idempotent: re-putting
|
|
185
|
+
the same key overwrites the file (should produce identical bytes
|
|
186
|
+
anyway) and refreshes ``last_accessed_at``.
|
|
187
|
+
"""
|
|
188
|
+
path = self._path_for(key, artifact_type)
|
|
189
|
+
os.makedirs(os.path.dirname(path), exist_ok=True)
|
|
190
|
+
tmp = path + ".tmp"
|
|
191
|
+
with open(tmp, "wb") as f:
|
|
192
|
+
f.write(data)
|
|
193
|
+
os.replace(tmp, path)
|
|
194
|
+
|
|
195
|
+
now = time.time()
|
|
196
|
+
provenance_json = provenance.to_json() if provenance is not None else "{}"
|
|
197
|
+
size = len(data)
|
|
198
|
+
|
|
199
|
+
assert self._conn is not None
|
|
200
|
+
with self._lock:
|
|
201
|
+
self._conn.execute(
|
|
202
|
+
"""INSERT INTO cas_entries
|
|
203
|
+
(key, artifact_type, action_hash, size_bytes,
|
|
204
|
+
created_at, last_accessed_at, refcount, provenance_json)
|
|
205
|
+
VALUES (?, ?, ?, ?, ?, ?, 0, ?)
|
|
206
|
+
ON CONFLICT(key, artifact_type) DO UPDATE SET
|
|
207
|
+
size_bytes = excluded.size_bytes,
|
|
208
|
+
last_accessed_at = excluded.last_accessed_at,
|
|
209
|
+
action_hash = excluded.action_hash,
|
|
210
|
+
provenance_json = excluded.provenance_json
|
|
211
|
+
""",
|
|
212
|
+
(
|
|
213
|
+
key, artifact_type, action_hash, size,
|
|
214
|
+
now, now, provenance_json,
|
|
215
|
+
),
|
|
216
|
+
)
|
|
217
|
+
self._conn.commit()
|
|
218
|
+
return path
|
|
219
|
+
|
|
220
|
+
def get(self, key: str, artifact_type: str) -> bytes | None:
|
|
221
|
+
"""Return stored bytes, or None if the entry does not exist."""
|
|
222
|
+
path = self._path_for(key, artifact_type)
|
|
223
|
+
if not os.path.exists(path):
|
|
224
|
+
return None
|
|
225
|
+
with open(path, "rb") as f:
|
|
226
|
+
data = f.read()
|
|
227
|
+
# Refresh access time (best-effort; a failure here shouldn't break reads).
|
|
228
|
+
try:
|
|
229
|
+
self._touch(key, artifact_type)
|
|
230
|
+
except sqlite3.OperationalError:
|
|
231
|
+
pass
|
|
232
|
+
return data
|
|
233
|
+
|
|
234
|
+
def exists(self, key: str, artifact_type: str) -> bool:
|
|
235
|
+
"""Cheap existence check: manifest.db + filesystem."""
|
|
236
|
+
assert self._conn is not None
|
|
237
|
+
with self._lock:
|
|
238
|
+
row = self._conn.execute(
|
|
239
|
+
"SELECT 1 FROM cas_entries WHERE key = ? AND artifact_type = ?",
|
|
240
|
+
(key, artifact_type),
|
|
241
|
+
).fetchone()
|
|
242
|
+
if row is None:
|
|
243
|
+
return False
|
|
244
|
+
return os.path.exists(self._path_for(key, artifact_type))
|
|
245
|
+
|
|
246
|
+
def stat(self, key: str, artifact_type: str) -> CasEntry | None:
|
|
247
|
+
"""Return metadata for one entry, or None."""
|
|
248
|
+
assert self._conn is not None
|
|
249
|
+
with self._lock:
|
|
250
|
+
row = self._conn.execute(
|
|
251
|
+
"""SELECT key, artifact_type, action_hash, size_bytes, created_at,
|
|
252
|
+
last_accessed_at, refcount, provenance_json
|
|
253
|
+
FROM cas_entries WHERE key = ? AND artifact_type = ?""",
|
|
254
|
+
(key, artifact_type),
|
|
255
|
+
).fetchone()
|
|
256
|
+
if row is None:
|
|
257
|
+
return None
|
|
258
|
+
provenance: Provenance | None
|
|
259
|
+
if row[7] and row[7] != "{}":
|
|
260
|
+
try:
|
|
261
|
+
import json
|
|
262
|
+
provenance = Provenance.from_dict(json.loads(row[7]))
|
|
263
|
+
except (ValueError, KeyError):
|
|
264
|
+
provenance = None
|
|
265
|
+
else:
|
|
266
|
+
provenance = None
|
|
267
|
+
return CasEntry(
|
|
268
|
+
key=row[0],
|
|
269
|
+
artifact_type=row[1],
|
|
270
|
+
action_hash=row[2],
|
|
271
|
+
size_bytes=row[3],
|
|
272
|
+
created_at=row[4],
|
|
273
|
+
last_accessed_at=row[5],
|
|
274
|
+
refcount=row[6],
|
|
275
|
+
provenance=provenance,
|
|
276
|
+
)
|
|
277
|
+
|
|
278
|
+
def _touch(self, key: str, artifact_type: str) -> None:
|
|
279
|
+
assert self._conn is not None
|
|
280
|
+
with self._lock:
|
|
281
|
+
self._conn.execute(
|
|
282
|
+
"UPDATE cas_entries SET last_accessed_at = ? "
|
|
283
|
+
"WHERE key = ? AND artifact_type = ?",
|
|
284
|
+
(time.time(), key, artifact_type),
|
|
285
|
+
)
|
|
286
|
+
self._conn.commit()
|
|
287
|
+
|
|
288
|
+
# ------------------------------------------------------------------
|
|
289
|
+
# Reference counting
|
|
290
|
+
# ------------------------------------------------------------------
|
|
291
|
+
|
|
292
|
+
def incref(self, key: str, artifact_type: str) -> int:
|
|
293
|
+
"""Increment refcount; returns new value. Returns 0 if entry not found."""
|
|
294
|
+
assert self._conn is not None
|
|
295
|
+
with self._lock:
|
|
296
|
+
cur = self._conn.execute(
|
|
297
|
+
"UPDATE cas_entries SET refcount = refcount + 1 "
|
|
298
|
+
"WHERE key = ? AND artifact_type = ?",
|
|
299
|
+
(key, artifact_type),
|
|
300
|
+
)
|
|
301
|
+
self._conn.commit()
|
|
302
|
+
if cur.rowcount == 0:
|
|
303
|
+
return 0
|
|
304
|
+
row = self._conn.execute(
|
|
305
|
+
"SELECT refcount FROM cas_entries "
|
|
306
|
+
"WHERE key = ? AND artifact_type = ?",
|
|
307
|
+
(key, artifact_type),
|
|
308
|
+
).fetchone()
|
|
309
|
+
return int(row[0]) if row else 0
|
|
310
|
+
|
|
311
|
+
def decref(self, key: str, artifact_type: str) -> int:
|
|
312
|
+
"""Decrement refcount (not below zero); returns new value."""
|
|
313
|
+
assert self._conn is not None
|
|
314
|
+
with self._lock:
|
|
315
|
+
self._conn.execute(
|
|
316
|
+
"UPDATE cas_entries "
|
|
317
|
+
"SET refcount = CASE WHEN refcount > 0 THEN refcount - 1 ELSE 0 END "
|
|
318
|
+
"WHERE key = ? AND artifact_type = ?",
|
|
319
|
+
(key, artifact_type),
|
|
320
|
+
)
|
|
321
|
+
self._conn.commit()
|
|
322
|
+
row = self._conn.execute(
|
|
323
|
+
"SELECT refcount FROM cas_entries "
|
|
324
|
+
"WHERE key = ? AND artifact_type = ?",
|
|
325
|
+
(key, artifact_type),
|
|
326
|
+
).fetchone()
|
|
327
|
+
return int(row[0]) if row else 0
|
|
328
|
+
|
|
329
|
+
# ------------------------------------------------------------------
|
|
330
|
+
# GC
|
|
331
|
+
# ------------------------------------------------------------------
|
|
332
|
+
|
|
333
|
+
def iter_orphans(
|
|
334
|
+
self, min_age_seconds: float = DEFAULT_GC_MIN_AGE_SECONDS,
|
|
335
|
+
) -> Iterator[tuple[str, str, int]]:
|
|
336
|
+
"""Yield (key, artifact_type, size_bytes) for entries with refcount=0
|
|
337
|
+
older than the age floor.
|
|
338
|
+
"""
|
|
339
|
+
cutoff = time.time() - min_age_seconds
|
|
340
|
+
assert self._conn is not None
|
|
341
|
+
with self._lock:
|
|
342
|
+
rows = self._conn.execute(
|
|
343
|
+
"""SELECT key, artifact_type, size_bytes FROM cas_entries
|
|
344
|
+
WHERE refcount <= 0 AND last_accessed_at <= ?
|
|
345
|
+
ORDER BY last_accessed_at""",
|
|
346
|
+
(cutoff,),
|
|
347
|
+
).fetchall()
|
|
348
|
+
for row in rows:
|
|
349
|
+
yield row[0], row[1], int(row[2])
|
|
350
|
+
|
|
351
|
+
def gc(
|
|
352
|
+
self,
|
|
353
|
+
min_age_seconds: float = DEFAULT_GC_MIN_AGE_SECONDS,
|
|
354
|
+
dry_run: bool = True,
|
|
355
|
+
) -> dict[str, Any]:
|
|
356
|
+
"""Delete orphaned entries (refcount=0, older than age floor).
|
|
357
|
+
|
|
358
|
+
Returns a summary dict with ``{would_delete_count, would_free_bytes,
|
|
359
|
+
deleted_count, freed_bytes, dry_run, examples}``.
|
|
360
|
+
|
|
361
|
+
``examples`` is a short list of entries (up to 10) so callers can
|
|
362
|
+
show a preview of what would (or did) get deleted.
|
|
363
|
+
"""
|
|
364
|
+
would_count = 0
|
|
365
|
+
would_bytes = 0
|
|
366
|
+
examples: list[dict[str, Any]] = []
|
|
367
|
+
|
|
368
|
+
to_delete: list[tuple[str, str]] = []
|
|
369
|
+
for key, artifact_type, size in self.iter_orphans(min_age_seconds):
|
|
370
|
+
would_count += 1
|
|
371
|
+
would_bytes += size
|
|
372
|
+
if len(examples) < 10:
|
|
373
|
+
examples.append({
|
|
374
|
+
"key": key,
|
|
375
|
+
"artifact_type": artifact_type,
|
|
376
|
+
"size_bytes": size,
|
|
377
|
+
})
|
|
378
|
+
to_delete.append((key, artifact_type))
|
|
379
|
+
|
|
380
|
+
if dry_run:
|
|
381
|
+
return {
|
|
382
|
+
"dry_run": True,
|
|
383
|
+
"would_delete_count": would_count,
|
|
384
|
+
"would_free_bytes": would_bytes,
|
|
385
|
+
"deleted_count": 0,
|
|
386
|
+
"freed_bytes": 0,
|
|
387
|
+
"examples": examples,
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
deleted = 0
|
|
391
|
+
freed = 0
|
|
392
|
+
assert self._conn is not None
|
|
393
|
+
for key, artifact_type in to_delete:
|
|
394
|
+
path = self._path_for(key, artifact_type)
|
|
395
|
+
try:
|
|
396
|
+
size = os.path.getsize(path)
|
|
397
|
+
except OSError:
|
|
398
|
+
size = 0
|
|
399
|
+
try:
|
|
400
|
+
os.unlink(path)
|
|
401
|
+
except FileNotFoundError:
|
|
402
|
+
pass
|
|
403
|
+
except OSError as exc:
|
|
404
|
+
logger.warning("cas: failed to unlink %s: %s", path, exc)
|
|
405
|
+
continue
|
|
406
|
+
with self._lock:
|
|
407
|
+
self._conn.execute(
|
|
408
|
+
"DELETE FROM cas_entries WHERE key = ? AND artifact_type = ?",
|
|
409
|
+
(key, artifact_type),
|
|
410
|
+
)
|
|
411
|
+
self._conn.commit()
|
|
412
|
+
deleted += 1
|
|
413
|
+
freed += size
|
|
414
|
+
|
|
415
|
+
return {
|
|
416
|
+
"dry_run": False,
|
|
417
|
+
"would_delete_count": would_count,
|
|
418
|
+
"would_free_bytes": would_bytes,
|
|
419
|
+
"deleted_count": deleted,
|
|
420
|
+
"freed_bytes": freed,
|
|
421
|
+
"examples": examples,
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
# ------------------------------------------------------------------
|
|
425
|
+
# Stats / introspection
|
|
426
|
+
# ------------------------------------------------------------------
|
|
427
|
+
|
|
428
|
+
def stats(self) -> dict[str, Any]:
|
|
429
|
+
"""Return summary stats for the CAS: per-type counts + total bytes."""
|
|
430
|
+
assert self._conn is not None
|
|
431
|
+
with self._lock:
|
|
432
|
+
rows = self._conn.execute(
|
|
433
|
+
"""SELECT artifact_type, COUNT(*), SUM(size_bytes),
|
|
434
|
+
SUM(CASE WHEN refcount <= 0 THEN 1 ELSE 0 END)
|
|
435
|
+
FROM cas_entries GROUP BY artifact_type"""
|
|
436
|
+
).fetchall()
|
|
437
|
+
by_type = {
|
|
438
|
+
row[0]: {
|
|
439
|
+
"count": int(row[1] or 0),
|
|
440
|
+
"bytes": int(row[2] or 0),
|
|
441
|
+
"orphans": int(row[3] or 0),
|
|
442
|
+
}
|
|
443
|
+
for row in rows
|
|
444
|
+
}
|
|
445
|
+
total = {
|
|
446
|
+
"count": sum(v["count"] for v in by_type.values()),
|
|
447
|
+
"bytes": sum(v["bytes"] for v in by_type.values()),
|
|
448
|
+
"orphans": sum(v["orphans"] for v in by_type.values()),
|
|
449
|
+
}
|
|
450
|
+
return {
|
|
451
|
+
"cas_root": self.cas_root,
|
|
452
|
+
"schema_version": CAS_SCHEMA_VERSION,
|
|
453
|
+
"by_type": by_type,
|
|
454
|
+
"total": total,
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
def close(self) -> None:
|
|
458
|
+
if self._conn:
|
|
459
|
+
self._conn.close()
|
|
460
|
+
self._conn = None
|
|
461
|
+
|
|
462
|
+
def __del__(self) -> None:
|
|
463
|
+
try:
|
|
464
|
+
self.close()
|
|
465
|
+
except Exception: # noqa: BLE001
|
|
466
|
+
pass
|