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
attocode_intel/redis.py
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
"""Redis connection management."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import logging
|
|
6
|
+
from typing import TYPE_CHECKING
|
|
7
|
+
|
|
8
|
+
if TYPE_CHECKING:
|
|
9
|
+
from redis.asyncio import Redis
|
|
10
|
+
|
|
11
|
+
logger = logging.getLogger(__name__)
|
|
12
|
+
|
|
13
|
+
_redis: Redis | None = None
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
async def get_redis() -> Redis:
|
|
17
|
+
"""Get or create the Redis connection singleton."""
|
|
18
|
+
global _redis
|
|
19
|
+
if _redis is not None:
|
|
20
|
+
return _redis
|
|
21
|
+
|
|
22
|
+
from attocode_intel.api.deps import get_config
|
|
23
|
+
|
|
24
|
+
config = get_config()
|
|
25
|
+
if not config.redis_url:
|
|
26
|
+
raise RuntimeError("REDIS_URL not configured")
|
|
27
|
+
|
|
28
|
+
from redis.asyncio import Redis as AsyncRedis
|
|
29
|
+
|
|
30
|
+
_redis = AsyncRedis.from_url(config.redis_url, decode_responses=True)
|
|
31
|
+
logger.info("Redis connection initialized")
|
|
32
|
+
return _redis
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
async def close_redis() -> None:
|
|
36
|
+
"""Close the Redis connection."""
|
|
37
|
+
global _redis
|
|
38
|
+
if _redis is not None:
|
|
39
|
+
await _redis.aclose()
|
|
40
|
+
_redis = None
|
|
41
|
+
logger.info("Redis connection closed")
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
async def health_check() -> bool:
|
|
45
|
+
"""Check if Redis is reachable."""
|
|
46
|
+
try:
|
|
47
|
+
r = await get_redis()
|
|
48
|
+
await r.ping()
|
|
49
|
+
return True
|
|
50
|
+
except Exception:
|
|
51
|
+
return False
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
"""Workspace-scoped reference continuations over immutable result snapshots."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import json
|
|
5
|
+
import secrets
|
|
6
|
+
from collections import OrderedDict
|
|
7
|
+
from copy import deepcopy
|
|
8
|
+
|
|
9
|
+
from attocode_intel.output import bounded_compact, compact_result
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def scope(context, args):
|
|
13
|
+
tracker = context.stores.get("freshness")
|
|
14
|
+
return (context.workspace, context.source, context.revision, getattr(tracker, "generation", 0),
|
|
15
|
+
args["symbol_name"], args.get("file_path"), args.get("line"))
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def continuation(context, args):
|
|
19
|
+
cursor = args.get("cursor")
|
|
20
|
+
if not cursor:
|
|
21
|
+
return None
|
|
22
|
+
pages = context.stores.get("reference_pages", {})
|
|
23
|
+
record = pages.get(cursor)
|
|
24
|
+
if record is None or record["scope"] != scope(context, args):
|
|
25
|
+
raise ValueError("Reference cursor expired or belongs to another query; restart without cursor")
|
|
26
|
+
return record
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def paginate(context, args, payload, metadata, budget, compact):
|
|
30
|
+
size = args.get("page_size") if args.get("page_size") is not None else 20
|
|
31
|
+
if not 1 <= size <= 100:
|
|
32
|
+
raise ValueError("page_size must be between 1 and 100")
|
|
33
|
+
record = continuation(context, args)
|
|
34
|
+
offset = record["offset"] if record else 0
|
|
35
|
+
if record:
|
|
36
|
+
payload, metadata = record["payload"], record["metadata"]
|
|
37
|
+
rows = payload["references"]
|
|
38
|
+
token = secrets.token_hex(16)
|
|
39
|
+
page = {**payload, "references": rows[offset:offset + size],
|
|
40
|
+
"next_cursor": token, "total_references": len(rows)}
|
|
41
|
+
metadata = {**metadata, "truncated": offset + size < len(rows)}
|
|
42
|
+
if compact:
|
|
43
|
+
rendered = json.loads(bounded_compact(metadata, page, budget).content[0].text)
|
|
44
|
+
page = rendered["data"]
|
|
45
|
+
compact_meta = rendered["metadata"]
|
|
46
|
+
count = len(page["references"])
|
|
47
|
+
if count == 0 and offset < len(rows):
|
|
48
|
+
raise ValueError("max_tokens is too small for one reference and provenance; increase it")
|
|
49
|
+
more = offset + count < len(rows)
|
|
50
|
+
page["next_cursor"] = token if more else None
|
|
51
|
+
if more:
|
|
52
|
+
pages = context.stores.setdefault("reference_pages", OrderedDict())
|
|
53
|
+
pages[token] = {"scope": scope(context, args), "offset": offset + count,
|
|
54
|
+
"payload": payload, "metadata": metadata}
|
|
55
|
+
while len(pages) > 64:
|
|
56
|
+
pages.popitem(last=False)
|
|
57
|
+
if compact:
|
|
58
|
+
compact_meta["truncated"] = compact_meta.get("truncated", False) or more
|
|
59
|
+
return compact_result(compact_meta, page)
|
|
60
|
+
return deepcopy(page), metadata
|
attocode_intel/remote.py
ADDED
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
"""Authenticated repository selection and immutable Git snapshots."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import asyncio
|
|
6
|
+
import subprocess
|
|
7
|
+
import uuid
|
|
8
|
+
from contextvars import ContextVar
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
|
|
11
|
+
from filelock import FileLock
|
|
12
|
+
|
|
13
|
+
from attocode_intel.catalog import is_write, remote_available
|
|
14
|
+
from attocode_intel.request_context import RequestContext
|
|
15
|
+
|
|
16
|
+
remote_workspace: ContextVar[str] = ContextVar("remote_workspace", default="")
|
|
17
|
+
remote_profile: ContextVar[str] = ContextVar("remote_profile", default="full")
|
|
18
|
+
|
|
19
|
+
remote_identity: ContextVar[object | None] = ContextVar("intelligence_identity", default=None)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
async def authorized_repo(repo_id: str, auth, *, write: bool = False):
|
|
23
|
+
from fastapi import HTTPException
|
|
24
|
+
from sqlalchemy import select
|
|
25
|
+
|
|
26
|
+
from attocode_intel.db.engine import get_session
|
|
27
|
+
from attocode_intel.db.models import OrgMembership, Repository
|
|
28
|
+
|
|
29
|
+
try:
|
|
30
|
+
rid = uuid.UUID(repo_id)
|
|
31
|
+
except ValueError as exc:
|
|
32
|
+
raise HTTPException(
|
|
33
|
+
404, "Repository not found; supply workspace as a repository ID"
|
|
34
|
+
) from exc
|
|
35
|
+
if auth is None or auth.auth_method == "legacy":
|
|
36
|
+
raise HTTPException(401, "Authentication required")
|
|
37
|
+
required = "write" if write else "read"
|
|
38
|
+
if (
|
|
39
|
+
auth.auth_method == "api_key"
|
|
40
|
+
and required not in auth.scopes
|
|
41
|
+
and f"intelligence:{required}" not in auth.scopes
|
|
42
|
+
):
|
|
43
|
+
raise HTTPException(403, f"API key requires intelligence:{required} scope")
|
|
44
|
+
async for session in get_session():
|
|
45
|
+
repo = (
|
|
46
|
+
await session.execute(select(Repository).where(Repository.id == rid))
|
|
47
|
+
).scalar_one_or_none()
|
|
48
|
+
if repo is None or (auth.org_id and auth.org_id != repo.org_id):
|
|
49
|
+
raise HTTPException(404, "Repository not found")
|
|
50
|
+
if auth.user_id:
|
|
51
|
+
membership = (
|
|
52
|
+
await session.execute(
|
|
53
|
+
select(OrgMembership).where(
|
|
54
|
+
OrgMembership.org_id == repo.org_id,
|
|
55
|
+
OrgMembership.user_id == auth.user_id,
|
|
56
|
+
OrgMembership.accepted_at.is_not(None),
|
|
57
|
+
)
|
|
58
|
+
)
|
|
59
|
+
).scalar_one_or_none()
|
|
60
|
+
if membership is None:
|
|
61
|
+
raise HTTPException(404, "Repository not found")
|
|
62
|
+
elif auth.auth_method != "api_key" or auth.org_id != repo.org_id:
|
|
63
|
+
raise HTTPException(404, "Repository not found")
|
|
64
|
+
return repo
|
|
65
|
+
raise HTTPException(503, "Repository database unavailable")
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def committed_snapshot(
|
|
69
|
+
repo_path: str, cache_root: str, repo_id: str, revision: str
|
|
70
|
+
) -> tuple[str, str]:
|
|
71
|
+
"""Create a detached worktree; never change the source checkout or upload edits."""
|
|
72
|
+
|
|
73
|
+
def git(*args):
|
|
74
|
+
result = subprocess.run(
|
|
75
|
+
[
|
|
76
|
+
"git",
|
|
77
|
+
"-c",
|
|
78
|
+
"core.hooksPath=/dev/null",
|
|
79
|
+
"-c",
|
|
80
|
+
"core.fsmonitor=false",
|
|
81
|
+
"-C",
|
|
82
|
+
repo_path,
|
|
83
|
+
*args,
|
|
84
|
+
],
|
|
85
|
+
capture_output=True,
|
|
86
|
+
text=True,
|
|
87
|
+
timeout=90,
|
|
88
|
+
check=True,
|
|
89
|
+
)
|
|
90
|
+
return result.stdout.strip()
|
|
91
|
+
|
|
92
|
+
commit = git("rev-parse", "--verify", "--end-of-options", f"{revision}^{{commit}}")
|
|
93
|
+
base = Path(cache_root) / ".intelligence-snapshots" / repo_id
|
|
94
|
+
base.mkdir(parents=True, exist_ok=True)
|
|
95
|
+
target = base / commit
|
|
96
|
+
with FileLock(str(base / "snapshots.lock"), timeout=90):
|
|
97
|
+
if not (target / ".git").exists():
|
|
98
|
+
git("worktree", "add", "--detach", str(target), commit)
|
|
99
|
+
# Prevent repository symlinks from exposing files outside the snapshot.
|
|
100
|
+
for path in target.rglob("*"):
|
|
101
|
+
if path.is_symlink() and not path.resolve().is_relative_to(target.resolve()):
|
|
102
|
+
raise ValueError(
|
|
103
|
+
f"Snapshot contains an external symlink: {path.relative_to(target)}"
|
|
104
|
+
)
|
|
105
|
+
return str(target.resolve()), commit
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
class RemoteResolver:
|
|
109
|
+
def __init__(self, config):
|
|
110
|
+
self.config = config
|
|
111
|
+
|
|
112
|
+
async def __call__(self, workspace, revision, name):
|
|
113
|
+
workspace = workspace or remote_workspace.get()
|
|
114
|
+
auth = remote_identity.get()
|
|
115
|
+
write = is_write(name)
|
|
116
|
+
repo = await authorized_repo(workspace, auth, write=write)
|
|
117
|
+
if not remote_available(name):
|
|
118
|
+
raise ValueError(
|
|
119
|
+
"This operation is local-only; remote source snapshots cannot be modified."
|
|
120
|
+
)
|
|
121
|
+
path = repo.clone_path or repo.local_path
|
|
122
|
+
if not path:
|
|
123
|
+
raise ValueError("Repository has not been cloned or indexed yet")
|
|
124
|
+
path, commit = await asyncio.to_thread(
|
|
125
|
+
committed_snapshot,
|
|
126
|
+
path,
|
|
127
|
+
self.config.git_clone_dir,
|
|
128
|
+
str(repo.id),
|
|
129
|
+
revision or repo.default_branch,
|
|
130
|
+
)
|
|
131
|
+
return RequestContext(path, str(repo.id), commit, "remote", auth)
|
|
@@ -0,0 +1,246 @@
|
|
|
1
|
+
"""Graph-ranked repo map using PageRank.
|
|
2
|
+
|
|
3
|
+
Ranks files and symbols by their connectivity in the
|
|
4
|
+
dependency graph, weighted by task relevance. Produces
|
|
5
|
+
a token-budgeted summary of the most important code.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
from dataclasses import dataclass, field
|
|
11
|
+
|
|
12
|
+
from attocode_intel.focused_evidence import task_terms
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@dataclass(slots=True)
|
|
16
|
+
class RankedEntry:
|
|
17
|
+
"""A ranked file or symbol in the repo map."""
|
|
18
|
+
path: str
|
|
19
|
+
score: float
|
|
20
|
+
symbols: list[str] = field(default_factory=list)
|
|
21
|
+
line_count: int = 0
|
|
22
|
+
category: str = "" # "core", "util", "test", etc.
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
@dataclass(slots=True)
|
|
26
|
+
class RepoMapResult:
|
|
27
|
+
"""Result of repo map ranking."""
|
|
28
|
+
entries: list[RankedEntry]
|
|
29
|
+
total_files: int
|
|
30
|
+
token_budget: int
|
|
31
|
+
tokens_used: int
|
|
32
|
+
truncated: bool = False
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def pagerank(
|
|
36
|
+
adjacency: dict[str, list[str]],
|
|
37
|
+
*,
|
|
38
|
+
damping: float = 0.85,
|
|
39
|
+
iterations: int = 30,
|
|
40
|
+
tolerance: float = 1e-6,
|
|
41
|
+
) -> dict[str, float]:
|
|
42
|
+
"""Compute PageRank scores for nodes in a directed graph.
|
|
43
|
+
|
|
44
|
+
Args:
|
|
45
|
+
adjacency: Maps each node to its outgoing edges (dependencies).
|
|
46
|
+
damping: Damping factor (probability of following a link).
|
|
47
|
+
iterations: Maximum iterations.
|
|
48
|
+
tolerance: Convergence threshold.
|
|
49
|
+
|
|
50
|
+
Returns:
|
|
51
|
+
Dict mapping node -> PageRank score.
|
|
52
|
+
"""
|
|
53
|
+
nodes = set(adjacency.keys())
|
|
54
|
+
for targets in adjacency.values():
|
|
55
|
+
nodes.update(targets)
|
|
56
|
+
|
|
57
|
+
if not nodes:
|
|
58
|
+
return {}
|
|
59
|
+
|
|
60
|
+
n = len(nodes)
|
|
61
|
+
node_list = sorted(nodes)
|
|
62
|
+
node_idx = {node: i for i, node in enumerate(node_list)}
|
|
63
|
+
|
|
64
|
+
# Build reverse adjacency (who points to me?)
|
|
65
|
+
in_links: dict[int, list[int]] = {i: [] for i in range(n)}
|
|
66
|
+
out_degree: dict[int, int] = {i: 0 for i in range(n)}
|
|
67
|
+
|
|
68
|
+
for source, targets in adjacency.items():
|
|
69
|
+
src_idx = node_idx[source]
|
|
70
|
+
valid_targets = [t for t in targets if t in node_idx]
|
|
71
|
+
out_degree[src_idx] = len(valid_targets)
|
|
72
|
+
for target in valid_targets:
|
|
73
|
+
in_links[node_idx[target]].append(src_idx)
|
|
74
|
+
|
|
75
|
+
# Initialize uniform
|
|
76
|
+
scores = [1.0 / n] * n
|
|
77
|
+
base = (1.0 - damping) / n
|
|
78
|
+
|
|
79
|
+
# Identify dangling nodes (no outgoing edges)
|
|
80
|
+
dangling = [i for i in range(n) if out_degree[i] == 0]
|
|
81
|
+
|
|
82
|
+
for _ in range(iterations):
|
|
83
|
+
# Redistribute dangling node rank uniformly
|
|
84
|
+
dangling_sum = sum(scores[i] for i in dangling)
|
|
85
|
+
new_scores = [0.0] * n
|
|
86
|
+
for i in range(n):
|
|
87
|
+
rank_sum = sum(
|
|
88
|
+
scores[j] / out_degree[j]
|
|
89
|
+
for j in in_links[i]
|
|
90
|
+
if out_degree[j] > 0
|
|
91
|
+
)
|
|
92
|
+
new_scores[i] = base + damping * (rank_sum + dangling_sum / n)
|
|
93
|
+
|
|
94
|
+
# Check convergence
|
|
95
|
+
diff = sum(abs(new_scores[i] - scores[i]) for i in range(n))
|
|
96
|
+
scores = new_scores
|
|
97
|
+
if diff < tolerance:
|
|
98
|
+
break
|
|
99
|
+
|
|
100
|
+
return {node_list[i]: scores[i] for i in range(n)}
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def _task_relevance(path: str, task_keywords: set[str], excluded_keywords: set[str]) -> float:
|
|
104
|
+
"""Compute task relevance score for a file path."""
|
|
105
|
+
if not task_keywords:
|
|
106
|
+
return 1.0
|
|
107
|
+
path_lower = path.lower()
|
|
108
|
+
matches = sum(1 for kw in task_keywords if kw.lower() in path_lower)
|
|
109
|
+
relevance = (0.1 if matches == 0
|
|
110
|
+
else min(1.0, 0.3 + 0.7 * (matches / len(task_keywords))))
|
|
111
|
+
if any(kw in path_lower for kw in excluded_keywords):
|
|
112
|
+
relevance *= 0.2 # An explicit exclusion is a hint, not a hard filter.
|
|
113
|
+
return relevance
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def _estimate_entry_tokens(entry: RankedEntry) -> int:
|
|
117
|
+
"""Estimate tokens for a ranked entry in the output."""
|
|
118
|
+
# path + symbols list
|
|
119
|
+
tokens = len(entry.path.split("/")) + 2
|
|
120
|
+
tokens += sum(len(s.split()) + 1 for s in entry.symbols)
|
|
121
|
+
return max(tokens, 3)
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def _categorize_path(path: str) -> str:
|
|
125
|
+
"""Categorize a file path."""
|
|
126
|
+
parts = path.lower().split("/")
|
|
127
|
+
if any(p in ("test", "tests", "spec", "specs") for p in parts):
|
|
128
|
+
return "test"
|
|
129
|
+
if any(p in ("util", "utils", "helpers", "lib") for p in parts):
|
|
130
|
+
return "util"
|
|
131
|
+
if any(p in ("core", "engine", "kernel") for p in parts):
|
|
132
|
+
return "core"
|
|
133
|
+
if any(p in ("api", "routes", "handlers", "views") for p in parts):
|
|
134
|
+
return "api"
|
|
135
|
+
if any(p in ("config", "settings", "conf") for p in parts):
|
|
136
|
+
return "config"
|
|
137
|
+
return "module"
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
def rank_repo_files(
|
|
141
|
+
adjacency: dict[str, list[str]],
|
|
142
|
+
*,
|
|
143
|
+
task_context: str = "",
|
|
144
|
+
token_budget: int = 1024,
|
|
145
|
+
symbols_by_file: dict[str, list[str]] | None = None,
|
|
146
|
+
line_counts: dict[str, int] | None = None,
|
|
147
|
+
exclude_tests: bool = True,
|
|
148
|
+
relevance_by_file: dict[str, float] | None = None,
|
|
149
|
+
) -> RepoMapResult:
|
|
150
|
+
"""Rank repository files by graph importance and task relevance.
|
|
151
|
+
|
|
152
|
+
Combines PageRank connectivity score with task-context keyword
|
|
153
|
+
relevance to produce a token-budgeted list of the most important
|
|
154
|
+
files and their key symbols.
|
|
155
|
+
|
|
156
|
+
Args:
|
|
157
|
+
adjacency: File dependency graph (file -> [imported files]).
|
|
158
|
+
task_context: Description of the current task for relevance scoring.
|
|
159
|
+
token_budget: Maximum tokens for the output.
|
|
160
|
+
symbols_by_file: Optional mapping of file -> symbol names.
|
|
161
|
+
line_counts: Optional mapping of file -> line count.
|
|
162
|
+
exclude_tests: Whether to exclude test files from ranking.
|
|
163
|
+
|
|
164
|
+
Returns:
|
|
165
|
+
RepoMapResult with ranked entries within token budget.
|
|
166
|
+
"""
|
|
167
|
+
symbols_by_file = symbols_by_file or {}
|
|
168
|
+
line_counts = line_counts or {}
|
|
169
|
+
|
|
170
|
+
# Compute PageRank
|
|
171
|
+
pr_scores = pagerank(adjacency)
|
|
172
|
+
|
|
173
|
+
# Query-conditioned source evidence is stronger than path words, but graph
|
|
174
|
+
# importance still supplies a useful fallback for files outside the pool.
|
|
175
|
+
task_keywords, excluded_keywords = task_terms(task_context)
|
|
176
|
+
relevance_by_file = relevance_by_file or {}
|
|
177
|
+
max_pr = max(pr_scores.values(), default=1.0) or 1.0
|
|
178
|
+
max_relevance = max(relevance_by_file.values(), default=1.0) or 1.0
|
|
179
|
+
|
|
180
|
+
# Score each file
|
|
181
|
+
scored: list[tuple[str, float]] = []
|
|
182
|
+
for path, pr_score in pr_scores.items():
|
|
183
|
+
if exclude_tests and _categorize_path(path) == "test":
|
|
184
|
+
continue
|
|
185
|
+
relevance = _task_relevance(path, task_keywords, excluded_keywords)
|
|
186
|
+
if task_context and relevance_by_file:
|
|
187
|
+
source_relevance = relevance_by_file.get(path, 0.0) / max_relevance
|
|
188
|
+
combined = 0.70 * source_relevance + 0.30 * (pr_score / max_pr) * relevance
|
|
189
|
+
else:
|
|
190
|
+
combined = pr_score * relevance
|
|
191
|
+
scored.append((path, combined))
|
|
192
|
+
|
|
193
|
+
# Sort by combined score descending
|
|
194
|
+
scored.sort(key=lambda x: x[1], reverse=True)
|
|
195
|
+
|
|
196
|
+
# Build entries within token budget
|
|
197
|
+
entries: list[RankedEntry] = []
|
|
198
|
+
tokens_used = 0
|
|
199
|
+
truncated = False
|
|
200
|
+
|
|
201
|
+
for path, score in scored:
|
|
202
|
+
entry = RankedEntry(
|
|
203
|
+
path=path,
|
|
204
|
+
score=score,
|
|
205
|
+
symbols=symbols_by_file.get(path, [])[:10], # Cap symbols per file
|
|
206
|
+
line_count=line_counts.get(path, 0),
|
|
207
|
+
category=_categorize_path(path),
|
|
208
|
+
)
|
|
209
|
+
entry_tokens = _estimate_entry_tokens(entry)
|
|
210
|
+
if tokens_used + entry_tokens > token_budget:
|
|
211
|
+
truncated = True
|
|
212
|
+
break
|
|
213
|
+
entries.append(entry)
|
|
214
|
+
tokens_used += entry_tokens
|
|
215
|
+
|
|
216
|
+
return RepoMapResult(
|
|
217
|
+
entries=entries,
|
|
218
|
+
total_files=len(pr_scores),
|
|
219
|
+
token_budget=token_budget,
|
|
220
|
+
tokens_used=tokens_used,
|
|
221
|
+
truncated=truncated,
|
|
222
|
+
)
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
def format_repo_map(result: RepoMapResult) -> str:
|
|
226
|
+
"""Format a RepoMapResult as a human-readable string."""
|
|
227
|
+
if not result.entries:
|
|
228
|
+
return "No files ranked."
|
|
229
|
+
|
|
230
|
+
lines = [f"Repo map ({len(result.entries)}/{result.total_files} files, ~{result.tokens_used} tokens):"]
|
|
231
|
+
lines.append("")
|
|
232
|
+
|
|
233
|
+
for entry in result.entries:
|
|
234
|
+
score_pct = entry.score * 100
|
|
235
|
+
prefix = f" [{entry.category:>6}] {entry.path}"
|
|
236
|
+
if entry.symbols:
|
|
237
|
+
syms = ", ".join(entry.symbols[:5])
|
|
238
|
+
if len(entry.symbols) > 5:
|
|
239
|
+
syms += f" (+{len(entry.symbols) - 5})"
|
|
240
|
+
prefix += f" ({syms})"
|
|
241
|
+
lines.append(f"{prefix} [{score_pct:.1f}%]")
|
|
242
|
+
|
|
243
|
+
if result.truncated:
|
|
244
|
+
lines.append(f"\n ... {result.total_files - len(result.entries)} more files omitted (token budget)")
|
|
245
|
+
|
|
246
|
+
return "\n".join(lines)
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
"""Identity and workspace selection for one intelligence operation."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from contextlib import contextmanager
|
|
6
|
+
from contextvars import ContextVar
|
|
7
|
+
from dataclasses import dataclass, field
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
from typing import Any
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
@dataclass(frozen=True)
|
|
13
|
+
class RequestContext:
|
|
14
|
+
project_dir: str
|
|
15
|
+
workspace: str
|
|
16
|
+
revision: str = "working-tree"
|
|
17
|
+
source: str = "local"
|
|
18
|
+
identity: Any = None
|
|
19
|
+
stores: dict[str, Any] = field(default_factory=dict, compare=False)
|
|
20
|
+
|
|
21
|
+
@property
|
|
22
|
+
def service(self):
|
|
23
|
+
from attocode_intel.service import CodeIntelService
|
|
24
|
+
|
|
25
|
+
if "service" not in self.stores:
|
|
26
|
+
self.stores["service"] = CodeIntelService(self.project_dir)
|
|
27
|
+
return self.stores["service"]
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
current_request: ContextVar[RequestContext | None] = ContextVar(
|
|
31
|
+
"intelligence_request", default=None
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
@contextmanager
|
|
36
|
+
def bind_request(context: RequestContext):
|
|
37
|
+
token = current_request.set(context)
|
|
38
|
+
try:
|
|
39
|
+
yield context
|
|
40
|
+
finally:
|
|
41
|
+
current_request.reset(token)
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def scoped_store(name, factory):
|
|
45
|
+
context = current_request.get()
|
|
46
|
+
if context is None:
|
|
47
|
+
return None
|
|
48
|
+
if name not in context.stores:
|
|
49
|
+
context.stores[name] = factory(context.project_dir)
|
|
50
|
+
return context.stores[name]
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def resolve_workspace(workspace: str = "", default: str = "") -> str:
|
|
54
|
+
"""Explicit roots win; discovery recognizes .git files in worktrees.
|
|
55
|
+
|
|
56
|
+
Discovery never selects the home directory. ``~/.attocode`` holds
|
|
57
|
+
user-wide settings, and a server that took it as a project marker
|
|
58
|
+
indexed all of home. Use ``--project ~`` to select home on purpose.
|
|
59
|
+
"""
|
|
60
|
+
if workspace or default:
|
|
61
|
+
path = Path(workspace or default).expanduser().resolve()
|
|
62
|
+
if not path.is_dir():
|
|
63
|
+
raise ValueError(f"Workspace directory does not exist: {path}")
|
|
64
|
+
return str(path)
|
|
65
|
+
cwd = Path.cwd().resolve()
|
|
66
|
+
home = Path.home().resolve()
|
|
67
|
+
for path in (cwd, *cwd.parents):
|
|
68
|
+
if path != home and any(
|
|
69
|
+
(path / marker).exists()
|
|
70
|
+
for marker in (
|
|
71
|
+
".git",
|
|
72
|
+
".attocode",
|
|
73
|
+
"pyproject.toml",
|
|
74
|
+
"package.json",
|
|
75
|
+
"Cargo.toml",
|
|
76
|
+
"go.mod",
|
|
77
|
+
)
|
|
78
|
+
):
|
|
79
|
+
return str(path)
|
|
80
|
+
raise ValueError(
|
|
81
|
+
"No repository selected. Supply workspace or start with --project /path/to/repo."
|
|
82
|
+
)
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
"""Pluggable rule-based analysis engine.
|
|
2
|
+
|
|
3
|
+
Provides a unified rule model, registry, loader, executor, enricher,
|
|
4
|
+
and formatter for multi-language static analysis with language packs,
|
|
5
|
+
user plugins, and deterministic pre-filtering.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from attocode_intel.rules.model import (
|
|
9
|
+
AutoFix,
|
|
10
|
+
EnrichedFinding,
|
|
11
|
+
FewShotExample,
|
|
12
|
+
RuleCategory,
|
|
13
|
+
RuleSeverity,
|
|
14
|
+
RuleSource,
|
|
15
|
+
RuleTier,
|
|
16
|
+
UnifiedRule,
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
__all__ = [
|
|
20
|
+
"AutoFix",
|
|
21
|
+
"EnrichedFinding",
|
|
22
|
+
"FewShotExample",
|
|
23
|
+
"RuleCategory",
|
|
24
|
+
"RuleSeverity",
|
|
25
|
+
"RuleSource",
|
|
26
|
+
"RuleTier",
|
|
27
|
+
"UnifiedRule",
|
|
28
|
+
]
|