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,127 @@
1
+ """Finding enricher — adds surrounding context for agent reasoning."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import logging
6
+ import os
7
+ import re
8
+ import threading
9
+ from pathlib import Path
10
+
11
+ from attocode_intel.rules.model import EnrichedFinding
12
+
13
+ logger = logging.getLogger(__name__)
14
+
15
+ _CONTEXT_LINES = 10
16
+
17
+ # mtime-aware file cache: path -> (mtime, lines)
18
+ _file_cache: dict[str, tuple[float, tuple[str, ...]]] = {}
19
+ _file_cache_lock = threading.Lock()
20
+ _CACHE_MAX = 256
21
+
22
+
23
+ def _read_file_lines(file_path: str) -> tuple[str, ...]:
24
+ """Read file lines with mtime-aware caching (invalidates on change)."""
25
+ try:
26
+ mtime = os.path.getmtime(file_path)
27
+ except OSError:
28
+ return ()
29
+ with _file_cache_lock:
30
+ cached = _file_cache.get(file_path)
31
+ if cached and cached[0] == mtime:
32
+ return cached[1]
33
+ # I/O outside lock
34
+ try:
35
+ lines = tuple(Path(file_path).read_text(encoding="utf-8", errors="replace").splitlines())
36
+ except OSError:
37
+ return ()
38
+ with _file_cache_lock:
39
+ _file_cache[file_path] = (mtime, lines)
40
+ if len(_file_cache) > _CACHE_MAX:
41
+ oldest = next(iter(_file_cache))
42
+ del _file_cache[oldest]
43
+ return lines
44
+
45
+
46
+ # Function-def patterns for enclosing function detection
47
+ _FUNC_PATTERNS: dict[str, re.Pattern[str]] = {
48
+ "python": re.compile(r"^\s*(?:async\s+)?def\s+(\w+)\s*\("),
49
+ "javascript": re.compile(
50
+ r"(?:(?:async\s+)?function\s+(\w+)|(?:const|let|var)\s+(\w+)\s*=\s*(?:async\s*)?\()"
51
+ ),
52
+ "typescript": re.compile(
53
+ r"(?:(?:async\s+)?function\s+(\w+)|(?:const|let|var)\s+(\w+)\s*=\s*(?:async\s*)?\()"
54
+ ),
55
+ "go": re.compile(r"^func\s+(?:\(\w+\s+\*?\w+\)\s+)?(\w+)\s*\("),
56
+ "rust": re.compile(r"^\s*(?:pub\s+)?(?:async\s+)?fn\s+(\w+)"),
57
+ "java": re.compile(r"^\s*(?:public|private|protected|static|\s)*\s+\w+\s+(\w+)\s*\("),
58
+ "kotlin": re.compile(r"^\s*(?:fun|suspend\s+fun)\s+(\w+)"),
59
+ "ruby": re.compile(r"^\s*def\s+(\w+)"),
60
+ "csharp": re.compile(r"^\s*(?:public|private|protected|static|\s)*\s+\w+\s+(\w+)\s*\("),
61
+ "cpp": re.compile(r"^\s*(?:\w+\s+)*\w+\s+(\w+)\s*\("),
62
+ "c": re.compile(r"^\s*(?:\w+\s+)*\w+\s+(\w+)\s*\("),
63
+ }
64
+
65
+ _EXT_LANG: dict[str, str] = {
66
+ ".py": "python", ".pyi": "python",
67
+ ".js": "javascript", ".jsx": "javascript", ".mjs": "javascript",
68
+ ".ts": "typescript", ".tsx": "typescript",
69
+ ".go": "go", ".rs": "rust", ".java": "java",
70
+ ".kt": "kotlin", ".rb": "ruby", ".cs": "csharp",
71
+ ".cpp": "cpp", ".hpp": "cpp", ".c": "c", ".h": "c",
72
+ }
73
+
74
+
75
+ def _detect_language(file_path: str) -> str:
76
+ ext = os.path.splitext(file_path)[1].lower()
77
+ return _EXT_LANG.get(ext, "")
78
+
79
+
80
+ def _find_enclosing_function(lines: tuple[str, ...], target_line: int, language: str) -> str:
81
+ """Find the function name enclosing target_line (1-indexed)."""
82
+ pat = _FUNC_PATTERNS.get(language)
83
+ if pat is None:
84
+ return ""
85
+ for i in range(min(target_line - 1, len(lines) - 1), -1, -1):
86
+ m = pat.search(lines[i])
87
+ if m:
88
+ for g in m.groups():
89
+ if g:
90
+ return g
91
+ return ""
92
+
93
+
94
+ def enrich_findings(
95
+ findings: list[EnrichedFinding],
96
+ *,
97
+ project_dir: str = "",
98
+ context_lines: int = _CONTEXT_LINES,
99
+ ) -> list[EnrichedFinding]:
100
+ """Enrich findings with surrounding context and enclosing function.
101
+
102
+ Mutates findings in-place and returns them for chaining.
103
+ """
104
+ for finding in findings:
105
+ if project_dir and not os.path.isabs(finding.file):
106
+ abs_path = os.path.join(project_dir, finding.file)
107
+ else:
108
+ abs_path = finding.file
109
+
110
+ lines = _read_file_lines(abs_path)
111
+ if not lines:
112
+ continue
113
+
114
+ idx = finding.line - 1 # 0-indexed
115
+
116
+ # Surrounding context
117
+ start = max(0, idx - context_lines)
118
+ end = min(len(lines), idx + context_lines + 1)
119
+ finding.context_before = list(lines[start:idx])
120
+ finding.context_after = list(lines[idx + 1:end])
121
+
122
+ # Enclosing function
123
+ lang = _detect_language(abs_path)
124
+ if lang and not finding.function_name:
125
+ finding.function_name = _find_enclosing_function(lines, finding.line, lang)
126
+
127
+ return findings
@@ -0,0 +1,452 @@
1
+ """Evolutionary rule optimisation (A8).
2
+
3
+ Genetic-programming-style search over rule space. The evolutionary loop
4
+ takes a seed rule (or population) plus a labelled fixture directory and
5
+ iteratively mutates / crosses over candidates, keeping the elite under a
6
+ fitness function (precision + recall + speed). Designed as the
7
+ optimisation layer that pairs with :mod:`rules.synthesis` (which
8
+ produces seeds) and :mod:`rules.testing` (which provides ground truth
9
+ via inline annotations).
10
+
11
+ This v1 focuses on regex Tier-1 rules. Structural / composite mutation
12
+ operators are deliberately scoped out — they need richer AST surgery.
13
+ """
14
+
15
+ from __future__ import annotations
16
+
17
+ import json
18
+ import logging
19
+ import random
20
+ import re
21
+ import time
22
+ from dataclasses import dataclass, field, replace
23
+ from enum import StrEnum
24
+ from pathlib import Path
25
+
26
+ from attocode_intel.rules.model import (
27
+ RuleSeverity,
28
+ RuleSource,
29
+ UnifiedRule,
30
+ )
31
+ from attocode_intel.rules.testing import RuleTestRunner
32
+
33
+ logger = logging.getLogger(__name__)
34
+
35
+
36
+ # Severity ordering used by SEVERITY_UP / SEVERITY_DOWN mutations.
37
+ _SEVERITY_LADDER: list[RuleSeverity] = [
38
+ RuleSeverity.INFO,
39
+ RuleSeverity.LOW,
40
+ RuleSeverity.MEDIUM,
41
+ RuleSeverity.HIGH,
42
+ RuleSeverity.CRITICAL,
43
+ ]
44
+
45
+
46
+ # ---------------------------------------------------------------------------
47
+ # Fitness
48
+ # ---------------------------------------------------------------------------
49
+
50
+
51
+ @dataclass(slots=True)
52
+ class Fitness:
53
+ """Per-rule fitness signal sourced from RuleTestRunner + timing.
54
+
55
+ Composite is a weighted blend; defaults match the roadmap brief
56
+ ("weighted combination of precision, recall, CASTLE score, and
57
+ execution speed"). We omit CASTLE here since it isn't wired yet —
58
+ speed bonus and recall fill that slot.
59
+ """
60
+
61
+ precision: float
62
+ recall: float
63
+ f1: float
64
+ speed_ms: float
65
+ composite: float
66
+
67
+
68
+ def _compute_composite(
69
+ *,
70
+ precision: float,
71
+ recall: float,
72
+ speed_ms: float,
73
+ speed_budget_ms: float = 50.0,
74
+ ) -> float:
75
+ """Weighted blend: 50% precision + 35% recall + 15% speed bonus.
76
+
77
+ Speed bonus is ``max(0, 1 - speed_ms/budget)`` so anything under the
78
+ budget gets a positive contribution; over-budget rules drop to zero.
79
+ """
80
+ speed_bonus = max(0.0, 1.0 - speed_ms / speed_budget_ms)
81
+ return 0.50 * precision + 0.35 * recall + 0.15 * speed_bonus
82
+
83
+
84
+ def evaluate_rule(
85
+ rule: UnifiedRule,
86
+ *,
87
+ fixtures_dir: str,
88
+ project_dir: str = "",
89
+ ) -> Fitness:
90
+ """Run ``rule`` against the fixture corpus and compute fitness.
91
+
92
+ Annotations on fixture lines are ``# expect: <rule-id>`` (TP target)
93
+ and ``# ok: <rule-id>`` (negative — must NOT fire).
94
+ """
95
+ runner = RuleTestRunner([rule], project_dir=project_dir or fixtures_dir)
96
+ t0 = time.monotonic()
97
+ suite = runner.run_test_suite(fixtures_dir)
98
+ elapsed_ms = (time.monotonic() - t0) * 1000.0
99
+
100
+ tp = sum(
101
+ len([a for a in fr.passed if a.kind == "expect"])
102
+ for fr in suite.file_results
103
+ )
104
+ fn = sum(
105
+ sum(1 for ann, _msg in fr.failed if ann.kind == "expect")
106
+ for fr in suite.file_results
107
+ )
108
+ fp = sum(len(fr.false_positives) for fr in suite.file_results)
109
+
110
+ precision = tp / (tp + fp) if (tp + fp) else 0.0
111
+ recall = tp / (tp + fn) if (tp + fn) else 0.0
112
+ f1 = (
113
+ 2 * precision * recall / (precision + recall)
114
+ if (precision + recall)
115
+ else 0.0
116
+ )
117
+ composite = _compute_composite(
118
+ precision=precision, recall=recall, speed_ms=elapsed_ms,
119
+ )
120
+ return Fitness(
121
+ precision=precision,
122
+ recall=recall,
123
+ f1=round(f1, 4),
124
+ speed_ms=round(elapsed_ms, 3),
125
+ composite=round(composite, 4),
126
+ )
127
+
128
+
129
+ # ---------------------------------------------------------------------------
130
+ # Mutations
131
+ # ---------------------------------------------------------------------------
132
+
133
+
134
+ class MutationOperator(StrEnum):
135
+ """Atomic mutations applied to a rule. None of them invent new
136
+ pattern primitives — they only loosen / tighten what's already
137
+ there. Composing many small mutations across generations is what
138
+ gives the search its reach."""
139
+
140
+ REGEX_WIDEN = "regex_widen"
141
+ REGEX_NARROW = "regex_narrow"
142
+ SEVERITY_UP = "severity_up"
143
+ SEVERITY_DOWN = "severity_down"
144
+ CONFIDENCE_UP = "confidence_up"
145
+ CONFIDENCE_DOWN = "confidence_down"
146
+
147
+
148
+ def _widen_regex(pattern_src: str) -> str | None:
149
+ """Drop one ``\\b`` anchor (the most common form of over-tightening)."""
150
+ if r"\b" in pattern_src:
151
+ return pattern_src.replace(r"\b", "", 1)
152
+ return None
153
+
154
+
155
+ def _narrow_regex(pattern_src: str) -> str | None:
156
+ """Add ``\\b`` boundaries on both sides if the pattern has no anchors."""
157
+ if pattern_src.startswith(r"\b") and pattern_src.endswith(r"\b"):
158
+ return None # already maximally anchored
159
+ out = pattern_src
160
+ if not out.startswith(r"\b"):
161
+ out = r"\b" + out
162
+ if not out.endswith(r"\b"):
163
+ out = out + r"\b"
164
+ return out if out != pattern_src else None
165
+
166
+
167
+ def _shift_severity(rule: UnifiedRule, *, direction: int) -> RuleSeverity | None:
168
+ try:
169
+ idx = _SEVERITY_LADDER.index(rule.severity)
170
+ except ValueError:
171
+ return None
172
+ new_idx = idx + direction
173
+ if 0 <= new_idx < len(_SEVERITY_LADDER) and new_idx != idx:
174
+ return _SEVERITY_LADDER[new_idx]
175
+ return None
176
+
177
+
178
+ def mutate(
179
+ rule: UnifiedRule,
180
+ operator: MutationOperator,
181
+ *,
182
+ rng: random.Random | None = None,
183
+ confidence_step: float = 0.1,
184
+ ) -> UnifiedRule | None:
185
+ """Apply ``operator`` to ``rule``. Returns the mutated copy or
186
+ ``None`` when the mutation can't apply (e.g. widen on an already
187
+ anchorless pattern). The rule's id is preserved — the caller is
188
+ expected to re-id during crossover."""
189
+ rng = rng or random.Random()
190
+
191
+ if operator in (MutationOperator.REGEX_WIDEN, MutationOperator.REGEX_NARROW):
192
+ if rule.pattern is None:
193
+ return None
194
+ new_src: str | None
195
+ if operator == MutationOperator.REGEX_WIDEN:
196
+ new_src = _widen_regex(rule.pattern.pattern)
197
+ else:
198
+ new_src = _narrow_regex(rule.pattern.pattern)
199
+ if new_src is None:
200
+ return None
201
+ try:
202
+ new_re = re.compile(new_src)
203
+ except re.error:
204
+ return None
205
+ return replace(rule, pattern=new_re)
206
+
207
+ if operator == MutationOperator.SEVERITY_UP:
208
+ new_sev = _shift_severity(rule, direction=+1)
209
+ if new_sev is None:
210
+ return None
211
+ return replace(rule, severity=new_sev)
212
+
213
+ if operator == MutationOperator.SEVERITY_DOWN:
214
+ new_sev = _shift_severity(rule, direction=-1)
215
+ if new_sev is None:
216
+ return None
217
+ return replace(rule, severity=new_sev)
218
+
219
+ if operator == MutationOperator.CONFIDENCE_UP:
220
+ new_conf = min(1.0, round(rule.confidence + confidence_step, 4))
221
+ if new_conf == rule.confidence:
222
+ return None
223
+ return replace(rule, confidence=new_conf)
224
+
225
+ if operator == MutationOperator.CONFIDENCE_DOWN:
226
+ new_conf = max(0.0, round(rule.confidence - confidence_step, 4))
227
+ if new_conf == rule.confidence:
228
+ return None
229
+ return replace(rule, confidence=new_conf)
230
+
231
+ return None
232
+
233
+
234
+ # ---------------------------------------------------------------------------
235
+ # Crossover
236
+ # ---------------------------------------------------------------------------
237
+
238
+
239
+ def crossover(
240
+ parent_a: UnifiedRule,
241
+ parent_b: UnifiedRule,
242
+ *,
243
+ rng: random.Random | None = None,
244
+ child_id: str = "",
245
+ ) -> UnifiedRule:
246
+ """Combine two parents into a child rule. We splice scalar fields
247
+ 50/50 and prefer parent A's pattern (the regex is the most
248
+ structural piece — random splicing across regex strings tends to
249
+ produce invalid syntax)."""
250
+ rng = rng or random.Random()
251
+ pick_a = lambda: rng.random() < 0.5 # noqa: E731 — local lambda
252
+ return UnifiedRule(
253
+ id=child_id or f"{parent_a.id}-x-{parent_b.id}",
254
+ name=parent_a.name if pick_a() else parent_b.name,
255
+ description=parent_a.description if pick_a() else parent_b.description,
256
+ severity=parent_a.severity if pick_a() else parent_b.severity,
257
+ category=parent_a.category if pick_a() else parent_b.category,
258
+ languages=list(parent_a.languages), # languages stay with A's pattern
259
+ pattern=parent_a.pattern,
260
+ structural_pattern=parent_a.structural_pattern,
261
+ cwe=parent_a.cwe if pick_a() else parent_b.cwe,
262
+ tags=list(set(parent_a.tags) | set(parent_b.tags)),
263
+ source=RuleSource.USER,
264
+ tier=parent_a.tier,
265
+ confidence=round((parent_a.confidence + parent_b.confidence) / 2, 4),
266
+ recommendation=parent_a.recommendation if pick_a() else parent_b.recommendation,
267
+ explanation=parent_a.explanation if pick_a() else parent_b.explanation,
268
+ pack=parent_a.pack,
269
+ )
270
+
271
+
272
+ # ---------------------------------------------------------------------------
273
+ # Selection + evolution loop
274
+ # ---------------------------------------------------------------------------
275
+
276
+
277
+ def tournament_select(
278
+ ranked: list[tuple[UnifiedRule, Fitness]],
279
+ *,
280
+ k: int = 3,
281
+ rng: random.Random | None = None,
282
+ ) -> UnifiedRule:
283
+ """Pick ``k`` random candidates and return the highest-composite one."""
284
+ rng = rng or random.Random()
285
+ if not ranked:
286
+ raise ValueError("cannot select from empty population")
287
+ sample = rng.sample(ranked, min(k, len(ranked)))
288
+ sample.sort(key=lambda x: -x[1].composite)
289
+ return sample[0][0]
290
+
291
+
292
+ @dataclass(slots=True)
293
+ class EvolutionState:
294
+ """Snapshot of an evolution run after termination."""
295
+
296
+ population: list[UnifiedRule]
297
+ fitnesses: list[Fitness]
298
+ best_rule: UnifiedRule
299
+ best_fitness: Fitness
300
+ generations_run: int
301
+ stopped_reason: str # "max_generations" | "plateau"
302
+ history: list[float] = field(default_factory=list) # best composite per gen
303
+
304
+
305
+ def evolve(
306
+ seeds: list[UnifiedRule],
307
+ *,
308
+ fixtures_dir: str,
309
+ project_dir: str = "",
310
+ max_generations: int = 50,
311
+ population_size: int = 12,
312
+ elite_frac: float = 0.1,
313
+ plateau_gens: int = 5,
314
+ plateau_eps: float = 1e-3,
315
+ rng: random.Random | None = None,
316
+ log_path: str = "",
317
+ ) -> EvolutionState:
318
+ """Run the evolutionary loop. Caller supplies one or more seed rules;
319
+ we pad/trim to ``population_size`` via mutation. ``fixtures_dir`` is
320
+ a directory of inline-annotated source files (same format consumed
321
+ by the ``test_rules`` MCP tool)."""
322
+ if not seeds:
323
+ raise ValueError("evolve() requires at least one seed rule")
324
+ if max_generations < 1:
325
+ raise ValueError(
326
+ f"evolve() requires max_generations >= 1, got {max_generations}"
327
+ )
328
+
329
+ rng = rng or random.Random(42)
330
+
331
+ # Dedupe seeds by id — duplicates would just burn fitness evaluations
332
+ # on identical individuals before any mutation can diverge them.
333
+ deduped: dict[str, UnifiedRule] = {}
334
+ for s in seeds:
335
+ deduped.setdefault(s.id, s)
336
+ seeds = list(deduped.values())
337
+
338
+ # Initialise population: seed + mutated copies until we hit the size.
339
+ population: list[UnifiedRule] = list(seeds)[:population_size]
340
+ operators = list(MutationOperator)
341
+ while len(population) < population_size:
342
+ parent = rng.choice(seeds)
343
+ op = rng.choice(operators)
344
+ child = mutate(parent, op, rng=rng)
345
+ population.append(child or parent)
346
+
347
+ elite_count = max(1, int(population_size * elite_frac))
348
+ history: list[float] = []
349
+ audit_log: list[dict] = []
350
+
351
+ best_rule = population[0]
352
+ best_fitness = Fitness(0.0, 0.0, 0.0, 0.0, 0.0)
353
+ stopped_reason = "max_generations"
354
+ generations_run = 0
355
+ # Snapshot the last generation's evaluated population so the final
356
+ # ``EvolutionState`` carries fitnesses that actually correspond to
357
+ # the rules we report — and so we don't pay for re-evaluation after
358
+ # the loop ends. Updated each generation.
359
+ last_evaluated_rules: list[UnifiedRule] = list(population)
360
+ last_evaluated_fits: list[Fitness] = []
361
+
362
+ def _safe_evaluate(rule: UnifiedRule) -> Fitness:
363
+ """Defensive wrapper — RuleTestRunner can raise on a malformed
364
+ fixture or a tree-sitter crash. Surface the failure as a
365
+ zero-fitness candidate so the loop continues and the audit log
366
+ captures the broken individual instead of crashing the run."""
367
+ try:
368
+ return evaluate_rule(
369
+ rule, fixtures_dir=fixtures_dir, project_dir=project_dir,
370
+ )
371
+ except Exception as exc: # noqa: BLE001 — we want all errors caught
372
+ logger.warning(
373
+ "evaluate_rule failed for %s: %s", rule.id, exc,
374
+ )
375
+ return Fitness(0.0, 0.0, 0.0, 0.0, 0.0)
376
+
377
+ for gen in range(max_generations):
378
+ generations_run = gen + 1
379
+ fitnesses = [_safe_evaluate(r) for r in population]
380
+ ranked = sorted(
381
+ zip(population, fitnesses, strict=True),
382
+ key=lambda x: -x[1].composite,
383
+ )
384
+ last_evaluated_rules = [r for r, _ in ranked]
385
+ last_evaluated_fits = [f for _, f in ranked]
386
+ gen_best_rule, gen_best_fit = ranked[0]
387
+ history.append(gen_best_fit.composite)
388
+
389
+ audit_log.append({
390
+ "generation": gen,
391
+ "best_id": gen_best_rule.id,
392
+ "best_pattern": (
393
+ gen_best_rule.pattern.pattern if gen_best_rule.pattern else ""
394
+ ),
395
+ "best_composite": gen_best_fit.composite,
396
+ "best_precision": gen_best_fit.precision,
397
+ "best_recall": gen_best_fit.recall,
398
+ "population_mean_composite": round(
399
+ sum(f.composite for f in fitnesses) / len(fitnesses), 4,
400
+ ),
401
+ })
402
+
403
+ if gen_best_fit.composite > best_fitness.composite:
404
+ best_rule = gen_best_rule
405
+ best_fitness = gen_best_fit
406
+
407
+ # Plateau check: best composite hasn't budged across
408
+ # ``plateau_gens`` consecutive generations. Require ≥2 entries
409
+ # in history so a single-point window can't trigger the stop.
410
+ if len(history) >= max(2, plateau_gens):
411
+ recent = history[-plateau_gens:]
412
+ if max(recent) - min(recent) < plateau_eps:
413
+ stopped_reason = "plateau"
414
+ break
415
+
416
+ # Build next generation: elite + tournament-selected children.
417
+ # Crossover children get unique IDs (parent_a-x-parent_b-gN) so
418
+ # the population keeps diverse identifiers across generations
419
+ # — without this every child collapses onto parent_a.id.
420
+ next_pop: list[UnifiedRule] = [r for r, _ in ranked[:elite_count]]
421
+ child_idx = 0
422
+ while len(next_pop) < population_size:
423
+ parent_a = tournament_select(ranked, k=3, rng=rng)
424
+ if rng.random() < 0.5 and len(ranked) > 1:
425
+ parent_b = tournament_select(ranked, k=3, rng=rng)
426
+ child_id = f"{parent_a.id}-x-{parent_b.id}-g{gen}-{child_idx}"
427
+ child = crossover(parent_a, parent_b, rng=rng, child_id=child_id)
428
+ else:
429
+ op = rng.choice(operators)
430
+ child = mutate(parent_a, op, rng=rng) or parent_a
431
+ next_pop.append(child)
432
+ child_idx += 1
433
+ population = next_pop
434
+
435
+ if log_path:
436
+ try:
437
+ Path(log_path).parent.mkdir(parents=True, exist_ok=True)
438
+ with Path(log_path).open("a", encoding="utf-8") as fh:
439
+ for entry in audit_log:
440
+ fh.write(json.dumps(entry) + "\n")
441
+ except OSError as exc:
442
+ logger.warning("Failed to persist evolution log to %s: %s", log_path, exc)
443
+
444
+ return EvolutionState(
445
+ population=last_evaluated_rules,
446
+ fitnesses=last_evaluated_fits,
447
+ best_rule=best_rule,
448
+ best_fitness=best_fitness,
449
+ generations_run=generations_run,
450
+ stopped_reason=stopped_reason,
451
+ history=history,
452
+ )