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,141 @@
|
|
|
1
|
+
"""Small evidence bundles composed from one refreshed workspace."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def select_definitions(service, symbol_name, file_path=None, line=None):
|
|
8
|
+
root = Path(service.project_dir).resolve()
|
|
9
|
+
if file_path is not None:
|
|
10
|
+
path = (root / file_path).resolve()
|
|
11
|
+
if not path.is_relative_to(root):
|
|
12
|
+
raise ValueError("Path must remain inside the selected repository")
|
|
13
|
+
file_path = path.relative_to(root).as_posix()
|
|
14
|
+
if line is not None and line < 1:
|
|
15
|
+
raise ValueError("line must be a positive definition start line")
|
|
16
|
+
ast = service._get_ast_service()
|
|
17
|
+
if (file_path is not None and file_path not in ast._ast_cache
|
|
18
|
+
and any(fi.relative_path == file_path for fi in ast._context_mgr._files)):
|
|
19
|
+
ast.ensure_file_parsed(file_path)
|
|
20
|
+
if file_path is not None:
|
|
21
|
+
# A top-level exact name elsewhere must not hide a method in this file.
|
|
22
|
+
definitions = ast.get_file_symbols(file_path)
|
|
23
|
+
definitions = [d for d in definitions if d.file_path == file_path
|
|
24
|
+
and (d.qualified_name == symbol_name or d.name == symbol_name
|
|
25
|
+
or d.qualified_name.endswith(f".{symbol_name}"))]
|
|
26
|
+
else:
|
|
27
|
+
definitions = ast.find_symbol(symbol_name, include_shadowed=line is not None)
|
|
28
|
+
return [d for d in definitions if line is None or d.start_line == line]
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def location(definition):
|
|
32
|
+
return {key: getattr(definition, key) for key in
|
|
33
|
+
("name", "qualified_name", "kind", "file_path", "start_line", "end_line")}
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def scoped_references(service, symbol_name, file_path=None, line=None):
|
|
37
|
+
data = service.cross_references_data(symbol_name)
|
|
38
|
+
if file_path is not None or line is not None:
|
|
39
|
+
definitions = select_definitions(service, symbol_name, file_path, line)
|
|
40
|
+
data["definitions"] = [location(d) for d in definitions]
|
|
41
|
+
data["selection_unique"] = len(definitions) == 1
|
|
42
|
+
if not definitions:
|
|
43
|
+
data["references"] = []
|
|
44
|
+
data["total_references"] = 0
|
|
45
|
+
data["reference_candidates"] = []
|
|
46
|
+
data["total_reference_candidates"] = 0
|
|
47
|
+
elif len(definitions) == 1:
|
|
48
|
+
from attocode_intel.symbol_links import classify_symbol_references
|
|
49
|
+
linked, candidates = classify_symbol_references(service._get_ast_service(), definitions[0])
|
|
50
|
+
data["references"] = linked
|
|
51
|
+
data["total_references"] = len(linked)
|
|
52
|
+
data["reference_candidates"] = candidates[:20]
|
|
53
|
+
data["total_reference_candidates"] = len(candidates)
|
|
54
|
+
data["reference_caveat"] = (
|
|
55
|
+
"References are linked by same-file identity, explicit import bindings, or a language server. "
|
|
56
|
+
"Same-name syntax matches without that evidence remain reference_candidates."
|
|
57
|
+
)
|
|
58
|
+
data["references"] = sorted(data["references"], key=lambda r: (
|
|
59
|
+
r["file_path"], r["line"], r["ref_kind"], r["source"], r.get("symbol", "")))
|
|
60
|
+
seen = set()
|
|
61
|
+
unique = []
|
|
62
|
+
for ref in data["references"]:
|
|
63
|
+
identity = tuple(sorted((key, str(value)) for key, value in ref.items()))
|
|
64
|
+
if identity not in seen:
|
|
65
|
+
seen.add(identity)
|
|
66
|
+
unique.append(ref)
|
|
67
|
+
data["references"] = unique
|
|
68
|
+
return data
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def inspect_symbol_data(service, symbol_name, file_path=None, line=None, source_start_line=None, task_hint=None):
|
|
72
|
+
definitions = select_definitions(service, symbol_name, file_path, line)
|
|
73
|
+
if len(definitions) != 1:
|
|
74
|
+
return {"status": "ambiguous" if definitions else "not_found", "symbol": symbol_name,
|
|
75
|
+
"definitions": [location(d) for d in definitions[:10]],
|
|
76
|
+
"total_definitions": len(definitions), "ambiguous": len(definitions) > 1,
|
|
77
|
+
"follow_up": {"tool": "inspect_symbol", "select": "file_path and start_line as line"}}
|
|
78
|
+
definition = definitions[0]
|
|
79
|
+
path = definition.file_path
|
|
80
|
+
root = Path(service.project_dir).resolve()
|
|
81
|
+
source_path = (root / path).resolve()
|
|
82
|
+
if not source_path.is_relative_to(root):
|
|
83
|
+
raise ValueError("Path must remain inside the selected repository")
|
|
84
|
+
# Check file identity across reading so an outside editor cannot pair stale lines with new text.
|
|
85
|
+
before = source_path.stat()
|
|
86
|
+
from attocode_intel.request_context import current_request
|
|
87
|
+
request = current_request.get()
|
|
88
|
+
tracker = request.stores.get("freshness") if request else None
|
|
89
|
+
expected = (tracker.manifest or {}).get(path) if tracker else None
|
|
90
|
+
if expected is not None and expected != (before.st_mtime_ns, before.st_ctime_ns, before.st_size):
|
|
91
|
+
raise ValueError("Source changed after indexing; retry inspection")
|
|
92
|
+
source = source_path.read_text(encoding="utf-8", errors="replace").splitlines()
|
|
93
|
+
after = source_path.stat()
|
|
94
|
+
if (before.st_mtime_ns, before.st_ctime_ns, before.st_size) != (
|
|
95
|
+
after.st_mtime_ns, after.st_ctime_ns, after.st_size):
|
|
96
|
+
raise ValueError("Source changed during inspection; retry")
|
|
97
|
+
from attocode_intel.tools.composite_tools import suggest_tests_data
|
|
98
|
+
|
|
99
|
+
references = scoped_references(service, symbol_name, path, definition.start_line)
|
|
100
|
+
imports = service.dependencies_data(path)
|
|
101
|
+
linked_test_files = {ref["file_path"] for ref in references["references"]}
|
|
102
|
+
tests = suggest_tests_data([path], symbol_name=definition.qualified_name, task_hint=task_hint,
|
|
103
|
+
linked_reference_files=linked_test_files)["candidates"]
|
|
104
|
+
if source_start_line is not None and (isinstance(source_start_line, bool) or int(source_start_line) != source_start_line):
|
|
105
|
+
raise ValueError("source_start_line must be an integer inside the selected definition")
|
|
106
|
+
start = max(1, definition.start_line) if source_start_line is None else int(source_start_line)
|
|
107
|
+
if not definition.start_line <= start <= min(len(source), definition.end_line):
|
|
108
|
+
raise ValueError("source_start_line must be inside the selected definition")
|
|
109
|
+
focused_preview = task_hint and source_start_line is None and definition.end_line - definition.start_line >= 60
|
|
110
|
+
end = min(len(source), definition.end_line, start + (11 if focused_preview else 59))
|
|
111
|
+
selection = {"symbol_name": definition.qualified_name, "file_path": path,
|
|
112
|
+
"line": definition.start_line}
|
|
113
|
+
relationships = ([{"file_path": p, "kind": "imports"} for p in imports["imports"]]
|
|
114
|
+
+ [{"file_path": p, "kind": "imported_by"} for p in imports["imported_by"]])
|
|
115
|
+
result = {
|
|
116
|
+
"status": "found", "definition": location(definition),
|
|
117
|
+
"source": {"start_line": start, "end_line": end,
|
|
118
|
+
"text": "\n".join(source[start - 1:end]),
|
|
119
|
+
"truncated": start > definition.start_line or end < definition.end_line},
|
|
120
|
+
"references": references["references"][:8], "total_references": references["total_references"],
|
|
121
|
+
"reference_candidates": references.get("reference_candidates", [])[:5],
|
|
122
|
+
"total_reference_candidates": references.get("total_reference_candidates", 0),
|
|
123
|
+
"ambiguous": references["ambiguous"],
|
|
124
|
+
"reference_caveat": references.get("reference_caveat", "Syntax references are candidates; verify source."),
|
|
125
|
+
"imports": relationships[:5], "total_imports": len(relationships),
|
|
126
|
+
"tests": tests[:5], "total_tests": len(tests), "absence_proven": False,
|
|
127
|
+
"follow_up": {
|
|
128
|
+
"references": {"tool": "cross_references", **selection},
|
|
129
|
+
"imports": {"tool": "dependencies", "path": path},
|
|
130
|
+
"tests": {"tool": "suggest_tests", "files": [path], "symbol_name": definition.qualified_name,
|
|
131
|
+
**({"task_hint": task_hint} if task_hint else {})},
|
|
132
|
+
"source": {"file_path": path, "start_line": definition.start_line, "end_line": definition.end_line},
|
|
133
|
+
"next_source": {"tool": "inspect_symbol", **selection, "source_start_line": end + 1}
|
|
134
|
+
if end < definition.end_line else None,
|
|
135
|
+
},
|
|
136
|
+
}
|
|
137
|
+
if task_hint:
|
|
138
|
+
from attocode_intel.focused_evidence import source_excerpts
|
|
139
|
+
result["source_excerpts"] = source_excerpts(source, definition, task_hint, preview_end=end)
|
|
140
|
+
result["excerpt_selection"] = "Identifiers and executable literals ranked with enclosing syntax context; not exhaustive behavior coverage or data flow. Context ranges belong to their excerpt; cite each range separately."
|
|
141
|
+
return result
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
"""Resolve selected-symbol evidence through local import bindings."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
from collections import deque
|
|
5
|
+
from pathlib import Path, PurePosixPath
|
|
6
|
+
|
|
7
|
+
from attocode_intel._internal.integrations.context.cross_references import SymbolRef
|
|
8
|
+
from attocode_intel._internal.integrations.context.syntax_evidence import reference_candidates
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def _file_index(ast):
|
|
12
|
+
try:
|
|
13
|
+
from attocode_intel._internal.integrations.context.codebase_context import _build_file_index
|
|
14
|
+
return _build_file_index(ast._context_mgr._files, ast._root_dir)
|
|
15
|
+
except AttributeError:
|
|
16
|
+
return None
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def _imports_from(ast, source, target, index):
|
|
20
|
+
"""Return bindings in *source* whose module resolves to *target*."""
|
|
21
|
+
if index is None:
|
|
22
|
+
return []
|
|
23
|
+
try:
|
|
24
|
+
from attocode_intel._internal.integrations.context.codebase_context import IMPORT_RESOLVERS
|
|
25
|
+
file_ast = ast._ast_cache.get(source)
|
|
26
|
+
if file_ast is None:
|
|
27
|
+
ast.ensure_file_parsed(source)
|
|
28
|
+
file_ast = ast._ast_cache.get(source)
|
|
29
|
+
resolver = IMPORT_RESOLVERS.get(file_ast.language) if file_ast else None
|
|
30
|
+
return [imp for imp in (file_ast.imports if file_ast else [])
|
|
31
|
+
if resolver and resolver(imp.module, source, index) == target]
|
|
32
|
+
except (AttributeError, OSError, ValueError):
|
|
33
|
+
return []
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def linked_names_by_file(ast, definition):
|
|
37
|
+
"""Map importers to local names explicitly bound to one definition.
|
|
38
|
+
|
|
39
|
+
Reexports propagate the public name through TypeScript export clauses and
|
|
40
|
+
Python package initializers. Ordinary imports establish a binding only in
|
|
41
|
+
the importing file.
|
|
42
|
+
"""
|
|
43
|
+
names = {definition.file_path: {definition.name, definition.qualified_name}}
|
|
44
|
+
index = _file_index(ast)
|
|
45
|
+
queue = deque([(definition.file_path, definition.name)])
|
|
46
|
+
visited = set(queue)
|
|
47
|
+
while queue:
|
|
48
|
+
target, exported_name = queue.popleft()
|
|
49
|
+
try:
|
|
50
|
+
dependents = ast.get_dependents(target)
|
|
51
|
+
except AttributeError:
|
|
52
|
+
break
|
|
53
|
+
for source in dependents:
|
|
54
|
+
for imp in _imports_from(ast, source, target, index):
|
|
55
|
+
imported = imp.names[-1] if imp.names else ("*" if imp.is_reexport else "")
|
|
56
|
+
if imported not in {exported_name, "*"}:
|
|
57
|
+
continue
|
|
58
|
+
local = imp.alias or (exported_name if imported == "*" else imported)
|
|
59
|
+
bound = names.setdefault(source, set())
|
|
60
|
+
bound.update({exported_name, imported, local})
|
|
61
|
+
reexport = imp.is_reexport or PurePosixPath(source).name == "__init__.py"
|
|
62
|
+
state = (source, local)
|
|
63
|
+
if reexport and local and state not in visited:
|
|
64
|
+
visited.add(state)
|
|
65
|
+
queue.append(state)
|
|
66
|
+
return names
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def classify_symbol_references(ast, definition):
|
|
70
|
+
"""Partition references into linked evidence and unresolved candidates."""
|
|
71
|
+
names_by_file = linked_names_by_file(ast, definition)
|
|
72
|
+
query_names = {definition.name, definition.qualified_name}
|
|
73
|
+
for names in names_by_file.values():
|
|
74
|
+
query_names.update(names)
|
|
75
|
+
for path in names_by_file:
|
|
76
|
+
try:
|
|
77
|
+
ast.ensure_references_indexed(path)
|
|
78
|
+
except AttributeError:
|
|
79
|
+
pass
|
|
80
|
+
candidates = []
|
|
81
|
+
seen = set()
|
|
82
|
+
for name in query_names:
|
|
83
|
+
try:
|
|
84
|
+
refs = ast.get_callers(name)
|
|
85
|
+
except AttributeError:
|
|
86
|
+
refs = []
|
|
87
|
+
for ref in refs:
|
|
88
|
+
identity = (ref.file_path, ref.line, ref.ref_kind, ref.source, ref.symbol_name,
|
|
89
|
+
getattr(ref, "syntax_name", ""))
|
|
90
|
+
if identity in seen:
|
|
91
|
+
continue
|
|
92
|
+
seen.add(identity)
|
|
93
|
+
candidates.append(ref)
|
|
94
|
+
|
|
95
|
+
# Calls through a public alias may not be in the global reference index:
|
|
96
|
+
# the alias itself is not a definition, so the normal indexer deliberately
|
|
97
|
+
# skips it. Scan only files that an explicit binding connected to the
|
|
98
|
+
# selected definition, keeping this bounded and structurally scoped.
|
|
99
|
+
try:
|
|
100
|
+
root = Path(ast._root_dir)
|
|
101
|
+
original_names = {definition.name, definition.qualified_name}
|
|
102
|
+
for path, local_names in names_by_file.items():
|
|
103
|
+
if not local_names - original_names:
|
|
104
|
+
continue
|
|
105
|
+
file_ast = ast._ast_cache.get(path)
|
|
106
|
+
if file_ast is None:
|
|
107
|
+
continue
|
|
108
|
+
content = (root / path).read_text(encoding="utf-8", errors="replace")
|
|
109
|
+
for written, kind, line in reference_candidates(str(root / path), content, file_ast.language) or []:
|
|
110
|
+
parts = written.split(".")
|
|
111
|
+
if not ({written, parts[0], parts[-1]} & local_names):
|
|
112
|
+
continue
|
|
113
|
+
ref = SymbolRef(symbol_name=written, ref_kind=kind, file_path=path,
|
|
114
|
+
line=line, source="tree-sitter")
|
|
115
|
+
identity = (ref.file_path, ref.line, ref.ref_kind, ref.source, ref.symbol_name,
|
|
116
|
+
getattr(ref, "syntax_name", ""))
|
|
117
|
+
if identity not in seen:
|
|
118
|
+
seen.add(identity)
|
|
119
|
+
candidates.append(ref)
|
|
120
|
+
except (AttributeError, OSError):
|
|
121
|
+
pass
|
|
122
|
+
|
|
123
|
+
try:
|
|
124
|
+
same_name_definitions = ast.find_symbol(definition.name)
|
|
125
|
+
definition_count = len({(item.file_path, item.qualified_name)
|
|
126
|
+
for item in same_name_definitions})
|
|
127
|
+
except AttributeError:
|
|
128
|
+
same_name_definitions = []
|
|
129
|
+
definition_count = 2
|
|
130
|
+
same_file_count = len({item.qualified_name for item in same_name_definitions
|
|
131
|
+
if item.file_path == definition.file_path})
|
|
132
|
+
selected_parent = (definition.qualified_name.rsplit(".", 1)[0]
|
|
133
|
+
if "." in definition.qualified_name else "")
|
|
134
|
+
linked, unresolved = [], []
|
|
135
|
+
for ref in candidates:
|
|
136
|
+
local_names = names_by_file.get(ref.file_path, set())
|
|
137
|
+
syntax_name = getattr(ref, "syntax_name", "") or ref.symbol_name
|
|
138
|
+
caller = getattr(ref, "caller_qualified_name", "")
|
|
139
|
+
if (ref.file_path == definition.file_path
|
|
140
|
+
and (same_file_count <= 1
|
|
141
|
+
or (selected_parent and caller.rsplit(".", 1)[0] == selected_parent))):
|
|
142
|
+
resolution = "same_file"
|
|
143
|
+
elif {syntax_name, syntax_name.split(".", 1)[0]} & local_names:
|
|
144
|
+
resolution = "import_binding"
|
|
145
|
+
elif ref.source == "lsp" and definition_count == 1:
|
|
146
|
+
resolution = "language_server"
|
|
147
|
+
else:
|
|
148
|
+
resolution = "same_name_candidate"
|
|
149
|
+
row = {"ref_kind": ref.ref_kind, "file_path": ref.file_path, "line": ref.line,
|
|
150
|
+
"source": ref.source, "symbol": ref.symbol_name, "resolution": resolution}
|
|
151
|
+
if syntax_name != ref.symbol_name:
|
|
152
|
+
row["syntax"] = syntax_name
|
|
153
|
+
(linked if resolution != "same_name_candidate" else unresolved).append(row)
|
|
154
|
+
|
|
155
|
+
by_site = {}
|
|
156
|
+
for row in unresolved + linked:
|
|
157
|
+
site = row["file_path"], row["line"], row["ref_kind"], row["source"]
|
|
158
|
+
existing = by_site.get(site)
|
|
159
|
+
if (existing is None or existing["resolution"] == "same_name_candidate"
|
|
160
|
+
or row["resolution"] != "same_name_candidate"):
|
|
161
|
+
by_site[site] = row
|
|
162
|
+
linked = [row for row in by_site.values() if row["resolution"] != "same_name_candidate"]
|
|
163
|
+
unresolved = [row for row in by_site.values() if row["resolution"] == "same_name_candidate"]
|
|
164
|
+
def order(row):
|
|
165
|
+
return row["file_path"], row["line"], row["ref_kind"], row["source"], row["symbol"]
|
|
166
|
+
|
|
167
|
+
return sorted(linked, key=order), sorted(unresolved, key=order)
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
"""Opt-in timings outside model responses; never writes source or arguments."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import json
|
|
5
|
+
import logging
|
|
6
|
+
import os
|
|
7
|
+
import time
|
|
8
|
+
from contextlib import contextmanager
|
|
9
|
+
from contextvars import ContextVar
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
|
|
12
|
+
timings: ContextVar[dict | None] = ContextVar("intelligence_timings", default=None)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@contextmanager
|
|
16
|
+
def measure(name):
|
|
17
|
+
start = time.monotonic()
|
|
18
|
+
try:
|
|
19
|
+
yield
|
|
20
|
+
finally:
|
|
21
|
+
current = timings.get()
|
|
22
|
+
if current is not None:
|
|
23
|
+
current[name] = round(current.get(name, 0) + (time.monotonic() - start) * 1000, 2)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def enabled():
|
|
27
|
+
return bool(os.environ.get("ATTOCODE_INTEL_TRACE"))
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def emit(event: dict) -> None:
|
|
31
|
+
destination = os.environ.get("ATTOCODE_INTEL_TRACE")
|
|
32
|
+
if not destination:
|
|
33
|
+
return
|
|
34
|
+
try:
|
|
35
|
+
path = Path(destination).expanduser()
|
|
36
|
+
if not path.is_absolute():
|
|
37
|
+
return
|
|
38
|
+
fd = os.open(path, os.O_WRONLY | os.O_APPEND | os.O_CREAT, 0o600)
|
|
39
|
+
with os.fdopen(fd, "a") as stream:
|
|
40
|
+
stream.write(json.dumps(event, separators=(",", ":")) + "\n")
|
|
41
|
+
except OSError:
|
|
42
|
+
logging.getLogger(__name__).warning("Could not write intelligence timing trace")
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
"""Symbol-focused test evidence, reusing the parsed index instead of scanning file bodies."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
from pathlib import PurePosixPath
|
|
5
|
+
|
|
6
|
+
from attocode_intel.focused_evidence import task_terms, terms
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def is_test_path(path):
|
|
10
|
+
parts = PurePosixPath(path).parts
|
|
11
|
+
name = parts[-1] if parts else ""
|
|
12
|
+
return (any(p in {"test", "tests", "__tests__", "spec", "specs"} for p in parts)
|
|
13
|
+
or name.startswith("test_") or ".test." in name or ".spec." in name
|
|
14
|
+
or name.endswith(("_test.py", "_test.go", "Test.java", "Test.kt")))
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def rank_symbol_tests(ast, suggestions, files, symbol_name, task_hint, distances,
|
|
18
|
+
linked_reference_files=None, relevance_by_file=None):
|
|
19
|
+
selected = [s for f in files for s in ast.get_file_symbols(f)
|
|
20
|
+
if s.file_path == f and (s.name == symbol_name or s.qualified_name == symbol_name)]
|
|
21
|
+
symbol_terms = terms(symbol_name or "")
|
|
22
|
+
hint_terms, excluded_terms = task_terms(task_hint or "")
|
|
23
|
+
relevance_by_file = relevance_by_file or {}
|
|
24
|
+
direct = set(linked_reference_files or ())
|
|
25
|
+
if linked_reference_files is None:
|
|
26
|
+
for symbol in selected:
|
|
27
|
+
from attocode_intel.symbol_links import classify_symbol_references
|
|
28
|
+
linked, _ = classify_symbol_references(ast, symbol)
|
|
29
|
+
direct.update(r["file_path"] for r in linked if is_test_path(r["file_path"]))
|
|
30
|
+
# Search indexed test names as well as paths. A generic suite filename can
|
|
31
|
+
# contain a focused regression; filename matching alone should not hide it.
|
|
32
|
+
matches = {}
|
|
33
|
+
for path in set(ast.index.file_symbols) | set(suggestions) | direct:
|
|
34
|
+
inline = path in files and any("Rust inline test" in r for r in suggestions.get(path, {}).get("reasons", []))
|
|
35
|
+
if not is_test_path(path) and not inline:
|
|
36
|
+
continue
|
|
37
|
+
path_terms = terms(PurePosixPath(path).stem)
|
|
38
|
+
names = set().union(*(terms(s.name) for s in ast.get_file_symbols(path) if s.file_path == path))
|
|
39
|
+
symbol_hits = symbol_terms & (path_terms | names)
|
|
40
|
+
hint_hits = hint_terms & (path_terms | names)
|
|
41
|
+
excluded_hits = excluded_terms & (path_terms | names)
|
|
42
|
+
if path not in suggestions and path not in direct and not symbol_hits and not hint_hits:
|
|
43
|
+
continue
|
|
44
|
+
reasons = []
|
|
45
|
+
if path in direct:
|
|
46
|
+
reasons.append(f"Reference linked to selected symbol `{symbol_name}` by import or language-server evidence")
|
|
47
|
+
if symbol_hits:
|
|
48
|
+
reasons.append("Test name/path matches symbol terms: " + ", ".join(sorted(symbol_hits)))
|
|
49
|
+
if hint_hits:
|
|
50
|
+
reasons.append("Test name/path matches task terms: " + ", ".join(sorted(hint_hits)))
|
|
51
|
+
info = suggestions.get(path, {"priority": 3, "reasons": []})
|
|
52
|
+
matches[path] = {"file_path": path, "priority": 1 if path in direct else info["priority"],
|
|
53
|
+
"reasons": (reasons + info["reasons"])[:3],
|
|
54
|
+
"evidence": {"selected_symbol_reference": path in direct,
|
|
55
|
+
"reference_resolution": "linked" if path in direct else None,
|
|
56
|
+
"symbol_terms": sorted(symbol_hits), "task_terms": sorted(hint_hits),
|
|
57
|
+
"import_distance": distances.get(path)},
|
|
58
|
+
"_excluded_hits": len(excluded_hits)}
|
|
59
|
+
def order(row):
|
|
60
|
+
evidence = row["evidence"]
|
|
61
|
+
# Selected-symbol references are strongest, followed by an actual reverse
|
|
62
|
+
# import path. Lexical matches remain useful candidates after graph evidence.
|
|
63
|
+
return (not evidence["selected_symbol_reference"],
|
|
64
|
+
evidence["import_distance"] is None,
|
|
65
|
+
row["_excluded_hits"] > 0,
|
|
66
|
+
-relevance_by_file.get(row["file_path"], 0.0),
|
|
67
|
+
-(len(evidence["task_terms"]) + len(evidence["symbol_terms"])),
|
|
68
|
+
row["priority"], evidence["import_distance"] if evidence["import_distance"] is not None else float("inf"),
|
|
69
|
+
row["file_path"].startswith("docs/"), row["file_path"])
|
|
70
|
+
ranked = sorted(matches.values(), key=order)
|
|
71
|
+
for row in ranked:
|
|
72
|
+
row.pop("_excluded_hits")
|
|
73
|
+
return ranked
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
"""Testing utilities for attocode-code-intel.
|
|
2
|
+
|
|
3
|
+
This module provides standardized fixtures, mocks, and helpers for
|
|
4
|
+
testing code intelligence tools and MCP integrations.
|
|
5
|
+
|
|
6
|
+
Example usage in tests::
|
|
7
|
+
|
|
8
|
+
import pytest
|
|
9
|
+
from attocode_intel.testing import (
|
|
10
|
+
code_intel_service,
|
|
11
|
+
ast_service,
|
|
12
|
+
sample_project,
|
|
13
|
+
MockServiceFactory,
|
|
14
|
+
create_sample_project,
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
class TestSearchTools:
|
|
18
|
+
@pytest.fixture(autouse=True)
|
|
19
|
+
def setup(self, code_intel_service, ast_service):
|
|
20
|
+
self.service = code_intel_service
|
|
21
|
+
self.ast = ast_service
|
|
22
|
+
|
|
23
|
+
def test_symbol_search(self):
|
|
24
|
+
result = search_tools.symbol_search(symbol_name="foo")
|
|
25
|
+
assert "foo" in result
|
|
26
|
+
"""
|
|
27
|
+
|
|
28
|
+
from attocode_intel.testing.fixtures import (
|
|
29
|
+
ast_service,
|
|
30
|
+
code_intel_service,
|
|
31
|
+
sample_project,
|
|
32
|
+
)
|
|
33
|
+
from attocode_intel.testing.mocks import MockServiceFactory
|
|
34
|
+
from attocode_intel.testing.helpers import create_sample_project, get_tool_names
|
|
35
|
+
|
|
36
|
+
__all__ = [
|
|
37
|
+
"ast_service",
|
|
38
|
+
"code_intel_service",
|
|
39
|
+
"sample_project",
|
|
40
|
+
"MockServiceFactory",
|
|
41
|
+
"create_sample_project",
|
|
42
|
+
"get_tool_names",
|
|
43
|
+
]
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
"""Standardized pytest fixtures for code intelligence testing.
|
|
2
|
+
|
|
3
|
+
Provides reusable fixtures for CodeIntelService, ASTService, and
|
|
4
|
+
related components. These fixtures handle initialization, cleanup,
|
|
5
|
+
and reset of singletons to ensure test isolation.
|
|
6
|
+
|
|
7
|
+
For real repos, set ATTOCODE_PROJECT_DIR env var before running tests.
|
|
8
|
+
|
|
9
|
+
Usage::
|
|
10
|
+
|
|
11
|
+
import pytest
|
|
12
|
+
from attocode_intel.testing.fixtures import code_intel_service, ast_service
|
|
13
|
+
|
|
14
|
+
class TestMyTool:
|
|
15
|
+
def test_something(self, code_intel_service, ast_service):
|
|
16
|
+
assert code_intel_service is not None
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
from __future__ import annotations
|
|
20
|
+
|
|
21
|
+
from pathlib import Path
|
|
22
|
+
from typing import TYPE_CHECKING
|
|
23
|
+
|
|
24
|
+
import pytest
|
|
25
|
+
|
|
26
|
+
if TYPE_CHECKING:
|
|
27
|
+
from attocode_intel.service import CodeIntelService
|
|
28
|
+
from attocode_intel._internal.integrations.context.ast_service import ASTService
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
# ---------------------------------------------------------------------------
|
|
32
|
+
# Service fixtures
|
|
33
|
+
# ---------------------------------------------------------------------------
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
@pytest.fixture
|
|
37
|
+
def sample_project(tmp_path: Path) -> Path:
|
|
38
|
+
"""Create a minimal Python project in tmp_path for testing.
|
|
39
|
+
|
|
40
|
+
Creates:
|
|
41
|
+
- src/main.py, src/utils.py
|
|
42
|
+
- tests/test_main.py
|
|
43
|
+
- pyproject.toml
|
|
44
|
+
"""
|
|
45
|
+
src = tmp_path / "src"
|
|
46
|
+
tests = tmp_path / "tests"
|
|
47
|
+
|
|
48
|
+
src.mkdir(parents=True, exist_ok=True)
|
|
49
|
+
tests.mkdir(parents=True, exist_ok=True)
|
|
50
|
+
|
|
51
|
+
(src / "__init__.py").write_text("")
|
|
52
|
+
(src / "main.py").write_text(
|
|
53
|
+
'import os\nfrom src.utils import helper\n\ndef main():\n """Entry point."""\n return helper(42)\n\ndef cli(args: list) -> int:\n """CLI interface."""\n return 0\n'
|
|
54
|
+
)
|
|
55
|
+
(src / "utils.py").write_text(
|
|
56
|
+
'import os\n\ndef helper(value: int) -> str:\n """Process a value."""\n return str(value)\n\nclass BaseProcessor:\n """Base class."""\n def process(self) -> None: pass\n'
|
|
57
|
+
)
|
|
58
|
+
(tests / "__init__.py").write_text("")
|
|
59
|
+
(tests / "test_main.py").write_text(
|
|
60
|
+
'import pytest\nfrom src.main import main\n\ndef test_main():\n assert main() == "42"\n'
|
|
61
|
+
)
|
|
62
|
+
(tmp_path / "pyproject.toml").write_text(
|
|
63
|
+
'[project]\nname = "test"\nversion = "0.1.0"\n'
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
return tmp_path
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
@pytest.fixture
|
|
70
|
+
def code_intel_service(
|
|
71
|
+
sample_project: Path,
|
|
72
|
+
monkeypatch: pytest.MonkeyPatch,
|
|
73
|
+
) -> "CodeIntelService":
|
|
74
|
+
"""Provide a CodeIntelService instance for the sample project.
|
|
75
|
+
|
|
76
|
+
Resets all singletons before and after the test to ensure isolation.
|
|
77
|
+
"""
|
|
78
|
+
import attocode_intel._shared as ci_shared
|
|
79
|
+
from attocode_intel.service import CodeIntelService
|
|
80
|
+
|
|
81
|
+
monkeypatch.setenv("ATTOCODE_PROJECT_DIR", str(sample_project))
|
|
82
|
+
|
|
83
|
+
CodeIntelService._reset_instances()
|
|
84
|
+
ci_shared._service = None
|
|
85
|
+
ci_shared._remote_service = None
|
|
86
|
+
|
|
87
|
+
service = CodeIntelService.get_instance(str(sample_project))
|
|
88
|
+
|
|
89
|
+
yield service
|
|
90
|
+
|
|
91
|
+
CodeIntelService._reset_instances()
|
|
92
|
+
ci_shared._service = None
|
|
93
|
+
ci_shared._remote_service = None
|
|
94
|
+
monkeypatch.delenv("ATTOCODE_PROJECT_DIR", raising=False)
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
@pytest.fixture
|
|
98
|
+
def ast_service(
|
|
99
|
+
sample_project: Path,
|
|
100
|
+
monkeypatch: pytest.MonkeyPatch,
|
|
101
|
+
) -> "ASTService":
|
|
102
|
+
"""Provide an ASTService instance for the sample project.
|
|
103
|
+
|
|
104
|
+
Initializes skeleton indexing for fast test execution.
|
|
105
|
+
"""
|
|
106
|
+
import attocode_intel._shared as ci_shared
|
|
107
|
+
from attocode_intel._internal.integrations.context.ast_service import ASTService
|
|
108
|
+
|
|
109
|
+
monkeypatch.setenv("ATTOCODE_PROJECT_DIR", str(sample_project))
|
|
110
|
+
|
|
111
|
+
ASTService.clear_instances()
|
|
112
|
+
ci_shared._ast_service = None
|
|
113
|
+
|
|
114
|
+
service = ASTService.get_instance(str(sample_project))
|
|
115
|
+
if not service.initialized:
|
|
116
|
+
service.initialize_skeleton(indexing_depth="minimal")
|
|
117
|
+
|
|
118
|
+
yield service
|
|
119
|
+
|
|
120
|
+
ASTService.clear_instances()
|
|
121
|
+
ci_shared._ast_service = None
|
|
122
|
+
monkeypatch.delenv("ATTOCODE_PROJECT_DIR", raising=False)
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
"""Helper utilities for code intelligence testing.
|
|
2
|
+
|
|
3
|
+
Provides utility functions for creating test projects and
|
|
4
|
+
common test patterns. For real repos, use ATTOCODE_PROJECT_DIR.
|
|
5
|
+
|
|
6
|
+
Usage::
|
|
7
|
+
|
|
8
|
+
from attocode_intel.testing.helpers import create_sample_project
|
|
9
|
+
|
|
10
|
+
# Create a project with custom files
|
|
11
|
+
project = create_sample_project(tmp_path, {
|
|
12
|
+
"src/main.py": "def foo(): pass",
|
|
13
|
+
"src/utils.py": "def bar(): pass",
|
|
14
|
+
})
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
from __future__ import annotations
|
|
18
|
+
|
|
19
|
+
from pathlib import Path
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def create_sample_project(
|
|
23
|
+
tmp_path: Path,
|
|
24
|
+
files: dict[str, str],
|
|
25
|
+
*,
|
|
26
|
+
with_git: bool = False,
|
|
27
|
+
) -> Path:
|
|
28
|
+
"""Create a sample project with custom file structure.
|
|
29
|
+
|
|
30
|
+
Args:
|
|
31
|
+
tmp_path: The parent temporary directory.
|
|
32
|
+
files: Dict mapping file paths (relative to project root)
|
|
33
|
+
to their contents.
|
|
34
|
+
with_git: Whether to create a .git directory.
|
|
35
|
+
|
|
36
|
+
Returns:
|
|
37
|
+
Path to the created project root.
|
|
38
|
+
|
|
39
|
+
Example::
|
|
40
|
+
|
|
41
|
+
project = create_sample_project(tmp_path, {
|
|
42
|
+
"src/main.py": "def main(): pass",
|
|
43
|
+
"src/utils.py": "def helper(): pass",
|
|
44
|
+
"tests/test_main.py": "def test_main(): pass",
|
|
45
|
+
})
|
|
46
|
+
"""
|
|
47
|
+
for file_path, content in files.items():
|
|
48
|
+
full_path = tmp_path / file_path
|
|
49
|
+
full_path.parent.mkdir(parents=True, exist_ok=True)
|
|
50
|
+
full_path.write_text(content)
|
|
51
|
+
|
|
52
|
+
if with_git:
|
|
53
|
+
git_dir = tmp_path / ".git"
|
|
54
|
+
git_dir.mkdir(parents=True, exist_ok=True)
|
|
55
|
+
(git_dir / "config").write_text("[core]\n\tautocrlf = true\n")
|
|
56
|
+
|
|
57
|
+
return tmp_path
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def get_tool_names() -> list[str]:
|
|
61
|
+
"""Return the list of MCP tool module names.
|
|
62
|
+
|
|
63
|
+
Returns:
|
|
64
|
+
List of tool module names (without .py extension).
|
|
65
|
+
"""
|
|
66
|
+
from attocode_intel import tools as tools_module
|
|
67
|
+
|
|
68
|
+
tool_dir = Path(tools_module.__file__).parent
|
|
69
|
+
return [f.stem for f in tool_dir.glob("*.py") if not f.stem.startswith("_")]
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
__all__ = [
|
|
73
|
+
"create_sample_project",
|
|
74
|
+
"get_tool_names",
|
|
75
|
+
]
|