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,486 @@
1
+ """History tools for the code-intel MCP server.
2
+
3
+ Tools: code_evolution, recent_changes, change_coupling, churn_hotspots,
4
+ merge_risk.
5
+
6
+ Local mode uses git subprocess calls directly.
7
+ Service mode queries the Commit + CommitFileStat DB tables.
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ import logging
13
+ import os
14
+ import subprocess
15
+ import threading
16
+ from collections import defaultdict
17
+ from typing import TYPE_CHECKING
18
+
19
+ from attocode_intel._shared import (
20
+ _get_project_dir,
21
+ _get_remote_service,
22
+ _get_service,
23
+ mcp,
24
+ )
25
+
26
+ if TYPE_CHECKING:
27
+ from attocode_intel._internal.integrations.context.temporal_coupling import TemporalCouplingAnalyzer
28
+
29
+ logger = logging.getLogger(__name__)
30
+
31
+
32
+ # ---------------------------------------------------------------------------
33
+ # Helpers — git subprocess for local mode
34
+ # ---------------------------------------------------------------------------
35
+
36
+
37
+ def _run_git(args: list[str], cwd: str) -> str:
38
+ """Run a git command and return stdout, or empty string on error."""
39
+ try:
40
+ result = subprocess.run(
41
+ ["git"] + args,
42
+ cwd=cwd,
43
+ capture_output=True,
44
+ text=True,
45
+ timeout=30,
46
+ )
47
+ if result.returncode != 0:
48
+ logger.debug("git %s failed: %s", " ".join(args), result.stderr.strip())
49
+ return ""
50
+ return result.stdout
51
+ except (FileNotFoundError, subprocess.TimeoutExpired) as exc:
52
+ logger.debug("git command error: %s", exc)
53
+ return ""
54
+
55
+
56
+ def _parse_evolution_output(raw: str) -> list[dict]:
57
+ """Parse git log output with --numstat --format=%H|%an|%ae|%aI|%s.
58
+
59
+ Returns a list of commit dicts with per-file stats.
60
+ """
61
+ if not raw.strip():
62
+ return []
63
+
64
+ commits: list[dict] = []
65
+ current: dict | None = None
66
+
67
+ for line in raw.splitlines():
68
+ line = line.strip()
69
+ if not line:
70
+ continue
71
+
72
+ # Header line: sha|author_name|author_email|date|subject
73
+ if "|" in line and line.count("|") >= 4:
74
+ parts = line.split("|", 4)
75
+ if len(parts) == 5 and len(parts[0]) >= 7:
76
+ if current is not None:
77
+ commits.append(current)
78
+ current = {
79
+ "sha": parts[0],
80
+ "author": parts[1],
81
+ "email": parts[2],
82
+ "date": parts[3],
83
+ "subject": parts[4],
84
+ "files": [],
85
+ }
86
+ continue
87
+
88
+ # Numstat line: added\tremoved\tpath
89
+ if current is not None and "\t" in line:
90
+ parts = line.split("\t")
91
+ if len(parts) >= 3:
92
+ try:
93
+ added = int(parts[0]) if parts[0] != "-" else 0
94
+ removed = int(parts[1]) if parts[1] != "-" else 0
95
+ except ValueError:
96
+ continue
97
+ file_path = parts[2]
98
+ # Handle renames: old => new
99
+ if " => " in file_path:
100
+ file_path = file_path.split(" => ")[-1].rstrip("}")
101
+ if "{" in file_path:
102
+ # Handle partial rename like src/{old => new}/file.py
103
+ file_path = file_path.replace("{", "").replace("}", "")
104
+ current["files"].append({
105
+ "path": file_path,
106
+ "added": added,
107
+ "removed": removed,
108
+ })
109
+
110
+ if current is not None:
111
+ commits.append(current)
112
+
113
+ return commits
114
+
115
+
116
+ def _filter_by_symbol(commits: list[dict], symbol: str) -> list[dict]:
117
+ """Filter commits to only those whose subject or file paths mention the symbol."""
118
+ if not symbol:
119
+ return commits
120
+ symbol_lower = symbol.lower()
121
+ filtered = []
122
+ for commit in commits:
123
+ if symbol_lower in commit["subject"].lower():
124
+ filtered.append(commit)
125
+ continue
126
+ # Check if any changed file might contain the symbol
127
+ # (heuristic: symbol name appears in the path)
128
+ for f in commit["files"]:
129
+ if symbol_lower in f["path"].lower():
130
+ filtered.append(commit)
131
+ break
132
+ return filtered
133
+
134
+
135
+ # ---------------------------------------------------------------------------
136
+ # MCP Tools
137
+ # ---------------------------------------------------------------------------
138
+
139
+
140
+ @mcp.tool()
141
+ def code_evolution(
142
+ path: str,
143
+ symbol: str = "",
144
+ since: str = "",
145
+ max_results: int = 20,
146
+ ) -> str:
147
+ """Show change history for a file or symbol.
148
+
149
+ Traces how a file (or a specific symbol within it) has evolved over
150
+ time, showing commits with line-level change statistics. Useful for
151
+ understanding why code looks the way it does and who changed it.
152
+
153
+ Args:
154
+ path: File path (relative to project root or absolute).
155
+ symbol: Optional symbol name to filter commits mentioning it.
156
+ since: Optional date filter (e.g. "2024-01-01", "3 months ago").
157
+ max_results: Maximum number of commits to return (default 20).
158
+ """
159
+ remote = _get_remote_service()
160
+ if remote is not None:
161
+ return remote.code_evolution(path=path, symbol=symbol, since=since, max_results=max_results)
162
+
163
+ project_dir = _get_project_dir()
164
+
165
+ # Resolve path relative to project
166
+ if os.path.isabs(path):
167
+ try:
168
+ rel_path = os.path.relpath(path, project_dir)
169
+ except ValueError:
170
+ rel_path = path
171
+ else:
172
+ rel_path = path
173
+
174
+ # Build git log command
175
+ git_args = [
176
+ "log",
177
+ "--follow",
178
+ "--numstat",
179
+ "--format=%H|%an|%ae|%aI|%s",
180
+ f"-{max_results * 2}", # fetch extra to account for filtering
181
+ ]
182
+ if since:
183
+ git_args.append(f"--since={since}")
184
+ git_args.extend(["--", rel_path])
185
+
186
+ raw = _run_git(git_args, project_dir)
187
+ if not raw.strip():
188
+ return f"No commit history found for '{rel_path}'."
189
+
190
+ commits = _parse_evolution_output(raw)
191
+
192
+ # Filter by symbol if requested
193
+ if symbol:
194
+ commits = _filter_by_symbol(commits, symbol)
195
+
196
+ commits = commits[:max_results]
197
+
198
+ if not commits:
199
+ msg = f"No commits found for '{rel_path}'"
200
+ if symbol:
201
+ msg += f" mentioning symbol '{symbol}'"
202
+ return msg + "."
203
+
204
+ # Format output
205
+ lines = [f"Change history for {rel_path}"]
206
+ if symbol:
207
+ lines[0] += f" (symbol: {symbol})"
208
+ lines.append(f"({len(commits)} commits shown)\n")
209
+
210
+ for i, commit in enumerate(commits, 1):
211
+ sha_short = commit["sha"][:8]
212
+ date_str = commit["date"][:10] if len(commit["date"]) >= 10 else commit["date"]
213
+ lines.append(
214
+ f" {i:>3}. {sha_short} {date_str} {commit['author']}"
215
+ )
216
+ lines.append(f" {commit['subject']}")
217
+
218
+ # Show file stats
219
+ total_added = 0
220
+ total_removed = 0
221
+ for f in commit["files"]:
222
+ total_added += f["added"]
223
+ total_removed += f["removed"]
224
+
225
+ if len(commit["files"]) == 1:
226
+ f = commit["files"][0]
227
+ lines.append(f" +{f['added']} -{f['removed']} {f['path']}")
228
+ elif commit["files"]:
229
+ lines.append(f" +{total_added} -{total_removed} across {len(commit['files'])} files")
230
+ # Show the target file specifically if among many
231
+ for f in commit["files"]:
232
+ if rel_path in f["path"] or f["path"] in rel_path:
233
+ lines.append(f" +{f['added']} -{f['removed']} {f['path']}")
234
+ break
235
+
236
+ lines.append("")
237
+
238
+ # Summary
239
+ total_commits = len(commits)
240
+ all_authors = sorted(set(c["author"] for c in commits))
241
+ lines.append(f"Summary: {total_commits} commits by {len(all_authors)} author(s)")
242
+ if len(all_authors) <= 5:
243
+ lines.append(f"Authors: {', '.join(all_authors)}")
244
+ else:
245
+ lines.append(f"Top authors: {', '.join(all_authors[:5])}, +{len(all_authors) - 5} more")
246
+
247
+ return "\n".join(lines)
248
+
249
+
250
+ @mcp.tool()
251
+ def recent_changes(
252
+ days: int = 7,
253
+ path: str = "",
254
+ top_n: int = 20,
255
+ ) -> str:
256
+ """Show recently modified files and change frequency.
257
+
258
+ Aggregates recent git activity to identify files with the most
259
+ changes — useful for finding active development areas, understanding
260
+ project velocity, and spotting potential merge conflict hotspots.
261
+
262
+ Args:
263
+ days: Look back this many days (default 7).
264
+ path: Optional path prefix to filter (e.g. "src/api/").
265
+ top_n: Number of top files to show (default 20).
266
+ """
267
+ remote = _get_remote_service()
268
+ if remote is not None:
269
+ return remote.recent_changes(days=days, path=path, top_n=top_n)
270
+
271
+ project_dir = _get_project_dir()
272
+
273
+ git_args = [
274
+ "log",
275
+ f"--since={days} days ago",
276
+ "--numstat",
277
+ "--format=%H|%aI|%s",
278
+ ]
279
+ if path:
280
+ git_args.extend(["--", path])
281
+
282
+ raw = _run_git(git_args, project_dir)
283
+ if not raw.strip():
284
+ scope = f" under '{path}'" if path else ""
285
+ return f"No changes found in the last {days} day(s){scope}."
286
+
287
+ # Parse and aggregate
288
+ file_stats: dict[str, dict] = defaultdict(
289
+ lambda: {"commits": 0, "added": 0, "removed": 0, "authors": set(), "last_date": ""}
290
+ )
291
+ commit_count = 0
292
+ all_authors: set[str] = set()
293
+ current_date = ""
294
+
295
+ for line in raw.splitlines():
296
+ line = line.strip()
297
+ if not line:
298
+ continue
299
+
300
+ # Header line
301
+ if "|" in line and line.count("|") >= 2:
302
+ parts = line.split("|", 2)
303
+ if len(parts) == 3 and len(parts[0]) >= 7:
304
+ commit_count += 1
305
+ current_date = parts[1][:10] if len(parts[1]) >= 10 else parts[1]
306
+ # Extract author from a separate git log if needed
307
+ continue
308
+
309
+ # Numstat line
310
+ if "\t" in line:
311
+ parts = line.split("\t")
312
+ if len(parts) >= 3:
313
+ try:
314
+ added = int(parts[0]) if parts[0] != "-" else 0
315
+ removed = int(parts[1]) if parts[1] != "-" else 0
316
+ except ValueError:
317
+ continue
318
+ file_path = parts[2]
319
+ if " => " in file_path:
320
+ file_path = file_path.split(" => ")[-1].rstrip("}")
321
+ if "{" in file_path:
322
+ file_path = file_path.replace("{", "").replace("}", "")
323
+
324
+ stats = file_stats[file_path]
325
+ stats["commits"] += 1
326
+ stats["added"] += added
327
+ stats["removed"] += removed
328
+ if current_date and (not stats["last_date"] or current_date > stats["last_date"]):
329
+ stats["last_date"] = current_date
330
+
331
+ if not file_stats:
332
+ scope = f" under '{path}'" if path else ""
333
+ return f"No file changes found in the last {days} day(s){scope}."
334
+
335
+ # Also get author info with a separate command
336
+ author_args = [
337
+ "log",
338
+ f"--since={days} days ago",
339
+ "--format=%an",
340
+ ]
341
+ if path:
342
+ author_args.extend(["--", path])
343
+ author_raw = _run_git(author_args, project_dir)
344
+ if author_raw:
345
+ all_authors = set(a.strip() for a in author_raw.splitlines() if a.strip())
346
+
347
+ # Sort by commit frequency, then by total churn
348
+ ranked = sorted(
349
+ file_stats.items(),
350
+ key=lambda kv: (kv[1]["commits"], kv[1]["added"] + kv[1]["removed"]),
351
+ reverse=True,
352
+ )[:top_n]
353
+
354
+ # Format output
355
+ scope_label = f" under '{path}'" if path else ""
356
+ lines = [
357
+ f"Recent changes (last {days} day(s){scope_label})",
358
+ f"{commit_count} commits, {len(file_stats)} files modified, "
359
+ f"{len(all_authors)} contributor(s)\n",
360
+ ]
361
+
362
+ # Table header
363
+ lines.append(f" {'#':>3} {'Commits':>7} {'Added':>6} {'Removed':>7} {'Last':>10} File")
364
+ lines.append(f" {'':->3} {'':->7} {'':->6} {'':->7} {'':->10} {'':->40}")
365
+
366
+ for i, (fpath, stats) in enumerate(ranked, 1):
367
+ last_date = stats["last_date"] if stats["last_date"] else "?"
368
+ lines.append(
369
+ f" {i:>3} {stats['commits']:>7} "
370
+ f"+{stats['added']:>5} -{stats['removed']:>6} "
371
+ f"{last_date:>10} {fpath}"
372
+ )
373
+
374
+ # Churn summary
375
+ total_added = sum(s["added"] for s in file_stats.values())
376
+ total_removed = sum(s["removed"] for s in file_stats.values())
377
+ lines.append(f"\nTotal churn: +{total_added} -{total_removed} lines")
378
+
379
+ if all_authors:
380
+ sorted_authors = sorted(all_authors)
381
+ if len(sorted_authors) <= 5:
382
+ lines.append(f"Contributors: {', '.join(sorted_authors)}")
383
+ else:
384
+ lines.append(
385
+ f"Contributors: {', '.join(sorted_authors[:5])}, "
386
+ f"+{len(sorted_authors) - 5} more"
387
+ )
388
+
389
+ return "\n".join(lines)
390
+
391
+
392
+ # ---------------------------------------------------------------------------
393
+ # Temporal coupling helpers
394
+ # ---------------------------------------------------------------------------
395
+
396
+ _temporal_analyzer: TemporalCouplingAnalyzer | None = None
397
+ _temporal_lock = threading.Lock()
398
+
399
+
400
+ def _get_temporal_analyzer() -> TemporalCouplingAnalyzer:
401
+ """Lazily initialize the TemporalCouplingAnalyzer singleton."""
402
+ from attocode_intel.request_context import current_request, scoped_store
403
+ if current_request.get() is not None:
404
+ from attocode_intel._internal.integrations.context.temporal_coupling import TemporalCouplingAnalyzer
405
+ return scoped_store("temporal", TemporalCouplingAnalyzer)
406
+ global _temporal_analyzer
407
+ if _temporal_analyzer is None:
408
+ with _temporal_lock:
409
+ if _temporal_analyzer is None:
410
+ from attocode_intel._internal.integrations.context.temporal_coupling import (
411
+ TemporalCouplingAnalyzer,
412
+ )
413
+
414
+ project_dir = _get_project_dir()
415
+ _temporal_analyzer = TemporalCouplingAnalyzer(project_dir=project_dir)
416
+ return _temporal_analyzer
417
+
418
+
419
+ @mcp.tool()
420
+ def change_coupling(
421
+ file: str,
422
+ days: int = 90,
423
+ min_coupling: float = 0.3,
424
+ top_k: int = 20,
425
+ ) -> str:
426
+ """Find files that frequently change alongside a given file.
427
+
428
+ Uses git history to build a co-change frequency matrix. Files that
429
+ appear in the same commit as the target file are scored by how often
430
+ they co-occur relative to their individual change frequency.
431
+
432
+ A coupling score of 1.0 means the files ALWAYS change together.
433
+ A score of 0.5 means they change together half the time.
434
+
435
+ Args:
436
+ file: Target file path (relative to project root).
437
+ days: Time window in days (default 90).
438
+ min_coupling: Minimum coupling score to include (0.0-1.0, default 0.3).
439
+ top_k: Maximum number of results (default 20).
440
+ """
441
+ return _get_service().change_coupling(
442
+ file=file,
443
+ days=days,
444
+ min_coupling=min_coupling,
445
+ top_k=top_k,
446
+ )
447
+
448
+
449
+ @mcp.tool()
450
+ def churn_hotspots(
451
+ days: int = 90,
452
+ top_n: int = 20,
453
+ ) -> str:
454
+ """Rank files by change frequency and churn intensity.
455
+
456
+ Combines commit frequency with line-level churn to identify the
457
+ most actively modified files. High-churn files are candidates for
458
+ refactoring, increased test coverage, or architectural attention.
459
+
460
+ Args:
461
+ days: Time window in days (default 90).
462
+ top_n: Number of top files to return (default 20).
463
+ """
464
+ return _get_service().churn_hotspots(days=days, top_n=top_n)
465
+
466
+
467
+ @mcp.tool()
468
+ def merge_risk(
469
+ files: list[str],
470
+ days: int = 90,
471
+ ) -> str:
472
+ """Predict which other files will likely need changes.
473
+
474
+ Given a set of files you're about to modify, predicts which other
475
+ files are at risk of needing changes too — based on both temporal
476
+ coupling (git co-change history) and structural coupling (import
477
+ dependencies).
478
+
479
+ Each prediction is annotated with its source ("temporal",
480
+ "structural", or "both") and a confidence score.
481
+
482
+ Args:
483
+ files: List of file paths being modified.
484
+ days: Time window for temporal coupling (default 90).
485
+ """
486
+ return _get_service().merge_risk(files=files, days=days)
@@ -0,0 +1,222 @@
1
+ """Learning / memory tools for the code-intel MCP server.
2
+
3
+ Tools: recall, record_learning, learning_feedback, list_learnings.
4
+ """
5
+
6
+ from __future__ import annotations
7
+
8
+ import logging
9
+ import threading
10
+ from typing import TYPE_CHECKING
11
+
12
+ from attocode_intel._shared import (
13
+ _get_project_dir,
14
+ _get_remote_service,
15
+ mcp,
16
+ )
17
+
18
+ if TYPE_CHECKING:
19
+ from attocode_intel._internal.integrations.context.memory_store import MemoryStore
20
+
21
+ logger = logging.getLogger(__name__)
22
+
23
+ # ---------------------------------------------------------------------------
24
+ # Lazy memory store singleton
25
+ # ---------------------------------------------------------------------------
26
+
27
+ _memory_store_lock = threading.Lock()
28
+
29
+
30
+ def _get_memory_store() -> MemoryStore:
31
+ """Lazily initialize and return the MemoryStore singleton (thread-safe).
32
+
33
+ Uses ``server._memory_store`` as the authoritative location so tests
34
+ can reset it by setting ``srv._memory_store = None``.
35
+ """
36
+ from attocode_intel.request_context import current_request
37
+ if (request := current_request.get()) is not None:
38
+ return request.service._get_memory_store()
39
+ import attocode_intel.server as _srv
40
+ from attocode_intel._internal.integrations.context.memory_store import MemoryStore
41
+
42
+ existing = _srv._memory_store
43
+ if isinstance(existing, MemoryStore):
44
+ return existing
45
+ # server._memory_store is None — create a fresh instance
46
+ with _memory_store_lock:
47
+ # Double-check after acquiring lock
48
+ existing = _srv._memory_store
49
+ if isinstance(existing, MemoryStore):
50
+ return existing
51
+
52
+ store = MemoryStore(_get_project_dir())
53
+ _srv._memory_store = store
54
+ return store
55
+
56
+
57
+ # ---------------------------------------------------------------------------
58
+ # Tools
59
+ # ---------------------------------------------------------------------------
60
+
61
+
62
+ @mcp.tool()
63
+ def recall(query: str, scope: str = "", max_results: int = 10) -> str:
64
+ """Retrieve relevant project learnings (patterns, conventions, gotchas).
65
+
66
+ Call this at the start of a task or when working in unfamiliar code.
67
+ Scope narrows results to a directory subtree (e.g. 'src/api/').
68
+
69
+ Args:
70
+ query: Natural language description of what you're working on.
71
+ scope: Optional directory scope to filter learnings.
72
+ max_results: Maximum number of learnings to return.
73
+ """
74
+ remote = _get_remote_service()
75
+ if remote is not None:
76
+ return remote.recall(query, scope=scope, max_results=max_results)
77
+ store = _get_memory_store()
78
+ results = store.recall(query, scope=scope, max_results=max_results)
79
+ if not results:
80
+ return "No relevant learnings found for this project."
81
+
82
+ lines = [f"## Project Learnings ({len(results)} relevant)\n"]
83
+ for r in results:
84
+ lines.append(f"- **[{r['type']}]** (confidence: {r['confidence']:.0%}, id: {r['id']})")
85
+ lines.append(f" {r['description']}")
86
+ if r["details"]:
87
+ lines.append(f" _{r['details']}_")
88
+
89
+ # Increment apply_count for returned learnings (best-effort)
90
+ for r in results:
91
+ try:
92
+ store.record_applied(r["id"])
93
+ except Exception:
94
+ logger.debug("Failed to record_applied for learning %d", r["id"])
95
+
96
+ return "\n".join(lines)
97
+
98
+
99
+ @mcp.tool()
100
+ def record_learning(
101
+ type: str, # noqa: A002
102
+ description: str,
103
+ details: str = "",
104
+ scope: str = "",
105
+ confidence: float = 0.7,
106
+ anchor_blob_oid: str = "",
107
+ ) -> str:
108
+ """Record a project learning for future recall.
109
+
110
+ Call this when you discover something important about the codebase:
111
+ patterns, conventions, gotchas, workarounds, or anti-patterns.
112
+
113
+ Args:
114
+ type: One of 'pattern', 'antipattern', 'workaround', 'convention', 'gotcha'.
115
+ description: Short description (1-2 sentences).
116
+ details: Optional longer explanation or example.
117
+ scope: Optional directory scope (e.g. 'src/api/').
118
+ confidence: Initial confidence 0.0-1.0 (default 0.7).
119
+ anchor_blob_oid: Optional ``"git:<sha>"`` / ``"sha256:<hex>"`` anchor
120
+ for the file this learning is about. If left empty and ``scope``
121
+ looks like a concrete file path, it's auto-computed so the
122
+ learning survives renames and ``orphan_scan`` can verify
123
+ reachability via git.
124
+ """
125
+ remote = _get_remote_service()
126
+ if remote is not None:
127
+ return remote.record_learning(
128
+ type=type, description=description, details=details,
129
+ scope=scope, confidence=confidence,
130
+ )
131
+ store = _get_memory_store()
132
+
133
+ # Auto-compute the anchor when the caller didn't supply one and the
134
+ # scope looks like a file path we can hash.
135
+ resolved_anchor = anchor_blob_oid
136
+ if not resolved_anchor and scope:
137
+ import os as _os
138
+ project_dir = _get_project_dir()
139
+ candidate = scope if _os.path.isabs(scope) else _os.path.join(project_dir, scope)
140
+ if _os.path.isfile(candidate):
141
+ try:
142
+ from attocode_intel._internal.integrations.context.blob_oid import (
143
+ BlobOidCache,
144
+ compute_blob_oid,
145
+ )
146
+ cache = BlobOidCache(project_dir=project_dir)
147
+ try:
148
+ resolved_anchor = compute_blob_oid(
149
+ candidate, project_dir, cache=cache,
150
+ )
151
+ finally:
152
+ cache.close()
153
+ except Exception as exc: # noqa: BLE001
154
+ logger.debug("record_learning: anchor compute failed: %s", exc)
155
+ resolved_anchor = ""
156
+
157
+ try:
158
+ learning_id = store.add(
159
+ type=type, description=description,
160
+ details=details, scope=scope, confidence=confidence,
161
+ anchor_blob_oid=resolved_anchor,
162
+ )
163
+ except ValueError as e:
164
+ return f"Error: {e}"
165
+ suffix = f" (anchor={resolved_anchor[:24]}…)" if resolved_anchor else ""
166
+ return f"Recorded learning #{learning_id}: [{type}] {description}{suffix}"
167
+
168
+
169
+ @mcp.tool()
170
+ def learning_feedback(learning_id: int, helpful: bool) -> str:
171
+ """Mark a previously recalled learning as helpful or unhelpful.
172
+
173
+ Call this after a recalled learning influenced your work, to improve
174
+ future recall quality. Unhelpful learnings are eventually auto-archived.
175
+
176
+ Args:
177
+ learning_id: The ID from a previous recall result.
178
+ helpful: Whether the learning was actually useful.
179
+ """
180
+ remote = _get_remote_service()
181
+ if remote is not None:
182
+ return remote.learning_feedback(learning_id, helpful)
183
+ store = _get_memory_store()
184
+ store.record_feedback(learning_id, helpful)
185
+ action = "boosted" if helpful else "reduced"
186
+ return f"Feedback recorded — confidence {action} for learning #{learning_id}."
187
+
188
+
189
+ @mcp.tool()
190
+ def list_learnings(
191
+ status: str = "active",
192
+ type: str = "", # noqa: A002
193
+ scope: str = "",
194
+ ) -> str:
195
+ """List all stored project learnings.
196
+
197
+ Args:
198
+ status: Filter by status: 'active' or 'archived'.
199
+ type: Optional filter by type (pattern/antipattern/workaround/convention/gotcha).
200
+ scope: Optional filter by directory scope.
201
+ """
202
+ remote = _get_remote_service()
203
+ if remote is not None:
204
+ return remote.list_learnings(status=status, type=type, scope=scope)
205
+ store = _get_memory_store()
206
+ results = store.list_all(status=status, type=type or None)
207
+ if scope:
208
+ results = [r for r in results if r["scope"].startswith(scope) or r["scope"] == ""]
209
+ if not results:
210
+ return "No learnings found matching the filters."
211
+
212
+ lines = [f"## Learnings ({len(results)} total)\n"]
213
+ lines.append("| ID | Type | Description | Confidence | Applied | Scope |")
214
+ lines.append("|---|---|---|---|---|---|")
215
+ for r in results:
216
+ desc = r["description"][:60] + ("..." if len(r["description"]) > 60 else "")
217
+ lines.append(
218
+ f"| {r['id']} | {r['type']} | {desc} "
219
+ f"| {r['confidence']:.0%} | {r['apply_count']}x "
220
+ f"| {r['scope'] or '(global)'} |"
221
+ )
222
+ return "\n".join(lines)