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,159 @@
|
|
|
1
|
+
"""Retrieval pins — make ranked-result tools deterministic across calls.
|
|
2
|
+
|
|
3
|
+
A ``RetrievalPin`` is a content-addressed snapshot of the state of every
|
|
4
|
+
code-intel store at a moment in time. Every ranked-result tool
|
|
5
|
+
(``semantic_search``, ``fast_search``, ``repo_map``, etc.) emits one in its
|
|
6
|
+
response; a caller can then pass the pin back to ``retrieve_with_pin`` to
|
|
7
|
+
re-run the same query against the same state, with a loud drift error if
|
|
8
|
+
the underlying stores have changed.
|
|
9
|
+
|
|
10
|
+
The per-store hash is cheap: ``sha256(max(updated_at) || row_count || schema_version)``.
|
|
11
|
+
That's stable under no-op reads and changes on any write.
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
import base64
|
|
17
|
+
import hashlib
|
|
18
|
+
import json
|
|
19
|
+
import secrets
|
|
20
|
+
import time
|
|
21
|
+
from dataclasses import asdict, dataclass, field
|
|
22
|
+
from typing import TYPE_CHECKING, Any
|
|
23
|
+
|
|
24
|
+
from attocode_intel.artifacts.hashing import canonical_json
|
|
25
|
+
|
|
26
|
+
if TYPE_CHECKING:
|
|
27
|
+
from collections.abc import Mapping
|
|
28
|
+
|
|
29
|
+
PIN_SCHEMA_VERSION = 1
|
|
30
|
+
DEFAULT_TTL_SECONDS = 86_400 # 24 hours
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def compute_store_hash(
|
|
34
|
+
*,
|
|
35
|
+
schema_version: str | int,
|
|
36
|
+
row_count: int,
|
|
37
|
+
max_updated_at: float | None,
|
|
38
|
+
extra: Mapping[str, Any] | None = None,
|
|
39
|
+
) -> str:
|
|
40
|
+
"""Compute a cheap, stable content hash for one store's current state.
|
|
41
|
+
|
|
42
|
+
Intentionally only uses three fields (schema version, row count, latest
|
|
43
|
+
``updated_at``) so the hash can be computed with a single cheap SQL
|
|
44
|
+
aggregate query. ``extra`` is for stores that need to mix in something
|
|
45
|
+
else (e.g. active embedding model pointer) — pass a small dict, it goes
|
|
46
|
+
through canonical JSON.
|
|
47
|
+
|
|
48
|
+
Two stores on two machines with exactly the same rows should produce
|
|
49
|
+
the same hash; mutations (insert/update/delete) change it; pure reads
|
|
50
|
+
do not.
|
|
51
|
+
"""
|
|
52
|
+
body = {
|
|
53
|
+
"schema": str(schema_version),
|
|
54
|
+
"rows": int(row_count),
|
|
55
|
+
"max_updated_at": float(max_updated_at or 0.0),
|
|
56
|
+
"extra": dict(extra or {}),
|
|
57
|
+
}
|
|
58
|
+
return hashlib.sha256(canonical_json(body)).hexdigest()
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def make_pin_id() -> str:
|
|
62
|
+
"""Return a short, URL-safe, unguessable pin id: ``pin_<22 base32 chars>``."""
|
|
63
|
+
raw = secrets.token_bytes(16)
|
|
64
|
+
token = base64.b32encode(raw).decode("ascii").rstrip("=").lower()
|
|
65
|
+
return f"pin_{token}"
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
@dataclass(frozen=True, slots=True)
|
|
69
|
+
class RetrievalPin:
|
|
70
|
+
"""Immutable snapshot of code-intel state.
|
|
71
|
+
|
|
72
|
+
Fields:
|
|
73
|
+
- ``pin_id``: short identifier (see :func:`make_pin_id`).
|
|
74
|
+
- ``schema_version``: for forward-compat.
|
|
75
|
+
- ``manifest_hashes``: per-store name → hex store hash.
|
|
76
|
+
- ``manifest_hash``: sha256 of the canonical JSON of ``manifest_hashes``
|
|
77
|
+
(plus overlay_id). This is the thing to compare when checking drift.
|
|
78
|
+
- ``overlay_id``: optional branch/overlay identifier (local side).
|
|
79
|
+
- ``branch_id``: optional DB branch id (server side).
|
|
80
|
+
- ``created_at``, ``expires_at``: unix seconds; 0 for no expiry.
|
|
81
|
+
"""
|
|
82
|
+
|
|
83
|
+
pin_id: str
|
|
84
|
+
schema_version: int
|
|
85
|
+
manifest_hashes: dict[str, str] = field(default_factory=dict)
|
|
86
|
+
manifest_hash: str = ""
|
|
87
|
+
overlay_id: str | None = None
|
|
88
|
+
branch_id: str | None = None
|
|
89
|
+
created_at: float = 0.0
|
|
90
|
+
expires_at: float = 0.0
|
|
91
|
+
|
|
92
|
+
@classmethod
|
|
93
|
+
def create(
|
|
94
|
+
cls,
|
|
95
|
+
*,
|
|
96
|
+
manifest_hashes: Mapping[str, str],
|
|
97
|
+
overlay_id: str | None = None,
|
|
98
|
+
branch_id: str | None = None,
|
|
99
|
+
ttl_seconds: int = DEFAULT_TTL_SECONDS,
|
|
100
|
+
pin_id: str | None = None,
|
|
101
|
+
) -> RetrievalPin:
|
|
102
|
+
"""Build a new pin from a dict of per-store hashes."""
|
|
103
|
+
manifest = {str(k): str(v) for k, v in sorted(manifest_hashes.items())}
|
|
104
|
+
manifest_hash = hashlib.sha256(
|
|
105
|
+
canonical_json({"manifest": manifest, "overlay": overlay_id, "branch": branch_id})
|
|
106
|
+
).hexdigest()
|
|
107
|
+
now = time.time()
|
|
108
|
+
return cls(
|
|
109
|
+
pin_id=pin_id or make_pin_id(),
|
|
110
|
+
schema_version=PIN_SCHEMA_VERSION,
|
|
111
|
+
manifest_hashes=dict(manifest),
|
|
112
|
+
manifest_hash=manifest_hash,
|
|
113
|
+
overlay_id=overlay_id,
|
|
114
|
+
branch_id=branch_id,
|
|
115
|
+
created_at=now,
|
|
116
|
+
expires_at=now + ttl_seconds if ttl_seconds > 0 else 0.0,
|
|
117
|
+
)
|
|
118
|
+
|
|
119
|
+
def is_expired(self, now: float | None = None) -> bool:
|
|
120
|
+
"""Whether the pin has passed its TTL. Pins with expires_at == 0 never expire."""
|
|
121
|
+
if self.expires_at <= 0:
|
|
122
|
+
return False
|
|
123
|
+
return (now if now is not None else time.time()) >= self.expires_at
|
|
124
|
+
|
|
125
|
+
def drift_from(self, current_hashes: Mapping[str, str]) -> dict[str, tuple[str, str]]:
|
|
126
|
+
"""Return a map of store_name -> (pinned_hash, current_hash) for any store that
|
|
127
|
+
changed vs the pinned state. Empty dict means no drift.
|
|
128
|
+
|
|
129
|
+
Stores present in one side but not the other are reported with the
|
|
130
|
+
missing side as ``""`` — this is usually a schema change and should
|
|
131
|
+
be treated as drift.
|
|
132
|
+
"""
|
|
133
|
+
drift: dict[str, tuple[str, str]] = {}
|
|
134
|
+
all_keys = set(self.manifest_hashes) | set(current_hashes)
|
|
135
|
+
for key in all_keys:
|
|
136
|
+
pinned = self.manifest_hashes.get(key, "")
|
|
137
|
+
current = current_hashes.get(key, "")
|
|
138
|
+
if pinned != current:
|
|
139
|
+
drift[key] = (pinned, current)
|
|
140
|
+
return drift
|
|
141
|
+
|
|
142
|
+
def to_dict(self) -> dict[str, Any]:
|
|
143
|
+
return asdict(self)
|
|
144
|
+
|
|
145
|
+
def to_json(self) -> str:
|
|
146
|
+
return json.dumps(self.to_dict(), sort_keys=True, ensure_ascii=True)
|
|
147
|
+
|
|
148
|
+
@classmethod
|
|
149
|
+
def from_dict(cls, data: Mapping[str, Any]) -> RetrievalPin:
|
|
150
|
+
return cls(
|
|
151
|
+
pin_id=str(data["pin_id"]),
|
|
152
|
+
schema_version=int(data.get("schema_version", PIN_SCHEMA_VERSION)),
|
|
153
|
+
manifest_hashes=dict(data.get("manifest_hashes", {})),
|
|
154
|
+
manifest_hash=str(data.get("manifest_hash", "")),
|
|
155
|
+
overlay_id=data.get("overlay_id"),
|
|
156
|
+
branch_id=data.get("branch_id"),
|
|
157
|
+
created_at=float(data.get("created_at", 0.0)),
|
|
158
|
+
expires_at=float(data.get("expires_at", 0.0)),
|
|
159
|
+
)
|
attocode_intel/audit.py
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
"""Audit event logging utility — writes to DB and publishes via Redis."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import logging
|
|
6
|
+
import uuid
|
|
7
|
+
from typing import TYPE_CHECKING
|
|
8
|
+
|
|
9
|
+
if TYPE_CHECKING:
|
|
10
|
+
from sqlalchemy.ext.asyncio import AsyncSession
|
|
11
|
+
|
|
12
|
+
logger = logging.getLogger(__name__)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
async def log_event(
|
|
16
|
+
session: AsyncSession,
|
|
17
|
+
org_id: uuid.UUID,
|
|
18
|
+
event_type: str,
|
|
19
|
+
*,
|
|
20
|
+
repo_id: uuid.UUID | None = None,
|
|
21
|
+
user_id: uuid.UUID | None = None,
|
|
22
|
+
detail: dict | None = None,
|
|
23
|
+
) -> None:
|
|
24
|
+
"""Log an audit event to DB and publish to Redis for real-time streaming.
|
|
25
|
+
|
|
26
|
+
Args:
|
|
27
|
+
session: Active DB session (caller should commit).
|
|
28
|
+
org_id: Organization ID.
|
|
29
|
+
event_type: Event type string (e.g. "org.created", "api_key.revoked").
|
|
30
|
+
repo_id: Optional repository ID.
|
|
31
|
+
user_id: Optional user ID who performed the action.
|
|
32
|
+
detail: Optional JSON-serializable detail dict.
|
|
33
|
+
"""
|
|
34
|
+
from attocode_intel.db.models import AuditEvent
|
|
35
|
+
|
|
36
|
+
event = AuditEvent(
|
|
37
|
+
org_id=org_id,
|
|
38
|
+
repo_id=repo_id,
|
|
39
|
+
user_id=user_id,
|
|
40
|
+
event_type=event_type,
|
|
41
|
+
detail=detail or {},
|
|
42
|
+
)
|
|
43
|
+
session.add(event)
|
|
44
|
+
|
|
45
|
+
# Also publish to Redis for real-time WebSocket streaming
|
|
46
|
+
try:
|
|
47
|
+
from attocode_intel.pubsub import publish_event
|
|
48
|
+
|
|
49
|
+
channel_id = str(repo_id) if repo_id else str(org_id)
|
|
50
|
+
await publish_event(channel_id, f"audit.{event_type}", {
|
|
51
|
+
"org_id": str(org_id),
|
|
52
|
+
"repo_id": str(repo_id) if repo_id else None,
|
|
53
|
+
"user_id": str(user_id) if user_id else None,
|
|
54
|
+
"event_type": event_type,
|
|
55
|
+
"detail": detail or {},
|
|
56
|
+
})
|
|
57
|
+
except Exception:
|
|
58
|
+
# Don't fail the main operation if Redis publish fails
|
|
59
|
+
logger.debug("Failed to publish audit event to Redis", exc_info=True)
|
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
"""Bug finder — scans diffs for potential issues.
|
|
2
|
+
|
|
3
|
+
Analyzes code changes between branches, looking for common
|
|
4
|
+
bug patterns, edge cases, security issues, and performance
|
|
5
|
+
problems. Reports findings with confidence levels.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import logging
|
|
11
|
+
import re
|
|
12
|
+
from dataclasses import dataclass, field
|
|
13
|
+
from enum import StrEnum
|
|
14
|
+
|
|
15
|
+
from attocode_intel._internal.types.severity import Severity
|
|
16
|
+
|
|
17
|
+
logger = logging.getLogger(__name__)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class FindingCategory(StrEnum):
|
|
21
|
+
"""Category of bug finding."""
|
|
22
|
+
LOGIC_ERROR = "logic_error"
|
|
23
|
+
EDGE_CASE = "edge_case"
|
|
24
|
+
SECURITY = "security"
|
|
25
|
+
PERFORMANCE = "performance"
|
|
26
|
+
ERROR_HANDLING = "error_handling"
|
|
27
|
+
TYPE_SAFETY = "type_safety"
|
|
28
|
+
CONCURRENCY = "concurrency"
|
|
29
|
+
RESOURCE_LEAK = "resource_leak"
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
@dataclass(slots=True)
|
|
33
|
+
class Finding:
|
|
34
|
+
"""A single bug finding."""
|
|
35
|
+
file: str
|
|
36
|
+
line: int
|
|
37
|
+
severity: Severity
|
|
38
|
+
category: FindingCategory
|
|
39
|
+
confidence: float # 0.0-1.0
|
|
40
|
+
description: str
|
|
41
|
+
suggestion: str = ""
|
|
42
|
+
code_snippet: str = ""
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
@dataclass(slots=True)
|
|
46
|
+
class BugReport:
|
|
47
|
+
"""Complete bug report for a scan."""
|
|
48
|
+
findings: list[Finding] = field(default_factory=list)
|
|
49
|
+
files_scanned: int = 0
|
|
50
|
+
lines_analyzed: int = 0
|
|
51
|
+
scan_duration: float = 0.0
|
|
52
|
+
|
|
53
|
+
@property
|
|
54
|
+
def critical_count(self) -> int:
|
|
55
|
+
return sum(1 for f in self.findings if f.severity == Severity.CRITICAL)
|
|
56
|
+
|
|
57
|
+
@property
|
|
58
|
+
def high_count(self) -> int:
|
|
59
|
+
return sum(1 for f in self.findings if f.severity == Severity.HIGH)
|
|
60
|
+
|
|
61
|
+
def filter_by_confidence(self, min_confidence: float = 0.5) -> list[Finding]:
|
|
62
|
+
"""Filter findings by minimum confidence level."""
|
|
63
|
+
return [f for f in self.findings if f.confidence >= min_confidence]
|
|
64
|
+
|
|
65
|
+
def format_report(self, *, min_confidence: float = 0.5) -> str:
|
|
66
|
+
"""Format findings as a human-readable report."""
|
|
67
|
+
filtered = self.filter_by_confidence(min_confidence)
|
|
68
|
+
if not filtered:
|
|
69
|
+
return f"No findings above {min_confidence:.0%} confidence ({self.files_scanned} files scanned)."
|
|
70
|
+
|
|
71
|
+
lines = [
|
|
72
|
+
f"Bug Report: {len(filtered)} findings ({self.files_scanned} files, {self.lines_analyzed} lines)",
|
|
73
|
+
"",
|
|
74
|
+
]
|
|
75
|
+
|
|
76
|
+
# Group by severity
|
|
77
|
+
for severity in (Severity.CRITICAL, Severity.HIGH, Severity.MEDIUM, Severity.LOW):
|
|
78
|
+
group = [f for f in filtered if f.severity == severity]
|
|
79
|
+
if not group:
|
|
80
|
+
continue
|
|
81
|
+
lines.append(f"### {severity.value.upper()} ({len(group)})")
|
|
82
|
+
for finding in group:
|
|
83
|
+
confidence_pct = f"{finding.confidence:.0%}"
|
|
84
|
+
lines.append(
|
|
85
|
+
f" [{confidence_pct}] {finding.file}:{finding.line} — "
|
|
86
|
+
f"{finding.description}"
|
|
87
|
+
)
|
|
88
|
+
if finding.suggestion:
|
|
89
|
+
lines.append(f" Fix: {finding.suggestion}")
|
|
90
|
+
lines.append("")
|
|
91
|
+
|
|
92
|
+
return "\n".join(lines)
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
# --- Static pattern-based analysis ---
|
|
96
|
+
|
|
97
|
+
_PATTERNS: list[tuple[str, FindingCategory, Severity, str, float]] = [
|
|
98
|
+
# (regex, category, severity, description_template, confidence)
|
|
99
|
+
(r"except\s*:", FindingCategory.ERROR_HANDLING, Severity.MEDIUM,
|
|
100
|
+
"Bare except clause catches all exceptions including KeyboardInterrupt", 0.9),
|
|
101
|
+
(r"eval\s*\(", FindingCategory.SECURITY, Severity.HIGH,
|
|
102
|
+
"Use of eval() can execute arbitrary code", 0.85),
|
|
103
|
+
(r"exec\s*\(", FindingCategory.SECURITY, Severity.HIGH,
|
|
104
|
+
"Use of exec() can execute arbitrary code", 0.8),
|
|
105
|
+
(r"subprocess\.call\(.*shell\s*=\s*True", FindingCategory.SECURITY, Severity.HIGH,
|
|
106
|
+
"subprocess with shell=True is vulnerable to injection", 0.85),
|
|
107
|
+
(r"\.format\(.*\)\s*$", FindingCategory.SECURITY, Severity.LOW,
|
|
108
|
+
"Consider f-strings over .format() for clarity", 0.3),
|
|
109
|
+
(r"TODO|FIXME|HACK|XXX", FindingCategory.LOGIC_ERROR, Severity.INFO,
|
|
110
|
+
"Unresolved TODO/FIXME marker", 0.95),
|
|
111
|
+
(r"time\.sleep\(\d{2,}", FindingCategory.PERFORMANCE, Severity.MEDIUM,
|
|
112
|
+
"Long sleep duration may indicate polling pattern", 0.6),
|
|
113
|
+
(r"except\s+Exception\s+as\s+\w+:\s*\n\s*pass", FindingCategory.ERROR_HANDLING, Severity.MEDIUM,
|
|
114
|
+
"Exception silently swallowed", 0.75),
|
|
115
|
+
]
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def scan_text(
|
|
119
|
+
text: str,
|
|
120
|
+
file_path: str = "<unknown>",
|
|
121
|
+
*,
|
|
122
|
+
line_offset: int = 0,
|
|
123
|
+
) -> list[Finding]:
|
|
124
|
+
"""Scan text for common bug patterns.
|
|
125
|
+
|
|
126
|
+
Args:
|
|
127
|
+
text: Source code text.
|
|
128
|
+
file_path: File path for reporting.
|
|
129
|
+
line_offset: Line offset for diff-based scanning.
|
|
130
|
+
|
|
131
|
+
Returns:
|
|
132
|
+
List of findings.
|
|
133
|
+
"""
|
|
134
|
+
findings: list[Finding] = []
|
|
135
|
+
lines = text.split("\n")
|
|
136
|
+
|
|
137
|
+
for i, line in enumerate(lines, start=1 + line_offset):
|
|
138
|
+
for pattern, category, severity, desc, confidence in _PATTERNS:
|
|
139
|
+
if re.search(pattern, line):
|
|
140
|
+
findings.append(Finding(
|
|
141
|
+
file=file_path,
|
|
142
|
+
line=i,
|
|
143
|
+
severity=severity,
|
|
144
|
+
category=category,
|
|
145
|
+
confidence=confidence,
|
|
146
|
+
description=desc,
|
|
147
|
+
code_snippet=line.strip()[:120],
|
|
148
|
+
))
|
|
149
|
+
|
|
150
|
+
return findings
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
_HUNK_RE = re.compile(r"^@@ -\d+(?:,\d+)? \+(\d+)")
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
def _added_lines(diff_text: str) -> list[tuple[str, list[tuple[int, str]]]]:
|
|
157
|
+
"""Added lines per file, numbered as they sit in the new file.
|
|
158
|
+
|
|
159
|
+
A hunk header sets the new-file line; context lines advance it, removed
|
|
160
|
+
lines do not. A diff without hunk headers numbers added lines from 1.
|
|
161
|
+
"""
|
|
162
|
+
files: list[tuple[str, list[tuple[int, str]]]] = []
|
|
163
|
+
lineno = 1
|
|
164
|
+
for line in diff_text.split("\n"):
|
|
165
|
+
if line.startswith("+++ b/"):
|
|
166
|
+
files.append((line[6:], []))
|
|
167
|
+
lineno = 1
|
|
168
|
+
elif not files:
|
|
169
|
+
continue
|
|
170
|
+
elif m := _HUNK_RE.match(line):
|
|
171
|
+
lineno = int(m.group(1))
|
|
172
|
+
elif line.startswith("+") and not line.startswith("+++"):
|
|
173
|
+
files[-1][1].append((lineno, line[1:]))
|
|
174
|
+
lineno += 1
|
|
175
|
+
elif line.startswith(" ") or not line: # context; some tools strip the space
|
|
176
|
+
lineno += 1
|
|
177
|
+
return [(path, added) for path, added in files if added]
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
def parse_diff_files(diff_text: str) -> list[tuple[str, str]]:
|
|
181
|
+
"""Parse a unified diff into (file_path, changed_content) pairs."""
|
|
182
|
+
return [(path, "\n".join(text for _, text in added)) for path, added in _added_lines(diff_text)]
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
def scan_diff(diff_text: str) -> BugReport:
|
|
186
|
+
"""Scan a unified diff for bug patterns.
|
|
187
|
+
|
|
188
|
+
Parses the diff, extracts added lines, and runs
|
|
189
|
+
pattern-based analysis on each changed file.
|
|
190
|
+
"""
|
|
191
|
+
files = _added_lines(diff_text)
|
|
192
|
+
all_findings: list[Finding] = []
|
|
193
|
+
total_lines = 0
|
|
194
|
+
|
|
195
|
+
for file_path, added in files:
|
|
196
|
+
if not file_path.endswith((".py", ".js", ".ts", ".go", ".rs", ".java")):
|
|
197
|
+
continue
|
|
198
|
+
for lineno, text in added:
|
|
199
|
+
all_findings.extend(scan_text(text, file_path, line_offset=lineno - 1))
|
|
200
|
+
total_lines += len(added)
|
|
201
|
+
|
|
202
|
+
return BugReport(
|
|
203
|
+
findings=all_findings,
|
|
204
|
+
files_scanned=len(files),
|
|
205
|
+
lines_analyzed=total_lines,
|
|
206
|
+
)
|
attocode_intel/bundle.py
ADDED
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
"""Portable local artifact bundles for code-intel state."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
import tarfile
|
|
7
|
+
from datetime import UTC, datetime
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
from tempfile import TemporaryDirectory
|
|
10
|
+
|
|
11
|
+
from attocode_intel import __version__
|
|
12
|
+
from attocode_intel.artifacts.hashing import sha256_file
|
|
13
|
+
|
|
14
|
+
_SCHEMA_VERSION = 1
|
|
15
|
+
_ARTIFACTS = (
|
|
16
|
+
("artifacts/index/symbols.db", ".attocode/index/symbols.db"),
|
|
17
|
+
("artifacts/vectors/embeddings.db", ".attocode/vectors/embeddings.db"),
|
|
18
|
+
("artifacts/cache/memory.db", ".attocode/cache/memory.db"),
|
|
19
|
+
("artifacts/adrs.db", ".attocode/adrs.db"),
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def _metadata(project_dir: Path) -> dict[str, object]:
|
|
24
|
+
artifacts: list[dict[str, object]] = []
|
|
25
|
+
for bundle_path, rel_path in _ARTIFACTS:
|
|
26
|
+
source = project_dir / rel_path
|
|
27
|
+
present = source.exists()
|
|
28
|
+
artifacts.append({
|
|
29
|
+
"path": bundle_path,
|
|
30
|
+
"present": present,
|
|
31
|
+
"size_bytes": source.stat().st_size if present else 0,
|
|
32
|
+
"sha256": sha256_file(source) if present else None,
|
|
33
|
+
})
|
|
34
|
+
|
|
35
|
+
return {
|
|
36
|
+
"schema_version": _SCHEMA_VERSION,
|
|
37
|
+
"created_at": datetime.now(UTC).isoformat().replace("+00:00", "Z"),
|
|
38
|
+
"project_name": project_dir.name,
|
|
39
|
+
"project_root_basename": project_dir.name,
|
|
40
|
+
"attocode_version": __version__,
|
|
41
|
+
"artifacts": artifacts,
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def export_bundle(project_dir: str, output_path: str) -> Path:
|
|
46
|
+
"""Export local code-intel artifacts into a tar.gz bundle."""
|
|
47
|
+
project_root = Path(project_dir).resolve()
|
|
48
|
+
destination = Path(output_path).resolve()
|
|
49
|
+
destination.parent.mkdir(parents=True, exist_ok=True)
|
|
50
|
+
metadata = _metadata(project_root)
|
|
51
|
+
|
|
52
|
+
with TemporaryDirectory(prefix="attocode-bundle-") as tmpdir:
|
|
53
|
+
root = Path(tmpdir) / "attocode-bundle"
|
|
54
|
+
root.mkdir(parents=True, exist_ok=True)
|
|
55
|
+
metadata_path = root / "metadata.json"
|
|
56
|
+
metadata_path.write_text(json.dumps(metadata, indent=2) + "\n", encoding="utf-8")
|
|
57
|
+
|
|
58
|
+
for bundle_path, rel_path in _ARTIFACTS:
|
|
59
|
+
source = project_root / rel_path
|
|
60
|
+
if not source.exists():
|
|
61
|
+
continue
|
|
62
|
+
target = root / bundle_path
|
|
63
|
+
target.parent.mkdir(parents=True, exist_ok=True)
|
|
64
|
+
target.write_bytes(source.read_bytes())
|
|
65
|
+
|
|
66
|
+
with tarfile.open(destination, "w:gz") as archive:
|
|
67
|
+
archive.add(root, arcname="attocode-bundle")
|
|
68
|
+
|
|
69
|
+
return destination
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def inspect_bundle(bundle_path: str) -> dict[str, object]:
|
|
73
|
+
"""Read bundle metadata without touching local project state."""
|
|
74
|
+
bundle = Path(bundle_path).resolve()
|
|
75
|
+
with tarfile.open(bundle, "r:gz") as archive:
|
|
76
|
+
metadata_member = archive.getmember("attocode-bundle/metadata.json")
|
|
77
|
+
fh = archive.extractfile(metadata_member)
|
|
78
|
+
if fh is None:
|
|
79
|
+
raise FileNotFoundError("metadata.json missing from bundle")
|
|
80
|
+
return json.loads(fh.read().decode("utf-8"))
|