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,240 @@
|
|
|
1
|
+
"""Content-hash-gated incremental update pipeline.
|
|
2
|
+
|
|
3
|
+
This is the core pipeline that processes file changes from hook notifications.
|
|
4
|
+
It reads file content, computes SHA-256, compares to the current branch manifest,
|
|
5
|
+
and only re-parses/re-indexes files whose content has actually changed.
|
|
6
|
+
|
|
7
|
+
Flow:
|
|
8
|
+
paths → read content → hash → compare to manifest → skip unchanged
|
|
9
|
+
→ store new content → update overlay → parse symbols → queue embeddings
|
|
10
|
+
→ publish event
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
import logging
|
|
16
|
+
import uuid
|
|
17
|
+
from pathlib import Path
|
|
18
|
+
from typing import TYPE_CHECKING
|
|
19
|
+
|
|
20
|
+
from attocode_intel.indexing.parser import detect_language, extract_imports, extract_symbols
|
|
21
|
+
|
|
22
|
+
if TYPE_CHECKING:
|
|
23
|
+
from sqlalchemy.ext.asyncio import AsyncSession
|
|
24
|
+
|
|
25
|
+
logger = logging.getLogger(__name__)
|
|
26
|
+
|
|
27
|
+
_MAX_FILE_SIZE = 1_000_000 # 1MB
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class IncrementalPipeline:
|
|
31
|
+
"""Content-hash-gated incremental indexing pipeline.
|
|
32
|
+
|
|
33
|
+
Given a list of changed file paths and a branch, this pipeline:
|
|
34
|
+
1. Reads each file from disk (local mode) or git (service mode)
|
|
35
|
+
2. Computes SHA-256 of the content
|
|
36
|
+
3. Compares to current manifest — skips if SHA matches (no-op)
|
|
37
|
+
4. Stores new content in content-addressed store
|
|
38
|
+
5. Updates branch overlay (path → new SHA)
|
|
39
|
+
6. Extracts symbols (skips if SHA already has symbols — dedup)
|
|
40
|
+
7. Queues embedding generation for new content
|
|
41
|
+
8. Returns stats about what changed
|
|
42
|
+
"""
|
|
43
|
+
|
|
44
|
+
def __init__(self, session: AsyncSession) -> None:
|
|
45
|
+
from attocode_intel.storage.branch_overlay import BranchOverlay
|
|
46
|
+
from attocode_intel.storage.content_store import ContentStore
|
|
47
|
+
from attocode_intel.storage.embedding_store import EmbeddingStore
|
|
48
|
+
from attocode_intel.storage.symbol_store import SymbolStore
|
|
49
|
+
|
|
50
|
+
self._session = session
|
|
51
|
+
from attocode_intel.storage.dependency_store import DependencyStore
|
|
52
|
+
|
|
53
|
+
self._content_store = ContentStore(session)
|
|
54
|
+
self._symbol_store = SymbolStore(session)
|
|
55
|
+
self._embedding_store = EmbeddingStore(session)
|
|
56
|
+
self._overlay = BranchOverlay(session)
|
|
57
|
+
self._dep_store = DependencyStore(session)
|
|
58
|
+
|
|
59
|
+
@staticmethod
|
|
60
|
+
async def acquire_branch_lock(session: AsyncSession, branch_id: uuid.UUID) -> None:
|
|
61
|
+
"""Acquire a PostgreSQL advisory lock for a branch to prevent concurrent indexing."""
|
|
62
|
+
from sqlalchemy import text
|
|
63
|
+
|
|
64
|
+
lock_key = int.from_bytes(branch_id.bytes[:8], "big") & 0x7FFFFFFFFFFFFFFF
|
|
65
|
+
await session.execute(
|
|
66
|
+
text("SELECT pg_advisory_xact_lock(:key)").bindparams(key=lock_key)
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
async def process_file_changes(
|
|
70
|
+
self,
|
|
71
|
+
branch_id: uuid.UUID,
|
|
72
|
+
paths: list[str],
|
|
73
|
+
base_dir: str | None = None,
|
|
74
|
+
git_reader: object | None = None,
|
|
75
|
+
repo_id: str | None = None,
|
|
76
|
+
ref: str | None = None,
|
|
77
|
+
file_contents: dict[str, bytes] | None = None,
|
|
78
|
+
) -> dict:
|
|
79
|
+
"""Process a batch of file changes for a branch.
|
|
80
|
+
|
|
81
|
+
Content sources (checked in order):
|
|
82
|
+
1. file_contents — inline content sent by the client (remote mode)
|
|
83
|
+
2. base_dir — read from local filesystem
|
|
84
|
+
3. git_reader+repo_id+ref — read from git
|
|
85
|
+
|
|
86
|
+
Returns stats: {processed, skipped_unchanged, skipped_missing,
|
|
87
|
+
skipped_too_large, symbols_updated, errors}
|
|
88
|
+
"""
|
|
89
|
+
stats = {
|
|
90
|
+
"processed": 0,
|
|
91
|
+
"skipped_unchanged": 0,
|
|
92
|
+
"skipped_missing": 0,
|
|
93
|
+
"skipped_too_large": 0,
|
|
94
|
+
"skipped_binary": 0,
|
|
95
|
+
"symbols_updated": 0,
|
|
96
|
+
"errors": 0,
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
# Acquire advisory lock to prevent concurrent indexing on same branch
|
|
100
|
+
try:
|
|
101
|
+
await self.acquire_branch_lock(self._session, branch_id)
|
|
102
|
+
except Exception:
|
|
103
|
+
pass # Non-Postgres backends (e.g. tests) — skip locking
|
|
104
|
+
|
|
105
|
+
# Get current manifest for comparison
|
|
106
|
+
manifest = await self._overlay.resolve_manifest(branch_id)
|
|
107
|
+
|
|
108
|
+
# Batch: collect files to update
|
|
109
|
+
overlay_updates: list[tuple[str, str, str]] = [] # (path, sha, status)
|
|
110
|
+
|
|
111
|
+
for path in paths:
|
|
112
|
+
try:
|
|
113
|
+
content = await self._read_file(path, base_dir, git_reader, repo_id, ref, file_contents=file_contents)
|
|
114
|
+
|
|
115
|
+
if content is None:
|
|
116
|
+
# File was deleted
|
|
117
|
+
if path in manifest:
|
|
118
|
+
await self._overlay.delete_file(branch_id, path)
|
|
119
|
+
stats["processed"] += 1
|
|
120
|
+
else:
|
|
121
|
+
stats["skipped_missing"] += 1
|
|
122
|
+
continue
|
|
123
|
+
|
|
124
|
+
if len(content) > _MAX_FILE_SIZE:
|
|
125
|
+
stats["skipped_too_large"] += 1
|
|
126
|
+
continue
|
|
127
|
+
|
|
128
|
+
# Content-hash comparison — the key optimization
|
|
129
|
+
sha = self._content_store.hash_content(content)
|
|
130
|
+
current_sha = manifest.get(path)
|
|
131
|
+
|
|
132
|
+
if sha == current_sha:
|
|
133
|
+
stats["skipped_unchanged"] += 1
|
|
134
|
+
continue
|
|
135
|
+
|
|
136
|
+
# New or changed content
|
|
137
|
+
language = detect_language(path)
|
|
138
|
+
await self._content_store.store(content, language)
|
|
139
|
+
|
|
140
|
+
# Determine status
|
|
141
|
+
status = "added" if path not in manifest else "modified"
|
|
142
|
+
overlay_updates.append((path, sha, status))
|
|
143
|
+
|
|
144
|
+
# Extract symbols (skip if SHA already indexed — content dedup)
|
|
145
|
+
symbols = extract_symbols(content, path)
|
|
146
|
+
if symbols:
|
|
147
|
+
count = await self._symbol_store.upsert_symbols(
|
|
148
|
+
sha, symbols, skip_if_exists=True
|
|
149
|
+
)
|
|
150
|
+
stats["symbols_updated"] += count or len(symbols)
|
|
151
|
+
|
|
152
|
+
# Extract imports and resolve to dependencies
|
|
153
|
+
imports = extract_imports(content, path, language)
|
|
154
|
+
if imports and not await self._dep_store.has_dependencies(sha):
|
|
155
|
+
from attocode_intel.indexing.full_indexer import _resolve_import_path
|
|
156
|
+
|
|
157
|
+
known_paths = set(manifest.keys()) | {u[0] for u in overlay_updates}
|
|
158
|
+
path_to_sha = dict(manifest)
|
|
159
|
+
for u_path, u_sha, _ in overlay_updates:
|
|
160
|
+
path_to_sha[u_path] = u_sha
|
|
161
|
+
|
|
162
|
+
deps = []
|
|
163
|
+
for imp in imports:
|
|
164
|
+
target = _resolve_import_path(imp, path, known_paths, language)
|
|
165
|
+
if target and target in path_to_sha:
|
|
166
|
+
deps.append({
|
|
167
|
+
"target_sha": path_to_sha[target],
|
|
168
|
+
"dep_type": "import",
|
|
169
|
+
"weight": 1.0,
|
|
170
|
+
})
|
|
171
|
+
if deps:
|
|
172
|
+
await self._dep_store.upsert_dependencies(sha, deps)
|
|
173
|
+
stats["dependencies_extracted"] = stats.get("dependencies_extracted", 0) + len(deps)
|
|
174
|
+
|
|
175
|
+
# Queue embedding generation (dedup: skip if already embedded)
|
|
176
|
+
if not await self._embedding_store.has_embeddings(sha):
|
|
177
|
+
if repo_id:
|
|
178
|
+
from attocode_intel.workers.job_utils import enqueue_embedding_job
|
|
179
|
+
await enqueue_embedding_job(repo_id, ref or "main")
|
|
180
|
+
|
|
181
|
+
stats["processed"] += 1
|
|
182
|
+
|
|
183
|
+
except Exception as e:
|
|
184
|
+
logger.warning("Error processing %s: %s", path, e)
|
|
185
|
+
stats["errors"] += 1
|
|
186
|
+
|
|
187
|
+
# Batch update overlay (single version bump)
|
|
188
|
+
if overlay_updates:
|
|
189
|
+
await self._overlay.set_files_batch(branch_id, overlay_updates)
|
|
190
|
+
|
|
191
|
+
return stats
|
|
192
|
+
|
|
193
|
+
async def _read_file(
|
|
194
|
+
self,
|
|
195
|
+
path: str,
|
|
196
|
+
base_dir: str | None,
|
|
197
|
+
git_reader: object | None,
|
|
198
|
+
repo_id: str | None,
|
|
199
|
+
ref: str | None,
|
|
200
|
+
file_contents: dict[str, bytes] | None = None,
|
|
201
|
+
) -> bytes | None:
|
|
202
|
+
"""Read file content from inline content, disk, or git.
|
|
203
|
+
|
|
204
|
+
Content sources (checked in order):
|
|
205
|
+
1. file_contents — inline content sent by the client
|
|
206
|
+
2. base_dir — local filesystem
|
|
207
|
+
3. git_reader — git object reader
|
|
208
|
+
|
|
209
|
+
Returns None if file doesn't exist (deleted) or no content source available.
|
|
210
|
+
C1 fix: Validates resolved path is within base_dir to prevent path traversal.
|
|
211
|
+
"""
|
|
212
|
+
# Priority 1: inline content (sent by client for remote servers)
|
|
213
|
+
if file_contents is not None and path in file_contents:
|
|
214
|
+
return file_contents[path]
|
|
215
|
+
|
|
216
|
+
# Priority 2: local filesystem
|
|
217
|
+
if base_dir:
|
|
218
|
+
base = Path(base_dir).resolve()
|
|
219
|
+
full_path = (base / path).resolve()
|
|
220
|
+
# C1: prevent path traversal (e.g. ../../etc/passwd)
|
|
221
|
+
if not full_path.is_relative_to(base):
|
|
222
|
+
raise ValueError(
|
|
223
|
+
f"Path traversal detected: '{path}' resolves outside base directory"
|
|
224
|
+
)
|
|
225
|
+
if not full_path.is_file():
|
|
226
|
+
return None
|
|
227
|
+
try:
|
|
228
|
+
return full_path.read_bytes()
|
|
229
|
+
except (OSError, PermissionError):
|
|
230
|
+
return None
|
|
231
|
+
|
|
232
|
+
# Priority 3: git reader
|
|
233
|
+
if git_reader and repo_id and ref:
|
|
234
|
+
try:
|
|
235
|
+
return git_reader.read_file(repo_id, ref, path) # type: ignore[union-attr]
|
|
236
|
+
except Exception:
|
|
237
|
+
return None
|
|
238
|
+
|
|
239
|
+
# No content source — file is missing or deleted
|
|
240
|
+
return None
|
|
@@ -0,0 +1,403 @@
|
|
|
1
|
+
"""Parser bridge — connects indexing pipeline with existing AST/CodeAnalyzer."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import logging
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from typing import TYPE_CHECKING
|
|
8
|
+
|
|
9
|
+
if TYPE_CHECKING:
|
|
10
|
+
pass
|
|
11
|
+
|
|
12
|
+
logger = logging.getLogger(__name__)
|
|
13
|
+
|
|
14
|
+
# Language detection by extension
|
|
15
|
+
_EXT_TO_LANG: dict[str, str] = {
|
|
16
|
+
".py": "python",
|
|
17
|
+
".js": "javascript",
|
|
18
|
+
".ts": "typescript",
|
|
19
|
+
".tsx": "typescript",
|
|
20
|
+
".jsx": "javascript",
|
|
21
|
+
".go": "go",
|
|
22
|
+
".rs": "rust",
|
|
23
|
+
".java": "java",
|
|
24
|
+
".rb": "ruby",
|
|
25
|
+
".c": "c",
|
|
26
|
+
".cpp": "cpp",
|
|
27
|
+
".h": "c",
|
|
28
|
+
".hpp": "cpp",
|
|
29
|
+
".metal": "cpp",
|
|
30
|
+
".cs": "csharp",
|
|
31
|
+
".php": "php",
|
|
32
|
+
".swift": "swift",
|
|
33
|
+
".kt": "kotlin",
|
|
34
|
+
".scala": "scala",
|
|
35
|
+
".lua": "lua",
|
|
36
|
+
".ex": "elixir",
|
|
37
|
+
".exs": "elixir",
|
|
38
|
+
".sh": "bash",
|
|
39
|
+
".bash": "bash",
|
|
40
|
+
".zig": "zig",
|
|
41
|
+
".nim": "nim",
|
|
42
|
+
".fs": "fsharp",
|
|
43
|
+
".fsi": "fsharp",
|
|
44
|
+
".cr": "crystal",
|
|
45
|
+
".m": "objc",
|
|
46
|
+
".mm": "objc",
|
|
47
|
+
".yaml": "yaml",
|
|
48
|
+
".yml": "yaml",
|
|
49
|
+
".toml": "toml",
|
|
50
|
+
".json": "json",
|
|
51
|
+
".html": "html",
|
|
52
|
+
".css": "css",
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def detect_language(path: str) -> str | None:
|
|
57
|
+
"""Detect programming language from file extension."""
|
|
58
|
+
ext = Path(path).suffix.lower()
|
|
59
|
+
return _EXT_TO_LANG.get(ext)
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def parse_content(content_sha: str, content: bytes, filename: str) -> dict:
|
|
63
|
+
"""Parse file content and return structured result keyed by content SHA.
|
|
64
|
+
|
|
65
|
+
Bridge function for the content-hash-gated incremental pipeline.
|
|
66
|
+
Returns: {content_sha, language, symbols, imports}
|
|
67
|
+
"""
|
|
68
|
+
language = detect_language(filename)
|
|
69
|
+
symbols = extract_symbols(content, filename)
|
|
70
|
+
imports = extract_imports(content, filename, language)
|
|
71
|
+
|
|
72
|
+
return {
|
|
73
|
+
"content_sha": content_sha,
|
|
74
|
+
"language": language,
|
|
75
|
+
"symbols": symbols,
|
|
76
|
+
"imports": imports,
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def extract_imports(content: bytes, path: str, language: str | None) -> list[str]:
|
|
81
|
+
"""Extract import statements from file content. Returns list of module names."""
|
|
82
|
+
import re
|
|
83
|
+
|
|
84
|
+
if language is None:
|
|
85
|
+
return []
|
|
86
|
+
|
|
87
|
+
try:
|
|
88
|
+
text = content.decode("utf-8", errors="replace")
|
|
89
|
+
except Exception:
|
|
90
|
+
return []
|
|
91
|
+
|
|
92
|
+
imports = []
|
|
93
|
+
if language == "python":
|
|
94
|
+
for m in re.finditer(r"^from\s+([\w.]+)\s+import\s+", text, re.MULTILINE):
|
|
95
|
+
imports.append(m.group(1))
|
|
96
|
+
for m in re.finditer(r"^import\s+([\w.]+(?:\s*,\s*[\w.]+)*)", text, re.MULTILINE):
|
|
97
|
+
for name in m.group(1).split(","):
|
|
98
|
+
name = name.strip().split(" as ")[0].strip()
|
|
99
|
+
if name:
|
|
100
|
+
imports.append(name)
|
|
101
|
+
elif language in ("javascript", "typescript"):
|
|
102
|
+
# import X from 'module'; import { X } from 'module'; import 'module'
|
|
103
|
+
for m in re.finditer(r"""import\s+(?:.*?\s+from\s+)?['"]([\w@/.\\-]+)['"]""", text):
|
|
104
|
+
imports.append(m.group(1))
|
|
105
|
+
# require('module')
|
|
106
|
+
for m in re.finditer(r"""require\s*\(\s*['"]([\w@/.\\-]+)['"]""", text):
|
|
107
|
+
imports.append(m.group(1))
|
|
108
|
+
# Re-exports: export { X } from 'module'; export * from 'module'
|
|
109
|
+
for m in re.finditer(r"""export\s+(?:\*|(?:\{[^}]*\}|\w+)\s+)from\s+['"]([\w@/.\\-]+)['"]""", text):
|
|
110
|
+
imports.append(m.group(1))
|
|
111
|
+
elif language == "go":
|
|
112
|
+
# Restrict to the Go import block, not all quoted strings.
|
|
113
|
+
import_block = re.search(r'import\s*\((.*?)\)', text, re.DOTALL)
|
|
114
|
+
if import_block:
|
|
115
|
+
for m in re.finditer(r'"([\w/.\\-]+)"', import_block.group(1)):
|
|
116
|
+
imports.append(m.group(1))
|
|
117
|
+
# Single import: import "pkg"
|
|
118
|
+
for m in re.finditer(r'^import\s+"([\w/.\\-]+)"', text, re.MULTILINE):
|
|
119
|
+
imports.append(m.group(1))
|
|
120
|
+
elif language == "rust":
|
|
121
|
+
for m in re.finditer(r"^use\s+([\w:]+)", text, re.MULTILINE):
|
|
122
|
+
imports.append(m.group(1))
|
|
123
|
+
elif language == "java":
|
|
124
|
+
for m in re.finditer(r"^import\s+([\w.]+);", text, re.MULTILINE):
|
|
125
|
+
imports.append(m.group(1))
|
|
126
|
+
|
|
127
|
+
return imports
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def _enclosing_caller(symbols: list[dict], line: int) -> str:
|
|
131
|
+
"""Return the qualified name of the function/method whose line range
|
|
132
|
+
contains ``line``. When several symbols nest, the innermost (smallest
|
|
133
|
+
span) wins. Returns an empty string when no enclosing function exists
|
|
134
|
+
(top-level call) or when symbols extraction failed.
|
|
135
|
+
"""
|
|
136
|
+
best_qname = ""
|
|
137
|
+
best_span = -1
|
|
138
|
+
for sym in symbols:
|
|
139
|
+
kind = sym.get("kind", "")
|
|
140
|
+
if kind not in ("function", "method"):
|
|
141
|
+
continue
|
|
142
|
+
start = sym.get("line_start", 0)
|
|
143
|
+
end = sym.get("line_end", start)
|
|
144
|
+
if start <= line <= end:
|
|
145
|
+
span = end - start
|
|
146
|
+
if best_span < 0 or span < best_span:
|
|
147
|
+
qname = sym.get("qualified_name") or sym.get("name") or ""
|
|
148
|
+
if qname:
|
|
149
|
+
best_qname = qname
|
|
150
|
+
best_span = span
|
|
151
|
+
return best_qname
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
def extract_references(content: bytes, path: str) -> list[dict]:
|
|
155
|
+
"""Extract symbol references (call sites, imports) from file content.
|
|
156
|
+
|
|
157
|
+
Returns list of dicts: {symbol_name, ref_kind, line, caller_qualified_name}.
|
|
158
|
+
``caller_qualified_name`` is the enclosing function/method (best-effort,
|
|
159
|
+
inferred by line range against extracted symbols); empty for top-level
|
|
160
|
+
calls or when symbol extraction is unavailable.
|
|
161
|
+
"""
|
|
162
|
+
import re
|
|
163
|
+
|
|
164
|
+
language = detect_language(path)
|
|
165
|
+
if not language or language not in ("python", "javascript", "typescript"):
|
|
166
|
+
return []
|
|
167
|
+
|
|
168
|
+
try:
|
|
169
|
+
text = content.decode("utf-8", errors="replace")
|
|
170
|
+
except Exception:
|
|
171
|
+
return []
|
|
172
|
+
|
|
173
|
+
# Symbols are needed to attribute callers; failure here is non-fatal,
|
|
174
|
+
# we just emit refs without caller_qualified_name.
|
|
175
|
+
try:
|
|
176
|
+
symbols = extract_symbols(content, path)
|
|
177
|
+
except Exception:
|
|
178
|
+
symbols = []
|
|
179
|
+
|
|
180
|
+
refs: list[dict] = []
|
|
181
|
+
seen: set[tuple[str, int]] = set() # dedupe (name, line)
|
|
182
|
+
|
|
183
|
+
for i, line_text in enumerate(text.split("\n"), 1):
|
|
184
|
+
stripped = line_text.strip()
|
|
185
|
+
# Skip comment lines
|
|
186
|
+
if stripped.startswith("#") or stripped.startswith("//"):
|
|
187
|
+
continue
|
|
188
|
+
# Skip definition lines (def/class/function/interface/type)
|
|
189
|
+
if re.match(r"^\s*(?:async\s+)?(?:def|class|function|interface|type)\s", line_text):
|
|
190
|
+
continue
|
|
191
|
+
# Skip import lines — those are tracked as dependencies
|
|
192
|
+
if re.match(r"^\s*(?:from\s|import\s|export\s.*from)", line_text):
|
|
193
|
+
continue
|
|
194
|
+
|
|
195
|
+
caller = _enclosing_caller(symbols, i)
|
|
196
|
+
|
|
197
|
+
# Function/method calls: name(
|
|
198
|
+
for m in re.finditer(r"\b(\w+)\s*\(", stripped):
|
|
199
|
+
name = m.group(1)
|
|
200
|
+
# Skip language keywords and builtins
|
|
201
|
+
if name in _SKIP_NAMES:
|
|
202
|
+
continue
|
|
203
|
+
key = (name, i)
|
|
204
|
+
if key not in seen:
|
|
205
|
+
seen.add(key)
|
|
206
|
+
refs.append({
|
|
207
|
+
"symbol_name": name,
|
|
208
|
+
"ref_kind": "call",
|
|
209
|
+
"line": i,
|
|
210
|
+
"caller_qualified_name": caller,
|
|
211
|
+
})
|
|
212
|
+
|
|
213
|
+
# Method calls: obj.method(
|
|
214
|
+
for m in re.finditer(r"\b\w+\.(\w+)\s*\(", stripped):
|
|
215
|
+
name = m.group(1)
|
|
216
|
+
if name in _SKIP_NAMES:
|
|
217
|
+
continue
|
|
218
|
+
key = (name, i)
|
|
219
|
+
if key not in seen:
|
|
220
|
+
seen.add(key)
|
|
221
|
+
refs.append({
|
|
222
|
+
"symbol_name": name,
|
|
223
|
+
"ref_kind": "call",
|
|
224
|
+
"line": i,
|
|
225
|
+
"caller_qualified_name": caller,
|
|
226
|
+
})
|
|
227
|
+
|
|
228
|
+
return refs
|
|
229
|
+
|
|
230
|
+
|
|
231
|
+
_SKIP_NAMES = frozenset({
|
|
232
|
+
# Python keywords/builtins
|
|
233
|
+
"if", "else", "elif", "for", "while", "try", "except", "finally",
|
|
234
|
+
"with", "as", "return", "yield", "raise", "pass", "break", "continue",
|
|
235
|
+
"print", "len", "range", "list", "dict", "set", "tuple", "str", "int",
|
|
236
|
+
"float", "bool", "type", "super", "isinstance", "issubclass", "hasattr",
|
|
237
|
+
"getattr", "setattr", "delattr", "property", "staticmethod", "classmethod",
|
|
238
|
+
"enumerate", "zip", "map", "filter", "sorted", "reversed", "min", "max",
|
|
239
|
+
"sum", "abs", "any", "all", "next", "iter", "open", "round", "repr",
|
|
240
|
+
# JS/TS keywords/builtins
|
|
241
|
+
"require", "import", "export", "new", "typeof", "void", "delete",
|
|
242
|
+
"Array", "Object", "String", "Number", "Boolean", "Promise", "Date",
|
|
243
|
+
"Math", "JSON", "console", "setTimeout", "setInterval", "clearTimeout",
|
|
244
|
+
"clearInterval", "parseInt", "parseFloat", "Error", "RegExp", "Map", "Set",
|
|
245
|
+
})
|
|
246
|
+
|
|
247
|
+
|
|
248
|
+
def extract_symbols(content: bytes, path: str) -> list[dict]:
|
|
249
|
+
"""Extract symbols from file content using tree-sitter if available.
|
|
250
|
+
|
|
251
|
+
Falls back to regex-based extraction for common patterns.
|
|
252
|
+
Returns list of symbol dicts with keys: name, kind, line_start, line_end,
|
|
253
|
+
signature, exported.
|
|
254
|
+
"""
|
|
255
|
+
language = detect_language(path)
|
|
256
|
+
if not language:
|
|
257
|
+
return []
|
|
258
|
+
|
|
259
|
+
try:
|
|
260
|
+
text = content.decode("utf-8", errors="replace")
|
|
261
|
+
except Exception:
|
|
262
|
+
return []
|
|
263
|
+
|
|
264
|
+
# Try tree-sitter first
|
|
265
|
+
try:
|
|
266
|
+
return _extract_with_tree_sitter(text, language)
|
|
267
|
+
except Exception:
|
|
268
|
+
pass
|
|
269
|
+
|
|
270
|
+
# Fallback to regex
|
|
271
|
+
return _extract_with_regex(text, language)
|
|
272
|
+
|
|
273
|
+
|
|
274
|
+
def _extract_with_tree_sitter(text: str, language: str) -> list[dict]:
|
|
275
|
+
"""Extract symbols using tree-sitter."""
|
|
276
|
+
|
|
277
|
+
# This will raise if tree-sitter is not available
|
|
278
|
+
from attocode_intel._internal.integrations.context.ts_parser import _get_parser as get_parser
|
|
279
|
+
|
|
280
|
+
parser = get_parser(language)
|
|
281
|
+
if parser is None:
|
|
282
|
+
raise RuntimeError("No parser available")
|
|
283
|
+
|
|
284
|
+
tree = parser.parse(bytes(text, "utf-8"))
|
|
285
|
+
symbols = []
|
|
286
|
+
|
|
287
|
+
# Walk tree for function/class definitions
|
|
288
|
+
_walk_tree(tree.root_node, symbols, language)
|
|
289
|
+
return symbols
|
|
290
|
+
|
|
291
|
+
|
|
292
|
+
def _walk_tree(node, symbols: list[dict], language: str) -> None:
|
|
293
|
+
"""Recursively walk AST nodes to extract symbol definitions."""
|
|
294
|
+
kind_map = {
|
|
295
|
+
"function_definition": "function",
|
|
296
|
+
"function_declaration": "function",
|
|
297
|
+
"method_definition": "method",
|
|
298
|
+
"method_declaration": "method",
|
|
299
|
+
"class_definition": "class",
|
|
300
|
+
"class_declaration": "class",
|
|
301
|
+
"interface_declaration": "interface",
|
|
302
|
+
"type_alias_declaration": "type",
|
|
303
|
+
"variable_declaration": "variable",
|
|
304
|
+
"const_declaration": "constant",
|
|
305
|
+
"struct_item": "class",
|
|
306
|
+
"enum_item": "class",
|
|
307
|
+
"trait_item": "interface",
|
|
308
|
+
"type_declaration": "type",
|
|
309
|
+
"enum_declaration": "class",
|
|
310
|
+
"struct_declaration": "class",
|
|
311
|
+
"object_declaration": "class",
|
|
312
|
+
"protocol_declaration": "interface",
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
node_type = node.type
|
|
316
|
+
if node_type in kind_map:
|
|
317
|
+
name_node = node.child_by_field_name("name")
|
|
318
|
+
if name_node:
|
|
319
|
+
is_async = node.parent and node.parent.type in (
|
|
320
|
+
"async_function_definition", "decorated_definition",
|
|
321
|
+
) or node_type.startswith("async_")
|
|
322
|
+
# Build signature from parameters node if available
|
|
323
|
+
sig = None
|
|
324
|
+
params_node = node.child_by_field_name("parameters")
|
|
325
|
+
if params_node:
|
|
326
|
+
sig = params_node.text.decode("utf-8", errors="replace")
|
|
327
|
+
ret_node = node.child_by_field_name("return_type")
|
|
328
|
+
if ret_node:
|
|
329
|
+
sig += f" -> {ret_node.text.decode('utf-8', errors='replace')}"
|
|
330
|
+
exported = False
|
|
331
|
+
if node.parent and node.parent.type == "export_statement":
|
|
332
|
+
exported = True
|
|
333
|
+
symbols.append({
|
|
334
|
+
"name": name_node.text.decode("utf-8"),
|
|
335
|
+
"kind": kind_map[node_type],
|
|
336
|
+
"line_start": node.start_point[0] + 1,
|
|
337
|
+
"line_end": node.end_point[0] + 1,
|
|
338
|
+
"signature": sig,
|
|
339
|
+
"exported": exported,
|
|
340
|
+
"metadata": {"async": True} if is_async else {},
|
|
341
|
+
})
|
|
342
|
+
|
|
343
|
+
for child in node.children:
|
|
344
|
+
_walk_tree(child, symbols, language)
|
|
345
|
+
|
|
346
|
+
|
|
347
|
+
def _extract_with_regex(text: str, language: str) -> list[dict]:
|
|
348
|
+
"""Regex-based symbol extraction fallback."""
|
|
349
|
+
import re
|
|
350
|
+
|
|
351
|
+
symbols = []
|
|
352
|
+
lines = text.split("\n")
|
|
353
|
+
|
|
354
|
+
patterns = {
|
|
355
|
+
"python": [
|
|
356
|
+
(r"^(?:async\s+)?def\s+(\w+)", "function"),
|
|
357
|
+
(r"^class\s+(\w+)", "class"),
|
|
358
|
+
],
|
|
359
|
+
"javascript": [
|
|
360
|
+
(r"(?:export\s+)?(?:async\s+)?function\s+(\w+)", "function"),
|
|
361
|
+
(r"(?:export\s+)?class\s+(\w+)", "class"),
|
|
362
|
+
(r"(?:export\s+)?const\s+(\w+)\s*=", "constant"),
|
|
363
|
+
],
|
|
364
|
+
"typescript": [
|
|
365
|
+
(r"(?:export\s+)?(?:async\s+)?function\s+(\w+)", "function"),
|
|
366
|
+
(r"(?:export\s+)?class\s+(\w+)", "class"),
|
|
367
|
+
(r"(?:export\s+)?interface\s+(\w+)", "interface"),
|
|
368
|
+
(r"(?:export\s+)?type\s+(\w+)", "type"),
|
|
369
|
+
],
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
lang_patterns = patterns.get(language, patterns.get("javascript", []))
|
|
373
|
+
|
|
374
|
+
for i, line in enumerate(lines, 1):
|
|
375
|
+
stripped = line.strip()
|
|
376
|
+
for pattern, kind in lang_patterns:
|
|
377
|
+
match = re.match(pattern, stripped)
|
|
378
|
+
if match:
|
|
379
|
+
is_async = "async" in stripped.split("def ")[0] if "def " in stripped else "async" in stripped.split("function")[0] if "function" in stripped else False
|
|
380
|
+
# Extract signature from the line
|
|
381
|
+
sig = None
|
|
382
|
+
paren_start = stripped.find("(")
|
|
383
|
+
if paren_start != -1:
|
|
384
|
+
# Find matching close paren (simple single-line)
|
|
385
|
+
paren_end = stripped.rfind(")")
|
|
386
|
+
if paren_end > paren_start:
|
|
387
|
+
sig = stripped[paren_start:paren_end + 1]
|
|
388
|
+
# Check for return type annotation
|
|
389
|
+
rest = stripped[paren_end + 1:].strip()
|
|
390
|
+
if rest.startswith("->"):
|
|
391
|
+
sig += f" {rest.split(':')[0].strip()}"
|
|
392
|
+
symbols.append({
|
|
393
|
+
"name": match.group(1),
|
|
394
|
+
"kind": kind,
|
|
395
|
+
"line_start": i,
|
|
396
|
+
"line_end": i,
|
|
397
|
+
"signature": sig,
|
|
398
|
+
"exported": "export" in line,
|
|
399
|
+
"metadata": {"async": True} if is_async else {},
|
|
400
|
+
})
|
|
401
|
+
break
|
|
402
|
+
|
|
403
|
+
return symbols
|