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.
Files changed (345) hide show
  1. attocode_code_intel-0.1.0.dist-info/METADATA +221 -0
  2. attocode_code_intel-0.1.0.dist-info/RECORD +345 -0
  3. attocode_code_intel-0.1.0.dist-info/WHEEL +4 -0
  4. attocode_code_intel-0.1.0.dist-info/entry_points.txt +2 -0
  5. attocode_code_intel-0.1.0.dist-info/licenses/LICENSE +21 -0
  6. attocode_intel/GUIDELINES.md +432 -0
  7. attocode_intel/__init__.py +52 -0
  8. attocode_intel/_internal/__init__.py +1 -0
  9. attocode_intel/_internal/errors/__init__.py +126 -0
  10. attocode_intel/_internal/integrations/__init__.py +1 -0
  11. attocode_intel/_internal/integrations/context/__init__.py +1 -0
  12. attocode_intel/_internal/integrations/context/architecture_drift.py +354 -0
  13. attocode_intel/_internal/integrations/context/ast_chunker.py +202 -0
  14. attocode_intel/_internal/integrations/context/ast_service.py +1483 -0
  15. attocode_intel/_internal/integrations/context/blob_oid.py +423 -0
  16. attocode_intel/_internal/integrations/context/cache_manifest.py +221 -0
  17. attocode_intel/_internal/integrations/context/cas.py +466 -0
  18. attocode_intel/_internal/integrations/context/code_analyzer.py +528 -0
  19. attocode_intel/_internal/integrations/context/codebase_ast.py +1461 -0
  20. attocode_intel/_internal/integrations/context/codebase_context.py +1736 -0
  21. attocode_intel/_internal/integrations/context/cross_mode.py +263 -0
  22. attocode_intel/_internal/integrations/context/cross_references.py +616 -0
  23. attocode_intel/_internal/integrations/context/embedding_rotation.py +597 -0
  24. attocode_intel/_internal/integrations/context/embeddings.py +282 -0
  25. attocode_intel/_internal/integrations/context/frecency.py +427 -0
  26. attocode_intel/_internal/integrations/context/fuzzy.py +320 -0
  27. attocode_intel/_internal/integrations/context/hierarchical_explorer.py +331 -0
  28. attocode_intel/_internal/integrations/context/hydration.py +90 -0
  29. attocode_intel/_internal/integrations/context/index_repair.py +87 -0
  30. attocode_intel/_internal/integrations/context/index_store.py +586 -0
  31. attocode_intel/_internal/integrations/context/memory_store.py +602 -0
  32. attocode_intel/_internal/integrations/context/module_resolution.py +52 -0
  33. attocode_intel/_internal/integrations/context/query_constraints.py +361 -0
  34. attocode_intel/_internal/integrations/context/query_history.py +417 -0
  35. attocode_intel/_internal/integrations/context/reranker.py +351 -0
  36. attocode_intel/_internal/integrations/context/semantic_search.py +2931 -0
  37. attocode_intel/_internal/integrations/context/syntax_evidence.py +94 -0
  38. attocode_intel/_internal/integrations/context/systemone_ranker.py +245 -0
  39. attocode_intel/_internal/integrations/context/temporal_coupling.py +411 -0
  40. attocode_intel/_internal/integrations/context/trigram_index.py +820 -0
  41. attocode_intel/_internal/integrations/context/trigram_regex.py +171 -0
  42. attocode_intel/_internal/integrations/context/ts_parser.py +2199 -0
  43. attocode_intel/_internal/integrations/context/vector_store.py +795 -0
  44. attocode_intel/_internal/integrations/feature_flags/__init__.py +357 -0
  45. attocode_intel/_internal/integrations/lsp/__init__.py +1 -0
  46. attocode_intel/_internal/integrations/lsp/client.py +1140 -0
  47. attocode_intel/_internal/integrations/mcp/__init__.py +1 -0
  48. attocode_intel/_internal/integrations/mcp/client.py +253 -0
  49. attocode_intel/_internal/integrations/security/__init__.py +1 -0
  50. attocode_intel/_internal/integrations/security/custom_rules.py +176 -0
  51. attocode_intel/_internal/integrations/security/dataflow.py +522 -0
  52. attocode_intel/_internal/integrations/security/dependency_audit.py +303 -0
  53. attocode_intel/_internal/integrations/security/matcher.py +44 -0
  54. attocode_intel/_internal/integrations/security/patterns.py +724 -0
  55. attocode_intel/_internal/integrations/security/scanner.py +351 -0
  56. attocode_intel/_internal/integrations/utilities/__init__.py +1 -0
  57. attocode_intel/_internal/integrations/utilities/ignore.py +64 -0
  58. attocode_intel/_internal/integrations/utilities/token_estimate.py +51 -0
  59. attocode_intel/_internal/tricks/__init__.py +1 -0
  60. attocode_intel/_internal/tricks/recursive_context.py +357 -0
  61. attocode_intel/_internal/types/__init__.py +1 -0
  62. attocode_intel/_internal/types/events.py +289 -0
  63. attocode_intel/_internal/types/severity.py +15 -0
  64. attocode_intel/_shared.py +341 -0
  65. attocode_intel/analysis_coverage.py +56 -0
  66. attocode_intel/api/__init__.py +1 -0
  67. attocode_intel/api/access.py +32 -0
  68. attocode_intel/api/app.py +247 -0
  69. attocode_intel/api/auth/__init__.py +128 -0
  70. attocode_intel/api/auth/api_keys.py +61 -0
  71. attocode_intel/api/auth/context.py +34 -0
  72. attocode_intel/api/auth/jwt.py +66 -0
  73. attocode_intel/api/auth/oauth.py +146 -0
  74. attocode_intel/api/auth/passwords.py +15 -0
  75. attocode_intel/api/deps.py +351 -0
  76. attocode_intel/api/knowledge_bridge.py +13 -0
  77. attocode_intel/api/mcp_transport.py +51 -0
  78. attocode_intel/api/middleware.py +411 -0
  79. attocode_intel/api/models.py +698 -0
  80. attocode_intel/api/providers/__init__.py +1 -0
  81. attocode_intel/api/providers/db_provider.py +1106 -0
  82. attocode_intel/api/providers/local_provider.py +476 -0
  83. attocode_intel/api/providers/remote_provider.py +740 -0
  84. attocode_intel/api/repository_source.py +21 -0
  85. attocode_intel/api/routes/__init__.py +1 -0
  86. attocode_intel/api/routes/_pin_helper.py +158 -0
  87. attocode_intel/api/routes/activity.py +148 -0
  88. attocode_intel/api/routes/adr.py +102 -0
  89. attocode_intel/api/routes/analysis.py +515 -0
  90. attocode_intel/api/routes/api_keys.py +170 -0
  91. attocode_intel/api/routes/auth.py +428 -0
  92. attocode_intel/api/routes/branches.py +262 -0
  93. attocode_intel/api/routes/cross_repo_search.py +198 -0
  94. attocode_intel/api/routes/embeddings.py +386 -0
  95. attocode_intel/api/routes/files.py +132 -0
  96. attocode_intel/api/routes/files_v2.py +388 -0
  97. attocode_intel/api/routes/gc.py +322 -0
  98. attocode_intel/api/routes/git_v2.py +433 -0
  99. attocode_intel/api/routes/graph.py +174 -0
  100. attocode_intel/api/routes/graph_viz.py +160 -0
  101. attocode_intel/api/routes/health.py +62 -0
  102. attocode_intel/api/routes/history.py +175 -0
  103. attocode_intel/api/routes/jobs.py +84 -0
  104. attocode_intel/api/routes/learning.py +205 -0
  105. attocode_intel/api/routes/lsp.py +272 -0
  106. attocode_intel/api/routes/notify.py +623 -0
  107. attocode_intel/api/routes/operations.py +27 -0
  108. attocode_intel/api/routes/orgs.py +924 -0
  109. attocode_intel/api/routes/preferences.py +100 -0
  110. attocode_intel/api/routes/presence.py +42 -0
  111. attocode_intel/api/routes/projects.py +116 -0
  112. attocode_intel/api/routes/repos.py +47 -0
  113. attocode_intel/api/routes/search.py +180 -0
  114. attocode_intel/api/routes/snapshots.py +614 -0
  115. attocode_intel/api/routes/webhooks.py +313 -0
  116. attocode_intel/api/routes/websocket.py +116 -0
  117. attocode_intel/api/static/graph.html +239 -0
  118. attocode_intel/api/utils.py +96 -0
  119. attocode_intel/artifacts/__init__.py +45 -0
  120. attocode_intel/artifacts/hashing.py +230 -0
  121. attocode_intel/artifacts/provenance.py +190 -0
  122. attocode_intel/artifacts/retrieval_pin.py +159 -0
  123. attocode_intel/audit.py +59 -0
  124. attocode_intel/bug_finder.py +206 -0
  125. attocode_intel/bundle.py +80 -0
  126. attocode_intel/catalog.py +242 -0
  127. attocode_intel/change_review.py +75 -0
  128. attocode_intel/cli.py +3062 -0
  129. attocode_intel/community.py +63 -0
  130. attocode_intel/confidence/__init__.py +144 -0
  131. attocode_intel/confidence/jev.py +234 -0
  132. attocode_intel/confidence/llm.py +330 -0
  133. attocode_intel/confidence/redact.py +44 -0
  134. attocode_intel/confidence/settings.py +74 -0
  135. attocode_intel/config.py +256 -0
  136. attocode_intel/crypto.py +31 -0
  137. attocode_intel/db/__init__.py +7 -0
  138. attocode_intel/db/base.py +33 -0
  139. attocode_intel/db/engine.py +61 -0
  140. attocode_intel/db/models.py +529 -0
  141. attocode_intel/entrypoint.py +185 -0
  142. attocode_intel/focused_evidence.py +130 -0
  143. attocode_intel/freshness.py +154 -0
  144. attocode_intel/gateway.py +734 -0
  145. attocode_intel/git/__init__.py +7 -0
  146. attocode_intel/git/credentials.py +75 -0
  147. attocode_intel/git/manager.py +493 -0
  148. attocode_intel/git/models.py +96 -0
  149. attocode_intel/git/storage.py +98 -0
  150. attocode_intel/graph_query_parser.py +761 -0
  151. attocode_intel/helpers.py +712 -0
  152. attocode_intel/indexing/__init__.py +29 -0
  153. attocode_intel/indexing/debouncer.py +136 -0
  154. attocode_intel/indexing/delta_indexer.py +151 -0
  155. attocode_intel/indexing/full_indexer.py +323 -0
  156. attocode_intel/indexing/incremental.py +240 -0
  157. attocode_intel/indexing/parser.py +403 -0
  158. attocode_intel/indexing/summarizer.py +264 -0
  159. attocode_intel/installer.py +1896 -0
  160. attocode_intel/knowledge.py +269 -0
  161. attocode_intel/local_knowledge.py +77 -0
  162. attocode_intel/migrations/alembic.ini +36 -0
  163. attocode_intel/migrations/env.py +71 -0
  164. attocode_intel/migrations/script.py.mako +27 -0
  165. attocode_intel/migrations/versions/001_initial_schema.py +219 -0
  166. attocode_intel/migrations/versions/002_branch_version_and_embeddings.py +61 -0
  167. attocode_intel/migrations/versions/003_embedding_uniqueness_and_cascade.py +51 -0
  168. attocode_intel/migrations/versions/004_audit_events.py +38 -0
  169. attocode_intel/migrations/versions/005_user_preferences.py +30 -0
  170. attocode_intel/migrations/versions/006_google_oauth.py +27 -0
  171. attocode_intel/migrations/versions/007_pgvector_embeddings.py +28 -0
  172. attocode_intel/migrations/versions/008_symbol_references.py +37 -0
  173. attocode_intel/migrations/versions/009_webhook_secret_encryption.py +25 -0
  174. attocode_intel/migrations/versions/010_add_commits_table.py +40 -0
  175. attocode_intel/migrations/versions/011_commit_changed_files.py +25 -0
  176. attocode_intel/migrations/versions/012_branch_merged_at.py +25 -0
  177. attocode_intel/migrations/versions/013_revoked_tokens.py +32 -0
  178. attocode_intel/migrations/versions/014_blame_hunks.py +40 -0
  179. attocode_intel/migrations/versions/015_commit_file_stats.py +40 -0
  180. attocode_intel/migrations/versions/016_embedding_provenance_columns.py +179 -0
  181. attocode_intel/migrations/versions/017_provenance_table.py +102 -0
  182. attocode_intel/migrations/versions/018_repo_snapshots.py +178 -0
  183. attocode_intel/migrations/versions/019_symref_caller.py +42 -0
  184. attocode_intel/migrations/versions/020_shared_knowledge.py +29 -0
  185. attocode_intel/onboarding.py +258 -0
  186. attocode_intel/output.py +178 -0
  187. attocode_intel/precision.py +133 -0
  188. attocode_intel/presence.py +160 -0
  189. attocode_intel/probe.py +110 -0
  190. attocode_intel/project_dir.py +97 -0
  191. attocode_intel/pubsub.py +113 -0
  192. attocode_intel/query_ranking.py +242 -0
  193. attocode_intel/readiness.py +972 -0
  194. attocode_intel/redis.py +51 -0
  195. attocode_intel/reference_pages.py +60 -0
  196. attocode_intel/remote.py +131 -0
  197. attocode_intel/repo_ranker.py +246 -0
  198. attocode_intel/request_context.py +82 -0
  199. attocode_intel/rules/__init__.py +28 -0
  200. attocode_intel/rules/ci.py +320 -0
  201. attocode_intel/rules/combinators.py +293 -0
  202. attocode_intel/rules/enricher.py +127 -0
  203. attocode_intel/rules/evolution.py +452 -0
  204. attocode_intel/rules/executor.py +531 -0
  205. attocode_intel/rules/filters/__init__.py +5 -0
  206. attocode_intel/rules/filters/pipeline.py +151 -0
  207. attocode_intel/rules/formatter.py +198 -0
  208. attocode_intel/rules/hygiene.py +242 -0
  209. attocode_intel/rules/importers/__init__.py +1 -0
  210. attocode_intel/rules/importers/semgrep.py +382 -0
  211. attocode_intel/rules/loader.py +292 -0
  212. attocode_intel/rules/marketplace.py +375 -0
  213. attocode_intel/rules/metavar.py +181 -0
  214. attocode_intel/rules/model.py +317 -0
  215. attocode_intel/rules/packs/__init__.py +1 -0
  216. attocode_intel/rules/packs/community/_license_templates/Apache-2.0.txt +176 -0
  217. attocode_intel/rules/packs/community/_license_templates/MIT.txt +21 -0
  218. attocode_intel/rules/packs/community/bandit-python/LICENSE +176 -0
  219. attocode_intel/rules/packs/community/bandit-python/NOTICE +13 -0
  220. attocode_intel/rules/packs/community/bandit-python/PORTING.md +23 -0
  221. attocode_intel/rules/packs/community/bandit-python/manifest.yaml +14 -0
  222. attocode_intel/rules/packs/community/bandit-python/rules/B105-hardcoded-password.yaml +23 -0
  223. attocode_intel/rules/packs/community/bandit-python/rules/B303-weak-md5.yaml +21 -0
  224. attocode_intel/rules/packs/community/bandit-python/rules/B304-weak-des.yaml +20 -0
  225. attocode_intel/rules/packs/community/bandit-python/rules/B307-use-of-eval.yaml +22 -0
  226. attocode_intel/rules/packs/community/bandit-python/rules/B324-weak-sha1.yaml +21 -0
  227. attocode_intel/rules/packs/community/bandit-python/rules/B501-no-cert-validation.yaml +23 -0
  228. attocode_intel/rules/packs/community/bandit-python/rules/B602-shell-true.yaml +22 -0
  229. attocode_intel/rules/packs/community/bandit-python/rules/B608-sqli.yaml +22 -0
  230. attocode_intel/rules/packs/community/eslint-typescript/LICENSE +21 -0
  231. attocode_intel/rules/packs/community/eslint-typescript/NOTICE +13 -0
  232. attocode_intel/rules/packs/community/eslint-typescript/PORTING.md +23 -0
  233. attocode_intel/rules/packs/community/eslint-typescript/manifest.yaml +14 -0
  234. attocode_intel/rules/packs/community/eslint-typescript/rules/eqeqeq.yaml +20 -0
  235. attocode_intel/rules/packs/community/eslint-typescript/rules/no-eval.yaml +22 -0
  236. attocode_intel/rules/packs/community/eslint-typescript/rules/no-implied-eval.yaml +20 -0
  237. attocode_intel/rules/packs/community/eslint-typescript/rules/no-new-func.yaml +21 -0
  238. attocode_intel/rules/packs/community/eslint-typescript/rules/no-throw-literal.yaml +19 -0
  239. attocode_intel/rules/packs/community/eslint-typescript/rules/no-var.yaml +20 -0
  240. attocode_intel/rules/packs/community/eslint-typescript/rules/no-with.yaml +18 -0
  241. attocode_intel/rules/packs/community/gosec-go/LICENSE +176 -0
  242. attocode_intel/rules/packs/community/gosec-go/NOTICE +13 -0
  243. attocode_intel/rules/packs/community/gosec-go/PORTING.md +23 -0
  244. attocode_intel/rules/packs/community/gosec-go/manifest.yaml +14 -0
  245. attocode_intel/rules/packs/community/gosec-go/rules/G101-hardcoded-credentials.yaml +22 -0
  246. attocode_intel/rules/packs/community/gosec-go/rules/G102-bind-all-interfaces.yaml +21 -0
  247. attocode_intel/rules/packs/community/gosec-go/rules/G103-unsafe-block.yaml +21 -0
  248. attocode_intel/rules/packs/community/gosec-go/rules/G201-sql-format.yaml +21 -0
  249. attocode_intel/rules/packs/community/gosec-go/rules/G304-file-inclusion.yaml +23 -0
  250. attocode_intel/rules/packs/community/gosec-go/rules/G401-weak-cipher.yaml +20 -0
  251. attocode_intel/rules/packs/community/gosec-go/rules/G402-tls-min-version.yaml +20 -0
  252. attocode_intel/rules/packs/community/gosec-go/rules/G501-md5-import.yaml +21 -0
  253. attocode_intel/rules/packs/examples/cpp/manifest.yaml +4 -0
  254. attocode_intel/rules/packs/examples/cpp/rules/security.yaml +90 -0
  255. attocode_intel/rules/packs/examples/cpp/rules/structural.yaml +42 -0
  256. attocode_intel/rules/packs/examples/csharp/manifest.yaml +4 -0
  257. attocode_intel/rules/packs/examples/csharp/rules/security.yaml +69 -0
  258. attocode_intel/rules/packs/examples/csharp/rules/structural.yaml +42 -0
  259. attocode_intel/rules/packs/examples/go/manifest.yaml +4 -0
  260. attocode_intel/rules/packs/examples/go/rules/performance.yaml +136 -0
  261. attocode_intel/rules/packs/examples/go/rules/security.yaml +48 -0
  262. attocode_intel/rules/packs/examples/go/rules/structural.yaml +58 -0
  263. attocode_intel/rules/packs/examples/go/taint/sanitizers.yaml +24 -0
  264. attocode_intel/rules/packs/examples/go/taint/sinks.yaml +51 -0
  265. attocode_intel/rules/packs/examples/go/taint/sources.yaml +26 -0
  266. attocode_intel/rules/packs/examples/java/manifest.yaml +4 -0
  267. attocode_intel/rules/packs/examples/java/rules/correctness.yaml +55 -0
  268. attocode_intel/rules/packs/examples/java/rules/structural.yaml +48 -0
  269. attocode_intel/rules/packs/examples/java/taint/sinks.yaml +37 -0
  270. attocode_intel/rules/packs/examples/java/taint/sources.yaml +21 -0
  271. attocode_intel/rules/packs/examples/kotlin/manifest.yaml +4 -0
  272. attocode_intel/rules/packs/examples/kotlin/rules/correctness.yaml +50 -0
  273. attocode_intel/rules/packs/examples/kotlin/rules/structural.yaml +38 -0
  274. attocode_intel/rules/packs/examples/php/manifest.yaml +4 -0
  275. attocode_intel/rules/packs/examples/php/rules/security.yaml +66 -0
  276. attocode_intel/rules/packs/examples/php/rules/structural.yaml +41 -0
  277. attocode_intel/rules/packs/examples/python/manifest.yaml +4 -0
  278. attocode_intel/rules/packs/examples/python/rules/performance.yaml +90 -0
  279. attocode_intel/rules/packs/examples/python/rules/structural.yaml +55 -0
  280. attocode_intel/rules/packs/examples/ruby/manifest.yaml +4 -0
  281. attocode_intel/rules/packs/examples/ruby/rules/security.yaml +73 -0
  282. attocode_intel/rules/packs/examples/ruby/rules/structural.yaml +41 -0
  283. attocode_intel/rules/packs/examples/rust/manifest.yaml +4 -0
  284. attocode_intel/rules/packs/examples/rust/rules/correctness.yaml +51 -0
  285. attocode_intel/rules/packs/examples/rust/rules/structural.yaml +49 -0
  286. attocode_intel/rules/packs/examples/typescript/manifest.yaml +4 -0
  287. attocode_intel/rules/packs/examples/typescript/rules/performance.yaml +70 -0
  288. attocode_intel/rules/packs/examples/typescript/rules/structural.yaml +48 -0
  289. attocode_intel/rules/packs/pack_loader.py +244 -0
  290. attocode_intel/rules/plugins/__init__.py +1 -0
  291. attocode_intel/rules/plugins/plugin_loader.py +132 -0
  292. attocode_intel/rules/profiling.py +332 -0
  293. attocode_intel/rules/registry.py +213 -0
  294. attocode_intel/rules/sarif.py +187 -0
  295. attocode_intel/rules/synthesis.py +420 -0
  296. attocode_intel/rules/testing.py +326 -0
  297. attocode_intel/server.py +734 -0
  298. attocode_intel/service.py +3416 -0
  299. attocode_intel/sharing.py +57 -0
  300. attocode_intel/storage/__init__.py +6 -0
  301. attocode_intel/storage/branch_overlay.py +352 -0
  302. attocode_intel/storage/content_store.py +220 -0
  303. attocode_intel/storage/dependency_store.py +143 -0
  304. attocode_intel/storage/diff_engine.py +187 -0
  305. attocode_intel/storage/embedding_store.py +594 -0
  306. attocode_intel/storage/provenance_store.py +137 -0
  307. attocode_intel/storage/security_scanner_db.py +164 -0
  308. attocode_intel/storage/symbol_store.py +160 -0
  309. attocode_intel/structural_evidence.py +144 -0
  310. attocode_intel/symbol_inspection.py +141 -0
  311. attocode_intel/symbol_links.py +167 -0
  312. attocode_intel/telemetry.py +42 -0
  313. attocode_intel/test_ranking.py +73 -0
  314. attocode_intel/testing/__init__.py +43 -0
  315. attocode_intel/testing/fixtures.py +122 -0
  316. attocode_intel/testing/helpers.py +75 -0
  317. attocode_intel/testing/mocks.py +99 -0
  318. attocode_intel/tools/__init__.py +2 -0
  319. attocode_intel/tools/adr_tools.py +628 -0
  320. attocode_intel/tools/analysis_tools.py +382 -0
  321. attocode_intel/tools/composite_tools.py +695 -0
  322. attocode_intel/tools/cross_mode_tools.py +173 -0
  323. attocode_intel/tools/dead_code_tools.py +582 -0
  324. attocode_intel/tools/distill_tools.py +353 -0
  325. attocode_intel/tools/frecency_tools.py +220 -0
  326. attocode_intel/tools/fuzzy_tools.py +255 -0
  327. attocode_intel/tools/history_tools.py +486 -0
  328. attocode_intel/tools/learning_tools.py +222 -0
  329. attocode_intel/tools/lsp_tools.py +218 -0
  330. attocode_intel/tools/maintenance_tools.py +1662 -0
  331. attocode_intel/tools/navigation_tools.py +220 -0
  332. attocode_intel/tools/overlay_tools.py +540 -0
  333. attocode_intel/tools/pin_store.py +432 -0
  334. attocode_intel/tools/pin_tools.py +266 -0
  335. attocode_intel/tools/query_constraints_tools.py +262 -0
  336. attocode_intel/tools/query_history_tools.py +158 -0
  337. attocode_intel/tools/readiness_tools.py +44 -0
  338. attocode_intel/tools/rule_tools.py +992 -0
  339. attocode_intel/tools/search_tools.py +770 -0
  340. attocode_intel/tools/snapshot_tools.py +624 -0
  341. attocode_intel/workers/__init__.py +1 -0
  342. attocode_intel/workers/job_utils.py +111 -0
  343. attocode_intel/workers/jobs.py +573 -0
  344. attocode_intel/workers/run.py +30 -0
  345. 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)