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,29 @@
|
|
|
1
|
+
"""Indexing pipeline -- full and delta indexing with progress tracking.
|
|
2
|
+
|
|
3
|
+
Exports are lazy-loaded to avoid pulling in tree-sitter and AST parsing
|
|
4
|
+
modules at import time.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import importlib
|
|
10
|
+
from typing import Any
|
|
11
|
+
|
|
12
|
+
__all__ = ["FullIndexer", "DeltaIndexer"]
|
|
13
|
+
|
|
14
|
+
# Lazy-load map: attribute name -> (module_path, attribute_name)
|
|
15
|
+
_LAZY_IMPORTS: dict[str, tuple[str, str]] = {
|
|
16
|
+
"DeltaIndexer": ("attocode_intel.indexing.delta_indexer", "DeltaIndexer"),
|
|
17
|
+
"FullIndexer": ("attocode_intel.indexing.full_indexer", "FullIndexer"),
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def __getattr__(name: str) -> Any:
|
|
22
|
+
if name in _LAZY_IMPORTS:
|
|
23
|
+
module_path, attr = _LAZY_IMPORTS[name]
|
|
24
|
+
mod = importlib.import_module(module_path)
|
|
25
|
+
value = getattr(mod, attr)
|
|
26
|
+
# Cache on the module so subsequent accesses are fast
|
|
27
|
+
globals()[name] = value
|
|
28
|
+
return value
|
|
29
|
+
raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
"""Per-project file change debouncer.
|
|
2
|
+
|
|
3
|
+
Collects file change notifications and fires a batch update after a
|
|
4
|
+
configurable quiet period (default 500ms). Prevents redundant re-parsing
|
|
5
|
+
when a single tool call writes multiple files.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import asyncio
|
|
11
|
+
import logging
|
|
12
|
+
from collections import defaultdict
|
|
13
|
+
from typing import Awaitable, Callable
|
|
14
|
+
|
|
15
|
+
logger = logging.getLogger(__name__)
|
|
16
|
+
|
|
17
|
+
# Type alias for the batch handler
|
|
18
|
+
# handler(project_id, branch, paths, files)
|
|
19
|
+
BatchHandler = Callable[[str, str, list[str], dict[str, str] | None], Awaitable[None]]
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
_MAX_PENDING_PER_KEY = 10_000 # M9: cap pending paths to prevent unbounded memory
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class FileChangeDebouncer:
|
|
26
|
+
"""Debounces file change notifications per (project, branch).
|
|
27
|
+
|
|
28
|
+
Usage:
|
|
29
|
+
debouncer = FileChangeDebouncer(handler=my_batch_handler)
|
|
30
|
+
await debouncer.notify("proj-1", "feat/x", ["src/a.py", "src/b.py"])
|
|
31
|
+
# ... 200ms later ...
|
|
32
|
+
await debouncer.notify("proj-1", "feat/x", ["src/c.py"])
|
|
33
|
+
# handler fires ~500ms after last notify with all 3 paths
|
|
34
|
+
"""
|
|
35
|
+
|
|
36
|
+
def __init__(
|
|
37
|
+
self,
|
|
38
|
+
handler: BatchHandler,
|
|
39
|
+
delay_seconds: float = 0.5,
|
|
40
|
+
max_pending_per_key: int = _MAX_PENDING_PER_KEY,
|
|
41
|
+
) -> None:
|
|
42
|
+
self._handler = handler
|
|
43
|
+
self._delay = delay_seconds
|
|
44
|
+
self._max_pending = max_pending_per_key
|
|
45
|
+
# Pending paths per (project_id, branch)
|
|
46
|
+
self._pending: dict[tuple[str, str], set[str]] = defaultdict(set)
|
|
47
|
+
# Pending file contents per (project_id, branch) — path → base64 content
|
|
48
|
+
self._pending_files: dict[tuple[str, str], dict[str, str]] = defaultdict(dict)
|
|
49
|
+
# Active timers per (project_id, branch)
|
|
50
|
+
self._timers: dict[tuple[str, str], asyncio.Task] = {}
|
|
51
|
+
|
|
52
|
+
async def notify(self, project_id: str, branch: str, paths: list[str], *, files: dict[str, str] | None = None) -> None:
|
|
53
|
+
"""Add paths to the pending batch for (project_id, branch).
|
|
54
|
+
|
|
55
|
+
Resets the debounce timer. Handler fires after `delay_seconds` of quiet.
|
|
56
|
+
M9: Caps pending paths per key at max_pending_per_key.
|
|
57
|
+
"""
|
|
58
|
+
key = (project_id, branch)
|
|
59
|
+
pending = self._pending[key]
|
|
60
|
+
pending.update(paths)
|
|
61
|
+
|
|
62
|
+
# Accumulate file contents (latest wins per path)
|
|
63
|
+
if files:
|
|
64
|
+
self._pending_files[key].update(files)
|
|
65
|
+
|
|
66
|
+
# M9: cap pending paths to prevent unbounded memory
|
|
67
|
+
if len(pending) > self._max_pending:
|
|
68
|
+
logger.warning(
|
|
69
|
+
"Debouncer: pending paths for %s/%s exceeded %d, truncating",
|
|
70
|
+
project_id, branch, self._max_pending,
|
|
71
|
+
)
|
|
72
|
+
# Keep first max_pending items (arbitrary but bounded)
|
|
73
|
+
excess = len(pending) - self._max_pending
|
|
74
|
+
for _ in range(excess):
|
|
75
|
+
pending.pop()
|
|
76
|
+
|
|
77
|
+
# Cancel existing timer if any
|
|
78
|
+
if key in self._timers:
|
|
79
|
+
self._timers[key].cancel()
|
|
80
|
+
|
|
81
|
+
# Start new timer
|
|
82
|
+
self._timers[key] = asyncio.create_task(self._fire_after_delay(key))
|
|
83
|
+
|
|
84
|
+
async def _fire_after_delay(self, key: tuple[str, str]) -> None:
|
|
85
|
+
"""Wait for the quiet period, then fire the batch handler."""
|
|
86
|
+
try:
|
|
87
|
+
await asyncio.sleep(self._delay)
|
|
88
|
+
except asyncio.CancelledError:
|
|
89
|
+
return
|
|
90
|
+
|
|
91
|
+
# Copy batch, then clear pending — prevents lost updates if new
|
|
92
|
+
# paths arrive between pop and handler execution.
|
|
93
|
+
batch = self._pending.pop(key, set())
|
|
94
|
+
files = self._pending_files.pop(key, None) or None
|
|
95
|
+
paths = list(batch)
|
|
96
|
+
self._timers.pop(key, None)
|
|
97
|
+
|
|
98
|
+
if not paths:
|
|
99
|
+
return
|
|
100
|
+
|
|
101
|
+
project_id, branch = key
|
|
102
|
+
logger.info(
|
|
103
|
+
"Debouncer firing for %s/%s: %d files",
|
|
104
|
+
project_id, branch, len(paths),
|
|
105
|
+
)
|
|
106
|
+
try:
|
|
107
|
+
await self._handler(project_id, branch, paths, files)
|
|
108
|
+
except Exception:
|
|
109
|
+
logger.exception(
|
|
110
|
+
"Error in debounce handler for %s/%s",
|
|
111
|
+
project_id, branch,
|
|
112
|
+
)
|
|
113
|
+
|
|
114
|
+
async def flush(self, project_id: str, branch: str) -> None:
|
|
115
|
+
"""Force-fire pending batch immediately (e.g., on shutdown)."""
|
|
116
|
+
key = (project_id, branch)
|
|
117
|
+
if key in self._timers:
|
|
118
|
+
self._timers[key].cancel()
|
|
119
|
+
self._timers.pop(key, None)
|
|
120
|
+
paths = list(self._pending.pop(key, set()))
|
|
121
|
+
files = self._pending_files.pop(key, None) or None
|
|
122
|
+
if paths:
|
|
123
|
+
await self._handler(project_id, branch, paths, files)
|
|
124
|
+
|
|
125
|
+
async def shutdown(self) -> None:
|
|
126
|
+
"""Cancel all pending timers. Call on app shutdown."""
|
|
127
|
+
for task in self._timers.values():
|
|
128
|
+
task.cancel()
|
|
129
|
+
self._timers.clear()
|
|
130
|
+
self._pending.clear()
|
|
131
|
+
self._pending_files.clear()
|
|
132
|
+
|
|
133
|
+
@property
|
|
134
|
+
def pending_count(self) -> int:
|
|
135
|
+
"""Number of pending batches."""
|
|
136
|
+
return len(self._pending)
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
"""Delta indexer — processes only changed files between two refs.
|
|
2
|
+
|
|
3
|
+
Uses content-hash comparison to skip files whose content hasn't actually changed
|
|
4
|
+
(e.g., git reports a mode change but the content is identical).
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import logging
|
|
10
|
+
import uuid
|
|
11
|
+
from typing import TYPE_CHECKING, Callable
|
|
12
|
+
|
|
13
|
+
from attocode_intel.indexing.parser import detect_language, extract_symbols
|
|
14
|
+
|
|
15
|
+
if TYPE_CHECKING:
|
|
16
|
+
from sqlalchemy.ext.asyncio import AsyncSession
|
|
17
|
+
|
|
18
|
+
from attocode_intel.git.manager import GitRepoManager
|
|
19
|
+
|
|
20
|
+
logger = logging.getLogger(__name__)
|
|
21
|
+
|
|
22
|
+
_MAX_FILE_SIZE = 1_000_000
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class DeltaIndexer:
|
|
26
|
+
"""Delta indexer — only processes changed files.
|
|
27
|
+
|
|
28
|
+
Computes the git diff between the previously indexed commit and the
|
|
29
|
+
current HEAD, then processes only additions, modifications, and deletions.
|
|
30
|
+
|
|
31
|
+
Content-hash gating: even for files git reports as changed, we compare
|
|
32
|
+
the SHA-256 of the new content to the existing manifest entry. If the
|
|
33
|
+
content hash matches, we skip re-parsing (idempotent).
|
|
34
|
+
"""
|
|
35
|
+
|
|
36
|
+
def __init__(
|
|
37
|
+
self,
|
|
38
|
+
session: AsyncSession,
|
|
39
|
+
git_manager: GitRepoManager,
|
|
40
|
+
progress_callback: Callable[[dict], None] | None = None,
|
|
41
|
+
) -> None:
|
|
42
|
+
self._session = session
|
|
43
|
+
self._git = git_manager
|
|
44
|
+
self._progress = progress_callback or (lambda _: None)
|
|
45
|
+
|
|
46
|
+
async def index(
|
|
47
|
+
self,
|
|
48
|
+
repo_id: str,
|
|
49
|
+
branch_id: uuid.UUID,
|
|
50
|
+
from_ref: str,
|
|
51
|
+
to_ref: str,
|
|
52
|
+
) -> dict:
|
|
53
|
+
"""Perform a delta index between two refs.
|
|
54
|
+
|
|
55
|
+
Returns stats dict: {added, modified, deleted, skipped_unchanged,
|
|
56
|
+
symbols_updated, errors, duration_ms}.
|
|
57
|
+
"""
|
|
58
|
+
import time
|
|
59
|
+
|
|
60
|
+
from attocode_intel.storage.branch_overlay import BranchOverlay
|
|
61
|
+
from attocode_intel.storage.content_store import ContentStore
|
|
62
|
+
from attocode_intel.storage.symbol_store import SymbolStore
|
|
63
|
+
|
|
64
|
+
start = time.monotonic()
|
|
65
|
+
content_store = ContentStore(self._session)
|
|
66
|
+
symbol_store = SymbolStore(self._session)
|
|
67
|
+
overlay = BranchOverlay(self._session)
|
|
68
|
+
|
|
69
|
+
stats = {
|
|
70
|
+
"added": 0,
|
|
71
|
+
"modified": 0,
|
|
72
|
+
"deleted": 0,
|
|
73
|
+
"skipped_unchanged": 0,
|
|
74
|
+
"symbols_updated": 0,
|
|
75
|
+
"errors": 0,
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
# Get current manifest for content-hash comparison
|
|
79
|
+
manifest = await overlay.resolve_manifest(branch_id)
|
|
80
|
+
|
|
81
|
+
diff_entries = self._git.get_diff(repo_id, from_ref, to_ref)
|
|
82
|
+
total = len(diff_entries)
|
|
83
|
+
|
|
84
|
+
self._progress({"phase": "delta_indexing", "total": total, "current": 0})
|
|
85
|
+
|
|
86
|
+
# Batch overlay updates for single version bump
|
|
87
|
+
overlay_updates: list[tuple[str, str, str]] = []
|
|
88
|
+
|
|
89
|
+
for i, entry in enumerate(diff_entries):
|
|
90
|
+
try:
|
|
91
|
+
if entry.status == "deleted":
|
|
92
|
+
await overlay.delete_file(branch_id, entry.path)
|
|
93
|
+
stats["deleted"] += 1
|
|
94
|
+
elif entry.status in ("added", "modified"):
|
|
95
|
+
content = self._git.read_file(repo_id, to_ref, entry.path)
|
|
96
|
+
if len(content) > _MAX_FILE_SIZE:
|
|
97
|
+
continue
|
|
98
|
+
|
|
99
|
+
# Content-hash gating
|
|
100
|
+
sha = content_store.hash_content(content)
|
|
101
|
+
if sha == manifest.get(entry.path):
|
|
102
|
+
stats["skipped_unchanged"] += 1
|
|
103
|
+
continue
|
|
104
|
+
|
|
105
|
+
language = detect_language(entry.path)
|
|
106
|
+
await content_store.store(content, language)
|
|
107
|
+
overlay_updates.append((entry.path, sha, entry.status))
|
|
108
|
+
|
|
109
|
+
# Extract and update symbols (skip if already indexed for this SHA)
|
|
110
|
+
symbols = extract_symbols(content, entry.path)
|
|
111
|
+
if symbols:
|
|
112
|
+
count = await symbol_store.upsert_symbols(
|
|
113
|
+
sha, symbols, skip_if_exists=True
|
|
114
|
+
)
|
|
115
|
+
stats["symbols_updated"] += count or len(symbols)
|
|
116
|
+
|
|
117
|
+
stats[entry.status] = stats.get(entry.status, 0) + 1
|
|
118
|
+
elif entry.status == "renamed":
|
|
119
|
+
if entry.old_path:
|
|
120
|
+
await overlay.delete_file(branch_id, entry.old_path)
|
|
121
|
+
content = self._git.read_file(repo_id, to_ref, entry.path)
|
|
122
|
+
if len(content) <= _MAX_FILE_SIZE:
|
|
123
|
+
sha = content_store.hash_content(content)
|
|
124
|
+
language = detect_language(entry.path)
|
|
125
|
+
await content_store.store(content, language)
|
|
126
|
+
overlay_updates.append((entry.path, sha, "added"))
|
|
127
|
+
stats["modified"] += 1
|
|
128
|
+
|
|
129
|
+
except Exception as e:
|
|
130
|
+
logger.warning("Error delta-indexing %s: %s", entry.path, e)
|
|
131
|
+
stats["errors"] += 1
|
|
132
|
+
|
|
133
|
+
if (i + 1) % 50 == 0:
|
|
134
|
+
self._progress({"phase": "delta_indexing", "total": total, "current": i + 1})
|
|
135
|
+
|
|
136
|
+
# Batch overlay update (single version bump)
|
|
137
|
+
if overlay_updates:
|
|
138
|
+
await overlay.set_files_batch(branch_id, overlay_updates)
|
|
139
|
+
|
|
140
|
+
await self._session.commit()
|
|
141
|
+
|
|
142
|
+
duration_ms = int((time.monotonic() - start) * 1000)
|
|
143
|
+
stats["duration_ms"] = duration_ms
|
|
144
|
+
|
|
145
|
+
self._progress({"phase": "completed", "stats": stats})
|
|
146
|
+
logger.info(
|
|
147
|
+
"Delta index complete: +%d ~%d -%d =%d(unchanged), %d symbols in %dms",
|
|
148
|
+
stats["added"], stats["modified"], stats["deleted"],
|
|
149
|
+
stats["skipped_unchanged"], stats["symbols_updated"], duration_ms,
|
|
150
|
+
)
|
|
151
|
+
return stats
|
|
@@ -0,0 +1,323 @@
|
|
|
1
|
+
"""Full indexer — reads all files from git tree, indexes content, symbols, deps.
|
|
2
|
+
|
|
3
|
+
Uses content-addressed storage: files with the same content across branches
|
|
4
|
+
are stored and indexed only once.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import logging
|
|
10
|
+
import uuid
|
|
11
|
+
from typing import TYPE_CHECKING, Callable
|
|
12
|
+
|
|
13
|
+
from attocode_intel.indexing.parser import detect_language, extract_imports, extract_references, extract_symbols
|
|
14
|
+
|
|
15
|
+
if TYPE_CHECKING:
|
|
16
|
+
from sqlalchemy.ext.asyncio import AsyncSession
|
|
17
|
+
|
|
18
|
+
from attocode_intel.git.manager import GitRepoManager
|
|
19
|
+
|
|
20
|
+
logger = logging.getLogger(__name__)
|
|
21
|
+
|
|
22
|
+
# Skip binary/large files
|
|
23
|
+
_SKIP_EXTENSIONS = {".png", ".jpg", ".jpeg", ".gif", ".ico", ".svg", ".woff", ".woff2",
|
|
24
|
+
".ttf", ".eot", ".pdf", ".zip", ".tar", ".gz", ".bin", ".exe",
|
|
25
|
+
".dll", ".so", ".dylib", ".pyc", ".pyo", ".class", ".o"}
|
|
26
|
+
_MAX_FILE_SIZE = 1_000_000 # 1MB
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def _load_path_aliases(known_paths: set[str], config_contents: dict[str, bytes]) -> dict[str, str]:
|
|
30
|
+
"""Parse tsconfig.json/jsconfig.json for path alias mappings.
|
|
31
|
+
|
|
32
|
+
Returns dict mapping alias prefix to resolved directory.
|
|
33
|
+
E.g., {"@/": "frontend/src/"} from paths: {"@/*": ["./src/*"]}
|
|
34
|
+
"""
|
|
35
|
+
import json
|
|
36
|
+
from pathlib import PurePosixPath
|
|
37
|
+
|
|
38
|
+
aliases: dict[str, str] = {}
|
|
39
|
+
config_names = [p for p in known_paths if p.endswith(("tsconfig.json", "jsconfig.json"))]
|
|
40
|
+
|
|
41
|
+
for config_path in config_names:
|
|
42
|
+
raw = config_contents.get(config_path)
|
|
43
|
+
if not raw:
|
|
44
|
+
continue
|
|
45
|
+
try:
|
|
46
|
+
config = json.loads(raw.decode("utf-8", errors="replace"))
|
|
47
|
+
paths = config.get("compilerOptions", {}).get("paths", {})
|
|
48
|
+
base_url = config.get("compilerOptions", {}).get("baseUrl", ".")
|
|
49
|
+
config_dir = str(PurePosixPath(config_path).parent)
|
|
50
|
+
|
|
51
|
+
for alias_pattern, targets in paths.items():
|
|
52
|
+
if not targets:
|
|
53
|
+
continue
|
|
54
|
+
prefix = alias_pattern.rstrip("*")
|
|
55
|
+
target = targets[0].rstrip("*")
|
|
56
|
+
resolved = str(PurePosixPath(config_dir) / base_url / target)
|
|
57
|
+
resolved = resolved.lstrip("./")
|
|
58
|
+
if not resolved.endswith("/"):
|
|
59
|
+
resolved += "/"
|
|
60
|
+
aliases[prefix] = resolved
|
|
61
|
+
except (json.JSONDecodeError, KeyError, UnicodeDecodeError):
|
|
62
|
+
continue
|
|
63
|
+
return aliases
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def _resolve_import_path(
|
|
67
|
+
import_name: str,
|
|
68
|
+
source_path: str,
|
|
69
|
+
known_paths: set[str],
|
|
70
|
+
language: str | None,
|
|
71
|
+
aliases: dict[str, str] | None = None,
|
|
72
|
+
) -> str | None:
|
|
73
|
+
"""Resolve an import string to a file path in the repo.
|
|
74
|
+
|
|
75
|
+
Returns the matching path or None if unresolvable.
|
|
76
|
+
"""
|
|
77
|
+
from pathlib import PurePosixPath
|
|
78
|
+
|
|
79
|
+
if language == "python":
|
|
80
|
+
# foo.bar -> foo/bar.py or foo/bar/__init__.py
|
|
81
|
+
parts = import_name.replace(".", "/")
|
|
82
|
+
candidates = [f"{parts}.py", f"{parts}/__init__.py"]
|
|
83
|
+
elif language in ("javascript", "typescript"):
|
|
84
|
+
if import_name.startswith("."):
|
|
85
|
+
# Relative import — resolve against source dir
|
|
86
|
+
source_dir = str(PurePosixPath(source_path).parent)
|
|
87
|
+
base = str(PurePosixPath(source_dir) / import_name)
|
|
88
|
+
# Normalize path (remove ../)
|
|
89
|
+
base = str(PurePosixPath(base))
|
|
90
|
+
else:
|
|
91
|
+
# Try path aliases first
|
|
92
|
+
base = None
|
|
93
|
+
if aliases:
|
|
94
|
+
for prefix, target_dir in aliases.items():
|
|
95
|
+
if import_name.startswith(prefix):
|
|
96
|
+
remainder = import_name[len(prefix):]
|
|
97
|
+
base = f"{target_dir}{remainder}"
|
|
98
|
+
break
|
|
99
|
+
if base is None:
|
|
100
|
+
# Bare specifier (node_modules) — skip
|
|
101
|
+
return None
|
|
102
|
+
candidates = [
|
|
103
|
+
f"{base}.ts", f"{base}.tsx", f"{base}.js", f"{base}.jsx",
|
|
104
|
+
f"{base}/index.ts", f"{base}/index.tsx",
|
|
105
|
+
f"{base}/index.js", f"{base}/index.jsx",
|
|
106
|
+
base, # exact match (e.g., .json, .css)
|
|
107
|
+
]
|
|
108
|
+
else:
|
|
109
|
+
return None
|
|
110
|
+
|
|
111
|
+
for c in candidates:
|
|
112
|
+
# Normalize: strip leading ./ if any
|
|
113
|
+
c = c.lstrip("./")
|
|
114
|
+
if c in known_paths:
|
|
115
|
+
return c
|
|
116
|
+
return None
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
class FullIndexer:
|
|
120
|
+
"""Full repository indexer.
|
|
121
|
+
|
|
122
|
+
Reads all files from a git tree at a specific ref, computes content hashes,
|
|
123
|
+
extracts symbols, and stores everything in content-addressed storage.
|
|
124
|
+
|
|
125
|
+
Content dedup: if a file's content has already been stored (from another
|
|
126
|
+
branch or previous indexing), the content, symbols, and embeddings are
|
|
127
|
+
reused automatically via SHA-256 keying.
|
|
128
|
+
"""
|
|
129
|
+
|
|
130
|
+
def __init__(
|
|
131
|
+
self,
|
|
132
|
+
session: AsyncSession,
|
|
133
|
+
git_manager: GitRepoManager,
|
|
134
|
+
progress_callback: Callable[[dict], None] | None = None,
|
|
135
|
+
) -> None:
|
|
136
|
+
self._session = session
|
|
137
|
+
self._git = git_manager
|
|
138
|
+
self._progress = progress_callback or (lambda _: None)
|
|
139
|
+
|
|
140
|
+
async def index(
|
|
141
|
+
self,
|
|
142
|
+
repo_id: str,
|
|
143
|
+
branch_id: uuid.UUID,
|
|
144
|
+
ref: str = "main",
|
|
145
|
+
) -> dict:
|
|
146
|
+
"""Perform a full index of a branch.
|
|
147
|
+
|
|
148
|
+
Returns stats dict: {files_indexed, symbols_found, skipped_existing,
|
|
149
|
+
errors, duration_ms}.
|
|
150
|
+
"""
|
|
151
|
+
import time
|
|
152
|
+
|
|
153
|
+
from attocode_intel.storage.branch_overlay import BranchOverlay
|
|
154
|
+
from attocode_intel.storage.content_store import ContentStore
|
|
155
|
+
from attocode_intel.storage.dependency_store import DependencyStore
|
|
156
|
+
from attocode_intel.storage.symbol_store import SymbolStore
|
|
157
|
+
|
|
158
|
+
start = time.monotonic()
|
|
159
|
+
content_store = ContentStore(self._session)
|
|
160
|
+
symbol_store = SymbolStore(self._session)
|
|
161
|
+
dep_store = DependencyStore(self._session)
|
|
162
|
+
overlay = BranchOverlay(self._session)
|
|
163
|
+
|
|
164
|
+
stats = {
|
|
165
|
+
"files_indexed": 0,
|
|
166
|
+
"symbols_found": 0,
|
|
167
|
+
"references_found": 0,
|
|
168
|
+
"skipped_existing": 0,
|
|
169
|
+
"skipped_binary": 0,
|
|
170
|
+
"dependencies_found": 0,
|
|
171
|
+
"errors": 0,
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
# Get all files from git tree recursively
|
|
175
|
+
all_files = self._walk_tree(repo_id, ref)
|
|
176
|
+
total = len(all_files)
|
|
177
|
+
known_paths = {path for path, _ in all_files}
|
|
178
|
+
|
|
179
|
+
self._progress({"phase": "indexing", "total": total, "current": 0})
|
|
180
|
+
|
|
181
|
+
# Capture tsconfig/jsconfig contents for path alias resolution
|
|
182
|
+
config_contents: dict[str, bytes] = {}
|
|
183
|
+
|
|
184
|
+
# Batch overlay updates
|
|
185
|
+
overlay_updates: list[tuple[str, str, str]] = []
|
|
186
|
+
# Track path->sha for dependency resolution (second pass)
|
|
187
|
+
path_to_sha: dict[str, str] = {}
|
|
188
|
+
# Deferred import extraction: (path, sha, imports, language)
|
|
189
|
+
deferred_deps: list[tuple[str, str, list[str], str | None]] = []
|
|
190
|
+
# Deferred reference extraction: (sha, content, path)
|
|
191
|
+
deferred_refs: list[tuple[str, bytes, str]] = []
|
|
192
|
+
|
|
193
|
+
for i, (path, oid) in enumerate(all_files):
|
|
194
|
+
try:
|
|
195
|
+
# Skip binary/large files
|
|
196
|
+
ext = "." + path.rsplit(".", 1)[-1] if "." in path else ""
|
|
197
|
+
if ext.lower() in _SKIP_EXTENSIONS:
|
|
198
|
+
stats["skipped_binary"] += 1
|
|
199
|
+
continue
|
|
200
|
+
|
|
201
|
+
content = self._git.read_file(repo_id, ref, path)
|
|
202
|
+
if path.endswith(("tsconfig.json", "jsconfig.json")):
|
|
203
|
+
config_contents[path] = content
|
|
204
|
+
if len(content) > _MAX_FILE_SIZE:
|
|
205
|
+
stats["skipped_binary"] += 1
|
|
206
|
+
continue
|
|
207
|
+
|
|
208
|
+
# Content-addressed storage — dedup across branches
|
|
209
|
+
language = detect_language(path)
|
|
210
|
+
sha = await content_store.store(content, language)
|
|
211
|
+
overlay_updates.append((path, sha, "added"))
|
|
212
|
+
path_to_sha[path] = sha
|
|
213
|
+
|
|
214
|
+
# Extract symbols (skip if SHA already has symbols — cross-branch dedup)
|
|
215
|
+
symbols = extract_symbols(content, path)
|
|
216
|
+
if symbols:
|
|
217
|
+
count = await symbol_store.upsert_symbols(
|
|
218
|
+
sha, symbols, skip_if_exists=True
|
|
219
|
+
)
|
|
220
|
+
if count == 0:
|
|
221
|
+
stats["skipped_existing"] += 1
|
|
222
|
+
else:
|
|
223
|
+
stats["symbols_found"] += count
|
|
224
|
+
|
|
225
|
+
# Extract imports (defer resolution until all paths are known)
|
|
226
|
+
imports = extract_imports(content, path, language)
|
|
227
|
+
if imports:
|
|
228
|
+
deferred_deps.append((path, sha, imports, language))
|
|
229
|
+
|
|
230
|
+
# Defer reference extraction for third pass
|
|
231
|
+
if language in ("python", "javascript", "typescript"):
|
|
232
|
+
deferred_refs.append((sha, content, path))
|
|
233
|
+
|
|
234
|
+
stats["files_indexed"] += 1
|
|
235
|
+
|
|
236
|
+
if (i + 1) % 100 == 0:
|
|
237
|
+
self._progress({"phase": "indexing", "total": total, "current": i + 1})
|
|
238
|
+
# Flush batch overlay updates periodically
|
|
239
|
+
if overlay_updates:
|
|
240
|
+
await overlay.set_files_batch(branch_id, overlay_updates)
|
|
241
|
+
overlay_updates = []
|
|
242
|
+
await self._session.flush()
|
|
243
|
+
|
|
244
|
+
except Exception as e:
|
|
245
|
+
logger.warning("Error indexing %s: %s", path, e)
|
|
246
|
+
stats["errors"] += 1
|
|
247
|
+
|
|
248
|
+
# Final batch
|
|
249
|
+
if overlay_updates:
|
|
250
|
+
await overlay.set_files_batch(branch_id, overlay_updates)
|
|
251
|
+
|
|
252
|
+
# Resolve path aliases from tsconfig/jsconfig
|
|
253
|
+
aliases = _load_path_aliases(known_paths, config_contents)
|
|
254
|
+
|
|
255
|
+
# Second pass: resolve imports to dependencies
|
|
256
|
+
self._progress({"phase": "resolving_dependencies", "total": len(deferred_deps), "current": 0})
|
|
257
|
+
for idx, (source_path, source_sha, imports, language) in enumerate(deferred_deps):
|
|
258
|
+
try:
|
|
259
|
+
if await dep_store.has_dependencies(source_sha):
|
|
260
|
+
continue
|
|
261
|
+
deps: list[dict] = []
|
|
262
|
+
for imp in imports:
|
|
263
|
+
target_path = _resolve_import_path(imp, source_path, known_paths, language, aliases=aliases)
|
|
264
|
+
if target_path and target_path in path_to_sha:
|
|
265
|
+
deps.append({
|
|
266
|
+
"target_sha": path_to_sha[target_path],
|
|
267
|
+
"dep_type": "import",
|
|
268
|
+
"weight": 1.0,
|
|
269
|
+
})
|
|
270
|
+
if deps:
|
|
271
|
+
await dep_store.upsert_dependencies(source_sha, deps)
|
|
272
|
+
stats["dependencies_found"] += len(deps)
|
|
273
|
+
except Exception as e:
|
|
274
|
+
logger.warning("Error resolving deps for %s: %s", source_path, e)
|
|
275
|
+
|
|
276
|
+
# Third pass: extract symbol references (call sites)
|
|
277
|
+
self._progress({"phase": "extracting_references", "total": len(deferred_refs), "current": 0})
|
|
278
|
+
from attocode_intel.db.models import SymbolReference
|
|
279
|
+
|
|
280
|
+
refs_seen_shas: set[str] = set()
|
|
281
|
+
for idx, (sha, content, file_path) in enumerate(deferred_refs):
|
|
282
|
+
if sha in refs_seen_shas:
|
|
283
|
+
continue
|
|
284
|
+
refs_seen_shas.add(sha)
|
|
285
|
+
try:
|
|
286
|
+
refs = extract_references(content, file_path)
|
|
287
|
+
for r in refs:
|
|
288
|
+
self._session.add(SymbolReference(
|
|
289
|
+
content_sha=sha,
|
|
290
|
+
symbol_name=r["symbol_name"],
|
|
291
|
+
ref_kind=r["ref_kind"],
|
|
292
|
+
line=r["line"],
|
|
293
|
+
caller_qualified_name=r.get("caller_qualified_name", ""),
|
|
294
|
+
))
|
|
295
|
+
stats["references_found"] += len(refs)
|
|
296
|
+
except Exception as e:
|
|
297
|
+
logger.warning("Error extracting refs for %s: %s", file_path, e)
|
|
298
|
+
|
|
299
|
+
await self._session.commit()
|
|
300
|
+
|
|
301
|
+
duration_ms = int((time.monotonic() - start) * 1000)
|
|
302
|
+
stats["duration_ms"] = duration_ms
|
|
303
|
+
|
|
304
|
+
self._progress({"phase": "completed", "stats": stats})
|
|
305
|
+
logger.info(
|
|
306
|
+
"Full index complete: %d files, %d symbols, %d refs, %d reused, %d errors in %dms",
|
|
307
|
+
stats["files_indexed"], stats["symbols_found"], stats["references_found"],
|
|
308
|
+
stats["skipped_existing"], stats["errors"], duration_ms,
|
|
309
|
+
)
|
|
310
|
+
return stats
|
|
311
|
+
|
|
312
|
+
def _walk_tree(self, repo_id: str, ref: str, path: str = "") -> list[tuple[str, str]]:
|
|
313
|
+
"""Recursively walk the git tree and return (path, oid) pairs."""
|
|
314
|
+
entries = self._git.get_tree(repo_id, ref, path)
|
|
315
|
+
files: list[tuple[str, str]] = []
|
|
316
|
+
|
|
317
|
+
for entry in entries:
|
|
318
|
+
if entry.type == "blob":
|
|
319
|
+
files.append((entry.path, entry.oid))
|
|
320
|
+
elif entry.type == "tree":
|
|
321
|
+
files.extend(self._walk_tree(repo_id, ref, entry.path))
|
|
322
|
+
|
|
323
|
+
return files
|