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,628 @@
1
+ """Architecture Decision Record (ADR) tools for the code-intel MCP server.
2
+
3
+ Tools: record_adr, list_adrs, get_adr, update_adr_status.
4
+
5
+ Local mode stores ADRs in a SQLite database at ``.attocode/adrs.db``,
6
+ following the same lazy-singleton pattern as ``learning_tools.py``.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ import json
12
+ import logging
13
+ import os
14
+ import sqlite3
15
+ import threading
16
+ import time
17
+ from dataclasses import dataclass, field
18
+
19
+ from attocode_intel._shared import (
20
+ _get_project_dir,
21
+ _get_remote_service,
22
+ mcp,
23
+ )
24
+
25
+ logger = logging.getLogger(__name__)
26
+
27
+ # ---------------------------------------------------------------------------
28
+ # Valid statuses and transitions
29
+ # ---------------------------------------------------------------------------
30
+
31
+ VALID_STATUSES = {"proposed", "accepted", "deprecated", "superseded"}
32
+
33
+ # Allowed status transitions: current -> set of valid next statuses
34
+ _STATUS_TRANSITIONS: dict[str, set[str]] = {
35
+ "proposed": {"accepted", "deprecated", "superseded"},
36
+ "accepted": {"deprecated", "superseded"},
37
+ "deprecated": {"superseded"},
38
+ "superseded": set(), # terminal
39
+ }
40
+
41
+ # ---------------------------------------------------------------------------
42
+ # ADRStore — SQLite-backed persistence
43
+ # ---------------------------------------------------------------------------
44
+
45
+
46
+ @dataclass(slots=True)
47
+ class ADRStore:
48
+ """SQLite-backed persistent store for Architecture Decision Records.
49
+
50
+ Stores ADRs with auto-incrementing numbers, status tracking,
51
+ JSON-encoded related_files and tags, and LIKE-based search.
52
+ """
53
+
54
+ project_dir: str
55
+ db_path: str = field(default="", repr=False)
56
+ _conn: sqlite3.Connection | None = field(default=None, repr=False)
57
+
58
+ def __post_init__(self) -> None:
59
+ if not self.db_path:
60
+ store_dir = os.path.join(self.project_dir, ".attocode")
61
+ os.makedirs(store_dir, exist_ok=True)
62
+ self.db_path = os.path.join(store_dir, "adrs.db")
63
+
64
+ self._conn = sqlite3.connect(self.db_path)
65
+ self._conn.execute("PRAGMA journal_mode=WAL")
66
+ self._conn.execute("PRAGMA synchronous=NORMAL")
67
+ self._create_tables()
68
+
69
+ def _get_conn(self) -> sqlite3.Connection:
70
+ """Return the active connection or raise if closed."""
71
+ if self._conn is None:
72
+ raise RuntimeError("ADRStore connection is closed")
73
+ return self._conn
74
+
75
+ def _create_tables(self) -> None:
76
+ """Create tables if they don't exist."""
77
+ conn = self._get_conn()
78
+ conn.executescript("""
79
+ CREATE TABLE IF NOT EXISTS adrs (
80
+ number INTEGER PRIMARY KEY AUTOINCREMENT,
81
+ title TEXT NOT NULL,
82
+ status TEXT NOT NULL DEFAULT 'proposed',
83
+ context TEXT NOT NULL DEFAULT '',
84
+ decision TEXT NOT NULL DEFAULT '',
85
+ consequences TEXT NOT NULL DEFAULT '',
86
+ related_files TEXT NOT NULL DEFAULT '[]',
87
+ tags TEXT NOT NULL DEFAULT '[]',
88
+ author TEXT NOT NULL DEFAULT '',
89
+ superseded_by INTEGER,
90
+ created_at TEXT NOT NULL,
91
+ updated_at TEXT NOT NULL,
92
+ anchor_blob_oids TEXT NOT NULL DEFAULT '[]',
93
+ FOREIGN KEY (superseded_by) REFERENCES adrs(number)
94
+ );
95
+
96
+ CREATE INDEX IF NOT EXISTS idx_adrs_status ON adrs(status);
97
+ """)
98
+ self._migrate_schema()
99
+ conn.commit()
100
+
101
+ def _migrate_schema(self) -> None:
102
+ """Add the ``anchor_blob_oids`` column if missing.
103
+
104
+ Older ADR databases lack this column. Runs at every open and is
105
+ idempotent: a fresh store is a no-op, an old store gets the
106
+ column added in place. Commits the DDL explicitly so callers
107
+ that invoke this helper standalone (not via ``_create_tables``)
108
+ still see the new column persisted on the same connection.
109
+ """
110
+ conn = self._get_conn()
111
+ columns = {
112
+ row[1]
113
+ for row in conn.execute("PRAGMA table_info(adrs)").fetchall()
114
+ }
115
+ if "anchor_blob_oids" not in columns:
116
+ conn.execute(
117
+ "ALTER TABLE adrs ADD COLUMN anchor_blob_oids "
118
+ "TEXT NOT NULL DEFAULT '[]'"
119
+ )
120
+ conn.commit()
121
+
122
+ # ------------------------------------------------------------------
123
+ # Write operations
124
+ # ------------------------------------------------------------------
125
+
126
+ def add(
127
+ self,
128
+ title: str,
129
+ context: str,
130
+ decision: str,
131
+ consequences: str = "",
132
+ related_files: list[str] | None = None,
133
+ tags: list[str] | None = None,
134
+ author: str = "",
135
+ anchor_blob_oids: list[str] | None = None,
136
+ ) -> int:
137
+ """Record a new ADR. Returns the ADR number.
138
+
139
+ Args:
140
+ anchor_blob_oids: Optional content-addressed anchors for the
141
+ files this ADR references (e.g. ``["git:abc123", "git:def456"]``).
142
+ When set, ``orphan_scan`` can check git reachability to
143
+ detect ADRs whose referenced content is no longer in the
144
+ repo. Pass an empty list to fall back to the path-based
145
+ ``related_files`` heuristic.
146
+ """
147
+ conn = self._get_conn()
148
+ now = time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime())
149
+
150
+ cursor = conn.execute(
151
+ """INSERT INTO adrs
152
+ (title, status, context, decision, consequences,
153
+ related_files, tags, author, created_at, updated_at,
154
+ anchor_blob_oids)
155
+ VALUES (?, 'proposed', ?, ?, ?, ?, ?, ?, ?, ?, ?)""",
156
+ (
157
+ title,
158
+ context,
159
+ decision,
160
+ consequences,
161
+ json.dumps(related_files or []),
162
+ json.dumps(tags or []),
163
+ author,
164
+ now,
165
+ now,
166
+ json.dumps(anchor_blob_oids or []),
167
+ ),
168
+ )
169
+ conn.commit()
170
+ return cursor.lastrowid # type: ignore[return-value]
171
+
172
+ def update_status(
173
+ self,
174
+ number: int,
175
+ new_status: str,
176
+ superseded_by: int | None = None,
177
+ ) -> None:
178
+ """Update an ADR's status with transition validation.
179
+
180
+ Raises:
181
+ ValueError: If the status or transition is invalid.
182
+ KeyError: If the ADR number doesn't exist.
183
+ """
184
+ conn = self._get_conn()
185
+ new_status = new_status.lower()
186
+
187
+ if new_status not in VALID_STATUSES:
188
+ raise ValueError(
189
+ f"Invalid status '{new_status}'. Must be one of: {sorted(VALID_STATUSES)}"
190
+ )
191
+
192
+ # Fetch current status
193
+ row = conn.execute(
194
+ "SELECT status FROM adrs WHERE number = ?", (number,)
195
+ ).fetchone()
196
+ if row is None:
197
+ raise KeyError(f"ADR #{number} not found")
198
+
199
+ current_status = row[0]
200
+ allowed = _STATUS_TRANSITIONS.get(current_status, set())
201
+ if new_status not in allowed:
202
+ raise ValueError(
203
+ f"Cannot transition ADR #{number} from '{current_status}' to "
204
+ f"'{new_status}'. Allowed transitions: {sorted(allowed) if allowed else 'none (terminal)'}"
205
+ )
206
+
207
+ if new_status == "superseded" and superseded_by is None:
208
+ raise ValueError(
209
+ "Must provide 'superseded_by' ADR number when setting status to 'superseded'"
210
+ )
211
+
212
+ # Validate superseded_by target exists if provided
213
+ if superseded_by is not None:
214
+ target = conn.execute(
215
+ "SELECT number FROM adrs WHERE number = ?", (superseded_by,)
216
+ ).fetchone()
217
+ if target is None:
218
+ raise ValueError(f"Superseding ADR #{superseded_by} not found")
219
+
220
+ now = time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime())
221
+ conn.execute(
222
+ """UPDATE adrs
223
+ SET status = ?, superseded_by = ?, updated_at = ?
224
+ WHERE number = ?""",
225
+ (new_status, superseded_by, now, number),
226
+ )
227
+ conn.commit()
228
+
229
+ # ------------------------------------------------------------------
230
+ # Read operations
231
+ # ------------------------------------------------------------------
232
+
233
+ def get(self, number: int) -> dict | None:
234
+ """Get a single ADR by number. Returns None if not found."""
235
+ conn = self._get_conn()
236
+ row = conn.execute(
237
+ """SELECT number, title, status, context, decision, consequences,
238
+ related_files, tags, author, superseded_by,
239
+ created_at, updated_at, anchor_blob_oids
240
+ FROM adrs WHERE number = ?""",
241
+ (number,),
242
+ ).fetchone()
243
+ if row is None:
244
+ return None
245
+ return self._row_to_dict(row)
246
+
247
+ def list_all(
248
+ self,
249
+ status: str = "",
250
+ tag: str = "",
251
+ search: str = "",
252
+ ) -> list[dict]:
253
+ """List ADRs with optional filtering.
254
+
255
+ Args:
256
+ status: Filter by status (proposed/accepted/deprecated/superseded).
257
+ tag: Filter by tag (matches any tag in the JSON array).
258
+ search: Search term matched via LIKE on title + context + decision.
259
+ """
260
+ conn = self._get_conn()
261
+
262
+ query = (
263
+ "SELECT number, title, status, context, decision, consequences,"
264
+ " related_files, tags, author, superseded_by,"
265
+ " created_at, updated_at, anchor_blob_oids"
266
+ " FROM adrs WHERE 1=1"
267
+ )
268
+ params: list[str] = []
269
+
270
+ if status:
271
+ query += " AND status = ?"
272
+ params.append(status.lower())
273
+
274
+ if search:
275
+ like_term = f"%{search}%"
276
+ query += " AND (title LIKE ? OR context LIKE ? OR decision LIKE ?)"
277
+ params.extend([like_term, like_term, like_term])
278
+
279
+ query += " ORDER BY number DESC"
280
+
281
+ rows = conn.execute(query, params).fetchall()
282
+ results = [self._row_to_dict(row) for row in rows]
283
+
284
+ # Post-filter by tag (JSON array stored as text)
285
+ if tag:
286
+ tag_lower = tag.lower()
287
+ results = [
288
+ r for r in results
289
+ if any(t.lower() == tag_lower for t in r["tags"])
290
+ ]
291
+
292
+ return results
293
+
294
+ # ------------------------------------------------------------------
295
+ # Helpers
296
+ # ------------------------------------------------------------------
297
+
298
+ def _row_to_dict(self, row: tuple) -> dict:
299
+ """Convert a row tuple to a dict.
300
+
301
+ Tolerant of the row length so older call sites that SELECT 12
302
+ columns (without ``anchor_blob_oids``) still work — the missing
303
+ anchors default to an empty list.
304
+ """
305
+ result = {
306
+ "number": row[0],
307
+ "title": row[1],
308
+ "status": row[2],
309
+ "context": row[3],
310
+ "decision": row[4],
311
+ "consequences": row[5],
312
+ "related_files": json.loads(row[6]) if row[6] else [],
313
+ "tags": json.loads(row[7]) if row[7] else [],
314
+ "author": row[8],
315
+ "superseded_by": row[9],
316
+ "created_at": row[10],
317
+ "updated_at": row[11],
318
+ }
319
+ if len(row) > 12 and row[12]:
320
+ try:
321
+ result["anchor_blob_oids"] = json.loads(row[12])
322
+ except (json.JSONDecodeError, TypeError):
323
+ result["anchor_blob_oids"] = []
324
+ else:
325
+ result["anchor_blob_oids"] = []
326
+ return result
327
+
328
+ # ------------------------------------------------------------------
329
+ # Deletion
330
+ # ------------------------------------------------------------------
331
+
332
+ def clear_all(self, status_filter: str = "") -> int:
333
+ """Hard-delete ADRs.
334
+
335
+ Args:
336
+ status_filter: If non-empty, only delete ADRs whose ``status``
337
+ matches (e.g. ``"superseded"``). Empty wipes every row.
338
+
339
+ Returns the count deleted.
340
+
341
+ Note: The ``superseded_by`` FK means deleting only some ADRs can
342
+ leave dangling references. Callers that care should either pass
343
+ no filter (delete everything) or only delete rows in a terminal
344
+ state like ``superseded``.
345
+ """
346
+ conn = self._get_conn()
347
+ # No threading lock on ADRStore — its existing methods don't use one.
348
+ if status_filter:
349
+ cursor = conn.execute(
350
+ "DELETE FROM adrs WHERE status = ?",
351
+ (status_filter,),
352
+ )
353
+ else:
354
+ cursor = conn.execute("DELETE FROM adrs")
355
+ conn.commit()
356
+ return cursor.rowcount
357
+
358
+ def delete_by_number(self, number: int) -> bool:
359
+ """Hard-delete one ADR by number. Returns True on success."""
360
+ conn = self._get_conn()
361
+ cursor = conn.execute("DELETE FROM adrs WHERE number = ?", (number,))
362
+ conn.commit()
363
+ return cursor.rowcount > 0
364
+
365
+ # ------------------------------------------------------------------
366
+ # Lifecycle
367
+ # ------------------------------------------------------------------
368
+
369
+ def __enter__(self) -> ADRStore:
370
+ return self
371
+
372
+ def __exit__(self, *exc: object) -> None:
373
+ self.close()
374
+
375
+ def close(self) -> None:
376
+ """Close the database connection."""
377
+ if self._conn:
378
+ self._conn.close()
379
+ self._conn = None
380
+
381
+
382
+ # ---------------------------------------------------------------------------
383
+ # Lazy ADR store singleton
384
+ # ---------------------------------------------------------------------------
385
+
386
+ _adr_store_lock = threading.Lock()
387
+ _adr_store: ADRStore | None = None
388
+
389
+
390
+ def _get_adr_store() -> ADRStore:
391
+ """Lazily initialize and return the ADRStore singleton (thread-safe)."""
392
+ from attocode_intel.request_context import current_request, scoped_store
393
+ if current_request.get() is not None:
394
+ return scoped_store("adrs", ADRStore)
395
+ global _adr_store
396
+
397
+ if _adr_store is not None:
398
+ return _adr_store
399
+
400
+ with _adr_store_lock:
401
+ # Double-check after acquiring lock
402
+ if _adr_store is not None:
403
+ return _adr_store
404
+
405
+ store = ADRStore(_get_project_dir())
406
+ _adr_store = store
407
+ return store
408
+
409
+
410
+ # ---------------------------------------------------------------------------
411
+ # Tools
412
+ # ---------------------------------------------------------------------------
413
+
414
+
415
+ @mcp.tool()
416
+ def record_adr(
417
+ title: str,
418
+ context: str,
419
+ decision: str,
420
+ consequences: str = "",
421
+ related_files: list[str] | None = None,
422
+ tags: list[str] | None = None,
423
+ anchor_blob_oids: list[str] | None = None,
424
+ ) -> str:
425
+ """Record an architecture decision.
426
+
427
+ Call this when making significant design choices that affect the codebase
428
+ structure, technology selections, or cross-cutting concerns. ADRs create
429
+ a persistent record of *why* decisions were made.
430
+
431
+ Args:
432
+ title: Short title for the decision (e.g. "Use SQLite for local storage").
433
+ context: What is the issue or problem motivating this decision?
434
+ decision: What is the change being proposed or decided?
435
+ consequences: What are the trade-offs and implications? (optional)
436
+ related_files: List of file paths affected by this decision (optional).
437
+ tags: List of tags for categorization, e.g. ["database", "performance"] (optional).
438
+ anchor_blob_oids: Optional list of content-addressed anchors
439
+ (``"git:<sha>"`` / ``"sha256:<hex>"``) for the files this ADR
440
+ references. If left empty, one is auto-computed per entry in
441
+ ``related_files`` that resolves to a real file.
442
+ """
443
+ remote = _get_remote_service()
444
+ if remote is not None:
445
+ return remote.record_adr(
446
+ title=title,
447
+ context=context,
448
+ decision=decision,
449
+ consequences=consequences,
450
+ related_files=related_files,
451
+ tags=tags,
452
+ )
453
+
454
+ # Auto-compute anchors from related_files when caller didn't supply them.
455
+ resolved_anchors: list[str] = list(anchor_blob_oids or [])
456
+ if not resolved_anchors and related_files:
457
+ try:
458
+ from attocode_intel._internal.integrations.context.blob_oid import (
459
+ BlobOidCache,
460
+ compute_blob_oids_batch,
461
+ )
462
+ project_dir = _get_project_dir()
463
+ cache = BlobOidCache(project_dir=project_dir)
464
+ try:
465
+ oid_map = compute_blob_oids_batch(
466
+ related_files, project_dir, cache=cache,
467
+ )
468
+ for rf in related_files:
469
+ oid = oid_map.get(rf, "")
470
+ if oid and not oid.startswith("sha256:missing:"):
471
+ resolved_anchors.append(oid)
472
+ finally:
473
+ cache.close()
474
+ except Exception as exc:
475
+ logger.debug("record_adr: anchor compute failed: %s", exc)
476
+ resolved_anchors = []
477
+
478
+ store = _get_adr_store()
479
+ try:
480
+ adr_number = store.add(
481
+ title=title,
482
+ context=context,
483
+ decision=decision,
484
+ consequences=consequences,
485
+ related_files=related_files,
486
+ tags=tags,
487
+ anchor_blob_oids=resolved_anchors or None,
488
+ )
489
+ except Exception as e:
490
+ return f"Error recording ADR: {e}"
491
+
492
+ anchor_suffix = (
493
+ f" ({len(resolved_anchors)} anchor(s))" if resolved_anchors else ""
494
+ )
495
+ return (
496
+ f"Recorded ADR #{adr_number}: {title}{anchor_suffix}\n"
497
+ f"Status: proposed\n"
498
+ f"Use `update_adr_status` to accept, deprecate, or supersede this decision."
499
+ )
500
+
501
+
502
+ @mcp.tool()
503
+ def list_adrs(status: str = "", tag: str = "", search: str = "") -> str:
504
+ """List architecture decision records.
505
+
506
+ Browse recorded ADRs with optional filtering by status, tag, or search text.
507
+
508
+ Args:
509
+ status: Filter by status: proposed, accepted, deprecated, superseded.
510
+ tag: Filter by tag (e.g. "database").
511
+ search: Free text search across title, context, and decision.
512
+ """
513
+ remote = _get_remote_service()
514
+ if remote is not None:
515
+ return remote.list_adrs(status=status, tag=tag, search=search)
516
+
517
+ store = _get_adr_store()
518
+ results = store.list_all(status=status, tag=tag, search=search)
519
+
520
+ if not results:
521
+ filters = []
522
+ if status:
523
+ filters.append(f"status={status}")
524
+ if tag:
525
+ filters.append(f"tag={tag}")
526
+ if search:
527
+ filters.append(f"search={search}")
528
+ filter_desc = f" (filters: {', '.join(filters)})" if filters else ""
529
+ return f"No ADRs found{filter_desc}."
530
+
531
+ lines = [f"## Architecture Decision Records ({len(results)} total)\n"]
532
+ lines.append("| # | Title | Status | Tags | Created |")
533
+ lines.append("|---|-------|--------|------|---------|")
534
+ for r in results:
535
+ title = r["title"][:50] + ("..." if len(r["title"]) > 50 else "")
536
+ tags_str = ", ".join(r["tags"]) if r["tags"] else "-"
537
+ created = r["created_at"][:10] # YYYY-MM-DD
538
+ superseded_note = f" (by #{r['superseded_by']})" if r["superseded_by"] else ""
539
+ lines.append(
540
+ f"| {r['number']} | {title} | {r['status']}{superseded_note} "
541
+ f"| {tags_str} | {created} |"
542
+ )
543
+ return "\n".join(lines)
544
+
545
+
546
+ @mcp.tool()
547
+ def get_adr(number: int) -> str:
548
+ """Get a specific ADR by number.
549
+
550
+ Retrieves the full details of an architecture decision record.
551
+
552
+ Args:
553
+ number: The ADR number (auto-assigned when recorded).
554
+ """
555
+ remote = _get_remote_service()
556
+ if remote is not None:
557
+ return remote.get_adr(number)
558
+
559
+ store = _get_adr_store()
560
+ adr = store.get(number)
561
+
562
+ if adr is None:
563
+ return f"ADR #{number} not found."
564
+
565
+ lines = [f"# ADR #{adr['number']}: {adr['title']}\n"]
566
+ lines.append(f"**Status:** {adr['status']}")
567
+ if adr["superseded_by"]:
568
+ lines.append(f"**Superseded by:** ADR #{adr['superseded_by']}")
569
+ if adr["author"]:
570
+ lines.append(f"**Author:** {adr['author']}")
571
+ lines.append(f"**Created:** {adr['created_at']}")
572
+ lines.append(f"**Updated:** {adr['updated_at']}")
573
+
574
+ if adr["tags"]:
575
+ lines.append(f"**Tags:** {', '.join(adr['tags'])}")
576
+
577
+ lines.append(f"\n## Context\n\n{adr['context']}")
578
+ lines.append(f"\n## Decision\n\n{adr['decision']}")
579
+
580
+ if adr["consequences"]:
581
+ lines.append(f"\n## Consequences\n\n{adr['consequences']}")
582
+
583
+ if adr["related_files"]:
584
+ lines.append("\n## Related Files\n")
585
+ for f in adr["related_files"]:
586
+ lines.append(f"- `{f}`")
587
+
588
+ return "\n".join(lines)
589
+
590
+
591
+ @mcp.tool()
592
+ def update_adr_status(
593
+ number: int,
594
+ status: str,
595
+ superseded_by: int | None = None,
596
+ ) -> str:
597
+ """Update an ADR's status.
598
+
599
+ Transitions follow a defined lifecycle:
600
+ proposed -> accepted -> deprecated -> superseded.
601
+
602
+ When superseding, provide the number of the new ADR that replaces this one.
603
+
604
+ Args:
605
+ number: The ADR number to update.
606
+ status: New status: proposed, accepted, deprecated, superseded.
607
+ superseded_by: The ADR number that supersedes this one (required when status is 'superseded').
608
+ """
609
+ remote = _get_remote_service()
610
+ if remote is not None:
611
+ return remote.update_adr_status(
612
+ number=number,
613
+ status=status,
614
+ superseded_by=superseded_by,
615
+ )
616
+
617
+ store = _get_adr_store()
618
+ try:
619
+ store.update_status(number, status, superseded_by=superseded_by)
620
+ except KeyError as e:
621
+ return f"Error: {e}"
622
+ except ValueError as e:
623
+ return f"Error: {e}"
624
+
625
+ msg = f"ADR #{number} status updated to '{status}'."
626
+ if superseded_by is not None:
627
+ msg += f" Superseded by ADR #{superseded_by}."
628
+ return msg