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,432 @@
|
|
|
1
|
+
"""MCP-free pin storage + manifest hashing helpers.
|
|
2
|
+
|
|
3
|
+
Everything in this module is importable without touching the MCP
|
|
4
|
+
runtime. It exists so that the HTTP API providers (``api/providers/``)
|
|
5
|
+
can mint and persist retrieval pins without transitively importing
|
|
6
|
+
``attocode_intel._shared``, which would call ``sys.exit(1)`` in
|
|
7
|
+
environments where the ``mcp`` package isn't installed.
|
|
8
|
+
|
|
9
|
+
``tools/pin_tools.py`` re-exports everything here and layers the MCP
|
|
10
|
+
``@mcp.tool()`` decorators + the ``_stamp_pin`` footer on top.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
import logging
|
|
16
|
+
import os
|
|
17
|
+
import sqlite3
|
|
18
|
+
from typing import Any
|
|
19
|
+
|
|
20
|
+
from attocode_intel.artifacts import (
|
|
21
|
+
RetrievalPin,
|
|
22
|
+
compute_store_hash,
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
logger = logging.getLogger(__name__)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def _get_project_dir() -> str:
|
|
29
|
+
"""Dynamic dispatch to ``project_dir._get_project_dir``.
|
|
30
|
+
|
|
31
|
+
Looks up the function on every call so monkeypatches in
|
|
32
|
+
``project_dir`` are observed by pin_store without needing to
|
|
33
|
+
also patch this module directly. Prevents the "stale binding
|
|
34
|
+
across test fixtures" class of pollution that a ``from
|
|
35
|
+
project_dir import`` binding would otherwise introduce.
|
|
36
|
+
|
|
37
|
+
We use the neutral ``project_dir`` module rather than
|
|
38
|
+
``_shared`` so HTTP providers can import this without dragging
|
|
39
|
+
in the MCP runtime (which ``_shared`` ``sys.exit(1)`` s on if
|
|
40
|
+
the ``mcp`` package is missing).
|
|
41
|
+
"""
|
|
42
|
+
from attocode_intel import project_dir
|
|
43
|
+
return project_dir._get_project_dir()
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
# ---------------------------------------------------------------------------
|
|
47
|
+
# Per-store hash computation
|
|
48
|
+
# ---------------------------------------------------------------------------
|
|
49
|
+
|
|
50
|
+
# Declarative table for stdio stores — each entry is enough to compute the
|
|
51
|
+
# store's cheap manifest hash. ``schema_version_source`` is either a literal
|
|
52
|
+
# default or a ``("table", "where_key")`` tuple used to query a metadata row.
|
|
53
|
+
#
|
|
54
|
+
# We keep the shape simple on purpose: row_count + max(updated_at) + schema
|
|
55
|
+
# version + file presence is enough to change on any mutation while staying
|
|
56
|
+
# stable under no-op reads.
|
|
57
|
+
_STORE_DEFS: tuple[dict[str, Any], ...] = (
|
|
58
|
+
{
|
|
59
|
+
"name": "symbols",
|
|
60
|
+
"path_fragment": os.path.join("index", "symbols.db"),
|
|
61
|
+
"tables": ("symbols", "files", "refs", "dependencies"),
|
|
62
|
+
"timestamp_table": "files",
|
|
63
|
+
"timestamp_col": "mtime",
|
|
64
|
+
"schema_meta": ("metadata", "schema_version"),
|
|
65
|
+
"default_schema_version": "1",
|
|
66
|
+
},
|
|
67
|
+
{
|
|
68
|
+
"name": "embeddings",
|
|
69
|
+
"path_fragment": os.path.join("vectors", "embeddings.db"),
|
|
70
|
+
"tables": ("vectors", "file_metadata"),
|
|
71
|
+
"timestamp_table": "file_metadata",
|
|
72
|
+
"timestamp_col": "last_indexed_at",
|
|
73
|
+
"schema_meta": ("store_metadata", "schema_version"),
|
|
74
|
+
"default_schema_version": "1",
|
|
75
|
+
},
|
|
76
|
+
{
|
|
77
|
+
"name": "kw_index",
|
|
78
|
+
"path_fragment": os.path.join("index", "kw_index.db"),
|
|
79
|
+
"tables": ("kw_docs",),
|
|
80
|
+
"timestamp_table": None,
|
|
81
|
+
"timestamp_col": None,
|
|
82
|
+
"schema_meta": None,
|
|
83
|
+
"default_schema_version": "1",
|
|
84
|
+
},
|
|
85
|
+
{
|
|
86
|
+
"name": "learnings",
|
|
87
|
+
"path_fragment": os.path.join("cache", "memory.db"),
|
|
88
|
+
"tables": ("learnings",),
|
|
89
|
+
"timestamp_table": "learnings",
|
|
90
|
+
"timestamp_col": "updated_at",
|
|
91
|
+
"schema_meta": None,
|
|
92
|
+
"default_schema_version": "1",
|
|
93
|
+
},
|
|
94
|
+
{
|
|
95
|
+
"name": "adrs",
|
|
96
|
+
"path_fragment": "adrs.db",
|
|
97
|
+
"tables": ("adrs",),
|
|
98
|
+
"timestamp_table": "adrs",
|
|
99
|
+
"timestamp_col": "updated_at",
|
|
100
|
+
"schema_meta": None,
|
|
101
|
+
"default_schema_version": "1",
|
|
102
|
+
},
|
|
103
|
+
{
|
|
104
|
+
"name": "frecency",
|
|
105
|
+
"path_fragment": os.path.join("frecency", "frecency.db"),
|
|
106
|
+
"tables": ("frecency_accesses",),
|
|
107
|
+
"timestamp_table": "frecency_accesses",
|
|
108
|
+
"timestamp_col": "updated_at",
|
|
109
|
+
"schema_meta": ("meta", "schema_version"),
|
|
110
|
+
"default_schema_version": "1",
|
|
111
|
+
},
|
|
112
|
+
{
|
|
113
|
+
"name": "query_history",
|
|
114
|
+
"path_fragment": os.path.join("query_history", "query_history.db"),
|
|
115
|
+
"tables": ("query_selections",),
|
|
116
|
+
"timestamp_table": "query_selections",
|
|
117
|
+
"timestamp_col": "last_selected",
|
|
118
|
+
"schema_meta": ("meta", "schema_version"),
|
|
119
|
+
"default_schema_version": "1",
|
|
120
|
+
},
|
|
121
|
+
)
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def _hash_for_store(project_dir: str, defn: dict[str, Any]) -> str:
|
|
125
|
+
"""Compute the manifest hash for one stdio store.
|
|
126
|
+
|
|
127
|
+
Returns the string ``"absent"`` if the store's DB file doesn't exist —
|
|
128
|
+
that way "absent" vs "empty" vs "populated" all produce distinct hashes
|
|
129
|
+
and a drift check catches any of those transitions.
|
|
130
|
+
"""
|
|
131
|
+
db_path = os.path.join(project_dir, ".attocode", defn["path_fragment"])
|
|
132
|
+
if not os.path.exists(db_path):
|
|
133
|
+
return "absent"
|
|
134
|
+
|
|
135
|
+
try:
|
|
136
|
+
conn = sqlite3.connect(f"file:{db_path}?mode=ro", uri=True)
|
|
137
|
+
except sqlite3.Error as exc:
|
|
138
|
+
logger.debug("pin: could not open %s (%s)", db_path, exc)
|
|
139
|
+
return "unreadable"
|
|
140
|
+
|
|
141
|
+
try:
|
|
142
|
+
# Row count: sum across all declared tables (so dropping one and
|
|
143
|
+
# adding another still changes the hash).
|
|
144
|
+
total_rows = 0
|
|
145
|
+
existing_tables: list[str] = []
|
|
146
|
+
for tbl in defn["tables"]:
|
|
147
|
+
try:
|
|
148
|
+
row = conn.execute(f"SELECT COUNT(*) FROM {tbl}").fetchone()
|
|
149
|
+
except sqlite3.OperationalError:
|
|
150
|
+
continue
|
|
151
|
+
total_rows += int(row[0]) if row else 0
|
|
152
|
+
existing_tables.append(tbl)
|
|
153
|
+
|
|
154
|
+
max_updated_at: float | None = None
|
|
155
|
+
tt = defn["timestamp_table"]
|
|
156
|
+
tc = defn["timestamp_col"]
|
|
157
|
+
if tt and tc and tt in existing_tables:
|
|
158
|
+
try:
|
|
159
|
+
row = conn.execute(f"SELECT MAX({tc}) FROM {tt}").fetchone()
|
|
160
|
+
if row and row[0] is not None:
|
|
161
|
+
max_updated_at = float(row[0])
|
|
162
|
+
except (sqlite3.OperationalError, ValueError, TypeError):
|
|
163
|
+
pass
|
|
164
|
+
|
|
165
|
+
schema_version = defn["default_schema_version"]
|
|
166
|
+
sm = defn["schema_meta"]
|
|
167
|
+
if sm is not None:
|
|
168
|
+
meta_table, meta_key = sm
|
|
169
|
+
try:
|
|
170
|
+
row = conn.execute(
|
|
171
|
+
f"SELECT value FROM {meta_table} WHERE key = ?",
|
|
172
|
+
(meta_key,),
|
|
173
|
+
).fetchone()
|
|
174
|
+
if row:
|
|
175
|
+
schema_version = str(row[0])
|
|
176
|
+
except sqlite3.OperationalError:
|
|
177
|
+
pass
|
|
178
|
+
|
|
179
|
+
return compute_store_hash(
|
|
180
|
+
schema_version=schema_version,
|
|
181
|
+
row_count=total_rows,
|
|
182
|
+
max_updated_at=max_updated_at,
|
|
183
|
+
extra={"tables": sorted(existing_tables)},
|
|
184
|
+
)
|
|
185
|
+
finally:
|
|
186
|
+
conn.close()
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
def _hash_for_trigrams(project_dir: str) -> str:
|
|
190
|
+
"""Trigram index is binary files, not a DB — hash on presence + mtime + size."""
|
|
191
|
+
base = os.path.join(project_dir, ".attocode", "index")
|
|
192
|
+
parts: list[tuple[str, float, int]] = []
|
|
193
|
+
for name in ("trigrams.lookup", "trigrams.postings", "trigrams.db"):
|
|
194
|
+
path = os.path.join(base, name)
|
|
195
|
+
if not os.path.exists(path):
|
|
196
|
+
continue
|
|
197
|
+
st = os.stat(path)
|
|
198
|
+
parts.append((name, st.st_mtime, st.st_size))
|
|
199
|
+
if not parts:
|
|
200
|
+
return "absent"
|
|
201
|
+
return compute_store_hash(
|
|
202
|
+
schema_version="trigram_v1",
|
|
203
|
+
row_count=len(parts),
|
|
204
|
+
max_updated_at=max(p[1] for p in parts),
|
|
205
|
+
extra={"files": [[p[0], p[2]] for p in parts]},
|
|
206
|
+
)
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
def _compute_current_manifest_hashes(project_dir: str | None = None) -> dict[str, str]:
|
|
210
|
+
"""Return the current per-store manifest hashes for a project.
|
|
211
|
+
|
|
212
|
+
``project_dir`` is optional: if omitted, falls back to the active
|
|
213
|
+
project via ``_get_project_dir()`` — useful for pin_tools callers
|
|
214
|
+
that don't carry an explicit project context. Callers with a
|
|
215
|
+
bound service (e.g. ``LocalSearchProvider``) MUST pass the
|
|
216
|
+
service's ``project_dir`` so the resulting pin is minted against
|
|
217
|
+
the correct repo's on-disk stores.
|
|
218
|
+
"""
|
|
219
|
+
if project_dir is None:
|
|
220
|
+
project_dir = _get_project_dir()
|
|
221
|
+
hashes: dict[str, str] = {}
|
|
222
|
+
for defn in _STORE_DEFS:
|
|
223
|
+
hashes[defn["name"]] = _hash_for_store(project_dir, defn)
|
|
224
|
+
hashes["trigrams"] = _hash_for_trigrams(project_dir)
|
|
225
|
+
return hashes
|
|
226
|
+
|
|
227
|
+
|
|
228
|
+
# ---------------------------------------------------------------------------
|
|
229
|
+
# PinStore — persistent pin storage (small SQLite DB)
|
|
230
|
+
# ---------------------------------------------------------------------------
|
|
231
|
+
|
|
232
|
+
|
|
233
|
+
class PinStore:
|
|
234
|
+
"""SQLite-backed pin registry at ``.attocode/cache/pins.db``."""
|
|
235
|
+
|
|
236
|
+
def __init__(self, project_dir: str) -> None:
|
|
237
|
+
db_dir = os.path.join(project_dir, ".attocode", "cache")
|
|
238
|
+
os.makedirs(db_dir, exist_ok=True)
|
|
239
|
+
self.db_path = os.path.join(db_dir, "pins.db")
|
|
240
|
+
self._conn = sqlite3.connect(self.db_path, check_same_thread=False)
|
|
241
|
+
self._conn.execute("PRAGMA journal_mode=WAL")
|
|
242
|
+
self._create_tables()
|
|
243
|
+
|
|
244
|
+
def _create_tables(self) -> None:
|
|
245
|
+
self._conn.executescript(
|
|
246
|
+
"""
|
|
247
|
+
CREATE TABLE IF NOT EXISTS pins (
|
|
248
|
+
pin_id TEXT PRIMARY KEY,
|
|
249
|
+
created_at REAL NOT NULL,
|
|
250
|
+
expires_at REAL NOT NULL DEFAULT 0,
|
|
251
|
+
overlay_id TEXT,
|
|
252
|
+
branch_id TEXT,
|
|
253
|
+
manifest_hash TEXT NOT NULL,
|
|
254
|
+
manifest_json TEXT NOT NULL
|
|
255
|
+
);
|
|
256
|
+
CREATE INDEX IF NOT EXISTS ix_pins_expires ON pins(expires_at);
|
|
257
|
+
"""
|
|
258
|
+
)
|
|
259
|
+
self._conn.commit()
|
|
260
|
+
|
|
261
|
+
def save(self, pin: RetrievalPin) -> None:
|
|
262
|
+
import json
|
|
263
|
+
self._conn.execute(
|
|
264
|
+
"""INSERT OR REPLACE INTO pins
|
|
265
|
+
(pin_id, created_at, expires_at, overlay_id, branch_id,
|
|
266
|
+
manifest_hash, manifest_json)
|
|
267
|
+
VALUES (?, ?, ?, ?, ?, ?, ?)""",
|
|
268
|
+
(
|
|
269
|
+
pin.pin_id, pin.created_at, pin.expires_at,
|
|
270
|
+
pin.overlay_id, pin.branch_id, pin.manifest_hash,
|
|
271
|
+
json.dumps(pin.manifest_hashes, sort_keys=True),
|
|
272
|
+
),
|
|
273
|
+
)
|
|
274
|
+
self._conn.commit()
|
|
275
|
+
|
|
276
|
+
def get(self, pin_id: str) -> RetrievalPin | None:
|
|
277
|
+
import json
|
|
278
|
+
row = self._conn.execute(
|
|
279
|
+
"""SELECT pin_id, created_at, expires_at, overlay_id, branch_id,
|
|
280
|
+
manifest_hash, manifest_json FROM pins WHERE pin_id = ?""",
|
|
281
|
+
(pin_id,),
|
|
282
|
+
).fetchone()
|
|
283
|
+
if row is None:
|
|
284
|
+
return None
|
|
285
|
+
return RetrievalPin(
|
|
286
|
+
pin_id=row[0],
|
|
287
|
+
schema_version=1,
|
|
288
|
+
manifest_hashes=json.loads(row[6]),
|
|
289
|
+
manifest_hash=row[5],
|
|
290
|
+
overlay_id=row[3],
|
|
291
|
+
branch_id=row[4],
|
|
292
|
+
created_at=row[1],
|
|
293
|
+
expires_at=row[2],
|
|
294
|
+
)
|
|
295
|
+
|
|
296
|
+
def list_all(self) -> list[RetrievalPin]:
|
|
297
|
+
import json
|
|
298
|
+
rows = self._conn.execute(
|
|
299
|
+
"""SELECT pin_id, created_at, expires_at, overlay_id, branch_id,
|
|
300
|
+
manifest_hash, manifest_json FROM pins
|
|
301
|
+
ORDER BY created_at DESC"""
|
|
302
|
+
).fetchall()
|
|
303
|
+
return [
|
|
304
|
+
RetrievalPin(
|
|
305
|
+
pin_id=r[0],
|
|
306
|
+
schema_version=1,
|
|
307
|
+
manifest_hashes=json.loads(r[6]),
|
|
308
|
+
manifest_hash=r[5],
|
|
309
|
+
overlay_id=r[3],
|
|
310
|
+
branch_id=r[4],
|
|
311
|
+
created_at=r[1],
|
|
312
|
+
expires_at=r[2],
|
|
313
|
+
)
|
|
314
|
+
for r in rows
|
|
315
|
+
]
|
|
316
|
+
|
|
317
|
+
def delete(self, pin_id: str) -> bool:
|
|
318
|
+
cur = self._conn.execute("DELETE FROM pins WHERE pin_id = ?", (pin_id,))
|
|
319
|
+
self._conn.commit()
|
|
320
|
+
return cur.rowcount > 0
|
|
321
|
+
|
|
322
|
+
def gc_expired(self) -> int:
|
|
323
|
+
import time
|
|
324
|
+
cur = self._conn.execute(
|
|
325
|
+
"DELETE FROM pins WHERE expires_at > 0 AND expires_at < ?",
|
|
326
|
+
(time.time(),),
|
|
327
|
+
)
|
|
328
|
+
self._conn.commit()
|
|
329
|
+
return cur.rowcount
|
|
330
|
+
|
|
331
|
+
|
|
332
|
+
# ---------------------------------------------------------------------------
|
|
333
|
+
# Per-project PinStore cache
|
|
334
|
+
# ---------------------------------------------------------------------------
|
|
335
|
+
#
|
|
336
|
+
# Dict-keyed by absolute project_dir so a single interpreter serving
|
|
337
|
+
# multiple projects (e.g. an HTTP API with ``register_project``)
|
|
338
|
+
# maintains one PinStore per repo — not a "current project" singleton
|
|
339
|
+
# that silently points at whatever the env var happens to say.
|
|
340
|
+
#
|
|
341
|
+
# ``_pin_store`` / ``_pin_store_project`` remain as legacy aliases so
|
|
342
|
+
# existing test fixtures that reset them see a usable (empty) state,
|
|
343
|
+
# but the primary cache is ``_pin_stores``.
|
|
344
|
+
|
|
345
|
+
_pin_stores: dict[str, PinStore] = {}
|
|
346
|
+
|
|
347
|
+
_pin_store: PinStore | None = None
|
|
348
|
+
_pin_store_project: str = ""
|
|
349
|
+
|
|
350
|
+
|
|
351
|
+
def _get_pin_store(project_dir: str | None = None) -> PinStore:
|
|
352
|
+
"""Return the PinStore for a project, caching per project_dir.
|
|
353
|
+
|
|
354
|
+
``project_dir`` is optional: if omitted, falls back to the active
|
|
355
|
+
project via ``_get_project_dir()``. Pass the bound service's
|
|
356
|
+
``project_dir`` explicitly from HTTP providers so an API serving
|
|
357
|
+
multiple projects doesn't route pin writes to the wrong repo's
|
|
358
|
+
``.attocode/cache/pins.db``.
|
|
359
|
+
|
|
360
|
+
Legacy behavior: if the caller sets ``_pin_store = None`` via
|
|
361
|
+
monkeypatch, a fresh store is created for the current project.
|
|
362
|
+
"""
|
|
363
|
+
from attocode_intel.request_context import current_request, scoped_store
|
|
364
|
+
request = current_request.get()
|
|
365
|
+
if request is not None and (project_dir is None or os.path.realpath(project_dir) == request.project_dir):
|
|
366
|
+
return scoped_store("pins", PinStore)
|
|
367
|
+
global _pin_store, _pin_store_project
|
|
368
|
+
if project_dir is None:
|
|
369
|
+
project_dir = _get_project_dir()
|
|
370
|
+
current = os.path.abspath(project_dir)
|
|
371
|
+
|
|
372
|
+
# Legacy compat: if a test reset ``_pin_store``, honor that by
|
|
373
|
+
# re-creating on next access. Codex round-5 P3 fix: also pop the
|
|
374
|
+
# per-project dict entry — otherwise the cache returns the stale
|
|
375
|
+
# PinStore instance and the legacy reset is silently a no-op.
|
|
376
|
+
if _pin_store is None and current == _pin_store_project:
|
|
377
|
+
_pin_stores.pop(current, None)
|
|
378
|
+
_pin_store_project = ""
|
|
379
|
+
|
|
380
|
+
cached = _pin_stores.get(current)
|
|
381
|
+
if cached is None:
|
|
382
|
+
cached = PinStore(current)
|
|
383
|
+
_pin_stores[current] = cached
|
|
384
|
+
# Keep the legacy singleton in sync with whatever the most
|
|
385
|
+
# recent call was so ``_pin_store`` / ``_pin_store_project``
|
|
386
|
+
# attribute reads still reflect something sensible.
|
|
387
|
+
_pin_store = cached
|
|
388
|
+
_pin_store_project = current
|
|
389
|
+
return cached
|
|
390
|
+
|
|
391
|
+
|
|
392
|
+
def compute_and_persist_pin(
|
|
393
|
+
project_dir: str | None = None, *, ttl_seconds: int = 0,
|
|
394
|
+
) -> RetrievalPin:
|
|
395
|
+
"""MCP-free helper used by HTTP providers.
|
|
396
|
+
|
|
397
|
+
Computes the per-store manifest hashes for ``project_dir``,
|
|
398
|
+
derives a deterministic pin id (``pin_<hex20>`` of the manifest
|
|
399
|
+
hash), persists it via :class:`PinStore`, and returns the stored
|
|
400
|
+
:class:`RetrievalPin`. Round-trippable with ``pin_resolve`` and
|
|
401
|
+
``verify_pin`` on the stdio side because both read from the same
|
|
402
|
+
``.attocode/cache/pins.db``.
|
|
403
|
+
|
|
404
|
+
``project_dir`` is optional; if omitted it falls back to
|
|
405
|
+
auto-discovery via ``_get_project_dir()``. Callers with a bound
|
|
406
|
+
``CodeIntelService`` (e.g. ``LocalSearchProvider``) MUST pass
|
|
407
|
+
``svc.project_dir`` to avoid writing pins for project A into
|
|
408
|
+
project B's ``pins.db`` when one interpreter serves both.
|
|
409
|
+
|
|
410
|
+
Failures in hashing or persistence surface as exceptions so callers
|
|
411
|
+
can decide whether to swallow or log. This function MUST NOT catch
|
|
412
|
+
``SystemExit`` or ``BaseException`` — those indicate the process
|
|
413
|
+
should exit.
|
|
414
|
+
"""
|
|
415
|
+
if project_dir is None:
|
|
416
|
+
project_dir = _get_project_dir()
|
|
417
|
+
abs_dir = os.path.abspath(project_dir)
|
|
418
|
+
hashes = _compute_current_manifest_hashes(abs_dir)
|
|
419
|
+
raw = RetrievalPin.create(manifest_hashes=hashes, ttl_seconds=ttl_seconds)
|
|
420
|
+
deterministic_id = f"pin_{raw.manifest_hash[:20]}"
|
|
421
|
+
pin = RetrievalPin(
|
|
422
|
+
pin_id=deterministic_id,
|
|
423
|
+
schema_version=raw.schema_version,
|
|
424
|
+
manifest_hashes=raw.manifest_hashes,
|
|
425
|
+
manifest_hash=raw.manifest_hash,
|
|
426
|
+
overlay_id=raw.overlay_id,
|
|
427
|
+
branch_id=raw.branch_id,
|
|
428
|
+
created_at=raw.created_at,
|
|
429
|
+
expires_at=raw.expires_at,
|
|
430
|
+
)
|
|
431
|
+
_get_pin_store(abs_dir).save(pin)
|
|
432
|
+
return pin
|
|
@@ -0,0 +1,266 @@
|
|
|
1
|
+
"""Retrieval pin MCP tools — deterministic RAG for code-intel queries.
|
|
2
|
+
|
|
3
|
+
Every ranked-result tool (``semantic_search``, ``fast_search``, ``repo_map``,
|
|
4
|
+
etc.) is retrofitted to emit an ``index_pin`` footer on its response. A
|
|
5
|
+
caller can then:
|
|
6
|
+
|
|
7
|
+
- ``pin_current`` : mint a fresh pin of the current index state.
|
|
8
|
+
- ``pin_resolve`` : look up an existing pin's manifest.
|
|
9
|
+
- ``pin_list`` : list all active pins.
|
|
10
|
+
- ``pin_delete`` : drop a pin.
|
|
11
|
+
- ``verify_pin`` : compare a pin's recorded state to the current
|
|
12
|
+
state and return a drift report.
|
|
13
|
+
|
|
14
|
+
Phase 1 delivers the pin primitive + verification flow. Phase 2 layers on
|
|
15
|
+
``retrieve_with_pin`` (re-run a tool against pinned state) once the stacked
|
|
16
|
+
overlay machinery lands.
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
from __future__ import annotations
|
|
20
|
+
|
|
21
|
+
import logging
|
|
22
|
+
import sqlite3
|
|
23
|
+
|
|
24
|
+
from attocode_intel._shared import mcp
|
|
25
|
+
from attocode_intel.artifacts import RetrievalPin
|
|
26
|
+
|
|
27
|
+
# Pure helpers live in ``pin_store`` so they can be imported without
|
|
28
|
+
# dragging the MCP runtime in — critical for the HTTP API providers
|
|
29
|
+
# (see Codex P2 finding on ``local_provider.py`` round 3). This module
|
|
30
|
+
# layers the MCP ``@mcp.tool()`` decorators and the ``_stamp_pin``
|
|
31
|
+
# footer helper on top of those pure helpers.
|
|
32
|
+
from attocode_intel.tools.pin_store import ( # noqa: F401
|
|
33
|
+
_STORE_DEFS,
|
|
34
|
+
PinStore,
|
|
35
|
+
_compute_current_manifest_hashes,
|
|
36
|
+
_get_pin_store,
|
|
37
|
+
_get_project_dir,
|
|
38
|
+
_hash_for_store,
|
|
39
|
+
_hash_for_trigrams,
|
|
40
|
+
compute_and_persist_pin,
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
logger = logging.getLogger(__name__)
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
# ---------------------------------------------------------------------------
|
|
47
|
+
# _stamp_pin helper — used by all ranked-result tools
|
|
48
|
+
# ---------------------------------------------------------------------------
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def pin_stamped(fn): # type: ignore[no-untyped-def]
|
|
52
|
+
"""Decorator: append an ``index_pin`` footer to a tool's string return.
|
|
53
|
+
|
|
54
|
+
Apply *below* ``@mcp.tool()`` so the registered function is the
|
|
55
|
+
wrapped one::
|
|
56
|
+
|
|
57
|
+
@mcp.tool()
|
|
58
|
+
@pin_stamped
|
|
59
|
+
def semantic_search(...) -> str:
|
|
60
|
+
...
|
|
61
|
+
|
|
62
|
+
Non-string returns pass through unchanged — defensive against tools
|
|
63
|
+
that might return structured data in the future.
|
|
64
|
+
"""
|
|
65
|
+
import functools
|
|
66
|
+
|
|
67
|
+
@functools.wraps(fn)
|
|
68
|
+
def wrapper(*args, **kwargs): # type: ignore[no-untyped-def]
|
|
69
|
+
result = fn(*args, **kwargs)
|
|
70
|
+
if isinstance(result, str):
|
|
71
|
+
return _stamp_pin(result)
|
|
72
|
+
return result
|
|
73
|
+
|
|
74
|
+
return wrapper
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def _stamp_pin(result_text: str, *, persist: bool = True, ttl_seconds: int = 0) -> str:
|
|
78
|
+
"""Append an ``index_pin`` footer to a tool response.
|
|
79
|
+
|
|
80
|
+
Computes the current per-store manifest hashes, derives a
|
|
81
|
+
content-addressed deterministic pin id (``pin_<hex20>`` of the
|
|
82
|
+
manifest hash), persists it via the :class:`PinStore`, and appends
|
|
83
|
+
the full 64-char manifest hash to the response footer so callers
|
|
84
|
+
can later look the pin up via ``verify_pin`` or ``pin_resolve``.
|
|
85
|
+
|
|
86
|
+
Pin persistence is the default and the ``manifest_hash`` printed in
|
|
87
|
+
the footer is the complete hex string (verifiable via ``verify_pin``
|
|
88
|
+
/ ``pin_resolve``).
|
|
89
|
+
|
|
90
|
+
project_dir is resolved via ``_get_project_dir()`` in this module's
|
|
91
|
+
namespace and passed *explicitly* into the pin_store helpers, so
|
|
92
|
+
monkeypatching ``pin_tools._get_project_dir`` flows through to both
|
|
93
|
+
the manifest-hash computation and the PinStore path.
|
|
94
|
+
|
|
95
|
+
Idempotent: calling twice with no intervening writes produces the
|
|
96
|
+
same deterministic id, which collapses to a single upserted row in
|
|
97
|
+
the pin store.
|
|
98
|
+
"""
|
|
99
|
+
project_dir = _get_project_dir()
|
|
100
|
+
try:
|
|
101
|
+
hashes = _compute_current_manifest_hashes(project_dir)
|
|
102
|
+
except Exception as exc:
|
|
103
|
+
logger.debug("pin: hash computation failed: %s", exc)
|
|
104
|
+
return result_text
|
|
105
|
+
|
|
106
|
+
pin = RetrievalPin.create(manifest_hashes=hashes, ttl_seconds=ttl_seconds)
|
|
107
|
+
# Use a deterministic id derived from the manifest_hash so repeat calls
|
|
108
|
+
# on an unchanged state yield the same pin_id. The prefix length (20
|
|
109
|
+
# hex chars = 80 bits) is collision-resistant for single-project use
|
|
110
|
+
# while staying short enough to paste into an MCP prompt.
|
|
111
|
+
deterministic_id = f"pin_{pin.manifest_hash[:20]}"
|
|
112
|
+
|
|
113
|
+
if persist:
|
|
114
|
+
pin_to_save = RetrievalPin(
|
|
115
|
+
pin_id=deterministic_id,
|
|
116
|
+
schema_version=pin.schema_version,
|
|
117
|
+
manifest_hashes=pin.manifest_hashes,
|
|
118
|
+
manifest_hash=pin.manifest_hash,
|
|
119
|
+
overlay_id=pin.overlay_id,
|
|
120
|
+
branch_id=pin.branch_id,
|
|
121
|
+
created_at=pin.created_at,
|
|
122
|
+
expires_at=pin.expires_at,
|
|
123
|
+
)
|
|
124
|
+
try:
|
|
125
|
+
_get_pin_store(project_dir).save(pin_to_save)
|
|
126
|
+
except sqlite3.Error as exc:
|
|
127
|
+
logger.debug("pin: persist failed: %s", exc)
|
|
128
|
+
|
|
129
|
+
# Codex M3: no more truncation. The footer includes the full
|
|
130
|
+
# ``manifest_hash`` so downstream tools can look up or hand-verify
|
|
131
|
+
# the pin without round-tripping through pin_resolve.
|
|
132
|
+
footer = (
|
|
133
|
+
f"\n\n---\nindex_pin: {deterministic_id}\n"
|
|
134
|
+
f"manifest_hash: {pin.manifest_hash}"
|
|
135
|
+
)
|
|
136
|
+
if result_text.endswith("\n"):
|
|
137
|
+
return result_text + footer.lstrip("\n")
|
|
138
|
+
return result_text + footer
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
# ---------------------------------------------------------------------------
|
|
142
|
+
# MCP tools
|
|
143
|
+
# ---------------------------------------------------------------------------
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
@mcp.tool()
|
|
147
|
+
def pin_current(ttl_seconds: int = 86400) -> str:
|
|
148
|
+
"""Mint a retrieval pin capturing the current code-intel state.
|
|
149
|
+
|
|
150
|
+
A pin is a content-addressed snapshot of every local store's manifest
|
|
151
|
+
hash (symbols, embeddings, learnings, ADRs, trigrams, etc.). Later you
|
|
152
|
+
can call ``verify_pin(pin_id)`` to check whether the state has drifted
|
|
153
|
+
since the pin was minted.
|
|
154
|
+
|
|
155
|
+
Args:
|
|
156
|
+
ttl_seconds: Pin expires after this many seconds. 0 = never expires.
|
|
157
|
+
Default 86400 (24h).
|
|
158
|
+
|
|
159
|
+
Returns:
|
|
160
|
+
The pin_id + a summary of hashed stores.
|
|
161
|
+
|
|
162
|
+
The ``pin_id`` is derived deterministically from the manifest hash
|
|
163
|
+
(``pin_<hex20>``), matching ``_stamp_pin`` and HTTP search-response
|
|
164
|
+
pins. Two calls on unchanged state collapse to the same id, so the
|
|
165
|
+
PinStore upsert never accumulates duplicates.
|
|
166
|
+
"""
|
|
167
|
+
project_dir = _get_project_dir()
|
|
168
|
+
hashes = _compute_current_manifest_hashes(project_dir)
|
|
169
|
+
raw = RetrievalPin.create(manifest_hashes=hashes, ttl_seconds=ttl_seconds)
|
|
170
|
+
deterministic_id = f"pin_{raw.manifest_hash[:20]}"
|
|
171
|
+
pin = RetrievalPin(
|
|
172
|
+
pin_id=deterministic_id,
|
|
173
|
+
schema_version=raw.schema_version,
|
|
174
|
+
manifest_hashes=raw.manifest_hashes,
|
|
175
|
+
manifest_hash=raw.manifest_hash,
|
|
176
|
+
overlay_id=raw.overlay_id,
|
|
177
|
+
branch_id=raw.branch_id,
|
|
178
|
+
created_at=raw.created_at,
|
|
179
|
+
expires_at=raw.expires_at,
|
|
180
|
+
)
|
|
181
|
+
_get_pin_store(project_dir).save(pin)
|
|
182
|
+
lines = [f"Pinned: {pin.pin_id}", f"manifest_hash: {pin.manifest_hash}"]
|
|
183
|
+
if pin.expires_at > 0:
|
|
184
|
+
import time
|
|
185
|
+
remaining = int(pin.expires_at - time.time())
|
|
186
|
+
lines.append(f"expires_in: {remaining}s")
|
|
187
|
+
else:
|
|
188
|
+
lines.append("expires_in: never")
|
|
189
|
+
lines.append("stores:")
|
|
190
|
+
for name, h in sorted(hashes.items()):
|
|
191
|
+
short = h[:16] + "…" if len(h) > 20 else h
|
|
192
|
+
lines.append(f" - {name}: {short}")
|
|
193
|
+
return "\n".join(lines)
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
@mcp.tool()
|
|
197
|
+
def pin_resolve(pin_id: str) -> str:
|
|
198
|
+
"""Look up a previously-minted retrieval pin by id."""
|
|
199
|
+
pin = _get_pin_store(_get_project_dir()).get(pin_id)
|
|
200
|
+
if pin is None:
|
|
201
|
+
return f"No pin with id {pin_id!r}"
|
|
202
|
+
lines = [
|
|
203
|
+
f"pin_id: {pin.pin_id}",
|
|
204
|
+
f"manifest_hash: {pin.manifest_hash}",
|
|
205
|
+
f"created_at: {pin.created_at}",
|
|
206
|
+
f"expires_at: {pin.expires_at if pin.expires_at > 0 else 'never'}",
|
|
207
|
+
f"expired: {pin.is_expired()}",
|
|
208
|
+
"stores:",
|
|
209
|
+
]
|
|
210
|
+
for name, h in sorted(pin.manifest_hashes.items()):
|
|
211
|
+
lines.append(f" - {name}: {h}")
|
|
212
|
+
return "\n".join(lines)
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
@mcp.tool()
|
|
216
|
+
def pin_list() -> str:
|
|
217
|
+
"""List all retrieval pins, most recent first."""
|
|
218
|
+
store = _get_pin_store(_get_project_dir())
|
|
219
|
+
store.gc_expired()
|
|
220
|
+
pins = store.list_all()
|
|
221
|
+
if not pins:
|
|
222
|
+
return "No pins."
|
|
223
|
+
lines = [f"{len(pins)} pin(s):"]
|
|
224
|
+
for pin in pins:
|
|
225
|
+
status = "expired" if pin.is_expired() else "active"
|
|
226
|
+
lines.append(
|
|
227
|
+
f" - {pin.pin_id} ({status}) manifest={pin.manifest_hash[:12]}…"
|
|
228
|
+
)
|
|
229
|
+
return "\n".join(lines)
|
|
230
|
+
|
|
231
|
+
|
|
232
|
+
@mcp.tool()
|
|
233
|
+
def pin_delete(pin_id: str) -> str:
|
|
234
|
+
"""Delete a retrieval pin by id."""
|
|
235
|
+
deleted = _get_pin_store(_get_project_dir()).delete(pin_id)
|
|
236
|
+
return f"Deleted pin {pin_id}" if deleted else f"No pin with id {pin_id!r}"
|
|
237
|
+
|
|
238
|
+
|
|
239
|
+
@mcp.tool()
|
|
240
|
+
def verify_pin(pin_id: str) -> str:
|
|
241
|
+
"""Check whether the code-intel state has drifted since a pin was minted.
|
|
242
|
+
|
|
243
|
+
Returns a drift report: for each store, whether its hash matches the
|
|
244
|
+
pinned value. If there is no drift, a query re-run against the current
|
|
245
|
+
state is guaranteed to produce the same ranked results as when the pin
|
|
246
|
+
was minted (assuming the tool is deterministic for a fixed index state —
|
|
247
|
+
which is the whole point of these pins).
|
|
248
|
+
"""
|
|
249
|
+
project_dir = _get_project_dir()
|
|
250
|
+
pin = _get_pin_store(project_dir).get(pin_id)
|
|
251
|
+
if pin is None:
|
|
252
|
+
return f"No pin with id {pin_id!r}"
|
|
253
|
+
if pin.is_expired():
|
|
254
|
+
return f"Pin {pin_id} has expired."
|
|
255
|
+
|
|
256
|
+
current = _compute_current_manifest_hashes(project_dir)
|
|
257
|
+
drift = pin.drift_from(current)
|
|
258
|
+
if not drift:
|
|
259
|
+
return f"Pin {pin_id}: no drift. State is identical to the pinned snapshot."
|
|
260
|
+
|
|
261
|
+
lines = [f"Pin {pin_id}: DRIFT in {len(drift)} store(s):"]
|
|
262
|
+
for name, (pinned_hash, current_hash) in sorted(drift.items()):
|
|
263
|
+
lines.append(
|
|
264
|
+
f" - {name}: pinned={pinned_hash[:16]}… current={current_hash[:16]}…"
|
|
265
|
+
)
|
|
266
|
+
return "\n".join(lines)
|