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,586 @@
|
|
|
1
|
+
"""SQLite-backed persistent index for symbols, references, and dependencies.
|
|
2
|
+
|
|
3
|
+
Stores parsed AST data so that ASTService can skip re-parsing unchanged
|
|
4
|
+
files on restart. Modeled after ``VectorStore`` (WAL mode, mtime tracking).
|
|
5
|
+
|
|
6
|
+
Database location: ``.attocode/index/symbols.db``
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import logging
|
|
12
|
+
import os
|
|
13
|
+
import sqlite3
|
|
14
|
+
import threading
|
|
15
|
+
import time
|
|
16
|
+
from dataclasses import dataclass, field
|
|
17
|
+
from typing import Any
|
|
18
|
+
|
|
19
|
+
from filelock import FileLock
|
|
20
|
+
|
|
21
|
+
logger = logging.getLogger(__name__)
|
|
22
|
+
|
|
23
|
+
SCHEMA_VERSION = "6"
|
|
24
|
+
SQLITE_BUSY_TIMEOUT_MS = 30_000
|
|
25
|
+
INDEX_INIT_LOCK_TIMEOUT_SECONDS = 30
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class IndexSchemaVersionError(RuntimeError):
|
|
29
|
+
"""The persisted structural index cannot be opened by this client version."""
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
@dataclass(slots=True)
|
|
33
|
+
class StoredFile:
|
|
34
|
+
"""Cached file metadata."""
|
|
35
|
+
|
|
36
|
+
path: str
|
|
37
|
+
mtime: float
|
|
38
|
+
size: int
|
|
39
|
+
language: str
|
|
40
|
+
line_count: int
|
|
41
|
+
content_hash: str
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
@dataclass(slots=True)
|
|
45
|
+
class StoredSymbol:
|
|
46
|
+
"""A persisted symbol definition."""
|
|
47
|
+
|
|
48
|
+
id: int
|
|
49
|
+
file_path: str
|
|
50
|
+
name: str
|
|
51
|
+
qualified_name: str
|
|
52
|
+
kind: str
|
|
53
|
+
line: int
|
|
54
|
+
end_line: int
|
|
55
|
+
source: str # "tree-sitter" | "lsp"
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
@dataclass(slots=True)
|
|
59
|
+
class StoredReference:
|
|
60
|
+
"""A persisted symbol reference."""
|
|
61
|
+
|
|
62
|
+
id: int
|
|
63
|
+
file_path: str
|
|
64
|
+
symbol_name: str
|
|
65
|
+
ref_kind: str
|
|
66
|
+
line: int
|
|
67
|
+
column: int
|
|
68
|
+
source: str # "tree-sitter" | "lsp"
|
|
69
|
+
syntax_name: str = "" # identifier/member expression as written in source
|
|
70
|
+
caller_qualified_name: str = "" # enclosing function/method (call-graph edge)
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
@dataclass(slots=True)
|
|
74
|
+
class IndexStore:
|
|
75
|
+
"""SQLite-backed persistent index store.
|
|
76
|
+
|
|
77
|
+
Usage::
|
|
78
|
+
|
|
79
|
+
store = IndexStore(db_path=".attocode/index/symbols.db")
|
|
80
|
+
store.save_file(StoredFile(...))
|
|
81
|
+
store.save_symbols("src/main.py", [StoredSymbol(...)])
|
|
82
|
+
stale = store.get_stale_files({"src/main.py": 1234567.0})
|
|
83
|
+
"""
|
|
84
|
+
|
|
85
|
+
db_path: str
|
|
86
|
+
_conn: sqlite3.Connection | None = field(default=None, repr=False)
|
|
87
|
+
_lock: threading.Lock = field(default_factory=threading.Lock, repr=False)
|
|
88
|
+
# When the on-disk schema differed from ``SCHEMA_VERSION`` at startup,
|
|
89
|
+
# we wipe and rebuild — this records the prior version so callers
|
|
90
|
+
# (e.g. ``readiness_report``) can surface a "schema migrated, full
|
|
91
|
+
# re-index pending" hint instead of leaving users guessing why the
|
|
92
|
+
# first request after upgrade is slow.
|
|
93
|
+
_schema_rebuilt_from: str | None = field(default=None, repr=False)
|
|
94
|
+
|
|
95
|
+
def __post_init__(self) -> None:
|
|
96
|
+
os.makedirs(os.path.dirname(self.db_path) or ".", exist_ok=True)
|
|
97
|
+
try:
|
|
98
|
+
if self.db_path == ":memory:":
|
|
99
|
+
self._initialize_connection()
|
|
100
|
+
else:
|
|
101
|
+
# Multiple MCP clients commonly share one workspace. Serialize
|
|
102
|
+
# schema inspection/rebuild so simultaneous restarts do not race
|
|
103
|
+
# over DDL while hydration is opening the same SQLite store.
|
|
104
|
+
with FileLock(f"{self.db_path}.init.lock", timeout=INDEX_INIT_LOCK_TIMEOUT_SECONDS):
|
|
105
|
+
self._initialize_connection()
|
|
106
|
+
except Exception:
|
|
107
|
+
if self._conn is not None:
|
|
108
|
+
self._conn.close()
|
|
109
|
+
self._conn = None
|
|
110
|
+
raise
|
|
111
|
+
|
|
112
|
+
def _initialize_connection(self) -> None:
|
|
113
|
+
self._conn = sqlite3.connect(
|
|
114
|
+
self.db_path,
|
|
115
|
+
check_same_thread=False,
|
|
116
|
+
timeout=SQLITE_BUSY_TIMEOUT_MS / 1000,
|
|
117
|
+
)
|
|
118
|
+
self._conn.execute(f"PRAGMA busy_timeout={SQLITE_BUSY_TIMEOUT_MS}")
|
|
119
|
+
self._conn.execute("PRAGMA journal_mode=WAL")
|
|
120
|
+
self._conn.execute("PRAGMA synchronous=NORMAL")
|
|
121
|
+
self._conn.execute("PRAGMA foreign_keys=ON")
|
|
122
|
+
self._migrate_stale_schema()
|
|
123
|
+
self._create_tables()
|
|
124
|
+
self._check_schema_version()
|
|
125
|
+
|
|
126
|
+
@staticmethod
|
|
127
|
+
def _compare_schema_version(stored: str) -> int:
|
|
128
|
+
try:
|
|
129
|
+
stored_version = int(stored)
|
|
130
|
+
current_version = int(SCHEMA_VERSION)
|
|
131
|
+
except ValueError as error:
|
|
132
|
+
raise IndexSchemaVersionError(
|
|
133
|
+
f"Unrecognized structural index schema {stored!r}; "
|
|
134
|
+
"upgrade Attocode or rebuild the workspace index."
|
|
135
|
+
) from error
|
|
136
|
+
return (stored_version > current_version) - (stored_version < current_version)
|
|
137
|
+
|
|
138
|
+
def _get_conn(self) -> sqlite3.Connection:
|
|
139
|
+
if self._conn is None:
|
|
140
|
+
raise RuntimeError("IndexStore connection is closed")
|
|
141
|
+
return self._conn
|
|
142
|
+
|
|
143
|
+
def _migrate_stale_schema(self) -> None:
|
|
144
|
+
"""Drop stale data tables BEFORE (re)creating them when the on-disk
|
|
145
|
+
schema predates the current version.
|
|
146
|
+
|
|
147
|
+
``_create_tables`` builds the v3 indexes (e.g. ``ix_refs_caller`` on
|
|
148
|
+
``refs(caller_qualified_name)``). On a pre-v3 DB whose ``refs`` table
|
|
149
|
+
lacks that column, creating the index raises ``OperationalError`` and
|
|
150
|
+
the store can't even open — so the wipe in ``_check_schema_version``
|
|
151
|
+
never runs. Dropping the stale tables here keeps the upgrade path
|
|
152
|
+
working; data is rebuilt from source on the next index. Children are
|
|
153
|
+
dropped before ``files`` to respect FK ordering.
|
|
154
|
+
"""
|
|
155
|
+
conn = self._get_conn()
|
|
156
|
+
try:
|
|
157
|
+
row = conn.execute(
|
|
158
|
+
"SELECT value FROM metadata WHERE key = 'schema_version'"
|
|
159
|
+
).fetchone()
|
|
160
|
+
except sqlite3.OperationalError:
|
|
161
|
+
return # no metadata table => brand-new DB, nothing to migrate
|
|
162
|
+
if not row or row[0] == SCHEMA_VERSION:
|
|
163
|
+
return
|
|
164
|
+
if self._compare_schema_version(row[0]) > 0:
|
|
165
|
+
raise IndexSchemaVersionError(
|
|
166
|
+
f"Structural index schema {row[0]} is newer than this client's "
|
|
167
|
+
f"schema {SCHEMA_VERSION}; restart using the upgraded Attocode executable."
|
|
168
|
+
)
|
|
169
|
+
logger.warning(
|
|
170
|
+
"Index schema is stale (%s, expected %s); dropping data tables in "
|
|
171
|
+
"%r so the new schema can be created — a full re-index will run on "
|
|
172
|
+
"next access.",
|
|
173
|
+
row[0], SCHEMA_VERSION, self.db_path,
|
|
174
|
+
)
|
|
175
|
+
conn.executescript(
|
|
176
|
+
"DROP TABLE IF EXISTS refs;"
|
|
177
|
+
"DROP TABLE IF EXISTS symbols;"
|
|
178
|
+
"DROP TABLE IF EXISTS dependencies;"
|
|
179
|
+
"DROP TABLE IF EXISTS files;"
|
|
180
|
+
)
|
|
181
|
+
conn.commit()
|
|
182
|
+
|
|
183
|
+
def _create_tables(self) -> None:
|
|
184
|
+
conn = self._get_conn()
|
|
185
|
+
conn.executescript("""
|
|
186
|
+
CREATE TABLE IF NOT EXISTS metadata (
|
|
187
|
+
key TEXT PRIMARY KEY,
|
|
188
|
+
value TEXT NOT NULL
|
|
189
|
+
);
|
|
190
|
+
|
|
191
|
+
CREATE TABLE IF NOT EXISTS files (
|
|
192
|
+
path TEXT PRIMARY KEY,
|
|
193
|
+
mtime REAL NOT NULL,
|
|
194
|
+
size INTEGER NOT NULL,
|
|
195
|
+
language TEXT NOT NULL DEFAULT '',
|
|
196
|
+
line_count INTEGER NOT NULL DEFAULT 0,
|
|
197
|
+
content_hash TEXT NOT NULL DEFAULT ''
|
|
198
|
+
);
|
|
199
|
+
|
|
200
|
+
CREATE TABLE IF NOT EXISTS symbols (
|
|
201
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
202
|
+
file_path TEXT NOT NULL REFERENCES files(path) ON DELETE CASCADE,
|
|
203
|
+
name TEXT NOT NULL,
|
|
204
|
+
qualified_name TEXT NOT NULL,
|
|
205
|
+
kind TEXT NOT NULL,
|
|
206
|
+
line INTEGER NOT NULL DEFAULT 0,
|
|
207
|
+
end_line INTEGER NOT NULL DEFAULT 0,
|
|
208
|
+
source TEXT NOT NULL DEFAULT 'tree-sitter'
|
|
209
|
+
);
|
|
210
|
+
|
|
211
|
+
CREATE TABLE IF NOT EXISTS refs (
|
|
212
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
213
|
+
file_path TEXT NOT NULL REFERENCES files(path) ON DELETE CASCADE,
|
|
214
|
+
symbol_name TEXT NOT NULL,
|
|
215
|
+
ref_kind TEXT NOT NULL DEFAULT 'call',
|
|
216
|
+
line INTEGER NOT NULL DEFAULT 0,
|
|
217
|
+
col INTEGER NOT NULL DEFAULT 0,
|
|
218
|
+
source TEXT NOT NULL DEFAULT 'tree-sitter',
|
|
219
|
+
syntax_name TEXT NOT NULL DEFAULT '',
|
|
220
|
+
caller_qualified_name TEXT NOT NULL DEFAULT ''
|
|
221
|
+
);
|
|
222
|
+
|
|
223
|
+
CREATE TABLE IF NOT EXISTS dependencies (
|
|
224
|
+
source_path TEXT NOT NULL,
|
|
225
|
+
target_path TEXT NOT NULL,
|
|
226
|
+
PRIMARY KEY (source_path, target_path)
|
|
227
|
+
);
|
|
228
|
+
|
|
229
|
+
CREATE INDEX IF NOT EXISTS ix_symbols_name ON symbols(name);
|
|
230
|
+
CREATE INDEX IF NOT EXISTS ix_symbols_qname ON symbols(qualified_name);
|
|
231
|
+
CREATE INDEX IF NOT EXISTS ix_symbols_file ON symbols(file_path);
|
|
232
|
+
CREATE INDEX IF NOT EXISTS ix_refs_symbol ON refs(symbol_name);
|
|
233
|
+
CREATE INDEX IF NOT EXISTS ix_refs_file ON refs(file_path);
|
|
234
|
+
CREATE INDEX IF NOT EXISTS ix_refs_caller ON refs(caller_qualified_name);
|
|
235
|
+
CREATE INDEX IF NOT EXISTS ix_deps_target ON dependencies(target_path);
|
|
236
|
+
""")
|
|
237
|
+
conn.commit()
|
|
238
|
+
|
|
239
|
+
def _check_schema_version(self) -> None:
|
|
240
|
+
conn = self._get_conn()
|
|
241
|
+
row = conn.execute(
|
|
242
|
+
"SELECT value FROM metadata WHERE key = 'schema_version'"
|
|
243
|
+
).fetchone()
|
|
244
|
+
if row:
|
|
245
|
+
if row[0] != SCHEMA_VERSION:
|
|
246
|
+
if self._compare_schema_version(row[0]) > 0:
|
|
247
|
+
raise IndexSchemaVersionError(
|
|
248
|
+
f"Structural index schema {row[0]} is newer than this client's "
|
|
249
|
+
f"schema {SCHEMA_VERSION}; restart using the upgraded Attocode executable."
|
|
250
|
+
)
|
|
251
|
+
# Schema migrations clear all persisted index data — the
|
|
252
|
+
# first request after upgrade will trigger a full
|
|
253
|
+
# re-index that may take a while on big repos. We log
|
|
254
|
+
# this prominently and stash the prior version on the
|
|
255
|
+
# store instance + in the SQLite metadata table so a
|
|
256
|
+
# future readiness probe can attribute a slow first
|
|
257
|
+
# request to the rebuild rather than to indexing bugs.
|
|
258
|
+
logger.warning(
|
|
259
|
+
"Index schema changed (%s -> %s); clearing %r and "
|
|
260
|
+
"rebuilding from source on next access — full "
|
|
261
|
+
"re-index will run on demand. Subsequent requests "
|
|
262
|
+
"may be slow until indexing catches up.",
|
|
263
|
+
row[0], SCHEMA_VERSION, self.db_path,
|
|
264
|
+
)
|
|
265
|
+
self.clear_all()
|
|
266
|
+
conn.execute(
|
|
267
|
+
"INSERT OR REPLACE INTO metadata (key, value) VALUES ('schema_version', ?)",
|
|
268
|
+
(SCHEMA_VERSION,),
|
|
269
|
+
)
|
|
270
|
+
conn.execute(
|
|
271
|
+
"INSERT OR REPLACE INTO metadata (key, value) "
|
|
272
|
+
"VALUES ('schema_rebuilt_from_version', ?)",
|
|
273
|
+
(row[0],),
|
|
274
|
+
)
|
|
275
|
+
conn.commit()
|
|
276
|
+
self._schema_rebuilt_from = row[0]
|
|
277
|
+
else:
|
|
278
|
+
conn.execute(
|
|
279
|
+
"INSERT OR REPLACE INTO metadata (key, value) VALUES ('schema_version', ?)",
|
|
280
|
+
(SCHEMA_VERSION,),
|
|
281
|
+
)
|
|
282
|
+
conn.commit()
|
|
283
|
+
|
|
284
|
+
# ------------------------------------------------------------------
|
|
285
|
+
# Metadata
|
|
286
|
+
# ------------------------------------------------------------------
|
|
287
|
+
|
|
288
|
+
def get_meta(self, key: str) -> str | None:
|
|
289
|
+
conn = self._get_conn()
|
|
290
|
+
with self._lock:
|
|
291
|
+
row = conn.execute(
|
|
292
|
+
"SELECT value FROM metadata WHERE key = ?", (key,)
|
|
293
|
+
).fetchone()
|
|
294
|
+
return row[0] if row else None
|
|
295
|
+
|
|
296
|
+
def set_meta(self, key: str, value: str) -> None:
|
|
297
|
+
conn = self._get_conn()
|
|
298
|
+
with self._lock:
|
|
299
|
+
conn.execute(
|
|
300
|
+
"INSERT OR REPLACE INTO metadata (key, value) VALUES (?, ?)",
|
|
301
|
+
(key, value),
|
|
302
|
+
)
|
|
303
|
+
conn.commit()
|
|
304
|
+
|
|
305
|
+
# ------------------------------------------------------------------
|
|
306
|
+
# Files
|
|
307
|
+
# ------------------------------------------------------------------
|
|
308
|
+
|
|
309
|
+
def save_file(self, f: StoredFile) -> None:
|
|
310
|
+
conn = self._get_conn()
|
|
311
|
+
with self._lock:
|
|
312
|
+
conn.execute(
|
|
313
|
+
"""INSERT OR REPLACE INTO files
|
|
314
|
+
(path, mtime, size, language, line_count, content_hash)
|
|
315
|
+
VALUES (?, ?, ?, ?, ?, ?)""",
|
|
316
|
+
(f.path, f.mtime, f.size, f.language, f.line_count, f.content_hash),
|
|
317
|
+
)
|
|
318
|
+
conn.commit()
|
|
319
|
+
|
|
320
|
+
def save_files_batch(self, files: list[StoredFile]) -> None:
|
|
321
|
+
conn = self._get_conn()
|
|
322
|
+
rows = [
|
|
323
|
+
(f.path, f.mtime, f.size, f.language, f.line_count, f.content_hash)
|
|
324
|
+
for f in files
|
|
325
|
+
]
|
|
326
|
+
with self._lock:
|
|
327
|
+
conn.executemany(
|
|
328
|
+
"""INSERT OR REPLACE INTO files
|
|
329
|
+
(path, mtime, size, language, line_count, content_hash)
|
|
330
|
+
VALUES (?, ?, ?, ?, ?, ?)""",
|
|
331
|
+
rows,
|
|
332
|
+
)
|
|
333
|
+
conn.commit()
|
|
334
|
+
|
|
335
|
+
def get_file(self, path: str) -> StoredFile | None:
|
|
336
|
+
conn = self._get_conn()
|
|
337
|
+
with self._lock:
|
|
338
|
+
row = conn.execute(
|
|
339
|
+
"SELECT path, mtime, size, language, line_count, content_hash "
|
|
340
|
+
"FROM files WHERE path = ?",
|
|
341
|
+
(path,),
|
|
342
|
+
).fetchone()
|
|
343
|
+
if row:
|
|
344
|
+
return StoredFile(*row)
|
|
345
|
+
return None
|
|
346
|
+
|
|
347
|
+
def get_all_files(self) -> list[StoredFile]:
|
|
348
|
+
conn = self._get_conn()
|
|
349
|
+
with self._lock:
|
|
350
|
+
rows = conn.execute(
|
|
351
|
+
"SELECT path, mtime, size, language, line_count, content_hash FROM files"
|
|
352
|
+
).fetchall()
|
|
353
|
+
return [StoredFile(*r) for r in rows]
|
|
354
|
+
|
|
355
|
+
def get_stale_files(self, file_mtimes: dict[str, float]) -> list[str]:
|
|
356
|
+
"""Return file paths where current mtime > stored mtime (or not indexed)."""
|
|
357
|
+
conn = self._get_conn()
|
|
358
|
+
with self._lock:
|
|
359
|
+
rows = conn.execute("SELECT path, mtime FROM files").fetchall()
|
|
360
|
+
indexed = {row[0]: row[1] for row in rows}
|
|
361
|
+
stale: list[str] = []
|
|
362
|
+
for fpath, current_mtime in file_mtimes.items():
|
|
363
|
+
stored_mtime = indexed.get(fpath)
|
|
364
|
+
if stored_mtime is None or current_mtime > stored_mtime:
|
|
365
|
+
stale.append(fpath)
|
|
366
|
+
return stale
|
|
367
|
+
|
|
368
|
+
def get_deleted_files(self, current_files: set[str]) -> list[str]:
|
|
369
|
+
"""Return indexed files no longer present on disk."""
|
|
370
|
+
conn = self._get_conn()
|
|
371
|
+
with self._lock:
|
|
372
|
+
rows = conn.execute("SELECT path FROM files").fetchall()
|
|
373
|
+
return [r[0] for r in rows if r[0] not in current_files]
|
|
374
|
+
|
|
375
|
+
def remove_file(self, path: str) -> None:
|
|
376
|
+
"""Remove a file and all its symbols/refs (CASCADE)."""
|
|
377
|
+
conn = self._get_conn()
|
|
378
|
+
with self._lock:
|
|
379
|
+
conn.execute("DELETE FROM files WHERE path = ?", (path,))
|
|
380
|
+
conn.execute(
|
|
381
|
+
"DELETE FROM dependencies WHERE source_path = ? OR target_path = ?",
|
|
382
|
+
(path, path),
|
|
383
|
+
)
|
|
384
|
+
conn.commit()
|
|
385
|
+
|
|
386
|
+
# ------------------------------------------------------------------
|
|
387
|
+
# Symbols
|
|
388
|
+
# ------------------------------------------------------------------
|
|
389
|
+
|
|
390
|
+
def save_symbols(self, file_path: str, symbols: list[dict[str, Any]]) -> None:
|
|
391
|
+
"""Save symbol definitions for a file (replaces existing)."""
|
|
392
|
+
conn = self._get_conn()
|
|
393
|
+
rows = [
|
|
394
|
+
(
|
|
395
|
+
file_path,
|
|
396
|
+
s["name"],
|
|
397
|
+
s["qualified_name"],
|
|
398
|
+
s["kind"],
|
|
399
|
+
s.get("line", 0),
|
|
400
|
+
s.get("end_line", 0),
|
|
401
|
+
s.get("source", "tree-sitter"),
|
|
402
|
+
)
|
|
403
|
+
for s in symbols
|
|
404
|
+
]
|
|
405
|
+
with self._lock:
|
|
406
|
+
conn.execute("DELETE FROM symbols WHERE file_path = ?", (file_path,))
|
|
407
|
+
conn.executemany(
|
|
408
|
+
"""INSERT INTO symbols
|
|
409
|
+
(file_path, name, qualified_name, kind, line, end_line, source)
|
|
410
|
+
VALUES (?, ?, ?, ?, ?, ?, ?)""",
|
|
411
|
+
rows,
|
|
412
|
+
)
|
|
413
|
+
conn.commit()
|
|
414
|
+
|
|
415
|
+
def load_symbols(self, file_path: str | None = None) -> list[StoredSymbol]:
|
|
416
|
+
"""Load symbols, optionally filtered by file."""
|
|
417
|
+
conn = self._get_conn()
|
|
418
|
+
with self._lock:
|
|
419
|
+
if file_path:
|
|
420
|
+
rows = conn.execute(
|
|
421
|
+
"SELECT id, file_path, name, qualified_name, kind, line, end_line, source "
|
|
422
|
+
"FROM symbols WHERE file_path = ?",
|
|
423
|
+
(file_path,),
|
|
424
|
+
).fetchall()
|
|
425
|
+
else:
|
|
426
|
+
rows = conn.execute(
|
|
427
|
+
"SELECT id, file_path, name, qualified_name, kind, line, end_line, source "
|
|
428
|
+
"FROM symbols"
|
|
429
|
+
).fetchall()
|
|
430
|
+
return [
|
|
431
|
+
StoredSymbol(
|
|
432
|
+
id=r[0], file_path=r[1], name=r[2], qualified_name=r[3],
|
|
433
|
+
kind=r[4], line=r[5], end_line=r[6], source=r[7],
|
|
434
|
+
)
|
|
435
|
+
for r in rows
|
|
436
|
+
]
|
|
437
|
+
|
|
438
|
+
# ------------------------------------------------------------------
|
|
439
|
+
# References
|
|
440
|
+
# ------------------------------------------------------------------
|
|
441
|
+
|
|
442
|
+
def save_references(self, file_path: str, refs: list[dict[str, Any]]) -> None:
|
|
443
|
+
"""Save references for a file (replaces existing)."""
|
|
444
|
+
conn = self._get_conn()
|
|
445
|
+
rows = [
|
|
446
|
+
(
|
|
447
|
+
file_path,
|
|
448
|
+
r["symbol_name"],
|
|
449
|
+
r.get("ref_kind", "call"),
|
|
450
|
+
r.get("line", 0),
|
|
451
|
+
r.get("column", 0),
|
|
452
|
+
r.get("source", "tree-sitter"),
|
|
453
|
+
r.get("syntax_name", ""),
|
|
454
|
+
r.get("caller_qualified_name", ""),
|
|
455
|
+
)
|
|
456
|
+
for r in refs
|
|
457
|
+
]
|
|
458
|
+
with self._lock:
|
|
459
|
+
conn.execute("DELETE FROM refs WHERE file_path = ?", (file_path,))
|
|
460
|
+
conn.executemany(
|
|
461
|
+
"""INSERT INTO refs
|
|
462
|
+
(file_path, symbol_name, ref_kind, line, col, source,
|
|
463
|
+
syntax_name, caller_qualified_name)
|
|
464
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?)""",
|
|
465
|
+
rows,
|
|
466
|
+
)
|
|
467
|
+
conn.commit()
|
|
468
|
+
|
|
469
|
+
def load_references(self, file_path: str | None = None) -> list[StoredReference]:
|
|
470
|
+
"""Load references, optionally filtered by file."""
|
|
471
|
+
conn = self._get_conn()
|
|
472
|
+
with self._lock:
|
|
473
|
+
if file_path:
|
|
474
|
+
rows = conn.execute(
|
|
475
|
+
"SELECT id, file_path, symbol_name, ref_kind, line, col, "
|
|
476
|
+
"source, syntax_name, caller_qualified_name FROM refs WHERE file_path = ?",
|
|
477
|
+
(file_path,),
|
|
478
|
+
).fetchall()
|
|
479
|
+
else:
|
|
480
|
+
rows = conn.execute(
|
|
481
|
+
"SELECT id, file_path, symbol_name, ref_kind, line, col, "
|
|
482
|
+
"source, syntax_name, caller_qualified_name FROM refs"
|
|
483
|
+
).fetchall()
|
|
484
|
+
return [
|
|
485
|
+
StoredReference(
|
|
486
|
+
id=r[0], file_path=r[1], symbol_name=r[2], ref_kind=r[3],
|
|
487
|
+
line=r[4], column=r[5], source=r[6], syntax_name=r[7],
|
|
488
|
+
caller_qualified_name=r[8],
|
|
489
|
+
)
|
|
490
|
+
for r in rows
|
|
491
|
+
]
|
|
492
|
+
|
|
493
|
+
# ------------------------------------------------------------------
|
|
494
|
+
# Dependencies
|
|
495
|
+
# ------------------------------------------------------------------
|
|
496
|
+
|
|
497
|
+
def save_dependencies(self, source_path: str, targets: set[str]) -> None:
|
|
498
|
+
"""Save dependency edges from source to targets (replaces existing)."""
|
|
499
|
+
conn = self._get_conn()
|
|
500
|
+
with self._lock:
|
|
501
|
+
conn.execute(
|
|
502
|
+
"DELETE FROM dependencies WHERE source_path = ?", (source_path,)
|
|
503
|
+
)
|
|
504
|
+
if targets:
|
|
505
|
+
conn.executemany(
|
|
506
|
+
"INSERT OR IGNORE INTO dependencies (source_path, target_path) VALUES (?, ?)",
|
|
507
|
+
[(source_path, t) for t in targets],
|
|
508
|
+
)
|
|
509
|
+
conn.commit()
|
|
510
|
+
|
|
511
|
+
def save_dependencies_batch(self, edges: list[tuple[str, str]]) -> None:
|
|
512
|
+
"""Bulk save dependency edges."""
|
|
513
|
+
conn = self._get_conn()
|
|
514
|
+
with self._lock:
|
|
515
|
+
conn.executemany(
|
|
516
|
+
"INSERT OR IGNORE INTO dependencies (source_path, target_path) VALUES (?, ?)",
|
|
517
|
+
edges,
|
|
518
|
+
)
|
|
519
|
+
conn.commit()
|
|
520
|
+
|
|
521
|
+
def load_dependencies(self) -> dict[str, set[str]]:
|
|
522
|
+
"""Load all dependency edges as forward map."""
|
|
523
|
+
conn = self._get_conn()
|
|
524
|
+
with self._lock:
|
|
525
|
+
rows = conn.execute(
|
|
526
|
+
"SELECT source_path, target_path FROM dependencies"
|
|
527
|
+
).fetchall()
|
|
528
|
+
forward: dict[str, set[str]] = {}
|
|
529
|
+
for src, tgt in rows:
|
|
530
|
+
forward.setdefault(src, set()).add(tgt)
|
|
531
|
+
return forward
|
|
532
|
+
|
|
533
|
+
def clear_dependencies(self) -> None:
|
|
534
|
+
conn = self._get_conn()
|
|
535
|
+
with self._lock:
|
|
536
|
+
conn.execute("DELETE FROM dependencies")
|
|
537
|
+
conn.commit()
|
|
538
|
+
|
|
539
|
+
# ------------------------------------------------------------------
|
|
540
|
+
# Bulk operations
|
|
541
|
+
# ------------------------------------------------------------------
|
|
542
|
+
|
|
543
|
+
def clear_all(self) -> None:
|
|
544
|
+
"""Drop all data (keeps schema)."""
|
|
545
|
+
conn = self._get_conn()
|
|
546
|
+
with self._lock:
|
|
547
|
+
conn.executescript("""
|
|
548
|
+
DELETE FROM refs;
|
|
549
|
+
DELETE FROM symbols;
|
|
550
|
+
DELETE FROM dependencies;
|
|
551
|
+
DELETE FROM files;
|
|
552
|
+
DELETE FROM metadata WHERE key = 'structural_snapshot_v1';
|
|
553
|
+
""")
|
|
554
|
+
conn.commit()
|
|
555
|
+
|
|
556
|
+
def stats(self) -> dict[str, int]:
|
|
557
|
+
"""Return counts of files, symbols, refs, and dependencies."""
|
|
558
|
+
conn = self._get_conn()
|
|
559
|
+
with self._lock:
|
|
560
|
+
files = conn.execute("SELECT COUNT(*) FROM files").fetchone()[0]
|
|
561
|
+
symbols = conn.execute("SELECT COUNT(*) FROM symbols").fetchone()[0]
|
|
562
|
+
refs = conn.execute("SELECT COUNT(*) FROM refs").fetchone()[0]
|
|
563
|
+
deps = conn.execute("SELECT COUNT(*) FROM dependencies").fetchone()[0]
|
|
564
|
+
return {
|
|
565
|
+
"files": files,
|
|
566
|
+
"symbols": symbols,
|
|
567
|
+
"references": refs,
|
|
568
|
+
"dependencies": deps,
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
def record_scan_time(self) -> None:
|
|
572
|
+
"""Record current time as last full scan timestamp."""
|
|
573
|
+
self.set_meta("last_full_scan", str(time.time()))
|
|
574
|
+
|
|
575
|
+
def get_last_scan_time(self) -> float | None:
|
|
576
|
+
"""Return last full scan timestamp, or None if never scanned."""
|
|
577
|
+
val = self.get_meta("last_full_scan")
|
|
578
|
+
return float(val) if val else None
|
|
579
|
+
|
|
580
|
+
def close(self) -> None:
|
|
581
|
+
if self._conn:
|
|
582
|
+
self._conn.close()
|
|
583
|
+
self._conn = None
|
|
584
|
+
|
|
585
|
+
def __del__(self) -> None:
|
|
586
|
+
self.close()
|