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,417 @@
|
|
|
1
|
+
"""Query history tracker with combo boosting.
|
|
2
|
+
|
|
3
|
+
Tracks successful query-result pairs to boost files that are repeatedly
|
|
4
|
+
opened with the same query. This is different from frecency which tracks
|
|
5
|
+
file access patterns.
|
|
6
|
+
|
|
7
|
+
Combo boosting:
|
|
8
|
+
- When user searches "foo" and opens "bar.py", we track that mapping
|
|
9
|
+
- If user repeatedly opens "bar.py" when searching "foo", we boost "bar.py"
|
|
10
|
+
- This helps prioritize commonly co-occuring results
|
|
11
|
+
|
|
12
|
+
Based on fff.nvim's query_tracker.rs.
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
from __future__ import annotations
|
|
16
|
+
|
|
17
|
+
import logging
|
|
18
|
+
import sqlite3
|
|
19
|
+
import threading
|
|
20
|
+
import time
|
|
21
|
+
from dataclasses import dataclass
|
|
22
|
+
from pathlib import Path
|
|
23
|
+
|
|
24
|
+
logger = logging.getLogger(__name__)
|
|
25
|
+
|
|
26
|
+
# Combo boosting constants
|
|
27
|
+
_MIN_COMBO_COUNT: int = 3 # Minimum selections before combo boost applies
|
|
28
|
+
_COMBO_BOOST_MULTIPLIER: float = 100.0 # Score multiplier for combo matches
|
|
29
|
+
_MAX_HISTORY_DAYS: int = 30 # Only consider selections within 30 days
|
|
30
|
+
|
|
31
|
+
# SQLite schema version
|
|
32
|
+
_SCHEMA_VERSION: int = 1
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
@dataclass
|
|
36
|
+
class QueryResult:
|
|
37
|
+
"""A tracked query result."""
|
|
38
|
+
query: str
|
|
39
|
+
file_path: str
|
|
40
|
+
count: int # How many times this query led to opening this file
|
|
41
|
+
last_selected: float # Unix timestamp of last selection
|
|
42
|
+
combo_score: float # Calculated combo boost score
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
@dataclass
|
|
46
|
+
class QueryHistoryStats:
|
|
47
|
+
"""Statistics about the query history."""
|
|
48
|
+
total_queries: int
|
|
49
|
+
total_selections: int
|
|
50
|
+
unique_queries: int
|
|
51
|
+
unique_files: int
|
|
52
|
+
combo_boosts: int # Number of files with combo boost > 0
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
class QueryHistoryTracker:
|
|
56
|
+
"""Tracks query-result pairs for combo boosting.
|
|
57
|
+
|
|
58
|
+
Stores which files were selected after which queries, allowing
|
|
59
|
+
us to boost results that commonly appear together.
|
|
60
|
+
"""
|
|
61
|
+
|
|
62
|
+
def __init__(
|
|
63
|
+
self,
|
|
64
|
+
db_path: str | Path = ".attocode/query_history",
|
|
65
|
+
*,
|
|
66
|
+
min_combo_count: int = _MIN_COMBO_COUNT,
|
|
67
|
+
combo_boost_multiplier: float = _COMBO_BOOST_MULTIPLIER,
|
|
68
|
+
) -> None:
|
|
69
|
+
self._db_path = Path(db_path)
|
|
70
|
+
self._min_combo_count = min_combo_count
|
|
71
|
+
self._combo_boost_multiplier = combo_boost_multiplier
|
|
72
|
+
self._lock = threading.Lock()
|
|
73
|
+
self._conn: sqlite3.Connection | None = None
|
|
74
|
+
self._ensure_db()
|
|
75
|
+
|
|
76
|
+
def _ensure_db(self) -> None:
|
|
77
|
+
"""Ensure the database directory and tables exist."""
|
|
78
|
+
self._db_path.mkdir(parents=True, exist_ok=True)
|
|
79
|
+
db_file = self._db_path / "query_history.db"
|
|
80
|
+
|
|
81
|
+
conn = sqlite3.connect(
|
|
82
|
+
str(db_file),
|
|
83
|
+
timeout=5.0,
|
|
84
|
+
check_same_thread=False,
|
|
85
|
+
)
|
|
86
|
+
conn.execute("PRAGMA journal_mode=WAL")
|
|
87
|
+
conn.execute("PRAGMA foreign_keys=ON")
|
|
88
|
+
|
|
89
|
+
# Main table: query_selections
|
|
90
|
+
conn.execute("""
|
|
91
|
+
CREATE TABLE IF NOT EXISTS query_selections (
|
|
92
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
93
|
+
query TEXT NOT NULL,
|
|
94
|
+
file_path TEXT NOT NULL,
|
|
95
|
+
count INTEGER NOT NULL DEFAULT 1,
|
|
96
|
+
last_selected REAL NOT NULL,
|
|
97
|
+
UNIQUE(query, file_path)
|
|
98
|
+
)
|
|
99
|
+
""")
|
|
100
|
+
|
|
101
|
+
# Index for fast lookups
|
|
102
|
+
conn.execute("""
|
|
103
|
+
CREATE INDEX IF NOT EXISTS idx_query
|
|
104
|
+
ON query_selections(query)
|
|
105
|
+
""")
|
|
106
|
+
|
|
107
|
+
conn.execute("""
|
|
108
|
+
CREATE INDEX IF NOT EXISTS idx_file
|
|
109
|
+
ON query_selections(file_path)
|
|
110
|
+
""")
|
|
111
|
+
|
|
112
|
+
# Meta table for schema version
|
|
113
|
+
conn.execute("""
|
|
114
|
+
CREATE TABLE IF NOT EXISTS meta (
|
|
115
|
+
key TEXT PRIMARY KEY,
|
|
116
|
+
value TEXT NOT NULL
|
|
117
|
+
)
|
|
118
|
+
""")
|
|
119
|
+
|
|
120
|
+
cursor = conn.execute(
|
|
121
|
+
"SELECT value FROM meta WHERE key = 'version'",
|
|
122
|
+
)
|
|
123
|
+
row = cursor.fetchone()
|
|
124
|
+
if row is None:
|
|
125
|
+
conn.execute(
|
|
126
|
+
"INSERT INTO meta (key, value) VALUES ('version', ?)",
|
|
127
|
+
(str(_SCHEMA_VERSION),),
|
|
128
|
+
)
|
|
129
|
+
conn.commit()
|
|
130
|
+
|
|
131
|
+
self._conn = conn
|
|
132
|
+
|
|
133
|
+
def close(self) -> None:
|
|
134
|
+
"""Close the database connection."""
|
|
135
|
+
with self._lock:
|
|
136
|
+
if self._conn is not None:
|
|
137
|
+
self._conn.close()
|
|
138
|
+
self._conn = None
|
|
139
|
+
|
|
140
|
+
def __del__(self) -> None:
|
|
141
|
+
try:
|
|
142
|
+
self.close()
|
|
143
|
+
except Exception:
|
|
144
|
+
pass
|
|
145
|
+
|
|
146
|
+
def track_selection(self, query: str, file_path: str) -> None:
|
|
147
|
+
"""Record that a user selected a file after searching for query.
|
|
148
|
+
|
|
149
|
+
Args:
|
|
150
|
+
query: The search query.
|
|
151
|
+
file_path: The file that was selected/opened.
|
|
152
|
+
"""
|
|
153
|
+
now = time.time()
|
|
154
|
+
query_lower = query.lower().strip()
|
|
155
|
+
file_path = str(file_path)
|
|
156
|
+
|
|
157
|
+
with self._lock:
|
|
158
|
+
if self._conn is None:
|
|
159
|
+
return
|
|
160
|
+
|
|
161
|
+
# Try to update existing
|
|
162
|
+
cursor = self._conn.execute(
|
|
163
|
+
"""
|
|
164
|
+
UPDATE query_selections
|
|
165
|
+
SET count = count + 1, last_selected = ?
|
|
166
|
+
WHERE query = ? AND file_path = ?
|
|
167
|
+
""",
|
|
168
|
+
(now, query_lower, file_path),
|
|
169
|
+
)
|
|
170
|
+
|
|
171
|
+
if cursor.rowcount == 0:
|
|
172
|
+
# Insert new record
|
|
173
|
+
self._conn.execute(
|
|
174
|
+
"""
|
|
175
|
+
INSERT INTO query_selections (query, file_path, count, last_selected)
|
|
176
|
+
VALUES (?, ?, 1, ?)
|
|
177
|
+
""",
|
|
178
|
+
(query_lower, file_path, now),
|
|
179
|
+
)
|
|
180
|
+
|
|
181
|
+
self._conn.commit()
|
|
182
|
+
|
|
183
|
+
# Prune old entries
|
|
184
|
+
cutoff = now - (_MAX_HISTORY_DAYS * 86400)
|
|
185
|
+
self._conn.execute(
|
|
186
|
+
"DELETE FROM query_selections WHERE last_selected < ?",
|
|
187
|
+
(cutoff,),
|
|
188
|
+
)
|
|
189
|
+
self._conn.commit()
|
|
190
|
+
|
|
191
|
+
def get_combo_boost(self, query: str, file_path: str) -> float:
|
|
192
|
+
"""Get the combo boost score for a query+file pair.
|
|
193
|
+
|
|
194
|
+
Args:
|
|
195
|
+
query: The search query.
|
|
196
|
+
file_path: The file to get the boost for.
|
|
197
|
+
|
|
198
|
+
Returns:
|
|
199
|
+
Combo boost score (0 if below min_combo_count).
|
|
200
|
+
"""
|
|
201
|
+
query_lower = query.lower().strip()
|
|
202
|
+
file_path = str(file_path)
|
|
203
|
+
|
|
204
|
+
with self._lock:
|
|
205
|
+
if self._conn is None:
|
|
206
|
+
return 0.0
|
|
207
|
+
|
|
208
|
+
cursor = self._conn.execute(
|
|
209
|
+
"""
|
|
210
|
+
SELECT count, last_selected FROM query_selections
|
|
211
|
+
WHERE query = ? AND file_path = ?
|
|
212
|
+
""",
|
|
213
|
+
(query_lower, file_path),
|
|
214
|
+
)
|
|
215
|
+
row = cursor.fetchone()
|
|
216
|
+
|
|
217
|
+
if row is None:
|
|
218
|
+
return 0.0
|
|
219
|
+
|
|
220
|
+
count, last_selected = row
|
|
221
|
+
|
|
222
|
+
# Check if within history window
|
|
223
|
+
now = time.time()
|
|
224
|
+
cutoff = now - (_MAX_HISTORY_DAYS * 86400)
|
|
225
|
+
if last_selected < cutoff:
|
|
226
|
+
return 0.0
|
|
227
|
+
|
|
228
|
+
# Apply combo boost if above threshold
|
|
229
|
+
if count >= self._min_combo_count:
|
|
230
|
+
return float(count) * self._combo_boost_multiplier
|
|
231
|
+
|
|
232
|
+
return 0.0
|
|
233
|
+
|
|
234
|
+
def get_combo_boosts_batch(
|
|
235
|
+
self,
|
|
236
|
+
query: str,
|
|
237
|
+
file_paths: list[str],
|
|
238
|
+
) -> dict[str, float]:
|
|
239
|
+
"""Get combo boost scores for multiple files for a query.
|
|
240
|
+
|
|
241
|
+
Args:
|
|
242
|
+
query: The search query.
|
|
243
|
+
file_paths: List of file paths to check.
|
|
244
|
+
|
|
245
|
+
Returns:
|
|
246
|
+
Dict mapping file_path -> combo_boost_score.
|
|
247
|
+
"""
|
|
248
|
+
boosts: dict[str, float] = {}
|
|
249
|
+
for path in file_paths:
|
|
250
|
+
boosts[path] = self.get_combo_boost(query, path)
|
|
251
|
+
return boosts
|
|
252
|
+
|
|
253
|
+
def get_top_files_for_query(
|
|
254
|
+
self,
|
|
255
|
+
query: str,
|
|
256
|
+
limit: int = 10,
|
|
257
|
+
) -> list[tuple[str, int, float]]:
|
|
258
|
+
"""Get the top files for a given query.
|
|
259
|
+
|
|
260
|
+
Args:
|
|
261
|
+
query: The search query.
|
|
262
|
+
limit: Maximum number of results.
|
|
263
|
+
|
|
264
|
+
Returns:
|
|
265
|
+
List of (file_path, count, combo_score) tuples.
|
|
266
|
+
"""
|
|
267
|
+
query_lower = query.lower().strip()
|
|
268
|
+
|
|
269
|
+
with self._lock:
|
|
270
|
+
if self._conn is None:
|
|
271
|
+
return []
|
|
272
|
+
|
|
273
|
+
cursor = self._conn.execute(
|
|
274
|
+
"""
|
|
275
|
+
SELECT file_path, count, last_selected
|
|
276
|
+
FROM query_selections
|
|
277
|
+
WHERE query = ? AND last_selected > ?
|
|
278
|
+
ORDER BY count DESC
|
|
279
|
+
LIMIT ?
|
|
280
|
+
""",
|
|
281
|
+
(query_lower, time.time() - (_MAX_HISTORY_DAYS * 86400), limit),
|
|
282
|
+
)
|
|
283
|
+
|
|
284
|
+
results: list[tuple[str, int, float]] = []
|
|
285
|
+
for row in cursor.fetchall():
|
|
286
|
+
file_path, count, last_selected = row
|
|
287
|
+
combo_score = (
|
|
288
|
+
float(count) * self._combo_boost_multiplier
|
|
289
|
+
if count >= self._min_combo_count
|
|
290
|
+
else 0.0
|
|
291
|
+
)
|
|
292
|
+
results.append((file_path, count, combo_score))
|
|
293
|
+
|
|
294
|
+
return results
|
|
295
|
+
|
|
296
|
+
def get_stats(self) -> QueryHistoryStats:
|
|
297
|
+
"""Get statistics about the query history."""
|
|
298
|
+
with self._lock:
|
|
299
|
+
if self._conn is None:
|
|
300
|
+
return QueryHistoryStats(
|
|
301
|
+
total_queries=0,
|
|
302
|
+
total_selections=0,
|
|
303
|
+
unique_queries=0,
|
|
304
|
+
unique_files=0,
|
|
305
|
+
combo_boosts=0,
|
|
306
|
+
)
|
|
307
|
+
|
|
308
|
+
# Total selections
|
|
309
|
+
cursor = self._conn.execute(
|
|
310
|
+
"SELECT COALESCE(SUM(count), 0) FROM query_selections",
|
|
311
|
+
)
|
|
312
|
+
total_selections = cursor.fetchone()[0] or 0
|
|
313
|
+
|
|
314
|
+
# Total queries
|
|
315
|
+
cursor = self._conn.execute(
|
|
316
|
+
"SELECT COUNT(*) FROM query_selections",
|
|
317
|
+
)
|
|
318
|
+
total_queries = cursor.fetchone()[0] or 0
|
|
319
|
+
|
|
320
|
+
# Unique queries
|
|
321
|
+
cursor = self._conn.execute(
|
|
322
|
+
"SELECT COUNT(DISTINCT query) FROM query_selections",
|
|
323
|
+
)
|
|
324
|
+
unique_queries = cursor.fetchone()[0] or 0
|
|
325
|
+
|
|
326
|
+
# Unique files
|
|
327
|
+
cursor = self._conn.execute(
|
|
328
|
+
"SELECT COUNT(DISTINCT file_path) FROM query_selections",
|
|
329
|
+
)
|
|
330
|
+
unique_files = cursor.fetchone()[0] or 0
|
|
331
|
+
|
|
332
|
+
# Files with combo boost > 0
|
|
333
|
+
cursor = self._conn.execute(
|
|
334
|
+
"""
|
|
335
|
+
SELECT COUNT(*) FROM query_selections
|
|
336
|
+
WHERE count >= ?
|
|
337
|
+
""",
|
|
338
|
+
(self._min_combo_count,),
|
|
339
|
+
)
|
|
340
|
+
combo_boosts = cursor.fetchone()[0] or 0
|
|
341
|
+
|
|
342
|
+
return QueryHistoryStats(
|
|
343
|
+
total_queries=total_queries,
|
|
344
|
+
total_selections=total_selections,
|
|
345
|
+
unique_queries=unique_queries,
|
|
346
|
+
unique_files=unique_files,
|
|
347
|
+
combo_boosts=combo_boosts,
|
|
348
|
+
)
|
|
349
|
+
|
|
350
|
+
def clear(self, query: str | None = None, file_path: str | None = None) -> int:
|
|
351
|
+
"""Clear query history.
|
|
352
|
+
|
|
353
|
+
Args:
|
|
354
|
+
query: If provided, clear only this query. If None, clear all.
|
|
355
|
+
file_path: If provided with query, clear only that pair.
|
|
356
|
+
|
|
357
|
+
Returns:
|
|
358
|
+
Number of entries cleared.
|
|
359
|
+
"""
|
|
360
|
+
with self._lock:
|
|
361
|
+
if self._conn is None:
|
|
362
|
+
return 0
|
|
363
|
+
|
|
364
|
+
if query is None:
|
|
365
|
+
self._conn.execute("DELETE FROM query_selections")
|
|
366
|
+
self._conn.commit()
|
|
367
|
+
return -1
|
|
368
|
+
|
|
369
|
+
query_lower = query.lower().strip()
|
|
370
|
+
|
|
371
|
+
if file_path is None:
|
|
372
|
+
cursor = self._conn.execute(
|
|
373
|
+
"DELETE FROM query_selections WHERE query = ?",
|
|
374
|
+
(query_lower,),
|
|
375
|
+
)
|
|
376
|
+
else:
|
|
377
|
+
cursor = self._conn.execute(
|
|
378
|
+
"DELETE FROM query_selections WHERE query = ? AND file_path = ?",
|
|
379
|
+
(query_lower, str(file_path)),
|
|
380
|
+
)
|
|
381
|
+
|
|
382
|
+
self._conn.commit()
|
|
383
|
+
return cursor.rowcount
|
|
384
|
+
|
|
385
|
+
|
|
386
|
+
# Global singleton instance
|
|
387
|
+
_tracker: QueryHistoryTracker | None = None
|
|
388
|
+
_tracker_lock = threading.Lock()
|
|
389
|
+
|
|
390
|
+
|
|
391
|
+
def get_query_tracker(
|
|
392
|
+
db_path: str | Path = ".attocode/query_history",
|
|
393
|
+
*,
|
|
394
|
+
min_combo_count: int = _MIN_COMBO_COUNT,
|
|
395
|
+
combo_boost_multiplier: float = _COMBO_BOOST_MULTIPLIER,
|
|
396
|
+
) -> QueryHistoryTracker:
|
|
397
|
+
"""Get or create the global QueryHistoryTracker instance."""
|
|
398
|
+
global _tracker
|
|
399
|
+
|
|
400
|
+
with _tracker_lock:
|
|
401
|
+
if _tracker is None:
|
|
402
|
+
_tracker = QueryHistoryTracker(
|
|
403
|
+
db_path=db_path,
|
|
404
|
+
min_combo_count=min_combo_count,
|
|
405
|
+
combo_boost_multiplier=combo_boost_multiplier,
|
|
406
|
+
)
|
|
407
|
+
return _tracker
|
|
408
|
+
|
|
409
|
+
|
|
410
|
+
def reset_query_tracker() -> None:
|
|
411
|
+
"""Reset the global tracker instance."""
|
|
412
|
+
global _tracker
|
|
413
|
+
|
|
414
|
+
with _tracker_lock:
|
|
415
|
+
if _tracker is not None:
|
|
416
|
+
_tracker.close()
|
|
417
|
+
_tracker = None
|