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,1106 @@
|
|
|
1
|
+
"""Service-mode providers — query Postgres DB directly."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import logging
|
|
6
|
+
import re
|
|
7
|
+
import uuid
|
|
8
|
+
from typing import TYPE_CHECKING
|
|
9
|
+
|
|
10
|
+
if TYPE_CHECKING:
|
|
11
|
+
from sqlalchemy.ext.asyncio import AsyncSession
|
|
12
|
+
|
|
13
|
+
from attocode_intel.api.deps import BranchContext
|
|
14
|
+
|
|
15
|
+
from attocode_intel.api.models import (
|
|
16
|
+
CodeChunkItem,
|
|
17
|
+
CommunityItem,
|
|
18
|
+
CommunityResponse,
|
|
19
|
+
ConventionsResponse,
|
|
20
|
+
ConventionStats,
|
|
21
|
+
CrossRefResponse,
|
|
22
|
+
DependencyGraphEdge,
|
|
23
|
+
DependencyGraphNode,
|
|
24
|
+
DependencyGraphResponse,
|
|
25
|
+
DependencyResponse,
|
|
26
|
+
FileAnalysisResponse,
|
|
27
|
+
FileMetricsItem,
|
|
28
|
+
HotspotsResponse,
|
|
29
|
+
ImpactAnalysisResponse,
|
|
30
|
+
ReferenceItem,
|
|
31
|
+
SearchResultItem,
|
|
32
|
+
SearchResultsResponse,
|
|
33
|
+
SecurityFinding,
|
|
34
|
+
SecurityScanResponse,
|
|
35
|
+
SymbolItem,
|
|
36
|
+
SymbolListResponse,
|
|
37
|
+
SymbolSearchResponse,
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
logger = logging.getLogger(__name__)
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
class DbAnalysisProvider:
|
|
44
|
+
"""DB-backed analysis provider for service mode."""
|
|
45
|
+
|
|
46
|
+
__slots__ = ("_project_id",)
|
|
47
|
+
|
|
48
|
+
def __init__(self, project_id: str) -> None:
|
|
49
|
+
self._project_id = project_id
|
|
50
|
+
|
|
51
|
+
async def symbols(self, path: str, branch: str) -> SymbolListResponse:
|
|
52
|
+
from sqlalchemy import select
|
|
53
|
+
|
|
54
|
+
from attocode_intel.api.deps import get_branch_context
|
|
55
|
+
from attocode_intel.db.engine import get_session
|
|
56
|
+
from attocode_intel.db.models import Symbol
|
|
57
|
+
|
|
58
|
+
async for session in get_session():
|
|
59
|
+
repo_id = uuid.UUID(self._project_id)
|
|
60
|
+
branch_ctx = await get_branch_context(repo_id, branch, session)
|
|
61
|
+
manifest = branch_ctx.manifest
|
|
62
|
+
|
|
63
|
+
if path:
|
|
64
|
+
content_sha = manifest.get(path)
|
|
65
|
+
if not content_sha:
|
|
66
|
+
return SymbolListResponse(path=path, symbols=[])
|
|
67
|
+
result = await session.execute(
|
|
68
|
+
select(Symbol).where(Symbol.content_sha == content_sha)
|
|
69
|
+
)
|
|
70
|
+
else:
|
|
71
|
+
shas = set(manifest.values())
|
|
72
|
+
if not shas:
|
|
73
|
+
return SymbolListResponse(path="", symbols=[])
|
|
74
|
+
result = await session.execute(
|
|
75
|
+
select(Symbol).where(Symbol.content_sha.in_(shas))
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
rows = result.scalars().all()
|
|
79
|
+
sha_to_path = branch_ctx.sha_to_path
|
|
80
|
+
items = [
|
|
81
|
+
SymbolItem(
|
|
82
|
+
name=s.name,
|
|
83
|
+
kind=s.kind,
|
|
84
|
+
file_path=sha_to_path.get(s.content_sha, ""),
|
|
85
|
+
start_line=s.line_start or 0,
|
|
86
|
+
end_line=s.line_end or 0,
|
|
87
|
+
signature=s.signature or "",
|
|
88
|
+
)
|
|
89
|
+
for s in rows
|
|
90
|
+
]
|
|
91
|
+
return SymbolListResponse(path=path, symbols=items)
|
|
92
|
+
|
|
93
|
+
return SymbolListResponse(path=path, symbols=[])
|
|
94
|
+
|
|
95
|
+
async def search_symbols(self, name: str, branch: str, directory: str = "") -> SymbolSearchResponse:
|
|
96
|
+
from sqlalchemy import select
|
|
97
|
+
|
|
98
|
+
from attocode_intel.api.deps import get_branch_context
|
|
99
|
+
from attocode_intel.db.engine import get_session
|
|
100
|
+
from attocode_intel.db.models import Symbol
|
|
101
|
+
|
|
102
|
+
async for session in get_session():
|
|
103
|
+
repo_id = uuid.UUID(self._project_id)
|
|
104
|
+
branch_ctx = await get_branch_context(repo_id, branch, session)
|
|
105
|
+
shas = branch_ctx.content_shas
|
|
106
|
+
if not shas:
|
|
107
|
+
return SymbolSearchResponse(query=name, definitions=[])
|
|
108
|
+
|
|
109
|
+
result = await session.execute(
|
|
110
|
+
select(Symbol)
|
|
111
|
+
.where(
|
|
112
|
+
Symbol.content_sha.in_(shas),
|
|
113
|
+
Symbol.name.ilike(f"%{name}%"),
|
|
114
|
+
)
|
|
115
|
+
.limit(100)
|
|
116
|
+
)
|
|
117
|
+
rows = result.scalars().all()
|
|
118
|
+
sha_to_path = branch_ctx.sha_to_path
|
|
119
|
+
items = [
|
|
120
|
+
SymbolItem(
|
|
121
|
+
name=s.name,
|
|
122
|
+
kind=s.kind,
|
|
123
|
+
file_path=sha_to_path.get(s.content_sha, ""),
|
|
124
|
+
start_line=s.line_start or 0,
|
|
125
|
+
end_line=s.line_end or 0,
|
|
126
|
+
signature=s.signature or "",
|
|
127
|
+
)
|
|
128
|
+
for s in rows
|
|
129
|
+
]
|
|
130
|
+
if directory:
|
|
131
|
+
items = [s for s in items if s.file_path.startswith(directory)]
|
|
132
|
+
return SymbolSearchResponse(query=name, definitions=items)
|
|
133
|
+
|
|
134
|
+
return SymbolSearchResponse(query=name, definitions=[])
|
|
135
|
+
|
|
136
|
+
async def cross_references(self, symbol_name: str, branch: str) -> CrossRefResponse:
|
|
137
|
+
from sqlalchemy import or_, select
|
|
138
|
+
|
|
139
|
+
from attocode_intel.api.deps import get_branch_context
|
|
140
|
+
from attocode_intel.db.engine import get_session
|
|
141
|
+
from attocode_intel.db.models import Symbol, SymbolReference
|
|
142
|
+
|
|
143
|
+
async for session in get_session():
|
|
144
|
+
repo_id = uuid.UUID(self._project_id)
|
|
145
|
+
branch_ctx = await get_branch_context(repo_id, branch, session)
|
|
146
|
+
content_shas = branch_ctx.content_shas
|
|
147
|
+
if not content_shas:
|
|
148
|
+
return CrossRefResponse(
|
|
149
|
+
symbol=symbol_name, definitions=[], references=[], total_references=0
|
|
150
|
+
)
|
|
151
|
+
|
|
152
|
+
result = await session.execute(
|
|
153
|
+
select(Symbol).where(Symbol.content_sha.in_(content_shas))
|
|
154
|
+
)
|
|
155
|
+
rows = result.scalars().all()
|
|
156
|
+
sha_to_path = branch_ctx.sha_to_path
|
|
157
|
+
|
|
158
|
+
definitions = [
|
|
159
|
+
SymbolItem(
|
|
160
|
+
name=s.name,
|
|
161
|
+
kind=s.kind,
|
|
162
|
+
file_path=sha_to_path.get(s.content_sha, ""),
|
|
163
|
+
start_line=s.line_start or 0,
|
|
164
|
+
end_line=s.line_end or 0,
|
|
165
|
+
signature=s.signature or "",
|
|
166
|
+
)
|
|
167
|
+
for s in rows
|
|
168
|
+
if s.name == symbol_name or s.name.endswith(f".{symbol_name}")
|
|
169
|
+
]
|
|
170
|
+
|
|
171
|
+
ref_result = await session.execute(
|
|
172
|
+
select(SymbolReference)
|
|
173
|
+
.where(
|
|
174
|
+
SymbolReference.content_sha.in_(content_shas),
|
|
175
|
+
or_(
|
|
176
|
+
SymbolReference.symbol_name == symbol_name,
|
|
177
|
+
SymbolReference.symbol_name.endswith(f".{symbol_name}"),
|
|
178
|
+
),
|
|
179
|
+
)
|
|
180
|
+
.limit(500)
|
|
181
|
+
)
|
|
182
|
+
ref_rows = ref_result.scalars().all()
|
|
183
|
+
references = [
|
|
184
|
+
ReferenceItem(
|
|
185
|
+
ref_kind=r.ref_kind,
|
|
186
|
+
file_path=sha_to_path.get(r.content_sha, ""),
|
|
187
|
+
line=r.line,
|
|
188
|
+
)
|
|
189
|
+
for r in ref_rows
|
|
190
|
+
]
|
|
191
|
+
|
|
192
|
+
return CrossRefResponse(
|
|
193
|
+
symbol=symbol_name,
|
|
194
|
+
definitions=definitions,
|
|
195
|
+
references=references,
|
|
196
|
+
total_references=len(references),
|
|
197
|
+
)
|
|
198
|
+
|
|
199
|
+
return CrossRefResponse(
|
|
200
|
+
symbol=symbol_name, definitions=[], references=[], total_references=0
|
|
201
|
+
)
|
|
202
|
+
|
|
203
|
+
async def impact_analysis(self, files: list[str], branch: str) -> ImpactAnalysisResponse:
|
|
204
|
+
from attocode_intel.api.deps import get_branch_context
|
|
205
|
+
from attocode_intel.api.models import ImpactLayer
|
|
206
|
+
from attocode_intel.db.engine import get_session
|
|
207
|
+
from attocode_intel.storage.dependency_store import DependencyStore
|
|
208
|
+
|
|
209
|
+
async for session in get_session():
|
|
210
|
+
repo_id = uuid.UUID(self._project_id)
|
|
211
|
+
branch_ctx = await get_branch_context(repo_id, branch, session)
|
|
212
|
+
manifest = branch_ctx.manifest
|
|
213
|
+
sha_to_path = branch_ctx.sha_to_path
|
|
214
|
+
dep_store = DependencyStore(session)
|
|
215
|
+
|
|
216
|
+
changed_shas: set[str] = set()
|
|
217
|
+
for f in files:
|
|
218
|
+
sha = manifest.get(f)
|
|
219
|
+
if sha:
|
|
220
|
+
changed_shas.add(sha)
|
|
221
|
+
|
|
222
|
+
visited: set[str] = set(changed_shas)
|
|
223
|
+
frontier = set(changed_shas)
|
|
224
|
+
layers: list[ImpactLayer] = []
|
|
225
|
+
current_depth = 0
|
|
226
|
+
while frontier:
|
|
227
|
+
current_depth += 1
|
|
228
|
+
next_frontier: set[str] = set()
|
|
229
|
+
for sha in frontier:
|
|
230
|
+
reverse = await dep_store.get_reverse(sha)
|
|
231
|
+
for r in reverse:
|
|
232
|
+
src = r["source_sha"]
|
|
233
|
+
if src not in visited and src in branch_ctx.content_shas:
|
|
234
|
+
visited.add(src)
|
|
235
|
+
next_frontier.add(src)
|
|
236
|
+
if next_frontier:
|
|
237
|
+
layer_files = sorted(
|
|
238
|
+
sha_to_path[sha]
|
|
239
|
+
for sha in next_frontier
|
|
240
|
+
if sha in sha_to_path
|
|
241
|
+
)
|
|
242
|
+
if layer_files:
|
|
243
|
+
layers.append(ImpactLayer(depth=current_depth, files=layer_files))
|
|
244
|
+
frontier = next_frontier
|
|
245
|
+
|
|
246
|
+
impacted_paths = sorted(
|
|
247
|
+
sha_to_path[sha]
|
|
248
|
+
for sha in visited - changed_shas
|
|
249
|
+
if sha in sha_to_path
|
|
250
|
+
)
|
|
251
|
+
|
|
252
|
+
return ImpactAnalysisResponse(
|
|
253
|
+
changed_files=files,
|
|
254
|
+
impacted_files=impacted_paths,
|
|
255
|
+
total_impacted=len(impacted_paths),
|
|
256
|
+
layers=layers,
|
|
257
|
+
)
|
|
258
|
+
|
|
259
|
+
return ImpactAnalysisResponse(changed_files=files, impacted_files=[], total_impacted=0)
|
|
260
|
+
|
|
261
|
+
async def dependency_graph(
|
|
262
|
+
self, start_file: str, depth: int, branch: str, directory: str = "",
|
|
263
|
+
) -> DependencyGraphResponse:
|
|
264
|
+
from attocode_intel.api.deps import get_branch_context
|
|
265
|
+
from attocode_intel.db.engine import get_session
|
|
266
|
+
from attocode_intel.storage.dependency_store import DependencyStore
|
|
267
|
+
|
|
268
|
+
async for session in get_session():
|
|
269
|
+
repo_id = uuid.UUID(self._project_id)
|
|
270
|
+
dep_store = DependencyStore(session)
|
|
271
|
+
branch_ctx = await get_branch_context(repo_id, branch, session)
|
|
272
|
+
raw = await dep_store.get_graph_for_branch(branch_ctx.branch_id)
|
|
273
|
+
|
|
274
|
+
raw_nodes = raw.get("nodes", [])
|
|
275
|
+
raw_edges = raw.get("edges", [])
|
|
276
|
+
|
|
277
|
+
# Fallback: compute edges on-the-fly from import extraction
|
|
278
|
+
# when the Dependency table is empty but we have files
|
|
279
|
+
if not raw_edges and raw_nodes:
|
|
280
|
+
raw_edges = await self._compute_import_edges(session, branch_ctx)
|
|
281
|
+
|
|
282
|
+
if start_file:
|
|
283
|
+
adj: dict[str, list[str]] = {}
|
|
284
|
+
for e in raw_edges:
|
|
285
|
+
adj.setdefault(e["source"], []).append(e["target"])
|
|
286
|
+
adj.setdefault(e["target"], []).append(e["source"])
|
|
287
|
+
|
|
288
|
+
# Resolve start_file: exact match or directory prefix
|
|
289
|
+
if start_file in adj:
|
|
290
|
+
seeds = {start_file}
|
|
291
|
+
else:
|
|
292
|
+
prefix = start_file.rstrip("/") + "/"
|
|
293
|
+
seeds = {n["path"] for n in raw_nodes if n["path"].startswith(prefix)}
|
|
294
|
+
|
|
295
|
+
if not seeds:
|
|
296
|
+
return DependencyGraphResponse(nodes=[], edges=[])
|
|
297
|
+
|
|
298
|
+
reachable: set[str] = set()
|
|
299
|
+
bfs_frontier = seeds
|
|
300
|
+
for _ in range(depth):
|
|
301
|
+
next_frontier: set[str] = set()
|
|
302
|
+
for f in bfs_frontier:
|
|
303
|
+
if f not in reachable:
|
|
304
|
+
reachable.add(f)
|
|
305
|
+
next_frontier.update(adj.get(f, []))
|
|
306
|
+
bfs_frontier = next_frontier - reachable
|
|
307
|
+
reachable.update(bfs_frontier)
|
|
308
|
+
|
|
309
|
+
raw_nodes = [n for n in raw_nodes if n["path"] in reachable]
|
|
310
|
+
raw_edges = [
|
|
311
|
+
e for e in raw_edges
|
|
312
|
+
if e["source"] in reachable and e["target"] in reachable
|
|
313
|
+
]
|
|
314
|
+
|
|
315
|
+
nodes = [
|
|
316
|
+
DependencyGraphNode(id=n["path"], label=n["path"], type="file")
|
|
317
|
+
for n in raw_nodes
|
|
318
|
+
]
|
|
319
|
+
edges = [
|
|
320
|
+
DependencyGraphEdge(
|
|
321
|
+
source=e["source"], target=e["target"],
|
|
322
|
+
type=e.get("type", "import"),
|
|
323
|
+
)
|
|
324
|
+
for e in raw_edges
|
|
325
|
+
]
|
|
326
|
+
|
|
327
|
+
# Filter by directory prefix if specified
|
|
328
|
+
if directory:
|
|
329
|
+
node_ids = {n.id for n in nodes if n.id.startswith(directory)}
|
|
330
|
+
nodes = [n for n in nodes if n.id in node_ids]
|
|
331
|
+
edges = [e for e in edges if e.source in node_ids and e.target in node_ids]
|
|
332
|
+
|
|
333
|
+
return DependencyGraphResponse(nodes=nodes, edges=edges)
|
|
334
|
+
|
|
335
|
+
return DependencyGraphResponse(nodes=[], edges=[])
|
|
336
|
+
|
|
337
|
+
async def _compute_import_edges(self, session: AsyncSession, branch_ctx: BranchContext) -> list[dict]:
|
|
338
|
+
"""Fallback: compute edges on-the-fly from import extraction when Dependency table is empty."""
|
|
339
|
+
import logging
|
|
340
|
+
|
|
341
|
+
from sqlalchemy import select
|
|
342
|
+
|
|
343
|
+
from attocode_intel.db.models import FileContent
|
|
344
|
+
from attocode_intel.indexing.full_indexer import _resolve_import_path
|
|
345
|
+
from attocode_intel.indexing.parser import detect_language, extract_imports
|
|
346
|
+
|
|
347
|
+
logger = logging.getLogger(__name__)
|
|
348
|
+
manifest = branch_ctx.manifest # path -> sha
|
|
349
|
+
known_paths = set(manifest.keys())
|
|
350
|
+
sha_to_path = {sha: path for path, sha in manifest.items()}
|
|
351
|
+
content_shas = list(set(manifest.values()))
|
|
352
|
+
|
|
353
|
+
if not content_shas:
|
|
354
|
+
return []
|
|
355
|
+
|
|
356
|
+
# Batch-fetch file content (limit to avoid memory issues)
|
|
357
|
+
MAX_FILES = 500
|
|
358
|
+
if len(content_shas) > MAX_FILES:
|
|
359
|
+
# Only process files matching start_file prefix if too many
|
|
360
|
+
content_shas = content_shas[:MAX_FILES]
|
|
361
|
+
|
|
362
|
+
result = await session.execute(
|
|
363
|
+
select(FileContent.sha256, FileContent.content).where(
|
|
364
|
+
FileContent.sha256.in_(content_shas)
|
|
365
|
+
)
|
|
366
|
+
)
|
|
367
|
+
rows = result.all()
|
|
368
|
+
|
|
369
|
+
edges: list[dict] = []
|
|
370
|
+
for sha, content in rows:
|
|
371
|
+
source_path = sha_to_path.get(sha)
|
|
372
|
+
if not source_path or content is None:
|
|
373
|
+
continue
|
|
374
|
+
language = detect_language(source_path)
|
|
375
|
+
if not language:
|
|
376
|
+
continue
|
|
377
|
+
try:
|
|
378
|
+
raw_content = content if isinstance(content, bytes) else content.encode("utf-8")
|
|
379
|
+
imports = extract_imports(raw_content, source_path, language)
|
|
380
|
+
for imp in imports:
|
|
381
|
+
target = _resolve_import_path(imp, source_path, known_paths, language)
|
|
382
|
+
if target and target != source_path:
|
|
383
|
+
edges.append({
|
|
384
|
+
"source": source_path,
|
|
385
|
+
"target": target,
|
|
386
|
+
"type": "import",
|
|
387
|
+
"weight": 1.0,
|
|
388
|
+
})
|
|
389
|
+
except Exception as e:
|
|
390
|
+
logger.debug("Import extraction failed for %s: %s", source_path, e)
|
|
391
|
+
|
|
392
|
+
logger.info("Computed %d import edges on-the-fly for %d files", len(edges), len(rows))
|
|
393
|
+
return edges
|
|
394
|
+
|
|
395
|
+
async def hotspots(self, branch: str, top_n: int) -> HotspotsResponse:
|
|
396
|
+
from sqlalchemy import func as sa_func
|
|
397
|
+
from sqlalchemy import select
|
|
398
|
+
|
|
399
|
+
from attocode_intel.api.deps import get_branch_context
|
|
400
|
+
from attocode_intel.db.engine import get_session
|
|
401
|
+
from attocode_intel.db.models import Dependency, FileContent, Symbol
|
|
402
|
+
|
|
403
|
+
async for session in get_session():
|
|
404
|
+
repo_id = uuid.UUID(self._project_id)
|
|
405
|
+
branch_ctx = await get_branch_context(repo_id, branch, session)
|
|
406
|
+
content_shas = branch_ctx.content_shas
|
|
407
|
+
sha_to_path = branch_ctx.sha_to_path
|
|
408
|
+
|
|
409
|
+
if not content_shas:
|
|
410
|
+
return HotspotsResponse(file_hotspots=[], function_hotspots=[], orphan_files=[])
|
|
411
|
+
|
|
412
|
+
sym_result = await session.execute(
|
|
413
|
+
select(Symbol.content_sha, sa_func.count())
|
|
414
|
+
.where(Symbol.content_sha.in_(content_shas))
|
|
415
|
+
.group_by(Symbol.content_sha)
|
|
416
|
+
)
|
|
417
|
+
sym_counts: dict[str, int] = dict(sym_result.all())
|
|
418
|
+
|
|
419
|
+
fan_out_result = await session.execute(
|
|
420
|
+
select(Dependency.source_sha, sa_func.count())
|
|
421
|
+
.where(Dependency.source_sha.in_(content_shas))
|
|
422
|
+
.group_by(Dependency.source_sha)
|
|
423
|
+
)
|
|
424
|
+
fan_out: dict[str, int] = dict(fan_out_result.all())
|
|
425
|
+
|
|
426
|
+
fan_in_result = await session.execute(
|
|
427
|
+
select(Dependency.target_sha, sa_func.count())
|
|
428
|
+
.where(Dependency.target_sha.in_(content_shas))
|
|
429
|
+
.group_by(Dependency.target_sha)
|
|
430
|
+
)
|
|
431
|
+
fan_in: dict[str, int] = dict(fan_in_result.all())
|
|
432
|
+
|
|
433
|
+
size_result = await session.execute(
|
|
434
|
+
select(FileContent.sha256, FileContent.size_bytes)
|
|
435
|
+
.where(FileContent.sha256.in_(content_shas))
|
|
436
|
+
)
|
|
437
|
+
file_sizes: dict[str, int] = dict(size_result.all())
|
|
438
|
+
|
|
439
|
+
items: list[FileMetricsItem] = []
|
|
440
|
+
for sha, path in sha_to_path.items():
|
|
441
|
+
sc = sym_counts.get(sha, 0)
|
|
442
|
+
fi = fan_in.get(sha, 0)
|
|
443
|
+
fo = fan_out.get(sha, 0)
|
|
444
|
+
size = file_sizes.get(sha, 0)
|
|
445
|
+
lines = max(size // 40, 1) if size > 0 else 0
|
|
446
|
+
max_sym = max(sym_counts.values()) if sym_counts else 1
|
|
447
|
+
max_conn = max(
|
|
448
|
+
(fi_ + fo_ for fi_, fo_ in zip(fan_in.values(), fan_out.values())),
|
|
449
|
+
default=1,
|
|
450
|
+
) or 1
|
|
451
|
+
max_size = max(file_sizes.values()) if file_sizes else 1
|
|
452
|
+
max_lines = max(max_size // 40, 1)
|
|
453
|
+
|
|
454
|
+
composite = (
|
|
455
|
+
0.3 * (sc / max(max_sym, 1))
|
|
456
|
+
+ 0.3 * ((fi + fo) / max(max_conn, 1))
|
|
457
|
+
+ 0.2 * (lines / max(max_lines, 1))
|
|
458
|
+
+ 0.2 * (size / max(max_size, 1))
|
|
459
|
+
)
|
|
460
|
+
items.append(FileMetricsItem(
|
|
461
|
+
path=path,
|
|
462
|
+
line_count=lines,
|
|
463
|
+
symbol_count=sc,
|
|
464
|
+
fan_in=fi,
|
|
465
|
+
fan_out=fo,
|
|
466
|
+
density=round(sc / max(lines, 1), 3),
|
|
467
|
+
composite=round(composite, 4),
|
|
468
|
+
))
|
|
469
|
+
|
|
470
|
+
items.sort(key=lambda x: x.composite, reverse=True)
|
|
471
|
+
return HotspotsResponse(
|
|
472
|
+
file_hotspots=items[:top_n],
|
|
473
|
+
function_hotspots=[],
|
|
474
|
+
orphan_files=[],
|
|
475
|
+
)
|
|
476
|
+
|
|
477
|
+
return HotspotsResponse(file_hotspots=[], function_hotspots=[], orphan_files=[])
|
|
478
|
+
|
|
479
|
+
async def dependencies(self, path: str, branch: str) -> DependencyResponse:
|
|
480
|
+
"""Falls back to CodeIntelService (no DB-specific implementation)."""
|
|
481
|
+
from attocode_intel.api.deps import get_service_or_404
|
|
482
|
+
|
|
483
|
+
svc = await get_service_or_404(self._project_id)
|
|
484
|
+
data = svc.dependencies_data(path)
|
|
485
|
+
return DependencyResponse(**data)
|
|
486
|
+
|
|
487
|
+
async def file_analysis(self, path: str, branch: str) -> FileAnalysisResponse:
|
|
488
|
+
"""Falls back to CodeIntelService (no DB-specific implementation)."""
|
|
489
|
+
from attocode_intel.api.deps import get_service_or_404
|
|
490
|
+
|
|
491
|
+
svc = await get_service_or_404(self._project_id)
|
|
492
|
+
data = svc.file_analysis_data(path)
|
|
493
|
+
return FileAnalysisResponse(
|
|
494
|
+
path=data["path"],
|
|
495
|
+
language=data["language"],
|
|
496
|
+
line_count=data["line_count"],
|
|
497
|
+
imports=data["imports"],
|
|
498
|
+
exports=data["exports"],
|
|
499
|
+
chunks=[CodeChunkItem(**c) for c in data["chunks"]],
|
|
500
|
+
)
|
|
501
|
+
|
|
502
|
+
async def conventions(
|
|
503
|
+
self, branch: str, sample_size: int, path: str,
|
|
504
|
+
) -> ConventionsResponse:
|
|
505
|
+
from sqlalchemy import select
|
|
506
|
+
|
|
507
|
+
from attocode_intel.api.deps import get_branch_context
|
|
508
|
+
from attocode_intel.db.engine import get_session
|
|
509
|
+
from attocode_intel.db.models import Symbol
|
|
510
|
+
|
|
511
|
+
snake_re = re.compile(r"^[a-z][a-z0-9_]*$")
|
|
512
|
+
camel_re = re.compile(r"^[a-z][a-zA-Z0-9]*$")
|
|
513
|
+
|
|
514
|
+
async for session in get_session():
|
|
515
|
+
repo_id = uuid.UUID(self._project_id)
|
|
516
|
+
branch_ctx = await get_branch_context(repo_id, branch, session)
|
|
517
|
+
manifest = branch_ctx.manifest
|
|
518
|
+
content_shas = branch_ctx.content_shas
|
|
519
|
+
|
|
520
|
+
if not content_shas:
|
|
521
|
+
return ConventionsResponse(sample_size=0, path=path, stats=ConventionStats())
|
|
522
|
+
|
|
523
|
+
if path:
|
|
524
|
+
filtered_shas = {sha for p, sha in manifest.items() if p.startswith(path)}
|
|
525
|
+
else:
|
|
526
|
+
filtered_shas = content_shas
|
|
527
|
+
|
|
528
|
+
if not filtered_shas:
|
|
529
|
+
return ConventionsResponse(sample_size=0, path=path, stats=ConventionStats())
|
|
530
|
+
|
|
531
|
+
result = await session.execute(
|
|
532
|
+
select(Symbol).where(Symbol.content_sha.in_(filtered_shas))
|
|
533
|
+
)
|
|
534
|
+
symbols = result.scalars().all()
|
|
535
|
+
|
|
536
|
+
total_functions = 0
|
|
537
|
+
total_classes = 0
|
|
538
|
+
snake_names = 0
|
|
539
|
+
camel_names = 0
|
|
540
|
+
typed_return = 0
|
|
541
|
+
async_count = 0
|
|
542
|
+
|
|
543
|
+
for s in symbols:
|
|
544
|
+
if s.kind in ("function", "method"):
|
|
545
|
+
total_functions += 1
|
|
546
|
+
if snake_re.match(s.name):
|
|
547
|
+
snake_names += 1
|
|
548
|
+
elif camel_re.match(s.name) and any(c.isupper() for c in s.name[1:]):
|
|
549
|
+
camel_names += 1
|
|
550
|
+
if s.signature and "->" in s.signature:
|
|
551
|
+
typed_return += 1
|
|
552
|
+
meta = s.metadata_ or {}
|
|
553
|
+
if meta.get("async"):
|
|
554
|
+
async_count += 1
|
|
555
|
+
elif s.kind == "class":
|
|
556
|
+
total_classes += 1
|
|
557
|
+
|
|
558
|
+
stats = ConventionStats(
|
|
559
|
+
total_functions=total_functions,
|
|
560
|
+
total_classes=total_classes,
|
|
561
|
+
snake_names=snake_names,
|
|
562
|
+
camel_names=camel_names,
|
|
563
|
+
typed_return=typed_return,
|
|
564
|
+
async_count=async_count,
|
|
565
|
+
)
|
|
566
|
+
return ConventionsResponse(
|
|
567
|
+
sample_size=len(filtered_shas),
|
|
568
|
+
path=path,
|
|
569
|
+
stats=stats,
|
|
570
|
+
)
|
|
571
|
+
|
|
572
|
+
return ConventionsResponse(sample_size=0, path=path, stats=ConventionStats())
|
|
573
|
+
|
|
574
|
+
|
|
575
|
+
class DbSearchProvider:
|
|
576
|
+
"""DB-backed search provider for service mode."""
|
|
577
|
+
|
|
578
|
+
__slots__ = ("_project_id",)
|
|
579
|
+
|
|
580
|
+
def __init__(self, project_id: str) -> None:
|
|
581
|
+
self._project_id = project_id
|
|
582
|
+
|
|
583
|
+
async def semantic_search(
|
|
584
|
+
self, query: str, top_k: int, file_filter: str, branch: str,
|
|
585
|
+
) -> SearchResultsResponse:
|
|
586
|
+
"""Run pgvector cosine similarity search."""
|
|
587
|
+
import uuid as _uuid
|
|
588
|
+
|
|
589
|
+
from attocode_intel.api.deps import get_branch_context, get_config
|
|
590
|
+
from attocode_intel.db.engine import get_session
|
|
591
|
+
from attocode_intel.storage.embedding_store import EmbeddingStore
|
|
592
|
+
from attocode_intel._internal.integrations.context.embeddings import create_embedding_provider
|
|
593
|
+
|
|
594
|
+
config = get_config()
|
|
595
|
+
provider = create_embedding_provider(config.embedding_model)
|
|
596
|
+
if provider.name == "none":
|
|
597
|
+
raise RuntimeError("No embedding provider available")
|
|
598
|
+
|
|
599
|
+
query_vectors = provider.embed([query])
|
|
600
|
+
query_vector = query_vectors[0]
|
|
601
|
+
|
|
602
|
+
async for session in get_session():
|
|
603
|
+
branch_ctx = await get_branch_context(
|
|
604
|
+
_uuid.UUID(self._project_id), branch, session,
|
|
605
|
+
)
|
|
606
|
+
|
|
607
|
+
store = EmbeddingStore(session)
|
|
608
|
+
results = await store.similarity_search(
|
|
609
|
+
branch_id=branch_ctx.branch_id,
|
|
610
|
+
query_vector=query_vector,
|
|
611
|
+
top_k=top_k,
|
|
612
|
+
model=provider.name,
|
|
613
|
+
file_filter=file_filter,
|
|
614
|
+
)
|
|
615
|
+
|
|
616
|
+
items = [
|
|
617
|
+
SearchResultItem(
|
|
618
|
+
file_path=r["file"],
|
|
619
|
+
score=r.get("score", 0.0),
|
|
620
|
+
snippet=r.get("chunk_text", ""),
|
|
621
|
+
)
|
|
622
|
+
for r in results
|
|
623
|
+
]
|
|
624
|
+
|
|
625
|
+
# Best-effort line number enrichment from symbols
|
|
626
|
+
query_words = [w for w in query.split() if len(w) >= 3]
|
|
627
|
+
if query_words:
|
|
628
|
+
from sqlalchemy import select as sa_select
|
|
629
|
+
|
|
630
|
+
from attocode_intel.db.models import BranchFile, Symbol
|
|
631
|
+
|
|
632
|
+
sha_by_file: dict[str, str] = {}
|
|
633
|
+
for r in results:
|
|
634
|
+
bf_result = await session.execute(
|
|
635
|
+
sa_select(BranchFile.content_sha).where(
|
|
636
|
+
BranchFile.branch_id == branch_ctx.branch_id,
|
|
637
|
+
BranchFile.path == r["file"],
|
|
638
|
+
).limit(1)
|
|
639
|
+
)
|
|
640
|
+
bf_row = bf_result.first()
|
|
641
|
+
if bf_row:
|
|
642
|
+
sha_by_file[r["file"]] = bf_row[0]
|
|
643
|
+
|
|
644
|
+
for item in items:
|
|
645
|
+
sha = sha_by_file.get(item.file_path)
|
|
646
|
+
if not sha:
|
|
647
|
+
continue
|
|
648
|
+
for word in query_words:
|
|
649
|
+
sym_result = await session.execute(
|
|
650
|
+
sa_select(Symbol.line_start)
|
|
651
|
+
.where(Symbol.content_sha == sha)
|
|
652
|
+
.where(Symbol.name.ilike(f"%{word}%"))
|
|
653
|
+
.limit(1)
|
|
654
|
+
)
|
|
655
|
+
row = sym_result.first()
|
|
656
|
+
if row:
|
|
657
|
+
item.line = row[0]
|
|
658
|
+
break
|
|
659
|
+
|
|
660
|
+
# Codex M4: populate the ``pin_id`` + ``manifest_hash`` fields
|
|
661
|
+
# so search responses carry the same state-snapshot identifier
|
|
662
|
+
# the local stdio MCP emits via ``_stamp_pin``.
|
|
663
|
+
from attocode_intel.api.routes._pin_helper import (
|
|
664
|
+
build_retrieval_pin,
|
|
665
|
+
)
|
|
666
|
+
try:
|
|
667
|
+
server_pin = await build_retrieval_pin(
|
|
668
|
+
session,
|
|
669
|
+
repo_id=_uuid.UUID(self._project_id),
|
|
670
|
+
branch_id=branch_ctx.branch_id,
|
|
671
|
+
)
|
|
672
|
+
pin_id = server_pin.pin_id
|
|
673
|
+
manifest_hash = server_pin.manifest_hash
|
|
674
|
+
except Exception:
|
|
675
|
+
pin_id = ""
|
|
676
|
+
manifest_hash = ""
|
|
677
|
+
|
|
678
|
+
return SearchResultsResponse(
|
|
679
|
+
query=query,
|
|
680
|
+
results=items,
|
|
681
|
+
total=len(items),
|
|
682
|
+
pin_id=pin_id,
|
|
683
|
+
manifest_hash=manifest_hash,
|
|
684
|
+
)
|
|
685
|
+
|
|
686
|
+
raise RuntimeError("No database session available")
|
|
687
|
+
|
|
688
|
+
|
|
689
|
+
async def security_scan(
|
|
690
|
+
self, mode: str, path: str, branch: str,
|
|
691
|
+
) -> SecurityScanResponse:
|
|
692
|
+
"""DB-backed security scan — runs regex patterns on stored content."""
|
|
693
|
+
import uuid as _uuid
|
|
694
|
+
|
|
695
|
+
from attocode_intel.api.deps import get_branch_context
|
|
696
|
+
from attocode_intel.db.engine import get_session
|
|
697
|
+
from attocode_intel.storage.security_scanner_db import db_security_scan
|
|
698
|
+
|
|
699
|
+
async for session in get_session():
|
|
700
|
+
repo_id = _uuid.UUID(self._project_id)
|
|
701
|
+
branch_ctx = await get_branch_context(repo_id, branch, session)
|
|
702
|
+
|
|
703
|
+
result = await db_security_scan(
|
|
704
|
+
session,
|
|
705
|
+
branch_ctx.branch_id,
|
|
706
|
+
branch_ctx.manifest,
|
|
707
|
+
mode=mode,
|
|
708
|
+
path_filter=path,
|
|
709
|
+
)
|
|
710
|
+
|
|
711
|
+
return SecurityScanResponse(
|
|
712
|
+
mode=result["mode"],
|
|
713
|
+
path=result["path"],
|
|
714
|
+
findings=[SecurityFinding(**f) for f in result["findings"]],
|
|
715
|
+
total_findings=result["total_findings"],
|
|
716
|
+
summary=result.get("summary", {}),
|
|
717
|
+
)
|
|
718
|
+
|
|
719
|
+
return SecurityScanResponse(
|
|
720
|
+
mode=mode, path=path, findings=[], total_findings=0,
|
|
721
|
+
summary={"error": "No database session available"},
|
|
722
|
+
)
|
|
723
|
+
|
|
724
|
+
|
|
725
|
+
class DbGraphProvider:
|
|
726
|
+
"""DB-backed graph provider for service mode."""
|
|
727
|
+
|
|
728
|
+
__slots__ = ("_project_id",)
|
|
729
|
+
|
|
730
|
+
def __init__(self, project_id: str) -> None:
|
|
731
|
+
self._project_id = project_id
|
|
732
|
+
|
|
733
|
+
async def graph_query(
|
|
734
|
+
self, file: str, edge_type: str, direction: str, depth: int, branch: str,
|
|
735
|
+
) -> GraphQueryResponse:
|
|
736
|
+
"""Falls back to CodeIntelService (no DB-specific implementation)."""
|
|
737
|
+
from attocode_intel.api.deps import get_service_or_404
|
|
738
|
+
from attocode_intel.api.models import GraphQueryHop, GraphQueryResponse
|
|
739
|
+
|
|
740
|
+
svc = await get_service_or_404(self._project_id)
|
|
741
|
+
data = svc.graph_query_data(
|
|
742
|
+
file=file, edge_type=edge_type, direction=direction, depth=depth,
|
|
743
|
+
)
|
|
744
|
+
return GraphQueryResponse(
|
|
745
|
+
root=data["root"],
|
|
746
|
+
direction=data["direction"],
|
|
747
|
+
depth=data["depth"],
|
|
748
|
+
hops=[GraphQueryHop(**h) for h in data["hops"]],
|
|
749
|
+
total_reachable=data["total_reachable"],
|
|
750
|
+
)
|
|
751
|
+
|
|
752
|
+
async def find_related(
|
|
753
|
+
self, file: str, top_k: int, branch: str,
|
|
754
|
+
) -> FindRelatedResponse:
|
|
755
|
+
"""Falls back to CodeIntelService (no DB-specific implementation)."""
|
|
756
|
+
from attocode_intel.api.deps import get_service_or_404
|
|
757
|
+
from attocode_intel.api.models import FindRelatedResponse, RelatedFileItem
|
|
758
|
+
|
|
759
|
+
svc = await get_service_or_404(self._project_id)
|
|
760
|
+
data = svc.find_related_data(file=file, top_k=top_k)
|
|
761
|
+
return FindRelatedResponse(
|
|
762
|
+
file=data["file"],
|
|
763
|
+
related=[RelatedFileItem(**r) for r in data["related"]],
|
|
764
|
+
)
|
|
765
|
+
|
|
766
|
+
async def community_detection(
|
|
767
|
+
self, branch: str, min_community_size: int, max_communities: int,
|
|
768
|
+
) -> CommunityResponse:
|
|
769
|
+
import os
|
|
770
|
+
from collections import Counter
|
|
771
|
+
|
|
772
|
+
from attocode_intel.api.deps import get_branch_context
|
|
773
|
+
from attocode_intel.api.models import CommunityBridge
|
|
774
|
+
from attocode_intel.community import bfs_connected_components, louvain_communities
|
|
775
|
+
from attocode_intel.db.engine import get_session
|
|
776
|
+
from attocode_intel.storage.dependency_store import DependencyStore
|
|
777
|
+
|
|
778
|
+
async for session in get_session():
|
|
779
|
+
repo_id = uuid.UUID(self._project_id)
|
|
780
|
+
branch_ctx = await get_branch_context(repo_id, branch, session)
|
|
781
|
+
dep_store = DependencyStore(session)
|
|
782
|
+
raw = await dep_store.get_graph_for_branch(branch_ctx.branch_id)
|
|
783
|
+
|
|
784
|
+
raw_edges = raw.get("edges", [])
|
|
785
|
+
all_files: set[str] = {n["path"] for n in raw.get("nodes", [])}
|
|
786
|
+
|
|
787
|
+
adj: dict[str, set[str]] = {}
|
|
788
|
+
weights: dict[tuple[str, str], float] = {}
|
|
789
|
+
for e in raw_edges:
|
|
790
|
+
src, tgt = e["source"], e["target"]
|
|
791
|
+
adj.setdefault(src, set()).add(tgt)
|
|
792
|
+
adj.setdefault(tgt, set()).add(src)
|
|
793
|
+
key = (min(src, tgt), max(src, tgt))
|
|
794
|
+
weights[key] = weights.get(key, 0.0) + e.get("weight", 1.0)
|
|
795
|
+
|
|
796
|
+
try:
|
|
797
|
+
communities, modularity = louvain_communities(all_files, adj, weights)
|
|
798
|
+
method = "louvain"
|
|
799
|
+
except ImportError:
|
|
800
|
+
communities, modularity = bfs_connected_components(all_files, adj)
|
|
801
|
+
method = "connected_components"
|
|
802
|
+
|
|
803
|
+
communities = [c for c in communities if len(c) >= min_community_size]
|
|
804
|
+
communities.sort(key=len, reverse=True)
|
|
805
|
+
communities = communities[:max_communities]
|
|
806
|
+
|
|
807
|
+
# Detect trivial results and use directory-based fallback
|
|
808
|
+
is_trivial = len(communities) <= 1 or modularity < 0.05
|
|
809
|
+
if is_trivial:
|
|
810
|
+
# Group files by top-level directory as modules
|
|
811
|
+
dir_groups: dict[str, list[str]] = {}
|
|
812
|
+
for f in all_files:
|
|
813
|
+
parts = f.split("/")
|
|
814
|
+
top_dir = parts[0] if len(parts) > 1 else "(root)"
|
|
815
|
+
dir_groups.setdefault(top_dir, []).append(f)
|
|
816
|
+
|
|
817
|
+
fallback_items: list[CommunityItem] = []
|
|
818
|
+
for idx_fb, (dir_name, file_list) in enumerate(
|
|
819
|
+
sorted(dir_groups.items(), key=lambda x: len(x[1]), reverse=True),
|
|
820
|
+
):
|
|
821
|
+
fallback_items.append(CommunityItem(
|
|
822
|
+
id=idx_fb,
|
|
823
|
+
files=sorted(file_list),
|
|
824
|
+
size=len(file_list),
|
|
825
|
+
theme=dir_name,
|
|
826
|
+
internal_edges=0,
|
|
827
|
+
external_edges=0,
|
|
828
|
+
hub=sorted(file_list)[0] if file_list else "",
|
|
829
|
+
hub_internal_degree=0,
|
|
830
|
+
top_dirs=[dir_name],
|
|
831
|
+
))
|
|
832
|
+
|
|
833
|
+
return CommunityResponse(
|
|
834
|
+
method=f"{method}+directory-fallback",
|
|
835
|
+
modularity=round(modularity, 4),
|
|
836
|
+
communities=fallback_items,
|
|
837
|
+
bridges=[],
|
|
838
|
+
)
|
|
839
|
+
|
|
840
|
+
# Build file-to-community-index mapping
|
|
841
|
+
file_to_comm: dict[str, int] = {}
|
|
842
|
+
items: list[CommunityItem] = []
|
|
843
|
+
for idx, comm in enumerate(communities):
|
|
844
|
+
for f in comm:
|
|
845
|
+
file_to_comm[f] = idx
|
|
846
|
+
|
|
847
|
+
internal = 0
|
|
848
|
+
external = 0
|
|
849
|
+
degree: dict[str, int] = {f: 0 for f in comm}
|
|
850
|
+
for f in comm:
|
|
851
|
+
for neighbor in adj.get(f, set()):
|
|
852
|
+
if neighbor in comm:
|
|
853
|
+
internal += 1
|
|
854
|
+
degree[f] += 1
|
|
855
|
+
else:
|
|
856
|
+
external += 1
|
|
857
|
+
internal //= 2
|
|
858
|
+
|
|
859
|
+
hub = max(degree, key=degree.get) if degree else "" # type: ignore[arg-type]
|
|
860
|
+
hub_deg = degree.get(hub, 0)
|
|
861
|
+
|
|
862
|
+
sorted_files = sorted(comm)
|
|
863
|
+
if sorted_files:
|
|
864
|
+
prefix = sorted_files[0]
|
|
865
|
+
for f in sorted_files[1:]:
|
|
866
|
+
while not f.startswith(prefix):
|
|
867
|
+
prefix = prefix.rsplit("/", 1)[0] if "/" in prefix else ""
|
|
868
|
+
if not prefix:
|
|
869
|
+
break
|
|
870
|
+
theme = prefix.rstrip("/") or "mixed"
|
|
871
|
+
else:
|
|
872
|
+
theme = ""
|
|
873
|
+
|
|
874
|
+
# Compute top directories
|
|
875
|
+
dirs = [os.path.dirname(f) or "(root)" for f in sorted_files]
|
|
876
|
+
top_dirs = [d for d, _ in Counter(dirs).most_common(3)]
|
|
877
|
+
|
|
878
|
+
items.append(CommunityItem(
|
|
879
|
+
id=idx,
|
|
880
|
+
files=sorted_files,
|
|
881
|
+
size=len(comm),
|
|
882
|
+
theme=theme,
|
|
883
|
+
internal_edges=internal,
|
|
884
|
+
external_edges=external,
|
|
885
|
+
hub=hub,
|
|
886
|
+
hub_internal_degree=hub_deg,
|
|
887
|
+
top_dirs=top_dirs,
|
|
888
|
+
))
|
|
889
|
+
|
|
890
|
+
# Compute bridges between communities
|
|
891
|
+
bridge_counts: dict[tuple[int, int], int] = {}
|
|
892
|
+
bridge_samples: dict[tuple[int, int], list[str]] = {}
|
|
893
|
+
for e in raw_edges:
|
|
894
|
+
src_comm = file_to_comm.get(e["source"])
|
|
895
|
+
tgt_comm = file_to_comm.get(e["target"])
|
|
896
|
+
if src_comm is None or tgt_comm is None or src_comm == tgt_comm:
|
|
897
|
+
continue
|
|
898
|
+
key = (min(src_comm, tgt_comm), max(src_comm, tgt_comm))
|
|
899
|
+
bridge_counts[key] = bridge_counts.get(key, 0) + 1
|
|
900
|
+
samples = bridge_samples.setdefault(key, [])
|
|
901
|
+
if len(samples) < 3:
|
|
902
|
+
samples.append(e["source"])
|
|
903
|
+
|
|
904
|
+
bridges = [
|
|
905
|
+
CommunityBridge(
|
|
906
|
+
source_id=k[0], target_id=k[1],
|
|
907
|
+
edge_count=v, sample_files=bridge_samples.get(k, []),
|
|
908
|
+
)
|
|
909
|
+
for k, v in sorted(bridge_counts.items(), key=lambda x: x[1], reverse=True)
|
|
910
|
+
]
|
|
911
|
+
|
|
912
|
+
return CommunityResponse(
|
|
913
|
+
method=method, modularity=round(modularity, 4),
|
|
914
|
+
communities=items, bridges=bridges,
|
|
915
|
+
)
|
|
916
|
+
|
|
917
|
+
return CommunityResponse(method="none", modularity=0.0, communities=[])
|
|
918
|
+
|
|
919
|
+
|
|
920
|
+
class DbLSPProvider:
|
|
921
|
+
"""DB-backed LSP provider for remote repos without a language server.
|
|
922
|
+
|
|
923
|
+
Provides best-effort definition/references/hover using the symbol and
|
|
924
|
+
symbol_reference tables. Diagnostics returns empty (requires real LS).
|
|
925
|
+
"""
|
|
926
|
+
|
|
927
|
+
__slots__ = ("_project_id",)
|
|
928
|
+
|
|
929
|
+
def __init__(self, project_id: str) -> None:
|
|
930
|
+
self._project_id = project_id
|
|
931
|
+
|
|
932
|
+
async def definition(self, file: str, line: int, col: int, branch: str) -> dict:
|
|
933
|
+
"""Find definition of symbol at position."""
|
|
934
|
+
from sqlalchemy import select
|
|
935
|
+
|
|
936
|
+
from attocode_intel.api.deps import get_branch_context
|
|
937
|
+
from attocode_intel.db.engine import get_session
|
|
938
|
+
from attocode_intel.db.models import Symbol
|
|
939
|
+
|
|
940
|
+
async for session in get_session():
|
|
941
|
+
repo_id = uuid.UUID(self._project_id)
|
|
942
|
+
branch_ctx = await get_branch_context(repo_id, branch, session)
|
|
943
|
+
sha = branch_ctx.manifest.get(file)
|
|
944
|
+
if not sha:
|
|
945
|
+
return {"definitions": []}
|
|
946
|
+
|
|
947
|
+
# Find symbol at the given line
|
|
948
|
+
result = await session.execute(
|
|
949
|
+
select(Symbol).where(
|
|
950
|
+
Symbol.content_sha == sha,
|
|
951
|
+
Symbol.line_start <= line,
|
|
952
|
+
Symbol.line_end >= line,
|
|
953
|
+
)
|
|
954
|
+
)
|
|
955
|
+
source_sym = result.scalars().first()
|
|
956
|
+
if source_sym is None:
|
|
957
|
+
return {"definitions": []}
|
|
958
|
+
|
|
959
|
+
# Search for a definition with the same name across the branch
|
|
960
|
+
all_shas = branch_ctx.content_shas
|
|
961
|
+
def_result = await session.execute(
|
|
962
|
+
select(Symbol).where(
|
|
963
|
+
Symbol.content_sha.in_(all_shas),
|
|
964
|
+
Symbol.name == source_sym.name,
|
|
965
|
+
Symbol.kind.in_(("class", "function", "method", "variable")),
|
|
966
|
+
).limit(10)
|
|
967
|
+
)
|
|
968
|
+
definitions = []
|
|
969
|
+
sha_to_path = branch_ctx.sha_to_path
|
|
970
|
+
for s in def_result.scalars():
|
|
971
|
+
definitions.append({
|
|
972
|
+
"file": sha_to_path.get(s.content_sha, ""),
|
|
973
|
+
"line": s.line_start or 0,
|
|
974
|
+
"name": s.name,
|
|
975
|
+
"kind": s.kind,
|
|
976
|
+
"signature": s.signature or "",
|
|
977
|
+
})
|
|
978
|
+
return {"definitions": definitions}
|
|
979
|
+
|
|
980
|
+
return {"definitions": []}
|
|
981
|
+
|
|
982
|
+
async def references(self, file: str, line: int, col: int, branch: str) -> dict:
|
|
983
|
+
"""Find references to symbol at position."""
|
|
984
|
+
from sqlalchemy import select
|
|
985
|
+
|
|
986
|
+
from attocode_intel.api.deps import get_branch_context
|
|
987
|
+
from attocode_intel.db.engine import get_session
|
|
988
|
+
from attocode_intel.db.models import Symbol, SymbolReference
|
|
989
|
+
|
|
990
|
+
async for session in get_session():
|
|
991
|
+
repo_id = uuid.UUID(self._project_id)
|
|
992
|
+
branch_ctx = await get_branch_context(repo_id, branch, session)
|
|
993
|
+
sha = branch_ctx.manifest.get(file)
|
|
994
|
+
if not sha:
|
|
995
|
+
return {"references": []}
|
|
996
|
+
|
|
997
|
+
# Find symbol at position
|
|
998
|
+
result = await session.execute(
|
|
999
|
+
select(Symbol).where(
|
|
1000
|
+
Symbol.content_sha == sha,
|
|
1001
|
+
Symbol.line_start <= line,
|
|
1002
|
+
Symbol.line_end >= line,
|
|
1003
|
+
)
|
|
1004
|
+
)
|
|
1005
|
+
source_sym = result.scalars().first()
|
|
1006
|
+
if source_sym is None:
|
|
1007
|
+
return {"references": []}
|
|
1008
|
+
|
|
1009
|
+
# Query references table
|
|
1010
|
+
all_shas = branch_ctx.content_shas
|
|
1011
|
+
ref_result = await session.execute(
|
|
1012
|
+
select(SymbolReference).where(
|
|
1013
|
+
SymbolReference.content_sha.in_(all_shas),
|
|
1014
|
+
SymbolReference.symbol_name == source_sym.name,
|
|
1015
|
+
).limit(500)
|
|
1016
|
+
)
|
|
1017
|
+
sha_to_path = branch_ctx.sha_to_path
|
|
1018
|
+
references = [
|
|
1019
|
+
{
|
|
1020
|
+
"file": sha_to_path.get(r.content_sha, ""),
|
|
1021
|
+
"line": r.line,
|
|
1022
|
+
"ref_kind": r.ref_kind,
|
|
1023
|
+
}
|
|
1024
|
+
for r in ref_result.scalars()
|
|
1025
|
+
]
|
|
1026
|
+
return {"references": references}
|
|
1027
|
+
|
|
1028
|
+
return {"references": []}
|
|
1029
|
+
|
|
1030
|
+
async def hover(self, file: str, line: int, col: int, branch: str) -> dict:
|
|
1031
|
+
"""Get hover info for symbol at position."""
|
|
1032
|
+
from sqlalchemy import select
|
|
1033
|
+
|
|
1034
|
+
from attocode_intel.api.deps import get_branch_context
|
|
1035
|
+
from attocode_intel.db.engine import get_session
|
|
1036
|
+
from attocode_intel.db.models import Symbol
|
|
1037
|
+
|
|
1038
|
+
async for session in get_session():
|
|
1039
|
+
repo_id = uuid.UUID(self._project_id)
|
|
1040
|
+
branch_ctx = await get_branch_context(repo_id, branch, session)
|
|
1041
|
+
sha = branch_ctx.manifest.get(file)
|
|
1042
|
+
if not sha:
|
|
1043
|
+
return {"hover": None}
|
|
1044
|
+
|
|
1045
|
+
result = await session.execute(
|
|
1046
|
+
select(Symbol).where(
|
|
1047
|
+
Symbol.content_sha == sha,
|
|
1048
|
+
Symbol.line_start <= line,
|
|
1049
|
+
Symbol.line_end >= line,
|
|
1050
|
+
)
|
|
1051
|
+
)
|
|
1052
|
+
sym = result.scalars().first()
|
|
1053
|
+
if sym is None:
|
|
1054
|
+
return {"hover": None}
|
|
1055
|
+
|
|
1056
|
+
return {
|
|
1057
|
+
"hover": {
|
|
1058
|
+
"name": sym.name,
|
|
1059
|
+
"kind": sym.kind,
|
|
1060
|
+
"signature": sym.signature or "",
|
|
1061
|
+
"exported": sym.exported,
|
|
1062
|
+
"line_start": sym.line_start,
|
|
1063
|
+
"line_end": sym.line_end,
|
|
1064
|
+
}
|
|
1065
|
+
}
|
|
1066
|
+
|
|
1067
|
+
return {"hover": None}
|
|
1068
|
+
|
|
1069
|
+
async def diagnostics(self, file: str, branch: str) -> dict:
|
|
1070
|
+
"""Return empty diagnostics — requires a real language server."""
|
|
1071
|
+
return {
|
|
1072
|
+
"diagnostics": [],
|
|
1073
|
+
"message": "Diagnostics require a running language server (not available for remote repos)",
|
|
1074
|
+
}
|
|
1075
|
+
|
|
1076
|
+
async def completions(self, file: str, line: int, col: int, branch: str, limit: int = 20) -> dict:
|
|
1077
|
+
"""Completions require a real language server."""
|
|
1078
|
+
return {
|
|
1079
|
+
"file": file, "line": line, "col": col,
|
|
1080
|
+
"completions": [], "total": 0,
|
|
1081
|
+
"error": "Completions require a running language server (not available for remote repos)",
|
|
1082
|
+
}
|
|
1083
|
+
|
|
1084
|
+
async def workspace_symbol(self, query: str, branch: str, limit: int = 30) -> dict:
|
|
1085
|
+
"""Workspace symbol search requires a real language server."""
|
|
1086
|
+
return {
|
|
1087
|
+
"query": query,
|
|
1088
|
+
"symbols": [], "total": 0,
|
|
1089
|
+
"error": "Workspace symbol search requires a running language server (not available for remote repos)",
|
|
1090
|
+
}
|
|
1091
|
+
|
|
1092
|
+
async def incoming_calls(self, file: str, line: int, col: int, branch: str) -> dict:
|
|
1093
|
+
"""Incoming call hierarchy requires a real language server."""
|
|
1094
|
+
return {
|
|
1095
|
+
"symbol": "", "file": file, "line": line, "col": col,
|
|
1096
|
+
"callers": [], "total": 0,
|
|
1097
|
+
"error": "Call hierarchy requires a running language server (not available for remote repos)",
|
|
1098
|
+
}
|
|
1099
|
+
|
|
1100
|
+
async def outgoing_calls(self, file: str, line: int, col: int, branch: str) -> dict:
|
|
1101
|
+
"""Outgoing call hierarchy requires a real language server."""
|
|
1102
|
+
return {
|
|
1103
|
+
"symbol": "", "file": file, "line": line, "col": col,
|
|
1104
|
+
"callees": [], "total": 0,
|
|
1105
|
+
"error": "Call hierarchy requires a running language server (not available for remote repos)",
|
|
1106
|
+
}
|