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,616 @@
|
|
|
1
|
+
"""Cross-reference index for codebase symbol analysis.
|
|
2
|
+
|
|
3
|
+
Builds a bidirectional index of symbol definitions and references,
|
|
4
|
+
file-level import dependencies, and dependents from parsed AST data.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import logging
|
|
10
|
+
import re
|
|
11
|
+
import threading
|
|
12
|
+
from dataclasses import dataclass, field
|
|
13
|
+
from typing import TYPE_CHECKING, Any
|
|
14
|
+
|
|
15
|
+
if TYPE_CHECKING:
|
|
16
|
+
from attocode_intel._internal.integrations.context.index_store import IndexStore
|
|
17
|
+
|
|
18
|
+
logger = logging.getLogger(__name__)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def _split_name_tokens(name: str) -> list[str]:
|
|
22
|
+
"""Split a symbol name into lowercase tokens.
|
|
23
|
+
|
|
24
|
+
Handles camelCase, PascalCase, snake_case, UPPER_CASE, and mixtures.
|
|
25
|
+
|
|
26
|
+
Examples::
|
|
27
|
+
|
|
28
|
+
"parseConfig" -> ["parse", "config"]
|
|
29
|
+
"parse_config" -> ["parse", "config"]
|
|
30
|
+
"HTTPServer" -> ["http", "server"]
|
|
31
|
+
"HTMLParser" -> ["html", "parser"]
|
|
32
|
+
"_main" -> ["main"]
|
|
33
|
+
"IO" -> ["io"]
|
|
34
|
+
"""
|
|
35
|
+
# First split on underscores
|
|
36
|
+
parts = name.split("_")
|
|
37
|
+
tokens: list[str] = []
|
|
38
|
+
for part in parts:
|
|
39
|
+
if not part:
|
|
40
|
+
continue
|
|
41
|
+
# Split camelCase / PascalCase: insert boundary before uppercase
|
|
42
|
+
# that follows lowercase, or before uppercase followed by lowercase
|
|
43
|
+
# (to handle "HTTPServer" -> "HTTP", "Server")
|
|
44
|
+
sub = re.sub(r"([a-z0-9])([A-Z])", r"\1_\2", part)
|
|
45
|
+
sub = re.sub(r"([A-Z]+)([A-Z][a-z])", r"\1_\2", sub)
|
|
46
|
+
for s in sub.split("_"):
|
|
47
|
+
if s:
|
|
48
|
+
tokens.append(s.lower())
|
|
49
|
+
return tokens
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
@dataclass(slots=True)
|
|
53
|
+
class SymbolRef:
|
|
54
|
+
"""A reference (call site, import, or attribute access) to a symbol."""
|
|
55
|
+
|
|
56
|
+
symbol_name: str # "parse_file" or "CodeIndex.build"
|
|
57
|
+
ref_kind: str # "call" | "import" | "attribute"
|
|
58
|
+
file_path: str
|
|
59
|
+
line: int
|
|
60
|
+
source: str = "tree-sitter" # "tree-sitter" | "lsp"
|
|
61
|
+
syntax_name: str = "" # identifier/member expression as written in source
|
|
62
|
+
# Qualified name of the function/method enclosing this reference. Empty
|
|
63
|
+
# when the reference is at module top level or the enclosing symbol
|
|
64
|
+
# could not be resolved. Populated for call-graph construction.
|
|
65
|
+
caller_qualified_name: str = ""
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
@dataclass(slots=True)
|
|
69
|
+
class SymbolLocation:
|
|
70
|
+
"""Location where a symbol is defined."""
|
|
71
|
+
|
|
72
|
+
name: str
|
|
73
|
+
qualified_name: str # "CodebaseContextManager.discover_files"
|
|
74
|
+
kind: str # "function" | "class" | "method"
|
|
75
|
+
file_path: str
|
|
76
|
+
start_line: int
|
|
77
|
+
end_line: int
|
|
78
|
+
source: str = "tree-sitter" # "tree-sitter" | "lsp"
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
@dataclass(slots=True)
|
|
82
|
+
class CrossRefIndex:
|
|
83
|
+
"""Bidirectional cross-reference index for a codebase.
|
|
84
|
+
|
|
85
|
+
Tracks symbol definitions, call sites, file-level import dependencies,
|
|
86
|
+
and reverse dependents. Includes an inverted name index for fast
|
|
87
|
+
multi-strategy symbol search.
|
|
88
|
+
|
|
89
|
+
Optionally backed by an ``IndexStore`` for persistence across restarts.
|
|
90
|
+
"""
|
|
91
|
+
|
|
92
|
+
# qualified_name -> list of definition locations
|
|
93
|
+
definitions: dict[str, list[SymbolLocation]] = field(default_factory=dict)
|
|
94
|
+
# symbol name -> list of reference sites
|
|
95
|
+
references: dict[str, list[SymbolRef]] = field(default_factory=dict)
|
|
96
|
+
# file path -> set of qualified symbol names defined in that file
|
|
97
|
+
file_symbols: dict[str, set[str]] = field(default_factory=dict)
|
|
98
|
+
# file path -> set of file paths it imports from
|
|
99
|
+
file_dependencies: dict[str, set[str]] = field(default_factory=dict)
|
|
100
|
+
# file path -> set of file paths that import it
|
|
101
|
+
file_dependents: dict[str, set[str]] = field(default_factory=dict)
|
|
102
|
+
# caller qualified name -> set of callee bare/qualified names
|
|
103
|
+
call_edges: dict[str, set[str]] = field(default_factory=dict)
|
|
104
|
+
# callee bare/qualified name -> set of caller qualified names (reverse)
|
|
105
|
+
callers_of: dict[str, set[str]] = field(default_factory=dict)
|
|
106
|
+
|
|
107
|
+
# --- Inverted name indexes (populated by add_definition) ---
|
|
108
|
+
# bare name -> set of qualified names
|
|
109
|
+
_name_to_qnames: dict[str, set[str]] = field(default_factory=dict)
|
|
110
|
+
# lowercased bare name -> set of qualified names
|
|
111
|
+
_lower_to_qnames: dict[str, set[str]] = field(default_factory=dict)
|
|
112
|
+
# individual token -> set of qualified names
|
|
113
|
+
_tokens_to_qnames: dict[str, set[str]] = field(default_factory=dict)
|
|
114
|
+
|
|
115
|
+
# Optional persistent store (set via set_store)
|
|
116
|
+
_store: Any = field(default=None, repr=False)
|
|
117
|
+
|
|
118
|
+
# Re-entrant lock guarding all mutating methods plus the iterating
|
|
119
|
+
# reads that need a consistent snapshot. Reentrancy matters because
|
|
120
|
+
# ``add_reference`` may call ``add_call_edge`` and ``remove_file``
|
|
121
|
+
# walks every other map. Background hydration (``ASTService``'s
|
|
122
|
+
# daemon thread) and LSP/MCP request threads can mutate the index
|
|
123
|
+
# concurrently, so the lock prevents corrupted dicts and torn lists.
|
|
124
|
+
_lock: threading.RLock = field(
|
|
125
|
+
default_factory=threading.RLock, repr=False, compare=False,
|
|
126
|
+
)
|
|
127
|
+
|
|
128
|
+
def set_store(self, store: IndexStore) -> None:
|
|
129
|
+
"""Attach an IndexStore for write-through persistence."""
|
|
130
|
+
self._store = store
|
|
131
|
+
|
|
132
|
+
def persist_file(self, file_path: str) -> None:
|
|
133
|
+
"""Write-through: save current in-memory symbols/refs for a file to the store."""
|
|
134
|
+
if self._store is None:
|
|
135
|
+
return
|
|
136
|
+
with self._lock:
|
|
137
|
+
self._persist_file_locked(file_path)
|
|
138
|
+
|
|
139
|
+
def persist_files(self, file_paths: set[str]) -> None:
|
|
140
|
+
"""Persist a changed batch without rescanning all references per file."""
|
|
141
|
+
with self._lock:
|
|
142
|
+
grouped: dict[str, list[SymbolRef]] = {}
|
|
143
|
+
for refs in self.references.values():
|
|
144
|
+
for ref in refs:
|
|
145
|
+
if ref.file_path in file_paths:
|
|
146
|
+
grouped.setdefault(ref.file_path, []).append(ref)
|
|
147
|
+
for path in sorted(file_paths):
|
|
148
|
+
self._persist_file_locked(path, grouped.get(path, []))
|
|
149
|
+
|
|
150
|
+
def _persist_file_locked(self, file_path: str, references=None) -> None:
|
|
151
|
+
if self._store is None:
|
|
152
|
+
return
|
|
153
|
+
# Collect symbols for this file
|
|
154
|
+
sym_dicts: list[dict[str, Any]] = []
|
|
155
|
+
for qname in self.file_symbols.get(file_path, set()):
|
|
156
|
+
for loc in self.definitions.get(qname, []):
|
|
157
|
+
if loc.file_path == file_path:
|
|
158
|
+
sym_dicts.append({
|
|
159
|
+
"name": loc.name,
|
|
160
|
+
"qualified_name": loc.qualified_name,
|
|
161
|
+
"kind": loc.kind,
|
|
162
|
+
"line": loc.start_line,
|
|
163
|
+
"end_line": loc.end_line,
|
|
164
|
+
"source": loc.source,
|
|
165
|
+
})
|
|
166
|
+
self._store.save_symbols(file_path, sym_dicts)
|
|
167
|
+
|
|
168
|
+
# Collect references originating from this file
|
|
169
|
+
ref_dicts: list[dict[str, Any]] = []
|
|
170
|
+
refs = references if references is not None else (
|
|
171
|
+
ref for refs in self.references.values() for ref in refs if ref.file_path == file_path
|
|
172
|
+
)
|
|
173
|
+
for ref in refs:
|
|
174
|
+
ref_dicts.append({
|
|
175
|
+
"symbol_name": ref.symbol_name,
|
|
176
|
+
"ref_kind": ref.ref_kind,
|
|
177
|
+
"line": ref.line,
|
|
178
|
+
"column": 0,
|
|
179
|
+
"source": ref.source,
|
|
180
|
+
"syntax_name": ref.syntax_name,
|
|
181
|
+
"caller_qualified_name": ref.caller_qualified_name,
|
|
182
|
+
})
|
|
183
|
+
self._store.save_references(file_path, ref_dicts)
|
|
184
|
+
|
|
185
|
+
def load_from_store(self) -> int:
|
|
186
|
+
"""Bulk-load symbols, references, and dependencies from the store.
|
|
187
|
+
|
|
188
|
+
Returns the number of files loaded.
|
|
189
|
+
"""
|
|
190
|
+
if self._store is None:
|
|
191
|
+
return 0
|
|
192
|
+
with self._lock:
|
|
193
|
+
return self._load_from_store_locked()
|
|
194
|
+
|
|
195
|
+
def _load_from_store_locked(self) -> int:
|
|
196
|
+
if self._store is None:
|
|
197
|
+
return 0
|
|
198
|
+
|
|
199
|
+
stored_symbols = self._store.load_symbols()
|
|
200
|
+
stored_refs = self._store.load_references()
|
|
201
|
+
stored_deps = self._store.load_dependencies()
|
|
202
|
+
|
|
203
|
+
files_loaded: set[str] = set()
|
|
204
|
+
|
|
205
|
+
for s in stored_symbols:
|
|
206
|
+
loc = SymbolLocation(
|
|
207
|
+
name=s.name,
|
|
208
|
+
qualified_name=s.qualified_name,
|
|
209
|
+
kind=s.kind,
|
|
210
|
+
file_path=s.file_path,
|
|
211
|
+
start_line=s.line,
|
|
212
|
+
end_line=s.end_line,
|
|
213
|
+
source=s.source,
|
|
214
|
+
)
|
|
215
|
+
self.add_definition(loc)
|
|
216
|
+
files_loaded.add(s.file_path)
|
|
217
|
+
|
|
218
|
+
for r in stored_refs:
|
|
219
|
+
ref = SymbolRef(
|
|
220
|
+
symbol_name=r.symbol_name,
|
|
221
|
+
ref_kind=r.ref_kind,
|
|
222
|
+
file_path=r.file_path,
|
|
223
|
+
line=r.line,
|
|
224
|
+
source=r.source,
|
|
225
|
+
syntax_name=getattr(r, "syntax_name", ""),
|
|
226
|
+
caller_qualified_name=getattr(r, "caller_qualified_name", ""),
|
|
227
|
+
)
|
|
228
|
+
self.add_reference(ref)
|
|
229
|
+
|
|
230
|
+
for src, targets in stored_deps.items():
|
|
231
|
+
for tgt in targets:
|
|
232
|
+
self.add_file_dependency(src, tgt)
|
|
233
|
+
|
|
234
|
+
logger.debug(
|
|
235
|
+
"CrossRefIndex loaded from store: %d files, %d symbols, %d refs",
|
|
236
|
+
len(files_loaded), len(stored_symbols), len(stored_refs),
|
|
237
|
+
)
|
|
238
|
+
return len(files_loaded)
|
|
239
|
+
|
|
240
|
+
def merge_lsp_results(
|
|
241
|
+
self,
|
|
242
|
+
file_path: str,
|
|
243
|
+
definitions: list[SymbolLocation],
|
|
244
|
+
references: list[SymbolRef],
|
|
245
|
+
*, verified_symbol: bool = False,
|
|
246
|
+
) -> int:
|
|
247
|
+
"""Merge LSP-sourced results into the index.
|
|
248
|
+
|
|
249
|
+
Deduplicates by (qualified_name, file, line) — LSP wins on conflict.
|
|
250
|
+
Returns the number of new entries added.
|
|
251
|
+
"""
|
|
252
|
+
with self._lock:
|
|
253
|
+
return self._merge_lsp_results_locked(file_path, definitions, references, verified_symbol=verified_symbol)
|
|
254
|
+
|
|
255
|
+
def _merge_lsp_results_locked(
|
|
256
|
+
self,
|
|
257
|
+
file_path: str,
|
|
258
|
+
definitions: list[SymbolLocation],
|
|
259
|
+
references: list[SymbolRef],
|
|
260
|
+
*, verified_symbol: bool = False,
|
|
261
|
+
) -> int:
|
|
262
|
+
added = 0
|
|
263
|
+
|
|
264
|
+
for loc in definitions:
|
|
265
|
+
# Create a copy with source="lsp" — don't mutate caller's input
|
|
266
|
+
lsp_loc = SymbolLocation(
|
|
267
|
+
name=loc.name, qualified_name=loc.qualified_name,
|
|
268
|
+
kind=loc.kind, file_path=loc.file_path,
|
|
269
|
+
start_line=loc.start_line, end_line=loc.end_line,
|
|
270
|
+
source="lsp",
|
|
271
|
+
)
|
|
272
|
+
existing = self.definitions.get(lsp_loc.qualified_name, [])
|
|
273
|
+
# Check for duplicate at same location
|
|
274
|
+
dup = False
|
|
275
|
+
for i, ex in enumerate(existing):
|
|
276
|
+
if ex.file_path == lsp_loc.file_path and ex.start_line == lsp_loc.start_line:
|
|
277
|
+
# LSP wins: replace tree-sitter entry
|
|
278
|
+
existing[i] = lsp_loc
|
|
279
|
+
dup = True
|
|
280
|
+
break
|
|
281
|
+
if not dup:
|
|
282
|
+
self.add_definition(lsp_loc)
|
|
283
|
+
added += 1
|
|
284
|
+
|
|
285
|
+
for ref in references:
|
|
286
|
+
lsp_ref = SymbolRef(
|
|
287
|
+
symbol_name=ref.symbol_name, ref_kind=ref.ref_kind,
|
|
288
|
+
file_path=ref.file_path, line=ref.line,
|
|
289
|
+
source="lsp",
|
|
290
|
+
syntax_name=ref.syntax_name,
|
|
291
|
+
caller_qualified_name=ref.caller_qualified_name,
|
|
292
|
+
)
|
|
293
|
+
# LSP wins on (file, line) collision — but ONLY when the
|
|
294
|
+
# incoming ref carries a resolved caller. Without one, the
|
|
295
|
+
# legacy ``ingest_lsp_results`` fallback path stores the
|
|
296
|
+
# *enclosing function* in ``symbol_name`` (not the callee),
|
|
297
|
+
# so replacing a tree-sitter entry would invert the edge.
|
|
298
|
+
# In that case we dedupe-skip, preserving the existing
|
|
299
|
+
# higher-precision tree-sitter ref.
|
|
300
|
+
existing = self.references.get(lsp_ref.symbol_name, [])
|
|
301
|
+
dup_index: int | None = None
|
|
302
|
+
for i, ex in enumerate(existing):
|
|
303
|
+
if ex.file_path == lsp_ref.file_path and ex.line == lsp_ref.line:
|
|
304
|
+
dup_index = i
|
|
305
|
+
break
|
|
306
|
+
|
|
307
|
+
if dup_index is not None:
|
|
308
|
+
if lsp_ref.caller_qualified_name or verified_symbol:
|
|
309
|
+
if not lsp_ref.syntax_name:
|
|
310
|
+
lsp_ref.syntax_name = existing[dup_index].syntax_name
|
|
311
|
+
existing[dup_index] = lsp_ref
|
|
312
|
+
if lsp_ref.ref_kind == "call":
|
|
313
|
+
self.add_call_edge(
|
|
314
|
+
lsp_ref.caller_qualified_name,
|
|
315
|
+
lsp_ref.symbol_name,
|
|
316
|
+
)
|
|
317
|
+
# else: no caller info — leave the tree-sitter entry alone.
|
|
318
|
+
else:
|
|
319
|
+
self.add_reference(lsp_ref)
|
|
320
|
+
added += 1
|
|
321
|
+
|
|
322
|
+
# Persist if store is available
|
|
323
|
+
self.persist_file(file_path)
|
|
324
|
+
return added
|
|
325
|
+
|
|
326
|
+
def add_definition(self, loc: SymbolLocation) -> None:
|
|
327
|
+
"""Register a symbol definition."""
|
|
328
|
+
with self._lock:
|
|
329
|
+
self.definitions.setdefault(loc.qualified_name, []).append(loc)
|
|
330
|
+
self.file_symbols.setdefault(loc.file_path, set()).add(loc.qualified_name)
|
|
331
|
+
# Populate inverted indexes
|
|
332
|
+
bare = loc.qualified_name.rsplit(".", 1)[-1]
|
|
333
|
+
self._name_to_qnames.setdefault(bare, set()).add(loc.qualified_name)
|
|
334
|
+
self._lower_to_qnames.setdefault(bare.lower(), set()).add(loc.qualified_name)
|
|
335
|
+
for token in _split_name_tokens(bare):
|
|
336
|
+
self._tokens_to_qnames.setdefault(token, set()).add(loc.qualified_name)
|
|
337
|
+
|
|
338
|
+
def add_reference(self, ref: SymbolRef) -> None:
|
|
339
|
+
"""Register a symbol reference."""
|
|
340
|
+
with self._lock:
|
|
341
|
+
self.references.setdefault(ref.symbol_name, []).append(ref)
|
|
342
|
+
if ref.ref_kind == "call" and ref.caller_qualified_name:
|
|
343
|
+
self.add_call_edge(ref.caller_qualified_name, ref.symbol_name)
|
|
344
|
+
|
|
345
|
+
def add_call_edge(self, caller: str, callee: str) -> None:
|
|
346
|
+
"""Record that ``caller`` calls ``callee``.
|
|
347
|
+
|
|
348
|
+
``caller`` should be a qualified name (e.g. ``MyClass.do_thing`` or a
|
|
349
|
+
bare top-level function name). ``callee`` is whatever the source
|
|
350
|
+
wrote — bare name when type/scope resolution is unavailable; the
|
|
351
|
+
consumer can disambiguate against ``self.definitions``.
|
|
352
|
+
"""
|
|
353
|
+
if not caller or not callee:
|
|
354
|
+
return
|
|
355
|
+
with self._lock:
|
|
356
|
+
self.call_edges.setdefault(caller, set()).add(callee)
|
|
357
|
+
self.callers_of.setdefault(callee, set()).add(caller)
|
|
358
|
+
|
|
359
|
+
def add_file_dependency(self, source: str, target: str) -> None:
|
|
360
|
+
"""Record that *source* imports from *target*."""
|
|
361
|
+
with self._lock:
|
|
362
|
+
self.file_dependencies.setdefault(source, set()).add(target)
|
|
363
|
+
self.file_dependents.setdefault(target, set()).add(source)
|
|
364
|
+
|
|
365
|
+
def remove_file(self, file_path: str) -> None:
|
|
366
|
+
"""Remove all index entries for a file (in-memory and store)."""
|
|
367
|
+
with self._lock:
|
|
368
|
+
self._remove_file_locked(file_path)
|
|
369
|
+
|
|
370
|
+
def _remove_file_locked(self, file_path: str) -> None:
|
|
371
|
+
if self._store is not None:
|
|
372
|
+
self._store.remove_file(file_path)
|
|
373
|
+
# Snapshot callers defined here BEFORE we mutate file_symbols below.
|
|
374
|
+
callers_in_file = set(self.file_symbols.get(file_path, set()))
|
|
375
|
+
# Remove definitions and clean up inverted indexes
|
|
376
|
+
for qname in list(self.file_symbols.get(file_path, [])):
|
|
377
|
+
defs = self.definitions.get(qname, [])
|
|
378
|
+
self.definitions[qname] = [d for d in defs if d.file_path != file_path]
|
|
379
|
+
if not self.definitions[qname]:
|
|
380
|
+
del self.definitions[qname]
|
|
381
|
+
# Clean up inverted indexes — qname fully gone
|
|
382
|
+
bare = qname.rsplit(".", 1)[-1]
|
|
383
|
+
s = self._name_to_qnames.get(bare)
|
|
384
|
+
if s:
|
|
385
|
+
s.discard(qname)
|
|
386
|
+
if not s:
|
|
387
|
+
del self._name_to_qnames[bare]
|
|
388
|
+
s = self._lower_to_qnames.get(bare.lower())
|
|
389
|
+
if s:
|
|
390
|
+
s.discard(qname)
|
|
391
|
+
if not s:
|
|
392
|
+
del self._lower_to_qnames[bare.lower()]
|
|
393
|
+
for token in _split_name_tokens(bare):
|
|
394
|
+
s = self._tokens_to_qnames.get(token)
|
|
395
|
+
if s:
|
|
396
|
+
s.discard(qname)
|
|
397
|
+
if not s:
|
|
398
|
+
del self._tokens_to_qnames[token]
|
|
399
|
+
self.file_symbols.pop(file_path, None)
|
|
400
|
+
|
|
401
|
+
# Remove references originating from this file
|
|
402
|
+
for name, refs in list(self.references.items()):
|
|
403
|
+
self.references[name] = [r for r in refs if r.file_path != file_path]
|
|
404
|
+
if not self.references[name]:
|
|
405
|
+
del self.references[name]
|
|
406
|
+
|
|
407
|
+
# Drop call edges whose caller was defined in this file. We can't
|
|
408
|
+
# reliably attribute callees from the file path alone (callees are
|
|
409
|
+
# bare names), so this is a best-effort scrub keyed on caller.
|
|
410
|
+
for caller in callers_in_file:
|
|
411
|
+
for callee in self.call_edges.pop(caller, set()):
|
|
412
|
+
rev = self.callers_of.get(callee)
|
|
413
|
+
if rev is not None:
|
|
414
|
+
rev.discard(caller)
|
|
415
|
+
if not rev:
|
|
416
|
+
del self.callers_of[callee]
|
|
417
|
+
|
|
418
|
+
# Remove dependency edges
|
|
419
|
+
for dep in self.file_dependencies.pop(file_path, set()):
|
|
420
|
+
dependents = self.file_dependents.get(dep)
|
|
421
|
+
if dependents:
|
|
422
|
+
dependents.discard(file_path)
|
|
423
|
+
if not dependents:
|
|
424
|
+
del self.file_dependents[dep]
|
|
425
|
+
|
|
426
|
+
# Remove as a dependent target
|
|
427
|
+
for src in list(self.file_dependents.get(file_path, [])):
|
|
428
|
+
deps = self.file_dependencies.get(src)
|
|
429
|
+
if deps:
|
|
430
|
+
deps.discard(file_path)
|
|
431
|
+
if not deps:
|
|
432
|
+
del self.file_dependencies[src]
|
|
433
|
+
self.file_dependents.pop(file_path, None)
|
|
434
|
+
|
|
435
|
+
# ------------------------------------------------------------------
|
|
436
|
+
# Search
|
|
437
|
+
# ------------------------------------------------------------------
|
|
438
|
+
|
|
439
|
+
def get_definitions(self, symbol_name: str, *, include_shadowed: bool = False) -> list[SymbolLocation]:
|
|
440
|
+
"""Look up definitions for a symbol (exact or suffix match)."""
|
|
441
|
+
# Try exact qualified name first
|
|
442
|
+
if symbol_name in self.definitions and not include_shadowed:
|
|
443
|
+
return self.definitions[symbol_name]
|
|
444
|
+
# Try suffix match (e.g. "discover_files" matches "CodebaseContextManager.discover_files")
|
|
445
|
+
results: list[SymbolLocation] = []
|
|
446
|
+
for qname, locs in self.definitions.items():
|
|
447
|
+
if qname == symbol_name or qname.endswith(f".{symbol_name}"):
|
|
448
|
+
results.extend(locs)
|
|
449
|
+
return results
|
|
450
|
+
|
|
451
|
+
def search_definitions(
|
|
452
|
+
self,
|
|
453
|
+
symbol_name: str,
|
|
454
|
+
*,
|
|
455
|
+
limit: int = 50,
|
|
456
|
+
kind_filter: str = "",
|
|
457
|
+
) -> list[tuple[SymbolLocation, float]]:
|
|
458
|
+
"""Multi-strategy symbol search returning (location, score) pairs.
|
|
459
|
+
|
|
460
|
+
Search strategies (in priority order):
|
|
461
|
+
1. Exact qualified name match (score 1.0)
|
|
462
|
+
2. Exact bare name match via index (score 0.95)
|
|
463
|
+
3. Case-insensitive bare name match (score 0.85)
|
|
464
|
+
4. Prefix match on bare name (score 0.75)
|
|
465
|
+
5. Substring match on bare name (score 0.60)
|
|
466
|
+
6. Token overlap (camelCase/snake_case) (score 0.50)
|
|
467
|
+
"""
|
|
468
|
+
# Collect candidates as {qualified_name: best_match_score}
|
|
469
|
+
candidates: dict[str, float] = {}
|
|
470
|
+
|
|
471
|
+
# 1. Exact qualified name
|
|
472
|
+
if symbol_name in self.definitions:
|
|
473
|
+
candidates[symbol_name] = 1.0
|
|
474
|
+
|
|
475
|
+
# 2. Exact bare name via inverted index — O(1)
|
|
476
|
+
bare_qnames = self._name_to_qnames.get(symbol_name, set())
|
|
477
|
+
for qn in bare_qnames:
|
|
478
|
+
if qn not in candidates:
|
|
479
|
+
candidates[qn] = 0.95
|
|
480
|
+
|
|
481
|
+
# 3. Case-insensitive bare name — O(1)
|
|
482
|
+
lower_qnames = self._lower_to_qnames.get(symbol_name.lower(), set())
|
|
483
|
+
for qn in lower_qnames:
|
|
484
|
+
if qn not in candidates:
|
|
485
|
+
candidates[qn] = 0.85
|
|
486
|
+
|
|
487
|
+
name_lower = symbol_name.lower()
|
|
488
|
+
|
|
489
|
+
# 4. Prefix match on bare names — scan index keys
|
|
490
|
+
for bare, qnames in self._name_to_qnames.items():
|
|
491
|
+
if bare.lower().startswith(name_lower) and len(bare) > len(symbol_name):
|
|
492
|
+
for qn in qnames:
|
|
493
|
+
if qn not in candidates:
|
|
494
|
+
candidates[qn] = 0.75
|
|
495
|
+
|
|
496
|
+
# 5. Substring match on bare names
|
|
497
|
+
if len(symbol_name) >= 2:
|
|
498
|
+
for bare, qnames in self._name_to_qnames.items():
|
|
499
|
+
if name_lower in bare.lower() and bare.lower() != name_lower:
|
|
500
|
+
for qn in qnames:
|
|
501
|
+
if qn not in candidates:
|
|
502
|
+
candidates[qn] = 0.60
|
|
503
|
+
|
|
504
|
+
# 6. Token overlap (camelCase/snake_case)
|
|
505
|
+
query_tokens = set(_split_name_tokens(symbol_name))
|
|
506
|
+
if query_tokens:
|
|
507
|
+
token_hits: dict[str, int] = {}
|
|
508
|
+
for token in query_tokens:
|
|
509
|
+
for qn in self._tokens_to_qnames.get(token, set()):
|
|
510
|
+
if qn not in candidates:
|
|
511
|
+
token_hits[qn] = token_hits.get(qn, 0) + 1
|
|
512
|
+
for qn, hit_count in token_hits.items():
|
|
513
|
+
# Score proportional to fraction of query tokens matched
|
|
514
|
+
score = 0.50 * (hit_count / len(query_tokens))
|
|
515
|
+
if score >= 0.25: # at least half the tokens must match
|
|
516
|
+
candidates[qn] = score
|
|
517
|
+
|
|
518
|
+
if not candidates:
|
|
519
|
+
return []
|
|
520
|
+
|
|
521
|
+
# Resolve candidates to SymbolLocations and compute composite scores
|
|
522
|
+
scored: list[tuple[SymbolLocation, float]] = []
|
|
523
|
+
for qname, match_score in candidates.items():
|
|
524
|
+
locs = self.definitions.get(qname, [])
|
|
525
|
+
for loc in locs:
|
|
526
|
+
if kind_filter and loc.kind != kind_filter:
|
|
527
|
+
continue
|
|
528
|
+
score = _rank_score(loc, match_score)
|
|
529
|
+
scored.append((loc, score))
|
|
530
|
+
|
|
531
|
+
scored.sort(key=lambda x: -x[1])
|
|
532
|
+
return scored[:limit]
|
|
533
|
+
|
|
534
|
+
def get_references(self, symbol_name: str) -> list[SymbolRef]:
|
|
535
|
+
"""Look up all call sites / references for a symbol (exact or suffix match)."""
|
|
536
|
+
# Include exact and qualified candidates; an exact key must not hide others.
|
|
537
|
+
# Suffix match (e.g. "clear" matches refs keyed as "MCPMetaTools.clear")
|
|
538
|
+
results: list[SymbolRef] = []
|
|
539
|
+
for ref_name, refs in self.references.items():
|
|
540
|
+
if ref_name == symbol_name or ref_name.endswith(f".{symbol_name}"):
|
|
541
|
+
results.extend(refs)
|
|
542
|
+
return results
|
|
543
|
+
|
|
544
|
+
def get_dependents(self, file_path: str) -> set[str]:
|
|
545
|
+
"""Files that import from *file_path*."""
|
|
546
|
+
return self.file_dependents.get(file_path, set())
|
|
547
|
+
|
|
548
|
+
def get_dependencies(self, file_path: str) -> set[str]:
|
|
549
|
+
"""Files that *file_path* imports from."""
|
|
550
|
+
return self.file_dependencies.get(file_path, set())
|
|
551
|
+
|
|
552
|
+
# ------------------------------------------------------------------
|
|
553
|
+
# Call graph
|
|
554
|
+
# ------------------------------------------------------------------
|
|
555
|
+
|
|
556
|
+
def get_callees(self, caller: str, *, depth: int = 1) -> set[str]:
|
|
557
|
+
"""Return symbols called by ``caller`` up to ``depth`` hops.
|
|
558
|
+
|
|
559
|
+
depth=1 returns direct callees only; depth=N follows up to N hops
|
|
560
|
+
through the in-memory ``call_edges`` map. Cycles are handled by
|
|
561
|
+
an internal visited set so the traversal always terminates.
|
|
562
|
+
"""
|
|
563
|
+
if depth < 1:
|
|
564
|
+
return set()
|
|
565
|
+
with self._lock:
|
|
566
|
+
visited: set[str] = set()
|
|
567
|
+
frontier: set[str] = {caller}
|
|
568
|
+
result: set[str] = set()
|
|
569
|
+
for _ in range(depth):
|
|
570
|
+
next_frontier: set[str] = set()
|
|
571
|
+
for node in frontier:
|
|
572
|
+
if node in visited:
|
|
573
|
+
continue
|
|
574
|
+
visited.add(node)
|
|
575
|
+
callees = self.call_edges.get(node, set())
|
|
576
|
+
result.update(callees)
|
|
577
|
+
next_frontier.update(callees - visited)
|
|
578
|
+
if not next_frontier:
|
|
579
|
+
break
|
|
580
|
+
frontier = next_frontier
|
|
581
|
+
return result
|
|
582
|
+
|
|
583
|
+
def get_callers(self, callee: str, *, depth: int = 1) -> set[str]:
|
|
584
|
+
"""Return symbols that call ``callee`` up to ``depth`` hops back."""
|
|
585
|
+
if depth < 1:
|
|
586
|
+
return set()
|
|
587
|
+
with self._lock:
|
|
588
|
+
visited: set[str] = set()
|
|
589
|
+
frontier: set[str] = {callee}
|
|
590
|
+
result: set[str] = set()
|
|
591
|
+
for _ in range(depth):
|
|
592
|
+
next_frontier: set[str] = set()
|
|
593
|
+
for node in frontier:
|
|
594
|
+
if node in visited:
|
|
595
|
+
continue
|
|
596
|
+
visited.add(node)
|
|
597
|
+
callers = self.callers_of.get(node, set())
|
|
598
|
+
result.update(callers)
|
|
599
|
+
next_frontier.update(callers - visited)
|
|
600
|
+
if not next_frontier:
|
|
601
|
+
break
|
|
602
|
+
frontier = next_frontier
|
|
603
|
+
return result
|
|
604
|
+
|
|
605
|
+
|
|
606
|
+
def _rank_score(loc: SymbolLocation, match_score: float) -> float:
|
|
607
|
+
"""Composite ranking combining match quality and symbol importance."""
|
|
608
|
+
importance = 0.0
|
|
609
|
+
if not loc.name.startswith("_"):
|
|
610
|
+
importance += 0.02
|
|
611
|
+
kind_boost = {"class": 0.03, "interface": 0.03, "function": 0.01, "method": 0.0}
|
|
612
|
+
importance += kind_boost.get(loc.kind, 0.0)
|
|
613
|
+
# LSP-sourced entries are more precise
|
|
614
|
+
if loc.source == "lsp":
|
|
615
|
+
importance += 0.05
|
|
616
|
+
return match_score + importance
|